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

6.8 Edit project details + change project key (with old-key redirects)

Done
Description

Project-admin editing of project details — name, avatar, key — by growing the 6.5.3 Details landing (the read-only identity page the 6.5 settings area lands on; its card names this story as the seam) into the editable surface. The load-bearing piece is changing the project key mid-project, Jira-faithfully (verified against the Atlassian "Editing a project key" doc + the Jira Cloud "Previous project keys" feature): on PROD → NIF, every issue identifier re-renders with its number preserved (PROD-42 → NIF-42), and the old key keeps working permanently — old issue links redirect, API calls on the old key resolve, and the old key stays reserved against other projects.

The alias table + central resolution (the durable shape). A new project_key_alias row records each retired key (workspace-unique; cascades with the project, so deletion frees its keys — the mirror rule). Resolution is alias-aware in ONE place — the project-by-key read path — so every key-addressed surface inherits it: the /api/projects/[key] routes resolve old keys and serve (the verified REST behaviour — no redirect), while the /issues/[key] pages parse the identifier prefix, resolve the alias, and 308-redirect to the canonical identifier (the verified link behaviour). Link TEXT is never rewritten (Jira parity: "link aliases will not be updated" — they resolve, not mutate). 6.2 saved filters need nothing: the FilterAST references project-scoped ids, never key strings.

The rename transaction. One tx: FOR-UPDATE lock on the project row; format guard (the shipped normalizeIdentifier contract — 3–5 uppercase A–Z/0–9; rung 2 over Jira's configurable ≥2-letters default); collision guard against BOTH live identifiers and OTHER projects' aliases (reclaiming the project's OWN previous key deletes that alias row — the verified revert path); a SINGLE bulk UPDATE work_item SET identifier = <new> || '-' || key (index-maintained, no per-row loop, no revision-row spam — the identifier is derived data; the key number never changes); alias insert; project update. Jira runs a background Lucene re-index here; ours is structurally cheaper — search reads the denormalized column, so the bulk UPDATE is the re-index, synchronous and atomic. The create-project path's identifier-suffix loop also grows alias-awareness (a new project must not take a reserved key).

Details editing. Name: a plain rename (breadcrumb/switcher update via the existing DTO; slug is a create-time artifact no URL consumes — NOT regenerated, recorded). Avatar: preset icon library + colour swatch rendered as the project chip in the switcher and details card; null = the shipped mono-identifier rendering (zero-migration backfill). Deviation recorded: no image upload (Jira's defaults are themselves a preset library; the 2.3.7 upload primitive is attachment-scoped; upload = documented extension). Previous keys: the details card lists the alias history with an explicit release-with-confirm control (the Jira Cloud "Previous project keys" remove — releasing un-reserves the key and breaks its old links, so it gets its own consequence confirm).

Real-product states (finding #57 + the sweep): admin-gated (the 6.4.3 capability — non-admins see the card read-only); validation, collision (live + alias), and unchanged-key states; an in-flight state while the bulk rewrite commits; success feedback naming the consequence ("old links keep working"); concurrency guards (a rename racing issue creation must never mint a stale-prefix identifier — both orderings asserted against the FOR-UPDATE lock).

Out of scope (documented extension slots, each justified): project description / category / lead / default-assignee fields (absent from the shipped model, rung 2; component default-assignees (5.4) cover the default-assignee use case); avatar image upload (above); changing the issue-key NUMBERING (Jira reserves numbers too — out entirely); a workspace-level "release on delete" admin view (deletion already cascades the aliases). This story is the capability 8.7 (Prodect → Motir rebrand) consumes. Relationship to 6.5: the backend (6.8.1/6.8.2) is independent; the UI grows the 6.5.3 Details landing and its design extends the 6.5.1 area asset (backward same-epic deps).

Verification

  • Pull the Story branch, pnpm install, pnpm prisma migrate dev (the project_key_alias + avatar-columns migration applies cleanly; re-run reports "No difference detected" — the FK-drift rule), pnpm db:seed, pnpm dev.
  • pnpm test:coverage — Vitest (real Postgres) over the rename tx (atomicity, lock ordering, collision matrix incl. aliases, reclaim + release) ≥90% per-file branch/fn/line.
  • Details page: sign in as zhuyue@motir.co / !QAZ1qaz → Settings → Project — the 6.5 area lands on Details, now editable, matching design/projects/details.mock.html panel-for-panel. Rename the project → the breadcrumb + switcher update. Pick an avatar icon + colour → the chip renders on the page and in the project switcher (closed + open states); clearing it restores the shipped mono-identifier rendering.
  • Key change: Change key PROD → NIF via the consequence modal (copy names the effects: identifiers re-render, old links keep working). After confirm: every issue shows NIF- with its number preserved (board, list, detail, links); the Details card's "Previous keys" row lists PROD.
  • Old-key resolution: an old bookmark /issues/PROD-7 308-redirects to /issues/NIF-7 (assert the permanent redirect + the canonical URL in the bar); /api/projects/PROD/members still serves (no redirect — the verified REST shape).
  • Reservation + revert: creating a new project with identifier PROD fails with the collision error (alias-reserved); changing the key back to PROD reclaims it (NIF becomes the alias). Releasing an alias via its confirm un-reserves it and its old links now 404.
  • Concurrency: the Vitest race specs pass — a rename concurrent with issue creation yields only canonical-prefix identifiers in both interleavings.
  • Gating: a viewer/member (non-admin) sees the card read-only; the PATCH rejects them with the typed 403.
  • pnpm test:e2e --grep project-details — Playwright over the rename → redirect → revert journey.
  • a11y check: the Details page, avatar picker, and both confirm modals pass the strict axe sweep; fully keyboard-operable; colour via --el-*, shape via element tokens.