diff --git a/apps/smart-forms-app/src/features/standalone/components/Standalone.tsx b/apps/smart-forms-app/src/features/standalone/components/Standalone.tsx index 14ab0c4a9..4307b6a87 100644 --- a/apps/smart-forms-app/src/features/standalone/components/Standalone.tsx +++ b/apps/smart-forms-app/src/features/standalone/components/Standalone.tsx @@ -36,7 +36,8 @@ const rendererPropsReducer = (state: RendererPropsState, action: RendererPropsAc questionnaire: action.payload.questionnaire, response: action.payload.response, additionalVars: action.payload.additionalVars, - terminologyServerUrl: action.payload.terminologyServerUrl + terminologyServerUrl: action.payload.terminologyServerUrl, + readOnly: action.payload.readOnly }; case 'SET_RESPONSE': return { ...state, response: action.payload }; @@ -44,6 +45,8 @@ const rendererPropsReducer = (state: RendererPropsState, action: RendererPropsAc return { ...state, additionalVars: action.payload }; case 'SET_TERMINOLOGY_SERVER': return { ...state, terminologyServerUrl: action.payload }; + case 'SET_READ_ONLY': + return { ...state, readOnly: action.payload }; default: return state; } @@ -55,7 +58,8 @@ function Standalone() { questionnaire: rendererPropsList[0].questionnaire, response: rendererPropsList[0].response, additionalVars: rendererPropsList[0].additionalVars, - terminologyServerUrl: rendererPropsList[0].terminologyServerUrl + terminologyServerUrl: rendererPropsList[0].terminologyServerUrl, + readOnly: rendererPropsList[0].readOnly }); const [resourcesShown, setResourcesShown] = useState(false); @@ -87,6 +91,7 @@ function Standalone() { questionnaireResponse={state.response ?? undefined} additionalVariables={state.additionalVars ?? undefined} terminologyServerUrl={state.terminologyServerUrl ?? undefined} + readOnly={state.readOnly} /> ); diff --git a/apps/smart-forms-app/src/features/standalone/components/StandalonePropsPicker.tsx b/apps/smart-forms-app/src/features/standalone/components/StandalonePropsPicker.tsx index 40306d396..86f1bcdce 100644 --- a/apps/smart-forms-app/src/features/standalone/components/StandalonePropsPicker.tsx +++ b/apps/smart-forms-app/src/features/standalone/components/StandalonePropsPicker.tsx @@ -107,6 +107,23 @@ function StandalonePropsPicker(props: StandalonePropsPickerProps) { label="Terminology server url" /> ) : null} + + {rendererPropsSingle.readOnly !== null ? ( + { + dispatch({ + type: 'SET_READ_ONLY', + payload: !state.readOnly + }); + }} + /> + } + label="Read only" + /> + ) : null} diff --git a/apps/smart-forms-app/src/features/standalone/interfaces/standalone.interface.ts b/apps/smart-forms-app/src/features/standalone/interfaces/standalone.interface.ts index 7cee86abd..390aec03b 100644 --- a/apps/smart-forms-app/src/features/standalone/interfaces/standalone.interface.ts +++ b/apps/smart-forms-app/src/features/standalone/interfaces/standalone.interface.ts @@ -23,6 +23,7 @@ export interface RendererPropsState { response: QuestionnaireResponse | null; additionalVars: Record | null; terminologyServerUrl: string | null; + readOnly: boolean; } export type RendererPropsActions = @@ -34,8 +35,10 @@ export type RendererPropsActions = response: QuestionnaireResponse | null; additionalVars: Record | null; terminologyServerUrl: string | null; + readOnly: boolean; }; } | { type: 'SET_RESPONSE'; payload: QuestionnaireResponse | null } | { type: 'SET_ADDITIONAL_VARS'; payload: Record | null } - | { type: 'SET_TERMINOLOGY_SERVER'; payload: string | null }; + | { type: 'SET_TERMINOLOGY_SERVER'; payload: string | null } + | { type: 'SET_READ_ONLY'; payload: boolean }; diff --git a/apps/smart-forms-app/src/features/standalone/utils/standaloneList.ts b/apps/smart-forms-app/src/features/standalone/utils/standaloneList.ts index cdf4142e6..a9635798f 100644 --- a/apps/smart-forms-app/src/features/standalone/utils/standaloneList.ts +++ b/apps/smart-forms-app/src/features/standalone/utils/standaloneList.ts @@ -33,27 +33,31 @@ export const rendererPropsList: RendererPropsState[] = [ questionnaire: Q715Json as Questionnaire, response: R715Json as QuestionnaireResponse, additionalVars: null, - terminologyServerUrl: null + terminologyServerUrl: null, + readOnly: false }, { id: 'TestGrid', questionnaire: QTestGridJson as Questionnaire, response: RTestGridJson as QuestionnaireResponse, additionalVars: AddVarsTestGridJson, - terminologyServerUrl: null + terminologyServerUrl: null, + readOnly: false }, { id: 'CVDRiskCalculator', questionnaire: QCVDRiskJson as Questionnaire, response: RCVDRiskJson as QuestionnaireResponse, additionalVars: null, - terminologyServerUrl: null + terminologyServerUrl: null, + readOnly: false }, { id: 'DemoAnswerExpression', questionnaire: QDemoAnsExp as Questionnaire, response: RDemoAnsExp as QuestionnaireResponse, additionalVars: null, - terminologyServerUrl: 'http://hapi.fhir.org/baseR4' + terminologyServerUrl: 'http://hapi.fhir.org/baseR4', + readOnly: false } ]; diff --git a/documentation/docs/sdc/population.mdx b/documentation/docs/sdc/population.mdx index f3f0a7297..eaef1ae43 100644 --- a/documentation/docs/sdc/population.mdx +++ b/documentation/docs/sdc/population.mdx @@ -73,7 +73,7 @@ In the below x-fhir-query variable, `{{%patient.id}}` is a reference to the laun } ``` -For usages, refer to [initial](/smart-forms/docs/sdc/population#initial), [initialExpression](/smart-forms/docs/sdc/population#initialExpression), and [calculatedExpression](<(/smart-forms/docs/sdc/population#calculatedExpression)>) sections on this page. +For usages, refer to [initial](/smart-forms/docs/sdc/population#initial), [initialExpression](/smart-forms/docs/sdc/population#initialExpression), and [calculatedExpression](/smart-forms/docs/sdc/population#calculatedexpression) sections on this page. ### X-FHIR-Query Variable @@ -103,7 +103,7 @@ After the query is executed, the expression can be consumed by initialExpression } ``` -For usages, refer to [initial](/smart-forms/docs/sdc/population#initial), [initialExpression](/smart-forms/docs/sdc/population#initialExpression), and [calculatedExpression](<(/smart-forms/docs/sdc/population#calculatedExpression)>) sections on this page. +For usages, refer to [initial](/smart-forms/docs/sdc/population#initial), [initialExpression](/smart-forms/docs/sdc/population#initialExpression), and [calculatedExpression](/smart-forms/docs/sdc/population#calculatedexpression) sections on this page. ### SourceQueries diff --git a/packages/smart-forms-renderer/.storybook/preview.ts b/packages/smart-forms-renderer/.storybook/preview.tsx similarity index 100% rename from packages/smart-forms-renderer/.storybook/preview.ts rename to packages/smart-forms-renderer/.storybook/preview.tsx diff --git a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx index b9644e6e1..efe859e2a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx @@ -64,15 +64,17 @@ const BooleanField = memo(function BooleanField(props: BooleanFieldProps) { - + + + diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx index ad1c3ca91..49904556a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx @@ -70,15 +70,17 @@ function ChoiceRadioAnswerOptionFields(props: ChoiceRadioAnswerOptionFieldsProps - + + + diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx index d936b3250..28cbad169 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx @@ -77,15 +77,17 @@ function ChoiceRadioAnswerValueSetFields(props: ChoiceRadioAnswerValueSetFieldsP - + + + diff --git a/packages/smart-forms-renderer/src/components/Renderer/SmartFormsRenderer.tsx b/packages/smart-forms-renderer/src/components/Renderer/SmartFormsRenderer.tsx index bc347bcc1..3d83f90f6 100644 --- a/packages/smart-forms-renderer/src/components/Renderer/SmartFormsRenderer.tsx +++ b/packages/smart-forms-renderer/src/components/Renderer/SmartFormsRenderer.tsx @@ -18,13 +18,13 @@ import React from 'react'; import RendererThemeProvider from '../../theme/Theme'; import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; -import useInitialiseRenderer from '../../hooks/useInitialiseRenderer'; +import useInitialiseForm from '../../hooks/useInitialiseForm'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import Typography from '@mui/material/Typography'; import { QueryClientProvider } from '@tanstack/react-query'; -import useQueryClient from '../../hooks/useQueryClient'; +import useRendererQueryClient from '../../hooks/useRendererQueryClient'; import BaseRenderer from './BaseRenderer'; import type Client from 'fhirclient/lib/Client'; @@ -53,7 +53,9 @@ export interface SmartFormsRendererProps { /** * A self-initialising wrapper around the BaseRenderer rendering engine. - * See SmartFormsRendererProps for props. + * // FIXME add github link + * + * @see {SmartFormsRendererProps} for props. * * @author Sean Fong */ @@ -67,15 +69,15 @@ function SmartFormsRenderer(props: SmartFormsRendererProps) { readOnly } = props; - const isLoading = useInitialiseRenderer( + const isLoading = useInitialiseForm( questionnaire, questionnaireResponse, - additionalVariables, + readOnly, terminologyServerUrl, - fhirClient, - readOnly + additionalVariables, + fhirClient ); - const queryClient = useQueryClient(); + const queryClient = useRendererQueryClient(); if (isLoading) { return ( diff --git a/packages/smart-forms-renderer/src/hooks/index.ts b/packages/smart-forms-renderer/src/hooks/index.ts index 2eef71d0a..5ba1b9c01 100644 --- a/packages/smart-forms-renderer/src/hooks/index.ts +++ b/packages/smart-forms-renderer/src/hooks/index.ts @@ -1,2 +1,3 @@ export { default as useHidden } from './useHidden'; export { default as useBuildForm } from './useBuildForm'; +export { default as useRendererQueryClient } from './useRendererQueryClient'; diff --git a/packages/smart-forms-renderer/src/hooks/useBuildForm.ts b/packages/smart-forms-renderer/src/hooks/useBuildForm.ts index cbd8971cf..fb9270f72 100644 --- a/packages/smart-forms-renderer/src/hooks/useBuildForm.ts +++ b/packages/smart-forms-renderer/src/hooks/useBuildForm.ts @@ -19,14 +19,38 @@ import { useLayoutEffect, useState } from 'react'; import { buildForm } from '../utils'; import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; -function useBuildForm(questionnaire: Questionnaire, questionnaireResponse?: QuestionnaireResponse) { +/** + * React hook wrapping around the buildForm() function to build a form from a questionnaire and an optional QuestionnaireResponse. + * @see {buildForm} for more information. + * + * @param questionnaire - Questionnaire to be rendered + * @param questionnaireResponse - Pre-populated/draft/loaded QuestionnaireResponse to be rendered (optional) + * @param readOnly - Applies read-only mode to all items in the form view + * @param terminologyServerUrl - Terminology server url to fetch terminology. If not provided, the default terminology server will be used. (optional) + * @param additionalVariables - Additional key-value pair of SDC variables for testing (optional) + * + * @author Sean Fong + */ +function useBuildForm( + questionnaire: Questionnaire, + questionnaireResponse?: QuestionnaireResponse, + readOnly?: boolean, + terminologyServerUrl?: string, + additionalVariables?: Record +) { const [isBuilding, setIsBuilding] = useState(true); useLayoutEffect(() => { - buildForm(questionnaire, questionnaireResponse).then(() => { + buildForm( + questionnaire, + questionnaireResponse, + readOnly, + terminologyServerUrl, + additionalVariables + ).then(() => { setIsBuilding(false); }); - }, [questionnaire, questionnaireResponse]); + }, [additionalVariables, questionnaire, questionnaireResponse, readOnly, terminologyServerUrl]); return isBuilding; } diff --git a/packages/smart-forms-renderer/src/hooks/useHidden.ts b/packages/smart-forms-renderer/src/hooks/useHidden.ts index c9cc85b24..0030987b5 100644 --- a/packages/smart-forms-renderer/src/hooks/useHidden.ts +++ b/packages/smart-forms-renderer/src/hooks/useHidden.ts @@ -20,6 +20,12 @@ import { useQuestionnaireStore } from '../stores'; import { isHiddenByEnableWhen } from '../utils/qItem'; import { structuredDataCapture } from 'fhir-sdc-helpers'; +/** + * React hook to determine if a QuestionnaireItem is hidden via item.hidden, enableWhens, enableWhenExpressions. + * When checking for repeating group enableWhen items, the parentRepeatGroupIndex should be provided. + * + * @author Sean Fong + */ function useHidden(qItem: QuestionnaireItem, parentRepeatGroupIndex?: number): boolean { const enableWhenIsActivated = useQuestionnaireStore.use.enableWhenIsActivated(); const enableWhenItems = useQuestionnaireStore.use.enableWhenItems(); diff --git a/packages/smart-forms-renderer/src/hooks/useInitialiseForm.ts b/packages/smart-forms-renderer/src/hooks/useInitialiseForm.ts new file mode 100644 index 000000000..23e4ca927 --- /dev/null +++ b/packages/smart-forms-renderer/src/hooks/useInitialiseForm.ts @@ -0,0 +1,93 @@ +/* + * 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 type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; +import { useLayoutEffect, useState } from 'react'; +import { buildForm } from '../utils'; +import type Client from 'fhirclient/lib/Client'; +import { useSmartConfigStore } from '../stores'; +import { initialiseFhirClient } from '../utils/manageForm'; + +/** + * React hook to initialise a form by: + * - Calling the buildForm() function to build a form from a questionnaire, an optional QuestionnaireResponse and other optional properties. + * - Setting a FHIRClient object to make further FHIR calls i.e. answerExpressions. + * + * @param questionnaire - Questionnaire to be rendered + * @param questionnaireResponse - Pre-populated/draft/loaded QuestionnaireResponse to be rendered (optional) + * @param readOnly - Applies read-only mode to all items in the form view + * @param terminologyServerUrl - Terminology server url to fetch terminology. If not provided, the default terminology server will be used. (optional) + * @param additionalVariables - Additional key-value pair of SDC variables for testing (optional) + * @param fhirClient - FHIRClient object to perform further FHIR calls. At the moment it's only used in answerExpressions (optional) + * + * @see {buildForm} for more information. + * + * @author Sean Fong + */ +function useInitialiseForm( + questionnaire: Questionnaire, + questionnaireResponse?: QuestionnaireResponse, + readOnly?: boolean, + terminologyServerUrl?: string, + additionalVariables?: Record, + fhirClient?: Client +): boolean { + const [isFhirClientReady, setIsFhirClientReady] = useState(true); + const [isBuilding, setIsBuilding] = useState(true); + + const setSmartClient = useSmartConfigStore.use.setClient(); + const setPatient = useSmartConfigStore.use.setPatient(); + const setUser = useSmartConfigStore.use.setUser(); + const setEncounter = useSmartConfigStore.use.setEncounter(); + + useLayoutEffect(() => { + setIsBuilding(true); + if (fhirClient) { + setIsFhirClientReady(true); + initialiseFhirClient(fhirClient).then(() => { + setIsFhirClientReady(false); + }); + } + + // Initialise form including Questionnaire and other optionally provided parameters + // Includes initialisation for enableWhen, enableWhenExpressions, calculatedExpressions, initialExpressions, answerExpressions, cache answerValueSets + buildForm( + questionnaire, + questionnaireResponse, + readOnly, + terminologyServerUrl, + additionalVariables + ).then(() => { + setIsBuilding(false); + }); + }, [ + questionnaire, + questionnaireResponse, + additionalVariables, + terminologyServerUrl, + fhirClient, + readOnly, + setSmartClient, + setPatient, + setUser, + setEncounter + ]); + + return isFhirClientReady && isBuilding; +} + +export default useInitialiseForm; diff --git a/packages/smart-forms-renderer/src/hooks/useInitialiseRenderer.ts b/packages/smart-forms-renderer/src/hooks/useInitialiseRenderer.ts deleted file mode 100644 index 569e71294..000000000 --- a/packages/smart-forms-renderer/src/hooks/useInitialiseRenderer.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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 type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; -import { useLayoutEffect, useState } from 'react'; -import { initialiseQuestionnaireResponse } from '../utils'; -import type Client from 'fhirclient/lib/Client'; -import { readEncounter, readPatient, readUser } from '../api/smartClient'; -import { - useQuestionnaireResponseStore, - useQuestionnaireStore, - useSmartConfigStore, - useTerminologyServerStore -} from '../stores'; - -function useInitialiseRenderer( - questionnaire: Questionnaire, - questionnaireResponse?: QuestionnaireResponse, - additionalVariables?: Record, - terminologyServerUrl?: string, - fhirClient?: Client, - readOnly?: boolean -): boolean { - const buildSourceQuestionnaire = useQuestionnaireStore.use.buildSourceQuestionnaire(); - const updatePopulatedProperties = useQuestionnaireStore.use.updatePopulatedProperties(); - const buildSourceResponse = useQuestionnaireResponseStore.use.buildSourceResponse(); - const setUpdatableResponseAsPopulated = - useQuestionnaireResponseStore.use.setUpdatableResponseAsPopulated(); - - const setTerminologyServerUrl = useTerminologyServerStore.use.setUrl(); - const resetTerminologyServerUrl = useTerminologyServerStore.use.resetUrl(); - const setSmartClient = useSmartConfigStore.use.setClient(); - const setPatient = useSmartConfigStore.use.setPatient(); - const setUser = useSmartConfigStore.use.setUser(); - const setEncounter = useSmartConfigStore.use.setEncounter(); - - const [loading, setLoading] = useState(true); - - useLayoutEffect(() => { - setLoading(true); - // set fhirClient if provided - if (fhirClient) { - setSmartClient(fhirClient); - readPatient(fhirClient).then((patient) => { - setPatient(patient); - }); - readUser(fhirClient).then((user) => { - setUser(user); - }); - readEncounter(fhirClient).then((encounter) => { - setEncounter(encounter); - }); - } - - // set terminology server url if provided, otherwise reset it back to ontoserver - if (terminologyServerUrl) { - setTerminologyServerUrl(terminologyServerUrl); - } else { - resetTerminologyServerUrl(); - } - - // initialise form including enableWhen, enableWhenExpressions, calculatedExpressions, initialExpressions, answerExpressions, cache answerValueSets - buildSourceQuestionnaire( - questionnaire, - questionnaireResponse, - additionalVariables, - terminologyServerUrl, - readOnly - ).then(() => { - buildSourceResponse(initialiseQuestionnaireResponse(questionnaire)); - - if (questionnaireResponse) { - const updatedResponse = updatePopulatedProperties(questionnaireResponse); - setUpdatableResponseAsPopulated(updatedResponse); - } - setLoading(false); - }); - }, [ - questionnaire, - questionnaireResponse, - buildSourceQuestionnaire, - buildSourceResponse, - setUpdatableResponseAsPopulated, - updatePopulatedProperties, - additionalVariables, - fhirClient, - setSmartClient, - setPatient, - setUser, - setEncounter, - terminologyServerUrl, - setTerminologyServerUrl, - resetTerminologyServerUrl, - readOnly - ]); - - return loading; -} - -export default useInitialiseRenderer; diff --git a/packages/smart-forms-renderer/src/hooks/useQueryClient.ts b/packages/smart-forms-renderer/src/hooks/useRendererQueryClient.ts similarity index 73% rename from packages/smart-forms-renderer/src/hooks/useQueryClient.ts rename to packages/smart-forms-renderer/src/hooks/useRendererQueryClient.ts index c7a70155e..dce3f7aa5 100644 --- a/packages/smart-forms-renderer/src/hooks/useQueryClient.ts +++ b/packages/smart-forms-renderer/src/hooks/useRendererQueryClient.ts @@ -17,7 +17,14 @@ import { QueryClient } from '@tanstack/react-query'; -function useQueryClient() { +/** + * Default QueryClient used by the renderer. + * You can customise your own QueryClient with your own options, use v4 of @tanstack/react-query. + * @see {@link https://tanstack.com/query/v4/docs/reference/QueryClient} + * + * @author Sean Fong + */ +function useRendererQueryClient() { return new QueryClient({ defaultOptions: { queries: { @@ -28,4 +35,4 @@ function useQueryClient() { }); } -export default useQueryClient; +export default useRendererQueryClient; diff --git a/packages/smart-forms-renderer/src/index.ts b/packages/smart-forms-renderer/src/index.ts index ab0e12138..edf37bef6 100644 --- a/packages/smart-forms-renderer/src/index.ts +++ b/packages/smart-forms-renderer/src/index.ts @@ -15,6 +15,12 @@ export { } from './components'; // state management store exports +export type { + QuestionnaireStoreType, + QuestionnaireResponseStoreType, + SmartConfigStoreType, + TerminologyServerStoreType +} from './stores'; export { questionnaireStore, useQuestionnaireStore, @@ -27,7 +33,7 @@ export { } from './stores'; // hooks exports -export { useHidden, useBuildForm } from './hooks'; +export { useHidden, useBuildForm, useRendererQueryClient } from './hooks'; // utils exports export type { ItemToRepopulate } from './utils'; diff --git a/packages/smart-forms-renderer/src/stores/index.ts b/packages/smart-forms-renderer/src/stores/index.ts index cca1de05f..f9a8fb0b9 100644 --- a/packages/smart-forms-renderer/src/stores/index.ts +++ b/packages/smart-forms-renderer/src/stores/index.ts @@ -1,7 +1,14 @@ +export type { QuestionnaireStoreType } from './questionnaireStore'; export { questionnaireStore, useQuestionnaireStore } from './questionnaireStore'; + +export type { QuestionnaireResponseStoreType } from './questionnaireResponseStore'; export { questionnaireResponseStore, useQuestionnaireResponseStore } from './questionnaireResponseStore'; + +export type { SmartConfigStoreType } from './smartConfigStore'; export { smartConfigStore, useSmartConfigStore } from './smartConfigStore'; + +export type { TerminologyServerStoreType } from './terminologyServerStore'; export { terminologyServerStore, useTerminologyServerStore } from './terminologyServerStore'; diff --git a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts index 5bd369458..ede316c1a 100644 --- a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts +++ b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts @@ -31,7 +31,28 @@ import { validateQuestionnaire } from '../utils/validateQuestionnaire'; import { questionnaireStore } from './questionnaireStore'; import { createQuestionnaireResponseItemMap } from '../utils/questionnaireResponseStoreUtils/updatableResponseItems'; -interface QuestionnaireResponseStoreType { +/** + * QuestionnaireResponseStore properties and methods + * Properties can be accessed for fine-grain details. + * Methods are usually used internally, using them from an external source is not recommended. + * + * @property sourceResponse - The original response created when the form is first initialised i.e. empty, pre-populated, opened saved draft + * @property updatableResponse - The current state of the response that is being updated via form fields + * @property updatableResponseItems - Key-value pair of updatableResponse items + * @property formChangesHistory - Array of form changes history in the form of deep-diff objects + * @property invalidItems - Key-value pair of invalid items based on defined value constraints in the questionnaire + * @property responseIsValid - Whether there are any invalid items in the response + * @method validateQuestionnaire - Used to validate the questionnaire response based on the questionnaire + * @method buildSourceResponse - Used to build the source response when the form is first initialised + * @method setUpdatableResponseAsPopulated - Used to set a pre-populated response as the current response + * @method updateResponse - Used to update the current response + * @method setUpdatableResponseAsSaved - Used to set a saved response as the current response + * @method setUpdatableResponseAsEmpty - Used to set an empty response as the current response + * @method destroySourceResponse - Used to destroy the source response and reset all properties + * + * @author Sean Fong + */ +export interface QuestionnaireResponseStoreType { sourceResponse: QuestionnaireResponse; updatableResponse: QuestionnaireResponse; updatableResponseItems: Record; @@ -50,6 +71,13 @@ interface QuestionnaireResponseStoreType { destroySourceResponse: () => void; } +/** + * QuestionnaireResponse state management store which contains all properties and methods to manage the state of the questionnaireResponse. + * This is the vanilla version of the store which can be used in non-React environments. + * @see {QuestionnaireResponseStoreType} for available properties and methods. + * + * @author Sean Fong + */ export const questionnaireResponseStore = createStore()( (set, get) => ({ sourceResponse: cloneDeep(emptyResponse), @@ -125,4 +153,12 @@ export const questionnaireResponseStore = createStore @@ -69,8 +71,21 @@ import { createQuestionnaireResponseItemMap } from '../utils/questionnaireRespon * @property populatedContext - Key-value pair of one-off pre-populated FHIRPath values * @property focusedLinkId - LinkId of the currently focused item * @property readOnly - Flag to set the form to read-only mode - // * @method buildSourceQuestionnaire - Used to build the source questionnaire with the provided questionnaire and questionnaire response + * @method buildSourceQuestionnaire - Used to build the source questionnaire with the provided questionnaire and optionally questionnaire response, additional variables, terminology server url and readyOnly flag + * @method destroySourceQuestionnaire - Used to destroy the source questionnaire and reset all properties + * @method switchTab - Used to switch the current tab index + * @method markTabAsComplete - Used to mark a tab index as complete + * @method updateEnableWhenItem - Used to update linked enableWhen items by updating a question with a new answer + * @method mutateRepeatEnableWhenItems - Used to add or remove instances of repeating enableWhen items + * @method toggleEnableWhenActivation - Used to toggle enableWhen checks on/off + * @method updateExpressions - Used to update all SDC expressions based on the updated questionnaire response + * @method addCodingToCache - Used to add a coding to the cached value set codings + * @method updatePopulatedProperties - Used to update all SDC expressions based on a pre-populated questionnaire response + * @method onFocusLinkId - Used to set the focused linkId + * @method setPopulatedContext - Used to set the populated contexts (launchContext, sourceQueries, x-fhir-query vars) for debugging purposes + * @method setFormAsReadOnly - Used to set the form as read-only * + * @author Sean Fong */ export interface QuestionnaireStoreType { sourceQuestionnaire: Questionnaire; @@ -96,7 +111,7 @@ export interface QuestionnaireStoreType { questionnaire: Questionnaire, questionnaireResponse?: QuestionnaireResponse, additionalVariables?: Record, - terminologyServerUrl?: string, // FIXME + terminologyServerUrl?: string, readOnly?: boolean ) => Promise; destroySourceQuestionnaire: () => void; @@ -122,17 +137,13 @@ export interface QuestionnaireStoreType { ) => QuestionnaireResponse; onFocusLinkId: (linkId: string) => void; setPopulatedContext: (newPopulatedContext: Record) => void; + setFormAsReadOnly: (readOnly: boolean) => void; } /** * Questionnaire state management store which contains all properties and methods to manage the state of the questionnaire. - * - * @param questionnaire - Input FHIR R4 Questionnaire to be rendered - * @param questionnaireResponse - Pre-populated QuestionnaireResponse to be rendered (optional) - * @param additionalVariables - Additional key-value pair of SDC variables for testing (optional) - * @param terminologyServerUrl - Terminology server url to fetch terminology. If not provided, the default terminology server will be used. (optional) - * @param fhirClient - FHIRClient object to perform further FHIR calls. At the moment it's only used in answerExpressions (optional) - * @param readOnly - Applies read-only mode to all items in the form + * This is the vanilla version of the store which can be used in non-React environments. + * @see {QuestionnaireStoreType} for available properties and methods. * * @author Sean Fong */ @@ -386,7 +397,19 @@ export const questionnaireStore = createStore()((set, ge setPopulatedContext: (newPopulatedContext: Record) => set(() => ({ populatedContext: newPopulatedContext + })), + setFormAsReadOnly: (readOnly: boolean) => + set(() => ({ + readOnly: readOnly })) })); +/** + * Questionnaire state management store which contains all properties and methods to manage the state of the questionnaire. + * This is the React version of the store which can be used as React hooks in React functional components. + * @see {QuestionnaireStoreType} for available properties and methods. + * @see {questionnaireStore} for the vanilla store. + * + * @author Sean Fong + */ export const useQuestionnaireStore = createSelectors(questionnaireStore); diff --git a/packages/smart-forms-renderer/src/stores/smartConfigStore.ts b/packages/smart-forms-renderer/src/stores/smartConfigStore.ts index d8a4b7346..18499e69b 100644 --- a/packages/smart-forms-renderer/src/stores/smartConfigStore.ts +++ b/packages/smart-forms-renderer/src/stores/smartConfigStore.ts @@ -20,6 +20,22 @@ import type { Encounter, Patient, Practitioner } from 'fhir/r4'; import type Client from 'fhirclient/lib/Client'; import { createSelectors } from './selector'; +/** + * SmartConfigStore properties and methods + * Properties can be accessed for fine-grain details. + * Methods are usually used internally, using them from an external source is not recommended. + * + * @property client - The FHIRClient object (https://github.com/smart-on-fhir/client-js) + * @property patient - The patient resource in context + * @property user - The user resource in context + * @property encounter - The encounter resource in context + * @method setClient - Set the FHIRClient object when launching via SMART App Launch + * @method setPatient - Set the patient resource in context + * @method setUser - Set the user resource in context + * @method setEncounter - Set the encounter resource in context + * + * @author Sean Fong + */ export interface SmartConfigStoreType { client: Client | null; patient: Patient | null; @@ -31,6 +47,16 @@ export interface SmartConfigStoreType { setEncounter: (encounter: Encounter) => void; } +/** + * Smart Config state management store. This is only used for answerExpressions. + * It is recommended to manage the state of the FHIRClient, patient, user, and encounter in the parent application, since the renderer doesn't provide pre-population capabilities. + * Will be deprecated in version 1.0.0. + * + * This is the vanilla version of the store which can be used in non-React environments. + * @see {SmartConfigStoreType} for available properties and methods. + * + * @author Sean Fong + */ export const smartConfigStore = createStore()((set) => ({ client: null, patient: null, @@ -42,4 +68,15 @@ export const smartConfigStore = createStore()((set) => ({ setEncounter: (encounter: Encounter) => set(() => ({ encounter: encounter })) })); +/** + * Smart Config state management store. This is only used for answerExpressions. + * It is recommended to manage the state of the FHIRClient, patient, user, and encounter in the parent application, since the renderer doesn't provide pre-population capabilities. + * Will be deprecated in version 1.0.0. + * + * This is the React version of the store which can be used as React hooks in React functional components. + * @see {SmartConfigStoreType} for available properties and methods. + * @see {smartConfigStore} for the vanilla store. + * + * @author Sean Fong + */ export const useSmartConfigStore = createSelectors(smartConfigStore); diff --git a/packages/smart-forms-renderer/src/stores/terminologyServerStore.ts b/packages/smart-forms-renderer/src/stores/terminologyServerStore.ts index 588cd137b..c4cd6fba3 100644 --- a/packages/smart-forms-renderer/src/stores/terminologyServerStore.ts +++ b/packages/smart-forms-renderer/src/stores/terminologyServerStore.ts @@ -20,16 +20,43 @@ import { createSelectors } from './selector'; const ONTOSERVER_R4 = 'https://r4.ontoserver.csiro.au/fhir'; -interface TerminologyServerStoreType { +/** + * TerminologyServerStore properties and methods + * Properties can be accessed for fine-grain details. + * Methods are usually used internally, using them from an external source is not recommended. + * + * @property url - The current terminology server URL + * @method setUrl - Set the terminology server URL + * @method resetUrl - Reset the terminology server URL to the default + * + * @author Sean Fong + */ +export interface TerminologyServerStoreType { url: string; setUrl: (newUrl: string) => void; resetUrl: () => void; } +/** + * Terminology server state management store. This is used for resolving valueSets externally. + * Defaults to use https://r4.ontoserver.csiro.au/fhir. + * This is the vanilla version of the store which can be used in non-React environments. + * @see {TerminologyServerStoreType} for available properties and methods. + * + * @author Sean Fong + */ export const terminologyServerStore = createStore()((set) => ({ url: ONTOSERVER_R4, setUrl: (newUrl: string) => set(() => ({ url: newUrl })), resetUrl: () => set(() => ({ url: ONTOSERVER_R4 })) })); +/** + * Terminology server state management store. This is used for resolving valueSets externally. + * Defaults to use https://r4.ontoserver.csiro.au/fhir. + * This is the React version of the store which can be used as React hooks in React functional components. + * @see {TerminologyServerStoreType} for available properties and methods. + * + * @author Sean Fong + */ export const useTerminologyServerStore = createSelectors(terminologyServerStore); diff --git a/packages/smart-forms-renderer/src/stories/BuildFormButtonForStorybook.tsx b/packages/smart-forms-renderer/src/stories/BuildFormButtonForStorybook.tsx index 1255a65b2..bb2e10d6a 100644 --- a/packages/smart-forms-renderer/src/stories/BuildFormButtonForStorybook.tsx +++ b/packages/smart-forms-renderer/src/stories/BuildFormButtonForStorybook.tsx @@ -17,20 +17,27 @@ // @ts-ignore import React from 'react'; +import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; import { Box, IconButton, Tooltip } from '@mui/material'; import Iconify from '../components/Iconify/Iconify'; +import { buildForm } from '../utils'; interface BuildFormButtonProps { - onBuild: () => void; + questionnaire: Questionnaire; + questionnaireResponse: QuestionnaireResponse; } function BuildFormButtonForStorybook(props: BuildFormButtonProps) { - const { onBuild } = props; + const { questionnaire, questionnaireResponse } = props; + + async function handleBuildForm() { + await buildForm(questionnaire, questionnaireResponse); + } return ( - - + + diff --git a/packages/smart-forms-renderer/src/stories/BuildFormButtonTesterWrapper.tsx b/packages/smart-forms-renderer/src/stories/BuildFormButtonTesterWrapper.tsx index 76093abb8..3333a39d6 100644 --- a/packages/smart-forms-renderer/src/stories/BuildFormButtonTesterWrapper.tsx +++ b/packages/smart-forms-renderer/src/stories/BuildFormButtonTesterWrapper.tsx @@ -20,10 +20,8 @@ import React from 'react'; import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; import { BaseRenderer } from '../components'; import { QueryClientProvider } from '@tanstack/react-query'; -import RendererThemeProvider from '../theme/Theme'; -import useQueryClient from '../hooks/useQueryClient'; -import useBuildForm from '../hooks/useBuildForm'; -import { buildForm } from '../utils'; +import { RendererThemeProvider } from '../theme'; +import { useBuildForm, useRendererQueryClient } from '../hooks'; import BuildFormButtonForStorybook from './BuildFormButtonForStorybook'; interface BuildFormButtonTesterWrapperProps { @@ -34,14 +32,10 @@ interface BuildFormButtonTesterWrapperProps { function BuildFormButtonTesterWrapper(props: BuildFormButtonTesterWrapperProps) { const { questionnaire, questionnaireResponse } = props; - const queryClient = useQueryClient(); + const queryClient = useRendererQueryClient(); const isBuilding = useBuildForm(questionnaire); - async function handleBuildForm() { - await buildForm(questionnaire, questionnaireResponse); - } - if (isBuilding) { return
Loading...
; } @@ -50,7 +44,10 @@ function BuildFormButtonTesterWrapper(props: BuildFormButtonTesterWrapperProps)
- +
diff --git a/packages/smart-forms-renderer/src/stories/BuildFormWrapper.tsx b/packages/smart-forms-renderer/src/stories/BuildFormWrapper.tsx index 2d2712d21..257ec1c69 100644 --- a/packages/smart-forms-renderer/src/stories/BuildFormWrapper.tsx +++ b/packages/smart-forms-renderer/src/stories/BuildFormWrapper.tsx @@ -22,7 +22,7 @@ import { BaseRenderer } from '../components'; import { QueryClientProvider } from '@tanstack/react-query'; import RendererThemeProvider from '../theme/Theme'; import { useBuildForm } from '../hooks'; -import useRendererQueryClient from '../hooks/useQueryClient'; +import useRendererQueryClient from '../hooks/useRendererQueryClient'; interface BuildFormWrapperProps { questionnaire: Questionnaire; diff --git a/packages/smart-forms-renderer/src/stories/ValidateFormButtonTesterWrapper.tsx b/packages/smart-forms-renderer/src/stories/FormValidationTesterWrapper.tsx similarity index 73% rename from packages/smart-forms-renderer/src/stories/ValidateFormButtonTesterWrapper.tsx rename to packages/smart-forms-renderer/src/stories/FormValidationTesterWrapper.tsx index 2326da1d7..81ed8f029 100644 --- a/packages/smart-forms-renderer/src/stories/ValidateFormButtonTesterWrapper.tsx +++ b/packages/smart-forms-renderer/src/stories/FormValidationTesterWrapper.tsx @@ -20,26 +20,23 @@ import React from 'react'; import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; import { BaseRenderer } from '../components'; import { QueryClientProvider } from '@tanstack/react-query'; -import RendererThemeProvider from '../theme/Theme'; -import useRendererQueryClient from '../hooks/useQueryClient'; -import useBuildForm from '../hooks/useBuildForm'; -import { useQuestionnaireResponseStore } from '../stores'; +import { RendererThemeProvider } from '../theme'; +import { useBuildForm, useRendererQueryClient } from '../hooks'; import { Grid } from '@mui/material'; +import FormValidationViewerForStorybook from './FormValidationViewerForStorybook'; -interface ValidateFormButtonTesterWrapperProps { +interface FormValidationTesterWrapperProps { questionnaire: Questionnaire; questionnaireResponse?: QuestionnaireResponse; } -function ValidateFormButtonTesterWrapper(props: ValidateFormButtonTesterWrapperProps) { +function FormValidationTesterWrapper(props: FormValidationTesterWrapperProps) { const { questionnaire, questionnaireResponse } = props; const isBuilding = useBuildForm(questionnaire, questionnaireResponse); const queryClient = useRendererQueryClient(); - const invalidItems = useQuestionnaireResponseStore.use.invalidItems(); - if (isBuilding) { return
Loading...
; } @@ -53,7 +50,7 @@ function ValidateFormButtonTesterWrapper(props: ValidateFormButtonTesterWrapperP -
{JSON.stringify(invalidItems, null, 2)}
+
@@ -62,4 +59,4 @@ function ValidateFormButtonTesterWrapper(props: ValidateFormButtonTesterWrapperP ); } -export default ValidateFormButtonTesterWrapper; +export default FormValidationTesterWrapper; diff --git a/packages/smart-forms-renderer/src/stories/FormValidationViewerForStorybook.tsx b/packages/smart-forms-renderer/src/stories/FormValidationViewerForStorybook.tsx new file mode 100644 index 000000000..0a87f4390 --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/FormValidationViewerForStorybook.tsx @@ -0,0 +1,28 @@ +/* + * 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. + */ + +// @ts-ignore +import React from 'react'; +import { useQuestionnaireResponseStore } from '../stores'; + +function FormValidationViewerForStorybook() { + const invalidItems = useQuestionnaireResponseStore.use.invalidItems(); + + return
{JSON.stringify(invalidItems, null, 2)}
; +} + +export default FormValidationViewerForStorybook; diff --git a/packages/smart-forms-renderer/src/stories/InitialiseFormWrapperForStorybook.tsx b/packages/smart-forms-renderer/src/stories/InitialiseFormWrapperForStorybook.tsx new file mode 100644 index 000000000..4c0e35ea6 --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/InitialiseFormWrapperForStorybook.tsx @@ -0,0 +1,100 @@ +/* + * 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. + */ + +// @ts-ignore +import React from 'react'; +import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; +import { BaseRenderer } from '../components'; +import { QueryClientProvider } from '@tanstack/react-query'; +import RendererThemeProvider from '../theme/Theme'; +import useRendererQueryClient from '../hooks/useRendererQueryClient'; +import type Client from 'fhirclient/lib/Client'; +import useInitialiseForm from '../hooks/useInitialiseForm'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; + +interface InitialiseFormWrapperProps { + questionnaire: Questionnaire; + questionnaireResponse?: QuestionnaireResponse; + readOnly?: boolean; + terminologyServerUrl?: string; + additionalVariables?: Record; + fhirClient?: Client; +} + +/** + * This is a one-to-one replacement for the SmartFormsRenderer for demo purposes. + * Instead of using this React component, define your own wrapper component that uses the BaseRenderer directly. + * Things to note: + * - It is required to wrap the BaseRenderer with the QueryClientProvider to make requests. + * - You can wrap the BaseRenderer with the RendererThemeProvider to apply the default renderer theme used in Smart Forms. Optionally, you can define your own ThemeProvider. + * - Make your buildForm() call in a button click or other event handler. Alternatively, you can use the useInitialiseForm hook to initialise the form. + * - Make your own initialiseFhirClient() call in a button click or other event handler. Alternatively, you can use the useInitialiseForm hook to initialise the form. + * - The initialised FHIRClient is only used for further FHIR calls. It does not provide pre-population capabilities. + * + * @author Sean Fong + */ +function InitialiseFormWrapperForStorybook(props: InitialiseFormWrapperProps) { + const { + questionnaire, + questionnaireResponse, + readOnly, + terminologyServerUrl, + additionalVariables, + fhirClient + } = props; + + // The renderer requires a @tanstack/react-query QueryClientProvider to make requests + const queryClient = useRendererQueryClient(); + + /** + * The useInitialiseForm hook provides initialisation logic for the form + * Alternatively (and recommended to do so), you can initialise your form via a button click or other event handler. + * + * // FIXME add github link + * @see BuildFormButtonTesterWrapper for button click usage examples. + */ + const isInitialising = useInitialiseForm( + questionnaire, + questionnaireResponse, + readOnly, + terminologyServerUrl, + additionalVariables, + fhirClient + ); + + // Free feel to customise your loading animation here + if (isInitialising) { + return ( + + + Loading questionnaire... + + ); + } + + return ( + + + + + + ); +} + +export default InitialiseFormWrapperForStorybook; diff --git a/packages/smart-forms-renderer/src/stories/PrePopButtonForStorybook.tsx b/packages/smart-forms-renderer/src/stories/PrePopButtonForStorybook.tsx index e9105aa4e..3ed4c4150 100644 --- a/packages/smart-forms-renderer/src/stories/PrePopButtonForStorybook.tsx +++ b/packages/smart-forms-renderer/src/stories/PrePopButtonForStorybook.tsx @@ -32,13 +32,15 @@ function PrePopButtonForStorybook(props: PrePopButtonProps) { return ( - - {isPopulating ? ( - - ) : ( - - )} - + + + {isPopulating ? ( + + ) : ( + + )} + + {isPopulating ? ( diff --git a/packages/smart-forms-renderer/src/stories/PrePopWrapper.tsx b/packages/smart-forms-renderer/src/stories/PrePopWrapper.tsx index fc1f452cd..001488f64 100644 --- a/packages/smart-forms-renderer/src/stories/PrePopWrapper.tsx +++ b/packages/smart-forms-renderer/src/stories/PrePopWrapper.tsx @@ -20,10 +20,9 @@ import React, { useState } from 'react'; import type { Patient, Practitioner, Questionnaire } from 'fhir/r4'; import { BaseRenderer } from '../components'; import { QueryClientProvider } from '@tanstack/react-query'; -import RendererThemeProvider from '../theme/Theme'; -import useQueryClient from '../hooks/useQueryClient'; +import { RendererThemeProvider } from '../theme'; +import { useBuildForm, useRendererQueryClient } from '../hooks'; import type Client from 'fhirclient/lib/Client'; -import useBuildForm from '../hooks/useBuildForm'; import { buildForm } from '../utils'; import PrePopButtonForStorybook from './PrePopButtonForStorybook'; import { populateQuestionnaire } from '@aehrc/sdc-populate'; @@ -43,7 +42,7 @@ function PrePopWrapper(props: PrePopWrapperProps) { const isBuilding = useBuildForm(questionnaire); - const queryClient = useQueryClient(); + const queryClient = useRendererQueryClient(); function handlePrepopulate() { setIsPopulating(true); diff --git a/packages/smart-forms-renderer/src/stories/testing/ValidateFormTesterWrapper.stories.tsx b/packages/smart-forms-renderer/src/stories/testing/ValidateFormTesterWrapper.stories.tsx index 1134f0d67..dad0f0675 100644 --- a/packages/smart-forms-renderer/src/stories/testing/ValidateFormTesterWrapper.stories.tsx +++ b/packages/smart-forms-renderer/src/stories/testing/ValidateFormTesterWrapper.stories.tsx @@ -16,16 +16,16 @@ */ import type { Meta, StoryObj } from '@storybook/react'; -import ValidateFormButtonTesterWrapper from '../ValidateFormButtonTesterWrapper'; +import ValidationFormTesterWrapper from '../FormValidationTesterWrapper'; import { qValidateFormButtonTester } from '../assets/questionnaires/QValidateFormButtonTester'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export const meta = { title: 'Component/Testing/Validate Form Button Tester', - component: ValidateFormButtonTesterWrapper, + component: ValidationFormTesterWrapper, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs tags: [] -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; diff --git a/packages/smart-forms-renderer/src/theme/Theme.tsx b/packages/smart-forms-renderer/src/theme/Theme.tsx index 997d6965e..29356e9d1 100644 --- a/packages/smart-forms-renderer/src/theme/Theme.tsx +++ b/packages/smart-forms-renderer/src/theme/Theme.tsx @@ -89,6 +89,12 @@ export const themeOptions: ThemeOptions = { } }; +/** + * Default Material UI theme used by the renderer. You can customise your own theme by defining a new ThemeProvider. + * @see {@link https://mui.com/material-ui/customization/theming/} + * + * @author Sean Fong + */ export function RendererThemeProvider({ children }: { children: ReactNode }) { const theme = createTheme(themeOptions); theme.components = componentsOverride(theme); diff --git a/packages/smart-forms-renderer/src/utils/initialise.ts b/packages/smart-forms-renderer/src/utils/initialise.ts index 2c5a39deb..0f3d0cebd 100644 --- a/packages/smart-forms-renderer/src/utils/initialise.ts +++ b/packages/smart-forms-renderer/src/utils/initialise.ts @@ -34,7 +34,7 @@ import { evaluateInitialCalculatedExpressions } from './calculatedExpression'; import { createQuestionnaireResponseItemMap } from './questionnaireResponseStoreUtils/updatableResponseItems'; /** - * Initialise a conformant questionnaireResponse from a given questionnaire + * Initialise a questionnaireResponse from a given questionnaire * optionally takes in an existing questionnaireResponse to be initialised * * @author Sean Fong diff --git a/packages/smart-forms-renderer/src/utils/manageForm.ts b/packages/smart-forms-renderer/src/utils/manageForm.ts index cd4637098..5af8c469b 100644 --- a/packages/smart-forms-renderer/src/utils/manageForm.ts +++ b/packages/smart-forms-renderer/src/utils/manageForm.ts @@ -1,19 +1,34 @@ import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; -import { questionnaireResponseStore, questionnaireStore } from '../stores'; +import { questionnaireResponseStore, questionnaireStore, smartConfigStore } from '../stores'; import { initialiseQuestionnaireResponse } from './initialise'; import { removeEmptyAnswers } from './removeEmptyAnswers'; +import { readEncounter, readPatient, readUser } from '../api/smartClient'; +import type Client from 'fhirclient/lib/Client'; /** * Build the form with an initial Questionnaire and an optional filled QuestionnaireResponse. * If a QuestionnaireResponse is not provided, an empty QuestionnaireResponse is set as the initial QuestionnaireResponse. + * There are other optional properties such as applying readOnly, providing a terminology server url and additional variables. + * + * @param questionnaire - Questionnaire to be rendered + * @param questionnaireResponse - Pre-populated/draft/loaded QuestionnaireResponse to be rendered (optional) + * @param readOnly - Applies read-only mode to all items in the form view + * @param terminologyServerUrl - Terminology server url to fetch terminology. If not provided, the default terminology server will be used. (optional) + * @param additionalVariables - Additional key-value pair of SDC variables for testing (optional) * * @author Sean Fong */ export async function buildForm( questionnaire: Questionnaire, - questionnaireResponse?: QuestionnaireResponse + questionnaireResponse?: QuestionnaireResponse, + readOnly?: boolean, + terminologyServerUrl?: string, + additionalVariables?: Record ): Promise { - await questionnaireStore.getState().buildSourceQuestionnaire(questionnaire); + // QR is set to undefined here to prevent it from being initialised twice. This is defined like that for backward compatibility purposes. + await questionnaireStore + .getState() + .buildSourceQuestionnaire(questionnaire, undefined, additionalVariables, terminologyServerUrl); const initialisedQuestionnaireResponse = initialiseQuestionnaireResponse( questionnaire, @@ -21,6 +36,10 @@ export async function buildForm( ); questionnaireResponseStore.getState().buildSourceResponse(initialisedQuestionnaireResponse); questionnaireStore.getState().updatePopulatedProperties(initialisedQuestionnaireResponse); + + if (readOnly) { + questionnaireStore.getState().setFormAsReadOnly(readOnly); + } } /** @@ -33,6 +52,30 @@ export function destroyForm(): void { questionnaireResponseStore.getState().destroySourceResponse(); } +/** + * Initialise the FHIRClient object to make further FHIR calls in the renderer. + * Note that this does not provide pre-population capabilities. + * + * @param fhirClient - FHIRClient object to perform further FHIR calls. At the moment it's only used in answerExpressions + * + * @author Sean Fong + */ +export async function initialiseFhirClient(fhirClient: Client): Promise { + smartConfigStore.getState().setClient(fhirClient); + const patientPromise = readPatient(fhirClient); + const userPromise = readUser(fhirClient); + const encounterPromise = readEncounter(fhirClient); + + const [patient, user, encounter] = await Promise.all([ + patientPromise, + userPromise, + encounterPromise + ]); + smartConfigStore.getState().setPatient(patient); + smartConfigStore.getState().setUser(user); + smartConfigStore.getState().setEncounter(encounter); +} + /** * Get the filled QuestionnaireResponse at its current state. * If no changes have been made to the form, the initial QuestionnaireResponse is returned. diff --git a/packages/smart-forms-renderer/src/utils/repopulateIntoResponse.ts b/packages/smart-forms-renderer/src/utils/repopulateIntoResponse.ts index 5afdb9924..15c767505 100644 --- a/packages/smart-forms-renderer/src/utils/repopulateIntoResponse.ts +++ b/packages/smart-forms-renderer/src/utils/repopulateIntoResponse.ts @@ -10,7 +10,7 @@ import { isSpecificItemControl } from './itemControl'; import { questionnaireResponseStore, questionnaireStore } from '../stores'; /** - * Re-populate checked items in the re-population dialog into the current QuestionnaireResponse. + * Re-populate checked items in the re-population dialog into the current QuestionnaireResponse * * @author Sean Fong */ diff --git a/packages/smart-forms-renderer/src/utils/repopulateItems.ts b/packages/smart-forms-renderer/src/utils/repopulateItems.ts index 8eb1f8731..5f019abae 100644 --- a/packages/smart-forms-renderer/src/utils/repopulateItems.ts +++ b/packages/smart-forms-renderer/src/utils/repopulateItems.ts @@ -30,6 +30,18 @@ import type { EnableWhenExpressions, EnableWhenItems } from '../interfaces/enabl import { isHiddenByEnableWhen } from './qItem'; import { questionnaireResponseStore, questionnaireStore } from '../stores'; +/** + * ItemToRepopulate interface + * + * @property qItem - The QuestionnaireItem to repopulate + * @property heading - The heading of the group to repopulate + * @property newQRItem - The new QuestionnaireResponseItem to replace the old one + * @property oldQRItem - The old QuestionnaireResponseItem to be replaced + * @property newQRItems - The new QuestionnaireResponseItems to replace the old ones + * @property oldQRItems - The old QuestionnaireResponseItems to be replaced + * + * @author Sean Fong + */ export interface ItemToRepopulate { qItem: QuestionnaireItem | null; heading: string | null; @@ -54,7 +66,7 @@ interface getItemsToRepopulateParams { } /** - * Compare latest data from the server with the current QuestionnaireResponse and decide items to re-populate. + * Compare latest data from the server with the current QuestionnaireResponse and decide items to re-populate * * @author Sean Fong */