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" + /> +
+ + + + +
+
+
+ ); +} 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} /> )} - + + + + @@ -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' }));