fhirsmith 0.16.1 → 0.18.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/CHANGELOG.md +130 -0
- package/README.md +105 -30
- package/config-template.json +130 -4
- package/extension-tracker/extension-tracker.js +111 -3
- package/extension-tracker/readme.md +3 -0
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +422 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/html.js +9 -0
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +222 -0
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -2
- package/packages/package-crawler.js +189 -82
- package/packages/packages.js +36 -63
- package/packages/utilities/dedupe-versions.js +22 -8
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +48 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +247 -54
- package/registry/uptime.js +154 -0
- package/release.sh +80 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/readme.md +1 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +9 -0
- package/tx/README.md +52 -0
- package/tx/cm/cm-database.js +62 -0
- package/tx/cm/cm-package.js +20 -3
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +142 -19
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +79 -39
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +107 -6
- package/tx/ocl/README.md +62 -6
- package/tx/ocl/cm-ocl.cjs +166 -44
- package/tx/ocl/cs-ocl.cjs +163 -13
- package/tx/ocl/resolve/public-access.js +143 -0
- package/tx/ocl/resolve/reference-resolver.js +458 -0
- package/tx/ocl/vs-ocl.cjs +167 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/problems.js +7 -5
- package/tx/provider.js +21 -5
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +37 -8
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +148 -5
- package/tx/vs/vs-database.js +26 -0
- package/tx/vs/vs-package.js +42 -10
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +34 -7
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/metadata.js +6 -1
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +120 -12
- package/gr.sh +0 -3
- package/registry/readme.md +0 -124
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
|
|
3
|
+
//
|
|
4
|
+
// Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* The clinical profile editor, in the browser (the administrator's). The page gives it
|
|
9
|
+
* window.IPS_PROFILE: the profile (its title and constraints, each with the shape the server
|
|
10
|
+
* found in its expression, or none), the IPS sections, the named code systems and resource
|
|
11
|
+
* types, and the URLs it works with. The page is in three parts: required sections and entries
|
|
12
|
+
* (Bundle.section(...)[.entry(...) or .entryIn(valueSet...)].exists(), a row of the first
|
|
13
|
+
* table), rules about resources (Bundle.resources(...) or Bundle.resourcesIn(valueSet...), and
|
|
14
|
+
* the rest: a row of the second - type, system, code or value set, the rest), and
|
|
15
|
+
* other rules (FHIRPath). A row's expression is made from it. Saving posts the title and the
|
|
16
|
+
* constraints (key, description, expression); the server checks them, and keeps the keys.
|
|
17
|
+
*/
|
|
18
|
+
(function () {
|
|
19
|
+
'use strict';
|
|
20
|
+
var cfg = window.IPS_PROFILE;
|
|
21
|
+
var root = document.getElementById('ips-profile-editor');
|
|
22
|
+
var LOINC = cfg.sectionSystem;
|
|
23
|
+
var dirty = false;
|
|
24
|
+
var saving = false;
|
|
25
|
+
|
|
26
|
+
function el(tag, attrs, children) {
|
|
27
|
+
var e = document.createElement(tag);
|
|
28
|
+
Object.keys(attrs || {}).forEach(function (k) {
|
|
29
|
+
if (k === 'class') { e.className = attrs[k]; } else if (k === 'text') { e.textContent = attrs[k]; } else if (k.indexOf('on') === 0) { e.addEventListener(k.substring(2), attrs[k]); } else if (attrs[k] !== undefined && attrs[k] !== null && attrs[k] !== false) { e.setAttribute(k, attrs[k] === true ? '' : attrs[k]); }
|
|
30
|
+
});
|
|
31
|
+
(children || []).forEach(function (c) { if (c) { e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); } });
|
|
32
|
+
return e;
|
|
33
|
+
}
|
|
34
|
+
function changed() { dirty = true; }
|
|
35
|
+
|
|
36
|
+
// ---- the two shapes (as ips/profiles.js has them) ----
|
|
37
|
+
|
|
38
|
+
var STR = "'((?:[^'\\\\]|\\\\.)*)'";
|
|
39
|
+
var ARGS = '\\s*' + STR + '\\s*,\\s*' + STR + "\\s*(?:,\\s*(\\d{1,6})\\s*,\\s*(?:(\\d{1,6})|'\\*')\\s*)?";
|
|
40
|
+
var CARD = "(?:,\\s*(\\d{1,6})\\s*,\\s*(?:(\\d{1,6})|'\\*')\\s*)?";
|
|
41
|
+
var SHAPE_RE = new RegExp('^\\s*Bundle\\s*\\.\\s*section\\s*\\(' + ARGS + '\\)\\s*(?:\\.\\s*(?:entry\\s*\\(' + ARGS + '\\)|entryIn\\s*\\(\\s*' + STR + '\\s*' + CARD + '\\))\\s*)?\\.\\s*exists\\s*\\(\\s*\\)\\s*$');
|
|
42
|
+
var CANONICAL_RE = /^[a-z][a-z0-9+.-]*:[^\s|]{1,250}(\|[^\s|]{1,64})?$/i;
|
|
43
|
+
// the system chosen for an entry or resources when they are matched by value set, not code
|
|
44
|
+
var VS = '#vs';
|
|
45
|
+
// a FHIRPath string's escapes: \' \" \` \\ \/ \f \n \r \t and \uXXXX
|
|
46
|
+
function unquote(s) {
|
|
47
|
+
return s.replace(/\\(u[0-9a-fA-F]{4}|.)/g, function (m, c) {
|
|
48
|
+
return c.length === 5 ? String.fromCharCode(parseInt(c.substring(1), 16)) : ({ n: '\n', r: '\r', t: '\t', f: '\f' }[c] || c);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function quote(s) { return "'" + String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"; }
|
|
52
|
+
function card(min, max, hasCard) { return { min: hasCard ? parseInt(min, 10) : null, max: hasCard ? (max === undefined ? '*' : parseInt(max, 10)) : null }; }
|
|
53
|
+
function part(system, code, min, max, hasCard) {
|
|
54
|
+
var c = card(min, max, hasCard);
|
|
55
|
+
return { system: unquote(system), code: unquote(code), min: c.min, max: c.max };
|
|
56
|
+
}
|
|
57
|
+
function parseShape(expression) {
|
|
58
|
+
var m = SHAPE_RE.exec(expression || '');
|
|
59
|
+
if (!m) { return null; }
|
|
60
|
+
var entry = null;
|
|
61
|
+
if (m[5] !== undefined) { entry = part(m[5], m[6], m[7], m[8], m[7] !== undefined); } else if (m[9] !== undefined) {
|
|
62
|
+
var c = card(m[10], m[11], m[10] !== undefined);
|
|
63
|
+
entry = { valueSet: unquote(m[9]), min: c.min, max: c.max };
|
|
64
|
+
}
|
|
65
|
+
return { section: part(m[1], m[2], m[3], m[4], m[3] !== undefined), entry: entry };
|
|
66
|
+
}
|
|
67
|
+
function cardText(p) { return p.min === null || p.min === undefined ? '' : p.min + '..' + p.max; }
|
|
68
|
+
function parseCard(text) {
|
|
69
|
+
if (!text || !text.trim()) { return { min: null, max: null }; }
|
|
70
|
+
var m = /^\s*(\d{1,6})\s*\.\.\s*(\d{1,6}|\*)\s*$/.exec(text);
|
|
71
|
+
if (!m) { return null; }
|
|
72
|
+
var min = parseInt(m[1], 10);
|
|
73
|
+
var max = m[2] === '*' ? '*' : parseInt(m[2], 10);
|
|
74
|
+
return max !== '*' && max < min ? null : { min: min, max: max };
|
|
75
|
+
}
|
|
76
|
+
function cardArgs(c) { return c.min !== null ? ', ' + c.min + ', ' + (c.max === '*' ? "'*'" : c.max) : ''; }
|
|
77
|
+
function args(system, code, c) { return quote(system) + ', ' + quote(code) + cardArgs(c); }
|
|
78
|
+
var TYPE = "\\s*(?:,\\s*'([A-Z][A-Za-z]{1,63})'\\s*)?";
|
|
79
|
+
var REST = '\\s*(\\.\\s*\\S[\\s\\S]*?)\\s*$';
|
|
80
|
+
var RSHAPE_RE = new RegExp('^\\s*Bundle\\s*\\.\\s*resources\\s*\\(\\s*' + STR + '\\s*,\\s*' + STR + TYPE + '\\)' + REST);
|
|
81
|
+
var RSHAPE_IN_RE = new RegExp('^\\s*Bundle\\s*\\.\\s*resourcesIn\\s*\\(\\s*' + STR + TYPE + '\\)' + REST);
|
|
82
|
+
function parseResourceShape(expression) {
|
|
83
|
+
var m = RSHAPE_RE.exec(expression || '');
|
|
84
|
+
if (m) { return { type: m[3] || null, system: unquote(m[1]), code: unquote(m[2]), rest: m[4] }; }
|
|
85
|
+
m = RSHAPE_IN_RE.exec(expression || '');
|
|
86
|
+
return m ? { type: m[2] || null, valueSet: unquote(m[1]), rest: m[3] } : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- the rows: mode 'section' (the first table), 'resource' (the second) or 'text' ----
|
|
90
|
+
|
|
91
|
+
function rowFrom(c) {
|
|
92
|
+
var shape = c.shape || null;
|
|
93
|
+
var rshape = c.rshape || null;
|
|
94
|
+
var r = { key: c.key || null, human: c.human || '', expression: c.expression || '', mode: shape ? 'section' : (rshape ? 'resource' : 'text') };
|
|
95
|
+
tableFields(r, shape || { section: { system: LOINC, code: '', min: null, max: null }, entry: null });
|
|
96
|
+
resourceFields(r, rshape || { type: null, system: cfg.systems[0].url, code: '', rest: '.exists()' });
|
|
97
|
+
return r;
|
|
98
|
+
}
|
|
99
|
+
function resourceFields(r, rshape) {
|
|
100
|
+
var byVs = rshape.valueSet !== undefined;
|
|
101
|
+
r.resType = rshape.type || '';
|
|
102
|
+
r.resSystem = byVs ? VS : rshape.system;
|
|
103
|
+
r.resCode = byVs ? '' : rshape.code;
|
|
104
|
+
r.resDisplay = '';
|
|
105
|
+
r.resVs = byVs ? rshape.valueSet : '';
|
|
106
|
+
r.resVsTitle = '';
|
|
107
|
+
r.resRest = rshape.rest;
|
|
108
|
+
}
|
|
109
|
+
/** The expression from the second table's row, or null with what is wrong. */
|
|
110
|
+
function resourceExpression(r) {
|
|
111
|
+
var problems = [];
|
|
112
|
+
var rest = (r.resRest || '').trim();
|
|
113
|
+
var byVs = r.resSystem === VS;
|
|
114
|
+
if (r.resType && !/^[A-Z][A-Za-z]{1,63}$/.test(r.resType)) { problems.push('resType'); }
|
|
115
|
+
if (byVs) {
|
|
116
|
+
if (!CANONICAL_RE.test(r.resVs.trim())) { problems.push('resVs'); }
|
|
117
|
+
} else {
|
|
118
|
+
if (!r.resSystem.trim()) { problems.push('resSystem'); }
|
|
119
|
+
if (!r.resCode.trim()) { problems.push('resCode'); }
|
|
120
|
+
}
|
|
121
|
+
if (!rest || rest === '.') { problems.push('resRest'); }
|
|
122
|
+
if (problems.length) { return { expression: null, problems: problems }; }
|
|
123
|
+
var type = r.resType ? ', ' + quote(r.resType) : '';
|
|
124
|
+
var head = byVs ? 'Bundle.resourcesIn(' + quote(r.resVs.trim()) + type + ')' : 'Bundle.resources(' + quote(r.resSystem.trim()) + ', ' + quote(r.resCode.trim()) + type + ')';
|
|
125
|
+
return { expression: head + (rest.charAt(0) === '.' ? rest : '.' + rest), problems: [] };
|
|
126
|
+
}
|
|
127
|
+
function rowExpression(r) {
|
|
128
|
+
return r.mode === 'section' ? tableExpression(r) : (r.mode === 'resource' ? resourceExpression(r) : { expression: r.expression, problems: [] });
|
|
129
|
+
}
|
|
130
|
+
function tableFields(r, shape) {
|
|
131
|
+
var e = shape.entry;
|
|
132
|
+
r.secSystem = shape.section.system;
|
|
133
|
+
r.secCode = shape.section.code;
|
|
134
|
+
r.secCard = cardText(shape.section);
|
|
135
|
+
r.secDisplay = '';
|
|
136
|
+
r.entSystem = !e ? '' : (e.valueSet !== undefined ? VS : e.system);
|
|
137
|
+
r.entCode = e && e.valueSet === undefined ? e.code : '';
|
|
138
|
+
r.entVs = e && e.valueSet !== undefined ? e.valueSet : '';
|
|
139
|
+
r.entVsTitle = '';
|
|
140
|
+
r.entCard = e ? cardText(e) : '';
|
|
141
|
+
r.entDisplay = '';
|
|
142
|
+
}
|
|
143
|
+
/** The row's expression, from its table, or null with what is wrong. */
|
|
144
|
+
function tableExpression(r) {
|
|
145
|
+
var problems = [];
|
|
146
|
+
var sc = parseCard(r.secCard);
|
|
147
|
+
var ec = parseCard(r.entCard);
|
|
148
|
+
var byVs = r.entSystem === VS;
|
|
149
|
+
if (!r.secSystem.trim()) { problems.push('secSystem'); }
|
|
150
|
+
if (!r.secCode.trim()) { problems.push('secCode'); }
|
|
151
|
+
if (!sc) { problems.push('secCard'); }
|
|
152
|
+
if (r.entSystem.trim() || r.entCode.trim() || r.entCard.trim()) {
|
|
153
|
+
if (byVs) {
|
|
154
|
+
if (!CANONICAL_RE.test(r.entVs.trim())) { problems.push('entVs'); }
|
|
155
|
+
} else {
|
|
156
|
+
if (!r.entSystem.trim()) { problems.push('entSystem'); }
|
|
157
|
+
if (!r.entCode.trim()) { problems.push('entCode'); }
|
|
158
|
+
}
|
|
159
|
+
if (!ec) { problems.push('entCard'); }
|
|
160
|
+
}
|
|
161
|
+
if (problems.length) { return { expression: null, problems: problems }; }
|
|
162
|
+
var x = 'Bundle.section(' + args(r.secSystem.trim(), r.secCode.trim(), sc) + ')';
|
|
163
|
+
if (byVs) { x += '.entryIn(' + quote(r.entVs.trim()) + cardArgs(ec) + ')'; } else if (r.entSystem.trim()) { x += '.entry(' + args(r.entSystem.trim(), r.entCode.trim(), ec) + ')'; }
|
|
164
|
+
return { expression: x + '.exists()', problems: [] };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
var state = { title: cfg.profile.title || '', rows: (cfg.profile.constraints || []).map(rowFrom) };
|
|
168
|
+
|
|
169
|
+
// ---- a code, looked up in a system ----
|
|
170
|
+
|
|
171
|
+
function codeInput(r, codeField, displayField, systemOf, onChange) {
|
|
172
|
+
var wrap = el('div', { class: 'ips-ed-code' });
|
|
173
|
+
var input = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: cfg.urls.lookup ? 'code, or type to look one up' : 'code' });
|
|
174
|
+
input.value = r[codeField];
|
|
175
|
+
var display = el('div', { class: 'ips-pf-display', text: r[displayField] || '' });
|
|
176
|
+
var list = el('div', { class: 'ips-ed-suggestions', hidden: true });
|
|
177
|
+
var timer = null;
|
|
178
|
+
var asked = 0;
|
|
179
|
+
input.addEventListener('input', function () {
|
|
180
|
+
r[codeField] = input.value.trim();
|
|
181
|
+
r[displayField] = '';
|
|
182
|
+
display.textContent = '';
|
|
183
|
+
changed();
|
|
184
|
+
onChange();
|
|
185
|
+
var q = input.value.trim();
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
var system = systemOf();
|
|
188
|
+
if (q.length < 2 || !cfg.urls.lookup || !system) { list.hidden = true; return; }
|
|
189
|
+
timer = setTimeout(function () {
|
|
190
|
+
var mine = ++asked;
|
|
191
|
+
fetch(cfg.urls.lookup + '?system=' + encodeURIComponent(system) + '&q=' + encodeURIComponent(q), { headers: { Accept: 'application/json' }, credentials: 'same-origin' })
|
|
192
|
+
.then(function (res) { return res.json(); })
|
|
193
|
+
.then(function (j) {
|
|
194
|
+
if (mine !== asked) { return; }
|
|
195
|
+
list.innerHTML = '';
|
|
196
|
+
var found = j && j.matches ? j.matches : [];
|
|
197
|
+
found.forEach(function (c) {
|
|
198
|
+
list.appendChild(el('div', { class: 'ips-ed-suggestion', onmousedown: function (e) {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
input.value = c.code;
|
|
201
|
+
r[codeField] = c.code;
|
|
202
|
+
r[displayField] = c.display || '';
|
|
203
|
+
display.textContent = r[displayField];
|
|
204
|
+
list.hidden = true;
|
|
205
|
+
changed();
|
|
206
|
+
onChange();
|
|
207
|
+
} }, [c.display || c.code, ' ', el('small', { text: c.code })]));
|
|
208
|
+
});
|
|
209
|
+
if (!found.length) {
|
|
210
|
+
list.appendChild(el('div', { class: 'ips-ed-suggestion ips-ed-suggestion-text', text: j && j.error ? j.error : 'Nothing found - "' + q + '" is taken as the code' }));
|
|
211
|
+
}
|
|
212
|
+
list.hidden = false;
|
|
213
|
+
})
|
|
214
|
+
.catch(function () { list.hidden = true; });
|
|
215
|
+
}, 250);
|
|
216
|
+
});
|
|
217
|
+
input.addEventListener('blur', function () { setTimeout(function () { list.hidden = true; }, 150); });
|
|
218
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Escape') { list.hidden = true; } });
|
|
219
|
+
wrap.appendChild(input);
|
|
220
|
+
wrap.appendChild(list);
|
|
221
|
+
wrap.appendChild(display);
|
|
222
|
+
wrap.input = input;
|
|
223
|
+
return wrap;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---- a value set, found by title (this server's own, then the terminology server's), or a canonical typed in ----
|
|
227
|
+
|
|
228
|
+
function vsInput(r, field, titleField, onChange) {
|
|
229
|
+
var wrap = el('div', { class: 'ips-ed-code' });
|
|
230
|
+
var input = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'value set: search by title, or a canonical URL' });
|
|
231
|
+
input.value = r[field];
|
|
232
|
+
var title = el('div', { class: 'ips-pf-display' });
|
|
233
|
+
var list = el('div', { class: 'ips-ed-suggestions', hidden: true });
|
|
234
|
+
var timer = null;
|
|
235
|
+
var asked = 0;
|
|
236
|
+
function showTitle(text, warn) {
|
|
237
|
+
title.textContent = text || '';
|
|
238
|
+
title.style.color = warn ? '#b8860b' : '';
|
|
239
|
+
}
|
|
240
|
+
function lookup(query, then) {
|
|
241
|
+
fetch(cfg.urls.lookupVs + '?' + query, { headers: { Accept: 'application/json' }, credentials: 'same-origin' })
|
|
242
|
+
.then(function (res) { return res.json(); })
|
|
243
|
+
.then(then)
|
|
244
|
+
.catch(function () { then({ matches: [], error: 'The value sets could not be looked up' }); });
|
|
245
|
+
}
|
|
246
|
+
// what is there already: its title, or a warning that it can't be found
|
|
247
|
+
function identify() {
|
|
248
|
+
var url = r[field].trim();
|
|
249
|
+
if (!url || !CANONICAL_RE.test(url)) { showTitle(''); return; }
|
|
250
|
+
if (r[titleField]) { showTitle(r[titleField]); return; }
|
|
251
|
+
var mine = ++asked;
|
|
252
|
+
lookup('url=' + encodeURIComponent(url), function (j) {
|
|
253
|
+
if (mine !== asked) { return; }
|
|
254
|
+
var m = j && j.matches && j.matches[0];
|
|
255
|
+
if (m) { r[titleField] = m.title; showTitle(m.title); } else { showTitle(j && j.error ? j.error : 'Not found on the terminology server: it must be known to it when documents are checked', true); }
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
input.addEventListener('input', function () {
|
|
259
|
+
r[field] = input.value.trim();
|
|
260
|
+
r[titleField] = '';
|
|
261
|
+
showTitle('');
|
|
262
|
+
changed();
|
|
263
|
+
onChange();
|
|
264
|
+
var q = input.value.trim();
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
if (q.length < 2) { list.hidden = true; return; }
|
|
267
|
+
if (CANONICAL_RE.test(q)) {
|
|
268
|
+
list.hidden = true;
|
|
269
|
+
timer = setTimeout(identify, 400);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
timer = setTimeout(function () {
|
|
273
|
+
var mine = ++asked;
|
|
274
|
+
lookup('q=' + encodeURIComponent(q), function (j) {
|
|
275
|
+
if (mine !== asked) { return; }
|
|
276
|
+
list.innerHTML = '';
|
|
277
|
+
var found = j && j.matches ? j.matches : [];
|
|
278
|
+
found.forEach(function (v) {
|
|
279
|
+
list.appendChild(el('div', { class: 'ips-ed-suggestion', onmousedown: function (e) {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
input.value = v.url;
|
|
282
|
+
r[field] = v.url;
|
|
283
|
+
r[titleField] = v.title;
|
|
284
|
+
showTitle(v.title);
|
|
285
|
+
list.hidden = true;
|
|
286
|
+
changed();
|
|
287
|
+
onChange();
|
|
288
|
+
} }, [v.title, v.own ? ' (this server\'s)' : '', el('br'), el('small', { text: v.url + (v.version ? ' (version ' + v.version + ')' : '') })]));
|
|
289
|
+
});
|
|
290
|
+
if (!found.length) {
|
|
291
|
+
list.appendChild(el('div', { class: 'ips-ed-suggestion ips-ed-suggestion-text', text: j && j.error ? j.error : 'No value set has that in its title' }));
|
|
292
|
+
}
|
|
293
|
+
list.hidden = false;
|
|
294
|
+
});
|
|
295
|
+
}, 250);
|
|
296
|
+
});
|
|
297
|
+
input.addEventListener('blur', function () { setTimeout(function () { list.hidden = true; }, 150); });
|
|
298
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Escape') { list.hidden = true; } });
|
|
299
|
+
wrap.appendChild(input);
|
|
300
|
+
wrap.appendChild(list);
|
|
301
|
+
wrap.appendChild(title);
|
|
302
|
+
wrap.input = input;
|
|
303
|
+
identify();
|
|
304
|
+
return wrap;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function cardInput(r, field, onChange) {
|
|
308
|
+
var input = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'any', title: 'blank for any number, or min..max (1..1, 1..*)' });
|
|
309
|
+
input.value = r[field];
|
|
310
|
+
input.addEventListener('input', function () { r[field] = input.value; changed(); onChange(); });
|
|
311
|
+
return input;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// the section: one of the IPS sections, or any code in any system
|
|
315
|
+
function sectionCell(r, onChange) {
|
|
316
|
+
var cell = el('div');
|
|
317
|
+
var select = el('select', { class: 'form-control ips-ed-input' });
|
|
318
|
+
cfg.sections.forEach(function (s) { select.appendChild(el('option', { value: s.code, text: 'Section: ' + s.title })); });
|
|
319
|
+
select.appendChild(el('option', { value: '', text: 'Another code...' }));
|
|
320
|
+
var isIps = r.secSystem === LOINC && cfg.sections.some(function (s) { return s.code === r.secCode; });
|
|
321
|
+
if (!r.secCode) { r.secSystem = LOINC; r.secCode = cfg.sections[0].code; isIps = true; }
|
|
322
|
+
select.value = isIps ? r.secCode : '';
|
|
323
|
+
var other = el('div');
|
|
324
|
+
other.hidden = isIps;
|
|
325
|
+
var system = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'code system URL' });
|
|
326
|
+
system.value = r.secSystem;
|
|
327
|
+
system.addEventListener('input', function () { r.secSystem = system.value.trim(); changed(); onChange(); });
|
|
328
|
+
var code = codeInput(r, 'secCode', 'secDisplay', function () { return r.secSystem; }, onChange);
|
|
329
|
+
other.appendChild(el('div', { class: 'ips-pf-pair' }, [system]));
|
|
330
|
+
other.appendChild(code);
|
|
331
|
+
select.addEventListener('change', function () {
|
|
332
|
+
if (select.value) {
|
|
333
|
+
r.secSystem = LOINC;
|
|
334
|
+
r.secCode = select.value;
|
|
335
|
+
other.hidden = true;
|
|
336
|
+
} else {
|
|
337
|
+
other.hidden = false;
|
|
338
|
+
system.value = r.secSystem;
|
|
339
|
+
code.input.value = r.secCode;
|
|
340
|
+
}
|
|
341
|
+
changed();
|
|
342
|
+
onChange();
|
|
343
|
+
});
|
|
344
|
+
cell.appendChild(select);
|
|
345
|
+
cell.appendChild(other);
|
|
346
|
+
return cell;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// the entry: no entry, entries in a value set, or a code in a named system or any other
|
|
350
|
+
function entryCell(r, onChange) {
|
|
351
|
+
var cell = el('div');
|
|
352
|
+
var select = el('select', { class: 'form-control ips-ed-input' }, [el('option', { value: '', text: '(no entry)' }), el('option', { value: VS, text: 'In a value set...' })]);
|
|
353
|
+
cfg.systems.forEach(function (s) { select.appendChild(el('option', { value: s.url, text: s.name })); });
|
|
354
|
+
select.appendChild(el('option', { value: '*', text: 'Another system...' }));
|
|
355
|
+
var named = r.entSystem === VS || cfg.systems.some(function (s) { return s.url === r.entSystem; });
|
|
356
|
+
select.value = !r.entSystem ? '' : (named ? r.entSystem : '*');
|
|
357
|
+
var url = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'code system URL' });
|
|
358
|
+
url.value = named ? '' : r.entSystem;
|
|
359
|
+
url.hidden = select.value !== '*';
|
|
360
|
+
url.addEventListener('input', function () { r.entSystem = url.value.trim(); changed(); onChange(); });
|
|
361
|
+
select.addEventListener('change', function () {
|
|
362
|
+
var wasVs = r.entSystem === VS;
|
|
363
|
+
r.entSystem = select.value === '*' ? url.value.trim() : select.value;
|
|
364
|
+
if (!select.value) { r.entCode = ''; r.entCard = ''; r.entVs = ''; }
|
|
365
|
+
changed();
|
|
366
|
+
if (wasVs !== (r.entSystem === VS) || !select.value || select.value === '*') { draw(); } else { onChange(); }
|
|
367
|
+
});
|
|
368
|
+
cell.appendChild(el('div', { class: 'ips-pf-pair' }, [select, url]));
|
|
369
|
+
if (r.entSystem === VS) {
|
|
370
|
+
cell.appendChild(cell.vs = vsInput(r, 'entVs', 'entVsTitle', onChange));
|
|
371
|
+
} else if (r.entSystem || select.value === '*') {
|
|
372
|
+
cell.appendChild(cell.code = codeInput(r, 'entCode', 'entDisplay', function () { return r.entSystem; }, onChange));
|
|
373
|
+
}
|
|
374
|
+
return cell;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// the second table: the resources' type, their code's system, the code
|
|
378
|
+
function typeCell(r, onChange) {
|
|
379
|
+
var cell = el('div');
|
|
380
|
+
var select = el('select', { class: 'form-control ips-ed-input' }, [el('option', { value: '', text: '(any type)' })]);
|
|
381
|
+
cfg.resourceTypes.forEach(function (t) { select.appendChild(el('option', { value: t, text: t })); });
|
|
382
|
+
select.appendChild(el('option', { value: '*', text: 'Another type...' }));
|
|
383
|
+
var listed = !r.resType || cfg.resourceTypes.indexOf(r.resType) >= 0;
|
|
384
|
+
select.value = listed ? r.resType : '*';
|
|
385
|
+
var other = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'resource type, e.g. Encounter' });
|
|
386
|
+
other.value = listed ? '' : r.resType;
|
|
387
|
+
other.hidden = listed;
|
|
388
|
+
other.addEventListener('input', function () { r.resType = other.value.trim(); changed(); onChange(); });
|
|
389
|
+
select.addEventListener('change', function () {
|
|
390
|
+
other.hidden = select.value !== '*';
|
|
391
|
+
r.resType = select.value === '*' ? other.value.trim() : select.value;
|
|
392
|
+
changed();
|
|
393
|
+
onChange();
|
|
394
|
+
});
|
|
395
|
+
cell.appendChild(select);
|
|
396
|
+
cell.appendChild(other);
|
|
397
|
+
cell.other = other;
|
|
398
|
+
return cell;
|
|
399
|
+
}
|
|
400
|
+
function systemCell(r, onChange) {
|
|
401
|
+
var cell = el('div');
|
|
402
|
+
var select = el('select', { class: 'form-control ips-ed-input' }, [el('option', { value: VS, text: 'In a value set...' })]);
|
|
403
|
+
cfg.systems.forEach(function (s) { select.appendChild(el('option', { value: s.url, text: s.name })); });
|
|
404
|
+
select.appendChild(el('option', { value: '*', text: 'Another system...' }));
|
|
405
|
+
var named = r.resSystem === VS || cfg.systems.some(function (s) { return s.url === r.resSystem; });
|
|
406
|
+
select.value = named ? r.resSystem : '*';
|
|
407
|
+
var url = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'code system URL' });
|
|
408
|
+
url.value = named ? '' : r.resSystem;
|
|
409
|
+
url.hidden = named;
|
|
410
|
+
url.addEventListener('input', function () { r.resSystem = url.value.trim(); changed(); onChange(); });
|
|
411
|
+
select.addEventListener('change', function () {
|
|
412
|
+
var wasVs = r.resSystem === VS;
|
|
413
|
+
url.hidden = select.value !== '*';
|
|
414
|
+
r.resSystem = select.value === '*' ? url.value.trim() : select.value;
|
|
415
|
+
changed();
|
|
416
|
+
// the code column becomes a value set's, or a code's
|
|
417
|
+
if (wasVs !== (r.resSystem === VS)) { draw(); } else { onChange(); }
|
|
418
|
+
});
|
|
419
|
+
cell.appendChild(select);
|
|
420
|
+
cell.appendChild(url);
|
|
421
|
+
cell.url = url;
|
|
422
|
+
return cell;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function drawRow(r, n) {
|
|
426
|
+
var box = el('div', { class: 'ips-pf-row' });
|
|
427
|
+
var human = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: 'What is expected (shown to people)' });
|
|
428
|
+
human.value = r.human;
|
|
429
|
+
human.addEventListener('input', function () { r.human = human.value; changed(); });
|
|
430
|
+
var toggle = el('a', { href: '#', class: 'ips-small', text: r.mode === 'text' ? 'Edit as table' : 'Edit as FHIRPath' });
|
|
431
|
+
toggle.addEventListener('click', function (e) {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
if (r.mode !== 'text') {
|
|
434
|
+
var t = rowExpression(r);
|
|
435
|
+
if (t.expression) { r.expression = t.expression; }
|
|
436
|
+
r.mode = 'text';
|
|
437
|
+
} else {
|
|
438
|
+
var shape = parseShape(r.expression);
|
|
439
|
+
var rshape = shape ? null : parseResourceShape(r.expression);
|
|
440
|
+
if (!shape && !rshape) {
|
|
441
|
+
window.alert('This rule is not one the tables can show: Bundle.section(...).exists(), Bundle.section(...).entry(...).exists() or .entryIn(...).exists(), or Bundle.resources(...) or Bundle.resourcesIn(...) and the rest of the rule');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (shape) { tableFields(r, shape); r.mode = 'section'; } else { resourceFields(r, rshape); r.mode = 'resource'; }
|
|
445
|
+
}
|
|
446
|
+
changed();
|
|
447
|
+
draw();
|
|
448
|
+
});
|
|
449
|
+
var remove = el('button', { type: 'button', class: 'btn btn-default ips-btn', style: 'font-size: 85%; padding: 1px 8px', text: 'Remove' });
|
|
450
|
+
remove.addEventListener('click', function () { state.rows.splice(state.rows.indexOf(r), 1); changed(); draw(); });
|
|
451
|
+
box.appendChild(el('div', { class: 'ips-pf-head' }, [el('b', { text: String(n) + '.' }), human, toggle, remove]));
|
|
452
|
+
if (r.mode === 'text') {
|
|
453
|
+
var text = el('textarea', { class: 'form-control ips-ed-input', rows: 2, placeholder: 'A FHIRPath expression on the document (the Bundle), true when it meets the expectation', style: 'font-family: monospace; width: 100%' });
|
|
454
|
+
text.value = r.expression;
|
|
455
|
+
text.addEventListener('input', function () { r.expression = text.value; changed(); });
|
|
456
|
+
box.appendChild(text);
|
|
457
|
+
return box;
|
|
458
|
+
}
|
|
459
|
+
var exprLine = el('div', { class: 'ips-pf-expr' });
|
|
460
|
+
var fields = {};
|
|
461
|
+
var update = function () {
|
|
462
|
+
var t = rowExpression(r);
|
|
463
|
+
exprLine.textContent = t.expression || '(incomplete)';
|
|
464
|
+
Object.keys(fields).forEach(function (k) { fields[k].classList.toggle('ips-pf-bad', t.problems.indexOf(k) >= 0); });
|
|
465
|
+
};
|
|
466
|
+
var grid;
|
|
467
|
+
if (r.mode === 'section') {
|
|
468
|
+
var sec = sectionCell(r, update);
|
|
469
|
+
var ent = entryCell(r, update);
|
|
470
|
+
fields.secCard = cardInput(r, 'secCard', update);
|
|
471
|
+
fields.entCard = cardInput(r, 'entCard', update);
|
|
472
|
+
if (ent.code) { fields.entCode = ent.code.input; }
|
|
473
|
+
if (ent.vs) { fields.entVs = ent.vs.input; }
|
|
474
|
+
grid = el('table', { class: 'ips-pf-grid' }, [
|
|
475
|
+
el('tr', {}, [el('th', { text: 'Section' }), el('th', { text: 'Cardinality' }), el('th', { text: 'Entries' }), el('th', { text: 'Cardinality' })]),
|
|
476
|
+
el('tr', {}, [el('td', {}, [sec]), el('td', { class: 'ips-pf-card' }, [fields.secCard]), el('td', {}, [ent]), el('td', { class: 'ips-pf-card' }, [fields.entCard])])
|
|
477
|
+
]);
|
|
478
|
+
} else {
|
|
479
|
+
var type = typeCell(r, update);
|
|
480
|
+
var system = systemCell(r, update);
|
|
481
|
+
var byVs = r.resSystem === VS;
|
|
482
|
+
var code = byVs ? vsInput(r, 'resVs', 'resVsTitle', update) : codeInput(r, 'resCode', 'resDisplay', function () { return r.resSystem; }, update);
|
|
483
|
+
var rest = el('input', { type: 'text', class: 'form-control ips-ed-input', placeholder: '.exists()', style: 'font-family: monospace; width: 100%', title: 'The rest of the rule: FHIRPath on the resources found, starting with a dot' });
|
|
484
|
+
rest.value = r.resRest;
|
|
485
|
+
rest.addEventListener('input', function () { r.resRest = rest.value; changed(); update(); });
|
|
486
|
+
fields.resType = type.other;
|
|
487
|
+
fields.resSystem = system.url;
|
|
488
|
+
fields[byVs ? 'resVs' : 'resCode'] = code.input;
|
|
489
|
+
fields.resRest = rest;
|
|
490
|
+
grid = el('table', { class: 'ips-pf-grid' }, [
|
|
491
|
+
el('tr', {}, [el('th', { text: 'Type' }), el('th', { text: 'System' }), el('th', { text: byVs ? 'Value set' : 'Code' }), el('th', { text: 'Rule' })]),
|
|
492
|
+
el('tr', {}, [el('td', { style: 'width: 16%' }, [type]), el('td', { style: 'width: 18%' }, [system]), el('td', { style: byVs ? 'width: 34%' : 'width: 22%' }, [code]), el('td', {}, [rest])])
|
|
493
|
+
]);
|
|
494
|
+
}
|
|
495
|
+
box.appendChild(grid);
|
|
496
|
+
box.appendChild(exprLine);
|
|
497
|
+
update();
|
|
498
|
+
return box;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
var ADD_LABELS = { section: 'Add a required section/entry', resource: 'Add a rule about resources', text: 'Add another rule' };
|
|
502
|
+
var MODE_OF_PART = { section: 'section', resource: 'resource', other: 'text' };
|
|
503
|
+
var messages = el('div');
|
|
504
|
+
|
|
505
|
+
/** The rows as the page shows them: the parts in order. */
|
|
506
|
+
function ordered() {
|
|
507
|
+
var out = [];
|
|
508
|
+
cfg.parts.forEach(function (p) { state.rows.forEach(function (r) { if (r.mode === MODE_OF_PART[p[0]]) { out.push(r); } }); });
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function draw() {
|
|
513
|
+
root.innerHTML = '';
|
|
514
|
+
var title = el('input', { type: 'text', class: 'form-control', placeholder: 'The profile\'s title, e.g. Cardiology referral', style: 'font-size: 110%; font-weight: bold' });
|
|
515
|
+
title.value = state.title;
|
|
516
|
+
title.addEventListener('input', function () { state.title = title.value; changed(); });
|
|
517
|
+
root.appendChild(el('div', { class: 'ips-banner' }, [el('label', { class: 'ips-small', text: 'Title' }), title]));
|
|
518
|
+
var n = 0;
|
|
519
|
+
cfg.parts.forEach(function (p) {
|
|
520
|
+
var mode = MODE_OF_PART[p[0]];
|
|
521
|
+
root.appendChild(el('h4', { text: p[1] }));
|
|
522
|
+
var these = state.rows.filter(function (r) { return r.mode === mode; });
|
|
523
|
+
if (!these.length) { root.appendChild(el('p', { class: 'ips-small', text: 'None.' })); }
|
|
524
|
+
these.forEach(function (r) { root.appendChild(drawRow(r, ++n)); });
|
|
525
|
+
root.appendChild(el('div', { class: 'ips-actions-line' }, [el('button', { type: 'button', class: 'btn btn-default', text: ADD_LABELS[mode], onclick: function () {
|
|
526
|
+
var r = rowFrom({});
|
|
527
|
+
r.mode = mode;
|
|
528
|
+
state.rows.push(r);
|
|
529
|
+
changed();
|
|
530
|
+
draw();
|
|
531
|
+
} })]));
|
|
532
|
+
});
|
|
533
|
+
root.appendChild(messages);
|
|
534
|
+
root.appendChild(el('div', { class: 'ips-actions-line' }, [
|
|
535
|
+
el('button', { type: 'button', class: 'btn btn-primary', text: 'Save', onclick: save }), ' ',
|
|
536
|
+
el('a', { class: 'btn btn-default', href: cfg.urls.back, text: 'Cancel' })
|
|
537
|
+
]));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function show(problems, kind) {
|
|
541
|
+
messages.innerHTML = '';
|
|
542
|
+
if (!problems.length) { return; }
|
|
543
|
+
var box = el('div', { class: 'alert alert-' + (kind || 'danger') });
|
|
544
|
+
if (problems.length === 1) { box.textContent = problems[0]; } else {
|
|
545
|
+
box.appendChild(el('ul', { style: 'margin: 0' }, problems.map(function (p) { return el('li', { text: p }); })));
|
|
546
|
+
}
|
|
547
|
+
messages.appendChild(box);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function save() {
|
|
551
|
+
if (saving) { return; }
|
|
552
|
+
var problems = [];
|
|
553
|
+
// in the page's order, so the numbers in the server's messages are the page's
|
|
554
|
+
var constraints = ordered().map(function (r, i) {
|
|
555
|
+
var t = rowExpression(r);
|
|
556
|
+
if (!t.expression && r.mode !== 'text') { problems.push('Rule ' + (i + 1) + ' is incomplete (the fields in red)'); }
|
|
557
|
+
return { key: r.key, human: r.human, expression: t.expression || '' };
|
|
558
|
+
});
|
|
559
|
+
if (!state.title.trim()) { problems.unshift('The profile needs a title'); }
|
|
560
|
+
if (problems.length) { show(problems); return; }
|
|
561
|
+
saving = true;
|
|
562
|
+
show(['Saving...'], 'info');
|
|
563
|
+
fetch(cfg.urls.save, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
564
|
+
body: JSON.stringify({ title: state.title, constraints: constraints, _csrf: cfg.csrf }) })
|
|
565
|
+
.then(function (res) {
|
|
566
|
+
return res.json().then(function (j) { return { ok: res.ok, j: j }; }, function () { return { ok: false, j: { problems: ['The server answered ' + res.status + (res.status === 403 ? ' (has your session expired? reload the page)' : '')] } }; });
|
|
567
|
+
})
|
|
568
|
+
.then(function (a) {
|
|
569
|
+
saving = false;
|
|
570
|
+
if (a.ok) { dirty = false; location.href = a.j.url; return; }
|
|
571
|
+
show(a.j.problems || ['The profile was not saved']);
|
|
572
|
+
})
|
|
573
|
+
.catch(function (e) { saving = false; show(['The profile was not saved: ' + e.message]); });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
window.addEventListener('beforeunload', function (e) { if (dirty) { e.preventDefault(); e.returnValue = ''; } });
|
|
577
|
+
draw();
|
|
578
|
+
})();
|