Skip to content

Commit

Permalink
Dynamically get target structure map to determine if QR is extractable
Browse files Browse the repository at this point in the history
  • Loading branch information
fongsean committed Jun 21, 2024
1 parent 04a5798 commit 75b55a5
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 63 deletions.
58 changes: 58 additions & 0 deletions apps/smart-forms-app/src/features/playground/api/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2024 Commonwealth Scientific and Industrial Research
* Organisation (CSIRO) ABN 41 687 119 230.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { HEADERS } from '../../../api/headers.ts';
import type { Questionnaire, StructureMap } from 'fhir/r4';
import * as FHIR from 'fhirclient';
import { FORMS_SERVER_URL } from '../../../globals.ts';

export async function fetchTargetStructureMap(
questionnaire: Questionnaire
): Promise<StructureMap | null> {
let targetStructureMapCanonical = questionnaire.extension?.find(
(extension) =>
extension.url ===
'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap'
)?.valueCanonical;

if (!targetStructureMapCanonical) {
return null;
}

targetStructureMapCanonical = targetStructureMapCanonical.replace('|', '&version=');
const requestUrl = `/StructureMap?url=${targetStructureMapCanonical}&_sort=_lastUpdated`;
const resource = await FHIR.client(FORMS_SERVER_URL).request({
url: requestUrl,
headers: HEADERS
});

// Response isn't a resource, exit early
if (!resource.resourceType) {
return null;
}

if (resource.resourceType === 'Bundle') {
return resource.entry?.find((entry: any) => entry.resource?.resourceType === 'StructureMap')

Check warning on line 49 in apps/smart-forms-app/src/features/playground/api/extract.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
?.resource as StructureMap;
}

if (resource.resourceType === 'StructureMap') {
return resource as StructureMap;
}

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,27 @@ import React from 'react';
import { CircularProgress, Fade, IconButton, Tooltip } from '@mui/material';
import Typography from '@mui/material/Typography';
import Iconify from '../../../components/Iconify/Iconify.tsx';
import { FORMS_SERVER_URL } from '../../../globals.ts';

interface ExtractForPlaygroundProps {
extractEnabled: boolean;
isExtracting: boolean;
onExtract: () => void;
}

function ExtractButtonForPlayground(props: ExtractForPlaygroundProps) {
const { isExtracting, onExtract } = props;
const { extractEnabled, isExtracting, onExtract } = props;

const toolTipText = extractEnabled
? 'Perform $extract'
: `The current questionnaire does not have a target StructureMap for $extract, or the target StructureMap cannot be found on ${FORMS_SERVER_URL}`;

return (
<>
<Tooltip title="Perform $extract" placement="bottom-end">
<Tooltip title={toolTipText} placement="bottom-end">
<span>
<IconButton
disabled={isExtracting}
disabled={isExtracting || !extractEnabled}
onClick={onExtract}
size="small"
color="primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,12 @@ interface Props {
jsonString: string;
onJsonStringChange: (jsonString: string) => void;
buildingState: 'idle' | 'building' | 'built';
isExtracting: boolean;
extractedResource: any;
onBuildForm: (jsonString: string) => unknown;
onDestroyForm: () => unknown;
}

function JsonEditor(props: Props) {
const {
jsonString,
onJsonStringChange,
buildingState,
isExtracting,
extractedResource,
onBuildForm,
onDestroyForm
} = props;
const { jsonString, onJsonStringChange, buildingState, onBuildForm, onDestroyForm } = props;

const [view, setView] = useState<'editor' | 'storeState'>('editor');
const [selectedStore, setSelectedStore] = useState<StateStore>('questionnaireResponseStore');
Expand Down Expand Up @@ -142,11 +132,7 @@ function JsonEditor(props: Props) {
/>
) : (
<Box sx={{ height: '100%', overflow: 'auto' }}>
<StoreStateViewer
selectedStore={selectedStore}
isExtracting={isExtracting}
extractedResource={extractedResource}
/>
<StoreStateViewer selectedStore={selectedStore} />
</Box>
)}
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,22 @@ import PopulationProgressSpinner from '../../../components/Spinners/PopulationPr
import { isQuestionnaire } from '../typePredicates/isQuestionnaire.ts';
import type { BuildState } from '../types/buildState.interface.ts';
import { useLocalStorage } from 'usehooks-ts';
import { buildForm, destroyForm, useQuestionnaireResponseStore } from '@aehrc/smart-forms-renderer';
import {
buildForm,
destroyForm,
removeEmptyAnswersFromResponse,
useQuestionnaireResponseStore,
useQuestionnaireStore
} from '@aehrc/smart-forms-renderer';
import RendererDebugFooter from '../../renderer/components/RendererDebugFooter/RendererDebugFooter.tsx';
import CloseSnackbar from '../../../components/Snackbar/CloseSnackbar.tsx';
import { TERMINOLOGY_SERVER_URL } from '../../../globals.ts';
import PlaygroundPicker from './PlaygroundPicker.tsx';
import type { Patient, Practitioner, Questionnaire } from 'fhir/r4';
import PlaygroundHeader from './PlaygroundHeader.tsx';
import { HEADERS } from '../../../api/headers.ts';
import { fetchTargetStructureMap } from '../api/extract.ts';
import { useExtractOperationStore } from '../stores/smartConfigStore.ts';

const defaultFhirServerUrl = 'https://hapi.fhir.org/baseR4';
const defaultExtractEndpoint = 'https://proxy.smartforms.io/fhir';
Expand All @@ -53,24 +61,36 @@ function Playground() {

// $extract-related states
const [isExtracting, setExtracting] = useState(false);
const [extractedResource, setExtractedResource] = useState<any>(null);

const sourceQuestionnaire = useQuestionnaireStore.use.sourceQuestionnaire();
const updatableResponse = useQuestionnaireResponseStore.use.updatableResponse();

const resetExtractOperationStore = useExtractOperationStore.use.resetStore();
const setTargetStructureMap = useExtractOperationStore.use.setTargetStructureMap();
const setExtractedResource = useExtractOperationStore.use.setExtractedResource();

const { enqueueSnackbar } = useSnackbar();

function handleDestroyForm() {
setBuildingState('idle');
resetExtractOperationStore();
destroyForm();
}

async function handleBuildQuestionnaireFromString(jsonString: string) {
setBuildingState('building');
resetExtractOperationStore();

setJsonString(jsonString);

try {
const parsedQuestionnaire = JSON.parse(jsonString);
if (isQuestionnaire(parsedQuestionnaire)) {
const targetStructureMap = await fetchTargetStructureMap(parsedQuestionnaire);
if (targetStructureMap) {
setTargetStructureMap(targetStructureMap);
}

await buildForm(parsedQuestionnaire, undefined, undefined, TERMINOLOGY_SERVER_URL);
setBuildingState('built');
} else {
Expand All @@ -94,13 +114,22 @@ function Playground() {

async function handleBuildQuestionnaireFromResource(questionnaire: Questionnaire) {
setBuildingState('building');
resetExtractOperationStore();

setJsonString(JSON.stringify(questionnaire, null, 2));
const targetStructureMap = await fetchTargetStructureMap(questionnaire);
if (targetStructureMap) {
setTargetStructureMap(targetStructureMap);
}

await buildForm(questionnaire, undefined, undefined, TERMINOLOGY_SERVER_URL);
setBuildingState('built');
}

function handleBuildQuestionnaireFromFile(jsonFile: File) {
setBuildingState('building');
resetExtractOperationStore();

if (!jsonFile.name.endsWith('.json')) {
enqueueSnackbar('Attached file must be a JSON file', {
variant: 'error',
Expand All @@ -118,7 +147,13 @@ function Playground() {
const jsonString = event.target?.result;
if (typeof jsonString === 'string') {
setJsonString(jsonString);
await buildForm(JSON.parse(jsonString), undefined, undefined, TERMINOLOGY_SERVER_URL);
const questionnaire = JSON.parse(jsonString);
const targetStructureMap = await fetchTargetStructureMap(questionnaire);
if (targetStructureMap) {
setTargetStructureMap(targetStructureMap);
}

await buildForm(questionnaire, undefined, undefined, TERMINOLOGY_SERVER_URL);
setBuildingState('built');
} else {
enqueueSnackbar('There was an issue with the attached JSON file.', {
Expand Down Expand Up @@ -146,7 +181,7 @@ function Playground() {
const response = await fetch(defaultExtractEndpoint + '/QuestionnaireResponse/$extract', {
method: 'POST',
headers: { ...HEADERS, 'Content-Type': 'application/json;charset=utf-8' },
body: JSON.stringify(updatableResponse)
body: JSON.stringify(removeEmptyAnswersFromResponse(sourceQuestionnaire, updatableResponse))
});
setExtracting(false);

Expand All @@ -159,7 +194,7 @@ function Playground() {
setExtractedResource(null);
} else {
enqueueSnackbar(
'Extract successful. See advanced properties > extracted to view extracted resource.',
'Extract successful. See Advanced Properties > Extracted to view extracted resource.',
{
preventDuplicate: true,
action: <CloseSnackbar />,
Expand Down Expand Up @@ -213,8 +248,6 @@ function Playground() {
jsonString={jsonString}
onJsonStringChange={(jsonString: string) => setJsonString(jsonString)}
buildingState={buildingState}
isExtracting={isExtracting}
extractedResource={extractedResource}
onBuildForm={handleBuildQuestionnaireFromString}
onDestroyForm={handleDestroyForm}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Box, Typography } from '@mui/material';
import useLaunchContextNames from '../hooks/useLaunchContextNames.ts';
import { TERMINOLOGY_SERVER_URL } from '../../../globals.ts';
import ExtractButtonForPlayground from './ExtractButtonForPlayground.tsx';
import { useExtractOperationStore } from '../stores/smartConfigStore.ts';

interface PlaygroundRendererProps {
endpointUrl: string | null;
Expand All @@ -38,12 +39,14 @@ function PlaygroundRenderer(props: PlaygroundRendererProps) {
const { endpointUrl, patient, user, isExtracting, onExtract } = props;

const sourceQuestionnaire = useQuestionnaireStore.use.sourceQuestionnaire();
const targetStructureMap = useExtractOperationStore.use.targetStructureMap();

const [isPopulating, setIsPopulating] = useState(false);

const { patientName, userName } = useLaunchContextNames(patient, user);

const prePopEnabled = endpointUrl !== null && patient !== null;
const extractEnabled = targetStructureMap !== null;

function handlePrepopulate() {
if (!prePopEnabled) {
Expand Down Expand Up @@ -78,24 +81,30 @@ function PlaygroundRenderer(props: PlaygroundRendererProps) {

return (
<>
{prePopEnabled ? (
<Box display="flex" alignItems="center" columnGap={1.5} mx={1}>
<PrePopButtonForPlayground isPopulating={isPopulating} onPopulate={handlePrepopulate} />
<ExtractButtonForPlayground isExtracting={isExtracting} onExtract={onExtract} />
<Box flexGrow={1} />

{patientName ? (
<Typography variant="subtitle2" color="text.secondary">
Patient: {patientName}
</Typography>
) : null}
{userName ? (
<Typography variant="subtitle2" color="text.secondary">
User: {userName}
</Typography>
) : null}
</Box>
) : null}
<Box display="flex" alignItems="center" columnGap={1.5} mx={1}>
<PrePopButtonForPlayground
prePopEnabled={prePopEnabled}
isPopulating={isPopulating}
onPopulate={handlePrepopulate}
/>
<ExtractButtonForPlayground
extractEnabled={extractEnabled}
isExtracting={isExtracting}
onExtract={onExtract}
/>
<Box flexGrow={1} />

{patientName ? (
<Typography variant="subtitle2" color="text.secondary">
Patient: {patientName}
</Typography>
) : null}
{userName ? (
<Typography variant="subtitle2" color="text.secondary">
User: {userName}
</Typography>
) : null}
</Box>
{isPopulating ? null : <BaseRenderer />}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,24 @@ import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import Typography from '@mui/material/Typography';

interface PrePopButtonForPlaygroundProps {
prePopEnabled: boolean;
isPopulating: boolean;
onPopulate: () => void;
}

function PrePopButtonForPlayground(props: PrePopButtonForPlaygroundProps) {
const { isPopulating, onPopulate } = props;
const { prePopEnabled, isPopulating, onPopulate } = props;

const toolTipText = prePopEnabled
? 'Pre-populate form'
: 'Please select a patient in the Launch Context settings (located on the top right) to enable pre-population';

return (
<>
<Tooltip title="Pre-populate form" placement="bottom-end">
<Tooltip title={toolTipText} placement="bottom-end">
<span>
<IconButton
disabled={isPopulating}
disabled={isPopulating || !prePopEnabled}
onClick={onPopulate}
size="small"
color="primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,10 @@ export type StateStore =

interface StoreStateViewerProps {
selectedStore: StateStore;
isExtracting: boolean;
extractedResource: any;
}

function StoreStateViewer(props: StoreStateViewerProps) {
const { selectedStore, isExtracting, extractedResource } = props;
const { selectedStore } = props;

if (selectedStore === 'questionnaireStore') {
return <QuestionnaireStoreViewer />;
Expand All @@ -56,9 +54,7 @@ function StoreStateViewer(props: StoreStateViewerProps) {
}

if (selectedStore === 'extractedResource') {
return (
<ExtractedSectionViewer isExtracting={isExtracting} extractedResource={extractedResource} />
);
return <ExtractedSectionViewer />;
}

return <Typography variant="h5">No store selected</Typography>;
Expand Down
Loading

0 comments on commit 75b55a5

Please sign in to comment.