Frontend refactor: unify the duplicated Vocabulary + Authority CRUD surfaces #64

Closed
opened 2026-06-08 13:42:12 +00:00 by logaritmisk · 1 comment
Owner

Severity: Medium (high leverage). From a frontend deep audit, 2026-06-08. The single highest-leverage refactor found.

Problem

The Vocabulary-terms and Authorities CRUD surfaces are two copies of one feature (~280 duplicated lines across 4 files):

  • vocab/term-row.tsx (81) vs authorities/authority-row.tsx (87) are near-identical twins: same useState(editing/labels/uri), same inline-edit <li> (LabelEditor + external-uri <Input> + save/cancel), same display row (flex items-center gap-2 border-b py-1 text-sm + edit button + DeleteConfirmDialog). Differences: the mutation hooks, the id keys, the confirm-delete i18n key.
  • authorities/authorities-page.tsx:46-153 vs vocab/vocabulary-terms.tsx:49-127 share the list+filter+create-form body: identical onCreate/onAdd shape (validate labels.some(l => l.label)setError(true) → mutate with external_uri: uri.trim() || null, labels → reset), identical filter input, identical 4-state list (isError / empty / noMatches / rows), identical create form.
  • The external-URI input trio (<Label> + <Input type="url">) is hand-rolled 4× (vocabulary-terms.tsx:104, authorities-page.tsx:127, term-row.tsx:31, authority-row.tsx:31).

A fix to inline-edit-row behavior must currently be made in two places and silently drifts.

Suggested fix

Extract shared components (suggested in src/components/):

  • LabelledRecordRow{ record, lang, onSave(labels, uri), onDelete(), deleteConfirmKey }; TermRow/AuthorityRow become ~15-line adapters.
  • LabelledRecordCreateFormLabelEditor + external-uri + validation + error display.
  • FilteredRecordList<T> — owns filter state + the four list states.

Authorities then differs only in its hooks + the kind-tabs nav; vocab differs only in its hooks. Folds the external-URI duplication away naturally.

Source: frontend deep audit (architecture dimension), 2026-06-08. Note: the objects table+detail+RHF surface is legitimately more complex and is correctly built differently — the divergence to fix is among the parallel metadata surfaces.

**Severity: Medium (high leverage).** _From a frontend deep audit, 2026-06-08. The single highest-leverage refactor found._ ## Problem The Vocabulary-terms and Authorities CRUD surfaces are two copies of one feature (~280 duplicated lines across 4 files): - **`vocab/term-row.tsx` (81) vs `authorities/authority-row.tsx` (87)** are near-identical twins: same `useState(editing/labels/uri)`, same inline-edit `<li>` (`LabelEditor` + external-uri `<Input>` + save/cancel), same display row (`flex items-center gap-2 border-b py-1 text-sm` + edit button + `DeleteConfirmDialog`). Differences: the mutation hooks, the id keys, the confirm-delete i18n key. - **`authorities/authorities-page.tsx:46-153` vs `vocab/vocabulary-terms.tsx:49-127`** share the list+filter+create-form body: identical `onCreate/onAdd` shape (validate `labels.some(l => l.label)` → `setError(true)` → mutate with `external_uri: uri.trim() || null, labels` → reset), identical filter input, identical 4-state list (`isError` / empty / `noMatches` / rows), identical create form. - The external-URI input trio (`<Label>` + `<Input type="url">`) is hand-rolled 4× (`vocabulary-terms.tsx:104`, `authorities-page.tsx:127`, `term-row.tsx:31`, `authority-row.tsx:31`). A fix to inline-edit-row behavior must currently be made in two places and silently drifts. ## Suggested fix Extract shared components (suggested in `src/components/`): - `LabelledRecordRow` — `{ record, lang, onSave(labels, uri), onDelete(), deleteConfirmKey }`; `TermRow`/`AuthorityRow` become ~15-line adapters. - `LabelledRecordCreateForm` — `LabelEditor` + external-uri + validation + error display. - `FilteredRecordList<T>` — owns filter state + the four list states. Authorities then differs only in its hooks + the kind-tabs nav; vocab differs only in its hooks. Folds the external-URI duplication away naturally. _Source: frontend deep audit (architecture dimension), 2026-06-08. Note: the `objects` table+detail+RHF surface is legitimately more complex and is correctly built differently — the divergence to fix is among the parallel metadata surfaces._
Author
Owner

Done in merge 404cf67. The Vocabulary-terms and Authorities CRUD surfaces now share three components in src/components/:

  • LabelledRecordRow — the inline display/edit row (label + external-URI + edit/delete). TermRow/AuthorityRow are now ~25-line adapters that supply only the mutation hooks, the mutate-arg shape, and the delete-confirm key.
  • LabelledRecordCreateFormLabelEditor + URI + required-label validation + MutationError + submit; owns its own form state, takes a heading/submitLabel/onCreate(labels, uri, reset).
  • FilteredRecordList<T> — filter input + alphabetical sort + the 4 states (loading / error / empty / no-matches), parameterized by loadErrorText/emptyText/renderRow.

The two pages keep only their page chrome (authorities: kind-tabs nav + PageTitle + redirect guard + breadcrumb; vocab: the vocab.terms caption + breadcrumb) and wire the shared pieces.

Behavior-preserving: the 3 existing tests (term-row.test.tsx, authorities.test.tsx, vocabularies.test.tsx) pass byte-identical; each shared component also got focused tests. Net −284 lines across the 4 adapted files (the duplicated row + filter/list/create-form bodies now exist once). One sound boundary fix: the rows normalize external_uri (?? null) since the generated views type it optional but RecordLike requires string | null — observably identical.

262 tests pass; typecheck/lint/build clean; check:size 216.4 KB gz (≈unchanged); check:colors clean; no new dependency; no new i18n keys; no codename.

Out of scope (untouched, as noted): vocabulary-list.tsx (key-based vocabularies) and the objects table/detail/RHF surface.

Done in merge `404cf67`. The Vocabulary-terms and Authorities CRUD surfaces now share three components in `src/components/`: - **`LabelledRecordRow`** — the inline display/edit row (label + external-URI + edit/delete). `TermRow`/`AuthorityRow` are now ~25-line adapters that supply only the mutation hooks, the mutate-arg shape, and the delete-confirm key. - **`LabelledRecordCreateForm`** — `LabelEditor` + URI + required-label validation + `MutationError` + submit; owns its own form state, takes a `heading`/`submitLabel`/`onCreate(labels, uri, reset)`. - **`FilteredRecordList<T>`** — filter input + alphabetical sort + the 4 states (loading / error / empty / no-matches), parameterized by `loadErrorText`/`emptyText`/`renderRow`. The two pages keep only their page chrome (authorities: kind-tabs nav + `PageTitle` + redirect guard + breadcrumb; vocab: the `vocab.terms` caption + breadcrumb) and wire the shared pieces. **Behavior-preserving:** the 3 existing tests (`term-row.test.tsx`, `authorities.test.tsx`, `vocabularies.test.tsx`) pass byte-identical; each shared component also got focused tests. Net **−284 lines** across the 4 adapted files (the duplicated row + filter/list/create-form bodies now exist once). One sound boundary fix: the rows normalize `external_uri` (`?? null`) since the generated views type it optional but `RecordLike` requires `string | null` — observably identical. 262 tests pass; typecheck/lint/build clean; check:size 216.4 KB gz (≈unchanged); check:colors clean; no new dependency; no new i18n keys; no codename. **Out of scope (untouched, as noted):** `vocabulary-list.tsx` (key-based vocabularies) and the `objects` table/detail/RHF surface.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logaritmisk/biggus-dickus#64