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;
+}