2.3.6 Full edit form at `/projects/[key]/issues/[key]/edit` · closes finding #46
Estimate: 18m · Depends on: 2.3.3, 2.3.4, 2.3.5
The full edit form — a dedicated route, not a modal. Lives at
app/(authed)/projects/[key]/issues/[key]/edit/page.tsx as a Server Component
that reads the work item via workItemsService.getWorkItem + the project's
workflow statuses + the current user's permissions, and renders a client
EditIssueForm with two Server Actions:
updateIssueAction (everything except status) and changeStatusAction
(status only). The status edit cannot share a code path with the non-status edits
— this is what closes finding #46.
Finding #46 — resolved here. Story 1.4's updateWorkItem
currently accepts a free-form status patch that bypasses 2.2.4's transition
validation. This Subtask:
- Removes the
statusfield fromupdateWorkItem's patch DTO (UpdateWorkItemInput) at the service layer. - Removes the
statushandling in the service body. - Updates the 1.4 test that exercised the ungated path so it now drives
workItemsService.updateStatus(the 2.2.4 gated path). Any other caller is scanned + redirected. - Adds a guard test asserting
UpdateWorkItemInputhas nostatuskey (TypeScript compile-time + a runtime grep guard). - Edit form wires the status field to
changeStatusAction(which callsupdateStatus) — all other fields go throughupdateIssueAction(which calls the now-status-freeupdateWorkItem). In the UI both controls live on the same form, but the submit handler routes the patch through two server roundtrips when both status + non-status fields changed in the same edit. Atomic-cross-field semantics are NOT required for v1 (finding #46's whole point is that status changes are a separate gated operation; coupling them back to the patch is the wrong shape).
Form fields. Every editable column on work_item: Title,
Description (MarkdownEditor from 2.3.5, full size, with the real
image-upload handler from 2.3.7 wired through if 2.3.7 landed — else the placeholder), Type
- Parent (2.3.4 pickers — type changes that invalidate the parent clear the parent with the
documented notice), Status (status picker over the project's workflow), Priority, Assignee,
Due date, Estimate (minutes). Explanation (
explanationMd) is rendered read-only here with an "AI-drafted" badge whenexplanationSource == ai_draft; editing the explanation goes to a separate Subtask in a later Story (the AI-draft regeneration loop is Epic 7). Reporter is read-only (set on create, immutable per Jira/Linear's standard shape).
Optimistic-concurrency check. The form reads the work item's
updatedAt on render and submits it as a hidden field. The Server Action passes
it to updateWorkItem, which (this Subtask extends the service to accept) checks
inside the transaction that updatedAt matches; mismatch → a typed
StaleWorkItemError 409 surfaces as "This issue was edited by someone else —
refresh and retry" with a refresh button. Last-write-wins is the shipped behavior, but the
user sees it instead of silently losing edits.
Revisions. Every field-level diff lands in the existing 1.4.6 revision
pipeline. The status change flows through 2.2.4's revision path (already in place); the
non-status diff flows through 1.4.6's updateWorkItem revision path. After this
Subtask the audit trail is complete for every field on the form.
Acceptance criteria
- Route renders for an existing issue; 404 for cross-workspace; redirect to sign-in when unauthenticated; permission gate uses the Story-1.2 membership pattern.
- All listed editable fields are in the form; reporter is read-only; explanation is read-only with the "AI-drafted" badge when applicable.
updateIssueActioncallsupdateWorkItemwhich NO LONGER acceptsstatus. Grep guard test asserts nostatuskey inUpdateWorkItemInputnor in the service method body.changeStatusActioncallsupdateStatus(2.2.4); illegal-transition surfaces as the inline status-picker error.- The Story-1.4 test that drove a status change through
updateWorkItemis updated to driveupdateStatusinstead; all 1.4 tests stay green. - Stale-edit detection: a Vitest scenario that mutates the row between read + submit produces
StaleWorkItemError409; the UI surfaces the refresh banner. - Mixed-edit (status + non-status fields) is submitted via TWO Server Action calls; both succeed or one fails — there's no all-or-nothing requirement (documented in the AC).
- Revisions: a Vitest exercise asserts an edit of title + status produces TWO revision rows (one per action call), each with the right
changeKind+ diff. - STRICT shell-a11y sweep extends to the edit route; zero violations.
Context refs
lib/services/workItemsService.ts(1.4 + 2.2.4) —updateWorkItem(the ungatedstatuspatch to REMOVE),updateStatus(the gated path to route status edits through)lib/workItems/errors.ts— addStaleWorkItemErrorbeside the existing errors; route mapping → 409- 1.4.6's revision pipeline + 2.2.4's status-revision path — both keep working unchanged
- 2.3.3/2.3.4/2.3.5 components — imported, not re-built
app/(authed)/projects/[key]/_components/— if any pattern exists, mirror it; else establish the per-issue route shape here- Story 1.2 membership-gating helpers + Story 1.5 layout — the route renders inside the authed shell
PRODECT_FINDINGS.mdentry #46 — append> Resolved: 2.3.6on completion