Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-164Done

3.2 Kanban board UI + drag-drop transitions

The board surface: a horizontally-scrolling row of columns, each a vertical stack of cards, with accessible drag-drop that moves a card between columns (a workflow transition) or reorders it within a column (a rank change). This is a pure frontend story — it builds NO new backend; every read and write goes through the Story-3.1 board API (GET /api/projects/[key]/boardBoardProjectionDto, GET …/board/columns/[id]/cards → the lazy page, POST …/board/move → the moved card). The load-bearing contract carried from 3.1: moving a card is a workflow transition, never a board-local write — a cross-column drop calls POST …/move, which resolves to issuesService.updateStatus under the project's policy mode; an in-column drop is a pure work_item.position rank change. The UI applies the move optimistically and reconciles against the server response, snapping the card back when the server rejects an illegal transition (IllegalBoardMoveError → HTTP 409) or an unmapped target (UnmappedColumnTargetError → 422).

Where it lives. The /boards route already exists as a stub placeholder (app/(authed)/boards/page.tsx renders ProjectStubPage … comingIn="Epic 3"); the sidebar "Boards" nav link + the Cmd-K "Go to Boards" entry are already wired (Story 1.5). This story replaces the stub with the real surface — no new navigation wiring. The board renders the active project's single default Kanban board (3.1 auto-seeds one per project); multi-board routing is a non-breaking addition planned as Story 3.7 (the API already takes a boardId).

Reuse, do not reinvent (the card visual language). A board card is the same issue, shown compactly — so it REUSES the issue-list card primitives, not a parallel set: the IssueTypeIcon (kind hue via --el-type-*), the Pill tones for status/priority, the ReadinessBadge blocked/ready signal, the assignee avatar, and the key/identifier chip — all already shipped under app/(authed)/issues/_components/issueCellPrimitives.tsx + components/ui/*. Clicking a card opens the existing IssueQuickView panel (Story 2.5), not a new detail surface. The board is a new arrangement of shipped primitives.

Accessibility is in scope, not deferred. The stub mandates "accessible keyboard DnD," and Jira/Linear both ship it — so drag-drop uses dnd-kit (@dnd-kit/core + @dnd-kit/sortable), the de-facto accessible React DnD library (pointer + keyboard sensors, a DragOverlay, and aria-live move announcements out of the box). This is a new dependency (no DnD lib exists yet); it is the mirror-product-standard choice (decision-ladder rung 1), not a deviation. A drag must be fully operable by keyboard (pick up / move / drop / cancel) with screen-reader announcements; drop targets are not signalled by colour alone (finding #35).

Scale shape — the board is bounded, never "render every card" (finding #57). 3.1 already returns a bounded first page per column with a per-column total count and a cursor; this story consumes that — each column shows its count, lazy-loads further cards on demand via GET …/board/columns/[id]/cards?cursor=, and virtualizes a tall column so the DOM row count stays bounded (reusing the windowing primitive Story 2.5.15 establishes for the issue tree — do NOT introduce a second virtualization library). A board that fetched and rendered every card would be prototype-thinking; the projection is paged by design and the UI must honour it.

Completeness — the real-product states, not just the happy path. A real board has: a loading skeleton (columns + card placeholders) while the projection streams; an empty state (project has no issues yet → a create-issue affordance); a no-board / error / forbidden state; and an unmapped-statuses affordance — 3.1's projection deliberately returns unmappedStatuses (project statuses mapped to no column, the Jira behaviour), which the board surfaces as a tray/banner (with a path to the board/workflow admin) rather than silently dropping them. The board is responsive (horizontal scroll on desktop, a usable mobile layout). These are planned as their own subtask, not bolted on.

Out of scope (Epic-3 siblings): swimlanes + WIP-limit enforcement / over-limit warnings (Story 3.3 — this story renders the wipLimit the projection returns as a count display only, and leaves a slot for 3.3); the sprint-scoped Scrum board (Story 4.5 — moved to Epic 4 per mistake #32); the cross-cutting drag-drop + WIP + swimlane Playwright journey (Story 3.5 — this story ships its OWN UI component tests + the core drag/snapback E2E, the same split 3.1.7 used). The column↔status mapping admin — where an unmapped status is put on a column (this story’s unmapped-tray links there) — is Story 3.6; board CRUD / multi-board is Story 3.7.

Verification

  • Pull the Story branch, pnpm install (picks up the new @dnd-kit/* deps), pnpm prisma migrate dev, pnpm db:seed, pnpm dev.
  • pnpm test — vitest covers the optimistic-move / snapback reducer (move applied → confirmed on 200; reverted on 409/422), the BoardCard / BoardColumn render (type hue, readiness, count), and the column lazy-load page-append logic.
  • pnpm test:e2e --grep board-ui — Playwright drives the real board: drag a card across columns (status persists), an illegal move snaps back (409), in-column reorder changes only rank, and the keyboard-DnD path moves a card end-to-end.
  • Visual / design check: open /boards on the seeded moooonmotir project. The board matches design/boards/board.mock.html — columns in workflow order with a card count each, cards showing type icon + key + title + assignee + priority + a blocked indicator where applicable, colour via --el-* (not grey-only, finding #54).
  • Move = transition check: drag a card from To Do → In Progress → the card lands, the count updates, and re-opening the issue (or the quick view) shows the new status. Attempt an illegal move under a restricted workflow (e.g. To Do → Done with no such transition) → the card animates back to its origin column and a toast explains the rejection; the issue status is unchanged.
  • In-column reorder check: drag a card up/down within a column → it stays in the column, no status change, the order persists on reload (rank only).
  • Keyboard-DnD check: focus a card, press the pick-up key (Space/Enter), arrow to another column, drop → the move happens with an aria-live announcement; Escape mid-drag cancels with the card returning home. Verify with the keyboard alone (no mouse).
  • Scale check (finding #57): pnpm db:seed:large, open a column with hundreds of cards → it shows the bounded first page + the total count + a "Load more" affordance; scrolling/loading fetches the next page via GET …/columns/[id]/cards?cursor=; the rendered DOM row count stays bounded (virtualized) even when the column is fully expanded.
  • Unmapped-status check (mirror-product): add a custom status via the workflow editor (Story 2.2.5) → the board shows it in the unmapped-statuses tray (NOT a new column, NOT silently dropped), with a link to map it; existing columns are unaffected.
  • States check: a brand-new project with no issues shows the empty state (with a create affordance); throttling the network shows the loading skeleton; a forced API error shows the error state with a retry.