fhirsmith 0.18.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +149 -0
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +132 -59
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +74 -21
- package/ips/presenter.js +1 -1
- package/ips/readme.md +132 -25
- package/ips/render.js +466 -77
- package/ips/store.js +79 -22
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/crawler.js +84 -108
- package/registry/documentation/configuration.md +34 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +5 -4
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +154 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +29 -1
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +148 -14
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +185 -66
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +608 -64
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +57 -58
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +278 -42
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/ecl.js +47 -6
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +166 -13
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +27 -4
- package/tx/vs/vs-vsac.js +648 -100
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
package/ips/render.js
CHANGED
|
@@ -27,7 +27,22 @@ const profiles = require('./profiles');
|
|
|
27
27
|
const { terminologyOf, systemName, describeVersion, linkForSystem, linkForCode, isExternalSystem } = require('./terminology');
|
|
28
28
|
const { issueSummary, checkOf, translationOf } = require('./pipeline');
|
|
29
29
|
const { jurisdiction } = require('./jurisdictions');
|
|
30
|
+
const { SNOMED_LINKABLE_EDITIONS, editionName } = require('../tx/sct/editions');
|
|
31
|
+
const { nationalityText } = require('./nationality');
|
|
32
|
+
|
|
33
|
+
// the nations there is a flag for (static/assets/images/flags/{code}.svg); any other is the globe
|
|
34
|
+
const FLAGS = new Set(['US', 'AU']);
|
|
35
|
+
|
|
36
|
+
/** The nation a document is from, as a flag (a black and white globe when it isn't known, or has no flag). */
|
|
37
|
+
function nationalityFlag(result) {
|
|
38
|
+
const code = result?.jurisdiction || null;
|
|
39
|
+
const title = result ? nationalityText(result) : 'The nationality has not been determined yet';
|
|
40
|
+
const src = code && FLAGS.has(code) ? `/assets/images/flags/${code.toLowerCase()}.svg` : '/assets/images/flags/unknown.svg';
|
|
41
|
+
const alt = code ? (jurisdiction(code)?.display || code) : 'Nationality unknown';
|
|
42
|
+
return `<img class="ips-flag" src="${src}" alt="${escape(alt)}" title="${escape(title)}"/>`;
|
|
43
|
+
}
|
|
30
44
|
const { dateText } = require('./presenter');
|
|
45
|
+
const { structureTree, flatten } = require('./structure');
|
|
31
46
|
const scripts = require('./scripts');
|
|
32
47
|
const { LOINC } = require('./editor-schema');
|
|
33
48
|
|
|
@@ -45,6 +60,8 @@ const STYLE = `
|
|
|
45
60
|
.ips-table td, .ips-table th { vertical-align: top; }
|
|
46
61
|
.ips-json { white-space: pre-wrap; word-break: break-all; font-size: 85%; background: #f8f9fa; padding: 8px; border: 1px solid #dee2e6; font-family: monospace; }
|
|
47
62
|
.ips-banner { background: #ede7f6; border: 1px solid #d1c4e9; border-radius: 4px; padding: 10px 14px; margin-bottom: 10px; }
|
|
63
|
+
.ips-banner-flagged { display: flex; align-items: center; gap: 14px; }
|
|
64
|
+
.ips-flag { height: 40px; width: auto; max-width: 80px; border: 1px solid #d1c4e9; flex: none; }
|
|
48
65
|
.ips-banner .ips-patient { font-size: 120%; }
|
|
49
66
|
.ips-banner .ips-project-name { font-size: 130%; }
|
|
50
67
|
.ips-actions-line { margin-bottom: 10px; }
|
|
@@ -55,13 +72,27 @@ const STYLE = `
|
|
|
55
72
|
.ips-banner .ips-facts { font-size: 90%; color: #444; margin-top: 4px; }
|
|
56
73
|
.ips-banner .ips-facts a { margin-left: 4px; }
|
|
57
74
|
.ips-steps { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; margin-bottom: 10px; font-size: 90%; }
|
|
58
|
-
.ips-
|
|
75
|
+
.ips-pipeline { display: flex; flex-wrap: wrap; row-gap: 4px; align-items: stretch; }
|
|
76
|
+
.ips-step { display: inline-flex; align-items: center; gap: 5px; padding: 3px 14px 3px 20px; margin-right: -8px; color: white; white-space: nowrap; cursor: default;
|
|
77
|
+
clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%, 10px 50%); }
|
|
78
|
+
.ips-step:first-child { padding-left: 12px; border-radius: 3px 0 0 3px; clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 0 100%); }
|
|
79
|
+
a.ips-step { cursor: pointer; color: white; text-decoration: none; }
|
|
80
|
+
a.ips-step:hover, a.ips-step:focus { color: white; text-decoration: none; filter: brightness(1.15); }
|
|
81
|
+
.ips-step-received { background: #5e35b1; }
|
|
59
82
|
.ips-step-done { background: #1e7e34; }
|
|
60
83
|
.ips-step-failed { background: #c82333; }
|
|
61
84
|
.ips-step-running { background: #0d6efd; }
|
|
62
85
|
.ips-step-waiting { background: #6c757d; }
|
|
63
|
-
.ips-
|
|
64
|
-
|
|
86
|
+
.ips-step-running .ips-step-icon { display: inline-block; animation: ips-spin 2s linear infinite; }
|
|
87
|
+
@keyframes ips-spin { to { transform: rotate(360deg); } }
|
|
88
|
+
.ips-switch { display: inline-flex; margin-bottom: 10px; font-size: 90%; }
|
|
89
|
+
.ips-switch a { padding: 3px 12px; border: 1px solid #ced4da; color: #444; text-decoration: none; }
|
|
90
|
+
.ips-switch a + a { border-left: none; }
|
|
91
|
+
.ips-switch a:first-child { border-radius: 3px 0 0 3px; }
|
|
92
|
+
.ips-switch a:last-child { border-radius: 0 3px 3px 0; }
|
|
93
|
+
.ips-switch a.active { background: #5e35b1; border-color: #5e35b1; color: white; }
|
|
94
|
+
.ips-doc-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 10px; }
|
|
95
|
+
.ips-doc-actions .btn { font-size: 90%; padding: 2px 10px; }
|
|
65
96
|
.ips-nav.nav-tabs { margin-bottom: 14px; }
|
|
66
97
|
.ips-nav.nav-tabs > li > a { padding: 6px 14px; }
|
|
67
98
|
.ips-explain { margin-top: 18px; padding-top: 8px; border-top: 1px solid #dee2e6; }
|
|
@@ -72,8 +103,9 @@ const STYLE = `
|
|
|
72
103
|
.ips-filter .ips-btn { font-size: 100%; padding: 1px 10px; line-height: 1.5; }
|
|
73
104
|
.ips-date { white-space: nowrap; }
|
|
74
105
|
.ips-narrative { border: 1px solid #dee2e6; padding: 12px; background: #fff; }
|
|
75
|
-
.ips-narrative h3 { font-size:
|
|
76
|
-
.ips-narrative h4 { font-size:
|
|
106
|
+
.ips-narrative h3 { font-size: 130%; margin-top: 8px; }
|
|
107
|
+
.ips-narrative h4 { font-size: 110%; margin-top: 18px; border-bottom: 1px solid #eee; }
|
|
108
|
+
.ips-narrative h5 { font-size: 100%; margin-top: 12px; }
|
|
77
109
|
.ips-narrative table { border-collapse: collapse; margin-bottom: 8px; }
|
|
78
110
|
.ips-narrative td, .ips-narrative th { border: 1px solid #ddd; padding: 2px 6px; font-size: 90%; }
|
|
79
111
|
.ips-meta { font-size: 85%; color: #555; margin-bottom: 10px; }
|
|
@@ -178,10 +210,39 @@ const STYLE = `
|
|
|
178
210
|
.ips-translated { font-size: 80%; color: #fff; background: #6f42c1; border-radius: 3px; padding: 0 5px; cursor: help; }
|
|
179
211
|
.ips-check { font-size: 130%; font-weight: bold; cursor: help; }
|
|
180
212
|
.ips-check-in { color: #1e7e34; }
|
|
181
|
-
.ips-check-international { color: #
|
|
213
|
+
.ips-check-international { color: #a35200; }
|
|
182
214
|
.ips-check-not-in { color: #c82333; }
|
|
183
215
|
.ips-check-unknown { color: #6c757d; }
|
|
184
216
|
.ips-inline { display: inline; }
|
|
217
|
+
.ips-tree-tools { margin-bottom: 8px; font-size: 90%; }
|
|
218
|
+
.ips-an { font-size: 90%; }
|
|
219
|
+
.ips-an > tbody > tr.ips-an-node > td { border-top: 2px solid #dee2e6; background: #f8f9fa; }
|
|
220
|
+
.ips-an > tbody > tr.ips-an-findings > td { border-top: none; padding: 2px 8px 8px 30px; }
|
|
221
|
+
.ips-an td.ips-an-path { word-break: break-all; }
|
|
222
|
+
.ips-an td.ips-an-path img { vertical-align: -3px; }
|
|
223
|
+
.ips-an tr.ips-tt-seen > td { background-color: #EFEFEF; }
|
|
224
|
+
.ips-an ul { margin: 0; padding-left: 18px; }
|
|
225
|
+
.ips-an-modifier { color: #800000; font-weight: bold; }
|
|
226
|
+
.ips-an-status { color: #000080; font-weight: bold; }
|
|
227
|
+
.ips-an-subject { color: #006400; font-weight: bold; }
|
|
228
|
+
.ips-an-attention { color: #b34700; font-weight: bold; }
|
|
229
|
+
.ips-an-ok { color: #1e7e34; }
|
|
230
|
+
.ips-an-narrative { color: #3a4d5e; }
|
|
231
|
+
.ips-an-negation { color: #3a4d5e; }
|
|
232
|
+
.ips-an-error { color: #c00000; font-weight: bold; }
|
|
233
|
+
.ips-tree-tools .btn { font-size: 90%; padding: 2px 10px; }
|
|
234
|
+
.ips-tt { font-size: 90%; width: auto; min-width: 60%; }
|
|
235
|
+
.ips-tt > thead > tr > th { background: #f0f0f0; font-weight: bold; white-space: nowrap; }
|
|
236
|
+
.ips-tt > tbody > tr > td { border-top: none; padding: 0 10px 0 4px; line-height: 24px; vertical-align: top; }
|
|
237
|
+
.ips-tt > tbody > tr > td.ips-tt-path { position: relative; white-space: nowrap; padding-right: 16px; }
|
|
238
|
+
.ips-tt td.ips-tt-path img { vertical-align: -3px; position: relative; z-index: 1; background: white; }
|
|
239
|
+
.ips-tt tr.ips-tt-parent:not(.ips-tt-closed) > td.ips-tt-path::after { content: ''; position: absolute; left: var(--ips-tt-down, 8px); top: 18px; bottom: 0; border-left: 1px solid #adb5bd; }
|
|
240
|
+
.ips-tt-toggle { position: absolute; top: 7px; width: 11px; height: 11px; padding: 0; z-index: 2; border: 1px solid #868e96; background: white; font-size: 9px; line-height: 9px; color: #495057; cursor: pointer; }
|
|
241
|
+
.ips-tt-toggle::before { content: '\\2212'; }
|
|
242
|
+
.ips-tt tr.ips-tt-closed .ips-tt-toggle::before { content: '+'; }
|
|
243
|
+
.ips-tt tr.ips-tt-seen > td { background-color: #EFEFEF; }
|
|
244
|
+
.ips-tt tr.ips-tt-missing > td:last-child { color: #c82333; }
|
|
245
|
+
.ips-tt .ips-tt-when { color: #3a4d5e; margin-left: 6px; }
|
|
185
246
|
</style>
|
|
186
247
|
<script>
|
|
187
248
|
// a form with data-confirm asks first: the message is an attribute (escaped as one), never script
|
|
@@ -416,7 +477,7 @@ function projectNav(project, ctx) {
|
|
|
416
477
|
/** The documentation at the foot of a project's page: how to reach it as a FHIR endpoint. */
|
|
417
478
|
function projectDocs(project, ctx) {
|
|
418
479
|
const purl = projectUrl(ctx, project);
|
|
419
|
-
return '<div class="ips-explain ips-docs"><
|
|
480
|
+
return '<div class="ips-explain ips-docs"><h3>Documentation</h3>' +
|
|
420
481
|
`<p>This project is a FHIR endpoint: <code>${purl}</code> (<a href="${purl}/metadata">CapabilityStatement</a>). ` +
|
|
421
482
|
'Documents are IPS Bundles (<code>type: document</code>, a Composition first, with the Patient it refers to) in JSON.</p>' +
|
|
422
483
|
'<table class="table table-sm ips-table ips-api"><tr><th>Request</th><th></th></tr>' +
|
|
@@ -473,7 +534,7 @@ function renderProject(project, query, results, search, ctx, problems) {
|
|
|
473
534
|
const columns = [
|
|
474
535
|
['_id', 'Id'], ['composition.patient.name', 'Patient'], ['composition.patient.gender', 'Gender'],
|
|
475
536
|
['composition.patient.birthdate', 'Born'], ['composition.date', 'Date'], ['composition.title', 'Title'],
|
|
476
|
-
['processing-status', 'Status'], ['_lastUpdated', 'Received']
|
|
537
|
+
['processing-status', 'Status'], ['_lastUpdated', 'Received'], [null, 'Nation']
|
|
477
538
|
];
|
|
478
539
|
if (ctx.canManage) {
|
|
479
540
|
columns.push([null, '']);
|
|
@@ -505,7 +566,8 @@ function renderProject(project, query, results, search, ctx, problems) {
|
|
|
505
566
|
`<td class="ips-date" title="${escape(r.comp_date || '')}">${escape(dateOnly(r.comp_date))}</td>` +
|
|
506
567
|
`<td>${escape(r.comp_title || '')}</td>` +
|
|
507
568
|
`<td title="${escape(ctx.canManage ? r.status_message || '' : '')}">${badge(r.status)}</td>` +
|
|
508
|
-
`<td class="ips-date" title="${escape(r.received)}">${escape(dateOnly(r.received))}</td
|
|
569
|
+
`<td class="ips-date" title="${escape(r.received)}">${escape(dateOnly(r.received))}</td>` +
|
|
570
|
+
`<td title="${escape(r.nationality ? (jurisdiction(r.nationality)?.display || r.nationality) : 'Not known')}">${escape(r.nationality || '??')}</td>`;
|
|
509
571
|
if (ctx.canManage) {
|
|
510
572
|
h += `<td><form method="post" action="${durl}/delete" class="ips-inline" data-confirm="${escape(`Delete document ${r.id}?`)}">${csrfField(ctx.csrf)}` +
|
|
511
573
|
'<button type="submit" class="btn btn-link" style="padding: 0; font-size: 85%">delete</button></form></td>';
|
|
@@ -641,7 +703,7 @@ function aiSettingsHtml(project, ctx, options) {
|
|
|
641
703
|
* a live element. Serialising closes what was left open, and each narrative is wrapped in
|
|
642
704
|
* its own div.
|
|
643
705
|
*/
|
|
644
|
-
function narrativeOf(res) {
|
|
706
|
+
function narrativeOf(res, level) {
|
|
645
707
|
const div = res?.text?.div;
|
|
646
708
|
if (typeof div !== 'string' || div.trim() === '') {
|
|
647
709
|
return null;
|
|
@@ -649,18 +711,54 @@ function narrativeOf(res) {
|
|
|
649
711
|
if (checkNarrative(div)) {
|
|
650
712
|
return '<p class="ips-small"><i>(narrative not shown: it contains active content)</i></p>';
|
|
651
713
|
}
|
|
652
|
-
return `<div class="ips-narrative-text">${parse5.serialize(parse5.parseFragment(div))}</div>`;
|
|
714
|
+
return `<div class="ips-narrative-text">${parse5.serialize(seatHeadings(parse5.parseFragment(div), level))}</div>`;
|
|
653
715
|
}
|
|
654
716
|
|
|
717
|
+
/**
|
|
718
|
+
* Sit a narrative's headings at the depth the page shows it at: shift them all by the same
|
|
719
|
+
* amount, so that the narrative's top heading is at `level` and its structure is kept.
|
|
720
|
+
* Narrative arrives with whatever heading levels its author (or the presenter) chose, and
|
|
721
|
+
* none of them know that the page is titled with an h2 - an h1 or h2 in a narrative breaks
|
|
722
|
+
* the page's heading structure (WCAG 1.3.1). Levels that would go past h6 stay at h6.
|
|
723
|
+
*
|
|
724
|
+
* @param {Object} fragment - a parse5 fragment, changed in place
|
|
725
|
+
* @param {number} level - the level the narrative's top heading should be at
|
|
726
|
+
* @returns {Object} the fragment
|
|
727
|
+
*/
|
|
728
|
+
function seatHeadings(fragment, level) {
|
|
729
|
+
const headings = [];
|
|
730
|
+
const walk = (node) => {
|
|
731
|
+
for (const c of node.childNodes || []) {
|
|
732
|
+
if (/^h[1-6]$/.test(c.nodeName)) {
|
|
733
|
+
headings.push(c);
|
|
734
|
+
}
|
|
735
|
+
walk(c);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
walk(fragment);
|
|
739
|
+
if (headings.length === 0) {
|
|
740
|
+
return fragment;
|
|
741
|
+
}
|
|
742
|
+
const shift = level - Math.min(...headings.map(h => Number(h.nodeName[1])));
|
|
743
|
+
for (const h of headings) {
|
|
744
|
+
const name = `h${Math.max(1, Math.min(6, Number(h.nodeName[1]) + shift))}`;
|
|
745
|
+
h.nodeName = name;
|
|
746
|
+
h.tagName = name;
|
|
747
|
+
}
|
|
748
|
+
return fragment;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// The page's title is the template's h2; within the narrative view the document title is an
|
|
752
|
+
// h3, its sections h4, their sections h5, and so on (down to h6)
|
|
655
753
|
function sectionsHtml(sections, level) {
|
|
656
754
|
let h = '';
|
|
657
755
|
for (const s of Array.isArray(sections) ? sections : []) {
|
|
658
756
|
if (!isObject(s)) {
|
|
659
757
|
continue;
|
|
660
758
|
}
|
|
661
|
-
const tag =
|
|
759
|
+
const tag = `h${Math.min(level, 6)}`;
|
|
662
760
|
h += `<${tag}>${escape(text(s.title) || text(s.code?.text) || '')}</${tag}>`;
|
|
663
|
-
const n = narrativeOf(s);
|
|
761
|
+
const n = narrativeOf(s, Math.min(level + 1, 6));
|
|
664
762
|
if (n) {
|
|
665
763
|
h += n;
|
|
666
764
|
} else if (!s.section) {
|
|
@@ -682,13 +780,221 @@ function assembledNarrative(bundle) {
|
|
|
682
780
|
}
|
|
683
781
|
let h = '<div class="ips-narrative">';
|
|
684
782
|
if (text(comp.title)) {
|
|
685
|
-
h += `<
|
|
783
|
+
h += `<h3>${escape(comp.title)}</h3>`;
|
|
686
784
|
}
|
|
687
|
-
h += narrativeOf(comp) || '';
|
|
688
|
-
h += sectionsHtml(comp.section,
|
|
785
|
+
h += narrativeOf(comp, 4) || '';
|
|
786
|
+
h += sectionsHtml(comp.section, 4);
|
|
689
787
|
return h + '</div>';
|
|
690
788
|
}
|
|
691
789
|
|
|
790
|
+
// ---- the structure view ----
|
|
791
|
+
|
|
792
|
+
/** A node's details: what it is, when, and a note (contained, shown at...). */
|
|
793
|
+
function nodeDetails(node) {
|
|
794
|
+
return [node.what, node.when ? `<span class="ips-tt-when">${node.when}</span>` : '', node.note ? `<i>(${escape(node.note)})</i>` : '']
|
|
795
|
+
.filter(Boolean).join(' ');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* The logical structure of a document (structure.js), as a tree table like the FHIR
|
|
800
|
+
* specification's. Columns: the path (where the reference is), the type, the id, and what
|
|
801
|
+
* the resource is and when. A reference that doesn't resolve in the document is shown as
|
|
802
|
+
* external; a resource expanded elsewhere is shown again, greyed, saying where, and doesn't expand.
|
|
803
|
+
*/
|
|
804
|
+
function renderStructure(bundle) {
|
|
805
|
+
const top = structureTree(bundle);
|
|
806
|
+
if (!top) {
|
|
807
|
+
return '<p><i>The document has no Composition.</i></p>';
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// the rows: the lines are drawn as the cell's background, so they carry on through rows
|
|
811
|
+
// that wrap; each level is UNIT px in, and the line to a node's children runs down from
|
|
812
|
+
// the middle of its icon
|
|
813
|
+
const UNIT = 20;
|
|
814
|
+
const LINE = '#adb5bd';
|
|
815
|
+
let rows = '';
|
|
816
|
+
let next = 0;
|
|
817
|
+
function walk(node, depth, parentId, lines, last, hidden) {
|
|
818
|
+
const id = next++;
|
|
819
|
+
const bg = [];
|
|
820
|
+
lines.forEach((on, k) => {
|
|
821
|
+
if (on) {
|
|
822
|
+
bg.push([UNIT * k + 10, 0, '1px', '100%']);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
if (depth > 0) {
|
|
826
|
+
const x = UNIT * (depth - 1) + 10;
|
|
827
|
+
bg.push([x, 0, '1px', last ? '12px' : '100%']);
|
|
828
|
+
bg.push([x, 12, `${UNIT - 8}px`, '1px']);
|
|
829
|
+
}
|
|
830
|
+
const lineStyle = bg.length === 0 ? '' :
|
|
831
|
+
`background-image: ${bg.map(() => `linear-gradient(${LINE}, ${LINE})`).join(', ')}; ` +
|
|
832
|
+
`background-position: ${bg.map(b => `${b[0]}px ${b[1]}px`).join(', ')}; ` +
|
|
833
|
+
`background-size: ${bg.map(b => `${b[2]} ${b[3]}`).join(', ')}; background-repeat: no-repeat; `;
|
|
834
|
+
const style = ` style="${lineStyle}padding-left: ${UNIT * depth + 2}px; --ips-tt-down: ${UNIT * depth + 10}px"`;
|
|
835
|
+
const kids = node.children.length > 0;
|
|
836
|
+
const closed = kids && !node.open;
|
|
837
|
+
const cls = [node.kind === 'missing' ? 'ips-tt-missing' : node.kind === 'seen' ? 'ips-tt-seen' : '', kids ? 'ips-tt-parent' : '', closed ? 'ips-tt-closed' : ''].filter(Boolean).join(' ');
|
|
838
|
+
const toggle = kids && depth > 0 ?
|
|
839
|
+
`<button type="button" class="ips-tt-toggle" style="left: ${UNIT * (depth - 1) + 5}px" aria-label="Show or hide what ${escape(node.path)} refers to"></button>` : '';
|
|
840
|
+
const idCell = node.id ? escape(node.id) : (node.kind === 'missing' ? `<code>${escape(node.ref)}</code>` : '');
|
|
841
|
+
const idTitle = node.ref ? ` title="${escape(`Reference: ${node.ref}`)}"` : '';
|
|
842
|
+
const details = nodeDetails(node);
|
|
843
|
+
rows += `<tr data-tt-id="${id}"` + (parentId === null ? '' : ` data-tt-parent="${parentId}"`) + (cls ? ` class="${cls}"` : '') + (hidden ? ' hidden' : '') + '>' +
|
|
844
|
+
`<td class="ips-tt-path"${style}>${toggle}<img src="/assets/images/icon_${node.icon}.png" alt="" width="16" height="16"/> ${escape(node.path)}</td>` +
|
|
845
|
+
`<td>${escape(node.type)}</td><td${idTitle}>${idCell}</td><td>${details}</td></tr>`;
|
|
846
|
+
node.children.forEach((c, i) => {
|
|
847
|
+
const lastChild = i === node.children.length - 1;
|
|
848
|
+
walk(c, depth + 1, id, depth === 0 ? [] : [...lines, !last], lastChild, hidden || closed);
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
walk(top, 0, null, [], true, false);
|
|
852
|
+
|
|
853
|
+
let h = '<div class="ips-tree-tools"><button type="button" class="btn btn-default ips-tt-all" data-open="1">Expand all</button> ' +
|
|
854
|
+
'<button type="button" class="btn btn-default ips-tt-all" data-open="">Collapse all</button></div>';
|
|
855
|
+
h += '<noscript><style>.ips-tt tr[hidden] { display: table-row; } .ips-tt-toggle, .ips-tree-tools { display: none; }</style></noscript>';
|
|
856
|
+
h += '<table class="table table-condensed ips-tt"><thead><tr><th>Path</th><th>Type</th><th>Id</th><th>Details</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
|
857
|
+
h += `<script>
|
|
858
|
+
(function () {
|
|
859
|
+
var rows = Array.prototype.slice.call(document.querySelectorAll('.ips-tt tbody tr'));
|
|
860
|
+
var byId = {};
|
|
861
|
+
rows.forEach(function (r) { byId[r.getAttribute('data-tt-id')] = r; });
|
|
862
|
+
// a row shows when its parent shows and is open (rows come parents first)
|
|
863
|
+
function refresh() {
|
|
864
|
+
rows.forEach(function (r) {
|
|
865
|
+
var p = byId[r.getAttribute('data-tt-parent')];
|
|
866
|
+
r.hidden = !!p && (p.hidden || p.classList.contains('ips-tt-closed'));
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
document.querySelectorAll('.ips-tt-toggle').forEach(function (b) {
|
|
870
|
+
b.addEventListener('click', function () { b.closest('tr').classList.toggle('ips-tt-closed'); refresh(); });
|
|
871
|
+
});
|
|
872
|
+
document.querySelectorAll('.ips-tt-all').forEach(function (b) {
|
|
873
|
+
b.addEventListener('click', function () {
|
|
874
|
+
var open = !!b.getAttribute('data-open');
|
|
875
|
+
rows.forEach(function (r) { if (r.getAttribute('data-tt-parent') !== null && r.classList.contains('ips-tt-parent')) { r.classList.toggle('ips-tt-closed', !open); } });
|
|
876
|
+
refresh();
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
})();
|
|
880
|
+
</script>`;
|
|
881
|
+
return h;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ---- the analysis view ----
|
|
885
|
+
|
|
886
|
+
/** One finding, as a list item. */
|
|
887
|
+
function findingHtml(f) {
|
|
888
|
+
if (f.url) {
|
|
889
|
+
const what = f.kind === 'modifier' ? 'Modifier extension' : 'Extension';
|
|
890
|
+
// known or not; the definition is on hover
|
|
891
|
+
const def = f.found ? [text(f.title), text(f.description)].filter(Boolean).join(': ') || 'The definition says nothing about it' : '';
|
|
892
|
+
const known = f.found ? `<span class="ips-an-known" title="${escape(def)}">known</span>` : '<i>not known</i>';
|
|
893
|
+
return `<li class="ips-an-${escape(f.kind)}">${what} <code>${escape(f.url)}</code> on <code>${escape(f.path)}</code>: ${known}</li>`;
|
|
894
|
+
}
|
|
895
|
+
return `<li class="ips-an-${escape(f.kind)}">${escape(f.text)}</li>`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Whether the analysis found nothing to say about a node: All OK, and nothing else but the
|
|
900
|
+
* narrative - which doesn't matter for a resource, but a section without narrative is a problem -
|
|
901
|
+
* and the Composition's negation (unless there's more than one approach: that's attention).
|
|
902
|
+
*/
|
|
903
|
+
function analysisOk(findings, isSection) {
|
|
904
|
+
if (!Array.isArray(findings) || !findings.some(f => f && f.kind === 'ok')) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
return findings.every(f => f && (f.kind === 'ok' || f.kind === 'negation' || (f.kind === 'narrative' && !(isSection && f.text === 'No narrative'))));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* The analysis view: the document's structure (structure.js) as a flat table - each node's
|
|
912
|
+
* row as in the structure view, with its full path - and under each resource and section a
|
|
913
|
+
* row with what the analysis step found (analysis.js).
|
|
914
|
+
*/
|
|
915
|
+
function renderAnalysis(row, options = {}) {
|
|
916
|
+
const r = row.results?.analysis;
|
|
917
|
+
if (!r || (r.result.status && r.result.status !== 'done') || !isObject(r.result.items)) {
|
|
918
|
+
return noResultNote(row, 'analysis', 'There is no analysis of this document', options);
|
|
919
|
+
}
|
|
920
|
+
const top = structureTree(row.bundle);
|
|
921
|
+
if (!top) {
|
|
922
|
+
return '<p><i>The document has no Composition.</i></p>';
|
|
923
|
+
}
|
|
924
|
+
const items = r.result.items;
|
|
925
|
+
let rows = '';
|
|
926
|
+
let total = 0;
|
|
927
|
+
let quiet = 0;
|
|
928
|
+
const nationality = row.results?.nationality?.result || null;
|
|
929
|
+
let nationShown = false;
|
|
930
|
+
for (const [node] of flatten(top)) {
|
|
931
|
+
// a reference to something not in the document has no rows: there's nothing to analyse
|
|
932
|
+
if (node.kind === 'missing') {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const cls = node.kind === 'seen' ? ' ips-tt-seen' : '';
|
|
936
|
+
const idCell = node.id ? escape(node.id) : '';
|
|
937
|
+
const idTitle = node.ref ? ` title="${escape(`Reference: ${node.ref}`)}"` : '';
|
|
938
|
+
// nothing to say (or analysed elsewhere): hidden by "Only show problems"
|
|
939
|
+
const ok = node.kind === 'seen' || analysisOk(items[node.key], String(node.key || '').startsWith('s:'));
|
|
940
|
+
const okAttr = ok ? ' data-ok="1"' : '';
|
|
941
|
+
total++;
|
|
942
|
+
if (ok) {
|
|
943
|
+
quiet++;
|
|
944
|
+
}
|
|
945
|
+
rows += `<tr class="ips-an-node${cls}"${okAttr}><td class="ips-an-path"><img src="/assets/images/icon_${node.icon}.png" alt="" width="16" height="16"/> ${escape(node.fullPath)}</td>` +
|
|
946
|
+
`<td>${escape(node.type)}</td><td${idTitle}>${idCell}</td><td>${nodeDetails(node)}</td></tr>`;
|
|
947
|
+
let body;
|
|
948
|
+
if (node.kind === 'seen') {
|
|
949
|
+
body = `<i>Analysed at ${escape(node.shownAt)}</i>`;
|
|
950
|
+
} else if (Array.isArray(items[node.key])) {
|
|
951
|
+
// the Composition's first bullet: which nation the document is from, and why
|
|
952
|
+
const nation = node.type === 'Composition' && !nationShown && nationality ? `<li class="ips-an-nation">${escape(nationalityText(nationality))}</li>` : '';
|
|
953
|
+
if (node.type === 'Composition') {
|
|
954
|
+
nationShown = true;
|
|
955
|
+
}
|
|
956
|
+
body = `<ul>${nation}${items[node.key].map(findingHtml).join('')}</ul>`;
|
|
957
|
+
} else {
|
|
958
|
+
body = '<i>Not analysed</i>';
|
|
959
|
+
}
|
|
960
|
+
rows += `<tr class="ips-an-findings${cls}"${okAttr}><td colspan="4">${body}</td></tr>`;
|
|
961
|
+
}
|
|
962
|
+
let h = `<p class="ips-small">Analysed ${escape(r.made)}` + (typeof r.result.ms === 'number' ? ` in ${escape(String(r.result.ms))} ms` : '') + '</p>';
|
|
963
|
+
// the choice is remembered in the browser (localStorage); without script, everything shows
|
|
964
|
+
h += '<style>.ips-an.ips-an-problems tr[data-ok] { display: none; }</style>';
|
|
965
|
+
h += '<div class="ips-filter mb-2 ips-an-filter" hidden><label><input type="checkbox" id="ips-an-problems"/> Only show problems</label> ' +
|
|
966
|
+
`<span class="ips-small ips-an-count" hidden>Showing ${total - quiet} of ${total}</span></div>`;
|
|
967
|
+
h += '<table class="table table-condensed ips-an"><thead><tr><th>Path</th><th>Type</th><th>Id</th><th>Details</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
|
968
|
+
h += '<div class="ips-explain"><p class="ips-small">What the analysis step found in each resource of the document as submitted, in the order of the structure view, and in the Composition section by section (the Composition\'s first bullet is which nation the document is from, and why - the nationality step): ' +
|
|
969
|
+
'extensions (with their definitions, from the validator, where it knows them; textLink is left out), <span class="ips-an-modifier">modifier extensions and implicitRules</span>, ' +
|
|
970
|
+
'<span class="ips-an-status">records that are inactive, entered in error, of unknown status, or missing a status they should have</span>, ' +
|
|
971
|
+
'<span class="ips-an-subject">a subject that is not the document\'s, or a focus</span>, ' +
|
|
972
|
+
'<span class="ips-an-attention">dates in the future where none is expected (not period ends, expiry or due dates, schedules, nor appointments, requests and plans that are not over, or events still to come), a Patient\'s links, a Composition that is not final, and a document that uses more than one approach to negation</span>, ' +
|
|
973
|
+
'a section\'s emptyReason or an author other than its container\'s, whether there is narrative, and - on the Composition - how the document says there is nothing to report (negation): with a section\'s emptyReason (the IPS pattern), with entries whose code says so, like No known allergy (the US Core pattern), or not at all; and <span class="ips-an-error">SNOMED CT codes that do not say which edition they are from, when the nationality of the document is unknown (the International Edition is assumed)</span>. A resource with nothing else to say is All OK. References to resources that are not in the document are left out. ' +
|
|
974
|
+
'An extension is known when the validator has its definition (hover over "known" to see it). Only show problems hides what is All OK - and, for a resource, whether it has narrative doesn\'t matter, but a section without narrative is a problem.</p></div>';
|
|
975
|
+
h += `<script>
|
|
976
|
+
(function () {
|
|
977
|
+
var KEY = 'fhirsmith.ips.analysis.problemsOnly';
|
|
978
|
+
var box = document.getElementById('ips-an-problems');
|
|
979
|
+
var table = document.querySelector('.ips-an');
|
|
980
|
+
var count = document.querySelector('.ips-an-count');
|
|
981
|
+
if (!box || !table) { return; }
|
|
982
|
+
box.closest('.ips-an-filter').hidden = false;
|
|
983
|
+
function apply() {
|
|
984
|
+
table.classList.toggle('ips-an-problems', box.checked);
|
|
985
|
+
count.hidden = !box.checked;
|
|
986
|
+
}
|
|
987
|
+
try { box.checked = window.localStorage.getItem(KEY) === '1'; } catch (e) { /* no storage: unchecked */ }
|
|
988
|
+
apply();
|
|
989
|
+
box.addEventListener('change', function () {
|
|
990
|
+
try { window.localStorage.setItem(KEY, box.checked ? '1' : '0'); } catch (e) { /* not remembered */ }
|
|
991
|
+
apply();
|
|
992
|
+
});
|
|
993
|
+
})();
|
|
994
|
+
</script>`;
|
|
995
|
+
return h;
|
|
996
|
+
}
|
|
997
|
+
|
|
692
998
|
// ---- the terminology view ----
|
|
693
999
|
|
|
694
1000
|
/** Text, as a link when there is somewhere to go. */
|
|
@@ -862,7 +1168,8 @@ document.querySelectorAll('.ips-copy').forEach(function (b) {
|
|
|
862
1168
|
|
|
863
1169
|
// ---- the validation view ----
|
|
864
1170
|
|
|
865
|
-
|
|
1171
|
+
// badge colours carry white text: each is at least 4.5:1 against it (WCAG 1.4.3)
|
|
1172
|
+
const SEVERITY_COLOURS = { fatal: '#c82333', error: '#c82333', warning: '#8a6100', information: '#117a8b' };
|
|
866
1173
|
|
|
867
1174
|
/** Where an issue is: its expression (FHIRPath), else its location. */
|
|
868
1175
|
function issueWhere(i) {
|
|
@@ -908,7 +1215,7 @@ function noResultNote(row, step, what, options = {}) {
|
|
|
908
1215
|
const PROFILE_OUTCOMES = {
|
|
909
1216
|
conforms: ['Conforms', '#1e7e34'],
|
|
910
1217
|
fails: ['Does not conform', '#c82333'],
|
|
911
|
-
unknown: ['Could not be fully checked', '#
|
|
1218
|
+
unknown: ['Could not be fully checked', '#8a6100']
|
|
912
1219
|
};
|
|
913
1220
|
|
|
914
1221
|
/**
|
|
@@ -951,13 +1258,16 @@ const TRANSLATION_OUTCOMES = {
|
|
|
951
1258
|
'no-translation': ['No translation', '#c82333'],
|
|
952
1259
|
failed: ['Failed', '#c82333'],
|
|
953
1260
|
'not-needed': ['Not needed', '#6c757d'],
|
|
954
|
-
'not-checked': ['Not checked', '#
|
|
1261
|
+
'not-checked': ['Not checked', '#a35200'],
|
|
955
1262
|
'no-codings': ['No codings', '#6c757d']
|
|
956
1263
|
};
|
|
957
1264
|
|
|
958
1265
|
/** A coding, briefly: system|code "display". */
|
|
959
1266
|
function briefCoding(c) {
|
|
960
|
-
|
|
1267
|
+
// the version too (for SNOMED CT, the edition): the same code in two editions is two codings
|
|
1268
|
+
const v = describeVersion(c.version);
|
|
1269
|
+
const ver = v.text ? ` <small title="${escape(v.title ? `${v.title} (${c.version})` : c.version)}">[${escape(v.text)}]</small>` : '';
|
|
1270
|
+
return `<code>${escape(`${systemName(c.system) || c.system || ''}|${c.code}`)}</code>${ver}` + (typeof c.display === 'string' && c.display ? ` "${escape(c.display)}"` : '');
|
|
961
1271
|
}
|
|
962
1272
|
|
|
963
1273
|
/**
|
|
@@ -977,6 +1287,11 @@ function renderTranslation(row, options = {}) {
|
|
|
977
1287
|
(typeof v.ms === 'number' ? ` in ${escape(String(v.ms))} ms` : '') + '</p>';
|
|
978
1288
|
h += `<p><b>${escape(`${counts.translated || 0} of ${counts.tried || 0} concepts translated (${counts.codings || 0} codings added)`)}` +
|
|
979
1289
|
(counts.failed ? escape(`, ${counts.failed} failed`) : '') + '</b></p>';
|
|
1290
|
+
if (v.defaultEdition && v.defaultEdition.edition) {
|
|
1291
|
+
const d = v.defaultEdition;
|
|
1292
|
+
h += `<p class="ips-small">SNOMED CT codes with no version were translated as codes of the ${editionHtml(d.edition)} - the SNOMED CT edition of the national profile ` +
|
|
1293
|
+
`<code>${escape(`${d.packageId}#${d.version}`)}</code>, for ${escape(d.jurisdiction)}, the nation the document is from.</p>`;
|
|
1294
|
+
}
|
|
980
1295
|
h += '<table class="table table-sm table-striped ips-table"><tr><th>Element</th><th>Codings</th><th>Value set</th><th>Outcome</th><th>Detail</th></tr>';
|
|
981
1296
|
for (const e of v.entries) {
|
|
982
1297
|
const [label, colour] = TRANSLATION_OUTCOMES[e.outcome] || [e.outcome || '?', '#6c757d'];
|
|
@@ -1016,7 +1331,8 @@ function renderGenerated(row, options = {}) {
|
|
|
1016
1331
|
if (checkNarrative(v.html)) {
|
|
1017
1332
|
return h + '<p><i>The generated narrative is not shown: it failed the active content check.</i></p>';
|
|
1018
1333
|
}
|
|
1019
|
-
|
|
1334
|
+
// the presenter's narrative is a document of its own (h1 title, h2 sections): seated under the page's h2
|
|
1335
|
+
h += `<div class="ips-narrative ips-generated">${parse5.serialize(seatHeadings(parse5.parseFragment(v.html), 3))}</div>`;
|
|
1020
1336
|
h += '<div class="ips-explain"><p class="ips-small">The narrative this server generates for the document (the IPS presenter): the Composition\'s header and the patient, ' +
|
|
1021
1337
|
'then the sections in the IPS order, each a table of its entries with the columns that suit the resource type (the ones no entry fills are left out) and the nested parts as bullets beneath. ' +
|
|
1022
1338
|
'Coded values show their text, else a display, else what the terminology server knows, else --. It is also written into the analysed copy as the Composition\'s and the sections\' narrative, which is what the validator sees.</p></div>';
|
|
@@ -1026,8 +1342,8 @@ function renderGenerated(row, options = {}) {
|
|
|
1026
1342
|
const tokens = (n) => (typeof n === 'number' ? n.toLocaleString('en-US') : '?');
|
|
1027
1343
|
const usd = (n) => (n < 0.01 && n > 0 ? `$${n.toFixed(4)}` : `$${n.toFixed(2)}`);
|
|
1028
1344
|
|
|
1029
|
-
const AI_SEVERITY_COLOURS = { high: '#c82333', medium: '#
|
|
1030
|
-
const GRADE_COLOURS = { A: '#1e7e34', B: '#
|
|
1345
|
+
const AI_SEVERITY_COLOURS = { high: '#c82333', medium: '#8a6100', low: '#117a8b' };
|
|
1346
|
+
const GRADE_COLOURS = { A: '#1e7e34', B: '#4e7d0e', C: '#8a6100', D: '#a35200', F: '#c82333' };
|
|
1031
1347
|
|
|
1032
1348
|
/**
|
|
1033
1349
|
* The AI assessment view: the grade, the summary, and the findings.
|
|
@@ -1058,7 +1374,7 @@ function renderAi(row, options = {}) {
|
|
|
1058
1374
|
}
|
|
1059
1375
|
h += `<p><span class="ips-badge ips-grade" style="background-color: ${GRADE_COLOURS[a.grade] || '#6c757d'}">Grade ${escape(a.grade || '?')}</span> ${escape(a.summary || '')}</p>`;
|
|
1060
1376
|
const sev = (x) => `<span class="ips-badge" style="background-color: ${AI_SEVERITY_COLOURS[x] || '#6c757d'}">${escape(x)}</span>`;
|
|
1061
|
-
h += '<
|
|
1377
|
+
h += '<h3>Consistency and integrity</h3>';
|
|
1062
1378
|
if (a.consistency.length === 0) {
|
|
1063
1379
|
h += '<p><i>No findings</i></p>';
|
|
1064
1380
|
} else {
|
|
@@ -1074,15 +1390,7 @@ function renderAi(row, options = {}) {
|
|
|
1074
1390
|
h += '<table class="table table-sm table-striped ips-table"><tr><th>Severity</th><th>Finding</th><th>Codes</th></tr>' +
|
|
1075
1391
|
a.terminology.map(x => `<tr><td>${sev(x.severity)}</td><td>${escape(x.finding)}</td><td>${x.codes.map(c => `<code>${escape(c)}</code>`).join('<br/>')}</td></tr>`).join('') + '</table>';
|
|
1076
1392
|
}
|
|
1077
|
-
h += '
|
|
1078
|
-
`<pre class="ips-json">${escape(String(v.raw || ''))}</pre></details>`;
|
|
1079
|
-
h += `<script>
|
|
1080
|
-
document.querySelectorAll('.ips-oo-toggle').forEach(function (b) {
|
|
1081
|
-
var d = b.closest('details');
|
|
1082
|
-
b.addEventListener('click', function (e) { e.preventDefault(); d.open = !d.open; });
|
|
1083
|
-
d.addEventListener('toggle', function () { b.textContent = (d.open ? 'Hide' : 'Show') + " the model's answer"; });
|
|
1084
|
-
});
|
|
1085
|
-
</script>`;
|
|
1393
|
+
h += showHide("the model's answer", `<pre class="ips-json">${escape(String(v.raw || ''))}</pre>`);
|
|
1086
1394
|
h += '<div class="ips-explain"><p class="ips-small">An AI service\'s assessment of the document (the analysed copy, with the narrative as submitted or without any, as the project\'s settings say): its consistency and integrity as a whole - do its details agree with each other and the overall picture, and is anything missing; with the narrative, what it says that the data does not back up - ' +
|
|
1087
1395
|
'and its terminology - are the codes consistent with each other and with their text. The grade runs A (consistent and complete) to F (not usable). It is the model\'s opinion, to be read as such.</p></div>';
|
|
1088
1396
|
return h;
|
|
@@ -1109,8 +1417,9 @@ function renderValidation(row, options = {}) {
|
|
|
1109
1417
|
* each national profile's page show it.
|
|
1110
1418
|
* @param {string} made - when the validation was done
|
|
1111
1419
|
* @param {{counts?: Object, outcome?: Object, profiles?: string[], validatorVersion?: string, ms?: number}} v
|
|
1112
|
-
* @param {{level?: string, docUrl?: string, hidden?: Object}} options - hidden: the
|
|
1113
|
-
* level form keeps (the view, and which national profile)
|
|
1420
|
+
* @param {{level?: string, defaultLevel?: string, docUrl?: string, hidden?: Object}} options - hidden: the
|
|
1421
|
+
* fields the level form keeps (the view, and which national profile); defaultLevel: the level when
|
|
1422
|
+
* none is asked for ('information' - everything - unless given)
|
|
1114
1423
|
*/
|
|
1115
1424
|
function validationOutcome(made, v, options = {}) {
|
|
1116
1425
|
const counts = v.counts || {};
|
|
@@ -1123,7 +1432,8 @@ function validationOutcome(made, v, options = {}) {
|
|
|
1123
1432
|
if (all.length === 0) {
|
|
1124
1433
|
return h;
|
|
1125
1434
|
}
|
|
1126
|
-
|
|
1435
|
+
// as asked for; else the view's own default (everything, unless it says otherwise)
|
|
1436
|
+
const level = LEVELS.some(([l]) => l === options.level) ? options.level : (options.defaultLevel || 'information');
|
|
1127
1437
|
const min = SEVERITY_RANK[level];
|
|
1128
1438
|
const issues = all.filter(i => (SEVERITY_RANK[text(i.severity)] ?? 3) >= min);
|
|
1129
1439
|
// the minimum level: a form that submits itself on change (and has a button without script)
|
|
@@ -1147,24 +1457,34 @@ function validationOutcome(made, v, options = {}) {
|
|
|
1147
1457
|
`<td>${escape(issueMessage(i))}</td></tr>`;
|
|
1148
1458
|
}
|
|
1149
1459
|
h += '</table>';
|
|
1150
|
-
// the raw OperationOutcome, behind a button
|
|
1151
|
-
h += '
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1460
|
+
// the raw OperationOutcome, behind a button
|
|
1461
|
+
h += showHide('the OperationOutcome', `<pre class="ips-json">${escape(JSON.stringify(v.outcome, null, 2))}</pre>`);
|
|
1462
|
+
return h;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Content behind a Show / Hide button: a <details> whose summary is styled as the button.
|
|
1467
|
+
* The summary itself is the control - a <button> inside it would be a control nested in a
|
|
1468
|
+
* control, which assistive technology can't make sense of (WCAG 4.1.2) - and it works from
|
|
1469
|
+
* the keyboard and without scripts. The script only changes its label to Hide when it's open.
|
|
1470
|
+
* @param {string} what - what is shown ('the OperationOutcome'), plain text
|
|
1471
|
+
* @param {string} body - the content, HTML
|
|
1472
|
+
*/
|
|
1473
|
+
function showHide(what, body) {
|
|
1474
|
+
return `<details class="ips-oo" data-what="${escape(what)}"><summary class="btn btn-default ips-btn">Show ${escape(what)}</summary>${body}</details>` +
|
|
1475
|
+
`<script>
|
|
1476
|
+
document.querySelectorAll('details.ips-oo:not([data-bound])').forEach(function (d) {
|
|
1477
|
+
d.setAttribute('data-bound', 'true');
|
|
1478
|
+
d.addEventListener('toggle', function () { d.querySelector('summary').textContent = (d.open ? 'Hide ' : 'Show ') + d.getAttribute('data-what'); });
|
|
1158
1479
|
});
|
|
1159
1480
|
</script>`;
|
|
1160
|
-
return h;
|
|
1161
1481
|
}
|
|
1162
1482
|
|
|
1163
1483
|
// a national profile's outcome on the page: [label, colour]
|
|
1164
1484
|
const NATIONAL_OUTCOMES = {
|
|
1165
1485
|
pass: ['Passed', '#1e7e34'],
|
|
1166
1486
|
fail: ['Failed', '#c82333'],
|
|
1167
|
-
unknown: ['Not checked', '#
|
|
1487
|
+
unknown: ['Not checked', '#8a6100']
|
|
1168
1488
|
};
|
|
1169
1489
|
|
|
1170
1490
|
/**
|
|
@@ -1196,7 +1516,8 @@ function renderNational(row, options = {}) {
|
|
|
1196
1516
|
return h + `<p><i>The document could not be validated against this profile${options.canManage && p.error ? `: ${escape(p.error)}` : ' (the reason is shown to the project\'s managers)'}.</i></p>`;
|
|
1197
1517
|
}
|
|
1198
1518
|
return h + validationOutcome(r.made, { counts: p.counts, outcome: p.outcome, profiles: [p.url], validatorVersion: v.validatorVersion, ms: p.ms },
|
|
1199
|
-
|
|
1519
|
+
// a national profile's issues are mostly about the national IG's own rules: errors only, unless asked
|
|
1520
|
+
{ ...options, defaultLevel: 'error', hidden: { view: 'national', np: String(index) } });
|
|
1200
1521
|
}
|
|
1201
1522
|
let h = '<table class="table table-sm ips-table"><tr><th>Jurisdiction</th><th>Package</th><th>Outcome</th><th>Issues</th></tr>';
|
|
1202
1523
|
v.profiles.forEach((np, i) => {
|
|
@@ -1245,9 +1566,27 @@ function jurisdictionHtml(code) {
|
|
|
1245
1566
|
return `<code>${escape(code)}</code>` + (j ? ` ${escape(j.display)}` : '');
|
|
1246
1567
|
}
|
|
1247
1568
|
|
|
1569
|
+
/** A SNOMED CT edition by name, with its module id on hover; '' for none. */
|
|
1570
|
+
function editionHtml(edition) {
|
|
1571
|
+
if (!edition) {
|
|
1572
|
+
return '';
|
|
1573
|
+
}
|
|
1574
|
+
return `<span title="${escape(`http://snomed.info/sct/${edition}`)}">${escape(editionName(edition) || edition)}</span>`;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/** The SNOMED CT edition select for a national profile: none, or one of the editions. */
|
|
1578
|
+
function editionSelect(form, edition) {
|
|
1579
|
+
let h = `<select form="${form}" name="snomedEdition" class="form-control" style="font-size: 85%; width: 14em" title="The SNOMED CT edition the IG takes for granted, if any">` +
|
|
1580
|
+
`<option value=""${edition ? '' : ' selected'}>(none)</option>`;
|
|
1581
|
+
for (const ed of SNOMED_LINKABLE_EDITIONS) {
|
|
1582
|
+
h += `<option value="${escape(ed)}"${ed === edition ? ' selected' : ''}>${escape(editionName(ed) || ed)}</option>`;
|
|
1583
|
+
}
|
|
1584
|
+
return h + '</select>';
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1248
1587
|
/**
|
|
1249
|
-
* The national profiles: for each, the national IG's package (id and version)
|
|
1250
|
-
* its profile on Bundle. Everyone sees the list; the administrator adds, edits and deletes.
|
|
1588
|
+
* The national profiles: for each, the national IG's package (id and version), the url of
|
|
1589
|
+
* its profile on Bundle, and the SNOMED CT edition it takes for granted (if any). Everyone sees the list; the administrator adds, edits and deletes.
|
|
1251
1590
|
* @param {Array} list - from store.nationalProfiles()
|
|
1252
1591
|
*/
|
|
1253
1592
|
function renderNationalProfiles(list, ctx, message) {
|
|
@@ -1256,7 +1595,7 @@ function renderNationalProfiles(list, ctx, message) {
|
|
|
1256
1595
|
let h = STYLE + userBar(ctx) + `<p class="ips-small"><a href="${base}">IPS projects</a> » National profiles</p>` + alert(message);
|
|
1257
1596
|
h += '<div class="ips-banner"><b class="ips-project-name">National profiles</b></div>';
|
|
1258
1597
|
h += '<p>A national profile is the profile a national implementation guide defines for an IPS document: the country or region it is for, the IG\'s package (its id and version), ' +
|
|
1259
|
-
'
|
|
1598
|
+
'the canonical URL of its profile on Bundle, and the SNOMED CT edition the IG takes for granted (if it uses SNOMED CT). They apply across all projects.' + (isAdmin ? '' : ' Only the administrator can change them.') + '</p>';
|
|
1260
1599
|
if (list.length === 0) {
|
|
1261
1600
|
h += '<p>There are no national profiles yet.</p>';
|
|
1262
1601
|
}
|
|
@@ -1264,9 +1603,9 @@ function renderNationalProfiles(list, ctx, message) {
|
|
|
1264
1603
|
if (list.length === 0) {
|
|
1265
1604
|
return h;
|
|
1266
1605
|
}
|
|
1267
|
-
h += '<table class="table table-sm table-striped ips-table"><tr><th>Jurisdiction</th><th>Package</th><th>Version</th><th>Profile on Bundle</th></tr>';
|
|
1606
|
+
h += '<table class="table table-sm table-striped ips-table"><tr><th>Jurisdiction</th><th>Package</th><th>Version</th><th>Profile on Bundle</th><th>SNOMED CT Edition</th></tr>';
|
|
1268
1607
|
for (const np of list) {
|
|
1269
|
-
h += `<tr><td>${jurisdictionHtml(np.jurisdiction)}</td><td><code>${escape(np.packageId)}</code></td><td>${escape(np.version)}</td><td><code>${escape(np.url)}</code></td></tr>`;
|
|
1608
|
+
h += `<tr><td>${jurisdictionHtml(np.jurisdiction)}</td><td><code>${escape(np.packageId)}</code></td><td>${escape(np.version)}</td><td><code>${escape(np.url)}</code></td><td>${editionHtml(np.snomedEdition)}</td></tr>`;
|
|
1270
1609
|
}
|
|
1271
1610
|
return h + '</table>';
|
|
1272
1611
|
}
|
|
@@ -1276,9 +1615,10 @@ function renderNationalProfiles(list, ctx, message) {
|
|
|
1276
1615
|
(np && np.jurisdiction && jurisdiction(np.jurisdiction) ? `<span class="ips-small">${escape(jurisdiction(np.jurisdiction).display)}</span>` : '') + '</td>' +
|
|
1277
1616
|
`<td><input form="${form}" type="text" name="packageId" required class="form-control" style="font-size: 85%; width: 15em" placeholder="e.g. hl7.fhir.au.ips" value="${escape(np ? np.packageId : '')}"/></td>` +
|
|
1278
1617
|
`<td><input form="${form}" type="text" name="version" required class="form-control" style="font-size: 85%; width: 8em" placeholder="version" value="${escape(np ? np.version : '')}"/></td>` +
|
|
1279
|
-
`<td><input form="${form}" type="url" name="url" required class="form-control" style="font-size: 85%; width: 100%; min-width: 30em" placeholder="url of the profile on Bundle" value="${escape(np ? np.url : '')}"/></td
|
|
1618
|
+
`<td><input form="${form}" type="url" name="url" required class="form-control" style="font-size: 85%; width: 100%; min-width: 30em" placeholder="url of the profile on Bundle" value="${escape(np ? np.url : '')}"/></td>` +
|
|
1619
|
+
`<td>${editionSelect(form, np ? np.snomedEdition : null)}</td>`;
|
|
1280
1620
|
// the package id and version are short; the url takes the rest of the row
|
|
1281
|
-
h += '<table class="table table-sm ips-table"><tr><th>Jurisdiction</th><th>Package</th><th>Version</th><th style="width: 100%">Profile on Bundle</th><th></th></tr>';
|
|
1621
|
+
h += '<table class="table table-sm ips-table"><tr><th>Jurisdiction</th><th>Package</th><th>Version</th><th style="width: 100%">Profile on Bundle</th><th>SNOMED CT Edition</th><th></th></tr>';
|
|
1282
1622
|
for (const np of list) {
|
|
1283
1623
|
const form = `np-${np.id}`;
|
|
1284
1624
|
const action = `${base}/national-profiles/${np.id}`;
|
|
@@ -1624,19 +1964,40 @@ function sharePanelScript() {
|
|
|
1624
1964
|
</script>`;
|
|
1625
1965
|
}
|
|
1626
1966
|
|
|
1627
|
-
// the views of a document
|
|
1967
|
+
// the views of a document, as tabs
|
|
1628
1968
|
const VIEWS = [
|
|
1629
1969
|
['narrative', 'Narrative'],
|
|
1970
|
+
['structure', 'Structure'],
|
|
1971
|
+
['analysis', 'Analysis'],
|
|
1630
1972
|
['translation', 'Translation'],
|
|
1631
|
-
['
|
|
1973
|
+
['terminology', 'Terminology'],
|
|
1632
1974
|
['validation', 'Validation'],
|
|
1633
1975
|
['national', 'National Profiles'],
|
|
1634
|
-
['terminology', 'Terminology'],
|
|
1635
1976
|
['profiles', 'Clinical Profiles'],
|
|
1636
1977
|
['ai', 'AI Assessment'],
|
|
1637
|
-
['
|
|
1978
|
+
['source', 'Source']
|
|
1638
1979
|
];
|
|
1639
1980
|
|
|
1981
|
+
// views that were tabs once, so old links still land: generated is now the narrative
|
|
1982
|
+
// tab's switch, json is now source
|
|
1983
|
+
const VIEW_ALIASES = {
|
|
1984
|
+
generated: { view: 'narrative', narrative: 'generated' },
|
|
1985
|
+
json: { view: 'source' }
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* The view a request asks for (?view=, and ?narrative= for the narrative tab's switch):
|
|
1990
|
+
* the default when it names none we know.
|
|
1991
|
+
* @returns {{view: string, narrative: string}} narrative is 'provided' or 'generated'
|
|
1992
|
+
*/
|
|
1993
|
+
function viewOf(query = {}) {
|
|
1994
|
+
const v = typeof query.view === 'string' ? query.view : '';
|
|
1995
|
+
const alias = VIEW_ALIASES[v];
|
|
1996
|
+
const view = alias ? alias.view : VIEWS.some(([k]) => k === v) ? v : VIEWS[0][0];
|
|
1997
|
+
const narrative = alias?.narrative || (query.narrative === 'generated' ? 'generated' : 'provided');
|
|
1998
|
+
return { view, narrative };
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1640
2001
|
/**
|
|
1641
2002
|
* A document.
|
|
1642
2003
|
* @param {Object} project
|
|
@@ -1669,16 +2030,36 @@ function stepState(row, step) {
|
|
|
1669
2030
|
return { state: 'waiting', message: null };
|
|
1670
2031
|
}
|
|
1671
2032
|
|
|
1672
|
-
|
|
1673
|
-
|
|
2033
|
+
// each step as the pipeline shows it, and the view that shows its work (as a query)
|
|
2034
|
+
const STEP_LABELS = {
|
|
2035
|
+
nationality: ['Nationality', 'view=analysis'],
|
|
2036
|
+
analysis: ['Analysis', 'view=analysis'],
|
|
2037
|
+
translate: ['Translate', 'view=translation'],
|
|
2038
|
+
narrative: ['Narrative', 'view=narrative&narrative=generated'],
|
|
2039
|
+
validation: ['Validation', 'view=validation'],
|
|
2040
|
+
national: ['National Profiles', 'view=national'],
|
|
2041
|
+
terminology: ['Terminology', 'view=terminology'],
|
|
2042
|
+
profiles: ['Clinical Profiles', 'view=profiles'],
|
|
2043
|
+
ai: ['AI Assessment', 'view=ai']
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* The processing pipeline as one connected line: the document's receipt, then each step
|
|
2048
|
+
* in the order they run, each coloured by where it is and linked to the view of its work.
|
|
2049
|
+
*/
|
|
2050
|
+
function stepsHtml(row, ctx, durl) {
|
|
2051
|
+
let h = `<span class="ips-step ips-step-received" title="${escape(`received ${row.received} (${row.source})`)}">Received</span>`;
|
|
1674
2052
|
for (const step of ctx.steps || []) {
|
|
1675
2053
|
const { state, message } = stepState(row, step);
|
|
1676
2054
|
const [, symbol, word] = STEP_STATES[state];
|
|
1677
2055
|
// a failure's reason can name the inside of the server: for managers
|
|
1678
2056
|
const detail = message && (state !== 'failed' || ctx.canManage) ? `: ${message}` : '';
|
|
1679
|
-
|
|
2057
|
+
const [label, query] = STEP_LABELS[step] || [step, null];
|
|
2058
|
+
const attrs = `class="ips-step ips-step-${state}" title="${escape(`${step} - ${word}${detail}`)}"`;
|
|
2059
|
+
const inner = `<span class="ips-step-icon">${symbol}</span> ${escape(label)}`;
|
|
2060
|
+
h += query && durl ? `<a ${attrs} href="${escape(`${durl}?${query}`)}">${inner}</a>` : `<span ${attrs}>${inner}</span>`;
|
|
1680
2061
|
}
|
|
1681
|
-
return h
|
|
2062
|
+
return `<div class="ips-pipeline">${h}</div>`;
|
|
1682
2063
|
}
|
|
1683
2064
|
|
|
1684
2065
|
function renderDocument(project, row, view, ctx) {
|
|
@@ -1686,8 +2067,8 @@ function renderDocument(project, row, view, ctx) {
|
|
|
1686
2067
|
const durl = `${purl}/Bundle/${row.id}`;
|
|
1687
2068
|
let h = STYLE + userBar(ctx) + crumbs(ctx, project, `Document ${row.id}`);
|
|
1688
2069
|
|
|
1689
|
-
// the banner: who and what
|
|
1690
|
-
h += '<div class="ips-banner">';
|
|
2070
|
+
// the banner: where from (a flag), who and what
|
|
2071
|
+
h += '<div class="ips-banner ips-banner-flagged">' + nationalityFlag(row.results?.nationality?.result) + '<div>';
|
|
1691
2072
|
h += `<div class="ips-patient"><b>${escape(row.patient_name || '(no name)')}</b>` +
|
|
1692
2073
|
(row.patient_gender ? `, ${escape(row.patient_gender)}` : '') +
|
|
1693
2074
|
(row.patient_birth ? `, born ${escape(row.patient_birth)}` : '') + '</div>';
|
|
@@ -1695,13 +2076,14 @@ function renderDocument(project, row, view, ctx) {
|
|
|
1695
2076
|
(row.comp_date ? `Document date ${escape(row.comp_date)} | ` : '') +
|
|
1696
2077
|
`Received ${escape(row.received)} (${escape(row.source)}) | ${row.size.toLocaleString()} bytes` +
|
|
1697
2078
|
` | <a href="${durl}?_format=json">FHIR JSON</a> | <a href="${durl}?download=1">Download</a></div>`;
|
|
1698
|
-
h += '</div>';
|
|
2079
|
+
h += '</div></div>';
|
|
1699
2080
|
|
|
1700
|
-
// the steps,
|
|
1701
|
-
h += '<div class="ips-steps">' + stepsHtml(row, ctx);
|
|
2081
|
+
// the steps, then (on a line of their own) what can be done
|
|
2082
|
+
h += '<div class="ips-steps">' + stepsHtml(row, ctx, durl) + '</div>';
|
|
1702
2083
|
{
|
|
1703
|
-
h += '<
|
|
1704
|
-
|
|
2084
|
+
h += '<div class="ips-doc-actions">';
|
|
2085
|
+
// a form like the others, so it looks like them
|
|
2086
|
+
h += `<form method="get" action="${purl}/compose" class="ips-inline"><input type="hidden" name="from" value="${row.id}"/><button type="submit" class="btn btn-default" title="Open this document in the editor; submitting makes a new document">Edit</button></form>`;
|
|
1705
2087
|
if (ctx.canShare) {
|
|
1706
2088
|
// sharing as a QR code: a form each (they work without scripts); with scripts, the
|
|
1707
2089
|
// page asks for JSON and shows the code in a panel over the page
|
|
@@ -1712,9 +2094,8 @@ function renderDocument(project, row, view, ctx) {
|
|
|
1712
2094
|
h += `<form method="post" action="${durl}/requeue" class="ips-inline">${csrfField(ctx.csrf)}<button type="submit" class="btn btn-default">Process again</button></form>` +
|
|
1713
2095
|
`<form method="post" action="${durl}/delete" class="ips-inline" data-confirm="${escape(`Delete document ${row.id}?`)}">${csrfField(ctx.csrf)}<button type="submit" class="btn btn-danger">Delete</button></form>`;
|
|
1714
2096
|
}
|
|
1715
|
-
h += '</
|
|
2097
|
+
h += '</div>';
|
|
1716
2098
|
}
|
|
1717
|
-
h += '</div>';
|
|
1718
2099
|
if (ctx.canShare) {
|
|
1719
2100
|
h += sharePanelScript();
|
|
1720
2101
|
}
|
|
@@ -1723,15 +2104,17 @@ function renderDocument(project, row, view, ctx) {
|
|
|
1723
2104
|
h += '<ul class="nav nav-tabs ips-nav">' + VIEWS.map(([v, label]) =>
|
|
1724
2105
|
`<li${v === view ? ' class="active"' : ''}><a href="${durl}?view=${v}">${label}</a></li>`).join('') + '</ul>';
|
|
1725
2106
|
|
|
1726
|
-
if (view === '
|
|
2107
|
+
if (view === 'structure') {
|
|
2108
|
+
h += renderStructure(row.bundle);
|
|
2109
|
+
} else if (view === 'analysis') {
|
|
2110
|
+
h += renderAnalysis(row, { canManage: ctx.canManage });
|
|
2111
|
+
} else if (view === 'source') {
|
|
1727
2112
|
h += `<pre class="ips-json">${escape(JSON.stringify(row.bundle, null, 2))}</pre>`;
|
|
1728
2113
|
} else if (view === 'terminology') {
|
|
1729
2114
|
// the analysed copy carries the checks; the concepts are the same
|
|
1730
2115
|
h += renderTerminology(row.analysed || row.bundle, { ...ctx, analysed: !!row.analysed, docUrl: durl });
|
|
1731
2116
|
} else if (view === 'translation') {
|
|
1732
2117
|
h += renderTranslation(row, { canManage: ctx.canManage });
|
|
1733
|
-
} else if (view === 'generated') {
|
|
1734
|
-
h += renderGenerated(row, { canManage: ctx.canManage });
|
|
1735
2118
|
} else if (view === 'profiles') {
|
|
1736
2119
|
h += renderProfileChecks(row, { base: ctx.base, canManage: ctx.canManage });
|
|
1737
2120
|
} else if (view === 'ai') {
|
|
@@ -1741,13 +2124,19 @@ function renderDocument(project, row, view, ctx) {
|
|
|
1741
2124
|
} else if (view === 'national') {
|
|
1742
2125
|
h += renderNational(row, { np: ctx.np, level: ctx.level, docUrl: durl, base: ctx.base, canManage: ctx.canManage });
|
|
1743
2126
|
} else {
|
|
1744
|
-
|
|
2127
|
+
// the narrative: as provided in the document, or as this server generates it
|
|
2128
|
+
const generated = ctx.narrative === 'generated';
|
|
2129
|
+
h += '<div class="ips-switch">' +
|
|
2130
|
+
`<a href="${durl}?view=narrative"${generated ? '' : ' class="active"'} title="The narrative in the document as it was submitted">Provided</a>` +
|
|
2131
|
+
`<a href="${durl}?view=narrative&narrative=generated"${generated ? ' class="active"' : ''} title="The narrative this server generates for the document">Generated</a></div>`;
|
|
2132
|
+
h += generated ? renderGenerated(row, { canManage: ctx.canManage }) : assembledNarrative(row.bundle);
|
|
1745
2133
|
}
|
|
1746
2134
|
return h;
|
|
1747
2135
|
}
|
|
1748
2136
|
|
|
1749
2137
|
module.exports = {
|
|
2138
|
+
analysisOk,
|
|
1750
2139
|
renderHome, renderLogin, renderProjects, renderUsers, renderProject, renderSubmit, renderSettings, renderDocument,
|
|
1751
|
-
assembledNarrative, renderTerminology, renderTranslation, renderGenerated, renderAi, renderValidation, renderNational, renderShare, renderCompose, renderProfileChecks,
|
|
1752
|
-
renderProfiles, renderProfile, renderProfileEditor, renderNationalProfiles, VIEWS
|
|
2140
|
+
assembledNarrative, renderStructure, renderAnalysis, renderTerminology, renderTranslation, renderGenerated, renderAi, renderValidation, renderNational, renderShare, renderCompose, renderProfileChecks,
|
|
2141
|
+
renderProfiles, renderProfile, renderProfileEditor, renderNationalProfiles, VIEWS, viewOf
|
|
1753
2142
|
};
|