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