2.3.3 `createIssue` Server Action + create-issue modal
Estimate: 18m
The Story's entry-point write surface. A thin Server Action
createIssue at app/(authed)/projects/[key]/_actions/createIssue.ts
calls the shipped workItemsService.createWorkItem (Story 1.4) — which already
owns key allocation (via projectRepository.allocateWorkItemNumber), initial-status
seeding (per 2.2.4, reads the project's workflow_status with isInitial=true),
parentage gate (via 2.1.2's assertValidParent), and the create-revision audit row.
This Subtask does not extend the service — it builds the Server Action wrapper
- the UI on top.
Modal UI. Reusable CreateIssueModal client component, opened
three ways: (1) the existing top-nav "+" button from Story 1.5.3's shell slot, (2) global "C" keyboard shortcut wired into lib/shortcuts.ts (the cheatsheet from 1.5.4 picks
it up automatically), (3) ⌘K palette command "Create issue" (1.5.4's
AppCommandPalette). Modal is a Radix Dialog with form fields in this order:
Type (defaults to task; the picker is the filtered combobox
from 2.3.4), Parent (optional; same combobox, filtered by 2.1.2's
canParent(parentKind, selectedType)), Title (required, single
line, max 200 chars per existing Story 1.4 validation), Description
(optional, the MarkdownEditor primitive from 2.3.5, compact "min" mode = textarea
- preview tab, NO image upload affordance in the modal — that's edit-form territory),
Assignee (optional; a workspace-member combobox), Priority
(defaults to
mediumper the schema). Reporter is NEVER a form field — always the current session user (the schema requires non-null reporter; defaulting server-side keeps the form lean and matches Jira/Linear's standard shape).
Submit flow. Optimistic UX is OFF for create (the new key is server-generated
via the project counter — there's no value to optimistic-render). On submit: form validation
(zod schema matching the service's expected DTO) → call createIssue Server Action
→ service throws either IllegalParentTypeError (422),
ProjectNotFoundError (404, cross-workspace defense), or
WorkspaceContextMissingError (500, infra bug); these map to inline form errors
(parent field) or a toast (project/workspace). On success the modal closes, a toast surfaces
the new identifier (e.g. "PROD-42 created" with the identifier as a link to the future detail
page — for now it links to /projects/[key]/issues/[key]/edit from 2.3.6), and
revalidatePath refreshes the active list view.
What this Subtask does NOT do: rich-text image upload (2.3.7), full
edit form (2.3.6), the filtered type+parent combobox internals (2.3.4 — this Subtask
imports it), the MarkdownEditor primitive internals (2.3.5 — imports it). If
2.3.4 or 2.3.5 haven't landed by the time this runs, the modal uses temporary stubs (a
plain <select> for type + a basic textarea for description) and swaps
to the real components when those land — a comment marks each stub.
Acceptance criteria
- Server Action
createIssueat the documented path; pure 4-layer thin wrapper (parse input → resolve workspaceId from session → callworkItemsService.createWorkItem→ return DTO or typed error). No business logic. - Modal opens via "+" nav button, global "C" shortcut, AND ⌘K command — all three entry points wired and verified in dev.
- Required fields: Type (default
task), Title (max 200 chars). Title-empty + Title-overflow surface inline form errors before submit. - Successful create: toast with the new identifier links to
/projects/[key]/issues/[key]/edit; list view revalidates; modal closes. IllegalParentTypeErrorfrom the service surfaces as an inline error on the Parent field (not a toast); cross-workspace project rejection 404s the action.- Reporter is never a form field; the session user is set server-side. A spec asserts a forged client payload with a
reporterIdattribute is IGNORED (the Server Action doesn't read it). - New
tests/issues/createIssueAction.test.ts: the action calls the service with the expected DTO, propagates typed errors, ignores client-supplied reporter, returns the created DTO. Service tests are NOT re-run here — 1.4.x already coverscreateWorkItem. - Modal-side React-Testing-Library spec under
tests/components/CreateIssueModal.test.tsx: the three entry points open it; required-field validation; submit calls the action with the form's values. - STRICT shell-a11y sweep (1.5.5's
shell-a11y.spec.ts) extended to render the modal in its default-open state — zero axe violations.
Context refs
lib/services/workItemsService.ts(Story 1.4) —createWorkItemsignature + the typed errors it throwslib/issues/parentRules.ts(2.1.2) —canParent, consumed by 2.3.4's picker but referenced here for the error pathlib/shortcuts.ts(1.5.4) — register "C" alongside the existing shortcuts; the cheatsheet auto-picks it upcomponents/ui/CommandPalette.tsx+AppCommandPalette(1.5.4) — register the "Create issue" command- Top-nav "+" slot from 1.5.3's shell — wire to open the modal
- Existing Story-1.5.5 axe sweep — extend, do not duplicate
motir-core/CLAUDE.md— Server Action / 4-layer rules