Files
biggus-dickus/docs/superpowers/specs/2026-06-04-frontend-spa-milestone-4-design.md
logaritmisk 684b5449ca docs(spec): frontend SPA milestone 4 (vocabulary & authority management) design
Two-pane vocab (list/create + terms/add) + kind-tabbed authorities
(list/create); shared sv/en LabelEditor; create+list only (no backend
edit/delete yet); 4 new hooks; enables the nav stubs; Vitest+RTL+MSW.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:00:02 +02:00

169 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend SPA — Milestone 4 (Vocabulary & Authority Management) — Design
**Date:** 2026-06-04
**Status:** Approved (brainstorming) — ready for implementation planning.
## Context
Milestones 13 (merged to `main` at `7a8e7ff`) delivered the SPA foundation, object
read/authoring, and the publishing workflow. The app shell's nav has **Vocabularies**
and **Authorities** items rendered as disabled stubs. Milestone 4 enables them: managing
the controlled vocabularies (and their terms) and the authority records that catalogue
fields reference.
Pure frontend — the admin endpoints already exist (built in the backend admin-CRUD
phase): `GET/POST /api/admin/vocabularies`, `GET/POST /api/admin/vocabularies/{id}/terms`,
`GET/POST /api/admin/authorities?kind=`.
Milestone roadmap: M1 foundation → M2 authoring → M3 publish → **M4 vocab/authority
(this)** → M5 search.
## Decisions (settled during brainstorming)
- **One milestone for both surfaces** (vocabularies+terms and authorities), sharing a
`LabelEditor` and a create-form pattern.
- **Two-pane masterdetail layout** (consistent with the Objects inspector): the
Vocabularies screen is vocab-list-left / terms-right; Authorities is kind-tabs + list.
- **Create + list only.** The backend exposes only create and list for vocabularies,
terms, and authorities — no update/delete — so M4 is create + list. Editing/deleting
reference data is a later milestone (needs backend endpoints first).
- **Fixed sv/en `LabelEditor`** (not arbitrary languages), matching the app's sv/en MVP
scope and M2's `localized_text` field; produces `LabelInput[]` of non-empty langs.
- **EN label required, SV optional** (canonical English), consistent with M2.
## Backend contract (already shipped — verify against `web/src/api/schema.d.ts`)
- `GET /api/admin/vocabularies``VocabularyView[]` (`{ id, key }`).
- `POST /api/admin/vocabularies` body `NewVocabularyRequest { key }``201 VocabularyView`.
- `GET /api/admin/vocabularies/{id}/terms``TermView[]` (`{ id, external_uri?, labels }`).
- `POST /api/admin/vocabularies/{id}/terms` body `NewTermRequest { external_uri?, labels }`
`201 CreatedId`.
- `GET /api/admin/authorities?kind=person|organisation|place``AuthorityView[]`
(`{ id, kind, external_uri?, labels }`).
- `POST /api/admin/authorities` body `NewAuthorityRequest { kind, external_uri?, labels }`
`201 CreatedId`.
- `LabelInput` / `LabelView` = `{ lang, label }`.
(Existing hooks from M2: `useTerms(vocabularyId)`, `useAuthorities(kind)`.)
## Scope (YAGNI)
**In:** Vocabularies screen (list + create vocabulary; per-vocab terms list + add term);
Authorities screen (kind-tabbed list + create authority); shared `LabelEditor` (sv/en);
4 new hooks; the two nav stubs enabled; client validation; list invalidation on create.
**Out:** update/delete of vocab/term/authority (no backend endpoints — later milestone);
audit of vocab/authority creation (backend follow-up #21); searchable pickers (#27);
search UI (M5); per-language beyond sv/en.
## Architecture
### Routes & navigation
Enable the `Vocabularies` and `Authorities` nav items in `app-shell.tsx` (currently
disabled buttons → active `NavLink`s). Routes under the protected `AppShell`, two-pane
via nested `<Outlet/>` like Objects:
```
/vocabularies → VocabulariesPage (list + create left; <Outlet/> right)
index → "select a vocabulary" prompt
:id → VocabularyTerms (the vocab's terms + add-term form)
/authorities → redirect to /authorities/person
/authorities/:kind → AuthoritiesPage (kind tabs + list + create), kind ∈ person|organisation|place
```
`/authorities/:kind` validates the kind param (unknown → redirect to `person`).
### Components / files
```
web/src/vocab/
vocabularies-page.tsx two-pane: VocabularyList (+ create) left, <Outlet/> right
vocabulary-list.tsx useVocabularies list + NewVocabularyForm
vocabulary-terms.tsx (:id) useTerms list + AddTermForm
web/src/authorities/
authorities-page.tsx kind tabs + AuthorityList(kind) + NewAuthorityForm(kind)
web/src/components/
label-editor.tsx shared sv/en label editor (RHF-controlled), -> LabelInput[]
web/src/api/queries.ts + useVocabularies, useCreateVocabulary, useAddTerm, useCreateAuthority
web/src/app.tsx + the routes above
web/src/shell/app-shell.tsx enable the Vocabularies + Authorities nav links
web/src/i18n/{en,sv}.json + vocab.* / authorities.* keys
```
Keep each page focused; the create forms (`NewVocabularyForm`, `AddTermForm`,
`NewAuthorityForm`) are small and may live in their page files or as siblings — the
shared piece that must be its own unit is `LabelEditor`.
### `LabelEditor`
A controlled editor rendering an **English** input and a **Swedish** input. Given/produces
`LabelInput[]` (`{ lang, label }`). On change it emits the array with only the non-empty
langs (so an empty SV is omitted). Used by `AddTermForm` and `NewAuthorityForm`.
Validation: the EN label is required (the parent form wires `required` on the EN field);
SV optional. (Mirrors M2's `localized_text` handling and the existing detail/edit label
rendering.)
### Data layer (new hooks in `queries.ts`)
- `useVocabularies()``GET /api/admin/vocabularies``VocabularyView[]`.
- `useCreateVocabulary()``POST /api/admin/vocabularies` `{ key }`; invalidate
`["vocabularies"]`.
- `useAddTerm()``POST /api/admin/vocabularies/{id}/terms` `{ external_uri?, labels }`;
invalidate `["terms", vocabularyId]`.
- `useCreateAuthority()``POST /api/admin/authorities` `{ kind, external_uri?, labels }`;
invalidate `["authorities", kind]`.
(`useTerms`/`useAuthorities` already use keys `["terms", vocabularyId]` /
`["authorities", kind]`; the mutations invalidate those exact keys.)
### Data flow
- **Create vocabulary:** form (`key`) → `useCreateVocabulary` → invalidate list; clear form.
- **Add term:** form (sv/en labels + optional uri) on `/vocabularies/:id`
`useAddTerm({ id, labels, external_uri })` → invalidate `["terms", id]`; clear form.
- **Create authority:** form on `/authorities/:kind` (labels + optional uri) →
`useCreateAuthority({ kind, labels, external_uri })` → invalidate `["authorities", kind]`;
clear form.
## Error handling
Create failures → a form-level error (reuse `form.rejected`). Lists show loading /
empty / error states (reuse the M1 list-state patterns). Required validation (vocab key;
EN label) blocks submit with inline messages. Unknown authority kind in the route →
redirect to `person`.
## Testing (Vitest + RTL + MSW)
- `LabelEditor` — entering EN+SV produces `[{lang:"en",...},{lang:"sv",...}]`; empty SV
omitted.
- Vocabularies: list renders; create a vocabulary → POST `{key}` (assert body) → list
invalidated/refetched shows it; selecting a vocab shows its terms; add a term →
POST with the labels body (assert) → terms refetch.
- Authorities: kind tabs switch the list (`?kind=`); create an authority for the active
kind → POST `{kind, labels}` (assert) → list refetch; required EN label blocks submit.
- Nav: the Vocabularies + Authorities nav items are enabled links (not disabled).
- New MSW handlers: `POST /api/admin/vocabularies`, `POST /api/admin/vocabularies/:id/terms`,
`POST /api/admin/authorities` (the GET handlers + the existing `?kind=` filter handler
are already present from M2).
## Acceptance criteria (Milestone 4 "done")
1. The Vocabularies and Authorities nav items are enabled and route to their screens.
2. A vocabulary can be created (key) and appears in the list; selecting it shows its
terms; a term can be added with sv/en labels (+ optional URI) and appears.
3. Authorities can be filtered by kind via tabs; an authority can be created for the
active kind with sv/en labels and appears in that kind's list.
4. The shared `LabelEditor` produces `LabelInput[]` with only non-empty langs; EN is
required.
5. Create failures surface a form-level error; lists have loading/empty/error states.
6. Web CI green (typecheck, lint, tests, build, bundle ≤150 KB gz).
## Out of scope / follow-ups
- Edit/delete of vocabularies, terms, authorities — needs backend endpoints first
(file a backend follow-up when this milestone lands).
- Audit of vocab/term/authority creation (#21).
- Searchable pickers / large-vocabulary handling (#27).
- Arbitrary (non sv/en) label languages.