diff --git a/docs/superpowers/specs/2026-06-04-frontend-spa-milestone-2-design.md b/docs/superpowers/specs/2026-06-04-frontend-spa-milestone-2-design.md new file mode 100644 index 0000000..86862f5 --- /dev/null +++ b/docs/superpowers/specs/2026-06-04-frontend-spa-milestone-2-design.md @@ -0,0 +1,197 @@ +# Frontend SPA — Milestone 2 (Object Authoring) — Design + +**Date:** 2026-06-04 +**Status:** Approved (brainstorming) — ready for implementation planning. + +## Context + +Milestone 1 (merged to `main` at `0a2398f`) delivered the SPA foundation: typed client, +TanStack Query hooks, app shell, sv/en i18n, login/session guard, and a read-only +two-pane Objects screen (paginated list + detail). Milestone 2 adds **authoring** — +create, edit, and delete catalogue objects, including the **dynamic flexible-field +form** driven by the field-definition registry. + +This is **pure frontend**: every endpoint already exists on the admin surface +(`POST/PUT/DELETE /api/admin/objects`, `PUT /api/admin/objects/{id}/fields`, +`GET /api/admin/field-definitions`, `GET /api/admin/vocabularies/{id}/terms`, +`GET /api/admin/authorities?kind=`). + +Milestone roadmap (from M1): M2 authoring (this) → M3 publish workflow → M4 +vocabulary/authority management → M5 search. + +## Decisions (settled during brainstorming) + +- **Create/edit flow shapes:** **edit in-place** in the right pane (`/objects/:id/edit`, + keeps the inspector + list context); **new** as a **full-width route** (`/objects/new`, + room for the full field set). +- **Reference fields (term/authority):** plain `` | string | +| `integer` | `` | number | +| `date` | `` | `YYYY-MM-DD` string | +| `boolean` | `` | boolean | +| `localized_text` | sv + en ``s | `{ sv?, en? }` (omit empty langs) | +| `term` | `` of `useAuthorities(definition.authority_kind)` | authority id (string) | + +Option labels render in the active locale (fall back to English, then the raw key — +same rule as M1's detail view). Fields render **grouped by `definition.group`** (a +group heading per non-empty group; ungrouped fields under no heading), preserving the +field-definitions API order within each group. `definition.required` drives a +client-side required rule. + +### Data flow + +- **Create** (`ObjectNewPage`): `useCreateObject` → `POST /objects` (core + visibility) → + `{id}`; if any flexible values are set, `useSetFields(id, values)` → `PUT + /objects/:id/fields`; invalidate `["objects"]`; navigate `/objects/:id`. +- **Edit** (`ObjectEditForm`): `useUpdateObject(id)` → `PUT /objects/:id` (core); + `useSetFields(id, values)` → `PUT /objects/:id/fields` (replace); invalidate + `["object", id]` + `["objects"]`; navigate `/objects/:id`. +- **Delete** (`DeleteObjectDialog`): `useDeleteObject(id)` → `DELETE /objects/:id`; + invalidate `["objects"]`; navigate `/objects`. + +Flexible-field submission uses **replace semantics**: the form sends the complete +desired field map. Cleared optional fields are omitted (removed); set fields are +included with their current value. + +### Error handling + +- **Client validation** (RHF): required fields present; integer is numeric; date is a + valid `YYYY-MM-DD`. Blocks submit with inline messages. Prevents most server 422s. +- **Server `422`** (bad date on create; `set_fields` unknown/type/unresolved — bare): + surface a **form-level alert** ("The server rejected the changes — check the + highlighted and referenced fields"). The bare `set_fields` 422 carries no per-field + detail, so client validation is the primary guard. +- **Partial create** (create `POST` succeeds, fields `PUT` fails): the core record now + exists as Draft. Rather than lose it, navigate to `/objects/:id/edit` with an error + banner so the user can retry the field values. (Documented behavior, tested.) +- **Edit a since-deleted object** (`404`): show the not-found state. +- Visibility is never sent on edit; `new` offers only Draft/Internal (never Public). + +### Testing (Vitest + RTL + MSW) + +- `FieldInput`: renders the correct control for each `data_type`; term/authority + selects populate options from MSW handlers (labels in active locale; value = id). +- **New flow**: fill core + one flexible field → submit → asserts `POST /objects` then + `PUT /objects/:id/fields` were called (MSW) → navigates to the detail route. +- **Edit flow**: form pre-fills from the loaded object → change a field → save → + `PUT /objects/:id` + `PUT .../fields` → returns to detail. +- **Delete**: confirm dialog → `DELETE` → navigates to the list. +- **Validation**: a required field left empty blocks submit and shows an error. +- **Visibility**: the `new` form's visibility select offers only Draft and Internal. +- **Partial create**: create OK but fields PUT 422 → lands on `/objects/:id/edit` with + an error banner. +- New MSW handlers: terms, authorities, `POST/PUT/DELETE /objects`, `PUT .../fields`. + +## Acceptance criteria (Milestone 2 "done") + +1. From the list, "New object" opens the full-width form; filling core + flexible fields + and submitting creates the object (Draft/Internal) and lands on its detail view. +2. From a record's detail, "Edit" opens the in-pane form pre-filled with current core + + flexible values; saving persists both and returns to the detail view. +3. All seven field types render and round-trip (incl. term/authority selects and sv/en + localized text). +4. Required fields are enforced client-side; the `new` form cannot set Public. +5. "Delete" confirms, deletes, and returns to the list; the list no longer shows it. +6. Partial-create failure lands on the edit page with the core record preserved. +7. Web CI green (typecheck, lint, tests, build, bundle ≤150 KB gz). + +## Out of scope / follow-ups + +- Searchable combobox + server-side term search for large vocabularies (later). +- Publish/visibility transitions (M3). +- Surfacing per-field server errors would require the backend `set_fields` 422 to carry + field detail (currently bare — see the admin 422 bodies note in issue #16's follow-up). +- `fields` map typing still relies on the `Record` cast pending + issue #24 (open-map OpenAPI typing).