2.4.9 Link management UI — add / remove relationship links
Estimate: 16m · Depends on: 2.4.5, 2.4.8
Make the 2.4.5 relationships panel INTERACTIVE — wire the shipped linkWorkItems / unlinkWorkItems (1.4.4) into it per the 2.4.8 design. Thin Server Actions (createLinkAction / removeLinkAction) over the gated service methods (no raw Prisma — 4-layer rule); a client add-link control opened from the panel header (kind selector + issue-search Combobox); an inline remove on each row; revalidatePath on the detail route so the groups AND the readiness banner update (adding a non-terminal blocker flips Ready→Blocked; removing it flips back). The typed link errors map to inline messages (no crash). The 2.4.5 "Manage in Epic 5" read-only note is REPLACED by the real control.
Candidate read. The issue-search Combobox needs an in-workspace candidate lookup (by identifier / title, excluding self + already-linked). Add a lightweight workItemsService.listLinkCandidates (workspace-scoped, explicit workspaceId filter — finding #26; cross-project allowed, matching the link model's same-workspace rule); full cross-cutting search is Epic 6, so this is a simple prefix/contains read, not the search engine. Scope: management lives on the DETAIL panel; the edit page's related-issues block stays READ-ONLY (an editor keeps context there — 2.4.5).
Acceptance criteria
- From the relationships panel, add a link of each of the five kinds to another in-workspace issue via the Combobox search; the new link appears in its group on revalidation, and the readiness banner re-judges (a new non-terminal
is_blocked_bytarget flips Ready→Blocked). - Remove a link inline (with confirm) → it disappears from its group + the banner re-judges;
relates_toremoves both reciprocal rows (service already does this). - Self-link / duplicate / cycle / cross-workspace are rejected with an INLINE message (the typed errors → 422/409), nothing persisted, no crash; the add control is permission-gated (workspace member; the gated service + the trigger backstop).
- Server Actions call ONE gated service method each (no raw Prisma);
listLinkCandidatescarries an explicitworkspaceIdfilter. - Vitest (real Postgres) for the actions +
listLinkCandidates(incl. excludes-self / excludes-already-linked / cross-workspace empty); a component test for the add control (kind + combobox + submit + error surfacing); the detail + edit routes stay green on the shell-a11y sweep (the add dialog/combobox is accessible — reuse the 2.3.4 Combobox semantics).
Context refs
design/work-items/links.mock.html+design-notes.md(2.4.8 — REQUIRED, match its layout/primitives)lib/services/workItemsService.ts—linkWorkItems/unlinkWorkItems+listCandidateParents(the candidate-read pattern to mirror forlistLinkCandidates)lib/workItems/linkErrors.ts+lib/dto/workItemLinks.ts(kinds +LinkWorkItemsInput)app/(authed)/issues/[key]/_components/RelationshipsPanel.tsx(2.4.5 — make it interactive) +components/ui/Combobox(2.3.4)- 2.3.3 / 2.3.6 Server-Action +
revalidatePathconventions