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>
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
-
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).
-
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
projectIdis updated via API call, And the sidebar tree reflects the new organization immediately with optimistic UI update. -
Given I am viewing diagrams within a project in the sidebar, When I drag to reorder diagrams, Then the
sortOrderis updated for affected diagrams, And the new order persists on page reload. -
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
sortOrdercolumn to diagram table and extend PATCH endpoint (AC: #2, #3)- 1.1: Add
sortOrder: integer().default(0)column todiagramtable inpackages/db/src/schema/diagram.ts - 1.2: Generate and apply Drizzle migration
- 1.3: Extend
updateDiagramBodySchemato acceptprojectId(string, optional) andsortOrder(integer, optional) in addition totitle - 1.4: Add bulk reorder endpoint:
POST /diagrams/reorderaccepting{ items: [{ id, sortOrder }] }for batch sortOrder updates
- 1.1: Add
- Task 2: Enhance RecentList with AI preview subtitle (AC: #1)
- 2.1: Modify
RecentList.tsxto displaylastAiMessagetruncated 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
updatedAtdescending (already the case from the API)
- 2.1: Modify
- 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/utilitiesinapps/web - 3.2: Wrap
ProjectTreecontent withDndContext+SortableContextproviders - 3.3: Make diagram items in expanded projects sortable with
useSortablehook - 3.4: Implement cross-project drag: detect when a diagram is dropped on a different project node → call PATCH to update
projectIdwith optimistic React Query update - 3.5: Implement intra-project reorder: detect sort order changes within a project → call
POST /diagrams/reorderwith 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)
- 3.1: Install
- Task 4: Update DiagramGrid to show diagrams in sortOrder (AC: #3)
- 4.1: Update the GET
/diagramsendpoint to order bysortOrder ASC, updatedAt DESC(sortOrder as primary, updatedAt as tiebreaker) - 4.2: Ensure DiagramGrid respects the API ordering
- 4.1: Update the GET
- Task 5: Verify empty state (AC: #4)
- 5.1: Confirm
EmptyDiagramscomponent renders when no diagrams exist (already implemented — verify integration)
- 5.1: Confirm
- Task 6: Tests (AC: all)
- 6.1: API tests for extended
updateDiagramBodySchema(projectId and sortOrder fields) - 6.2: API tests for
POST /diagrams/reorderendpoint (batch update, ownership check) - 6.3: Verify existing 120 tests still pass (139 tests pass)
- 6.1: API tests for extended
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-center → items-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:
- Wrap with
DndContextat theProjectTreelevel - Each expanded project's diagram list gets a
SortableContext - Each diagram item uses
useSortablehook for dragging - Projects themselves act as droppable areas using
useDroppable onDragEndhandler:- If dropped on a different project → PATCH diagram's
projectId - If reordered within same project → POST to
/diagrams/reorder
- If dropped on a different project → PATCH diagram's
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/50when 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: addsortOrdercolumnpackages/api/src/modules/diagram/router.ts— MODIFIED: extend PATCH schema, add POST /reorder, change orderingapps/web/src/modules/diagram/components/sidebar/RecentList.tsx— MODIFIED: add AI preview subtitleapps/web/src/modules/diagram/components/sidebar/ProjectTree.tsx— MODIFIED: add @dnd-kit DnDapps/web/src/modules/diagram/components/sidebar/DiagramSidebar.tsx— MINOR: may need DndContext wrapper if shared across tabspackages/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
/reorderendpoint 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
inArraywithout adding it — addinArrayto the drizzle-orm import in router.ts
Previous Story Intelligence (Story 1.3)
Key learnings to carry forward:
DropdownMenupreferred overContextMenufor action menus (more discoverable, accessible)- Hono RPC client pattern:
api.diagrams[":id"].$patch({ param: { id }, json: { ... } }) toast()fromsonnerfor 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
DiagramResponsetype exported fromDiagramCard.tsx— reuse for DnD data typingdiagramTypeConfigandtimeAgoexported fromDiagramCard.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 handling85e06c2 feat: implement Story 1.2 — organize diagrams into projects— project CRUD, sidebar, ProjectTree392da38 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.tshad to be updated to includesortOrderin 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/utilitiesinstalled inapps/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.setQueryDatawith invalidation on error /reorderroute placed before/:idto 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— addedsortOrdercolumnpackages/db/migrations/0001_fuzzy_gorilla_man.sql— migration for sortOrder columnpackages/db/migrations/meta/0000_snapshot.json— updated migration metadatapackages/db/migrations/meta/_journal.json— updated migration journalpackages/api/src/modules/diagram/router.ts— extended PATCH schema, added POST /reorder with duplicate ID validation, context-aware GET orderingapps/web/src/modules/diagram/components/sidebar/RecentList.tsx— AI preview subtitleapps/web/src/modules/diagram/components/sidebar/ProjectTree.tsx— @dnd-kit drag-and-drop with keyboard + pointer sensorsapps/web/package.json— added @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities dependenciespnpm-lock.yaml— updated lockfilepackages/api/tests/diagram/diagram-access-control.test.ts— extended with reorder + PATCH schema tests, replaced placeholder testspackages/api/tests/diagram/diagram-db-schema.test.ts— fixed for sortOrder field