From 7b4a20d45e393f08b3a4a3e965275967606c0a61 Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Fri, 21 Jun 2024 11:31:31 +0930 Subject: [PATCH] Add functionality to write extracted resource back to source FHIR server --- .../src/features/playground/api/extract.ts | 13 +++- .../components/ExtractButtonForPlayground.tsx | 4 +- .../playground/components/JsonEditor.tsx | 12 +++- .../playground/components/Playground.tsx | 1 + .../components/StoreStateViewer.tsx | 5 +- .../ExtractedResourceViewer.tsx | 67 ++++++++++++++++++- .../StoreStateViewers/GenericViewer.tsx | 5 +- 7 files changed, 96 insertions(+), 11 deletions(-) diff --git a/apps/smart-forms-app/src/features/playground/api/extract.ts b/apps/smart-forms-app/src/features/playground/api/extract.ts index b4c2c2fd..6611656f 100644 --- a/apps/smart-forms-app/src/features/playground/api/extract.ts +++ b/apps/smart-forms-app/src/features/playground/api/extract.ts @@ -16,7 +16,7 @@ */ import { HEADERS } from '../../../api/headers.ts'; -import type { Questionnaire, StructureMap } from 'fhir/r4'; +import type { Bundle, Questionnaire, StructureMap } from 'fhir/r4'; import * as FHIR from 'fhirclient'; import { FORMS_SERVER_URL } from '../../../globals.ts'; @@ -56,3 +56,14 @@ export async function fetchTargetStructureMap( return null; } + +export function extractedResourceIsBatchBundle( + extractedResource: any +): extractedResource is Bundle { + return ( + !!extractedResource && + !!extractedResource.resourceType && + extractedResource.resourceType === 'Bundle' && + (extractedResource.type === 'transaction' || extractedResource.type === 'batch') + ); +} diff --git a/apps/smart-forms-app/src/features/playground/components/ExtractButtonForPlayground.tsx b/apps/smart-forms-app/src/features/playground/components/ExtractButtonForPlayground.tsx index cdabe9ee..3268afa7 100644 --- a/apps/smart-forms-app/src/features/playground/components/ExtractButtonForPlayground.tsx +++ b/apps/smart-forms-app/src/features/playground/components/ExtractButtonForPlayground.tsx @@ -19,7 +19,7 @@ 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 CloudUploadIcon from '@mui/icons-material/CloudUpload'; import { FORMS_SERVER_URL } from '../../../globals.ts'; interface ExtractForPlaygroundProps { @@ -48,7 +48,7 @@ function ExtractButtonForPlayground(props: ExtractForPlaygroundProps) { {isExtracting ? ( ) : ( - + )} diff --git a/apps/smart-forms-app/src/features/playground/components/JsonEditor.tsx b/apps/smart-forms-app/src/features/playground/components/JsonEditor.tsx index ee5a7206..961038dd 100644 --- a/apps/smart-forms-app/src/features/playground/components/JsonEditor.tsx +++ b/apps/smart-forms-app/src/features/playground/components/JsonEditor.tsx @@ -28,12 +28,20 @@ interface Props { jsonString: string; onJsonStringChange: (jsonString: string) => void; buildingState: 'idle' | 'building' | 'built'; + fhirServerUrl: string; onBuildForm: (jsonString: string) => unknown; onDestroyForm: () => unknown; } function JsonEditor(props: Props) { - const { jsonString, onJsonStringChange, buildingState, onBuildForm, onDestroyForm } = props; + const { + jsonString, + onJsonStringChange, + buildingState, + fhirServerUrl, + onBuildForm, + onDestroyForm + } = props; const [view, setView] = useState<'editor' | 'storeState'>('editor'); const [selectedStore, setSelectedStore] = useState('questionnaireResponseStore'); @@ -132,7 +140,7 @@ function JsonEditor(props: Props) { /> ) : ( - + )} diff --git a/apps/smart-forms-app/src/features/playground/components/Playground.tsx b/apps/smart-forms-app/src/features/playground/components/Playground.tsx index 43af3898..307d8b9f 100644 --- a/apps/smart-forms-app/src/features/playground/components/Playground.tsx +++ b/apps/smart-forms-app/src/features/playground/components/Playground.tsx @@ -248,6 +248,7 @@ function Playground() { jsonString={jsonString} onJsonStringChange={(jsonString: string) => setJsonString(jsonString)} buildingState={buildingState} + fhirServerUrl={fhirServerUrl} onBuildForm={handleBuildQuestionnaireFromString} onDestroyForm={handleDestroyForm} /> diff --git a/apps/smart-forms-app/src/features/playground/components/StoreStateViewer.tsx b/apps/smart-forms-app/src/features/playground/components/StoreStateViewer.tsx index ae23eed7..1e1e7874 100644 --- a/apps/smart-forms-app/src/features/playground/components/StoreStateViewer.tsx +++ b/apps/smart-forms-app/src/features/playground/components/StoreStateViewer.tsx @@ -32,10 +32,11 @@ export type StateStore = interface StoreStateViewerProps { selectedStore: StateStore; + fhirServerUrl: string; } function StoreStateViewer(props: StoreStateViewerProps) { - const { selectedStore } = props; + const { selectedStore, fhirServerUrl } = props; if (selectedStore === 'questionnaireStore') { return ; @@ -54,7 +55,7 @@ function StoreStateViewer(props: StoreStateViewerProps) { } if (selectedStore === 'extractedResource') { - return ; + return ; } return No store selected; diff --git a/apps/smart-forms-app/src/features/playground/components/StoreStateViewers/ExtractedResourceViewer.tsx b/apps/smart-forms-app/src/features/playground/components/StoreStateViewers/ExtractedResourceViewer.tsx index eca1e949..50016bba 100644 --- a/apps/smart-forms-app/src/features/playground/components/StoreStateViewers/ExtractedResourceViewer.tsx +++ b/apps/smart-forms-app/src/features/playground/components/StoreStateViewers/ExtractedResourceViewer.tsx @@ -2,14 +2,58 @@ import { useState } from 'react'; import GenericStatePropertyPicker from './GenericStatePropertyPicker.tsx'; import GenericViewer from './GenericViewer.tsx'; import { useExtractOperationStore } from '../../stores/smartConfigStore.ts'; +import { Box, Tooltip } from '@mui/material'; +import { LoadingButton } from '@mui/lab'; +import { useSnackbar } from 'notistack'; +import { extractedResourceIsBatchBundle } from '../../api/extract.ts'; +import { HEADERS } from '../../../../api/headers.ts'; +import CloseSnackbar from '../../../../components/Snackbar/CloseSnackbar.tsx'; const extractedSectionPropertyNames: string[] = ['extracted']; -function ExtractedSectionViewer() { +interface ExtractedSectionViewerProps { + fhirServerUrl: string; +} + +function ExtractedSectionViewer(props: ExtractedSectionViewerProps) { + const { fhirServerUrl } = props; const [selectedProperty, setSelectedProperty] = useState('extracted'); const [showJsonTree, setShowJsonTree] = useState(false); + const [writingBack, setWritingBack] = useState(false); const extractedResource = useExtractOperationStore.use.extractedResource(); + const writeBackEnabled = extractedResourceIsBatchBundle(extractedResource); + + const { enqueueSnackbar } = useSnackbar(); + + // Write back extracted resource + async function handleExtract() { + if (!writeBackEnabled) { + return; + } + setWritingBack(true); + + const response = await fetch(fhirServerUrl, { + method: 'POST', + headers: { ...HEADERS, 'Content-Type': 'application/json;charset=utf-8' }, + body: JSON.stringify(extractedResource) + }); + setWritingBack(false); + + if (!response.ok) { + enqueueSnackbar('Failed to write back resource', { + variant: 'error', + preventDuplicate: true, + action: + }); + } else { + enqueueSnackbar(`Write back to ${fhirServerUrl} successful. See Network tab for response`, { + variant: 'success', + preventDuplicate: true, + action: + }); + } + } return ( <> @@ -22,8 +66,25 @@ function ExtractedSectionViewer() { propertyName={selectedProperty} propertyObject={extractedResource} showJsonTree={showJsonTree} - onToggleShowJsonTree={setShowJsonTree} - /> + onToggleShowJsonTree={setShowJsonTree}> + + + + + Write back + + + + + ); } diff --git a/apps/smart-forms-app/src/features/playground/components/StoreStateViewers/GenericViewer.tsx b/apps/smart-forms-app/src/features/playground/components/StoreStateViewers/GenericViewer.tsx index d2c22fb5..94d772c2 100644 --- a/apps/smart-forms-app/src/features/playground/components/StoreStateViewers/GenericViewer.tsx +++ b/apps/smart-forms-app/src/features/playground/components/StoreStateViewers/GenericViewer.tsx @@ -3,16 +3,18 @@ import NotesIcon from '@mui/icons-material/Notes'; import AccountTreeIcon from '@mui/icons-material/AccountTree'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import DebugResponseView from '../../../renderer/components/RendererDebugFooter/DebugResponseView.tsx'; +import { ReactNode } from 'react'; interface GenericViewerProps { propertyName: string; propertyObject: any; showJsonTree: boolean; onToggleShowJsonTree: (toggleShowJsonTree: boolean) => void; + children?: ReactNode; } function GenericViewer(props: GenericViewerProps) { - const { propertyName, propertyObject, showJsonTree, onToggleShowJsonTree } = props; + const { propertyName, propertyObject, showJsonTree, onToggleShowJsonTree, children } = props; if (propertyName === null) { return No property selected; @@ -57,6 +59,7 @@ function GenericViewer(props: GenericViewerProps) { : 'Use text view for fast Ctrl+F debugging.'} + {children} );