diff --git a/app/package.json b/app/package.json
index c8cb22fb..9169b9a1 100644
--- a/app/package.json
+++ b/app/package.json
@@ -30,6 +30,7 @@
"@types/fhir": "^0.0.36",
"dayjs": "^1.11.7",
"html-react-parser": "^3.0.15",
+ "luxon": "^3.3.0",
"next": "13.2.4",
"prism-react-renderer": "^1.3.5",
"react": "18.2.0",
@@ -38,6 +39,7 @@
"zod": "^3.21.4"
},
"devDependencies": {
+ "@types/luxon": "^3.3.0",
"@types/node": "18.15.3",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
diff --git a/app/src/components/ReleaseModal.tsx b/app/src/components/ReleaseModal.tsx
new file mode 100644
index 00000000..17349e30
--- /dev/null
+++ b/app/src/components/ReleaseModal.tsx
@@ -0,0 +1,140 @@
+import { trpc } from '@/util/trpc';
+import { ArtifactResourceType } from '@/util/types/fhir';
+import { Button, Center, Group, Modal, Stack, TextInput, Text, Tooltip } from '@mantine/core';
+import { DateTime } from 'luxon';
+import { useRouter } from 'next/router';
+import { useState } from 'react';
+import { AlertCircle, CircleCheck, InfoCircle } from 'tabler-icons-react';
+import { notifications } from '@mantine/notifications';
+
+export interface ReleaseModalProps {
+ open: boolean;
+ onClose: () => void;
+ id: string;
+ resourceType: ArtifactResourceType;
+}
+
+export default function ReleaseModal({ open = true, onClose, id, resourceType }: ReleaseModalProps) {
+ const router = useRouter();
+ const [version, setVersion] = useState('');
+
+ const { data: resource } = trpc.draft.getDraftById.useQuery({
+ id: id,
+ resourceType: resourceType
+ });
+ const ctx = trpc.useContext();
+ const deleteMutation = trpc.draft.deleteDraft.useMutation({
+ onSuccess: () => {
+ notifications.show({
+ title: `Draft ${resource?.resourceType} released!`,
+ message: `Draft ${resource?.resourceType}/${resource?.id} successfully released to the Publishable Measure Repository!`,
+ icon: ,
+ color: 'green'
+ });
+ ctx.draft.getDraftCounts.invalidate();
+ ctx.draft.getDrafts.invalidate();
+ },
+ onError: e => {
+ console.error(e);
+ notifications.show({
+ title: `Release Failed!`,
+ message: `Attempt to release ${resourceType} failed with message: ${e.message}`,
+ icon: ,
+ color: 'red'
+ });
+ }
+ });
+
+ async function confirm() {
+ // requirements:
+ // https://build.fhir.org/ig/HL7/cqf-measures/measure-repository-service.html#release
+ // TODO: release recursively all children (ignore for now).
+ if (resource) {
+ resource.version = version;
+ resource.status = 'active';
+ resource.date = DateTime.now().toISO() || '';
+ }
+
+ const res = await fetch(`${process.env.NEXT_PUBLIC_MRS_SERVER}/${resourceType}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json+fhir'
+ },
+ body: JSON.stringify(resource)
+ });
+
+ // Conditionally remove the base version if one is present
+ let location = res.headers.get('Location');
+ if (location?.substring(0, 5) === '4_0_1') {
+ location = location?.substring(5); // remove 4_0_1 (version)
+ }
+
+ if (res.status !== 201) {
+ console.error(res.statusText);
+ notifications.show({
+ title: `Release Failed!`,
+ message: `Server unable to process request`,
+ icon: ,
+ color: 'red'
+ });
+ } else if (!location) {
+ console.error('No resource location for released artifact');
+ notifications.show({
+ title: `Release Failed!`,
+ message: `No resource location exists for draft artifact`,
+ icon: ,
+ color: 'red'
+ });
+ } else {
+ // delete draft
+ deleteMutation.mutate({
+ resourceType: resourceType,
+ id: id
+ });
+
+ // direct user to published artifact detail page
+ router.push(location);
+ }
+ onClose();
+ }
+
+ return (
+
+
+
+
+ Release {resourceType}/{id}?
+
+
+
+
+
+
+
+
+ NOTE: By releasing this artifact to the Publishable Measure Repository, this draft instance will be removed.
+
+ setVersion(e.target.value)}
+ withAsterisk
+ description="An artifact must have a version before it can be released to the Publishable Measure Repository"
+ />
+
+
+
+ Release
+
+
+ Cancel
+
+
+
+
+
+ );
+}
diff --git a/app/src/pages/authoring/[resourceType]/[id].tsx b/app/src/pages/authoring/[resourceType]/[id].tsx
index b0f4373d..67d220b7 100644
--- a/app/src/pages/authoring/[resourceType]/[id].tsx
+++ b/app/src/pages/authoring/[resourceType]/[id].tsx
@@ -7,6 +7,7 @@ import { notifications } from '@mantine/notifications';
import { AlertCircle, CircleCheck, InfoCircle } from 'tabler-icons-react';
import { ArtifactResourceType } from '@/util/types/fhir';
import ArtifactFieldInput from '@/components/ArtifactFieldInput';
+import ReleaseModal from '@/components/ReleaseModal';
interface DraftArtifactUpdates {
url?: string;
@@ -36,6 +37,8 @@ export default function ResourceAuthoringPage() {
resourceType: resourceType as ArtifactResourceType
});
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
// checks if the field inputs have been changed by the user by checking
// that they are different from the saved field values on the draft artifact
// if the input is undefined on the draft artifact, then it is treated as
@@ -226,29 +229,38 @@ export default function ResourceAuthoringPage() {
data={libOptions}
/>
)}
- {
- const [additions, deletions] = parseUpdate(
- url,
- identifierValue,
- identifierSystem,
- name,
- title,
- description,
- library
- );
- resourceUpdate.mutate({
- resourceType: resourceType as ArtifactResourceType,
- id: id as string,
- additions: additions,
- deletions: deletions
- });
- }}
- disabled={!isChanged()} // only enable the submit button when a field has changed
- >
- Submit
-
+
+ {
+ const [additions, deletions] = parseUpdate(
+ url,
+ identifierValue,
+ identifierSystem,
+ name,
+ title,
+ description,
+ library
+ );
+ resourceUpdate.mutate({
+ resourceType: resourceType as ArtifactResourceType,
+ id: id as string,
+ additions: additions,
+ deletions: deletions
+ });
+ }}
+ disabled={!isChanged()} // only enable the submit button when a field has changed
+ >
+ Update
+
+ setIsModalOpen(true)}
+ //TODO/question: disabled={} any disable needed? any necessary fields?
+ >
+ Release
+
+
@@ -262,6 +274,13 @@ export default function ResourceAuthoringPage() {
+
+ setIsModalOpen(false)}
+ id={id as string}
+ resourceType={resourceType as ArtifactResourceType}
+ />
);
}
diff --git a/package-lock.json b/package-lock.json
index 95b59231..0cff3ef9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,7 @@
"@types/fhir": "^0.0.36",
"dayjs": "^1.11.7",
"html-react-parser": "^3.0.15",
+ "luxon": "^3.3.0",
"next": "13.2.4",
"prism-react-renderer": "^1.3.5",
"react": "18.2.0",
@@ -43,6 +44,7 @@
"zod": "^3.21.4"
},
"devDependencies": {
+ "@types/luxon": "^3.3.0",
"@types/node": "18.15.3",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
@@ -3731,6 +3733,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/cors": {
+ "version": "2.8.13",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
+ "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/express": {
"version": "4.17.17",
"dev": true,
@@ -3805,6 +3816,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/luxon": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz",
+ "integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==",
+ "dev": true
+ },
"node_modules/@types/mime": {
"version": "1.3.2",
"dev": true,
@@ -5234,7 +5251,8 @@
},
"node_modules/cors": {
"version": "2.8.5",
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
@@ -5277,6 +5295,14 @@
"luxon": "^1.28.1"
}
},
+ "node_modules/cql-execution/node_modules/luxon": {
+ "version": "1.28.1",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz",
+ "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/create-require": {
"version": "1.1.1",
"dev": true,
@@ -8979,10 +9005,11 @@
}
},
"node_modules/luxon": {
- "version": "1.28.1",
- "license": "MIT",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz",
+ "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==",
"engines": {
- "node": "*"
+ "node": ">=12"
}
},
"node_modules/make-dir": {
@@ -9188,7 +9215,8 @@
},
"node_modules/moment": {
"version": "2.29.4",
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
@@ -12285,6 +12313,7 @@
"license": "Apache-2.0",
"dependencies": {
"@projecttacoma/node-fhir-server-core": "^2.2.8",
+ "cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"fqm-execution": "git+https://git@github.com/projecttacoma/fqm-execution",
@@ -12294,6 +12323,7 @@
},
"devDependencies": {
"@shelf/jest-mongodb": "^4.1.4",
+ "@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/fhir": "^0.0.35",
"@types/jest": "^29.2.3",
@@ -14836,6 +14866,15 @@
"version": "2.1.2",
"dev": true
},
+ "@types/cors": {
+ "version": "2.8.13",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
+ "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/express": {
"version": "4.17.17",
"dev": true,
@@ -14900,6 +14939,12 @@
"version": "0.0.29",
"dev": true
},
+ "@types/luxon": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz",
+ "integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==",
+ "dev": true
+ },
"@types/mime": {
"version": "1.3.2",
"dev": true
@@ -15197,6 +15242,7 @@
"@trpc/react-query": "^10.26.0",
"@trpc/server": "^10.26.0",
"@types/fhir": "^0.0.36",
+ "@types/luxon": "^3.3.0",
"@types/node": "18.15.3",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
@@ -15207,6 +15253,7 @@
"eslint-config-next": "13.2.4",
"eslint-config-prettier": "^8.5.0",
"html-react-parser": "^3.0.15",
+ "luxon": "^3.3.0",
"next": "13.2.4",
"prettier": "^2.8.5",
"prism-react-renderer": "^1.3.5",
@@ -15885,6 +15932,8 @@
},
"cors": {
"version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "^4",
"vary": "^1"
@@ -15914,6 +15963,13 @@
"@lhncbc/ucum-lhc": "^4.1.3",
"immutable": "^4.1.0",
"luxon": "^1.28.1"
+ },
+ "dependencies": {
+ "luxon": {
+ "version": "1.28.1",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz",
+ "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw=="
+ }
}
},
"create-require": {
@@ -18283,7 +18339,9 @@
}
},
"luxon": {
- "version": "1.28.1"
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz",
+ "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg=="
},
"make-dir": {
"version": "3.1.0",
@@ -18324,6 +18382,7 @@
"requires": {
"@projecttacoma/node-fhir-server-core": "^2.2.8",
"@shelf/jest-mongodb": "^4.1.4",
+ "@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/fhir": "^0.0.35",
"@types/jest": "^29.2.3",
@@ -18332,6 +18391,7 @@
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
+ "cors": "^2.8.5",
"dotenv": "^16.0.3",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
@@ -18468,7 +18528,9 @@
}
},
"moment": {
- "version": "2.29.4"
+ "version": "2.29.4",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"moment-timezone": {
"version": "0.5.43",
diff --git a/service/package.json b/service/package.json
index 2704c431..a7b24c3b 100644
--- a/service/package.json
+++ b/service/package.json
@@ -25,6 +25,7 @@
},
"devDependencies": {
"@shelf/jest-mongodb": "^4.1.4",
+ "@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/fhir": "^0.0.35",
"@types/jest": "^29.2.3",
@@ -46,6 +47,7 @@
},
"dependencies": {
"@projecttacoma/node-fhir-server-core": "^2.2.8",
+ "cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"fqm-execution": "git+https://git@github.com/projecttacoma/fqm-execution",
diff --git a/service/src/app.ts b/service/src/app.ts
index 05feaa65..41a23609 100644
--- a/service/src/app.ts
+++ b/service/src/app.ts
@@ -1,7 +1,9 @@
import express from 'express';
+import cors from 'cors';
import { uploadTransactionBundle } from './services/BaseService';
export const app = express();
+app.use(cors({ exposedHeaders: 'Location' }));
app.use(express.json({ limit: '50mb', type: 'application/json+fhir' }));
app.use(express.json({ limit: '50mb', type: 'application/fhir+json' }));