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>
This commit is contained in:
@@ -0,0 +1,168 @@
|
|||||||
|
# Frontend SPA — Milestone 4 (Vocabulary & Authority Management) — Design
|
||||||
|
|
||||||
|
**Date:** 2026-06-04
|
||||||
|
**Status:** Approved (brainstorming) — ready for implementation planning.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Milestones 1–3 (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 master–detail 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.
|
||||||
Reference in New Issue
Block a user