Skip to content

feat(test-catalogue): localization and LOINC for panels and sample types - #4080

Merged
mozzy11 merged 4 commits into
DIGI-UW:developfrom
mozzy11:develop
Aug 18, 2026
Merged

feat(test-catalogue): localization and LOINC for panels and sample types#4080
mozzy11 merged 4 commits into
DIGI-UW:developfrom
mozzy11:develop

Conversation

@mozzy11

@mozzy11 mozzy11 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Localization and LOINC for panels and sample types

Eight items in and around the new Test Catalogue Management area. Each was investigated before it was changed; the root causes are below because several were not what they looked like.

1. Legacy Test Management links open legacy pages again

Manage Sample Types had stopped opening the legacy page. Admin.jsx routed /MasterListsPage/SampleTypeManagement to the new editor, which left the legacy page unreachable while its tile still said Manage Sample Types.

The legacy page answers there again (exact), and the new editor moved to /SampleTypeEditor/:sampleTypeId?/:section?, with AdminSideNav updated. I checked every other tile in that menu — PanelManagement, TestSectionManagement, MethodManagement — and only the sample type one had been taken over. A test now pins all of them.

2. Localization on the new Panel and Sample Type editors

Both keep their display name in the same generic localization tables a test does, and both already FK-link to their row there. So this was a missing section, not a missing store.

  • Two bridge endpoints: GET /rest/test-catalog/panels/{id}/localization and GET /rest/sample-types/{id}/localization, returning the same {fields:[{field, localizationId}]} shape the test endpoint returns.
  • LocalizationSection was already entity-agnostic below its copy, so it is shared rather than duplicated: it takes the bridge endpoint and the id.
  • The strings that name what is being translated are per entity, so a sample type is no longer told it is editing “this test’s name and reporting name”, and the field reads Sample Type Name / Panel Name.

Sample-type names loaded from configs/configuration/backend/sample-types land in those same tables, so what the file configured is what the editor shows.

3. LOINC on sample type configuration

example-sample-types.csv gains a loinc column (after sortOrder, before the localization:* block). A code there is recorded as LOINC / SAME_AS — the store the editor's Terminology section reads — so it is visible and editable.

The column is optional and backward compatible: header validation only ever required description and localAbbreviation, so every existing distro file keeps importing. Two deliberate choices:

  • A file without the column says nothing about LOINC rather than asking for an existing mapping to be cleared.
  • A relationship chosen in the editor survives re-import. The file names which code the specimen has, not what it means.

4. Panel LOINC, both directions, plus a migration

panel.loinc predates panel_terminology_mapping, and FHIR intake still routes electronic orders by the column.

Direction Status
Editor → panel.loinc already worked (saveMappingsForPanel)
panel.loinc → editor added (syncLegacyLoinc), wired into the legacy panel create path

Without the second leg, a code entered on the legacy pages was invisible to the editor — and saving there wrote the editor's empty set back over it.

Changeset 084-panel-loinc-terminology-backfill.xml carries existing codes across as SAME_AS, once. It is deliberately one-way: no migration writes panel.loinc, because keeping the two in step afterwards belongs in the service layer. It writes SAME_AS directly rather than repeating the test-side backfill's EQUIVALENT, which needed a second changeset to correct. docs/panel-loinc-terminology-migration.md covers how to run it, how to preview what it touches, and why it is safe to re-run.

5. Empty-state messaging, per entity

With nothing selected, the panels and sample types contexts fell through to the tests branch: standing on the Panels list you were shown the nine test sections, greyed, under “Click a test to edit its sections”. Each entity now greys its own sections with its own message:

  • Test Catalogue Editor — “Click a test to edit its sections”
  • Panel Editor — “Click a panel to edit its sections”
  • Sample Type Editor — “Click a sample type to edit its sections”

Localization appears among the sections for both new entities.

6. Editor names

Test Catalogue Editor, Panel Editor, Sample Type Editor, consistently across headings, navigation, labels and empty states — including two user-facing strings that still called the Panel Editor “Panel Management”, and stale defaultMessages.

The legacy pages keep their own names (“View Test Catalog” and friends). They are a different thing, and item 1 is about leaving legacy navigation alone. That does leave “Catalog” beside “Catalogue” in the app — happy to rename legacy too if that is preferred.

7. Selecting a sample type opens its editor

Was Edit-button only. The handler sits on the data cells rather than the row, so the Edit button in the last cell cannot fire it twice, and the row keeps its tab stop and Enter/Space.

8. The checkbox on a test row — findings

What it represents. Multi-select feeding the “Edit related” batch action, which opens the combined editor over the selected set and is disabled under two selections.

Does it have a valid purpose? Yes — so it is fixed, not removed.

Why it was not independently clickable. A previous attempt already passed onClick={(e) => e.stopPropagation()} to TableSelectRow. That is dead code. Carbon destructures a fixed prop list there:

({ ariaLabel, checked, id, name, onSelect, onChange, disabled, radio, className })

onClick is not among them, so the handler never reaches the DOM and the click bubbled to the row. The box did toggle — the navigation simply took the list away before a second row could be picked, which is why it looked like the checkbox did nothing.

Resolution. The row-opening click moved from TableRow onto the data TableCells, leaving the checkbox cell outside the clickable area. Selection, batch actions and keyboard access are unchanged, and a regression test asserts a checkbox click neither navigates nor fails to toggle.

Verification

37 new tests, every one inversion-verified (reverted the fix, watched the test fail, restored):

Area Tests
Panel LOINC sync + backfill (changeset SQL run verbatim) 10
Sample type LOINC, incl. CSV import end-to-end 10
Naming, section registries, empty-state wording 8
Legacy route wiring, all tiles 4
Sample type row click + keyboard 2
Sidenav per-entity greyed sections 2
Checkbox selects without navigating 1
  • Frontend: 1131 passing, 166 files
  • Backend: 5028 run, 0 failures. One error, ResultValidationServiceTest.testPersistdata_shouldCreateNewNote — a note-id constraint collision that passes in isolation, is unrelated to anything here, and has recurred across unrelated branches this week.
  • Spotless and Prettier clean.

Live, against a deployed build — 18 checks, 0 failures, no JS errors:

item 1  legacy Manage Sample Types opens the legacy page
item 6  new editor titled "Sample Type Editor"; area "Test Catalogue Management"
item 5  "Click a sample type to edit its sections" + 6 greyed sample type sections
item 5  "Click a panel to edit its sections" + 4 greyed panel sections
item 2  Localization listed and reachable for both entities, with a locale
        picker, the entity's own wording, and Save
item 7  row click → /SampleTypeEditor/4/basic-info
item 8  checkbox click stays on the list and toggles; a data cell still opens
        /TestCatalogEditor/380/basic-info
item 4  changeset EXECUTED; PanelB's legacy 222 now reads
        LOINC / 222 / SAME_AS through /panels/31/terminology

🤖 Generated with Claude Code

Eight things, in the new Test Catalogue Management area and around it.

Legacy navigation
-----------------
Manage Sample Types under the legacy Test Management menu had stopped
opening the legacy page. Admin routed /SampleTypeManagement to the new
editor, which left the legacy page unreachable while its tile still said
Manage Sample Types. The legacy page answers there again and the new
editor moved to /SampleTypeEditor. The other legacy tiles were checked;
only this one had been taken over.

Localization for panels and sample types
----------------------------------------
Both keep their display name in the same generic localization tables a
test does, and each already FK-links to its row there, so the editors were
missing a section rather than a store. Two endpoints bridge an id to the
backing localization id, and the section a test uses is now shared:
entity-agnostic already, so it takes the endpoint and the id rather than
being duplicated twice. The strings that name what is being translated are
entity-specific, so a sample type is no longer told it is editing "this
test's name and reporting name".

Names loaded from a sample-types configuration file land in those same
tables, so what the file configured is what the editor shows.

LOINC on sample types
---------------------
A sample-types configuration file can carry a loinc column, recorded as a
LOINC / SAME_AS terminology mapping — the store the editor reads, so a
configured code is visible and editable there. The column is optional: a
file without it says nothing about LOINC rather than asking for an
existing mapping to be cleared, and a relationship chosen in the editor
survives re-import, because the file names which code, not what it means.

Panel LOINC in both directions
------------------------------
panel.loinc predates panel_terminology_mapping and FHIR intake still
routes electronic orders by the column. Editor to column already worked;
column to editor did not, so a code entered on the legacy pages was
invisible to the editor and saving there would write its empty set back
over it. syncLegacyLoinc closes that leg, and changeset 084 carries
existing codes across as SAME_AS once. Keeping the two in step from then
on is the service layer's job, not another migration's — there is no
migration writing panel.loinc. Documented in
docs/panel-loinc-terminology-migration.md.

Empty states, per entity
------------------------
With nothing selected, the panels and sample types contexts fell through
to the tests branch: a reader on the Panels list was shown the nine test
sections, greyed, under "Click a test to edit its sections". Each entity
now greys its own sections and says so.

Names
-----
Test Catalogue Editor, Panel Editor, Sample Type Editor, consistently,
including two user-facing strings that still called the Panel Editor
"Panel Management". The legacy pages keep their own names — they are a
different thing and item 1 is about leaving them alone.

Selecting a sample type
-----------------------
Clicking a row opens it, as it does for tests and panels, rather than
requiring the Edit button. The handler sits on the data cells so the Edit
button cannot fire it twice, and the row keeps its tab stop and Enter.

The checkbox on a test row
--------------------------
It selects a row for the "Edit related" batch action, which needs two or
more tests — a real purpose, so it is fixed rather than removed. It was
not independently clickable because the onClick that tried to stop
propagation on TableSelectRow never reached the DOM: Carbon destructures a
fixed set of props there and onClick is not among them. The box did
toggle; the navigation took the list away before a second row could be
picked. The row-opening click moved to the data cells, leaving the
checkbox cell outside it — selection, batch actions and keyboard access
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mozzy11 and others added 3 commits August 18, 2026 12:29
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rename Existing Sample Types opened an empty page, and Rename Existing
Result List Options white-screened. Two unrelated causes, both small.

The sample type screen had no route. Its tile, its component and its REST
controller all existed; nothing in Admin's Switch matched
/MasterListsPage/SampleTypeRenameEntry, so the link resolved to the admin
shell with nothing inside it. Every other rename tile in that menu had one.

The result list screen died on a null option. Its list is built from
test_result rows rather than from the dictionary, so it can name a
dictionary entry that is no longer there, and getAllSelectListOptions added
whatever getDictionaryById returned. Those nulls reached the browser and
the first one took the page down, however many valid options followed it.
Two showed up on a dev database: a row with a blank value, and one naming a
dictionary id with no row behind it. The same class's getTestSelectDictionary
already skips ids that do not resolve; this one had been missed.

Verified against a deployed build: the endpoint went from 59 entries with
two nulls to 57 with none, both screens list and open their items, and a
rename saves and can be renamed back.

The route test now derives the rename tiles from the menu, so a tile added
without a route fails there rather than in someone's browser.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…itor

Renaming a result list option replaced its French name with the English
text. The option list carries one name per option — whichever locale it was
read in — so the screen had nothing else to send, and it sent what it was
displaying as every language. The French field had been commented out,
which hid the effect without stopping it.

EntityNamesProvider now answers for a result select option, the way it
already did for panels, sample types, test sections, units and methods, so
the screen can read what each language currently says. It prefills a field
per language from that and sends back exactly what is in them. A blank
field means that language is not being renamed, so it is left as it stands
rather than stored empty — otherwise editing one language would still clear
the other, just in the opposite direction.

An option whose name had never been translated also could not be renamed at
all: renameOption built a localization for it and saved it without ever
attaching it to the dictionary row, so the row was an orphan and the
displayed name never changed. Its French field now starts empty rather than
seeded with the English text, so saving cannot invent a translation.

Verified against a deployed build, reading the database directly: renaming
only the English leaves the French intact, renaming only the French leaves
the English intact, and a French name can be added where there was none.

Also, the panel entity was still called "Panels" while its two peers read as
editors. One key drives the SideNav item, the list heading and both
breadcrumb trails, so it now says Panel Editor in all four. The Test
Catalogue Editor's own Panels section keeps its name — it lists the panels a
test belongs to, and is not the editor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mozzy11
mozzy11 merged commit 4e267df into DIGI-UW:develop Aug 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant