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:
status field from updateWorkItem's
patch DTO (UpdateWorkItemInput) at the service layer.status handling in the service body.workItemsService.updateStatus (the 2.2.4 gated path). Any other caller is
scanned + redirected.UpdateWorkItemInput has no
status key (TypeScript compile-time + a runtime grep guard).changeStatusAction
(which calls updateStatus) — all other fields go through
updateIssueAction (which calls the now-status-free updateWorkItem).
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
explanationMd) is
rendered read-only here with an "AI-drafted" badge when explanationSource == 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.
updateIssueAction calls updateWorkItem which NO LONGER accepts status. Grep guard test asserts no status key in UpdateWorkItemInput nor in the service method body.changeStatusAction calls updateStatus (2.2.4); illegal-transition surfaces as the inline status-picker error.updateWorkItem is updated to drive updateStatus instead; all 1.4 tests stay green.StaleWorkItemError 409; the UI surfaces the refresh banner.changeKind + diff.lib/services/workItemsService.ts (1.4 + 2.2.4) — updateWorkItem (the ungated status patch to REMOVE), updateStatus (the gated path to route status edits through)lib/workItems/errors.ts — add StaleWorkItemError beside the existing errors; route mapping → 409app/(authed)/projects/[key]/_components/ — if any pattern exists, mirror it; else establish the per-issue route shape herePRODECT_FINDINGS.md entry #46 — append > Resolved: 2.3.6 on completion