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/package-lock.json b/package-lock.json index c3c279366..80e7fb6be 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.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 4dd4726d8..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.12.1", + "version": "0.13.1", "description": "FHIR Structured Data Captured (SDC) rendering engine for Smart Forms", "main": "lib/index.js", "scripts": { 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 ( - + - + - + - + onInputChange(event.target.value)} disabled={readOnly} label={displayPrompt} 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 825d0ca15..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'; @@ -54,7 +54,9 @@ function IntegerItem(props: IntegerItemProps) { displayPrompt, displayInstructions, entryFormat, + required, regexValidation, + minLength, maxLength } = useRenderingExtensions(qItem); @@ -71,7 +73,7 @@ function IntegerItem(props: IntegerItemProps) { const [value, setValue] = useNumberInput(valueInteger); // Perform validation checks - const feedback = useValidationError(value.toString(), regexValidation, maxLength); + const feedback = useValidationFeedback(value.toString(), regexValidation, minLength, maxLength); // Process calculated expressions const { calcExpUpdated } = useIntegerCalculatedExpression({ @@ -121,7 +123,11 @@ function IntegerItem(props: IntegerItemProps) { return ( - + - + {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) => { 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 ( - + - + - + - + - + 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..098c9d244 100644 --- a/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts +++ b/packages/smart-forms-renderer/src/hooks/useRenderingExtensions.ts @@ -16,8 +16,6 @@ */ import { - getMaxLength, - getReadOnly, getRegexValidation, getTextDisplayInstructions, getTextDisplayPrompt, @@ -26,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; @@ -33,20 +32,27 @@ interface RenderingExtensions { displayInstructions: string; readOnly: boolean; entryFormat: string; + required: boolean; regexValidation: RegexValidation | null; + minLength: number | null; maxLength: number | null; } function useRenderingExtensions(qItem: QuestionnaireItem): RenderingExtensions { - return { - displayUnit: getTextDisplayUnit(qItem), - displayPrompt: getTextDisplayPrompt(qItem), - displayInstructions: getTextDisplayInstructions(qItem), - readOnly: getReadOnly(qItem), - entryFormat: structuredDataCapture.getEntryFormat(qItem) ?? '', - regexValidation: getRegexValidation(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/useValidationFeedback.ts similarity index 53% rename from packages/smart-forms-renderer/src/hooks/useValidationError.ts rename to packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts index c492e4537..d0638ec59 100644 --- a/packages/smart-forms-renderer/src/hooks/useValidationError.ts +++ b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts @@ -16,33 +16,35 @@ */ import type { RegexValidation } from '../interfaces/regex.interface'; +import { getInputInvalidType } from '../utils/validateQuestionnaire'; -function useValidationError( +function useValidationFeedback( input: string, regexValidation: RegexValidation | null, + minLength: number | null, maxLength: number | null ): string { - let feedback = ''; - - if (input) { - // Test regex - if (regexValidation) { - if (!regexValidation.expression.test(input)) { - feedback = - regexValidation.feedback ?? - `Input should match the specified regex ${regexValidation.expression}`; - } - } - - // Test max character limit - if (maxLength) { - if (input.length > maxLength) { - feedback = 'Input exceeds maximum character limit.'; - } - } + 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 feedback; + return ''; } -export default useValidationError; +export default useValidationFeedback; diff --git a/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts b/packages/smart-forms-renderer/src/stores/questionnaireResponseStore.ts index d3d5d6866..f11b6ca03 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/validateQuestionnaire'; +import { validateQuestionnaire } from '../utils/validateQuestionnaire'; +import { questionnaireStore } from './questionnaireStore'; interface QuestionnaireResponseStoreType { sourceResponse: QuestionnaireResponse; updatableResponse: QuestionnaireResponse; formChangesHistory: (Diff[] | null)[]; + invalidItems: Record; + responseIsValid: boolean; + validateQuestionnaire: ( + questionnaire: Questionnaire, + updatedResponse: QuestionnaireResponse + ) => void; buildSourceResponse: (response: QuestionnaireResponse) => void; setUpdatableResponseAsPopulated: (populatedResponse: QuestionnaireResponse) => void; updateResponse: (updatedResponse: QuestionnaireResponse) => void; @@ -40,6 +49,32 @@ export const questionnaireResponseStore = createStore { + const tempInvalidItems = get().invalidItems; + + const enableWhenIsActivated = questionnaireStore.getState().enableWhenIsActivated; + const enableWhenItems = questionnaireStore.getState().enableWhenItems; + const enableWhenExpressions = questionnaireStore.getState().enableWhenExpressions; + + validateQuestionnaire({ + questionnaire, + questionnaireResponse: updatedResponse, + invalidItems: tempInvalidItems, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }); + + set(() => ({ + invalidItems: tempInvalidItems, + responseIsValid: Object.keys(tempInvalidItems).length === 0 + })); + }, buildSourceResponse: (questionnaireResponse: QuestionnaireResponse) => { set(() => ({ sourceResponse: questionnaireResponse, diff --git a/packages/smart-forms-renderer/src/utils/itemControl.ts b/packages/smart-forms-renderer/src/utils/itemControl.ts index 4fd03bcae..f928293ea 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 * @@ -259,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 * @@ -332,26 +295,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. @@ -388,12 +331,3 @@ export function getRegexValidation(qItem: QuestionnaireItem): RegexValidation | 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/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 */ diff --git a/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts b/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts new file mode 100644 index 000000000..88d12d8c3 --- /dev/null +++ b/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts @@ -0,0 +1,273 @@ +/* + * 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, 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'; + +interface ValidateQuestionnaireParams { + questionnaire: Questionnaire; + questionnaireResponse: QuestionnaireResponse; + invalidItems: Record; + 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 validateQuestionnaire( + params: ValidateQuestionnaireParams +): Record { + const { + questionnaire, + questionnaireResponse, + invalidItems, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + } = params; + + if ( + !questionnaire.item || + questionnaire.item.length === 0 || + !questionnaireResponse.item || + questionnaireResponse.item.length === 0 + ) { + return invalidItems; + } + + 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 + }; + } + + validateItemRecursive({ + qItem: topLevelQItem, + qrItem: topLevelQRItem, + invalidItems: invalidItems, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }); + } + + return invalidItems; +} + +interface ValidateItemRecursiveParams { + qItem: QuestionnaireItem; + qrItem: QuestionnaireResponseItem; + invalidItems: Record; + enableWhenIsActivated: boolean; + enableWhenItems: EnableWhenItems; + enableWhenExpressions: Record; +} + +function validateItemRecursive(params: ValidateItemRecursiveParams) { + const { + qItem, + qrItem, + invalidItems, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + } = params; + + if ( + isHidden({ + questionnaireItem: qItem, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }) + ) { + return; + } + + // FIXME repeat groups not working + if (qItem.type === 'group' && qItem.repeats) { + return validateRepeatGroup(qItem, qrItem, invalidItems); + } + + 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) { + invalidItems[qItem.linkId] = 'required'; + } + } + + 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 + }; + } + + validateItemRecursive({ + qItem: childQItem, + qrItem: childQRItem, + invalidItems: invalidItems, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }); + } + } + + validateSingleItem(qItem, qrItem, invalidItems); +} + +function validateSingleItem( + qItem: QuestionnaireItem, + qrItem: QuestionnaireResponseItem, + invalidItems: Record +) { + // Validate item.required + if (qItem.type !== 'display') { + if (qItem.required && !qrItem.answer) { + 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 validateRepeatGroup( + qItem: QuestionnaireItem, + qrItems: QuestionnaireResponseItem, + 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; +}