From df40318bc421424746f511e96654da8e433ba344 Mon Sep 17 00:00:00 2001 From: Alexander Kiel Date: Mon, 7 Apr 2025 20:00:11 +0200 Subject: [PATCH] Add Terminology Operations UI Closes: #2544 --- .github/value-set-expand/upload.sh | 3 +- .github/workflows/build.yml | 7 +- modules/frontend-e2e/Makefile | 9 +- modules/frontend-e2e/docker-compose.yml | 5 +- modules/frontend-e2e/package-lock.json | 28 +- modules/frontend-e2e/package.json | 4 + modules/frontend-e2e/src/code-system.spec.ts | 99 ++++ modules/frontend-e2e/src/fhir.spec.ts | 12 +- modules/frontend-e2e/upload.sh | 9 +- modules/frontend/src/app.html | 1 - modules/frontend/src/lib/breadcrumb.svelte | 15 + .../frontend/src/lib/breadcrumb/entry.svelte | 4 +- .../src/lib/breadcrumb/history.svelte | 16 - .../lib/breadcrumb/resource-history.svelte | 22 + .../src/lib/breadcrumb/resource.svelte | 26 +- .../src/lib/breadcrumb/type-history.svelte | 22 + .../frontend/src/lib/breadcrumb/type.svelte | 18 +- modules/frontend/src/lib/error-card.svelte | 4 +- modules/frontend/src/lib/fhir.ts | 13 +- .../src/lib/history/entry-card.svelte | 2 +- modules/frontend/src/lib/metadata.ts | 2 +- modules/frontend/src/lib/resource.ts | 19 + .../src/lib/resource/json/array.svelte | 7 +- .../src/lib/resource/json/object.svelte | 5 +- .../src/lib/resource/json/property.svelte | 5 +- .../src/lib/resource/json/value.svelte | 18 +- .../frontend/src/lib/resource/property.svelte | 12 +- .../src/lib/resource/resource-card.svelte | 18 +- .../src/lib/resource/resource-card.test.ts | 302 ++++++----- .../src/lib/resource/resource-card.ts | 474 ++++++++++++------ modules/frontend/src/lib/tab-item.svelte | 2 +- .../description/left-aligned/list.svelte | 10 +- .../description/left-aligned/row-3-2.svelte | 6 +- .../description/left-aligned/row-5-4.svelte | 6 +- .../frontend/src/lib/tailwind/dropdown.svelte | 67 +++ .../src/lib/tailwind/dropdown/item.svelte | 17 + modules/frontend/src/lib/tailwind/form.svelte | 23 + .../lib/tailwind/form/button-submit.svelte | 14 + .../src/lib/tailwind/form/check-box.svelte | 46 ++ .../src/lib/tailwind/form/check-boxes.svelte | 22 + .../src/lib/tailwind/form/section.svelte | 24 + .../src/lib/tailwind/form/text-field.svelte | 22 + .../src/lib/tailwind/logo-card/card.svelte | 6 +- .../src/lib/tailwind/logo-card/row.svelte | 4 +- .../src/lib/tailwind/simple-logo-card.svelte | 4 +- .../src/lib/tailwind/stats/simple.svelte | 4 +- .../src/lib/tailwind/table/table.svelte | 8 +- modules/frontend/src/lib/total-card.svelte | 3 +- modules/frontend/src/lib/util.test.ts | 2 +- modules/frontend/src/lib/util.ts | 9 + .../frontend/src/lib/values/address.svelte | 2 +- .../src/lib/values/contact-point.svelte | 2 +- .../frontend/src/lib/values/identifier.svelte | 5 +- .../src/lib/values/util/external-link.svelte | 4 +- modules/frontend/src/routes/+layout.svelte | 38 +- .../CodeSystem/$validate-code/+page.server.ts | 78 +++ .../CodeSystem/$validate-code/+page.svelte | 52 ++ .../[id=id]/$validate-code/+page.server.ts | 56 +++ .../[id=id]/$validate-code/+page.svelte | 61 +++ .../[id=id]/$validate-code/+page.ts | 34 ++ .../[id=id]/operation-dropdown.svelte | 11 + .../CodeSystem/operation-dropdown.svelte | 10 + .../src/routes/CodeSystem/result-list.svelte | 51 ++ .../ValueSet/$validate-code/+page.server.ts | 108 ++++ .../ValueSet/$validate-code/+page.svelte | 59 +++ .../ValueSet/[id=id]/$expand/+page.server.ts | 108 ++++ .../ValueSet/[id=id]/$expand/+page.svelte | 110 ++++ .../routes/ValueSet/[id=id]/$expand/+page.ts | 31 ++ .../[id=id]/$validate-code/+page.server.ts | 91 ++++ .../[id=id]/$validate-code/+page.svelte | 68 +++ .../ValueSet/[id=id]/$validate-code/+page.ts | 31 ++ .../[id=id]/operation-dropdown.svelte | 12 + .../routes/ValueSet/operation-dropdown.svelte | 10 + .../src/routes/ValueSet/result-list.svelte | 51 ++ .../src/routes/[type=type]/+error.svelte | 11 +- .../src/routes/[type=type]/+page.svelte | 29 +- .../routes/[type=type]/[id=id]/+error.svelte | 15 +- .../routes/[type=type]/[id=id]/+page.svelte | 41 +- .../[type=type]/[id=id]/_history/+page.svelte | 17 +- .../[type=type]/[id=id]/_history/+page.ts | 2 +- .../[id=id]/_history/[vid=vid]/+page.svelte | 15 +- .../[type=type]/[id=id]/history-button.svelte | 10 + .../[pageId=pageId]/+page.svelte | 19 +- .../__page/[pageId=pageId]/+page.svelte | 13 +- .../routes/[type=type]/_history/+page.svelte | 17 +- .../routes/[type=type]/history-button.svelte | 2 +- .../routes/[type=type]/metadata-button.svelte | 2 +- .../__admin/jobs/new/re-index/+page.server.ts | 2 +- .../jobs/re-index/[id=id]/+page.svelte | 4 +- .../[pageId=pageId]/+page.svelte | 2 +- .../frontend/src/routes/_history/+page.svelte | 2 +- .../routes/metadata/[type=type]/+page.svelte | 21 +- 92 files changed, 2265 insertions(+), 522 deletions(-) create mode 100644 modules/frontend-e2e/src/code-system.spec.ts create mode 100644 modules/frontend/src/lib/breadcrumb.svelte delete mode 100644 modules/frontend/src/lib/breadcrumb/history.svelte create mode 100644 modules/frontend/src/lib/breadcrumb/resource-history.svelte create mode 100644 modules/frontend/src/lib/breadcrumb/type-history.svelte create mode 100644 modules/frontend/src/lib/resource.ts create mode 100644 modules/frontend/src/lib/tailwind/dropdown.svelte create mode 100644 modules/frontend/src/lib/tailwind/dropdown/item.svelte create mode 100644 modules/frontend/src/lib/tailwind/form.svelte create mode 100644 modules/frontend/src/lib/tailwind/form/button-submit.svelte create mode 100644 modules/frontend/src/lib/tailwind/form/check-box.svelte create mode 100644 modules/frontend/src/lib/tailwind/form/check-boxes.svelte create mode 100644 modules/frontend/src/lib/tailwind/form/section.svelte create mode 100644 modules/frontend/src/lib/tailwind/form/text-field.svelte create mode 100644 modules/frontend/src/routes/CodeSystem/$validate-code/+page.server.ts create mode 100644 modules/frontend/src/routes/CodeSystem/$validate-code/+page.svelte create mode 100644 modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.server.ts create mode 100644 modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.svelte create mode 100644 modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.ts create mode 100644 modules/frontend/src/routes/CodeSystem/[id=id]/operation-dropdown.svelte create mode 100644 modules/frontend/src/routes/CodeSystem/operation-dropdown.svelte create mode 100644 modules/frontend/src/routes/CodeSystem/result-list.svelte create mode 100644 modules/frontend/src/routes/ValueSet/$validate-code/+page.server.ts create mode 100644 modules/frontend/src/routes/ValueSet/$validate-code/+page.svelte create mode 100644 modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.server.ts create mode 100644 modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.svelte create mode 100644 modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.ts create mode 100644 modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.server.ts create mode 100644 modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.svelte create mode 100644 modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.ts create mode 100644 modules/frontend/src/routes/ValueSet/[id=id]/operation-dropdown.svelte create mode 100644 modules/frontend/src/routes/ValueSet/operation-dropdown.svelte create mode 100644 modules/frontend/src/routes/ValueSet/result-list.svelte create mode 100644 modules/frontend/src/routes/[type=type]/[id=id]/history-button.svelte diff --git a/.github/value-set-expand/upload.sh b/.github/value-set-expand/upload.sh index 26eaa7e91..7b24f2ba6 100755 --- a/.github/value-set-expand/upload.sh +++ b/.github/value-set-expand/upload.sh @@ -3,14 +3,13 @@ FILENAME=$1 BASE="http://localhost:8080/fhir" -echo "Upload $FILENAME" - RESOURCE_TYPE="$(jq -r .resourceType "$FILENAME")" if [[ "$RESOURCE_TYPE" =~ ValueSet|CodeSystem ]]; then URL="$(jq -r .url "$FILENAME")" if [[ "$URL" =~ http://unitsofmeasure.org|http://snomed.info/sct|http://loinc.org|urn:ietf:bcp:13 ]]; then echo "Skip creating the code system or value set $URL which is internal in Blaze" else + echo "Upload $FILENAME" curl -sf -H "Content-Type: application/fhir+json" -H "Prefer: return=minimal" -d @"$FILENAME" "$BASE/$RESOURCE_TYPE" fi fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8dd5776d..2a32d46ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2034,8 +2034,8 @@ jobs: - name: Run Keycloak run: docker compose -f modules/frontend-e2e/docker-compose.yml up -d keycloak - - name: Sleep 20 Seconds - run: sleep 20 + - name: Install Dependencies + run: make -C modules/frontend-e2e install - name: Run Everything Else run: docker compose -f modules/frontend-e2e/docker-compose.yml up -d @@ -2061,9 +2061,6 @@ jobs: - name: Download Patient Resources run: modules/frontend-e2e/download-patient-resources.sh - - name: Install Playwright - run: make -C modules/frontend-e2e install - - name: Run Playwright Tests run: make -C modules/frontend-e2e test diff --git a/modules/frontend-e2e/Makefile b/modules/frontend-e2e/Makefile index 1bc2cc389..6479a1ca0 100644 --- a/modules/frontend-e2e/Makefile +++ b/modules/frontend-e2e/Makefile @@ -1,5 +1,5 @@ install: - npm ci + npm install npx playwright install --with-deps test: @@ -8,7 +8,10 @@ test: test-dev: DEV="1" npx playwright test -test-ui-dev: +test-dev-chromium: + DEV="1" npx playwright test --project chromium + +test-dev-ui: DEV="1" npx playwright test --ui --project chromium cloc-prod: @@ -16,4 +19,4 @@ cloc-prod: cloc-test: cloc src -.PHONY: fmt lint install test test-ui-dev test-coverage cloc-prod cloc-test clean +.PHONY: fmt lint install test test-dev-chromium test-dev-ui test-coverage cloc-prod cloc-test clean diff --git a/modules/frontend-e2e/docker-compose.yml b/modules/frontend-e2e/docker-compose.yml index 21831f682..51ea9fccf 100644 --- a/modules/frontend-e2e/docker-compose.yml +++ b/modules/frontend-e2e/docker-compose.yml @@ -35,9 +35,12 @@ services: backend: image: "blaze:latest" environment: - JAVA_TOOL_OPTIONS: "-Xmx2g" + JAVA_TOOL_OPTIONS: "-Xmx4g" LOG_LEVEL: "debug" ENABLE_ADMIN_API: "true" + ENABLE_TERMINOLOGY_SERVICE: "true" + ENABLE_TERMINOLOGY_LOINC: "true" + DB_RESOURCE_CACHE_SIZE: "10000" OPENID_PROVIDER_URL: "https://keycloak.localhost/realms/blaze" OPENID_CLIENT_TRUST_STORE: "/app/keycloak-trust-store.p12" OPENID_CLIENT_TRUST_STORE_PASS: "insecure" diff --git a/modules/frontend-e2e/package-lock.json b/modules/frontend-e2e/package-lock.json index a0034eea5..629b57452 100644 --- a/modules/frontend-e2e/package-lock.json +++ b/modules/frontend-e2e/package-lock.json @@ -8,6 +8,10 @@ "name": "frontend-e2e", "version": "1.0.0", "license": "ISC", + "dependencies": { + "de.medizininformatikinitiative.kerndatensatz.fall": "^2025.0.0", + "de.medizininformatikinitiative.kerndatensatz.laborbefund": "^2025.0.2" + }, "devDependencies": { "@playwright/test": "^1.42.1", "@types/node": "^22.0.0" @@ -30,15 +34,25 @@ } }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, + "node_modules/de.medizininformatikinitiative.kerndatensatz.fall": { + "version": "2025.0.0", + "resolved": "https://packages.simplifier.net/de.medizininformatikinitiative.kerndatensatz.fall/2025.0.0", + "integrity": "sha1-pHVfeJFy4KiCQ5D7b5cEdR14eLk=" + }, + "node_modules/de.medizininformatikinitiative.kerndatensatz.laborbefund": { + "version": "2025.0.2", + "resolved": "https://packages.simplifier.net/de.medizininformatikinitiative.kerndatensatz.laborbefund/2025.0.2", + "integrity": "sha1-jVoi5AmZD1hbni15Pl9+2xmDnRg=" + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -87,9 +101,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" } diff --git a/modules/frontend-e2e/package.json b/modules/frontend-e2e/package.json index 93b4ab018..d789f2d74 100644 --- a/modules/frontend-e2e/package.json +++ b/modules/frontend-e2e/package.json @@ -10,5 +10,9 @@ "devDependencies": { "@playwright/test": "^1.42.1", "@types/node": "^22.0.0" + }, + "dependencies": { + "de.medizininformatikinitiative.kerndatensatz.fall": "^2025.0.0", + "de.medizininformatikinitiative.kerndatensatz.laborbefund": "^2025.0.2" } } diff --git a/modules/frontend-e2e/src/code-system.spec.ts b/modules/frontend-e2e/src/code-system.spec.ts new file mode 100644 index 000000000..3c7f58806 --- /dev/null +++ b/modules/frontend-e2e/src/code-system.spec.ts @@ -0,0 +1,99 @@ +import { expect, type Locator, type Page, test } from '@playwright/test'; + +function breadcrumbItem(page: Page, text: string): Locator { + return page.getByLabel('Breadcrumb').getByRole('listitem').filter({ hasText: text }); +} + +test.beforeEach('Sign In', async ({ page }) => { + await page.goto('/fhir/CodeSystem'); + + // Blaze Sign-In Page + await expect(page).toHaveTitle('Sign-In - Blaze'); + await page.getByRole('button', { name: 'Sign in with Keycloak' }).click(); + + // Keycloak Sign-In Page + await expect(page).toHaveTitle('Sign in to Keycloak'); + await page.getByLabel('Username or email').fill('john'); + await page.getByLabel('Password', { exact: true }).fill('insecure'); + await page.getByRole('button', { name: 'Sign In' }).click(); + + await expect(page).toHaveTitle('CodeSystem - Blaze'); + await expect(breadcrumbItem(page, 'CodeSystem')).toBeVisible(); +}); + +test('Search Page', async ({ page }) => { + await expect(page.getByTitle('CodeSystem History')).toBeVisible(); + await expect(page.getByTitle('CodeSystem Metadata')).toBeVisible(); + await expect(page.getByText('Total:')).toBeVisible(); +}); + +test('Search for LOINC', async ({ page }) => { + await page.goto('/fhir/CodeSystem?url=http://loinc.org'); + + await expect(breadcrumbItem(page, 'CodeSystem')).toBeVisible(); + + await expect(page.getByRole('link', { name: 'LOINC Code System v2.78' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Url http://loinc.org' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Version 2.78' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Name LOINC' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Title LOINC Code System' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Status active' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Experimental false' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Publisher Regenstrief Institute, Inc.' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Content not-present' })).toBeVisible(); + + await page.getByRole('link', { name: 'LOINC Code System v2.78' }).click(); + + await expect(breadcrumbItem(page, 'CodeSystem')).toBeVisible(); + await expect(breadcrumbItem(page, 'LOINC Code System v2.78')).toBeVisible(); + await expect(page.getByRole('link', { name: 'LOINC Code System v2.78' })).toBeVisible(); +}); + +test.describe('$validate-code', () => { + test.describe('LOINC 718-7', () => { + test('type-level', async ({ page }) => { + await page.getByRole('button', { name: 'Operations' }).click(); + await page.getByRole('menuitem', { name: '$validate-code' }).click(); + + await expect(breadcrumbItem(page, 'CodeSystem')).toBeVisible(); + await expect(breadcrumbItem(page, '$validate-code')).toBeVisible(); + + await page.getByRole('heading', { name: 'Parameters' }).click(); + await page.getByLabel('URL').fill('http://loinc.org'); + await page.getByLabel('Code').fill('718-7'); + await page.getByRole('button', { name: 'Submit' }).click(); + + await expect(page.getByRole('listitem').filter({ hasText: 'Result true' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Display Hemoglobin [Mass/volume] in Blood' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Code 718-7' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'System http://loinc.org' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Version 2.78' })).toBeVisible(); + }); + + test('instance-level', async ({ page }) => { + await page.goto('/fhir/CodeSystem?url=http://loinc.org'); + + await page.getByRole('link', { name: 'LOINC Code System v2.78' }).click(); + + await expect(breadcrumbItem(page, 'LOINC Code System v2.78')).toBeVisible(); + await page.getByRole('button', { name: 'Operations' }).click(); + await page.getByRole('menuitem', { name: '$validate-code' }).click(); + + await page.getByRole('heading', { name: 'LOINC Code System v2.78' }).click(); + + await expect(breadcrumbItem(page, 'CodeSystem')).toBeVisible(); + await expect(breadcrumbItem(page, 'LOINC Code System v2.78')).toBeVisible(); + await expect(breadcrumbItem(page, '$validate-code')).toBeVisible(); + + await page.getByRole('heading', { name: 'Parameters' }).click(); + await page.getByLabel('Code').fill('718-7'); + await page.getByRole('button', { name: 'Submit' }).click(); + + await expect(page.getByRole('listitem').filter({ hasText: 'Result true' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Display Hemoglobin [Mass/volume] in Blood' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Code 718-7' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'System http://loinc.org' })).toBeVisible(); + await expect(page.getByRole('listitem').filter({ hasText: 'Version 2.78' })).toBeVisible(); + }); + }); +}); diff --git a/modules/frontend-e2e/src/fhir.spec.ts b/modules/frontend-e2e/src/fhir.spec.ts index 4bc83666f..5413cc60f 100644 --- a/modules/frontend-e2e/src/fhir.spec.ts +++ b/modules/frontend-e2e/src/fhir.spec.ts @@ -43,7 +43,7 @@ test('History Page', async ({page}) => { await page.getByRole('link', {name: 'History', exact: true}).click(); await expect(page).toHaveTitle("History - Blaze"); - await expect(page.getByText('Total: 92,300')).toBeVisible(); + await expect(page.getByText('Total:')).toBeVisible(); }); test('Metadata Page', async ({page}) => { @@ -71,7 +71,7 @@ test('Metadata Page', async ({page}) => { await page.getByRole('link', {name: 'Resources'}).click(); await expect(page).toHaveTitle("Encounter - Blaze"); - await expect(page.getByText('Total: 4,769')).toBeVisible(); + await expect(page.getByText('Total:')).toBeVisible(); }); test.describe('Admin', () => { @@ -223,8 +223,8 @@ test.describe('Admin', () => { await expect(page.getByText('Search Param URL ' + searchParamUrl)).toBeVisible(); // may appear later - await expect(page.getByText('Total Resources 92.3 k')).toBeVisible({timeout: 30000}); - await expect(page.getByText('Resources Processed 92.3 k')).toBeVisible({timeout: 50000}); + await expect(page.getByText('Total Resources')).toBeVisible({timeout: 30000}); + await expect(page.getByText('Resources Processed')).toBeVisible({timeout: 50000}); await expect(page.getByText('Processing Duration')).toBeVisible({timeout: 50000}); await expect(page.getByText('Status completed')).toBeVisible({timeout: 50000}); }); @@ -250,7 +250,7 @@ test('Patients Page', async ({page}) => { await expect(page).toHaveTitle("Patient - Blaze"); await expect(page.getByTitle('Patient History')).toBeVisible(); await expect(page.getByTitle('Patient Metadata')).toBeVisible(); - await expect(page.getByText('Total: 120')).toBeVisible(); + await expect(page.getByText('Total:')).toBeVisible(); await page.getByTitle('Patient Metadata').click(); await expect(page).toHaveTitle("Patient - Metadata - Blaze"); @@ -270,7 +270,7 @@ test('Patients History Page', async ({page}) => { await page.getByTitle('Patient History').click() await expect(page).toHaveTitle("History - Patient - Blaze"); - await expect(page.getByText('Total: 120')).toBeVisible(); + await expect(page.getByText('Total:')).toBeVisible(); }); test('Signing in after sign out goes to the Keycloak Sign-In Page', async ({page}) => { diff --git a/modules/frontend-e2e/upload.sh b/modules/frontend-e2e/upload.sh index c311d2db1..28c6f5f3f 100755 --- a/modules/frontend-e2e/upload.sh +++ b/modules/frontend-e2e/upload.sh @@ -12,9 +12,8 @@ blazectl --no-progress \ --token "$TOKEN" \ upload "$SCRIPT_DIR/../../.github/test-data/synthea" -echo "Download KDS Fall Package..." -wget -q --content-disposition "https://packages.simplifier.net/de.medizininformatikinitiative.kerndatensatz.fall/2025.0.0" -tar xzf de.medizininformatikinitiative.kerndatensatz.fall-2025.0.0.tgz - echo "Upload KDS Fall Profile..." -curl -sfH 'Content-Type: application/fhir+json' -H 'Prefer: return=minimal' --cacert "$CA_CERT" --oauth2-bearer "$TOKEN" -d @"package/StructureDefinition-mii-pr-fall-kontakt-gesundheitseinrichtung.json" "$BASE/StructureDefinition" +curl -sfH 'Content-Type: application/fhir+json' -H 'Prefer: return=minimal' --cacert "$CA_CERT" --oauth2-bearer "$TOKEN" -d @"$SCRIPT_DIR/node_modules/de.medizininformatikinitiative.kerndatensatz.fall/StructureDefinition-mii-pr-fall-kontakt-gesundheitseinrichtung.json" "$BASE/StructureDefinition" + +echo "Upload one Value Set..." +curl -sfH 'Content-Type: application/fhir+json' -H 'Prefer: return=minimal' --cacert "$CA_CERT" --oauth2-bearer "$TOKEN" -d @"$SCRIPT_DIR/node_modules/de.medizininformatikinitiative.kerndatensatz.laborbefund/ValueSet-mii-vs-labor-laborbereich.json" "$BASE/ValueSet" diff --git a/modules/frontend/src/app.html b/modules/frontend/src/app.html index 00c3f9b43..34fc1b573 100644 --- a/modules/frontend/src/app.html +++ b/modules/frontend/src/app.html @@ -2,7 +2,6 @@ - diff --git a/modules/frontend/src/lib/breadcrumb.svelte b/modules/frontend/src/lib/breadcrumb.svelte new file mode 100644 index 000000000..b09b78a55 --- /dev/null +++ b/modules/frontend/src/lib/breadcrumb.svelte @@ -0,0 +1,15 @@ + + + diff --git a/modules/frontend/src/lib/breadcrumb/entry.svelte b/modules/frontend/src/lib/breadcrumb/entry.svelte index c213d140e..421b5774f 100644 --- a/modules/frontend/src/lib/breadcrumb/entry.svelte +++ b/modules/frontend/src/lib/breadcrumb/entry.svelte @@ -1,6 +1,8 @@ - - - History - diff --git a/modules/frontend/src/lib/breadcrumb/resource-history.svelte b/modules/frontend/src/lib/breadcrumb/resource-history.svelte new file mode 100644 index 000000000..cd6aee883 --- /dev/null +++ b/modules/frontend/src/lib/breadcrumb/resource-history.svelte @@ -0,0 +1,22 @@ + + + + {#if last} + History + {:else} + History + {/if} + diff --git a/modules/frontend/src/lib/breadcrumb/resource.svelte b/modules/frontend/src/lib/breadcrumb/resource.svelte index 51e3e8980..2350f530e 100644 --- a/modules/frontend/src/lib/breadcrumb/resource.svelte +++ b/modules/frontend/src/lib/breadcrumb/resource.svelte @@ -1,12 +1,30 @@ - {page.params.id} + {#if last} + {name} + {:else} + {name} + {/if} diff --git a/modules/frontend/src/lib/breadcrumb/type-history.svelte b/modules/frontend/src/lib/breadcrumb/type-history.svelte new file mode 100644 index 000000000..640569a5e --- /dev/null +++ b/modules/frontend/src/lib/breadcrumb/type-history.svelte @@ -0,0 +1,22 @@ + + + + {#if last} + History + {:else} + History + {/if} + diff --git a/modules/frontend/src/lib/breadcrumb/type.svelte b/modules/frontend/src/lib/breadcrumb/type.svelte index bd6310f20..b06a6c36e 100644 --- a/modules/frontend/src/lib/breadcrumb/type.svelte +++ b/modules/frontend/src/lib/breadcrumb/type.svelte @@ -2,11 +2,21 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import Entry from './entry.svelte'; + + interface Props { + type?: string; + last?: boolean; + } + + let { type = page.params.type, last = false }: Props = $props(); - {page.params.type} + {#if last} + {type} + {:else} + {type} + {/if} diff --git a/modules/frontend/src/lib/error-card.svelte b/modules/frontend/src/lib/error-card.svelte index 240811108..f6a2a6020 100644 --- a/modules/frontend/src/lib/error-card.svelte +++ b/modules/frontend/src/lib/error-card.svelte @@ -1,4 +1,6 @@ {#if entry.fhirObject} - + {#snippet header()} {/snippet} diff --git a/modules/frontend/src/lib/metadata.ts b/modules/frontend/src/lib/metadata.ts index b640e9ea8..da011c62c 100644 --- a/modules/frontend/src/lib/metadata.ts +++ b/modules/frontend/src/lib/metadata.ts @@ -38,7 +38,7 @@ export async function fetchStructureDefinition( fetch: typeof window.fetch = window.fetch ) { const cached = structureDefinitionStore.get(type); - if (cached) { + if (cached !== undefined) { return cached; } diff --git a/modules/frontend/src/lib/resource.ts b/modules/frontend/src/lib/resource.ts new file mode 100644 index 000000000..b634cc695 --- /dev/null +++ b/modules/frontend/src/lib/resource.ts @@ -0,0 +1,19 @@ +import type { CodeSystem, FhirResource, ValueSet } from 'fhir/r4'; + +export function title(resource: FhirResource) { + if (resource.resourceType === 'CodeSystem') { + const codeSystem = resource as CodeSystem; + if (codeSystem.title && codeSystem.version) { + return `${codeSystem.title} v${codeSystem.version}`; + } + } + + if (resource.resourceType === 'ValueSet') { + const valueSet = resource as ValueSet; + if (valueSet.title && valueSet.version) { + return `${valueSet.title} v${valueSet.version}`; + } + } + + return `${resource.resourceType}/${resource.id}`; +} diff --git a/modules/frontend/src/lib/resource/json/array.svelte b/modules/frontend/src/lib/resource/json/array.svelte index 0b9899ca9..587a1a7d0 100644 --- a/modules/frontend/src/lib/resource/json/array.svelte +++ b/modules/frontend/src/lib/resource/json/array.svelte @@ -8,10 +8,13 @@ } let { indent, values }: Props = $props(); + + const maxLength = 100; + let length = Math.min(values.length, maxLength); -{'[\n'}{#each values as value, index}{index < values.length - 1 ? ',\n' : '\n'}{/each}{' '.repeat(indent)}{']'} + />{index < length - 1 ? ',\n' : '\n'}{/each}{' '.repeat(indent)}] diff --git a/modules/frontend/src/lib/resource/json/object.svelte b/modules/frontend/src/lib/resource/json/object.svelte index 3d9aa19ae..9fd2a27fc 100644 --- a/modules/frontend/src/lib/resource/json/object.svelte +++ b/modules/frontend/src/lib/resource/json/object.svelte @@ -11,9 +11,8 @@ let { indent = 0, insideArray = false, object }: Props = $props(); -{insideArray ? ' '.repeat(indent) : ''}{'{\n'}{#each object.properties as property, index (property.name)}{/each}{' '.repeat(indent)}{'}'} + />{/each}{' '.repeat(indent)}{'}'} diff --git a/modules/frontend/src/lib/resource/json/property.svelte b/modules/frontend/src/lib/resource/json/property.svelte index b02f533dd..24d719d8d 100644 --- a/modules/frontend/src/lib/resource/json/property.svelte +++ b/modules/frontend/src/lib/resource/json/property.svelte @@ -16,11 +16,10 @@ let { indent, isLast, property }: Props = $props(); - let primitiveExtensions = $derived( + let primitiveExtensions = !Array.isArray(property.value) && isPrimitive(property.value.type) ? (property.value as FhirPrimitive).extensions - : undefined - ); + : undefined; {' '.repeat(indent)}"{property.name}" - import { isPrimitive } from '../resource-card.js'; + import { type FhirObject, type FhirPrimitive, isPrimitive } from '../resource-card.js'; import PrimitiveValue from './primitive-value.svelte'; import Object from './object.svelte'; interface Props { indent: number; insideArray?: boolean; - // eslint-disable-next-line - value: any; //FhirObject | FhirPrimitive + value: FhirObject | FhirPrimitive; } let { indent, insideArray = false, value }: Props = $props(); + + function toPrimitive(value: FhirObject | FhirPrimitive): FhirPrimitive { + return value as FhirPrimitive; + } + + function toObject(value: FhirObject | FhirPrimitive): FhirObject { + return value as FhirObject; + } - {#if isPrimitive(value.type)}{insideArray ? ' '.repeat(indent) : ''}{:else}{/if} + value={toPrimitive(value)} + />{:else}{/if} diff --git a/modules/frontend/src/lib/resource/property.svelte b/modules/frontend/src/lib/resource/property.svelte index 9842e0ca4..1066c72ee 100644 --- a/modules/frontend/src/lib/resource/property.svelte +++ b/modules/frontend/src/lib/resource/property.svelte @@ -5,6 +5,7 @@ type FhirPrimitive, type FhirObject } from './resource-card.js'; + import type { Attachment, Identifier, @@ -14,6 +15,7 @@ Reference, Dosage } from 'fhir/r4'; + import PrimitiveValue from './primitive-value.svelte'; import ComplexValue from './complex-value.svelte'; import AttachmentValues from '$lib/values/attachment.svelte'; @@ -24,6 +26,8 @@ import ReferenceValues from '$lib/values/reference.svelte'; import DosageValues from '$lib/values/dosage.svelte'; + import { toTitleCase } from '$lib/util.js'; + interface Props { property: FhirProperty; } @@ -34,8 +38,6 @@ return Array.isArray(x) ? x : [x]; } - const name = property.name.substring(0, 1).toUpperCase() + property.name.substring(1); - const singlePrimitiveValue = isPrimitive(property.type) && !Array.isArray(property.value) ? (property.value as FhirPrimitive) @@ -79,8 +81,10 @@ } -
-
{name}
+
+
+ {toTitleCase(property.humanName ?? property.name)} +
{#if singlePrimitiveValue} diff --git a/modules/frontend/src/lib/resource/resource-card.svelte b/modules/frontend/src/lib/resource/resource-card.svelte index 755116c64..ef1842d67 100644 --- a/modules/frontend/src/lib/resource/resource-card.svelte +++ b/modules/frontend/src/lib/resource/resource-card.svelte @@ -1,5 +1,6 @@ -
+
{title}
{@render children?.()} diff --git a/modules/frontend/src/lib/tailwind/description/left-aligned/row-5-4.svelte b/modules/frontend/src/lib/tailwind/description/left-aligned/row-5-4.svelte index cc725d39f..e288e9510 100644 --- a/modules/frontend/src/lib/tailwind/description/left-aligned/row-5-4.svelte +++ b/modules/frontend/src/lib/tailwind/description/left-aligned/row-5-4.svelte @@ -1,13 +1,15 @@ -
+
{title}
{@render children?.()} diff --git a/modules/frontend/src/lib/tailwind/dropdown.svelte b/modules/frontend/src/lib/tailwind/dropdown.svelte new file mode 100644 index 000000000..916da2d5b --- /dev/null +++ b/modules/frontend/src/lib/tailwind/dropdown.svelte @@ -0,0 +1,67 @@ + + +
+
+ +
+ + + {#if open} + + {/if} +
diff --git a/modules/frontend/src/lib/tailwind/dropdown/item.svelte b/modules/frontend/src/lib/tailwind/dropdown/item.svelte new file mode 100644 index 000000000..eec999d97 --- /dev/null +++ b/modules/frontend/src/lib/tailwind/dropdown/item.svelte @@ -0,0 +1,17 @@ + + + +{name} diff --git a/modules/frontend/src/lib/tailwind/form.svelte b/modules/frontend/src/lib/tailwind/form.svelte new file mode 100644 index 000000000..88d968b61 --- /dev/null +++ b/modules/frontend/src/lib/tailwind/form.svelte @@ -0,0 +1,23 @@ + + +
+
+ {@render children?.()} +
+ +
+ {@render buttons?.()} +
+
diff --git a/modules/frontend/src/lib/tailwind/form/button-submit.svelte b/modules/frontend/src/lib/tailwind/form/button-submit.svelte new file mode 100644 index 000000000..0d6358ff9 --- /dev/null +++ b/modules/frontend/src/lib/tailwind/form/button-submit.svelte @@ -0,0 +1,14 @@ + + + diff --git a/modules/frontend/src/lib/tailwind/form/check-box.svelte b/modules/frontend/src/lib/tailwind/form/check-box.svelte new file mode 100644 index 000000000..b217ab548 --- /dev/null +++ b/modules/frontend/src/lib/tailwind/form/check-box.svelte @@ -0,0 +1,46 @@ + + +
+
+
+ + + + + +
+
+
+ +
+
diff --git a/modules/frontend/src/lib/tailwind/form/check-boxes.svelte b/modules/frontend/src/lib/tailwind/form/check-boxes.svelte new file mode 100644 index 000000000..ce793f65d --- /dev/null +++ b/modules/frontend/src/lib/tailwind/form/check-boxes.svelte @@ -0,0 +1,22 @@ + + +
+ {name} +
+ +
+
+ {@render children?.()} +
+
+
+
diff --git a/modules/frontend/src/lib/tailwind/form/section.svelte b/modules/frontend/src/lib/tailwind/form/section.svelte new file mode 100644 index 000000000..1c6115abf --- /dev/null +++ b/modules/frontend/src/lib/tailwind/form/section.svelte @@ -0,0 +1,24 @@ + + +
+

{name}

+ {#if description} +

{description}

+ {/if} + +
+ {@render children?.()} +
+
diff --git a/modules/frontend/src/lib/tailwind/form/text-field.svelte b/modules/frontend/src/lib/tailwind/form/text-field.svelte new file mode 100644 index 000000000..445e2c3d2 --- /dev/null +++ b/modules/frontend/src/lib/tailwind/form/text-field.svelte @@ -0,0 +1,22 @@ + + +
+ +
+ +
+
diff --git a/modules/frontend/src/lib/tailwind/logo-card/card.svelte b/modules/frontend/src/lib/tailwind/logo-card/card.svelte index a34a1e6d5..6199e4b62 100644 --- a/modules/frontend/src/lib/tailwind/logo-card/card.svelte +++ b/modules/frontend/src/lib/tailwind/logo-card/card.svelte @@ -1,9 +1,11 @@ + + + $validate-code - CodeSystem - Blaze + + +
+ + + + + $validate-code + + +
+ +
+
+
+ + + + + +
+ {#snippet buttons()} + + {/snippet} + + + {#if form?.incorrect} +

{form.msg}

+ {/if} + + {#if form?.result} + + {/if} +
diff --git a/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.server.ts b/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.server.ts new file mode 100644 index 000000000..97eb6a37d --- /dev/null +++ b/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.server.ts @@ -0,0 +1,56 @@ +import type { Actions } from './$types'; +import type { OperationOutcome, Parameters, ParametersParameter } from 'fhir/r4'; +import { base } from '$app/paths'; +import { fail } from '@sveltejs/kit'; + +export const actions = { + default: async ({ request, fetch, params }) => { + const data = await request.formData(); + const code = data.get('code') as string; + const display = data.get('display') as string; + const displayLanguage = data.get('displayLanguage') as string; + + const parameters: ParametersParameter[] = [ + { + name: 'code', + valueCode: code + } + ]; + + if (display !== '') { + parameters.push({ + name: 'display', + valueString: display + }); + } + + if (displayLanguage !== '') { + parameters.push({ + name: 'displayLanguage', + valueCode: displayLanguage + }); + } + + const res = await fetch(`${base}/CodeSystem/${params.id}/$validate-code`, { + method: 'POST', + headers: { 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json' }, + body: JSON.stringify({ + resourceType: 'Parameters', + parameter: parameters + }) + }); + + if (!res.ok) { + const error: OperationOutcome = await res.json(); + return fail(400, { + code, + display, + displayLanguage, + incorrect: true, + msg: error.issue[0]?.diagnostics ?? error.issue[0]?.details?.text + }); + } + + return { code, display, displayLanguage, result: (await res.json()) as Parameters }; + } +} satisfies Actions; diff --git a/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.svelte b/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.svelte new file mode 100644 index 000000000..dbcda8923 --- /dev/null +++ b/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.svelte @@ -0,0 +1,61 @@ + + + + $validate-code - {title(data.codeSystem)} - Blaze + + +
+ + + + + + $validate-code + + +
+ +
+

+ {title(data.codeSystem)} +

+ {#if data.codeSystem.description} +

{data.codeSystem.description}

+ {/if} + +
+
+ + + +
+ {#snippet buttons()} + + {/snippet} + + + {#if form?.incorrect} +

{form.msg}

+ {/if} + + {#if form?.result} + + {/if} +
diff --git a/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.ts b/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.ts new file mode 100644 index 000000000..1c7748a3d --- /dev/null +++ b/modules/frontend/src/routes/CodeSystem/[id=id]/$validate-code/+page.ts @@ -0,0 +1,34 @@ +import type { PageLoad } from './$types'; +import type { Bundle, CodeSystem } from 'fhir/r4'; + +import { base } from '$app/paths'; +import { error, type NumericRange } from '@sveltejs/kit'; + +export const load: PageLoad = async ({ fetch, params }) => { + const res = await fetch( + `${base}/CodeSystem?_id=${params.id}&_elements=version,title,description`, + { + headers: { + Accept: 'application/fhir+json' + } + } + ); + + if (!res.ok) { + error(res.status as NumericRange<400, 599>, { + short: res.status == 404 ? 'Not Found' : res.status == 410 ? 'Gone' : undefined, + message: + res.status == 404 + ? `The CodeSystem with ID ${params.id} was not found.` + : res.status == 410 + ? `The CodeSystem with ID ${params.id} was deleted. Please look into the history.` + : `An error happened while loading the CodeSystem with ID ${params.id}. Please try again later.` + }); + } + + const bundle: Bundle = await res.json(); + + return { + codeSystem: bundle?.entry?.[0].resource as CodeSystem + }; +}; diff --git a/modules/frontend/src/routes/CodeSystem/[id=id]/operation-dropdown.svelte b/modules/frontend/src/routes/CodeSystem/[id=id]/operation-dropdown.svelte new file mode 100644 index 000000000..6159c48a0 --- /dev/null +++ b/modules/frontend/src/routes/CodeSystem/[id=id]/operation-dropdown.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/modules/frontend/src/routes/CodeSystem/operation-dropdown.svelte b/modules/frontend/src/routes/CodeSystem/operation-dropdown.svelte new file mode 100644 index 000000000..a119d4fe6 --- /dev/null +++ b/modules/frontend/src/routes/CodeSystem/operation-dropdown.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/modules/frontend/src/routes/CodeSystem/result-list.svelte b/modules/frontend/src/routes/CodeSystem/result-list.svelte new file mode 100644 index 000000000..cdb893dc0 --- /dev/null +++ b/modules/frontend/src/routes/CodeSystem/result-list.svelte @@ -0,0 +1,51 @@ + + + + + {result} + + {#if !result} + + {parameter(parameters, 'message')?.valueString} + + {:else} + {@const display = parameter(parameters, 'display')?.valueString} + {#if display} + + {display} + + {/if} + {@const code = parameter(parameters, 'code')?.valueCode} + {#if code} + + {code} + + {/if} + {@const system = parameter(parameters, 'system')?.valueUri} + {#if system} + + {system} + + {/if} + {@const version = parameter(parameters, 'version')?.valueString} + {#if version} + + {version} + + {/if} + {/if} + diff --git a/modules/frontend/src/routes/ValueSet/$validate-code/+page.server.ts b/modules/frontend/src/routes/ValueSet/$validate-code/+page.server.ts new file mode 100644 index 000000000..c33eb5226 --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/$validate-code/+page.server.ts @@ -0,0 +1,108 @@ +import type { Actions } from './$types'; +import type { OperationOutcome, Parameters, ParametersParameter } from 'fhir/r4'; +import { base } from '$app/paths'; +import { fail } from '@sveltejs/kit'; + +export const actions = { + default: async ({ request, fetch }) => { + const data = await request.formData(); + const url = data.get('url') as string; + const valueSetVersion = data.get('valueSetVersion') as string; + const code = data.get('code') as string; + const system = data.get('system') as string; + const systemVersion = data.get('systemVersion') as string; + const display = data.get('display') as string; + const displayLanguage = data.get('displayLanguage') as string; + const inferSystem = Boolean(data.get('inferSystem')); + + const parameters: ParametersParameter[] = [ + { + name: 'url', + valueUri: url + }, + { + name: 'code', + valueCode: code + } + ]; + + if (valueSetVersion !== '') { + parameters.push({ + name: 'valueSetVersion', + valueString: valueSetVersion + }); + } + + if (system !== '') { + parameters.push({ + name: 'system', + valueString: system + }); + } + + if (systemVersion !== '') { + parameters.push({ + name: 'systemVersion', + valueString: systemVersion + }); + } + + if (display !== '') { + parameters.push({ + name: 'display', + valueString: display + }); + } + + if (displayLanguage !== '') { + parameters.push({ + name: 'displayLanguage', + valueCode: displayLanguage + }); + } + + if (inferSystem) { + parameters.push({ + name: 'inferSystem', + valueBoolean: true + }); + } + + const res = await fetch(`${base}/ValueSet/$validate-code`, { + method: 'POST', + headers: { 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json' }, + body: JSON.stringify({ + resourceType: 'Parameters', + parameter: parameters + }) + }); + + if (!res.ok) { + const error: OperationOutcome = await res.json(); + return fail(400, { + url, + valueSetVersion, + code, + system, + systemVersion, + display, + displayLanguage, + inferSystem, + incorrect: true, + msg: error.issue[0]?.diagnostics ?? error.issue[0]?.details?.text + }); + } + + return { + url, + valueSetVersion, + code, + system, + systemVersion, + display, + displayLanguage, + inferSystem, + result: (await res.json()) as Parameters + }; + } +} satisfies Actions; diff --git a/modules/frontend/src/routes/ValueSet/$validate-code/+page.svelte b/modules/frontend/src/routes/ValueSet/$validate-code/+page.svelte new file mode 100644 index 000000000..7edbf35ab --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/$validate-code/+page.svelte @@ -0,0 +1,59 @@ + + + + $validate-code - ValueSet - Blaze + + +
+ + + + + $validate-code + + +
+ +
+
+
+ + + + + + + + + + +
+ {#snippet buttons()} + + {/snippet} + + + {#if form?.incorrect} +

{form.msg}

+ {/if} + + {#if form?.result} + + {/if} +
diff --git a/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.server.ts b/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.server.ts new file mode 100644 index 000000000..3f3d328d3 --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.server.ts @@ -0,0 +1,108 @@ +import type { Actions } from './$types'; +import type { OperationOutcome, ParametersParameter, ValueSet } from 'fhir/r4'; +import { base } from '$app/paths'; +import { fail } from '@sveltejs/kit'; + +export const actions = { + default: async ({ request, fetch, params }) => { + const data = await request.formData(); + const property = data.get('property') as string; + const displayLanguage = data.get('displayLanguage') as string; + const systemVersion = data.get('systemVersion') as string; + const includeDesignations = Boolean(data.get('includeDesignations')); + const includeDefinition = Boolean(data.get('includeDefinition')); + const activeOnly = Boolean(data.get('activeOnly')); + const excludeNested = Boolean(data.get('excludeNested')); + + const parameters: ParametersParameter[] = [ + { + name: 'count', + valueInteger: 100 + } + ]; + + if (property !== '') { + parameters.push({ + name: 'property', + valueString: property + }); + } + + if (displayLanguage !== '') { + parameters.push({ + name: 'displayLanguage', + valueCode: displayLanguage + }); + } + + if (systemVersion !== '') { + parameters.push({ + name: 'system-version', + valueString: systemVersion + }); + } + + if (includeDesignations) { + parameters.push({ + name: 'includeDesignations', + valueBoolean: true + }); + } + + if (includeDefinition) { + parameters.push({ + name: 'includeDefinition', + valueBoolean: true + }); + } + + if (activeOnly) { + parameters.push({ + name: 'activeOnly', + valueBoolean: true + }); + } + + if (excludeNested) { + parameters.push({ + name: 'excludeNested', + valueBoolean: true + }); + } + + const res = await fetch(`${base}/ValueSet/${params.id}/$expand`, { + method: 'POST', + headers: { 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json' }, + body: JSON.stringify({ + resourceType: 'Parameters', + parameter: parameters + }) + }); + + if (!res.ok) { + const error: OperationOutcome = await res.json(); + return fail(400, { + property, + displayLanguage, + systemVersion, + includeDesignations, + includeDefinition, + activeOnly, + excludeNested, + incorrect: true, + msg: error.issue[0]?.diagnostics ?? error.issue[0]?.details?.text + }); + } + + return { + property, + displayLanguage, + systemVersion, + includeDesignations, + includeDefinition, + activeOnly, + excludeNested, + valueSet: (await res.json()) as ValueSet + }; + } +} satisfies Actions; diff --git a/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.svelte b/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.svelte new file mode 100644 index 000000000..4ecb676fb --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.svelte @@ -0,0 +1,110 @@ + + + + $expand - {title(data.valueSet)} - Blaze + + +
+ + + + + + $expand + + +
+ +
+

+ {title(data.valueSet)} +

+ {#if data.valueSet.description} +

{data.valueSet.description}

+ {/if} + +
+
+ + + + + + + + + +
+ {#snippet buttons()} + + {/snippet} + + + {#if form?.incorrect} +

{form.msg}

+ {/if} + + {#if form?.valueSet?.expansion?.contains} +
    + {#each form.valueSet.expansion.contains as contains} +
  • +
    +

    + {contains.display} +

    +
    +

    {contains.system}

    + + + + {#if contains.version} +

    {contains.version}

    + + + + {/if} +

    {contains.code}

    +
    + {#if contains.designation} +
      + {#each contains.designation as designation} +
    • {designation.value}
    • + {/each} +
    + {/if} +
    +
  • + {/each} +
+ {/if} +
diff --git a/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.ts b/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.ts new file mode 100644 index 000000000..a1d9b6660 --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/[id=id]/$expand/+page.ts @@ -0,0 +1,31 @@ +import type { PageLoad } from './$types'; +import type { Bundle, ValueSet } from 'fhir/r4'; + +import { base } from '$app/paths'; +import { error, type NumericRange } from '@sveltejs/kit'; + +export const load: PageLoad = async ({ fetch, params }) => { + const res = await fetch(`${base}/ValueSet?_id=${params.id}&_elements=version,title,description`, { + headers: { + Accept: 'application/fhir+json' + } + }); + + if (!res.ok) { + error(res.status as NumericRange<400, 599>, { + short: res.status == 404 ? 'Not Found' : res.status == 410 ? 'Gone' : undefined, + message: + res.status == 404 + ? `The ValueSet with ID ${params.id} was not found.` + : res.status == 410 + ? `The ValueSet with ID ${params.id} was deleted. Please look into the history.` + : `An error happened while loading the ValueSet with ID ${params.id}. Please try again later.` + }); + } + + const bundle: Bundle = await res.json(); + + return { + valueSet: bundle?.entry?.[0].resource as ValueSet + }; +}; diff --git a/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.server.ts b/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.server.ts new file mode 100644 index 000000000..a432dac48 --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.server.ts @@ -0,0 +1,91 @@ +import type { Actions } from './$types'; +import type { OperationOutcome, Parameters, ParametersParameter } from 'fhir/r4'; +import { base } from '$app/paths'; +import { fail } from '@sveltejs/kit'; + +export const actions = { + default: async ({ request, fetch, params }) => { + const data = await request.formData(); + const code = data.get('code') as string; + const system = data.get('system') as string; + const systemVersion = data.get('systemVersion') as string; + const display = data.get('display') as string; + const displayLanguage = data.get('displayLanguage') as string; + const inferSystem = Boolean(data.get('inferSystem')); + + const parameters: ParametersParameter[] = [ + { + name: 'code', + valueCode: code + } + ]; + + if (system !== '') { + parameters.push({ + name: 'system', + valueString: system + }); + } + + if (systemVersion !== '') { + parameters.push({ + name: 'systemVersion', + valueString: systemVersion + }); + } + + if (display !== '') { + parameters.push({ + name: 'display', + valueString: display + }); + } + + if (displayLanguage !== '') { + parameters.push({ + name: 'displayLanguage', + valueCode: displayLanguage + }); + } + + if (inferSystem) { + parameters.push({ + name: 'inferSystem', + valueBoolean: true + }); + } + + const res = await fetch(`${base}/ValueSet/${params.id}/$validate-code`, { + method: 'POST', + headers: { 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json' }, + body: JSON.stringify({ + resourceType: 'Parameters', + parameter: parameters + }) + }); + + if (!res.ok) { + const error: OperationOutcome = await res.json(); + return fail(400, { + code, + system, + systemVersion, + display, + displayLanguage, + inferSystem, + incorrect: true, + msg: error.issue[0]?.diagnostics ?? error.issue[0]?.details?.text + }); + } + + return { + code, + system, + systemVersion, + display, + displayLanguage, + inferSystem, + result: (await res.json()) as Parameters + }; + } +} satisfies Actions; diff --git a/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.svelte b/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.svelte new file mode 100644 index 000000000..eee4ddf05 --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.svelte @@ -0,0 +1,68 @@ + + + + $validate-code - {title(data.valueSet)} - Blaze + + +
+ + + + + + $validate-code + + +
+ +
+

+ {title(data.valueSet)} +

+ {#if data.valueSet.description} +

{data.valueSet.description}

+ {/if} + +
+
+ + + + + + + + +
+ {#snippet buttons()} + + {/snippet} + + + {#if form?.incorrect} +

{form.msg}

+ {/if} + + {#if form?.result} + + {/if} +
diff --git a/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.ts b/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.ts new file mode 100644 index 000000000..a1d9b6660 --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/[id=id]/$validate-code/+page.ts @@ -0,0 +1,31 @@ +import type { PageLoad } from './$types'; +import type { Bundle, ValueSet } from 'fhir/r4'; + +import { base } from '$app/paths'; +import { error, type NumericRange } from '@sveltejs/kit'; + +export const load: PageLoad = async ({ fetch, params }) => { + const res = await fetch(`${base}/ValueSet?_id=${params.id}&_elements=version,title,description`, { + headers: { + Accept: 'application/fhir+json' + } + }); + + if (!res.ok) { + error(res.status as NumericRange<400, 599>, { + short: res.status == 404 ? 'Not Found' : res.status == 410 ? 'Gone' : undefined, + message: + res.status == 404 + ? `The ValueSet with ID ${params.id} was not found.` + : res.status == 410 + ? `The ValueSet with ID ${params.id} was deleted. Please look into the history.` + : `An error happened while loading the ValueSet with ID ${params.id}. Please try again later.` + }); + } + + const bundle: Bundle = await res.json(); + + return { + valueSet: bundle?.entry?.[0].resource as ValueSet + }; +}; diff --git a/modules/frontend/src/routes/ValueSet/[id=id]/operation-dropdown.svelte b/modules/frontend/src/routes/ValueSet/[id=id]/operation-dropdown.svelte new file mode 100644 index 000000000..8570cecb4 --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/[id=id]/operation-dropdown.svelte @@ -0,0 +1,12 @@ + + + + + + diff --git a/modules/frontend/src/routes/ValueSet/operation-dropdown.svelte b/modules/frontend/src/routes/ValueSet/operation-dropdown.svelte new file mode 100644 index 000000000..7e7b4f14e --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/operation-dropdown.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/modules/frontend/src/routes/ValueSet/result-list.svelte b/modules/frontend/src/routes/ValueSet/result-list.svelte new file mode 100644 index 000000000..cdb893dc0 --- /dev/null +++ b/modules/frontend/src/routes/ValueSet/result-list.svelte @@ -0,0 +1,51 @@ + + + + + {result} + + {#if !result} + + {parameter(parameters, 'message')?.valueString} + + {:else} + {@const display = parameter(parameters, 'display')?.valueString} + {#if display} + + {display} + + {/if} + {@const code = parameter(parameters, 'code')?.valueCode} + {#if code} + + {code} + + {/if} + {@const system = parameter(parameters, 'system')?.valueUri} + {#if system} + + {system} + + {/if} + {@const version = parameter(parameters, 'version')?.valueString} + {#if version} + + {version} + + {/if} + {/if} + diff --git a/modules/frontend/src/routes/[type=type]/+error.svelte b/modules/frontend/src/routes/[type=type]/+error.svelte index 4d9c2cded..a26e25b16 100644 --- a/modules/frontend/src/routes/[type=type]/+error.svelte +++ b/modules/frontend/src/routes/[type=type]/+error.svelte @@ -1,6 +1,7 @@ @@ -14,13 +15,11 @@
- + + + + +
diff --git a/modules/frontend/src/routes/[type=type]/[id=id]/+page.svelte b/modules/frontend/src/routes/[type=type]/[id=id]/+page.svelte index f7edda9e4..4fa44eba9 100644 --- a/modules/frontend/src/routes/[type=type]/[id=id]/+page.svelte +++ b/modules/frontend/src/routes/[type=type]/[id=id]/+page.svelte @@ -1,7 +1,7 @@ - {page.params.type}/{page.params.id} - Blaze + {title(resource)} - Blaze
- +
+ + {#if page.params.type === 'CodeSystem'} + + {:else if page.params.type === 'ValueSet'} + + {/if} + +
{#if data.resource}
{#snippet header()} -
- History -
+
{/snippet}
diff --git a/modules/frontend/src/routes/[type=type]/[id=id]/_history/+page.svelte b/modules/frontend/src/routes/[type=type]/[id=id]/_history/+page.svelte index 8c95546a3..ef2b32ea4 100644 --- a/modules/frontend/src/routes/[type=type]/[id=id]/_history/+page.svelte +++ b/modules/frontend/src/routes/[type=type]/[id=id]/_history/+page.svelte @@ -3,10 +3,11 @@ import { page } from '$app/state'; + import Breadcrumb from '$lib/breadcrumb.svelte'; import BreadcrumbEntryHome from '$lib/breadcrumb/home.svelte'; import BreadcrumbEntryType from '$lib/breadcrumb/type.svelte'; import BreadcrumbEntryResource from '$lib/breadcrumb/resource.svelte'; - import BreadcrumbEntryHistory from '$lib/breadcrumb/history.svelte'; + import BreadcrumbEntryHistory from '$lib/breadcrumb/resource-history.svelte'; import EntryCard from '$lib/history/entry-card.svelte'; @@ -18,14 +19,12 @@
- + + + + + +
diff --git a/modules/frontend/src/routes/[type=type]/[id=id]/_history/+page.ts b/modules/frontend/src/routes/[type=type]/[id=id]/_history/+page.ts index 2e9fb6ac9..bd08b8211 100644 --- a/modules/frontend/src/routes/[type=type]/[id=id]/_history/+page.ts +++ b/modules/frontend/src/routes/[type=type]/[id=id]/_history/+page.ts @@ -5,7 +5,7 @@ import { error, type NumericRange } from '@sveltejs/kit'; import { transformBundle } from '$lib/resource/resource-card.js'; export const load: PageLoad = async ({ fetch, params }) => { - const res = await fetch(`${base}/${params.type}/${params.id}/_history`, { + const res = await fetch(`${base}/${params.type}/${params.id}/_history?_count&_summary=true`, { headers: { Accept: 'application/fhir+json' } }); diff --git a/modules/frontend/src/routes/[type=type]/[id=id]/_history/[vid=vid]/+page.svelte b/modules/frontend/src/routes/[type=type]/[id=id]/_history/[vid=vid]/+page.svelte index 6b37cbd83..8e7496e69 100644 --- a/modules/frontend/src/routes/[type=type]/[id=id]/_history/[vid=vid]/+page.svelte +++ b/modules/frontend/src/routes/[type=type]/[id=id]/_history/[vid=vid]/+page.svelte @@ -3,6 +3,7 @@ import { page } from '$app/state'; + import Breadcrumb from '$lib/breadcrumb.svelte'; import BreadcrumbEntryHome from '$lib/breadcrumb/home.svelte'; import BreadcrumbEntryType from '$lib/breadcrumb/type.svelte'; import BreadcrumbEntryResource from '$lib/breadcrumb/resource.svelte'; @@ -17,14 +18,12 @@
- + + + + + +
diff --git a/modules/frontend/src/routes/[type=type]/[id=id]/history-button.svelte b/modules/frontend/src/routes/[type=type]/[id=id]/history-button.svelte new file mode 100644 index 000000000..49f418650 --- /dev/null +++ b/modules/frontend/src/routes/[type=type]/[id=id]/history-button.svelte @@ -0,0 +1,10 @@ + + +History diff --git a/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.svelte b/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.svelte index ab75b86ef..7eff85169 100644 --- a/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.svelte +++ b/modules/frontend/src/routes/[type=type]/__history-page/[pageId=pageId]/+page.svelte @@ -3,9 +3,10 @@ import { page } from '$app/state'; + import Breadcrumb from '$lib/breadcrumb.svelte'; import BreadcrumbEntryHome from '$lib/breadcrumb/home.svelte'; import BreadcrumbEntryType from '$lib/breadcrumb/type.svelte'; - import BreadcrumbEntryHistory from '$lib/breadcrumb/history.svelte'; + import BreadcrumbEntryHistory from '$lib/breadcrumb/type-history.svelte'; import BreadcrumbEntryPage from '$lib/breadcrumb/page.svelte'; import TotalCard from '$lib/total-card.svelte'; @@ -20,19 +21,17 @@
- + + + + + +
-

+

{#if data.bundle.total !== undefined} {/if} diff --git a/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.svelte b/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.svelte index 679d66a02..a6d67113c 100644 --- a/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.svelte +++ b/modules/frontend/src/routes/[type=type]/__page/[pageId=pageId]/+page.svelte @@ -5,6 +5,7 @@ import { page } from '$app/state'; import { fade, slide } from 'svelte/transition'; + import Breadcrumb from '$lib/breadcrumb.svelte'; import BreadcrumbEntryHome from '$lib/breadcrumb/home.svelte'; import BreadcrumbEntryType from '$lib/breadcrumb/type.svelte'; import BreadcrumbEntryPage from '$lib/breadcrumb/page.svelte'; @@ -34,13 +35,11 @@

- + + + + +
diff --git a/modules/frontend/src/routes/[type=type]/_history/+page.svelte b/modules/frontend/src/routes/[type=type]/_history/+page.svelte index 7cabd51ff..1c6c8b9ff 100644 --- a/modules/frontend/src/routes/[type=type]/_history/+page.svelte +++ b/modules/frontend/src/routes/[type=type]/_history/+page.svelte @@ -3,9 +3,10 @@ import { page } from '$app/state'; + import Breadcrumb from '$lib/breadcrumb.svelte'; import BreadcrumbEntryHome from '$lib/breadcrumb/home.svelte'; import BreadcrumbEntryType from '$lib/breadcrumb/type.svelte'; - import BreadcrumbEntryHistory from '$lib/breadcrumb/history.svelte'; + import BreadcrumbEntryHistory from '$lib/breadcrumb/type-history.svelte'; import TotalCard from '$lib/total-card.svelte'; import TotalBadge from '$lib/total-badge.svelte'; @@ -19,18 +20,16 @@
- + + + + +
-

+

{#if data.bundle.total !== undefined} {/if} diff --git a/modules/frontend/src/routes/[type=type]/history-button.svelte b/modules/frontend/src/routes/[type=type]/history-button.svelte index fab35aebf..20ad78e61 100644 --- a/modules/frontend/src/routes/[type=type]/history-button.svelte +++ b/modules/frontend/src/routes/[type=type]/history-button.svelte @@ -5,6 +5,6 @@ History diff --git a/modules/frontend/src/routes/[type=type]/metadata-button.svelte b/modules/frontend/src/routes/[type=type]/metadata-button.svelte index b5edabaa8..2634441a2 100644 --- a/modules/frontend/src/routes/[type=type]/metadata-button.svelte +++ b/modules/frontend/src/routes/[type=type]/metadata-button.svelte @@ -5,6 +5,6 @@ Metadata diff --git a/modules/frontend/src/routes/__admin/jobs/new/re-index/+page.server.ts b/modules/frontend/src/routes/__admin/jobs/new/re-index/+page.server.ts index 68e7226ed..181bc34da 100644 --- a/modules/frontend/src/routes/__admin/jobs/new/re-index/+page.server.ts +++ b/modules/frontend/src/routes/__admin/jobs/new/re-index/+page.server.ts @@ -1,6 +1,6 @@ import type { Actions } from './$types'; -import { base } from '$app/paths'; import type { OperationOutcome, Task } from 'fhir/r4'; +import { base } from '$app/paths'; import { fail, redirect } from '@sveltejs/kit'; export const actions = { diff --git a/modules/frontend/src/routes/__admin/jobs/re-index/[id=id]/+page.svelte b/modules/frontend/src/routes/__admin/jobs/re-index/[id=id]/+page.svelte index 14dbc2dae..3b28ef3ff 100644 --- a/modules/frontend/src/routes/__admin/jobs/re-index/[id=id]/+page.svelte +++ b/modules/frontend/src/routes/__admin/jobs/re-index/[id=id]/+page.svelte @@ -2,10 +2,10 @@ import type { PageProps } from './$types'; import { invalidateAll } from '$app/navigation'; - import Status from '$lib/jobs/re-index/status.svelte'; import DescriptionList from '$lib/tailwind/description/left-aligned/list.svelte'; - import DateTime from '$lib/values/date-time.svelte'; import Row from '$lib/tailwind/description/left-aligned/row-3-2.svelte'; + import Status from '$lib/jobs/re-index/status.svelte'; + import DateTime from '$lib/values/date-time.svelte'; import prettyNum from '$lib/pretty-num'; import humanizeDuration from 'humanize-duration'; diff --git a/modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.svelte b/modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.svelte index 78aa4ef03..bafcf2e63 100644 --- a/modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.svelte +++ b/modules/frontend/src/routes/__history-page/[pageId=pageId]/+page.svelte @@ -14,7 +14,7 @@

-

+

{#if data.bundle.total !== undefined} {/if} diff --git a/modules/frontend/src/routes/_history/+page.svelte b/modules/frontend/src/routes/_history/+page.svelte index 78aa4ef03..bafcf2e63 100644 --- a/modules/frontend/src/routes/_history/+page.svelte +++ b/modules/frontend/src/routes/_history/+page.svelte @@ -14,7 +14,7 @@

-

+

{#if data.bundle.total !== undefined} {/if} diff --git a/modules/frontend/src/routes/metadata/[type=type]/+page.svelte b/modules/frontend/src/routes/metadata/[type=type]/+page.svelte index 2be4542e1..3d971b123 100644 --- a/modules/frontend/src/routes/metadata/[type=type]/+page.svelte +++ b/modules/frontend/src/routes/metadata/[type=type]/+page.svelte @@ -3,13 +3,15 @@ import { base } from '$app/paths'; import { page } from '$app/state'; + import { sortByProperty2 } from '$lib/util'; + import Breadcrumb from '$lib/breadcrumb.svelte'; import BreadcrumbEntryHome from '$lib/breadcrumb/home.svelte'; import BreadcrumbEntryMetadata from '$lib/breadcrumb/metadata.svelte'; import BreadcrumbEntryType from '$lib/breadcrumb/type.svelte'; import DescriptionList from '$lib/tailwind/description/left-aligned/list.svelte'; import Row from '$lib/tailwind/description/left-aligned/row-5-4.svelte'; - import { sortByProperty2 } from '$lib/util'; + import ExternalLink from '$lib/values/util/external-link.svelte'; let { data }: PageProps = $props(); @@ -26,13 +28,11 @@

- + + + + +
@@ -44,6 +44,11 @@ {#snippet description()} Resources {/snippet} + + {page.params.type} + {#if resource?.supportedProfile !== undefined}