MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-99

2.4.1 Detail route `/issues/[key]` + `getIssueDetail` read + page shell

Done
Description

Estimate: 16m

The route + data spine the rest of the Story hangs off. A Server Component at app/(authed)/issues/[key]/page.tsx resolves the active project (getActiveProject()) and reads the work item BY ITS PROJECT-SCOPED IDENTIFIER (e.g. PROD-42) — not by raw id — since that's what the URL carries and what every link in the app produces.

Service (one aggregate read). Add workItemsService.getIssueDetail(identifier, ctx) returning a single IssueDetailDto that bundles what the page needs in one round-trip: the work item, its parent (id + identifier + kind + title + status, for the breadcrumb), its direct children (same shape, for the child list), its links (blocked-by / blocks), and its workflow context (the project's statuses + legal next-transitions for the current status). It composes existing repo reads behind ONE service method (CLAUDE.md: the page calls one service method, not five repos). Tenant gate FIRST: a cross-workspace or non-existent identifier collapses to a uniform 404 (no existence leak) — reuse WorkItemNotFoundError. getWorkItemByIdentifier is the underlying repo lookup (@@unique([projectId, identifier])); if 2.3.6 already added it, reuse it — do not add a second copy.

Page shell. The two-column layout every later subtask fills: a header row (type icon from 2.1.1 · PROD-N identifier · title · a status pill placeholder 2.4.4 upgrades to the inline control), a main column (description/explanation — 2.4.2, child list — 2.4.3), and a metadata sidebar (core fields — 2.4.2; relationships — 2.4.5). An "Edit" button links to /issues/[key]/edit (2.3.6). The layout reserves clearly-commented Epic-5 extension slots (comments · attachments · custom fields · activity) as empty regions, so those land as fills rather than a re-layout. Renders inside the Story-1.5 authed shell; unauthenticated → sign-in redirect; non-member / cross-workspace → 404.

Acceptance criteria

  • Route renders for an existing issue addressed by PROD-N; cross-workspace / unknown identifier → 404 (no title/existence leak); unauthenticated → sign-in redirect.
  • workItemsService.getIssueDetail returns ONE IssueDetailDto (work item + parent + children + links + workflow context); the page makes exactly one service call. Explicit workspaceId gate (finding #26).
  • getWorkItemByIdentifier is the single shared identifier lookup (reused from 2.3.6 if present, else added here — never duplicated).
  • Header renders type icon + identifier + title + a (static, for now) status pill; an "Edit" button links to the 2.3.6 edit route; Epic-5 extension slots exist as documented empty regions.
  • Vitest (real Postgres) over getIssueDetail: happy path shape, cross-workspace 404, parent/children/links populated correctly. The STRICT shell-a11y sweep is extended to the detail route (added here, asserted across later subtasks).

Context refs

  • lib/services/workItemsService.ts + lib/repositories/workItemRepository.ts (1.4) — getWorkItem/listChildren/findById/getWorkItemByIdentifier + the workspaceId-filter convention
  • app/(authed)/issues/[key]/edit/ (2.3.6) — the sibling route shape + getActiveProject() resolution (finding #50)
  • lib/dto/workItems.ts + lib/mappers/workItemMappers.ts — DTO + mapper conventions (no @prisma/client leak)
  • Story 1.5 authed shell + 1.5.5 shell-a11y sweep — extend, don't duplicate
  • motir-core/CLAUDE.md — 4-layer, one-service-call-per-page, entity-naming
Status
Done
Type
Sub-task