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

2.3.3 `createIssue` Server Action + create-issue modal

Done
Description

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 medium per 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 createIssue at the documented path; pure 4-layer thin wrapper (parse input → resolve workspaceId from session → call workItemsService.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.
  • IllegalParentTypeError from 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 reporterId attribute 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 covers createWorkItem.
  • 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) — createWorkItem signature + the typed errors it throws
  • lib/issues/parentRules.ts (2.1.2) — canParent, consumed by 2.3.4's picker but referenced here for the error path
  • lib/shortcuts.ts (1.5.4) — register "C" alongside the existing shortcuts; the cheatsheet auto-picks it up
  • components/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