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.
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._
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.