Files
turbostarter/_bmad-output/implementation-artifacts/1-4-recent-view-and-drag-and-drop-organization.md
Alejandro Gutiérrez 098f4968be feat: implement Story 1.4 — recent view and drag-and-drop organization
Add sortOrder column to diagrams, extend PATCH endpoint with projectId
and sortOrder fields, add POST /diagrams/reorder bulk endpoint with
ownership verification and duplicate ID validation. Enhance RecentList
with lastAiMessage preview subtitle. Implement @dnd-kit drag-and-drop
in ProjectTree sidebar with cross-project moves, intra-project reorder,
optimistic updates, drag overlay, drop indicators, and keyboard/pointer
sensor support. Context-aware GET ordering: sortOrder for project views,
updatedAt for recent/all views. 141 tests pass.

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

16 KiB

Story 1.4: Recent View & Drag-and-Drop Organization

Status: done

Story

As a user, I want to see my recent diagrams with conversation context and reorder my workspace via drag-and-drop, so that I can quickly resume work and keep my projects organized.

Acceptance Criteria

  1. Given I have interacted with multiple diagrams, When I open the "Recent" tab in the sidebar, Then diagrams are sorted by last interaction (most recent first), And each item shows the diagram title, type icon, and the last AI chat message as a preview subtitle (truncated to ~60 chars).

  2. Given I am in the Projects tree view, When I drag a diagram from one project to another (or from unorganized to a project), Then the diagram's projectId is updated via API call, And the sidebar tree reflects the new organization immediately with optimistic UI update.

  3. Given I am viewing diagrams within a project in the sidebar, When I drag to reorder diagrams, Then the sortOrder is updated for affected diagrams, And the new order persists on page reload.

  4. Given I have no diagrams yet, When I view the dashboard, Then I see an empty state with a call-to-action: "Create your first diagram" button and a brief description of what domaingraph does.

Tasks / Subtasks

  • Task 1: Add sortOrder column to diagram table and extend PATCH endpoint (AC: #2, #3)
    • 1.1: Add sortOrder: integer().default(0) column to diagram table in packages/db/src/schema/diagram.ts
    • 1.2: Generate and apply Drizzle migration
    • 1.3: Extend updateDiagramBodySchema to accept projectId (string, optional) and sortOrder (integer, optional) in addition to title
    • 1.4: Add bulk reorder endpoint: POST /diagrams/reorder accepting { items: [{ id, sortOrder }] } for batch sortOrder updates
  • Task 2: Enhance RecentList with AI preview subtitle (AC: #1)
    • 2.1: Modify RecentList.tsx to display lastAiMessage truncated to ~60 chars as a subtitle below each diagram title
    • 2.2: Add muted text styling for the subtitle line
    • 2.3: Ensure the list is sorted by updatedAt descending (already the case from the API)
  • Task 3: Install @dnd-kit and implement drag-and-drop in sidebar (AC: #2, #3)
    • 3.1: Install @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities in apps/web
    • 3.2: Wrap ProjectTree content with DndContext + SortableContext providers
    • 3.3: Make diagram items in expanded projects sortable with useSortable hook
    • 3.4: Implement cross-project drag: detect when a diagram is dropped on a different project node → call PATCH to update projectId with optimistic React Query update
    • 3.5: Implement intra-project reorder: detect sort order changes within a project → call POST /diagrams/reorder with new sort orders
    • 3.6: Add drag overlay component showing the diagram title being dragged
    • 3.7: Add visual drop indicators (highlight target project on dragover)
  • Task 4: Update DiagramGrid to show diagrams in sortOrder (AC: #3)
    • 4.1: Update the GET /diagrams endpoint to order by sortOrder ASC, updatedAt DESC (sortOrder as primary, updatedAt as tiebreaker)
    • 4.2: Ensure DiagramGrid respects the API ordering
  • Task 5: Verify empty state (AC: #4)
    • 5.1: Confirm EmptyDiagrams component renders when no diagrams exist (already implemented — verify integration)
  • Task 6: Tests (AC: all)
    • 6.1: API tests for extended updateDiagramBodySchema (projectId and sortOrder fields)
    • 6.2: API tests for POST /diagrams/reorder endpoint (batch update, ownership check)
    • 6.3: Verify existing 120 tests still pass (139 tests pass)

Dev Notes

DB Schema Change — Add sortOrder to Diagrams

Modify packages/db/src/schema/diagram.ts:

export const diagram = pgTable("diagram", {
  id: text().primaryKey().notNull().$defaultFn(generateId),
  title: text().notNull(),
  type: diagramTypeEnum().notNull(),
  graphData: jsonb().$type<object>().default({}),
  userId: text()
    .references(() => user.id, { onDelete: "cascade" })
    .notNull(),
  projectId: text(),
  sortOrder: integer().default(0),   // ← NEW
  lastAiMessage: text(),
  deletedAt: timestamp(),
  createdAt: timestamp().defaultNow(),
  updatedAt: timestamp().$onUpdate(() => new Date()),
});

After adding the column:

pnpm --filter @turbostarter/db generate
pnpm --filter @turbostarter/db migrate

API — Extend PATCH and Add Reorder Endpoint

Extend updateDiagramBodySchema in packages/api/src/modules/diagram/router.ts:

export const updateDiagramBodySchema = z
  .object({
    title: z.string().min(1).max(255).optional(),
    projectId: z.string().nullable().optional(),
    sortOrder: z.number().int().min(0).optional(),
  })
  .refine(
    (data) => data.title !== undefined || data.projectId !== undefined || data.sortOrder !== undefined,
    { message: "At least one field must be provided" },
  );

Add bulk reorder endpoint:

const reorderDiagramsSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    sortOrder: z.number().int().min(0),
  })).min(1).max(100),
});

.post(
  "/reorder",
  enforceAuth,
  validate("json", reorderDiagramsSchema),
  async (c) => {
    const { items } = c.req.valid("json");

    // Verify all diagrams belong to the user
    const diagramIds = items.map((i) => i.id);
    const owned = await db.select({ id: diagram.id })
      .from(diagram)
      .where(and(
        inArray(diagram.id, diagramIds),
        eq(diagram.userId, c.var.user.id),
        isNull(diagram.deletedAt),
      ));

    if (owned.length !== diagramIds.length) {
      throw new HttpException(HttpStatusCode.FORBIDDEN, {
        code: "error.forbidden",
        message: "One or more diagrams not found or not owned",
      });
    }

    // Batch update sortOrders
    await db.transaction(async (tx) => {
      for (const item of items) {
        await tx.update(diagram)
          .set({ sortOrder: item.sortOrder })
          .where(eq(diagram.id, item.id));
      }
    });

    return c.json({ data: { success: true } });
  },
)

IMPORTANT: The /reorder route must be registered BEFORE /:id routes to avoid route matching conflicts. Place it right after the GET / and POST / routes.

Update GET / ordering:

.orderBy(asc(diagram.sortOrder), desc(diagram.updatedAt))

Import asc from drizzle-orm alongside existing imports.

Frontend — RecentList Enhancement

Modify apps/web/src/modules/diagram/components/sidebar/RecentList.tsx:

Add lastAiMessage preview below each diagram title:

<button key={d.id} className="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent/50 cursor-pointer"
  onClick={() => router.push(pathsConfig.dashboard.user.diagram(d.id))}
>
  <TypeIcon className={`h-4 w-4 shrink-0 mt-0.5 ${config.color}`} />
  <div className="flex-1 min-w-0">
    <span className="block truncate text-left">{d.title}</span>
    {d.lastAiMessage && (
      <span className="block truncate text-xs text-muted-foreground">
        {d.lastAiMessage.length > 60 ? `${d.lastAiMessage.slice(0, 60)}...` : d.lastAiMessage}
      </span>
    )}
  </div>
  <span className="shrink-0 text-xs text-muted-foreground mt-0.5">
    {d.updatedAt ? timeAgo(new Date(d.updatedAt)) : "just now"}
  </span>
</button>

Key changes: items-centeritems-start, add subtitle div, add mt-0.5 alignment.

Frontend — @dnd-kit Drag-and-Drop in ProjectTree

Install dependencies:

pnpm --filter web add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

Implementation approach for ProjectTree.tsx:

  1. Wrap with DndContext at the ProjectTree level
  2. Each expanded project's diagram list gets a SortableContext
  3. Each diagram item uses useSortable hook for dragging
  4. Projects themselves act as droppable areas using useDroppable
  5. onDragEnd handler:
    • If dropped on a different project → PATCH diagram's projectId
    • If reordered within same project → POST to /diagrams/reorder

DnD data structure:

type DragData = {
  type: "diagram";
  diagram: DiagramResponse;
  sourceProjectId: string | null;
};

type DropData = {
  type: "project";
  projectId: string | null;
};

Optimistic updates:

  • Use queryClient.setQueryData(["diagrams"], ...) for immediate UI feedback
  • Invalidate on mutation success/error
  • On error, revert to previous data via onMutate → return previous data → onError → rollback

Visual feedback:

  • Drag overlay: semi-transparent card showing diagram title + type icon
  • Drop target: project row highlights with ring-2 ring-primary/50 when a diagram is dragged over it
  • Sortable items: show insertion line indicator between items

Frontend — Empty State (Already Implemented)

EmptyDiagrams.tsx already exists with:

  • Large icon (Icons.LayoutDashboard)
  • "No diagrams yet" heading
  • Description text about AI-assisted diagramming
  • "Create your first diagram" CTA button via CreateDiagramDialog

Verify: DiagramGrid.tsx already checks diagrams.length === 0 and renders <EmptyDiagrams />. No changes needed.

Project Structure Notes

  • packages/db/src/schema/diagram.ts — MODIFIED: add sortOrder column
  • packages/api/src/modules/diagram/router.ts — MODIFIED: extend PATCH schema, add POST /reorder, change ordering
  • apps/web/src/modules/diagram/components/sidebar/RecentList.tsx — MODIFIED: add AI preview subtitle
  • apps/web/src/modules/diagram/components/sidebar/ProjectTree.tsx — MODIFIED: add @dnd-kit DnD
  • apps/web/src/modules/diagram/components/sidebar/DiagramSidebar.tsx — MINOR: may need DndContext wrapper if shared across tabs
  • packages/api/tests/diagram/ — NEW: reorder endpoint tests
  • Alignment: All paths match architecture doc patterns. Uses existing module structure.

Anti-Patterns to Avoid

  • NEVER use HTML5 native DnD — use @dnd-kit for accessible, cross-browser drag-and-drop with proper keyboard support
  • NEVER update sortOrder client-side only — always persist via API to ensure reload consistency
  • NEVER allow non-owners to reorder — the /reorder endpoint must verify ownership of ALL diagrams in the batch
  • NEVER replace entire diagram list on reorder — use granular sortOrder updates in a transaction
  • DO NOT break existing 120 tests — run full test suite after changes
  • DO NOT add DnD to the DiagramGrid (main content area) — DnD is only in the sidebar ProjectTree for v1
  • DO NOT truncate lastAiMessage in the API — truncation happens client-side for display flexibility
  • DO NOT import inArray without adding it — add inArray to the drizzle-orm import in router.ts

Previous Story Intelligence (Story 1.3)

Key learnings to carry forward:

  • DropdownMenu preferred over ContextMenu for action menus (more discoverable, accessible)
  • Hono RPC client pattern: api.diagrams[":id"].$patch({ param: { id }, json: { ... } })
  • toast() from sonner for user feedback on mutations
  • React Query invalidation: queryClient.invalidateQueries({ queryKey: ["diagrams"] })
  • PATCH validation: use .refine() to require at least one field
  • getOwnedDiagram() helper extracts ownership check (reuse for reorder endpoint's bulk check)
  • 120 existing tests pass — don't break them
  • DiagramCard has inline rename, kebab menu, delete dialog — don't re-implement
  • DiagramResponse type exported from DiagramCard.tsx — reuse for DnD data typing
  • diagramTypeConfig and timeAgo exported from DiagramCard.tsx — already reused in RecentList and ProjectTree

Git Intelligence

Recent commits (all Story 1.x):

  • e9cd685 feat: implement Story 1.3 — diagram access control and management — PATCH/DELETE endpoints, inline rename, 403 handling
  • 85e06c2 feat: implement Story 1.2 — organize diagrams into projects — project CRUD, sidebar, ProjectTree
  • 392da38 feat: implement Story 1.1 — create and view diagrams — diagram schema, CRUD, dashboard

Established patterns:

  • Commit message: feat: implement Story X.Y — description
  • Co-located tests: packages/api/tests/diagram/
  • Zod schemas exported from router files
  • Feature code in apps/web/src/modules/diagram/

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Story 1.4] — Full AC and technical notes
  • [Source: _bmad-output/planning-artifacts/architecture.md#Implementation Patterns] — Naming, structure, enforcement rules
  • [Source: _bmad-output/planning-artifacts/architecture.md#Enforcement Guidelines] — 7 mandatory rules for AI agents
  • [Source: _bmad-output/implementation-artifacts/1-3-diagram-access-control-and-management.md] — Previous story learnings
  • [Source: _bmad-output/project-context.md] — 62 critical implementation rules
  • [Source: packages/db/src/schema/diagram.ts] — Current diagram schema (lastAiMessage already exists, sortOrder to be added)
  • [Source: packages/api/src/modules/diagram/router.ts] — Current diagram router (GET, POST, PATCH, DELETE)
  • [Source: apps/web/src/modules/diagram/components/sidebar/ProjectTree.tsx] — Current sidebar tree (no DnD yet)
  • [Source: apps/web/src/modules/diagram/components/sidebar/RecentList.tsx] — Current recent list (no AI preview yet)
  • [Source: apps/web/src/modules/diagram/components/EmptyDiagrams.tsx] — Empty state (already implemented)

Dev Agent Record

Agent Model Used

Claude Opus 4.6

Debug Log References

  • Migration generated but not applied (no local DB running) — migration file at packages/db/migrations/0001_fuzzy_gorilla_man.sql
  • Pre-existing test diagram-db-schema.test.ts had to be updated to include sortOrder in complete record test

Completion Notes List

  • All 6 tasks complete, all 141 tests pass, both API and web typechecks pass
  • @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities installed in apps/web
  • DndContext wraps entire ProjectTree; projects are droppable zones, diagram items are sortable
  • Cross-project drag uses PATCH projectId; intra-project reorder uses POST /diagrams/reorder
  • Optimistic updates via queryClient.setQueryData with invalidation on error
  • /reorder route placed before /:id to avoid route conflicts
  • DB migration pending application (DB not running locally)

Senior Developer Review (AI)

Reviewer: Claude Opus 4.6 | Date: 2026-02-23

Issues Found: 1 High, 4 Medium, 3 Low | All HIGH + MEDIUM fixed

# Severity Issue Fix Applied
H1 HIGH GET ordering broke RecentList "Recent" semantics — sortOrder ASC was primary for all queries sortOrder ordering now only applies when projectId is specified
M1 MEDIUM No KeyboardSensor for keyboard DnD accessibility Added KeyboardSensor + sortableKeyboardCoordinates
M2 MEDIUM Missing duplicate ID validation in /reorder schema Added .refine() for unique ID check + test
M3 MEDIUM Placeholder tests (expect(true)) for reorder ownership Replaced with real schema validation tests
M4 MEDIUM N+1 sequential updates in /reorder transaction Accepted for v1 (100-item cap) — noted for future optimization
L1 LOW Story File List claimed wrong migration filename Fixed in File List below
L2 LOW Missing package.json + lockfile from File List Fixed in File List below
L3 LOW Triple-state overProjectId typing Accepted — functional, cosmetic concern only

File List

  • packages/db/src/schema/diagram.ts — added sortOrder column
  • packages/db/migrations/0001_fuzzy_gorilla_man.sql — migration for sortOrder column
  • packages/db/migrations/meta/0000_snapshot.json — updated migration metadata
  • packages/db/migrations/meta/_journal.json — updated migration journal
  • packages/api/src/modules/diagram/router.ts — extended PATCH schema, added POST /reorder with duplicate ID validation, context-aware GET ordering
  • apps/web/src/modules/diagram/components/sidebar/RecentList.tsx — AI preview subtitle
  • apps/web/src/modules/diagram/components/sidebar/ProjectTree.tsx — @dnd-kit drag-and-drop with keyboard + pointer sensors
  • apps/web/package.json — added @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities dependencies
  • pnpm-lock.yaml — updated lockfile
  • packages/api/tests/diagram/diagram-access-control.test.ts — extended with reorder + PATCH schema tests, replaced placeholder tests
  • packages/api/tests/diagram/diagram-db-schema.test.ts — fixed for sortOrder field