From df60faaebc7c2bfeb86e4578f25f3b7eaf9ecfea Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Thu, 31 Oct 2024 15:08:30 +1030 Subject: [PATCH] Add enableWhenAsReadOnly and flyover SDC UI override props in buildForm config --- .../GroupItem/GroupItemView.tsx | 2 +- .../FormComponents/ItemParts/FlyoverItem.tsx | 61 +++++++++++++++++++ .../ItemParts/ItemLabelWrapper.tsx | 51 +++++----------- .../RepeatGroup/RepeatGroupItem.tsx | 2 +- .../FormComponents/SingleItem/SingleItem.tsx | 2 +- .../SingleItem/SingleItemSwitcher.tsx | 13 ++-- .../src/hooks/useBuildForm.ts | 20 +++--- .../src/hooks/useHidden.ts | 12 ++++ .../src/hooks/useReadOnly.ts | 33 +++++++++- packages/smart-forms-renderer/src/index.ts | 3 +- .../src/interfaces/index.ts | 5 +- ...face.ts => overrideComponent.interface.ts} | 8 ++- .../src/stores/questionnaireStore.ts | 22 ++++--- .../src/stores/rendererStylingStore.ts | 4 ++ .../src/utils/manageForm.ts | 12 ++-- 15 files changed, 181 insertions(+), 69 deletions(-) create mode 100644 packages/smart-forms-renderer/src/components/FormComponents/ItemParts/FlyoverItem.tsx rename packages/smart-forms-renderer/src/interfaces/{customComponent.interface.ts => overrideComponent.interface.ts} (83%) diff --git a/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItemView.tsx b/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItemView.tsx index 88ec8b5bb..619e34da3 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItemView.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/GroupItem/GroupItemView.tsx @@ -80,7 +80,7 @@ function GroupItemView(props: GroupItemViewProps) { onQrRepeatGroupChange } = props; - const readOnly = useReadOnly(qItem, parentIsReadOnly); + const readOnly = useReadOnly(qItem, parentIsReadOnly, parentRepeatGroupIndex); // Render collapsible group item // If group item is a repeating instance, do not render group item as collapsible diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/FlyoverItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/FlyoverItem.tsx new file mode 100644 index 000000000..cc42a2d6d --- /dev/null +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/FlyoverItem.tsx @@ -0,0 +1,61 @@ +/* + * 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 React from 'react'; +import Tooltip from '@mui/material/Tooltip'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { useQuestionnaireStore } from '../../../stores'; + +interface FlyoverItemProps { + displayFlyover: string; +} + +function FlyoverItem(props: FlyoverItemProps) { + const { displayFlyover } = props; + + const sdcUiOverrideComponents = useQuestionnaireStore.use.sdcUiOverrideComponents(); + const FlyoverOverrideComponent = sdcUiOverrideComponents['flyover']; + + // If a flyover override component is defined for this item, render it + if (FlyoverOverrideComponent && typeof FlyoverOverrideComponent === 'function') { + return ; + } + + return ( + + + + + + ); +} + +export default FlyoverItem; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelWrapper.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelWrapper.tsx index 72a9a71c0..afc7ecdc2 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelWrapper.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelWrapper.tsx @@ -21,10 +21,9 @@ import ContextDisplayItem from './ContextDisplayItem'; import type { QuestionnaireItem } from 'fhir/r4'; import { getContextDisplays } from '../../../utils/tabs'; import ItemLabelText from './ItemLabelText'; -import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import FlyoverItem from './FlyoverItem'; interface LabelWrapperProps { qItem: QuestionnaireItem; @@ -39,40 +38,20 @@ function ItemLabelWrapper(props: LabelWrapperProps) { return ( - - - - {required ? ( - - * - - ) : null} - - - {displayFlyover !== '' ? ( - - ) : null} - - - - + + {required ? ( + + * + + ) : null} + + + {displayFlyover !== '' ? : null} + + {contextDisplayItems.map((item) => { diff --git a/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroupItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroupItem.tsx index 6aa5afdca..b24aa8fb0 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroupItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroupItem.tsx @@ -55,7 +55,7 @@ function RepeatGroupItem(props: RepeatGroupItemProps) { onQrItemChange } = props; - const readOnly = useReadOnly(qItem, parentIsReadOnly); + const readOnly = useReadOnly(qItem, parentIsReadOnly, repeatGroupIndex); return ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItem.tsx index de9f395a4..819d7a7e4 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItem.tsx @@ -102,7 +102,7 @@ function SingleItem(props: SingleItemProps) { [qItem] ); - const readOnly = useReadOnly(qItem, parentIsReadOnly); + const readOnly = useReadOnly(qItem, parentIsReadOnly, parentRepeatGroupIndex); const itemIsHidden = useHidden(qItem, parentRepeatGroupIndex); return ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx index 314f60ed1..ad081d202 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx @@ -57,14 +57,14 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { const { qItem, qrItem, isRepeated, isTabled, showMinimalView, parentIsReadOnly, onQrItemChange } = props; - const customComponents = useQuestionnaireStore.use.customComponents(); - const CustomComponent = customComponents[qItem.linkId]; + const qItemOverrideComponents = useQuestionnaireStore.use.qItemOverrideComponents(); + const QItemOverrideComponent = qItemOverrideComponents[qItem.linkId]; - // If a custom component is defined for this item, render it + // If a qItem override component is defined for this item, render it // Don't get too strict with the checks for now - if (CustomComponent && typeof CustomComponent === 'function') { + if (QItemOverrideComponent && typeof QItemOverrideComponent === 'function') { return ( - ; + return ; case 'boolean': return ( ); case 'quantity': - // FIXME quantity item uses the same component as decimal item currently return ( ` for testing (optional) * @param rendererStylingOptions - Renderer styling to be applied to the form. See docs for styling options. (optional) - * @param customComponents - FIXME add comment + * @param qItemOverrideComponents - FIXME add comment + * @param sdcUiOverrideComponents - FIXME add comment + * * * @author Sean Fong */ @@ -43,7 +46,8 @@ function useBuildForm( terminologyServerUrl?: string, additionalVariables?: Record, rendererStylingOptions?: RendererStyling, - customComponents?: Record> + qItemOverrideComponents?: Record>, + sdcUiOverrideComponents?: Record> ) { const [isBuilding, setIsBuilding] = useState(true); @@ -61,19 +65,21 @@ function useBuildForm( readOnly, terminologyServerUrl, additionalVariables, - customComponents + qItemOverrideComponents, + sdcUiOverrideComponents ).then(() => { setIsBuilding(false); }); }, [ - customComponents, questionnaire, questionnaireResponse, readOnly, - rendererStylingOptions, - setRendererStyling, terminologyServerUrl, - additionalVariables + additionalVariables, + rendererStylingOptions, + qItemOverrideComponents, + sdcUiOverrideComponents, + setRendererStyling ]); return isBuilding; diff --git a/packages/smart-forms-renderer/src/hooks/useHidden.ts b/packages/smart-forms-renderer/src/hooks/useHidden.ts index 0030987b5..2d27076b0 100644 --- a/packages/smart-forms-renderer/src/hooks/useHidden.ts +++ b/packages/smart-forms-renderer/src/hooks/useHidden.ts @@ -19,6 +19,7 @@ import type { QuestionnaireItem } from 'fhir/r4'; import { useQuestionnaireStore } from '../stores'; import { isHiddenByEnableWhen } from '../utils/qItem'; import { structuredDataCapture } from 'fhir-sdc-helpers'; +import { useRendererStylingStore } from '../stores/rendererStylingStore'; /** * React hook to determine if a QuestionnaireItem is hidden via item.hidden, enableWhens, enableWhenExpressions. @@ -31,10 +32,21 @@ function useHidden(qItem: QuestionnaireItem, parentRepeatGroupIndex?: number): b const enableWhenItems = useQuestionnaireStore.use.enableWhenItems(); const enableWhenExpressions = useQuestionnaireStore.use.enableWhenExpressions(); + const enableWhenAsReadOnly = useRendererStylingStore.use.enableWhenAsReadOnly(); + if (structuredDataCapture.getHidden(qItem)) { return true; } + // If enableWhenAsReadOnly is true, then items hidden by enableWhen should be displayed, but set as readOnly + // If enableWhenAsReadOnly is 'non-group', then items hidden by enableWhen should be displayed, but set as readOnly - only applies if item.type != group + if ( + enableWhenAsReadOnly === true || + (enableWhenAsReadOnly === 'non-group' && qItem.type !== 'group') + ) { + return false; + } + return isHiddenByEnableWhen({ linkId: qItem.linkId, enableWhenIsActivated, diff --git a/packages/smart-forms-renderer/src/hooks/useReadOnly.ts b/packages/smart-forms-renderer/src/hooks/useReadOnly.ts index e623d8269..5e206fd74 100644 --- a/packages/smart-forms-renderer/src/hooks/useReadOnly.ts +++ b/packages/smart-forms-renderer/src/hooks/useReadOnly.ts @@ -17,9 +17,40 @@ import type { QuestionnaireItem } from 'fhir/r4'; import useRenderingExtensions from './useRenderingExtensions'; +import { useRendererStylingStore } from '../stores/rendererStylingStore'; +import { isHiddenByEnableWhen } from '../utils/qItem'; +import { useQuestionnaireStore } from '../stores'; -function useReadOnly(qItem: QuestionnaireItem, parentIsReadOnly: boolean | undefined): boolean { +function useReadOnly( + qItem: QuestionnaireItem, + parentIsReadOnly: boolean | undefined, + parentRepeatGroupIndex?: number +): boolean { let { readOnly } = useRenderingExtensions(qItem); + + const enableWhenIsActivated = useQuestionnaireStore.use.enableWhenIsActivated(); + const enableWhenItems = useQuestionnaireStore.use.enableWhenItems(); + const enableWhenExpressions = useQuestionnaireStore.use.enableWhenExpressions(); + + const enableWhenAsReadOnly = useRendererStylingStore.use.enableWhenAsReadOnly(); + + // If enableWhenAsReadOnly is true, then items hidden by enableWhen should be displayed, but set as readOnly + // If enableWhenAsReadOnly is 'non-group', then items hidden by enableWhen should be displayed, but set as readOnly - only applies if item.type != group + if (!readOnly) { + if ( + enableWhenAsReadOnly === true || + (enableWhenAsReadOnly === 'non-group' && qItem.type !== 'group') + ) { + readOnly = isHiddenByEnableWhen({ + linkId: qItem.linkId, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions, + parentRepeatGroupIndex + }); + } + } + if (typeof parentIsReadOnly === 'boolean' && parentIsReadOnly) { readOnly = parentIsReadOnly; } diff --git a/packages/smart-forms-renderer/src/index.ts b/packages/smart-forms-renderer/src/index.ts index 9a46273f0..2caf276af 100644 --- a/packages/smart-forms-renderer/src/index.ts +++ b/packages/smart-forms-renderer/src/index.ts @@ -5,7 +5,8 @@ export type { Variables, VariableXFhirQuery, LaunchContext, - CustomComponentProps + QItemOverrideComponentProps, + SdcUiOverrideComponentProps } from './interfaces'; // component exports diff --git a/packages/smart-forms-renderer/src/interfaces/index.ts b/packages/smart-forms-renderer/src/interfaces/index.ts index 1c1bf7e60..ed7b72189 100644 --- a/packages/smart-forms-renderer/src/interfaces/index.ts +++ b/packages/smart-forms-renderer/src/interfaces/index.ts @@ -19,4 +19,7 @@ export type { Tab, Tabs } from './tab.interface'; export type { Variables, VariableXFhirQuery } from './variables.interface'; export type { LaunchContext } from './populate.interface'; export type { EnableWhenItems, EnableWhenExpressions } from './enableWhen.interface'; -export type { CustomComponentProps } from './customComponent.interface'; +export type { + QItemOverrideComponentProps, + SdcUiOverrideComponentProps +} from './overrideComponent.interface'; diff --git a/packages/smart-forms-renderer/src/interfaces/customComponent.interface.ts b/packages/smart-forms-renderer/src/interfaces/overrideComponent.interface.ts similarity index 83% rename from packages/smart-forms-renderer/src/interfaces/customComponent.interface.ts rename to packages/smart-forms-renderer/src/interfaces/overrideComponent.interface.ts index e0d29eb06..0846ba63a 100644 --- a/packages/smart-forms-renderer/src/interfaces/customComponent.interface.ts +++ b/packages/smart-forms-renderer/src/interfaces/overrideComponent.interface.ts @@ -15,9 +15,9 @@ * limitations under the License. */ -import { QuestionnaireItem, type QuestionnaireResponseItem } from 'fhir/r4'; +import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; -export interface CustomComponentProps { +export interface QItemOverrideComponentProps { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; isRepeated: boolean; @@ -25,3 +25,7 @@ export interface CustomComponentProps { parentIsReadOnly?: boolean; onQrItemChange: (qrItem: QuestionnaireResponseItem) => unknown; } + +export interface SdcUiOverrideComponentProps { + displayText: string; +} diff --git a/packages/smart-forms-renderer/src/stores/questionnaireStore.ts b/packages/smart-forms-renderer/src/stores/questionnaireStore.ts index 93f00e5a9..a6fb11ea1 100644 --- a/packages/smart-forms-renderer/src/stores/questionnaireStore.ts +++ b/packages/smart-forms-renderer/src/stores/questionnaireStore.ts @@ -48,7 +48,7 @@ import { questionnaireResponseStore } from './questionnaireResponseStore'; import { createQuestionnaireResponseItemMap } from '../utils/questionnaireResponseStoreUtils/updatableResponseItems'; import { insertCompleteAnswerOptionsIntoQuestionnaire } from '../utils/questionnaireStoreUtils/insertAnswerOptions'; import type { InitialExpression } from '../interfaces/initialExpression.interface'; -import type { CustomComponentProps } from '../interfaces'; +import type { QItemOverrideComponentProps, SdcUiOverrideComponentProps } from '../interfaces'; import type { ComponentType } from 'react'; /** @@ -76,6 +76,8 @@ import type { ComponentType } from 'react'; * @property cachedValueSetCodings - Key-value pair of cached value set codings `Record` * @property fhirPathContext - Key-value pair of evaluated FHIRPath values `Record` * @property populatedContext - Key-value pair of one-off pre-populated FHIRPath values `Record` + * @property qItemOverrideComponents - Key-value pair of React component overrides for Questionnaire Items via linkId `Record` + * @property sdcUiOverrideComponents - Key-value pair of React component overrides for SDC UI Controls https://hl7.org/fhir/extensions/ValueSet-questionnaire-item-control.html `Record` * @property focusedLinkId - LinkId of the currently focused item * @property readOnly - Flag to set the form to read-only mode * @property buildSourceQuestionnaire - Used to build the source questionnaire with the provided questionnaire and optionally questionnaire response, additional variables, terminology server url and readyOnly flag @@ -118,7 +120,8 @@ export interface QuestionnaireStoreType { cachedValueSetCodings: Record; fhirPathContext: Record; populatedContext: Record; - customComponents: Record>; + qItemOverrideComponents: Record>; + sdcUiOverrideComponents: Record>; focusedLinkId: string; readOnly: boolean; buildSourceQuestionnaire: ( @@ -127,7 +130,8 @@ export interface QuestionnaireStoreType { additionalVariables?: Record, terminologyServerUrl?: string, readOnly?: boolean, - customComponents?: Record> + qItemOverrideComponents?: Record>, + sdcUiOverrideComponents?: Record> ) => Promise; destroySourceQuestionnaire: () => void; switchTab: (newTabIndex: number) => void; @@ -186,7 +190,8 @@ export const questionnaireStore = createStore()((set, ge cachedValueSetCodings: {}, fhirPathContext: {}, populatedContext: {}, - customComponents: {}, + qItemOverrideComponents: {}, + sdcUiOverrideComponents: {}, focusedLinkId: '', readOnly: false, buildSourceQuestionnaire: async ( @@ -195,7 +200,8 @@ export const questionnaireStore = createStore()((set, ge additionalVariables = {}, terminologyServerUrl = terminologyServerStore.getState().url, readOnly = false, - customComponents = {} + qItemOverrideComponents = {}, + sdcUiOverrideComponents = {} ) => { const questionnaireModel = await createQuestionnaireModel( questionnaire, @@ -248,7 +254,8 @@ export const questionnaireStore = createStore()((set, ge processedValueSetCodings: questionnaireModel.processedValueSetCodings, processedValueSetUrls: questionnaireModel.processedValueSetUrls, fhirPathContext: updatedFhirPathContext, - customComponents: customComponents, + qItemOverrideComponents: qItemOverrideComponents, + sdcUiOverrideComponents: sdcUiOverrideComponents, readOnly: readOnly }); }, @@ -272,7 +279,8 @@ export const questionnaireStore = createStore()((set, ge processedValueSetCodings: {}, processedValueSetUrls: {}, fhirPathContext: {}, - customComponents: {} + qItemOverrideComponents: {}, + sdcUiOverrideComponents: {} }), switchTab: (newTabIndex: number) => set(() => ({ currentTabIndex: newTabIndex })), switchPage: (newPageIndex: number) => set(() => ({ currentPageIndex: newPageIndex })), diff --git a/packages/smart-forms-renderer/src/stores/rendererStylingStore.ts b/packages/smart-forms-renderer/src/stores/rendererStylingStore.ts index 651586305..45fc7433e 100644 --- a/packages/smart-forms-renderer/src/stores/rendererStylingStore.ts +++ b/packages/smart-forms-renderer/src/stores/rendererStylingStore.ts @@ -30,6 +30,7 @@ export interface RendererStyling { | '800' | '900' | 'default'; + enableWhenAsReadOnly?: boolean | 'non-group'; // fix the non group enablewhen disablePageCardView?: boolean; disablePageButtons?: boolean; } @@ -51,6 +52,7 @@ export interface RendererStylingStoreType { | '800' | '900' | 'default'; + enableWhenAsReadOnly: boolean | 'non-group'; disablePageCardView: boolean; disablePageButtons: boolean; setRendererStyling: (params: RendererStyling) => void; @@ -61,11 +63,13 @@ export interface RendererStylingStoreType { */ export const rendererStylingStore = createStore()((set) => ({ itemLabelFontWeight: 'default', + enableWhenAsReadOnly: false, disablePageCardView: false, disablePageButtons: false, setRendererStyling: (params: RendererStyling) => { set(() => ({ itemLabelFontWeight: params.itemLabelFontWeight ?? 'default', + enableWhenAsReadOnly: params.enableWhenAsReadOnly ?? false, disablePageCardView: params.disablePageCardView ?? false, disablePageButtons: params.disablePageButtons ?? false })); diff --git a/packages/smart-forms-renderer/src/utils/manageForm.ts b/packages/smart-forms-renderer/src/utils/manageForm.ts index af625de28..6af8ed9bc 100644 --- a/packages/smart-forms-renderer/src/utils/manageForm.ts +++ b/packages/smart-forms-renderer/src/utils/manageForm.ts @@ -11,7 +11,8 @@ import { readEncounter, readPatient, readUser } from '../api/smartClient'; import type Client from 'fhirclient/lib/Client'; import { updateQuestionnaireResponse } from './genericRecursive'; import { removeInternalRepeatIdsRecursive } from './removeRepeatId'; -import { ComponentType } from 'react'; +import type { ComponentType } from 'react'; +import type { QItemOverrideComponentProps, SdcUiOverrideComponentProps } from '../interfaces'; /** * Build the form with an initial Questionnaire and an optional filled QuestionnaireResponse. @@ -23,7 +24,8 @@ import { ComponentType } from 'react'; * @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 `Record` for testing (optional) - * @param customComponents - FIXME add comment + * @param qItemOverrideComponents - FIXME add comment + * @param sdcUiOverrideComponents - FIXME add comment * * @author Sean Fong */ @@ -33,7 +35,8 @@ export async function buildForm( readOnly?: boolean, terminologyServerUrl?: string, additionalVariables?: Record, - customComponents?: Record> + qItemOverrideComponents?: Record>, + sdcUiOverrideComponents?: Record> ): Promise { // Reset terminology server if (terminologyServerUrl) { @@ -51,7 +54,8 @@ export async function buildForm( additionalVariables, terminologyServerUrl, undefined, - customComponents + qItemOverrideComponents, + sdcUiOverrideComponents ); const initialisedQuestionnaireResponse = initialiseQuestionnaireResponse(