diff --git a/.env b/.env index e0aed43..75e6c54 100644 --- a/.env +++ b/.env @@ -3,7 +3,7 @@ HTTPS_CERT_PATH = server.cert HTTPS_KEY_PATH = server.key VITE_ALT_DRUG = true VITE_AUTH = http://localhost:8180 -VITE_CDS_SERVICE = http://localhost:8090/cds-services +VITE_CDS_SERVICE = http://localhost:8090/etasu/reset VITE_CLIENT = app-login VITE_CLIENT_SCOPES = launch offline_access openid profile user/Patient.read patient/Patient.read user/Practitioner.read VITE_DEFAULT_USER = pra1234 @@ -15,11 +15,8 @@ VITE_GENERATE_JWT = true VITE_GH_PAGES=false VITE_HOMEPAGE = http://localhost:8080 VITE_LAUNCH_URL = http://localhost:4040/launch -VITE_ORDER_SELECT = rems-order-select -VITE_ORDER_SIGN = rems-order-sign VITE_PASSWORD = alice VITE_PATIENT_FHIR_QUERY = Patient?_sort=identifier&_count=12 -VITE_PATIENT_VIEW = rems-patient-view VITE_PIMS_SERVER = http://localhost:5051/doctorOrders/api/addRx VITE_PUBLIC_KEYS = http://localhost:3000/request-generator/.well-known/jwks.json VITE_REALM = ClientFhirServer diff --git a/.gitignore b/.gitignore index 5c7ad5e..fa3e111 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ src/db.json # Environment variable files .env.local + +# Mac OS files +**/.DS_Store \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b98d3f8..6c28420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react-dom": "^17.0.0", "react-markdown": "^8.0.7", "react-router-dom": "^6.17.0", + "uuid": "^9.0.1", "vite": "^5.1.6", "vite-tsconfig-paths": "^4.3.2" }, @@ -2993,6 +2994,16 @@ "safe-json-stringify": "~1" } }, + "node_modules/@expo/bunyan/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@expo/cli": { "version": "0.17.7", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.17.7.tgz", @@ -4577,6 +4588,16 @@ "node": ">=12" } }, + "node_modules/@expo/rudder-sdk-node/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@expo/sdk-runtime-versions": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", @@ -22810,11 +22831,13 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "optional": true, - "peer": true, + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 4efe7f2..0000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/src/components/RequestBox/RequestBox.jsx b/src/components/RequestBox/RequestBox.jsx index 8ec1e8f..2adb992 100644 --- a/src/components/RequestBox/RequestBox.jsx +++ b/src/components/RequestBox/RequestBox.jsx @@ -17,7 +17,6 @@ const RequestBox = props => { }); const { - callback, prefetchedResources, submitInfo, patient, @@ -29,7 +28,8 @@ const RequestBox = props => { defaultUser, smartAppUrl, client, - pimsUrl + pimsUrl, + getRemsAdminUrl } = props; const emptyField = empty; @@ -61,12 +61,8 @@ const RequestBox = props => { }; const submitOrderSign = request => { - submitInfo(prepPrefetch(), request, patient, 'order-sign'); - }; - - const submit = () => { if (!_.isEmpty(request)) { - submitOrderSign(request); + submitInfo(prepPrefetch(), request, patient, 'order-sign'); } }; @@ -149,8 +145,7 @@ const RequestBox = props => { renderedPrefetches.set(resourceKey, renderedList); }); - console.log(renderedPrefetches); - console.log(Object.entries(renderedPrefetches)); + return (
Prefetched
@@ -284,6 +279,7 @@ const RequestBox = props => { const disableSendToCRD = isOrderNotSelected() || loading; const disableSendRx = isOrderNotSelected() || loading; const disableLaunchSmartOnFhir = isPatientNotSelected(); + const orderSignRemsAdmin = getRemsAdminUrl(request, 'order-sign'); return ( <> @@ -311,7 +307,7 @@ const RequestBox = props => { - diff --git a/src/components/RequestDashboard/SettingsSection.jsx b/src/components/RequestDashboard/SettingsSection.jsx index c373a9a..0c8b010 100644 --- a/src/components/RequestDashboard/SettingsSection.jsx +++ b/src/components/RequestDashboard/SettingsSection.jsx @@ -1,37 +1,54 @@ import React, { memo, useState, useEffect } from 'react'; -import { Button, Box, FormControlLabel, Grid, Checkbox, TextField } from '@mui/material'; +import { + Box, + Button, + Checkbox, + FormControlLabel, + Grid, + IconButton, + Paper, + Select, + MenuItem, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + TextField +} from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddIcon from '@mui/icons-material/Add'; -import useStyles from './styles'; import env from 'env-var'; import FHIR from 'fhirclient'; -import { headerDefinitions, types } from '../../util/data'; -import { actionTypes } from '../../containers/ContextProvider/reducer'; +import { headerDefinitions, medicationRequestToRemsAdmins } from '../../util/data'; +import { actionTypes, initialState } from '../../containers/ContextProvider/reducer'; import { SettingsContext } from '../../containers/ContextProvider/SettingsProvider'; +const CDS_HOOKS = ['order-sign', 'order-select', 'patient-view']; + const SettingsSection = props => { - const classes = useStyles(); const [state, dispatch] = React.useContext(SettingsContext); - const [headers, setHeaders] = useState([]); - const [originalValues, setOriginalValues] = useState([]); + + const fieldHeaders = Object.keys(headerDefinitions) + .map(key => ({ ...headerDefinitions[key], key })) + // Display the fields in descending order of type. If two fields are the same type, + // then sort by ascending order of display text. + .sort( + (self, other) => + -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) + ); useEffect(() => { - const headers = Object.keys(headerDefinitions) - .map(key => ({ ...headerDefinitions[key], key })) - // Display the fields in descending order of type. If two fields are the same type, then sort by ascending order of display text. - .sort( - (self, other) => - -self.type.localeCompare(other.type) || self.display.localeCompare(other.display) - ); - setHeaders(headers); - const originals = Object.keys(headerDefinitions).map(key => [key, state[key]]); - setOriginalValues(originals); - JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(element => { + JSON.parse(localStorage.getItem('reqgenSettings') || '[]').forEach(([key, value]) => { try { - updateSetting(element[0], element[1]); + updateSetting(key, value); } catch { - if (element[0]) { - console.log('Could not load setting:' + element[0]); + if (!key) { + console.log('Could not load setting:' + key); } } }); @@ -41,6 +58,7 @@ const SettingsSection = props => { type: actionTypes.flagStartup }); }, []); + const updateSetting = (key, value) => { dispatch({ type: actionTypes.updateSetting, @@ -48,20 +66,16 @@ const SettingsSection = props => { value: value }); }; + const saveSettings = () => { - const headers = Object.keys(headerDefinitions).map(key => [key, state[key]]); + const headers = Object.keys(state).map(key => [key, state[key]]); localStorage.setItem('reqgenSettings', JSON.stringify(headers)); }; const resetSettings = () => { - originalValues.forEach(e => { - try { - updateSetting(e[0], e[1]); - } catch { - console.log('Failed to reset setting value'); - } - }); + dispatch({ type: actionTypes.resetSettings }); }; + const clearQuestionnaireResponses = ({ defaultUser }) => () => { @@ -105,13 +119,11 @@ const SettingsSection = props => { console.log(error); }); }; + const resetRemsAdmin = ({ cdsUrl }) => () => { - let url = new URL(cdsUrl); - const resetUrl = url.origin + '/etasu/reset'; - - fetch(resetUrl, { + fetch(cdsUrl, { method: 'POST' }) .then(response => { @@ -123,6 +135,7 @@ const SettingsSection = props => { console.log(error); }); }; + const clearMedicationDispenses = ({ ehrUrl, access_token }) => () => { @@ -153,6 +166,7 @@ const SettingsSection = props => { console.log(e); }); }; + const reconnectEhr = ({ baseUrl, redirect }) => () => { @@ -163,6 +177,7 @@ const SettingsSection = props => { scope: env.get('VITE_CLIENT_SCOPES').asString() }); }; + const resetHeaderDefinitions = [ { display: 'Reset PIMS Database', @@ -195,9 +210,9 @@ const SettingsSection = props => { let firstCheckbox = true; let showBreak = true; return ( - - - {headers.map(({ key, type, display }) => { + + + {fieldHeaders.map(({ key, type, display }) => { switch (type) { case 'input': return ( @@ -207,9 +222,7 @@ const SettingsSection = props => { label={display} variant="outlined" value={state[key]} - onChange={event => { - updateSetting(key, event.target.value); - }} + onChange={event => updateSetting(key, event.target.value)} sx={{ width: '100%' }} />
@@ -230,9 +243,7 @@ const SettingsSection = props => { control={ { - updateSetting(key, event.target.checked); - }} + onChange={event => updateSetting(key, event.target.checked)} /> } label={display} @@ -248,35 +259,162 @@ const SettingsSection = props => { ); } })} + + + + + + + + Medication Display + Medication RxNorm Code + CDS Hook + REMS Admin Endpoint + {/* This empty TableCell corresponds to the add and delete + buttons. It is used to fill up the sticky header which + will appear over the gray/white table rows. */} + + + + + {Object.entries(state.medicationRequestToRemsAdmins).map(([key, row]) => { + return ( + + + + dispatch({ + type: actionTypes.updateCdsHookSetting, + settingId: key, + value: { display: event.target.value } + }) + } + sx={{ width: '100%' }} + /> + + + + dispatch({ + type: actionTypes.updateCdsHookSetting, + settingId: key, + value: { rxnorm: event.target.value } + }) + } + sx={{ width: '100%' }} + /> + + + + + + + dispatch({ + type: actionTypes.updateCdsHookSetting, + settingId: key, + value: { remsAdmin: event.target.value } + }) + } + sx={{ width: '100%' }} + /> + + + + + dispatch({ type: actionTypes.addCdsHookSetting, settingId: key }) + } + size="large" + > + + + + + + dispatch({ type: actionTypes.deleteCdsHookSetting, settingId: key }) + } + size="large" + > + + + + + + ); + })} + +
+
+
+ + {/* spacer */} +
+ + {resetHeaderDefinitions.map(({ key, display, reset, variant }) => { return ( - + ); })} - {/* spacer */} -
- + - + + - + - + ); }; diff --git a/src/components/SMARTBox/PatientBox.jsx b/src/components/SMARTBox/PatientBox.jsx index c347976..fea2e08 100644 --- a/src/components/SMARTBox/PatientBox.jsx +++ b/src/components/SMARTBox/PatientBox.jsx @@ -463,11 +463,7 @@ const PatientBox = props => { const makeResponseTable = (columns, options, type, patient) => { return ( - + diff --git a/src/containers/ContextProvider/reducer.js b/src/containers/ContextProvider/reducer.js index 051b364..d73f500 100644 --- a/src/containers/ContextProvider/reducer.js +++ b/src/containers/ContextProvider/reducer.js @@ -1,12 +1,80 @@ -import { headerDefinitions } from '../../util/data'; +import { headerDefinitions, medicationRequestToRemsAdmins } from '../../util/data'; +import { v4 as uuidv4 } from 'uuid'; + export const actionTypes = Object.freeze({ updatePatient: 'update_patient', // {type, value} updateSetting: 'update_setting', // {type, settingId, value} - flagStartup: 'flag_startup' // {type} + flagStartup: 'flag_startup', // {type} + deleteCdsHookSetting: 'DELETE_CDS_HOOK_SETTING', // {type, settingId} + addCdsHookSetting: 'ADD_CDS_HOOK_SETTING', // {type} + updateCdsHookSetting: 'UPDATE_CDS_HOOK_SETTING', // {type, settingId, value} + resetSettings: 'RESET_SETTINGS' // {type} }); + +const getNewStateWithoutCdsHookSetting = (state, settingId) => { + const newState = { ...state, medicationRequestToRemsAdmins: {} }; + for (const key of Object.keys(state.medicationRequestToRemsAdmins)) { + if (key !== settingId) { + newState.medicationRequestToRemsAdmins[key] = state.medicationRequestToRemsAdmins[key]; + } + } + return newState; +}; + +const getNewStateWithNewCdsHookSetting = (state, settingId) => { + const original = Object.entries(state.medicationRequestToRemsAdmins); + const indexOfSiblingToInsertBelow = original.findIndex(([key, _value]) => key === settingId); + const firstHalf = + indexOfSiblingToInsertBelow === 0 + ? [original[0]] + : original.slice(0, indexOfSiblingToInsertBelow + 1); + const secondHalf = + indexOfSiblingToInsertBelow === 0 + ? original.slice(indexOfSiblingToInsertBelow) + : original.slice(indexOfSiblingToInsertBelow - 1); + + const newState = { ...state, medicationRequestToRemsAdmins: {} }; + + for (const pair of firstHalf) { + const [key, value] = pair; + newState.medicationRequestToRemsAdmins[key] = value; + } + + newState.medicationRequestToRemsAdmins[uuidv4()] = { + rxnorm: 'Fill out Medication RxNorm Code', + display: 'Fill out Medication Display Name', + hook: 'order-sign', + remsAdmin: 'REMS Admin URL for CDS Hook' + }; + + for (const pair of secondHalf) { + const [key, value] = pair; + newState.medicationRequestToRemsAdmins[key] = value; + } + + return newState; +}; + // todo: add an enum that defines possible settings export const reducer = (state, action) => { switch (action.type) { + case actionTypes.deleteCdsHookSetting: + return getNewStateWithoutCdsHookSetting(state, action.settingId); + case actionTypes.addCdsHookSetting: + return getNewStateWithNewCdsHookSetting(state, action.settingId); + case actionTypes.updateCdsHookSetting: + return { + ...state, + medicationRequestToRemsAdmins: { + ...state.medicationRequestToRemsAdmins, + [action.settingId]: { + ...state.medicationRequestToRemsAdmins[action.settingId], + ...action.value + } + } + }; + case actionTypes.resetSettings: + return { ...initialState, startup: true }; case actionTypes.updateSetting: return { ...state, @@ -27,13 +95,24 @@ export const reducer = (state, action) => { } }; -const initialState = { - patient: null, - startup: false, - redirect: '' -}; -Object.keys(headerDefinitions).forEach(e => { - initialState[e] = headerDefinitions[e].default; // fill default settings values -}); +export const initialState = (() => { + let state = { + patient: null, + startup: false, + redirect: '', + medicationRequestToRemsAdmins: {} + }; + + Object.keys(headerDefinitions).forEach(key => { + state[key] = headerDefinitions[key].default; + }); -export { initialState }; + medicationRequestToRemsAdmins.forEach(row => { + const { rxnorm, display, hookEndpoints } = row; + hookEndpoints.forEach(({ hook, remsAdmin }) => { + const key = `${rxnorm}_${hook}`; + state.medicationRequestToRemsAdmins[key] = { rxnorm, display, hook, remsAdmin }; + }); + }); + return state; +})(); diff --git a/src/containers/RequestBuilder.jsx b/src/containers/RequestBuilder.jsx index 5bb65cf..76d176c 100644 --- a/src/containers/RequestBuilder.jsx +++ b/src/containers/RequestBuilder.jsx @@ -8,6 +8,7 @@ import RequestBox from '../components/RequestBox/RequestBox.jsx'; import buildRequest from '../util/buildRequest.js'; import { types } from '../util/data.js'; import { createJwt } from '../util/auth.js'; +import { getMedicationSpecificRemsAdminUrl } from '../util/util.js'; import Accordion from '@mui/material/Accordion'; import AccordionSummary from '@mui/material/AccordionSummary'; @@ -105,11 +106,18 @@ const RequestBuilder = props => { const submitInfo = (prefetch, request, patient, hook) => { console.log('Initiating form submission ', types.info); + const remsAdminUrl = getMedicationSpecificRemsAdminUrl(request, globalState, hook); + setState(prevState => ({ ...prevState, - loading: true, + loading: !!remsAdminUrl, patient })); + + if (!remsAdminUrl) { + return; + } + const hookConfig = { includeConfig: globalState.includeConfig, alternativeTherapy: globalState.alternativeTherapy @@ -126,17 +134,6 @@ const RequestBuilder = props => { hook, hookConfig ); - let cdsUrl = globalState.cdsUrl; - if (hook === 'order-sign') { - cdsUrl = cdsUrl + '/' + globalState.orderSign; - } else if (hook === 'order-select') { - cdsUrl = cdsUrl + '/' + globalState.orderSelect; - } else if (hook === 'patient-view') { - cdsUrl = cdsUrl + '/' + globalState.patientView; - } else { - console.log("ERROR: unknown hook type: '", hook); - return; - } let baseUrl = globalState.baseUrl; @@ -144,12 +141,12 @@ const RequestBuilder = props => { 'Content-Type': 'application/json' }; if (globalState.generateJsonToken) { - const jwt = 'Bearer ' + createJwt(baseUrl, cdsUrl); + const jwt = 'Bearer ' + createJwt(baseUrl, remsAdminUrl); headers.authorization = jwt; } try { - fetch(cdsUrl, { + fetch(remsAdminUrl, { method: 'POST', headers: new Headers(headers), body: JSON.stringify(json_request) @@ -298,6 +295,9 @@ const RequestBuilder = props => { defaultUser={globalState.defaultUser} loading={state.loading} patientFhirQuery={globalState.patientFhirQuery} + getRemsAdminUrl={(request, hook) => + getMedicationSpecificRemsAdminUrl(request, globalState, hook) + } /> )} diff --git a/src/util/data.js b/src/util/data.js index 49e8428..d308619 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -12,7 +12,7 @@ const headerDefinitions = { default: env.get('VITE_EHR_BASE').asString() }, cdsUrl: { - display: 'REMS Admin', + display: 'REMS Admin Reset Url', type: 'input', default: env.get('VITE_CDS_SERVICE').asString() }, @@ -46,26 +46,11 @@ const headerDefinitions = { type: 'input', default: env.get('VITE_LAUNCH_URL').asString() }, - orderSelect: { - display: 'Order Select Rest End Point', - type: 'input', - default: env.get('VITE_ORDER_SELECT').asString() - }, - orderSign: { - display: 'Order Sign Rest End Point', - type: 'input', - default: env.get('VITE_ORDER_SIGN').asString() - }, patientFhirQuery: { display: 'Patient FHIR Query', type: 'input', default: env.get('VITE_PATIENT_FHIR_QUERY').asString() }, - patientView: { - display: 'Patient View Rest End Point', - type: 'input', - default: env.get('VITE_PATIENT_VIEW').asString() - }, pimsUrl: { display: 'PIMS Server', type: 'input', @@ -93,6 +78,45 @@ const headerDefinitions = { } }; +const medicationRequestToRemsAdmins = Object.freeze([ + { + rxnorm: 2183126, + display: 'Turalio 200 MG Oral Capsule', + hookEndpoints: [ + { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/rems-order-sign' }, + { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/rems-order-select' }, + { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/rems-patient-view' } + ] + }, + { + rxnorm: 6064, + display: 'Isotretinoin 20 MG Oral Capsule', + hookEndpoints: [ + { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/rems-order-sign' }, + { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/rems-order-select' }, + { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/rems-patient-view' } + ] + }, + { + rxnorm: 1237051, + display: 'TIRF 200 UG Oral Transmucosal Lozenge', + hookEndpoints: [ + { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/rems-order-sign' }, + { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/rems-order-select' }, + { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/rems-patient-view' } + ] + }, + { + rxnorm: 1666386, + display: 'Addyi 100 MG Oral Tablet', + hookEndpoints: [ + { hook: 'order-sign', remsAdmin: 'http://localhost:8090/cds-services/rems-order-sign' }, + { hook: 'order-select', remsAdmin: 'http://localhost:8090/cds-services/rems-order-select' }, + { hook: 'patient-view', remsAdmin: 'http://localhost:8090/cds-services/rems-patient-view' } + ] + } +]); + const types = { error: 'errorClass', info: 'infoClass', @@ -218,4 +242,12 @@ const shortNameMap = { 'http://hl7.org/fhir/sid/ndc': 'NDC' }; -export { defaultValues, genderOptions, headerDefinitions, shortNameMap, stateOptions, types }; +export { + defaultValues, + genderOptions, + headerDefinitions, + shortNameMap, + stateOptions, + types, + medicationRequestToRemsAdmins +}; diff --git a/src/util/util.js b/src/util/util.js index 244e805..b76c684 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -92,4 +92,30 @@ function getEtasu(etasuUrl, responseCallback) { ); } -export { retrieveLaunchContext, getEtasu }; +const getMedicationSpecificRemsAdminUrl = (request, globalState, hook) => { + const display = request.medicationCodeableConcept?.coding?.[0]?.display; + const rxnorm = request.medicationCodeableConcept?.coding?.[0]?.code; + + if (!rxnorm) { + console.log("ERROR: unknown MedicationRequest code: '", rxnorm); + return undefined; + } + + if (!(hook === 'patient-view' || hook === 'order-sign' || hook === 'order-select')) { + console.log(`ERROR: unknown hook type: ${hook}`); + return undefined; + } + + const cdsUrl = Object.values(globalState.medicationRequestToRemsAdmins).find( + value => Number(value.rxnorm) === Number(rxnorm) && value.hook === hook + )?.remsAdmin; + + if (!cdsUrl) { + console.log(`Medication ${display} is not a REMS medication`); + return undefined; + } + + return cdsUrl; +}; + +export { retrieveLaunchContext, getEtasu, getMedicationSpecificRemsAdminUrl };