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-91

2.3.6 Full edit form at `/projects/[key]/issues/[key]/edit` · closes finding #46

Done
Description

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 status field from updateWorkItem's patch DTO (UpdateWorkItemInput) at the service layer.
  • Removes the status handling 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 UpdateWorkItemInput has no status key (TypeScript compile-time + a runtime grep guard).
  • Edit form wires the status field to 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

  • 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 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.

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.
  • 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.
  • The Story-1.4 test that drove a status change through updateWorkItem is updated to drive updateStatus instead; all 1.4 tests stay green.
  • Stale-edit detection: a Vitest scenario that mutates the row between read + submit produces StaleWorkItemError 409; 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 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 → 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.md entry #46 — append > Resolved: 2.3.6 on completion