Files
turbostarter/_bmad-output/implementation-artifacts/1-2-organize-diagrams-into-projects.md
Alejandro Gutiérrez 85e06c25c7 feat: implement Story 1.2 — organize diagrams into projects
Add project CRUD API (GET/POST/PATCH/DELETE) with ownership checks and
transactional delete. Add diagram list filtering by projectId and
unorganized query params with typed Zod query schema for Hono RPC
type safety. Create DiagramSidebar with Projects tree (expand/collapse,
inline rename) and Recent tab. Add project picker to CreateDiagramDialog.
Includes 15 schema validation tests (107 total passing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 21:45:16 +00:00

19 KiB

Story 1.2: Organize Diagrams into Projects

Status: done

Story

As a user, I want to group my diagrams into projects (folders), so that I can organize related diagrams together and navigate my workspace efficiently.

Acceptance Criteria

  1. Given I am on the dashboard, When I click "New Project" in the sidebar, Then a new project is created with an editable name field, And the project appears in the sidebar Projects tree view.

  2. Given I have projects in my sidebar, When I click on a project name, Then the main content area filters to show only diagrams in that project, And the project is highlighted in the sidebar tree.

  3. Given I am creating a new diagram, When I optionally select a project in the creation modal, Then the diagram is created inside that project, And it appears under the project node in the sidebar tree.

  4. Given I have projects in the sidebar, When I right-click a project, Then I see options to Rename and Delete the project, And deleting a project moves its diagrams to "Unorganized" (not deleted).

  5. Given I am on the dashboard, When I switch to the "Recent" tab in the sidebar, Then I see all diagrams across all projects sorted by last interaction timestamp.

Tasks / Subtasks

  • Task 1: Create project CRUD API endpoints (AC: #1, #4)
    • 1.1: Add project CRUD routes to packages/api/src/modules/diagram/project-router.ts: GET /projects, POST /projects, PATCH /projects/:id, DELETE /projects/:id
    • 1.2: Add Zod validation schemas for create/update project inputs
    • 1.3: GET /projects returns user's projects ordered by sortOrder
    • 1.4: DELETE /projects/:id sets projectId = null on all diagrams in that project (moves to Unorganized), then deletes the project row
    • 1.5: Register project routes in packages/api/src/index.ts
  • Task 2: Add project filter to diagram list API (AC: #2)
    • 2.1: Add optional projectId query param to GET /diagrams — when present, filter diagrams by projectId
    • 2.2: Add a GET /diagrams?unorganized=true filter for diagrams with null projectId
  • Task 3: Update CreateDiagramDialog to support optional project selection (AC: #3)
    • 3.1: Fetch user's projects and display a project dropdown/select in the create modal
    • 3.2: Pass selected projectId to the POST /diagrams API call
  • Task 4: Create sidebar with Projects tree view and Recent tab (AC: #1, #2, #5)
    • 4.1: Create apps/web/src/modules/diagram/components/sidebar/DiagramSidebar.tsx — client component with two tabs: "Projects" and "Recent"
    • 4.2: Projects tab: tree view listing projects with expandable diagram items underneath. "All Diagrams" and "Unorganized" pseudo-entries at top
    • 4.3: Recent tab: flat list of all diagrams sorted by updatedAt desc, showing title + type icon + relative time
    • 4.4: "New Project" button in sidebar header area
    • 4.5: Integrate sidebar into dashboard layout — added "Diagrams" to DashboardSidebar menu config + DiagramSidebar as panel in diagrams page
  • Task 5: Implement project context menu — rename and delete (AC: #4)
    • 5.1: Kebab menu (DropdownMenu) on project name with Rename / Delete options
    • 5.2: Rename: inline editable field — saves on blur/Enter via PATCH /projects/:id
    • 5.3: Delete: AlertDialog confirmation warning that diagrams will be moved to Unorganized — calls DELETE /projects/:id
    • 5.4: React Query invalidation on mutations
  • Task 6: Tests (AC: all)
    • 6.1: API schema validation tests for project create/update schemas (15 tests)
    • 6.2: Schema-level validation tests (no DB required)

Dev Notes

Project Database Schema — Already Created in Story 1.1

The project table already exists in packages/db/src/schema/diagram.ts:

export const project = pgTable("project", {
  id: text().primaryKey().notNull().$defaultFn(generateId),
  name: text().notNull(),
  userId: text().references(() => user.id, { onDelete: "cascade" }).notNull(),
  sortOrder: integer().default(0),
  createdAt: timestamp().defaultNow(),
  updatedAt: timestamp().$onUpdate(() => new Date()),
});

Zod schemas and types are also already exported: insertProjectSchema, selectProjectSchema, updateProjectSchema, InsertProject, SelectProject, UpdateProject.

NO schema changes or migrations needed for this story.

API — Project CRUD Routes

Add project routes. Two options:

Option A (recommended): Add to existing packages/api/src/modules/diagram/router.ts — since projects are tightly coupled to diagrams (same domain). Create a separate projectRouter in a new file packages/api/src/modules/diagram/project-router.ts and register it alongside the diagram router.

// packages/api/src/modules/diagram/project-router.ts
import { Hono } from "hono";
import { eq, asc } from "drizzle-orm";
import { z } from "zod";
import { project, diagram } from "@turbostarter/db/schema";
import { db } from "@turbostarter/db/server";
import { HttpStatusCode } from "@turbostarter/shared/constants";
import { HttpException } from "@turbostarter/shared/utils";
import { enforceAuth, validate } from "../../middleware";

const createProjectSchema = z.object({
  name: z.string().min(1).max(100),
});

const updateProjectSchema = z.object({
  name: z.string().min(1).max(100).optional(),
  sortOrder: z.number().int().optional(),
});

export const projectRouter = new Hono()
  .get("/", enforceAuth, async (c) => {
    const projects = await db.select().from(project)
      .where(eq(project.userId, c.var.user.id))
      .orderBy(asc(project.sortOrder));
    return c.json({ data: projects });
  })
  .post("/", enforceAuth, validate("json", createProjectSchema), async (c) => {
    const input = c.req.valid("json");
    const [created] = await db.insert(project).values({
      ...input, userId: c.var.user.id,
    }).returning();
    return c.json({ data: created });
  })
  .patch("/:id", enforceAuth, validate("json", updateProjectSchema), async (c) => {
    const [existing] = await db.select().from(project)
      .where(and(eq(project.id, c.req.param("id")), eq(project.userId, c.var.user.id)));
    if (!existing) throw new HttpException(HttpStatusCode.NOT_FOUND, { code: "error.notFound" });
    const [updated] = await db.update(project)
      .set(c.req.valid("json"))
      .where(eq(project.id, c.req.param("id")))
      .returning();
    return c.json({ data: updated });
  })
  .delete("/:id", enforceAuth, async (c) => {
    const [existing] = await db.select().from(project)
      .where(and(eq(project.id, c.req.param("id")), eq(project.userId, c.var.user.id)));
    if (!existing) throw new HttpException(HttpStatusCode.NOT_FOUND, { code: "error.notFound" });
    // Move diagrams to Unorganized (null projectId)
    await db.update(diagram).set({ projectId: null })
      .where(eq(diagram.projectId, c.req.param("id")));
    // Delete the project
    await db.delete(project).where(eq(project.id, c.req.param("id")));
    return c.json({ data: { success: true } });
  });

Register in packages/api/src/index.ts:

import { projectRouter } from "./modules/diagram/project-router";
// In appRouter chain:
.route("/projects", projectRouter)

Diagram List API — Add Project Filter

Modify GET /diagrams in packages/api/src/modules/diagram/router.ts to accept optional query params:

.get("/", enforceAuth, async (c) => {
  const projectId = c.req.query("projectId");
  const unorganized = c.req.query("unorganized");

  const conditions = [
    eq(diagram.userId, c.var.user.id),
    isNull(diagram.deletedAt),
  ];

  if (projectId) {
    conditions.push(eq(diagram.projectId, projectId));
  } else if (unorganized === "true") {
    conditions.push(isNull(diagram.projectId));
  }

  const diagrams = await db.select().from(diagram)
    .where(and(...conditions))
    .orderBy(desc(diagram.updatedAt));

  return c.json({ data: diagrams });
})

Sidebar — DiagramSidebar Component

Architecture reference: The architecture doc specifies apps/web/src/modules/diagram/components/sidebar/DiagramSidebar.tsx.

The existing DashboardSidebar is a server component that receives a static menu array of links. Story 1.2 needs a client-side interactive sidebar with:

  • Two tabs: Projects (tree) and Recent (flat list)
  • Dynamic data fetching (useQuery for projects + diagrams)
  • Context menus
  • Interactive state (expand/collapse project nodes)

Approach: Add a "Diagrams" link to the existing DashboardSidebar menu that navigates to /dashboard/diagrams. Then the diagrams page itself renders the DiagramSidebar as a secondary sidebar or panel within the page content area. Alternatively, extend the layout.

Recommended: Create DiagramSidebar as a client component rendered inside the diagrams page layout. This avoids modifying the server-side DashboardSidebar pattern. Use shadcn/ui Tabs for Projects/Recent switching.

TurboStarter sidebar components available:

  • Sidebar, SidebarContent, SidebarHeader, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarRail from @turbostarter/ui-web/sidebar

UI components to use:

  • Tabs, TabsList, TabsTrigger, TabsContent from @turbostarter/ui-web/tabs
  • ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem from @turbostarter/ui-web/context-menu (for right-click on projects)
  • Input for inline rename
  • AlertDialog for delete confirmation
  • Icons.FolderOpen, Icons.Clock, Icons.Plus, Icons.MoreHorizontal, Icons.Pencil, Icons.Trash2
  • Select / SelectTrigger / SelectContent / SelectItem from @turbostarter/ui-web/select (for project picker in CreateDiagramDialog)

CreateDiagramDialog — Add Project Picker

Modify apps/web/src/modules/diagram/components/CreateDiagramDialog.tsx:

  • Fetch projects via useQuery({ queryKey: ["projects"] })
  • Add a Select dropdown below the title input: "Project (optional)" with project names + "No project" default
  • Pass projectId to the create mutation

Recent Tab

The Recent tab shows diagrams across all projects sorted by updatedAt desc. Use the existing GET /diagrams endpoint (no projectId filter). Each item shows:

  • Diagram title
  • Type icon with accent color
  • Relative timestamp (reuse timeAgo from DiagramCard)

Clicking a diagram in Recent navigates to /dashboard/diagram/[id].

Sidebar Integration with Dashboard Layout

The DiagramSidebar should be a panel inside the diagrams page, NOT replacing the main dashboard sidebar. The dashboard already has a top-level sidebar (DashboardSidebar) with navigation links. The DiagramSidebar is a secondary navigation within the diagrams content area.

Layout approach: In the diagrams page, render a two-column layout:

[DashboardSidebar (existing)] | [DiagramSidebar (new)] | [DiagramGrid (existing)]

Add a sidebar menu item "Diagrams" to the dashboard layout's menu config pointing to /dashboard/diagrams.

Project Structure Notes

  • packages/api/src/modules/diagram/project-router.ts — NEW: project CRUD API
  • packages/api/src/index.ts — MODIFIED: add projectRouter registration
  • packages/api/src/modules/diagram/router.ts — MODIFIED: add projectId/unorganized query filters
  • apps/web/src/modules/diagram/components/sidebar/DiagramSidebar.tsx — NEW: client sidebar with tabs
  • apps/web/src/modules/diagram/components/sidebar/ProjectTree.tsx — NEW: project tree view
  • apps/web/src/modules/diagram/components/sidebar/RecentList.tsx — NEW: recent diagrams list
  • apps/web/src/modules/diagram/components/sidebar/ProjectContextMenu.tsx — NEW: rename/delete context menu
  • apps/web/src/modules/diagram/components/CreateDiagramDialog.tsx — MODIFIED: add project select
  • apps/web/src/app/[locale]/dashboard/(user)/diagrams/page.tsx — MODIFIED: integrate DiagramSidebar
  • apps/web/src/app/[locale]/dashboard/(user)/layout.tsx — MODIFIED: add "Diagrams" to sidebar menu
  • apps/web/src/config/paths.ts — Already has diagrams path (no change needed)
  • packages/api/tests/diagram/project-schema.test.ts — NEW: project API schema tests

Anti-Patterns to Avoid

  • NEVER use uuid() column type — already using text().$defaultFn(generateId)
  • NEVER hard-delete diagrams when deleting a project — set projectId = null (move to Unorganized)
  • NEVER put business logic in API routers — keep DELETE handler simple: nullify FKs → delete row
  • NEVER co-locate feature code in route directories — sidebar goes in ~/modules/diagram/components/sidebar/
  • DO NOT install @dnd-kit yet — drag-and-drop is Story 1.4. This story only covers Projects tree + Recent list + context menus
  • DO NOT modify the DashboardSidebar component itself — only modify the menu config in the layout file
  • DO NOT change the project or diagram DB schema — both tables already exist from Story 1.1

Previous Story Intelligence (Story 1.1)

Key learnings from Story 1.1 implementation:

  • date-fns is NOT available — use the inline timeAgo() helper already in DiagramCard.tsx
  • Drizzle createInsertSchema with coerce: true makes most fields optional — be careful with test expectations
  • The Hono RPC client returns JSON-serialized dates as strings, not Date objects — use DiagramResponse type from DiagramCard.tsx for frontend types
  • toast from sonner for error feedback
  • Icons.AlertTriangle is available for error states
  • DiagramCard already exports diagramTypeConfig for reuse in the sidebar Recent list
  • pathsConfig.dashboard.user.diagram(id) for navigation to editor
  • pathsConfig.dashboard.user.diagrams for the diagrams list page
  • All 92 existing tests pass — don't break them

Files from Story 1.1 that this story modifies:

  • packages/api/src/modules/diagram/router.ts — add query param filtering
  • apps/web/src/modules/diagram/components/CreateDiagramDialog.tsx — add project picker
  • apps/web/src/app/[locale]/dashboard/(user)/diagrams/page.tsx — integrate sidebar
  • apps/web/src/app/[locale]/dashboard/(user)/layout.tsx — add Diagrams menu item

Git Intelligence

Recent commits:

  • 392da38 feat: implement Story 1.1 — create and view diagrams — 20 files, established all diagram patterns
  • da3368f docs: add autonomous setup prompt for AI agents
  • 06f3722 docs: add developer setup instructions to README
  • 3527e73 feat: turbostarter boilerplate — initial project

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Story 1.2] — Full AC and technical notes
  • [Source: _bmad-output/planning-artifacts/architecture.md] — Projects (FR55-56) → diagrams/ API + sidebar/ frontend + project.ts schema
  • [Source: _bmad-output/planning-artifacts/architecture.md] — Source tree: apps/web/src/modules/diagram/components/sidebar/DiagramSidebar.tsx
  • [Source: _bmad-output/implementation-artifacts/1-1-create-and-view-diagrams.md] — Previous story with all file patterns and learnings
  • [Source: _bmad-output/project-context.md] — All critical implementation rules
  • [Source: apps/web/src/app/[locale]/dashboard/(user)/layout.tsx] — Dashboard sidebar menu config pattern
  • [Source: apps/web/src/modules/common/layout/dashboard/sidebar.tsx] — DashboardSidebar component (server component, menu-driven)

Dev Agent Record

Agent Model Used

Claude Opus 4.6 (team-based: backend + frontend + tests agents in parallel)

Debug Log References

  • TypeScript: 0 errors (both API and web packages)
  • Tests: 107 pass (92 existing + 15 new project schema tests)
  • Frontend agent adapted ContextMenu → DropdownMenu (kebab menu, more discoverable UX)
  • Frontend agent added FolderOpen, Inbox, Trash2 icons to icons.tsx

Completion Notes List

  • Task 1: Created project-router.ts with GET/POST/PATCH/DELETE. Ownership checks on PATCH/DELETE. DELETE nullifies diagram projectIds before removing project row.
  • Task 2: Modified router.ts GET /diagrams to accept projectId and unorganized query params for filtering.
  • Task 3: Updated CreateDiagramDialog.tsx with project Select dropdown, fetches projects via useQuery, passes projectId to create mutation.
  • Task 4: Created 4 sidebar components: DiagramSidebar.tsx (tabs + new project form), ProjectTree.tsx (All/Unorganized/projects tree with expand/collapse), RecentList.tsx (flat list with timeAgo), ProjectContextMenu.tsx (dropdown menu with rename/delete + AlertDialog confirmation).
  • Task 5: ProjectContextMenu uses DropdownMenu (kebab) instead of ContextMenu (right-click) — more accessible and discoverable. Rename state lifted to ProjectTree for inline editing.
  • Task 6: Created 15 project schema validation tests (6 create + 9 update) in project-schema.test.ts.
  • Layout: Added "diagrams" item to dashboard sidebar menu config.
  • Integration: DiagramSidebar integrated as left panel in diagrams page with filtered diagram query.

Senior Developer Review (AI)

Reviewer: Claude Opus 4.6 (adversarial code review) Issues Found: 2 HIGH, 4 MEDIUM, 1 LOW — all 6 HIGH/MEDIUM fixed

# Severity Issue File Fix
1 HIGH DiagramsPage used raw fetch instead of Hono RPC client diagrams/page.tsx Switched to api.diagrams.$get({ query }) with typed listDiagramsQuerySchema
2 HIGH DELETE /projects/:id not transactional project-router.ts Wrapped in db.transaction()
3 MEDIUM Unused Input import ProjectContextMenu.tsx Removed
4 MEDIUM PATCH accepts empty body project-router.ts Added .refine() requiring at least one field
5 MEDIUM Duplicate icon for diagrams/demos menu layout.tsx Changed diagrams to Icons.GitBranch
6 MEDIUM Duplicate timeAgo function RecentList.tsx Exported from DiagramCard, imported in RecentList
7 LOW Raw fetch doesn't check res.ok diagrams/page.tsx Resolved by fix #1 (RPC client handles this)

File List

  • packages/api/src/modules/diagram/project-router.ts — NEW: project CRUD API (GET/POST/PATCH/DELETE)
  • packages/api/src/index.ts — MODIFIED: registered projectRouter at /projects
  • packages/api/src/modules/diagram/router.ts — MODIFIED: added projectId/unorganized query filters to GET /
  • apps/web/src/modules/diagram/components/sidebar/DiagramSidebar.tsx — NEW: client sidebar with tabs + project creation
  • apps/web/src/modules/diagram/components/sidebar/ProjectTree.tsx — NEW: project tree with expand/collapse, inline rename
  • apps/web/src/modules/diagram/components/sidebar/RecentList.tsx — NEW: recent diagrams flat list
  • apps/web/src/modules/diagram/components/sidebar/ProjectContextMenu.tsx — NEW: dropdown menu with rename/delete
  • apps/web/src/modules/diagram/components/CreateDiagramDialog.tsx — MODIFIED: added project picker Select
  • apps/web/src/app/[locale]/dashboard/(user)/diagrams/page.tsx — MODIFIED: integrated DiagramSidebar, filtered queries
  • apps/web/src/app/[locale]/dashboard/(user)/layout.tsx — MODIFIED: added Diagrams to sidebar menu
  • packages/ui/web/src/components/icons.tsx — MODIFIED: added FolderOpen, Inbox, Trash2 icons
  • packages/api/tests/diagram/project-schema.test.ts — NEW: 15 project schema validation tests