5.3 Custom fields (per-project definitions)
The extensible-schema layer: project admins define custom fields — text · number · date · select (single, with managed options) · user — in the project settings area (6.4), and issues carry values for them, rendered and inline-edited on the detail rail beside the built-in fields. Values are stored typed-EAV (one row per issue × field with per-type value columns — the shape Jira itself uses) so Epic 6 can filter on them with real predicates and indexes — the stub's "design carefully" constraint, honoured in the schema, not deferred.
The Jira-verified shape (rung 1, team-managed, checked at plan time). Our five types each map to a verified member of Jira's team-managed set (Short text / Number / Date / Dropdown / People). Team-managed Dropdown is single-select only (multi-select is the separate Checkbox type), so single-select IS the mirror shape. Definitions live at Project settings → Fields, project-admin-gated (the 6.4 two-tier check), project-scoped, with the documented Jira caps adopted as guards: 50 fields per project, 55 options per field. On the issue view, fields WITH values render as rail cards; empty ones collapse behind "Show more fields" (Jira's hide-when-empty rule) — mirrored without the work-type layout-config subsystem (layouts / per-type required / create-form placement are the documented extension, composing into 6.5's settings hub). Field deletion is hard (team-managed has no trash: immediate, permanent, values destroyed — confirm names the value count). Options rename and reorder freely; an in-use option archives (hidden from new selection, existing values keep rendering with an archived mark) and deletes only when unused — the verified "Optimize" rule; in-use-delete semantics are undocumented in the mirror, so the safe rule wins. Value changes write the 1.4.6 revision diff (customFields.<key>) — the History entries Story 5.5 renders, same as built-in fields.
Validation is the service's job, per type: number → decimal (the storage column, not float drift); date → date-only ISO, UTC-safe (the dueDate convention); select → the option must belong to the field and (for NEW sets) not be archived; user → a workspace member who can view the project (the 6.4 assignableMembersService scoping — the same rule as assignee/mentions); text → length-capped. Setting a value upserts the row; clearing deletes it (no tombstone rows). Who edits values = who edits the issue (admin/member; read-only viewer sees values, no editors).
Bounded everywhere (finding #57). The detail read joins ≤50 definitions + this issue's value rows (one bounded query slotted into getIssueDetail's parallel fetch); the admin list is ≤50 by the cap; options ≤55. The Epic-6 contract is documented in the schema subtask: per-type value columns indexed by [fieldId, value*] so 6.1's filter builder compiles JOIN-on-value predicates without a schema change.
Completeness — the real-product states. Admin: empty ("No custom fields yet"), the caps reached (50/55 — disabled add + explanatory copy), delete confirm with value count, option archive vs delete split, loading/error. Rail: empty-value placeholder, "Show more fields" expander, per-type editors with inline validation errors, archived-option rendering, viewer read-only, concurrent-edit refresh (the rail's existing optimistic-concurrency pattern). All drawn by 5.3.4/5.3.5, asserted in 5.3.8.
Out of scope (documented extension slots, each justified): the remaining mirror types (paragraph / checkbox-multi / labels / multi-person / formula / time stamp — additive types on the same EAV substrate; labels overlap Story 5.4's taxonomy); work-type field layouts, per-type required flags, and create/edit-form placement (the layout-config admin subsystem — 6.5 composes the settings hub; values are editable the moment an issue exists via the rail); custom fields in the board/list/tree columns (Epic-6 saved-views territory); cross-project/global fields (company-managed Jira, not the team-managed mirror); and the filter UI over values (Story 6.1, which consumes the documented predicate contract).
Verification
- Pull the Story branch,
pnpm install,pnpm prisma migrate dev(applies the 5.3.1 definition/option/value migration cleanly; re-run reports "No difference detected"),pnpm db:seed,pnpm dev. pnpm test:coverage— Vitest (real Postgres) over the definitions + values services (CRUD, caps, per-type validation, option archive/delete rules, permission gates) ≥90% per-file branch/fn/line; new repo methods carry empty-input-guard tests.- Admin flow: sign in as
zhuyue@motir.co/!QAZ1qaz(project admin) → Project settings → Fields (matchingdesign/projects/fields.mock.html). Create one field of each type (Severity/select with 3 options, Customer/text, Effort/number, Go-live/date, Stakeholder/user) → all list in order; rename + reorder a select option; archive an in-use option (stays rendered on issues, gone from new pickers); delete an unused option; delete a field → the confirm names the value count and values vanish. Aseikooc@motir.co(member, not project admin) → the Fields page is read-only/forbidden. - Rail flow: open an issue → defined fields with values render as rail cards below the built-ins (matching
design/work-items/custom-fields.mock.html); empty ones sit behind "Show more fields"; set each type inline (text, number, date via DatePicker, select via Combobox, user via the member picker) → values persist and the revision trail recordscustomFields.<key>diffs; clear a value → the card returns to the empty set; invalid input (bad number, archived option, non-member user) errors inline, nothing persists. - Permissions: a project
viewersees values but no editors; cross-workspace reads 404 (finding #44). - Caps: the 51st field and 56th option are rejected with the explanatory error; the admin UI disables add at the cap.
- Epic-6 seam: the schema doc note exists (typed value columns +
[fieldId, value*]indexes + the JOIN predicate sketch); a raw SQL spot-check filters issues bySeverity = Highusing only the indexes (no JSON parsing). pnpm test:e2e --grep custom-fields— Playwright over the real stack: define → set on an issue → edit → History shows the change → delete field → values gone.- a11y check: the Fields admin page + the rail editors pass the strict axe sweep (labelled controls, keyboard-complete pickers, "Show more fields" as a disclosure, state as text); colour via
--el-*, shape via element shape tokens.