From 1069f4d17005e47982e72631503b03811c6b5541 Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 19 Feb 2024 13:06:05 +1030 Subject: [PATCH 01/10] Add required and readonly labels to Qitems --- .../QuestionnaireLabel.styles.ts | 1 - .../TableComponents/ResponseLabel.styles.ts | 1 - .../src/components/Box.styles.ts | 32 ++++++++++++- .../BooleanItem/BooleanItem.tsx | 8 +++- .../ChoiceItems/ChoiceAutocompleteItem.tsx | 42 ++++++++--------- .../ChoiceCheckboxAnswerOptionItem.tsx | 33 ++++++------- .../ChoiceCheckboxAnswerValueSetItem.tsx | 35 +++++++------- .../ChoiceRadioAnswerOptionItem.tsx | 34 ++++++-------- .../ChoiceRadioAnswerValueSetItem.tsx | 38 +++++++-------- .../ChoiceSelectAnswerOptionItem.tsx | 34 ++++++-------- .../ChoiceSelectAnswerValueSetItem.tsx | 38 +++++++-------- .../CustomDateItem/CustomDateItem.tsx | 9 +++- .../FormComponents/DateItem/DateItem.tsx | 9 +++- .../DateTimeItem/DateTimeItem.tsx | 9 +++- .../ItemParts/ItemExtensionLabels.tsx | 42 +++++++++++++++++ .../ItemParts/ItemFieldGrid.tsx | 23 ++++++---- .../OpenChoiceAutocompleteItem.tsx | 46 +++++++++---------- .../OpenChoiceCheckboxAnswerOptionItem.tsx | 43 ++++++++--------- .../OpenChoiceRadioAnswerOptionItem.tsx | 40 ++++++++-------- .../OpenChoiceSelectAnswerOptionItem.tsx | 36 +++++++-------- .../OpenChoiceSelectAnswerValueSetItem.tsx | 38 +++++++-------- .../FormComponents/RepeatItem/RepeatItem.tsx | 12 +++-- .../FormComponents/SliderItem/SliderItem.tsx | 8 +++- .../FormComponents/TimeItem/TimeItem.tsx | 9 +++- 24 files changed, 341 insertions(+), 279 deletions(-) create mode 100644 packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemExtensionLabels.tsx diff --git a/apps/smart-forms-app/src/features/dashboard/components/DashboardPages/QuestionnairePage/TableComponents/QuestionnaireLabel.styles.ts b/apps/smart-forms-app/src/features/dashboard/components/DashboardPages/QuestionnairePage/TableComponents/QuestionnaireLabel.styles.ts index 5ba4474c0..6a4ce3c59 100644 --- a/apps/smart-forms-app/src/features/dashboard/components/DashboardPages/QuestionnairePage/TableComponents/QuestionnaireLabel.styles.ts +++ b/apps/smart-forms-app/src/features/dashboard/components/DashboardPages/QuestionnairePage/TableComponents/QuestionnaireLabel.styles.ts @@ -53,7 +53,6 @@ export const QuestionnaireStyledLabel = styled(Box, { minWidth: 22, lineHeight: 0, borderRadius: 6, - cursor: 'default', alignItems: 'center', whiteSpace: 'nowrap', display: 'inline-flex', diff --git a/apps/smart-forms-app/src/features/dashboard/components/DashboardPages/ResponsesPage/TableComponents/ResponseLabel.styles.ts b/apps/smart-forms-app/src/features/dashboard/components/DashboardPages/ResponsesPage/TableComponents/ResponseLabel.styles.ts index d15d6bca7..05c83fa19 100644 --- a/apps/smart-forms-app/src/features/dashboard/components/DashboardPages/ResponsesPage/TableComponents/ResponseLabel.styles.ts +++ b/apps/smart-forms-app/src/features/dashboard/components/DashboardPages/ResponsesPage/TableComponents/ResponseLabel.styles.ts @@ -58,7 +58,6 @@ export const ResponseStyledLabel = styled(Box, { minWidth: 22, lineHeight: 0, borderRadius: 6, - cursor: 'default', alignItems: 'center', whiteSpace: 'nowrap', display: 'inline-flex', diff --git a/packages/smart-forms-renderer/src/components/Box.styles.ts b/packages/smart-forms-renderer/src/components/Box.styles.ts index 4db50057c..f1e658250 100644 --- a/packages/smart-forms-renderer/src/components/Box.styles.ts +++ b/packages/smart-forms-renderer/src/components/Box.styles.ts @@ -16,7 +16,7 @@ */ import Box from '@mui/material/Box'; -import { styled } from '@mui/material/styles'; +import { alpha, styled } from '@mui/material/styles'; import FormControlLabel from '@mui/material/FormControlLabel'; export const QGroupContainerBox = styled(Box, { @@ -37,3 +37,33 @@ export const FormTitleWrapper = styled(Box)(() => ({ export const StyledFormControlLabel = styled(FormControlLabel)(() => ({ height: 34 })); + +export const RequiredLabel = styled(Box)(({ theme }) => ({ + height: 12, + minWidth: 20, + lineHeight: 0, + borderRadius: 6, + alignItems: 'center', + whiteSpace: 'nowrap', + justifyContent: 'center', + padding: theme.spacing(1), + fontSize: theme.typography.caption.fontSize, + color: theme.palette.error.dark, + backgroundColor: alpha(theme.palette.error.main, 0.16), + fontWeight: theme.typography.fontWeightBold +})); + +export const ReadOnlyLabel = styled(Box)(({ theme }) => ({ + height: 12, + minWidth: 20, + lineHeight: 0, + borderRadius: 6, + alignItems: 'center', + whiteSpace: 'nowrap', + justifyContent: 'center', + padding: theme.spacing(1), + fontSize: theme.typography.caption.fontSize, + color: theme.palette.text.primary, + backgroundColor: theme.palette.grey['300'], + fontWeight: theme.typography.fontWeightBold +})); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx index f6d930c16..16a62b94e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx @@ -44,7 +44,7 @@ function BooleanItem(props: BooleanItemProps) { const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Init input value let checked = false; @@ -75,7 +75,11 @@ function BooleanItem(props: BooleanItemProps) { } return ( - + diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx index 064cfd59c..207211005 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx @@ -16,7 +16,6 @@ */ import React, { useState } from 'react'; -import Grid from '@mui/material/Grid'; import type { Coding, QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { createEmptyQrItem } from '../../../utils/qrItem'; @@ -31,10 +30,9 @@ import type { PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; import { AUTOCOMPLETE_DEBOUNCE_DURATION } from '../../../utils/debounce'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import useReadOnly from '../../../hooks/useReadOnly'; import ChoiceAutocompleteField from './ChoiceAutocompleteField'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface ChoiceAutocompleteItemProps extends PropsWithQrItemChangeHandler, @@ -56,7 +54,7 @@ function ChoiceAutocompleteItem(props: ChoiceAutocompleteItemProps) { } const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); const maxList = 10; @@ -106,25 +104,23 @@ function ChoiceAutocompleteItem(props: ChoiceAutocompleteItemProps) { return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx index 696de6897..50a989ac4 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx @@ -16,7 +16,6 @@ */ import React from 'react'; -import Grid from '@mui/material/Grid'; import type { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { createEmptyQrItem } from '../../../utils/qrItem'; @@ -30,9 +29,9 @@ import type { PropsWithShowMinimalViewAttribute } from '../../../interfaces/renderProps.interface'; import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import ChoiceCheckboxAnswerValueSetFields from './ChoiceCheckboxAnswerOptionFields'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface ChoiceCheckboxAnswerOptionItemProps extends PropsWithQrItemChangeHandler, @@ -60,7 +59,7 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro const answers = qrChoiceCheckbox.answer ? qrChoiceCheckbox.answer : []; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Event handlers function handleCheckedChange(changedValue: string) { @@ -97,21 +96,19 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx index 5f4023dbe..fd2ddd9ff 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx @@ -16,7 +16,6 @@ */ import React from 'react'; -import Grid from '@mui/material/Grid'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { createEmptyQrItem } from '../../../utils/qrItem'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; @@ -31,9 +30,9 @@ import type { PropsWithShowMinimalViewAttribute } from '../../../interfaces/renderProps.interface'; import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import ChoiceCheckboxAnswerValueSetFields from './ChoiceCheckboxAnswerValueSetFields'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface ChoiceCheckboxAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, @@ -62,7 +61,7 @@ function ChoiceCheckboxAnswerValueSetItem(props: ChoiceCheckboxAnswerValueSetIte const answers = qrChoiceCheckbox.answer ? qrChoiceCheckbox.answer : []; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Get codings/options from valueSet const { codings, serverError } = useValueSetCodings(qItem); @@ -102,22 +101,20 @@ function ChoiceCheckboxAnswerValueSetItem(props: ChoiceCheckboxAnswerValueSetIte return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx index 060b02153..1b4d7c8ba 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx @@ -16,7 +16,6 @@ */ import React from 'react'; -import Grid from '@mui/material/Grid'; import type { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { findInAnswerOptions, getQrChoiceValue } from '../../../utils/choice'; @@ -28,10 +27,9 @@ import type { PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import ChoiceRadioAnswerOptionFields from './ChoiceRadioAnswerOptionFields'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface ChoiceRadioAnswerOptionItemProps extends PropsWithQrItemChangeHandler, @@ -50,7 +48,7 @@ function ChoiceRadioAnswerOptionItem(props: ChoiceRadioAnswerOptionItemProps) { const valueRadio = getQrChoiceValue(qrChoiceRadio); const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Event handlers function handleChange(newValue: string) { @@ -76,21 +74,19 @@ function ChoiceRadioAnswerOptionItem(props: ChoiceRadioAnswerOptionItemProps) { return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx index b15b8dd98..b008108be 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx @@ -16,7 +16,6 @@ */ import React from 'react'; -import Grid from '@mui/material/Grid'; import type { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { findInAnswerValueSetCodings } from '../../../utils/choice'; @@ -29,10 +28,9 @@ import type { PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import ChoiceRadioAnswerValueSetFields from './ChoiceRadioAnswerValueSetFields'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface ChoiceRadioAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, @@ -47,7 +45,7 @@ function ChoiceRadioAnswerValueSetItem(props: ChoiceRadioAnswerValueSetItemProps const { qItem, qrItem, orientation, isRepeated, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Init input value const qrChoiceRadio = qrItem ?? createEmptyQrItem(qItem); @@ -88,23 +86,21 @@ function ChoiceRadioAnswerValueSetItem(props: ChoiceRadioAnswerValueSetItemProps return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx index 3b6e0b3a8..f1d358aa9 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx @@ -16,7 +16,6 @@ */ import React from 'react'; -import Grid from '@mui/material/Grid'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { findInAnswerOptions, getQrChoiceValue } from '../../../utils/choice'; @@ -29,10 +28,9 @@ import type { PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import ChoiceSelectAnswerOptionFields from './ChoiceSelectAnswerOptionFields'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface ChoiceSelectAnswerOptionItemProps extends PropsWithQrItemChangeHandler, @@ -47,7 +45,7 @@ function ChoiceSelectAnswerOptionItem(props: ChoiceSelectAnswerOptionItemProps) const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Init input value const qrChoiceSelect = qrItem ?? createEmptyQrItem(qItem); @@ -82,21 +80,19 @@ function ChoiceSelectAnswerOptionItem(props: ChoiceSelectAnswerOptionItemProps) return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx index bf1cd92ad..f5b96da70 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx @@ -16,7 +16,6 @@ */ import React, { useEffect, useMemo } from 'react'; -import Grid from '@mui/material/Grid'; import type { Coding, QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { createEmptyQrItem } from '../../../utils/qrItem'; @@ -29,10 +28,9 @@ import type { PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import ChoiceSelectAnswerValueSetFields from './ChoiceSelectAnswerValueSetFields'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface ChoiceSelectAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, @@ -47,7 +45,7 @@ function ChoiceSelectAnswerValueSetItem(props: ChoiceSelectAnswerValueSetItemPro const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Init input value const qrChoiceSelect = qrItem ?? createEmptyQrItem(qItem); @@ -108,23 +106,21 @@ function ChoiceSelectAnswerValueSetItem(props: ChoiceSelectAnswerValueSetItemPro return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/CustomDateItem/CustomDateItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/CustomDateItem/CustomDateItem.tsx index 16a6aa468..59a643267 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/CustomDateItem/CustomDateItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/CustomDateItem/CustomDateItem.tsx @@ -49,7 +49,8 @@ function CustomDateItem(props: CustomDateItemProps) { const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayPrompt, displayInstructions, entryFormat } = useRenderingExtensions(qItem); + const { displayPrompt, displayInstructions, entryFormat, required } = + useRenderingExtensions(qItem); const qrDate = qrItem ?? createEmptyQrItem(qItem); @@ -117,7 +118,11 @@ function CustomDateItem(props: CustomDateItemProps) { return ( - + - + - + + {required ? Required : null} + {readOnly ? Read-only : null} + + ); +} + +export default ItemExtensionLabels; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx index a822327b2..3c6429384 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx @@ -21,27 +21,32 @@ import Grid from '@mui/material/Grid'; import type { QuestionnaireItem } from 'fhir/r4'; import DisplayInstructions from '../DisplayItem/DisplayInstructions'; import LabelWrapper from './ItemLabelWrapper'; +import ItemExtensionLabels from './ItemExtensionLabels'; interface ItemFieldGridProps { children: ReactNode; qItem: QuestionnaireItem; displayInstructions: string | ReactElement; + required: boolean; readOnly: boolean; } function ItemFieldGrid(props: ItemFieldGridProps) { - const { children, qItem, displayInstructions, readOnly } = props; + const { children, qItem, displayInstructions, required, readOnly } = props; return ( - - - + <> + + + + + + + {children} + + - - {children} - - - + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx index a0b0cdd5a..b7aeb937d 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx @@ -16,7 +16,6 @@ */ import React, { useState } from 'react'; -import Grid from '@mui/material/Grid'; import type { Coding, QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; @@ -32,10 +31,9 @@ import type { PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; import { AUTOCOMPLETE_DEBOUNCE_DURATION } from '../../../utils/debounce'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import OpenChoiceAutocompleteField from './OpenChoiceAutocompleteField'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface OpenChoiceAutocompleteItemProps extends PropsWithQrItemChangeHandler, @@ -50,7 +48,7 @@ function OpenChoiceAutocompleteItem(props: OpenChoiceAutocompleteItemProps) { const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); const qrOpenChoice = qrItem ?? createEmptyQrItem(qItem); @@ -135,27 +133,25 @@ function OpenChoiceAutocompleteItem(props: OpenChoiceAutocompleteItemProps) { return ( - - - - - - setInput(newValue)} - onValueChange={handleValueChange} - onUnfocus={handleUnfocus} - /> - - - + + setInput(newValue)} + onValueChange={handleValueChange} + onUnfocus={handleUnfocus} + /> + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx index 8b5b37fde..944941837 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx @@ -16,7 +16,6 @@ */ import React, { useCallback, useMemo, useState } from 'react'; -import Grid from '@mui/material/Grid'; import type { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import { CheckBoxOption } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; @@ -37,9 +36,9 @@ import type { } from '../../../interfaces/renderProps.interface'; import { DEBOUNCE_DURATION } from '../../../utils/debounce'; import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import OpenChoiceCheckboxAnswerOptionFields from './OpenChoiceCheckboxAnswerOptionFields'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface OpenChoiceCheckboxAnswerOptionItemProps extends PropsWithQrItemChangeHandler, @@ -64,7 +63,7 @@ function OpenChoiceCheckboxAnswerOptionItem(props: OpenChoiceCheckboxAnswerOptio const readOnly = useReadOnly(qItem, parentIsReadOnly); const openLabelText = getOpenLabelText(qItem); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Init answers const qrOpenChoiceCheckbox = qrItem ?? createEmptyQrItem(qItem); @@ -159,26 +158,24 @@ function OpenChoiceCheckboxAnswerOptionItem(props: OpenChoiceCheckboxAnswerOptio return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx index ca7f1ac6c..1b8f6b2fa 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx @@ -16,7 +16,6 @@ */ import React, { useState } from 'react'; -import Grid from '@mui/material/Grid'; import type { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { createEmptyQrItem } from '../../../utils/qrItem'; @@ -30,10 +29,9 @@ import type { PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import OpenChoiceRadioAnswerOptionFields from './OpenChoiceRadioAnswerOptionFields'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface OpenChoiceRadioAnswerOptionItemProps extends PropsWithQrItemChangeHandler, @@ -49,7 +47,7 @@ function OpenChoiceRadioAnswerOptionItem(props: OpenChoiceRadioAnswerOptionItemP const readOnly = useReadOnly(qItem, parentIsReadOnly); const openLabelText = getOpenLabelText(qItem); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Init answers const qrOpenChoiceRadio = qrItem ?? createEmptyQrItem(qItem); @@ -121,24 +119,22 @@ function OpenChoiceRadioAnswerOptionItem(props: OpenChoiceRadioAnswerOptionItemP return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx index b1b7fc05d..0b10da620 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx @@ -16,7 +16,6 @@ */ import React from 'react'; -import Grid from '@mui/material/Grid'; import type { QuestionnaireItem, QuestionnaireItemAnswerOption, @@ -31,10 +30,9 @@ import type { PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import OpenChoiceSelectAnswerOptionField from './OpenChoiceSelectAnswerOptionField'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface OpenChoiceSelectAnswerOptionItemProps extends PropsWithQrItemChangeHandler, @@ -49,7 +47,7 @@ function OpenChoiceSelectAnswerOptionItem(props: OpenChoiceSelectAnswerOptionIte const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Init input value const answerOptions = qItem.answerOption; @@ -109,22 +107,20 @@ function OpenChoiceSelectAnswerOptionItem(props: OpenChoiceSelectAnswerOptionIte return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx index e6924881b..4ff9aef88 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx @@ -16,7 +16,6 @@ */ import React from 'react'; -import Grid from '@mui/material/Grid'; import type { Coding, QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { createEmptyQrItem } from '../../../utils/qrItem'; import { FullWidthFormComponentBox } from '../../Box.styles'; @@ -28,10 +27,9 @@ import type { PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler } from '../../../interfaces/renderProps.interface'; -import DisplayInstructions from '../DisplayItem/DisplayInstructions'; -import LabelWrapper from '../ItemParts/ItemLabelWrapper'; import OpenChoiceSelectAnswerValueSetField from './OpenChoiceSelectAnswerValueSetField'; import useReadOnly from '../../../hooks/useReadOnly'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; interface OpenChoiceSelectAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, @@ -46,7 +44,7 @@ function OpenChoiceSelectAnswerValueSetItem(props: OpenChoiceSelectAnswerValueSe const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); // Init input value const qrOpenChoice = qrItem ?? createEmptyQrItem(qItem); @@ -93,23 +91,21 @@ function OpenChoiceSelectAnswerValueSetItem(props: OpenChoiceSelectAnswerValueSe return ( - - - - - - - - - + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/RepeatItem/RepeatItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/RepeatItem/RepeatItem.tsx index 924281107..2768cbc60 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/RepeatItem/RepeatItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/RepeatItem/RepeatItem.tsx @@ -18,9 +18,9 @@ import React, { useState } from 'react'; import type { PropsWithParentIsReadOnlyAttribute, - PropsWithQrItemChangeHandler + PropsWithQrItemChangeHandler, + PropsWithShowMinimalViewAttribute } from '../../../interfaces/renderProps.interface'; -import type { PropsWithShowMinimalViewAttribute } from '../../../interfaces/renderProps.interface'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { nanoid } from 'nanoid'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; @@ -46,7 +46,7 @@ function RepeatItem(props: RepeatItemProps) { const { qItem, qrItem, showMinimalView, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); const initialRepeatAnswers = useInitialiseRepeatAnswers(qItem, qrItem); @@ -119,7 +119,11 @@ function RepeatItem(props: RepeatItemProps) { return ( - + {repeatAnswers.map(({ nanoId, answer }, index) => { const repeatAnswerQrItem = createEmptyQrItem(qItem); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx index f4e5663ae..5714e7779 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx @@ -45,7 +45,7 @@ function SliderItem(props: SliderItemProps) { const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); - const { displayInstructions } = useRenderingExtensions(qItem); + const { displayInstructions, required } = useRenderingExtensions(qItem); const { minValue, maxValue, stepValue, minLabel, maxLabel } = useSliderExtensions(qItem); const isInteracted = !!qrItem?.answer; @@ -91,7 +91,11 @@ function SliderItem(props: SliderItemProps) { return ( - + - + Date: Mon, 19 Feb 2024 13:08:42 +1030 Subject: [PATCH 02/10] Add minLength check --- .../IntegerItem/IntegerField.tsx | 1 + .../src/utils/itemControl.ts | 72 ++++++------------- .../smart-forms-renderer/src/utils/mapItem.ts | 2 + 3 files changed, 26 insertions(+), 49 deletions(-) diff --git a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerField.tsx index b2759f00a..b37e950d2 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerField.tsx @@ -52,6 +52,7 @@ function IntegerField(props: IntegerFieldProps) { id={linkId} value={value.toString()} error={!!feedback} + helperText={feedback} onChange={(event) => onInputChange(event.target.value)} disabled={readOnly} label={displayPrompt} diff --git a/packages/smart-forms-renderer/src/utils/itemControl.ts b/packages/smart-forms-renderer/src/utils/itemControl.ts index 4fd03bcae..c6bf54205 100644 --- a/packages/smart-forms-renderer/src/utils/itemControl.ts +++ b/packages/smart-forms-renderer/src/utils/itemControl.ts @@ -15,13 +15,7 @@ * limitations under the License. */ -import type { - Coding, - Expression, - Extension, - QuestionnaireItem, - QuestionnaireResponse -} from 'fhir/r4'; +import type { Coding, Expression, Extension, QuestionnaireItem } from 'fhir/r4'; import type { RegexValidation } from '../interfaces/regex.interface'; /** @@ -220,28 +214,6 @@ export function getMarkdownString(qItem: QuestionnaireItem): string | null { return null; } -/** - * Get questionnaire name from questionnaireResponse - * If questionnaireResponse does not have a name, fallback to questionnaireResponse questionnaireId - * - * @author Sean Fong - */ -export function getQuestionnaireNameFromResponse( - questionnaireResponse: QuestionnaireResponse -): string { - const itemControl = questionnaireResponse._questionnaire?.extension?.find( - (extension: Extension) => extension.url === 'http://hl7.org/fhir/StructureDefinition/display' - ); - - if (itemControl) { - if (itemControl.valueString) { - return itemControl.valueString.charAt(0).toUpperCase() + itemControl.valueString.slice(1); - } - } - - return questionnaireResponse.id ?? 'Unnamed Response'; -} - /** * Get text display prompt for items with itemControlCode prompt and has a prompt childItem * @@ -332,26 +304,6 @@ export function getTextDisplayInstructions(qItem: QuestionnaireItem): string { return ''; } -/** - * Get entry format if its extension is present - * i.e. DD-MM-YYYY for dates, HH:MM for times etc. - * - * @author Sean Fong - */ -export function getEntryFormat(qItem: QuestionnaireItem): string { - const itemControl = qItem.extension?.find( - (extension: Extension) => - extension.url === 'http://hl7.org/fhir/StructureDefinition/entryFormat' - ); - - if (itemControl) { - if (itemControl.valueString) { - return itemControl.valueString; - } - } - return ''; -} - /** * Get entry format if its extension is present * i.e. DD-MM-YYYY for dates, HH:MM for times etc. @@ -389,6 +341,28 @@ export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation | return null; } +/**maxLength + * Get minimum length of characters allowed if present + * + * @author Sean Fong + */ +export function getMinLength(qItem: QuestionnaireItem): number | null { + const itemControl = qItem.extension?.find( + (extension: Extension) => extension.url === 'http://hl7.org/fhir/StructureDefinition/minLength' + ); + + // Get minLength from extension + if (itemControl) { + const minLength = itemControl.valueInteger; + + if (minLength) { + return minLength; + } + } + + return null; +} + /** * Get maximum length of characters allowed if present * diff --git a/packages/smart-forms-renderer/src/utils/mapItem.ts b/packages/smart-forms-renderer/src/utils/mapItem.ts index 76c3f3cfb..2e31fc06f 100644 --- a/packages/smart-forms-renderer/src/utils/mapItem.ts +++ b/packages/smart-forms-renderer/src/utils/mapItem.ts @@ -23,6 +23,8 @@ import { isRepeatItemAndNotCheckbox } from './qItem'; * QuestionnaireItems without a corresponding QuestionnaireResponseItem is set as undefined. * i.e. QItems = [QItem0, QItem1, QItem2]. Only QItem0 and QItem2 have QrItems * Generated array: [QrItem0, undefined, QrItem2] + * Note: There's a bug where if the qItems are child items from a repeat group, the function fails at the isRepeatGroup line. + * Ensure that repeat groups are handled prior to calling this function. * * @author Sean Fong */ From 3cb416de873c733b00686fed1be4d5667a3e24ed Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 19 Feb 2024 13:09:50 +1030 Subject: [PATCH 03/10] Add item validity checks --- .../DecimalItem/DecimalItem.tsx | 19 +- .../IntegerItem/IntegerItem.tsx | 19 +- .../FormComponents/StringItem/StringItem.tsx | 19 +- .../FormComponents/TextItem/TextItem.tsx | 19 +- .../FormComponents/UrlItem/UrlItem.tsx | 19 +- .../src/components/Renderer/BaseRenderer.tsx | 3 + .../src/hooks/useRenderingExtensions.ts | 5 + .../src/hooks/useValidationError.ts | 27 ++- .../src/stores/questionnaireResponseStore.ts | 58 +++++- .../src/utils/validateRequired.ts | 192 ++++++++++++++++++ 10 files changed, 362 insertions(+), 18 deletions(-) create mode 100644 packages/smart-forms-renderer/src/utils/validateRequired.ts diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx index c236f8763..b2c030c7e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx @@ -39,6 +39,7 @@ import { getDecimalPrecision } from '../../../utils/itemControl'; import useDecimalCalculatedExpression from '../../../hooks/useDecimalCalculatedExpression'; import useStringInput from '../../../hooks/useStringInput'; import useReadOnly from '../../../hooks/useReadOnly'; +import { useQuestionnaireResponseStore } from '../../../stores'; interface DecimalItemProps extends PropsWithQrItemChangeHandler, @@ -52,6 +53,8 @@ interface DecimalItemProps function DecimalItem(props: DecimalItemProps) { const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; + const updateSingleItemValidity = useQuestionnaireResponseStore.use.updateSingleItemValidity(); + const readOnly = useReadOnly(qItem, parentIsReadOnly); const precision = getDecimalPrecision(qItem); const { @@ -59,7 +62,9 @@ function DecimalItem(props: DecimalItemProps) { displayPrompt, displayInstructions, entryFormat, + required, regexValidation, + minLength, maxLength } = useRenderingExtensions(qItem); @@ -80,7 +85,12 @@ function DecimalItem(props: DecimalItemProps) { const [input, setInput] = useStringInput(initialInput); // Perform validation checks - const feedback = useValidationError(input, regexValidation, maxLength); + const { invalidType, feedback } = useValidationError( + input, + regexValidation, + minLength, + maxLength + ); // Process calculated expressions const { calcExpUpdated } = useDecimalCalculatedExpression({ @@ -104,6 +114,7 @@ function DecimalItem(props: DecimalItemProps) { // eslint-disable-next-line react-hooks/exhaustive-deps const updateQrItemWithDebounce = useCallback( debounce((parsedNewInput: string) => { + updateSingleItemValidity(qItem.linkId, invalidType); onQrItemChange({ ...createEmptyQrItem(qItem), answer: precision @@ -133,7 +144,11 @@ function DecimalItem(props: DecimalItemProps) { return ( - + { + updateSingleItemValidity(qItem.linkId, invalidType); onQrItemChange({ ...createEmptyQrItem(qItem), answer: [{ valueInteger: newValue }] @@ -121,7 +132,11 @@ function IntegerItem(props: IntegerItemProps) { return ( - + { + updateSingleItemValidity(qItem.linkId, invalidType); const emptyQrItem = createEmptyQrItem(qItem); if (input !== '') { onQrItemChange({ ...emptyQrItem, answer: [{ valueString: input.trim() }] }); @@ -113,7 +124,11 @@ function StringItem(props: StringItemProps) { } return ( - + { + updateSingleItemValidity(qItem.linkId, invalidType); const emptyQrItem = createEmptyQrItem(qItem); if (input !== '') { onQrItemChange({ ...emptyQrItem, answer: [{ valueString: input.trim() }] }); @@ -111,7 +122,11 @@ function TextItem(props: TextItemProps) { } return ( - + { + updateSingleItemValidity(qItem.linkId, invalidType); const emptyQrItem = createEmptyQrItem(qItem); if (input !== '') { onQrItemChange({ ...emptyQrItem, answer: [{ valueUri: input }] }); @@ -100,7 +111,11 @@ function UrlItem(props: UrlItemProps) { } return ( - + mapQItemsIndex(sourceQuestionnaire), [sourceQuestionnaire]); @@ -45,6 +46,7 @@ function BaseRenderer() { updateQrItemsInGroup(newTopLevelQRItem, null, updatedResponse, qItemsIndexMap); updateExpressions(updatedResponse); + updateRequiredValidity(sourceQuestionnaire, updatedResponse); updateResponse(updatedResponse); } @@ -57,6 +59,7 @@ function BaseRenderer() { updateQrItemsInGroup(null, newTopLevelQRItems, updatedResponse, qItemsIndexMap); updateExpressions(updatedResponse); + updateRequiredValidity(sourceQuestionnaire, updatedResponse); updateResponse(updatedResponse); } diff --git a/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts b/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts index 565763863..b864f26cf 100644 --- a/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts +++ b/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts @@ -17,6 +17,7 @@ import { getMaxLength, + getMinLength, getReadOnly, getRegexValidation, getTextDisplayInstructions, @@ -33,7 +34,9 @@ interface RenderingExtensions { displayInstructions: string; readOnly: boolean; entryFormat: string; + required: boolean; regexValidation: RegexValidation | null; + minLength: number | null; maxLength: number | null; } @@ -44,7 +47,9 @@ function useRenderingExtensions(qItem: QuestionnaireItem): RenderingExtensions { displayInstructions: getTextDisplayInstructions(qItem), readOnly: getReadOnly(qItem), entryFormat: structuredDataCapture.getEntryFormat(qItem) ?? '', + required: qItem.required ?? false, regexValidation: getRegexValidation(qItem), + minLength: getMinLength(qItem), maxLength: getMaxLength(qItem) }; } diff --git a/packages/smart-forms-renderer/src/hooks/useValidationError.ts b/packages/smart-forms-renderer/src/hooks/useValidationError.ts index c492e4537..3a7c4dcbd 100644 --- a/packages/smart-forms-renderer/src/hooks/useValidationError.ts +++ b/packages/smart-forms-renderer/src/hooks/useValidationError.ts @@ -16,33 +16,46 @@ */ import type { RegexValidation } from '../interfaces/regex.interface'; +import { InvalidType } from '../utils/validateRequired'; function useValidationError( input: string, regexValidation: RegexValidation | null, + minLength: number | null, maxLength: number | null -): string { - let feedback = ''; +): { invalidType: InvalidType; feedback: string } { + const invalidStatus: { invalidType: InvalidType; feedback: string } = { + invalidType: null, + feedback: '' + }; if (input) { // Test regex if (regexValidation) { if (!regexValidation.expression.test(input)) { - feedback = - regexValidation.feedback ?? - `Input should match the specified regex ${regexValidation.expression}`; + invalidStatus.invalidType = 'regex'; + invalidStatus.feedback = `Input should match the specified regex ${regexValidation.expression}`; + } + } + + // Test min character limit + if (minLength) { + if (input.length < minLength) { + invalidStatus.invalidType = 'minLength'; + invalidStatus.feedback = `Enter at least ${minLength} characters.`; } } // Test max character limit if (maxLength) { if (input.length > maxLength) { - feedback = 'Input exceeds maximum character limit.'; + invalidStatus.invalidType = 'maxLength'; + invalidStatus.feedback = `Input exceeds maximum character limit of ${maxLength}.`; } } } - return feedback; + return invalidStatus; } export default useValidationError; diff --git a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts index d3d5d6866..a6ff2b0a2 100644 --- a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts +++ b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts @@ -16,17 +16,26 @@ */ import { createStore } from 'zustand/vanilla'; -import type { QuestionnaireResponse } from 'fhir/r4'; +import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; import { emptyResponse } from '../utils/emptyResource'; import cloneDeep from 'lodash.clonedeep'; import type { Diff } from 'deep-diff'; import { diff } from 'deep-diff'; import { createSelectors } from './selector'; +import type { InvalidType } from '../utils/validateRequired'; +import { validateQuestionnaireRequiredItems } from '../utils/validateRequired'; +import { questionnaireStore } from '../../lib'; interface QuestionnaireResponseStoreType { sourceResponse: QuestionnaireResponse; updatableResponse: QuestionnaireResponse; formChangesHistory: (Diff[] | null)[]; + invalidItems: Record; + updateRequiredValidity: ( + questionnaire: Questionnaire, + updatedResponse: QuestionnaireResponse + ) => void; + updateSingleItemValidity: (linkId: string, invalidType: InvalidType) => void; buildSourceResponse: (response: QuestionnaireResponse) => void; setUpdatableResponseAsPopulated: (populatedResponse: QuestionnaireResponse) => void; updateResponse: (updatedResponse: QuestionnaireResponse) => void; @@ -40,6 +49,53 @@ export const questionnaireResponseStore = createStore { + const tempInvalidItems = get().invalidItems; + + const enableWhenIsActivated = questionnaireStore.getState().enableWhenIsActivated; + const enableWhenItems = questionnaireStore.getState().enableWhenItems; + const enableWhenExpressions = questionnaireStore.getState().enableWhenExpressions; + const invalidRequiredLinkIds: string[] = []; + + // Remove and re-add invalid required items from the invalidItems object + for (const linkId in tempInvalidItems) { + if (tempInvalidItems[linkId] === 'required') { + delete tempInvalidItems[linkId]; + } + + if (tempInvalidItems[linkId] === null) { + delete tempInvalidItems[linkId]; + } + } + + validateQuestionnaireRequiredItems({ + questionnaire, + questionnaireResponse: updatedResponse, + invalidRequiredLinkIds, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }); + for (const linkId of invalidRequiredLinkIds) { + tempInvalidItems[linkId] = 'required'; + } + + set(() => ({ + invalidItems: tempInvalidItems + })); + }, + updateSingleItemValidity: (linkId: string, invalidType: InvalidType) => { + set(() => ({ + invalidItems: { + ...get().invalidItems, + [linkId]: invalidType + } + })); + }, buildSourceResponse: (questionnaireResponse: QuestionnaireResponse) => { set(() => ({ sourceResponse: questionnaireResponse, diff --git a/packages/smart-forms-renderer/src/utils/validateRequired.ts b/packages/smart-forms-renderer/src/utils/validateRequired.ts new file mode 100644 index 000000000..f6ffb2211 --- /dev/null +++ b/packages/smart-forms-renderer/src/utils/validateRequired.ts @@ -0,0 +1,192 @@ +/* + * Copyright 2023 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 { QuestionnaireResponse } from 'fhir/r4'; +import { Questionnaire, QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; +import { getQrItemsIndex, mapQItemsIndex } from './mapItem'; +import { EnableWhenExpression, EnableWhenItems } from '../interfaces/enableWhen.interface'; +import { isHidden } from './qItem'; + +export type InvalidType = 'regex' | 'minLength' | 'maxLength' | 'required' | null; + +interface ValidateQuestionnaireRequiredItemsParams { + questionnaire: Questionnaire; + questionnaireResponse: QuestionnaireResponse; + invalidRequiredLinkIds: string[]; + enableWhenIsActivated: boolean; + enableWhenItems: EnableWhenItems; + enableWhenExpressions: Record; +} + +/** + * Recursively go through the questionnaireResponse and check for un-filled required qItems + * At the moment item.required for group items are not checked + * FIXME will eventually be renamed to validate questionnaire + * + * @author Sean Fong + */ +export function validateQuestionnaireRequiredItems( + params: ValidateQuestionnaireRequiredItemsParams +): string[] { + const { + questionnaire, + questionnaireResponse, + invalidRequiredLinkIds, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + } = params; + + if ( + !questionnaire.item || + questionnaire.item.length === 0 || + !questionnaireResponse.item || + questionnaireResponse.item.length === 0 + ) { + return []; + } + + const qItemsIndexMap = mapQItemsIndex(questionnaire); + const topLevelQRItemsByIndex = getQrItemsIndex( + questionnaire.item, + questionnaireResponse.item, + qItemsIndexMap + ); + + for (const [index, topLevelQItem] of questionnaire.item.entries()) { + let topLevelQRItem = topLevelQRItemsByIndex[index] ?? { + linkId: topLevelQItem.linkId, + text: topLevelQItem.text + }; + + if (Array.isArray(topLevelQRItem)) { + topLevelQRItem = { + linkId: topLevelQItem.linkId, + text: topLevelQItem.text, + item: topLevelQRItem + }; + } + + validateRequiredItemRecursive({ + qItem: topLevelQItem, + qrItem: topLevelQRItem, + invalidRequiredLinkIds, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }); + } + + return invalidRequiredLinkIds; +} + +interface ValidateRequiredItemRecursiveParams { + qItem: QuestionnaireItem; + qrItem: QuestionnaireResponseItem; + invalidRequiredLinkIds: string[]; + enableWhenIsActivated: boolean; + enableWhenItems: EnableWhenItems; + enableWhenExpressions: Record; +} + +function validateRequiredItemRecursive(params: ValidateRequiredItemRecursiveParams) { + const { + qItem, + qrItem, + invalidRequiredLinkIds, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + } = params; + + if ( + isHidden({ + questionnaireItem: qItem, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }) + ) { + return; + } + + // FIXME repeat groups not working + if (qItem.type === 'group' && qItem.repeats) { + return validateRequiredRepeatGroup(qItem, qrItem, invalidRequiredLinkIds); + } + + const childQItems = qItem.item; + if (childQItems && childQItems.length > 0) { + const childQrItems = qrItem?.item ?? []; + + const indexMap = mapQItemsIndex(qItem); + const qrItemsByIndex = getQrItemsIndex(childQItems, childQrItems, indexMap); + + if (qItem.type === 'group' && qItem.required) { + if (!qrItem || qrItemsByIndex.length === 0) { + invalidRequiredLinkIds.push(qItem.linkId); + } + } + + for (const [index, childQItem] of childQItems.entries()) { + let childQRItem = qrItemsByIndex[index] ?? { + linkId: childQItem.linkId, + text: childQItem.text + }; + + if (Array.isArray(childQRItem)) { + childQRItem = { + linkId: childQItem.linkId, + text: childQItem.text, + item: childQRItem + }; + } + + validateRequiredItemRecursive({ + qItem: childQItem, + qrItem: childQRItem, + invalidRequiredLinkIds, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }); + } + } + + validateRequiredSingleItem(qItem, qrItem, invalidRequiredLinkIds); +} + +function validateRequiredSingleItem( + qItem: QuestionnaireItem, + qrItem: QuestionnaireResponseItem, + invalidLinkIds: string[] +) { + // Process non-group items + if (qItem.type !== 'display') { + if (qItem.required && !qrItem.answer) { + invalidLinkIds.push(qItem.linkId); + } + } +} + +function validateRequiredRepeatGroup( + qItem: QuestionnaireItem, + qrItems: QuestionnaireResponseItem, + invalidLinkIds: string[] +) { + return; +} From cb83f37216ffc9ec9aa9a19527e400ccc3f8968b Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 19 Feb 2024 16:53:38 +1030 Subject: [PATCH 04/10] Consider regex, minLength and maxLength validation in invalidItems, add reponseIsValid flag --- .../DecimalItem/DecimalItem.tsx | 13 +- .../IntegerItem/IntegerItem.tsx | 13 +- .../FormComponents/StringItem/StringItem.tsx | 13 +- .../FormComponents/TextItem/TextItem.tsx | 13 +- .../FormComponents/UrlItem/UrlItem.tsx | 13 +- .../src/components/Renderer/BaseRenderer.tsx | 8 +- .../src/hooks/useRenderingExtensions.ts | 29 ++-- .../src/hooks/useValidationError.ts | 61 -------- .../src/hooks/useValidationFeedback.ts | 50 +++++++ .../src/stores/questionnaireResponseStore.ts | 41 ++---- .../src/utils/itemControl.ts | 40 ------ ...teRequired.ts => validateQuestionnaire.ts} | 135 ++++++++++++++---- 12 files changed, 200 insertions(+), 229 deletions(-) delete mode 100644 packages/smart-forms-renderer/src/hooks/useValidationError.ts create mode 100644 packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts rename packages/smart-forms-renderer/src/utils/{validateRequired.ts => validateQuestionnaire.ts} (55%) diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx index b2c030c7e..d8fe689d4 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx @@ -25,7 +25,7 @@ import type { import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; import { FullWidthFormComponentBox } from '../../Box.styles'; -import useValidationError from '../../../hooks/useValidationError'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; import debounce from 'lodash.debounce'; import { DEBOUNCE_DURATION } from '../../../utils/debounce'; import { createEmptyQrItem } from '../../../utils/qrItem'; @@ -39,7 +39,6 @@ import { getDecimalPrecision } from '../../../utils/itemControl'; import useDecimalCalculatedExpression from '../../../hooks/useDecimalCalculatedExpression'; import useStringInput from '../../../hooks/useStringInput'; import useReadOnly from '../../../hooks/useReadOnly'; -import { useQuestionnaireResponseStore } from '../../../stores'; interface DecimalItemProps extends PropsWithQrItemChangeHandler, @@ -53,8 +52,6 @@ interface DecimalItemProps function DecimalItem(props: DecimalItemProps) { const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; - const updateSingleItemValidity = useQuestionnaireResponseStore.use.updateSingleItemValidity(); - const readOnly = useReadOnly(qItem, parentIsReadOnly); const precision = getDecimalPrecision(qItem); const { @@ -85,12 +82,7 @@ function DecimalItem(props: DecimalItemProps) { const [input, setInput] = useStringInput(initialInput); // Perform validation checks - const { invalidType, feedback } = useValidationError( - input, - regexValidation, - minLength, - maxLength - ); + const feedback = useValidationFeedback(input, regexValidation, minLength, maxLength); // Process calculated expressions const { calcExpUpdated } = useDecimalCalculatedExpression({ @@ -114,7 +106,6 @@ function DecimalItem(props: DecimalItemProps) { // eslint-disable-next-line react-hooks/exhaustive-deps const updateQrItemWithDebounce = useCallback( debounce((parsedNewInput: string) => { - updateSingleItemValidity(qItem.linkId, invalidType); onQrItemChange({ ...createEmptyQrItem(qItem), answer: precision diff --git a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx index 6decb0979..81bef1c85 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx @@ -24,7 +24,7 @@ import type { } from '../../../interfaces/renderProps.interface'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationError from '../../../hooks/useValidationError'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; import debounce from 'lodash.debounce'; import { createEmptyQrItem } from '../../../utils/qrItem'; import { DEBOUNCE_DURATION } from '../../../utils/debounce'; @@ -35,7 +35,6 @@ import { parseValidInteger } from '../../../utils/parseInputs'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import useNumberInput from '../../../hooks/useNumberInput'; import useReadOnly from '../../../hooks/useReadOnly'; -import { useQuestionnaireResponseStore } from '../../../stores'; interface IntegerItemProps extends PropsWithQrItemChangeHandler, @@ -47,8 +46,6 @@ interface IntegerItemProps } function IntegerItem(props: IntegerItemProps) { - const updateSingleItemValidity = useQuestionnaireResponseStore.use.updateSingleItemValidity(); - const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; const readOnly = useReadOnly(qItem, parentIsReadOnly); @@ -76,12 +73,7 @@ function IntegerItem(props: IntegerItemProps) { const [value, setValue] = useNumberInput(valueInteger); // Perform validation checks - const { invalidType, feedback } = useValidationError( - value.toString(), - regexValidation, - minLength, - maxLength - ); + const feedback = useValidationFeedback(value.toString(), regexValidation, minLength, maxLength); // Process calculated expressions const { calcExpUpdated } = useIntegerCalculatedExpression({ @@ -104,7 +96,6 @@ function IntegerItem(props: IntegerItemProps) { // eslint-disable-next-line react-hooks/exhaustive-deps const updateQrItemWithDebounce = useCallback( debounce((newValue: number) => { - updateSingleItemValidity(qItem.linkId, invalidType); onQrItemChange({ ...createEmptyQrItem(qItem), answer: [{ valueInteger: newValue }] diff --git a/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx index 8d98082e7..d09fd13e4 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx @@ -24,7 +24,7 @@ import type { } from '../../../interfaces/renderProps.interface'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationError from '../../../hooks/useValidationError'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; import debounce from 'lodash.debounce'; import { createEmptyQrItem } from '../../../utils/qrItem'; import { DEBOUNCE_DURATION } from '../../../utils/debounce'; @@ -34,7 +34,6 @@ import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import useStringCalculatedExpression from '../../../hooks/useStringCalculatedExpression'; import useStringInput from '../../../hooks/useStringInput'; import useReadOnly from '../../../hooks/useReadOnly'; -import { useQuestionnaireResponseStore } from '../../../stores'; interface StringItemProps extends PropsWithQrItemChangeHandler, @@ -47,8 +46,6 @@ interface StringItemProps function StringItem(props: StringItemProps) { const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; - const updateSingleItemValidity = useQuestionnaireResponseStore.use.updateSingleItemValidity(); - const readOnly = useReadOnly(qItem, parentIsReadOnly); const { displayUnit, @@ -69,12 +66,7 @@ function StringItem(props: StringItemProps) { const [input, setInput] = useStringInput(valueString); // Perform validation checks - const { invalidType, feedback } = useValidationError( - input, - regexValidation, - minLength, - maxLength - ); + const feedback = useValidationFeedback(input, regexValidation, minLength, maxLength); // Process calculated expressions const { calcExpUpdated } = useStringCalculatedExpression({ @@ -95,7 +87,6 @@ function StringItem(props: StringItemProps) { // eslint-disable-next-line react-hooks/exhaustive-deps const updateQrItemWithDebounce = useCallback( debounce((input: string) => { - updateSingleItemValidity(qItem.linkId, invalidType); const emptyQrItem = createEmptyQrItem(qItem); if (input !== '') { onQrItemChange({ ...emptyQrItem, answer: [{ valueString: input.trim() }] }); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx index 036206408..75b77a661 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx @@ -23,7 +23,7 @@ import type { } from '../../../interfaces/renderProps.interface'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationError from '../../../hooks/useValidationError'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; import debounce from 'lodash.debounce'; import { createEmptyQrItem } from '../../../utils/qrItem'; import { DEBOUNCE_DURATION } from '../../../utils/debounce'; @@ -33,7 +33,6 @@ import useStringCalculatedExpression from '../../../hooks/useStringCalculatedExp import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import useStringInput from '../../../hooks/useStringInput'; import useReadOnly from '../../../hooks/useReadOnly'; -import { useQuestionnaireResponseStore } from '../../../stores'; interface TextItemProps extends PropsWithQrItemChangeHandler, @@ -46,8 +45,6 @@ interface TextItemProps function TextItem(props: TextItemProps) { const { qItem, qrItem, isRepeated, parentIsReadOnly, onQrItemChange } = props; - const updateSingleItemValidity = useQuestionnaireResponseStore.use.updateSingleItemValidity(); - const readOnly = useReadOnly(qItem, parentIsReadOnly); const { displayUnit, @@ -68,12 +65,7 @@ function TextItem(props: TextItemProps) { const [input, setInput] = useStringInput(valueText); // Perform validation checks - const { invalidType, feedback } = useValidationError( - input, - regexValidation, - minLength, - maxLength - ); + const feedback = useValidationFeedback(input, regexValidation, minLength, maxLength); // Process calculated expressions const { calcExpUpdated } = useStringCalculatedExpression({ @@ -94,7 +86,6 @@ function TextItem(props: TextItemProps) { // eslint-disable-next-line react-hooks/exhaustive-deps const updateQrItemWithDebounce = useCallback( debounce((input: string) => { - updateSingleItemValidity(qItem.linkId, invalidType); const emptyQrItem = createEmptyQrItem(qItem); if (input !== '') { onQrItemChange({ ...emptyQrItem, answer: [{ valueString: input.trim() }] }); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx index 7f261ef35..431603630 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx @@ -24,7 +24,7 @@ import type { } from '../../../interfaces/renderProps.interface'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationError from '../../../hooks/useValidationError'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; import debounce from 'lodash.debounce'; import { createEmptyQrItem } from '../../../utils/qrItem'; import { DEBOUNCE_DURATION } from '../../../utils/debounce'; @@ -32,7 +32,6 @@ import { FullWidthFormComponentBox } from '../../Box.styles'; import UrlField from './UrlField'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import useReadOnly from '../../../hooks/useReadOnly'; -import { useQuestionnaireResponseStore } from '../../../stores'; interface UrlItemProps extends PropsWithQrItemChangeHandler, @@ -45,8 +44,6 @@ interface UrlItemProps function UrlItem(props: UrlItemProps) { const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; - const updateSingleItemValidity = useQuestionnaireResponseStore.use.updateSingleItemValidity(); - const readOnly = useReadOnly(qItem, parentIsReadOnly); const { displayUnit, @@ -67,12 +64,7 @@ function UrlItem(props: UrlItemProps) { const [input, setInput] = useState(valueUri); // Perform validation checks - const { invalidType, feedback } = useValidationError( - input, - regexValidation, - minLength, - maxLength - ); + const feedback = useValidationFeedback(input, regexValidation, minLength, maxLength); // Event handlers function handleChange(newInput: string) { @@ -83,7 +75,6 @@ function UrlItem(props: UrlItemProps) { // eslint-disable-next-line react-hooks/exhaustive-deps const updateQrItemWithDebounce = useCallback( debounce((input: string) => { - updateSingleItemValidity(qItem.linkId, invalidType); const emptyQrItem = createEmptyQrItem(qItem); if (input !== '') { onQrItemChange({ ...emptyQrItem, answer: [{ valueUri: input }] }); diff --git a/packages/smart-forms-renderer/src/components/Renderer/BaseRenderer.tsx b/packages/smart-forms-renderer/src/components/Renderer/BaseRenderer.tsx index 92ad0c97b..3157827db 100644 --- a/packages/smart-forms-renderer/src/components/Renderer/BaseRenderer.tsx +++ b/packages/smart-forms-renderer/src/components/Renderer/BaseRenderer.tsx @@ -32,9 +32,12 @@ function BaseRenderer() { const readOnly = useQuestionnaireStore.use.readOnly(); const updatableResponse = useQuestionnaireResponseStore.use.updatableResponse(); - const updateRequiredValidity = useQuestionnaireResponseStore.use.updateRequiredValidity(); + const updateRequiredValidity = useQuestionnaireResponseStore.use.validateQuestionnaire(); const updateResponse = useQuestionnaireResponseStore.use.updateResponse(); + const invalidItems = useQuestionnaireResponseStore.use.invalidItems(); + const responseIsValid = useQuestionnaireResponseStore.use.responseIsValid(); + const qItemsIndexMap = useMemo(() => mapQItemsIndex(sourceQuestionnaire), [sourceQuestionnaire]); function handleTopLevelQRItemSingleChange(newTopLevelQRItem: QuestionnaireResponseItem) { @@ -73,6 +76,9 @@ function BaseRenderer() { // If an item has multiple answers, it is a repeat group const topLevelQRItemsByIndex = getQrItemsIndex(topLevelQItems, topLevelQRItems, qItemsIndexMap); + console.log(invalidItems); + console.log(responseIsValid); + return ( diff --git a/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts b/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts index b864f26cf..098c9d244 100644 --- a/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts +++ b/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts @@ -16,9 +16,6 @@ */ import { - getMaxLength, - getMinLength, - getReadOnly, getRegexValidation, getTextDisplayInstructions, getTextDisplayPrompt, @@ -27,6 +24,7 @@ import { import type { QuestionnaireItem } from 'fhir/r4'; import type { RegexValidation } from '../interfaces/regex.interface'; import { structuredDataCapture } from 'fhir-sdc-helpers'; +import { useMemo } from 'react'; interface RenderingExtensions { displayUnit: string; @@ -41,17 +39,20 @@ interface RenderingExtensions { } function useRenderingExtensions(qItem: QuestionnaireItem): RenderingExtensions { - return { - displayUnit: getTextDisplayUnit(qItem), - displayPrompt: getTextDisplayPrompt(qItem), - displayInstructions: getTextDisplayInstructions(qItem), - readOnly: getReadOnly(qItem), - entryFormat: structuredDataCapture.getEntryFormat(qItem) ?? '', - required: qItem.required ?? false, - regexValidation: getRegexValidation(qItem), - minLength: getMinLength(qItem), - maxLength: getMaxLength(qItem) - }; + return useMemo( + () => ({ + displayUnit: getTextDisplayUnit(qItem), + displayPrompt: getTextDisplayPrompt(qItem), + displayInstructions: getTextDisplayInstructions(qItem), + readOnly: !!qItem.readOnly, + entryFormat: structuredDataCapture.getEntryFormat(qItem) ?? '', + required: qItem.required ?? false, + regexValidation: getRegexValidation(qItem), + minLength: structuredDataCapture.getMinLength(qItem) ?? null, + maxLength: qItem.maxLength ?? null + }), + [qItem] + ); } export default useRenderingExtensions; diff --git a/packages/smart-forms-renderer/src/hooks/useValidationError.ts b/packages/smart-forms-renderer/src/hooks/useValidationError.ts deleted file mode 100644 index 3a7c4dcbd..000000000 --- a/packages/smart-forms-renderer/src/hooks/useValidationError.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 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 { RegexValidation } from '../interfaces/regex.interface'; -import { InvalidType } from '../utils/validateRequired'; - -function useValidationError( - input: string, - regexValidation: RegexValidation | null, - minLength: number | null, - maxLength: number | null -): { invalidType: InvalidType; feedback: string } { - const invalidStatus: { invalidType: InvalidType; feedback: string } = { - invalidType: null, - feedback: '' - }; - - if (input) { - // Test regex - if (regexValidation) { - if (!regexValidation.expression.test(input)) { - invalidStatus.invalidType = 'regex'; - invalidStatus.feedback = `Input should match the specified regex ${regexValidation.expression}`; - } - } - - // Test min character limit - if (minLength) { - if (input.length < minLength) { - invalidStatus.invalidType = 'minLength'; - invalidStatus.feedback = `Enter at least ${minLength} characters.`; - } - } - - // Test max character limit - if (maxLength) { - if (input.length > maxLength) { - invalidStatus.invalidType = 'maxLength'; - invalidStatus.feedback = `Input exceeds maximum character limit of ${maxLength}.`; - } - } - } - - return invalidStatus; -} - -export default useValidationError; diff --git a/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts new file mode 100644 index 000000000..d0638ec59 --- /dev/null +++ b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2023 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 { RegexValidation } from '../interfaces/regex.interface'; +import { getInputInvalidType } from '../utils/validateQuestionnaire'; + +function useValidationFeedback( + input: string, + regexValidation: RegexValidation | null, + minLength: number | null, + maxLength: number | null +): string { + const invalidType = getInputInvalidType(input, regexValidation, minLength, maxLength); + + if (!invalidType) { + return ''; + } + + if (invalidType === 'regex' && regexValidation) { + return `Input should match the specified regex ${regexValidation.expression}`; + } + + // Test min character limit + if (invalidType === 'minLength' && minLength) { + return `Enter at least ${minLength} characters.`; + } + + // Test max character limit + if (invalidType === 'maxLength' && maxLength) { + return `Input exceeds maximum character limit of ${maxLength}.`; + } + + return ''; +} + +export default useValidationFeedback; diff --git a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts index a6ff2b0a2..151219465 100644 --- a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts +++ b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts @@ -22,8 +22,8 @@ import cloneDeep from 'lodash.clonedeep'; import type { Diff } from 'deep-diff'; import { diff } from 'deep-diff'; import { createSelectors } from './selector'; -import type { InvalidType } from '../utils/validateRequired'; -import { validateQuestionnaireRequiredItems } from '../utils/validateRequired'; +import type { InvalidType } from '../utils/validateQuestionnaire'; +import { validateQuestionnaire } from '../utils/validateQuestionnaire'; import { questionnaireStore } from '../../lib'; interface QuestionnaireResponseStoreType { @@ -31,11 +31,11 @@ interface QuestionnaireResponseStoreType { updatableResponse: QuestionnaireResponse; formChangesHistory: (Diff[] | null)[]; invalidItems: Record; - updateRequiredValidity: ( + responseIsValid: boolean; + validateQuestionnaire: ( questionnaire: Questionnaire, updatedResponse: QuestionnaireResponse ) => void; - updateSingleItemValidity: (linkId: string, invalidType: InvalidType) => void; buildSourceResponse: (response: QuestionnaireResponse) => void; setUpdatableResponseAsPopulated: (populatedResponse: QuestionnaireResponse) => void; updateResponse: (updatedResponse: QuestionnaireResponse) => void; @@ -50,7 +50,8 @@ export const questionnaireResponseStore = createStore { @@ -59,41 +60,19 @@ export const questionnaireResponseStore = createStore ({ - invalidItems: tempInvalidItems - })); - }, - updateSingleItemValidity: (linkId: string, invalidType: InvalidType) => { - set(() => ({ - invalidItems: { - ...get().invalidItems, - [linkId]: invalidType - } + invalidItems: tempInvalidItems, + responseIsValid: Object.keys(tempInvalidItems).length === 0 })); }, buildSourceResponse: (questionnaireResponse: QuestionnaireResponse) => { diff --git a/packages/smart-forms-renderer/src/utils/itemControl.ts b/packages/smart-forms-renderer/src/utils/itemControl.ts index c6bf54205..f928293ea 100644 --- a/packages/smart-forms-renderer/src/utils/itemControl.ts +++ b/packages/smart-forms-renderer/src/utils/itemControl.ts @@ -231,15 +231,6 @@ export function getTextDisplayPrompt(qItem: QuestionnaireItem): string { return ''; } -/** - * Check if item is readonly - * - * @author Sean Fong - */ -export function getReadOnly(qItem: QuestionnaireItem): boolean { - return !!qItem.readOnly; -} - /** * Get decimal text display unit for items with itemControlCode unit and has a unit childItem * @@ -340,34 +331,3 @@ export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation | return null; } - -/**maxLength - * Get minimum length of characters allowed if present - * - * @author Sean Fong - */ -export function getMinLength(qItem: QuestionnaireItem): number | null { - const itemControl = qItem.extension?.find( - (extension: Extension) => extension.url === 'http://hl7.org/fhir/StructureDefinition/minLength' - ); - - // Get minLength from extension - if (itemControl) { - const minLength = itemControl.valueInteger; - - if (minLength) { - return minLength; - } - } - - return null; -} - -/** - * Get maximum length of characters allowed if present - * - * @author Sean Fong - */ -export function getMaxLength(qItem: QuestionnaireItem): number | null { - return qItem.maxLength ?? null; -} diff --git a/packages/smart-forms-renderer/src/utils/validateRequired.ts b/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts similarity index 55% rename from packages/smart-forms-renderer/src/utils/validateRequired.ts rename to packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts index f6ffb2211..88d12d8c3 100644 --- a/packages/smart-forms-renderer/src/utils/validateRequired.ts +++ b/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts @@ -15,18 +15,21 @@ * limitations under the License. */ -import type { QuestionnaireResponse } from 'fhir/r4'; +import type { QuestionnaireResponse, QuestionnaireResponseItemAnswer } from 'fhir/r4'; import { Questionnaire, QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import { getQrItemsIndex, mapQItemsIndex } from './mapItem'; import { EnableWhenExpression, EnableWhenItems } from '../interfaces/enableWhen.interface'; import { isHidden } from './qItem'; +import { getRegexValidation } from './itemControl'; +import { structuredDataCapture } from 'fhir-sdc-helpers'; +import { RegexValidation } from '../interfaces/regex.interface'; -export type InvalidType = 'regex' | 'minLength' | 'maxLength' | 'required' | null; +export type InvalidType = 'regex' | 'minLength' | 'maxLength' | 'required'; -interface ValidateQuestionnaireRequiredItemsParams { +interface ValidateQuestionnaireParams { questionnaire: Questionnaire; questionnaireResponse: QuestionnaireResponse; - invalidRequiredLinkIds: string[]; + invalidItems: Record; enableWhenIsActivated: boolean; enableWhenItems: EnableWhenItems; enableWhenExpressions: Record; @@ -39,13 +42,13 @@ interface ValidateQuestionnaireRequiredItemsParams { * * @author Sean Fong */ -export function validateQuestionnaireRequiredItems( - params: ValidateQuestionnaireRequiredItemsParams -): string[] { +export function validateQuestionnaire( + params: ValidateQuestionnaireParams +): Record { const { questionnaire, questionnaireResponse, - invalidRequiredLinkIds, + invalidItems, enableWhenIsActivated, enableWhenItems, enableWhenExpressions @@ -57,7 +60,7 @@ export function validateQuestionnaireRequiredItems( !questionnaireResponse.item || questionnaireResponse.item.length === 0 ) { - return []; + return invalidItems; } const qItemsIndexMap = mapQItemsIndex(questionnaire); @@ -81,33 +84,33 @@ export function validateQuestionnaireRequiredItems( }; } - validateRequiredItemRecursive({ + validateItemRecursive({ qItem: topLevelQItem, qrItem: topLevelQRItem, - invalidRequiredLinkIds, + invalidItems: invalidItems, enableWhenIsActivated, enableWhenItems, enableWhenExpressions }); } - return invalidRequiredLinkIds; + return invalidItems; } -interface ValidateRequiredItemRecursiveParams { +interface ValidateItemRecursiveParams { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem; - invalidRequiredLinkIds: string[]; + invalidItems: Record; enableWhenIsActivated: boolean; enableWhenItems: EnableWhenItems; enableWhenExpressions: Record; } -function validateRequiredItemRecursive(params: ValidateRequiredItemRecursiveParams) { +function validateItemRecursive(params: ValidateItemRecursiveParams) { const { qItem, qrItem, - invalidRequiredLinkIds, + invalidItems, enableWhenIsActivated, enableWhenItems, enableWhenExpressions @@ -126,7 +129,7 @@ function validateRequiredItemRecursive(params: ValidateRequiredItemRecursivePara // FIXME repeat groups not working if (qItem.type === 'group' && qItem.repeats) { - return validateRequiredRepeatGroup(qItem, qrItem, invalidRequiredLinkIds); + return validateRepeatGroup(qItem, qrItem, invalidItems); } const childQItems = qItem.item; @@ -138,7 +141,7 @@ function validateRequiredItemRecursive(params: ValidateRequiredItemRecursivePara if (qItem.type === 'group' && qItem.required) { if (!qrItem || qrItemsByIndex.length === 0) { - invalidRequiredLinkIds.push(qItem.linkId); + invalidItems[qItem.linkId] = 'required'; } } @@ -156,10 +159,10 @@ function validateRequiredItemRecursive(params: ValidateRequiredItemRecursivePara }; } - validateRequiredItemRecursive({ + validateItemRecursive({ qItem: childQItem, qrItem: childQRItem, - invalidRequiredLinkIds, + invalidItems: invalidItems, enableWhenIsActivated, enableWhenItems, enableWhenExpressions @@ -167,26 +170,104 @@ function validateRequiredItemRecursive(params: ValidateRequiredItemRecursivePara } } - validateRequiredSingleItem(qItem, qrItem, invalidRequiredLinkIds); + validateSingleItem(qItem, qrItem, invalidItems); } -function validateRequiredSingleItem( +function validateSingleItem( qItem: QuestionnaireItem, qrItem: QuestionnaireResponseItem, - invalidLinkIds: string[] + invalidItems: Record ) { - // Process non-group items + // Validate item.required if (qItem.type !== 'display') { if (qItem.required && !qrItem.answer) { - invalidLinkIds.push(qItem.linkId); + invalidItems[qItem.linkId] = 'required'; + return invalidItems; } } + + // Validate regex, maxLength and minLength + if (qrItem.answer) { + for (const answer of qrItem.answer) { + if (answer.valueString || answer.valueInteger || answer.valueDecimal || answer.valueUri) { + const invalidInputType = getInputInvalidType( + getInputInString(answer), + getRegexValidation(qItem), + structuredDataCapture.getMinLength(qItem) ?? null, + qItem.maxLength ?? null + ); + + if (!invalidInputType) { + continue; + } + + // Assign invalid type and break - stop checking other answers if is a repeat item + switch (invalidInputType) { + case 'regex': + invalidItems[qItem.linkId] = 'regex'; + break; + case 'minLength': + invalidItems[qItem.linkId] = 'minLength'; + break; + case 'maxLength': + invalidItems[qItem.linkId] = 'maxLength'; + break; + } + break; + } + } + + // Reached the end of the loop and no invalid input type found + // If a required item is filled, remove the required invalid type + if (qItem.required && invalidItems[qItem.linkId] && invalidItems[qItem.linkId] === 'required') { + delete invalidItems[qItem.linkId]; + } + } + + return invalidItems; } -function validateRequiredRepeatGroup( +function validateRepeatGroup( qItem: QuestionnaireItem, qrItems: QuestionnaireResponseItem, - invalidLinkIds: string[] + invalidLinkIds: Record ) { return; } + +function getInputInString(answer: QuestionnaireResponseItemAnswer) { + if (answer.valueString) { + return answer.valueString; + } else if (answer.valueInteger) { + return answer.valueInteger.toString(); + } else if (answer.valueDecimal) { + return answer.valueDecimal.toString(); + } else if (answer.valueUri) { + return answer.valueUri; + } + + return ''; +} + +export function getInputInvalidType( + input: string, + regexValidation: RegexValidation | null, + minLength: number | null, + maxLength: number | null +): InvalidType | null { + if (input) { + if (regexValidation && !regexValidation.expression.test(input)) { + return 'regex'; + } + + if (minLength && input.length < minLength) { + return 'minLength'; + } + + if (maxLength && input.length > maxLength) { + return 'maxLength'; + } + } + + return null; +} From 575dbd130f3995227657691321dc89d8d9a40188 Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 19 Feb 2024 16:59:32 +1030 Subject: [PATCH 05/10] Fix reading function from /lib --- .../src/stores/questionnaireResponseStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts index 151219465..f11b6ca03 100644 --- a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts +++ b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts @@ -24,7 +24,7 @@ import { diff } from 'deep-diff'; import { createSelectors } from './selector'; import type { InvalidType } from '../utils/validateQuestionnaire'; import { validateQuestionnaire } from '../utils/validateQuestionnaire'; -import { questionnaireStore } from '../../lib'; +import { questionnaireStore } from './questionnaireStore'; interface QuestionnaireResponseStoreType { sourceResponse: QuestionnaireResponse; From 970b632a45d41b833554a77b2341e51ccf00d8bc Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 19 Feb 2024 17:01:47 +1030 Subject: [PATCH 06/10] Update renderer to v0.13.0 --- package-lock.json | 77 +++++++++++++++++++++- packages/smart-forms-renderer/package.json | 2 +- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3c279366..a442c284a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,6 +140,81 @@ "yui-lint": "^0.2.0" } }, + "apps/smart-forms-app/node_modules/@aehrc/smart-forms-renderer": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@aehrc/smart-forms-renderer/-/smart-forms-renderer-0.12.1.tgz", + "integrity": "sha512-x4PdZuTPsP48z+X9Q1Ggc6ysGuWKY2mqFb4uBCx5dznFWq+1Xbazoz+FoIPvR/vthFhq9Tq1HsH5aOSmt30Zfg==", + "dependencies": { + "@iconify/react": "^4.1.1", + "@types/fhir": "^0.0.38", + "dayjs": "^1.11.10", + "deep-diff": "^1.0.2", + "fhirclient": "^2.5.2", + "fhirpath": "^3.7.1", + "html-react-parser": "4.2.10", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "nanoid": "^5.0.1", + "react-beautiful-dnd": "^13.1.1", + "react-markdown": "^8.0.7", + "zustand": "^4.4.6" + }, + "peerDependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.10", + "@mui/lab": "^5.0.0-alpha.165", + "@mui/material": "^5.15.10", + "@mui/x-date-pickers": "^6.19.4", + "@tanstack/react-query": "^4.36.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "apps/smart-forms-app/node_modules/@aehrc/smart-forms-renderer/node_modules/html-react-parser": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-4.2.10.tgz", + "integrity": "sha512-JyKZVQ+kQ8PdycISwkuLbEEvV/k4hWhU6cb6TT7yGaYwdqA7cPt4VRYXkCZcix2vlQtgDBSMJUmPI2jpNjPGvg==", + "dependencies": { + "domhandler": "5.0.3", + "html-dom-parser": "5.0.3", + "react-property": "2.0.2", + "style-to-js": "1.1.8" + }, + "peerDependencies": { + "react": "0.14 || 15 || 16 || 17 || 18" + } + }, + "apps/smart-forms-app/node_modules/html-dom-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.3.tgz", + "integrity": "sha512-slsc6ipw88OUZjAayRs5NTmfOQCwcUa3hNyk6AdsbQxY09H5Lr1Y3CZ4ZlconMKql3Ga6sWg3HMoUzo7KSItaQ==", + "dependencies": { + "domhandler": "5.0.3", + "htmlparser2": "9.0.0" + } + }, + "apps/smart-forms-app/node_modules/inline-style-parser": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.2.tgz", + "integrity": "sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ==" + }, + "apps/smart-forms-app/node_modules/style-to-js": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.8.tgz", + "integrity": "sha512-bPSspCXkkhETLXnEgDbaoWRWyv3lF2bj32YIc8IElok2IIMHUlZtQUrxYmAkKUNxpluhH0qnKWrmuoXUyTY12g==", + "dependencies": { + "style-to-object": "1.0.3" + } + }, + "apps/smart-forms-app/node_modules/style-to-object": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.3.tgz", + "integrity": "sha512-xOpx7S53E0V3DpVsvt7ySvoiumRpfXiC99PUXLqGB3wiAnN9ybEIpuzlZ8LAZg+h1sl9JkEUwtSQXxcCgFqbbg==", + "dependencies": { + "inline-style-parser": "0.2.2" + } + }, "deployment/ehr-proxy/ehr-proxy-app": { "version": "0.1.0", "dependencies": { @@ -26489,7 +26564,7 @@ }, "packages/smart-forms-renderer": { "name": "@aehrc/smart-forms-renderer", - "version": "0.12.1", + "version": "0.13.0", "license": "Apache-2.0", "dependencies": { "@iconify/react": "^4.1.1", diff --git a/packages/smart-forms-renderer/package.json b/packages/smart-forms-renderer/package.json index 4dd4726d8..c0e792284 100644 --- a/packages/smart-forms-renderer/package.json +++ b/packages/smart-forms-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@aehrc/smart-forms-renderer", - "version": "0.12.1", + "version": "0.13.0", "description": "FHIR Structured Data Captured (SDC) rendering engine for Smart Forms", "main": "lib/index.js", "scripts": { From 2de64fd438a63f6ae03e4f3a8cabd4728f72ff6c Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 19 Feb 2024 17:02:30 +1030 Subject: [PATCH 07/10] Remove console log statements --- apps/smart-forms-app/vite.config.ts | 9 +++++---- .../src/components/Renderer/BaseRenderer.tsx | 6 ------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/smart-forms-app/vite.config.ts b/apps/smart-forms-app/vite.config.ts index 3436febf3..0a746fbdd 100644 --- a/apps/smart-forms-app/vite.config.ts +++ b/apps/smart-forms-app/vite.config.ts @@ -6,12 +6,13 @@ import svgr from 'vite-plugin-svgr'; export default defineConfig({ plugins: [react(), svgr()], optimizeDeps: { - include: ['@aehrc/sdc-assemble', '@aehrc/sdc-populate'] + include: ['@aehrc/sdc-assemble'] }, build: { commonjsOptions: { - include: [/node_modules/, '@aehrc/sdc-assemble', '@aehrc/sdc-populate'] + include: [/node_modules/, '@aehrc/sdc-assemble'] } - }, - resolve: { preserveSymlinks: true } + } + + // resolve: { preserveSymlinks: true } }); diff --git a/packages/smart-forms-renderer/src/components/Renderer/BaseRenderer.tsx b/packages/smart-forms-renderer/src/components/Renderer/BaseRenderer.tsx index 3157827db..2517caee0 100644 --- a/packages/smart-forms-renderer/src/components/Renderer/BaseRenderer.tsx +++ b/packages/smart-forms-renderer/src/components/Renderer/BaseRenderer.tsx @@ -35,9 +35,6 @@ function BaseRenderer() { const updateRequiredValidity = useQuestionnaireResponseStore.use.validateQuestionnaire(); const updateResponse = useQuestionnaireResponseStore.use.updateResponse(); - const invalidItems = useQuestionnaireResponseStore.use.invalidItems(); - const responseIsValid = useQuestionnaireResponseStore.use.responseIsValid(); - const qItemsIndexMap = useMemo(() => mapQItemsIndex(sourceQuestionnaire), [sourceQuestionnaire]); function handleTopLevelQRItemSingleChange(newTopLevelQRItem: QuestionnaireResponseItem) { @@ -76,9 +73,6 @@ function BaseRenderer() { // If an item has multiple answers, it is a repeat group const topLevelQRItemsByIndex = getQrItemsIndex(topLevelQItems, topLevelQRItems, qItemsIndexMap); - console.log(invalidItems); - console.log(responseIsValid); - return ( From e3f4fcd8c5fb8fcba67961748670c301f7f0f4d1 Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 19 Feb 2024 17:18:16 +1030 Subject: [PATCH 08/10] Reverse commit for vite config --- apps/smart-forms-app/vite.config.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/smart-forms-app/vite.config.ts b/apps/smart-forms-app/vite.config.ts index 0a746fbdd..3436febf3 100644 --- a/apps/smart-forms-app/vite.config.ts +++ b/apps/smart-forms-app/vite.config.ts @@ -6,13 +6,12 @@ import svgr from 'vite-plugin-svgr'; export default defineConfig({ plugins: [react(), svgr()], optimizeDeps: { - include: ['@aehrc/sdc-assemble'] + include: ['@aehrc/sdc-assemble', '@aehrc/sdc-populate'] }, build: { commonjsOptions: { - include: [/node_modules/, '@aehrc/sdc-assemble'] + include: [/node_modules/, '@aehrc/sdc-assemble', '@aehrc/sdc-populate'] } - } - - // resolve: { preserveSymlinks: true } + }, + resolve: { preserveSymlinks: true } }); From b118c0630a30bc1821895e7ece26ea251d57c7b4 Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 19 Feb 2024 17:19:09 +1030 Subject: [PATCH 09/10] Update package.json --- package-lock.json | 2 +- packages/smart-forms-renderer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a442c284a..80e7fb6be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26564,7 +26564,7 @@ }, "packages/smart-forms-renderer": { "name": "@aehrc/smart-forms-renderer", - "version": "0.13.0", + "version": "0.13.1", "license": "Apache-2.0", "dependencies": { "@iconify/react": "^4.1.1", diff --git a/packages/smart-forms-renderer/package.json b/packages/smart-forms-renderer/package.json index c0e792284..e2052cd5a 100644 --- a/packages/smart-forms-renderer/package.json +++ b/packages/smart-forms-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@aehrc/smart-forms-renderer", - "version": "0.13.0", + "version": "0.13.1", "description": "FHIR Structured Data Captured (SDC) rendering engine for Smart Forms", "main": "lib/index.js", "scripts": { From 750ceff2ed496c4e805a6d2de7781b227876da1c Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Tue, 20 Feb 2024 15:02:08 +1030 Subject: [PATCH 10/10] Make required and read-only indicators more subtle --- .../src/components/Box.styles.ts | 32 +------------- .../ItemParts/ItemExtensionLabels.tsx | 42 ------------------- .../ItemParts/ItemFieldGrid.tsx | 20 ++++----- .../ItemParts/ItemLabelText.tsx | 2 +- .../ItemParts/ItemLabelWrapper.tsx | 9 +++- 5 files changed, 17 insertions(+), 88 deletions(-) delete mode 100644 packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemExtensionLabels.tsx diff --git a/packages/smart-forms-renderer/src/components/Box.styles.ts b/packages/smart-forms-renderer/src/components/Box.styles.ts index f1e658250..4db50057c 100644 --- a/packages/smart-forms-renderer/src/components/Box.styles.ts +++ b/packages/smart-forms-renderer/src/components/Box.styles.ts @@ -16,7 +16,7 @@ */ import Box from '@mui/material/Box'; -import { alpha, styled } from '@mui/material/styles'; +import { styled } from '@mui/material/styles'; import FormControlLabel from '@mui/material/FormControlLabel'; export const QGroupContainerBox = styled(Box, { @@ -37,33 +37,3 @@ export const FormTitleWrapper = styled(Box)(() => ({ export const StyledFormControlLabel = styled(FormControlLabel)(() => ({ height: 34 })); - -export const RequiredLabel = styled(Box)(({ theme }) => ({ - height: 12, - minWidth: 20, - lineHeight: 0, - borderRadius: 6, - alignItems: 'center', - whiteSpace: 'nowrap', - justifyContent: 'center', - padding: theme.spacing(1), - fontSize: theme.typography.caption.fontSize, - color: theme.palette.error.dark, - backgroundColor: alpha(theme.palette.error.main, 0.16), - fontWeight: theme.typography.fontWeightBold -})); - -export const ReadOnlyLabel = styled(Box)(({ theme }) => ({ - height: 12, - minWidth: 20, - lineHeight: 0, - borderRadius: 6, - alignItems: 'center', - whiteSpace: 'nowrap', - justifyContent: 'center', - padding: theme.spacing(1), - fontSize: theme.typography.caption.fontSize, - color: theme.palette.text.primary, - backgroundColor: theme.palette.grey['300'], - fontWeight: theme.typography.fontWeightBold -})); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemExtensionLabels.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemExtensionLabels.tsx deleted file mode 100644 index afc98476e..000000000 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemExtensionLabels.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 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 { ReadOnlyLabel, RequiredLabel } from '../../Box.styles'; -import Box from '@mui/material/Box'; - -interface ItemExtensionLabelsProps { - required: boolean; - readOnly: boolean; -} - -function ItemExtensionLabels(props: ItemExtensionLabelsProps) { - const { required, readOnly } = props; - - if (!required && !readOnly) { - return null; - } - - return ( - - {required ? Required : null} - {readOnly ? Read-only : null} - - ); -} - -export default ItemExtensionLabels; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx index 3c6429384..7a1cb005c 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx @@ -21,7 +21,6 @@ import Grid from '@mui/material/Grid'; import type { QuestionnaireItem } from 'fhir/r4'; import DisplayInstructions from '../DisplayItem/DisplayInstructions'; import LabelWrapper from './ItemLabelWrapper'; -import ItemExtensionLabels from './ItemExtensionLabels'; interface ItemFieldGridProps { children: ReactNode; @@ -35,18 +34,15 @@ function ItemFieldGrid(props: ItemFieldGridProps) { const { children, qItem, displayInstructions, required, readOnly } = props; return ( - <> - - - - - - - {children} - - + + + - + + {children} + + + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelText.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelText.tsx index 44e7c787b..81e8b44ea 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelText.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelText.tsx @@ -54,7 +54,7 @@ const ItemLabelText = memo(function ItemLabelText(props: ItemLabelTextProps) { // parse regular text return ( - + {qItem.text} ); 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 56252eb89..2fbb62b6c 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelWrapper.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemLabelWrapper.tsx @@ -21,20 +21,25 @@ import ContextDisplayItem from './ContextDisplayItem'; import type { QuestionnaireItem } from 'fhir/r4'; import { getContextDisplays } from '../../../utils/tabs'; import ItemLabelText from './ItemLabelText'; +import Typography from '@mui/material/Typography'; interface LabelWrapperProps { qItem: QuestionnaireItem; + required: boolean; readOnly: boolean; } function ItemLabelWrapper(props: LabelWrapperProps) { - const { qItem, readOnly } = props; + const { qItem, required, readOnly } = props; const contextDisplayItems = getContextDisplays(qItem); return ( - + + + {required ? * : null} + {contextDisplayItems.map((item) => {