Skip to content

Radiology/image link column in the Patient List - #2939

Merged
khairul-syazwan merged 4 commits into
developfrom
khairul-syazwan/analyze-2849
Jul 29, 2026
Merged

Radiology/image link column in the Patient List#2939
khairul-syazwan merged 4 commits into
developfrom
khairul-syazwan/analyze-2849

Conversation

@khairul-syazwan

@khairul-syazwan khairul-syazwan commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator
Screen Recording 2026-07-27 at 9 34 19 AM Screenshot 2026-07-28 at 9 14 38 AM Screenshot 2026-07-27 at 9 34 54 AM image

Summary

Adds radiology / image data support to the Patient List (issue #2849). Researchers can now see imaging attributes (Study name, Modality, Date) alongside a patient and click straight through to an image viewer — without leaving the analytics workflow.

Two capabilities, both generic and reusable:

  1. Surface OMOP image_occurrence in the Patient List via a new @IMG query placeholder — so an "imaging"/"Radiology" interaction can be configured with no query-engine code change.
  2. Render link/URL columns — a patient-list cell whose value is a valid http(s) URL becomes a safe, new-tab link (with an external-link icon) instead of raw text. The radiology "Action" column uses this to open the viewer.

The PR is the plumbing. Getting DICOM into image_occurrence/Orthanc (ETL) and deploying a viewer are separate tracks; see Out of scope and Manual setup.


Architecture & how it works

flowchart TD
    C[(OMOP image_occurrence)] --> E["PA config: @IMG maps to a<br/>Radiology interaction<br/>Study / Modality / Date / Action"]
    E --> F["query-gen builds SQL<br/>joins patient + concept (modality)"]
    F --> G["result rows include a viewer URL"]
    G --> H{"Cell value is a<br/>valid http/https URL?"}
    H -->|yes| I["render link (new tab,<br/>rel=noopener) + icon"]
    H -->|no| J["render plain text"]
    I -->|click| K["image viewer opens the study"]

    classDef pr fill:#7c3aed,stroke:#a78bfa,color:#fff;
    classDef ctx fill:#3f3f46,stroke:#71717a,color:#e4e4e7;
    class E,F,G,H,I,J pr;
    class C,K ctx;
Loading

Purple = changed by this PR. Grey = surrounding context (data + viewer).

Backend — why no query-engine change is needed. The SQL generator is table-agnostic: it resolves interaction placeholders (@COND, @VISIT, …) against a per-dataset mapping. This PR adds @IMGimage_occurrence (joined to person via person_id), mirroring @COND exactly (8 keys). Modality resolves modality_concept_idconcept.concept_name through the existing @REF vocabulary-join pattern.

Frontend — the link cell. Each cell asks "is my value a valid http/https URL?" (isValidHttpUrl). If yes → an anchor opening in a new tab (rel="noopener noreferrer") with a config-driven label (falls back to a generic "Open"). If no → plain text, exactly as before. Dangerous schemes (javascript:, data:) are rejected and shown as text.


Changes (11 files, 2 commits)

Backend — surface image_occurrence

File Change
query-gen-svc/…/qe/settings/OldSettings.ts Add @IMG placeholder→table mapping (default).
analytics-svc/…/qe/settings/OldSettings.ts Same mapping (the two services keep identical maps).
mri-pa-config/…/config/configDefinition.ts Allow an optional patientlist.link { label } in the config schema.

Frontend — render URL/link columns

File Change
vue-mri-ui-lib/…/utils/urlUtils.ts (new) isValidHttpUrl() — http/https only; rejects empty/non-string/javascript:/data:/malformed.
vue-mri-ui-lib/…/components/PatientListLinkData.vue (new) The link cell: valid URL → anchor (new tab + icon); else the raw value as text.
vue-mri-ui-lib/…/components/icons/ExternalLinkIcon.vue (new) The ↗ "opens in new tab" icon.
vue-mri-ui-lib/…/components/PatientListControl.vue Per-cell dispatch (link vs. text) by value; carries the config link label into the column.
vue-mri-ui-lib/…/lib/MriConfigAttribute.ts getPatientListLink() accessor for the config link label.
…/__tests__/*.test.ts (3 new) Unit tests (14 total).

Design decisions

  • Detect links by value, not just by config flag. Robust even if the config's link flag doesn't survive the strict config schema. Rendering a URL as a safe new-tab link is generic, useful behavior.
  • The viewer URL is data, not hardcoded. The frontend renders whatever URL the row carries. Which viewer and how the URL is formed (OMOP wadors_uri vs. constructing an OHIF URL from the study UID) is a deployment decision — the UI is viewer-agnostic.

Manual setup for existing datasets (config)

New datasets can have this generated from the data model; existing datasets need a one-time, targeted config patch (via the config service save/activate, not raw SQL). Add the following to the dataset's config:

1. CDW config — @IMG mapping in advancedSettings.tableMapping (mirror the existing @COND quoting for your dialect):

"@IMG": "$$SCHEMA$$.\"image_occurrence\"",
"@IMG.PATIENT_ID": "\"person_id\"",
"@IMG.INTERACTION_ID": "\"image_occurrence_id\"",
"@IMG.CONDITION_ID": "\"image_occurrence_id\"",
"@IMG.PARENT_INTERACT_ID": "\"image_occurrence_id\"",
"@IMG.START": "\"image_occurrence_date\"",
"@IMG.END": "\"image_occurrence_date\"",
"@IMG.INTERACTION_TYPE": "\"modality_concept_id\""

2. ⚠️ Register @IMG as a dim placeholder in advancedSettings.tableTypePlaceholderMap.dimTables (without this, query-gen returns a 500):

{ "placeholder": "@IMG", "attributeTables": [], "hierarchy": true, "time": true, "oneToN": false, "condition": false }

3. Add the imageoccurrence interaction (patient.interactions.imageoccurrence, defaultPlaceholder: "@IMG") with attributes: studyname (@IMG."IMAGE_STUDY_UID"), modality (@REF.concept_name, filter @REF.concept_id = @IMG.MODALITY_CONCEPT_ID), imagedate (@IMG."IMAGE_OCCURRENCE_DATE", type time), action (@IMG."WADORS_URI" or a constructed viewer URL), and pid (@IMG.person_id) — required for row grouping.

4. Frontend config — a matching patient.interactions.imageoccurrence filtercard with each attribute patientlist.visible: true, and the action attribute carrying patientlist.link: { label: "OHIF viewer" }.

5. Applysave + activate via mri-pa-config config.xsjs (needs mri-admin), then add the columns from the patient list's column menu (or add them to chartOptions.list.initialColumns to auto-show).

Gotchas

Symptom Cause
500 when adding columns @IMG missing from tableTypePlaceholderMap.dimTables (step 2)
Imaging rows don't attach to patients missing pid attribute (step 3)
image_occurrence does not exist at query time dataset uses a cache that lacks the table → refresh the cache
Modality shows "No matching concept" DICOM vocabulary not loaded

- Add @img placeholder->table mapping (image_occurrence joined to person via
  person_id) in query-gen-svc and analytics-svc, so image_occurrence can be
  configured as a patient-list interaction with no query-engine code change.
- Allow an optional patientlist.link { label } in the PA config schema.
Patient-list cells whose value is a valid http(s) URL render as a new-tab link
(rel=noopener noreferrer) with an external-link icon; other values render as
plain text. Enables the radiology 'Action' column to open an image viewer.

- urlUtils.isValidHttpUrl: http/https-only URL validation
- PatientListLinkData.vue + ExternalLinkIcon.vue: the link cell
- PatientListControl: per-cell dispatch (link vs text) by value
- MriConfigAttribute: getPatientListLink/isPatientListLink for the config label

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds radiology/image support to the Patient List by enabling an @IMG placeholder for OMOP image_occurrence in backend default mappings and introducing a reusable frontend “link cell” renderer that safely turns valid http(s) values into new-tab links.

Changes:

  • Backend: add @IMG placeholder → image_occurrence default table mappings in both query-gen-svc and analytics-svc.
  • Frontend: add URL validation utility + new patient-list link cell component (with external-link icon) and wire it into patient-list rendering.
  • Config + tests: extend config schema for patientlist.link.label and add unit tests for URL validation, config accessor, and link cell rendering.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
plugins/ui/apps/vue-mri-ui-lib/src/utils/urlUtils.ts Adds isValidHttpUrl helper for safe http/https detection.
plugins/ui/apps/vue-mri-ui-lib/src/utils/tests/urlUtils.test.ts Unit tests for URL validation edge cases.
plugins/ui/apps/vue-mri-ui-lib/src/lib/MriConfigAttribute.ts Adds accessor for optional patientlist.link config.
plugins/ui/apps/vue-mri-ui-lib/src/lib/tests/MriConfigAttribute.test.ts Tests for the new link-config accessor.
plugins/ui/apps/vue-mri-ui-lib/src/components/PatientListLinkData.vue New patient-list cell renderer that displays safe new-tab links.
plugins/ui/apps/vue-mri-ui-lib/src/components/PatientListControl.vue Dispatches between text vs. link cell rendering and carries link label into column meta.
plugins/ui/apps/vue-mri-ui-lib/src/components/icons/ExternalLinkIcon.vue Adds external-link icon used by link cells.
plugins/ui/apps/vue-mri-ui-lib/src/components/tests/PatientListLinkData.test.ts Tests link rendering, label fallback, and unsafe/missing value behavior.
plugins/functions/query-gen-svc/src/qe/settings/OldSettings.ts Adds default @IMG placeholder mapping to image_occurrence.
plugins/functions/mri-pa-config/src/config/configDefinition.ts Extends config schema to allow optional patientlist.link.label.
plugins/functions/analytics-svc/src/qe/settings/OldSettings.ts Mirrors the @IMG default mapping addition.

Comment thread plugins/ui/apps/vue-mri-ui-lib/src/utils/urlUtils.ts
@khairul-syazwan khairul-syazwan changed the title feat: radiology/image link column in the Patient List (#2849) Radiology/image link column in the Patient List (#2849) Jul 23, 2026
@khairul-syazwan khairul-syazwan changed the title Radiology/image link column in the Patient List (#2849) Radiology/image link column in the Patient List Jul 23, 2026
@khairul-syazwan
khairul-syazwan added this pull request to the merge queue Jul 28, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 28, 2026
@khairul-syazwan
khairul-syazwan added this pull request to the merge queue Jul 29, 2026
Merged via the queue into develop with commit 5661e8b Jul 29, 2026
72 of 73 checks passed
@khairul-syazwan
khairul-syazwan deleted the khairul-syazwan/analyze-2849 branch July 29, 2026 04:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants