Frontend refactor: unify the duplicated Vocabulary + Authority CRUD surfaces #64
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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) vsauthorities/authority-row.tsx(87) are near-identical twins: sameuseState(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-153vsvocab/vocabulary-terms.tsx:49-127share the list+filter+create-form body: identicalonCreate/onAddshape (validatelabels.some(l => l.label)→setError(true)→ mutate withexternal_uri: uri.trim() || null, labels→ reset), identical filter input, identical 4-state list (isError/ empty /noMatches/ rows), identical create form.<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/AuthorityRowbecome ~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
objectstable+detail+RHF surface is legitimately more complex and is correctly built differently — the divergence to fix is among the parallel metadata surfaces.Done in merge
404cf67. The Vocabulary-terms and Authorities CRUD surfaces now share three components insrc/components/:LabelledRecordRow— the inline display/edit row (label + external-URI + edit/delete).TermRow/AuthorityRoware 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 aheading/submitLabel/onCreate(labels, uri, reset).FilteredRecordList<T>— filter input + alphabetical sort + the 4 states (loading / error / empty / no-matches), parameterized byloadErrorText/emptyText/renderRow.The two pages keep only their page chrome (authorities: kind-tabs nav +
PageTitle+ redirect guard + breadcrumb; vocab: thevocab.termscaption + 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 normalizeexternal_uri(?? null) since the generated views type it optional butRecordLikerequiresstring | 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 theobjectstable/detail/RHF surface.