From 478dd25622607f309e1ea056e3a09df746c3f406 Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Wed, 26 Feb 2025 18:26:49 +1030 Subject: [PATCH] - Add support for required-based validation to all item types - Implement feedbackFromParent to manually pass feedback from a wrapper of a single item - Update renderer to v1.0.0-alpha.24 --- package-lock.json | 2 +- packages/smart-forms-renderer/package.json | 2 +- .../AttachmentItem/AttachmentField.tsx | 5 ++ .../AttachmentItem/AttachmentFieldWrapper.tsx | 4 + .../AttachmentItem/AttachmentItem.tsx | 23 +++++- .../BooleanItem/BooleanField.tsx | 7 +- .../BooleanItem/BooleanItem.tsx | 16 +++- .../ChoiceItems/ChoiceAutocompleteItem.tsx | 26 +++--- .../ChoiceCheckboxAnswerOptionFields.tsx | 25 +++--- .../ChoiceCheckboxAnswerOptionItem.tsx | 14 +++- .../ChoiceCheckboxAnswerValueSetFields.tsx | 25 +++--- .../ChoiceCheckboxAnswerValueSetItem.tsx | 14 +++- .../ChoiceItems/ChoiceItemSwitcher.tsx | 12 ++- .../ChoiceRadioAnswerOptionFields.tsx | 8 +- .../ChoiceRadioAnswerOptionItem.tsx | 7 +- .../ChoiceRadioAnswerValueSetFields.tsx | 8 +- .../ChoiceRadioAnswerValueSetItem.tsx | 8 +- .../ChoiceSelectAnswerOptionFields.tsx | 6 +- .../ChoiceSelectAnswerOptionItem.tsx | 7 +- .../ChoiceSelectAnswerValueSetFields.tsx | 75 +++++++++-------- .../ChoiceSelectAnswerValueSetItem.tsx | 15 +++- .../CustomDateItem/CustomDateItem.tsx | 4 +- .../CustomDateTimeItem/CustomDateTimeItem.tsx | 4 +- .../DecimalItem/DecimalItem.tsx | 9 ++- .../IntegerItem/IntegerItem.tsx | 7 +- .../components/FormComponents/Item.styles.ts | 7 ++ .../OpenChoiceAutocompleteItem.tsx | 26 +++--- .../OpenChoiceCheckboxAnswerOptionFields.tsx | 42 +++++----- .../OpenChoiceCheckboxAnswerOptionItem.tsx | 12 ++- ...OpenChoiceCheckboxAnswerValueSetFields.tsx | 42 +++++----- .../OpenChoiceCheckboxAnswerValueSetItem.tsx | 12 ++- .../OpenChoiceItemSwitcher.tsx | 12 ++- .../OpenChoiceRadioAnswerOptionFields.tsx | 46 ++++++----- .../OpenChoiceRadioAnswerOptionItem.tsx | 12 ++- .../OpenChoiceRadioAnswerValueSetFields.tsx | 46 ++++++----- .../OpenChoiceRadioAnswerValueSetItem.tsx | 12 ++- .../OpenChoiceSelectAnswerOptionField.tsx | 81 +++++++++++-------- .../OpenChoiceSelectAnswerOptionItem.tsx | 11 ++- .../OpenChoiceSelectAnswerValueSetField.tsx | 5 ++ .../OpenChoiceSelectAnswerValueSetItem.tsx | 11 ++- .../QuantityItem/QuantityItem.tsx | 7 +- .../FormComponents/SingleItem/SingleItem.tsx | 6 +- .../SingleItem/SingleItemSwitcher.tsx | 21 ++++- .../SingleItem/SingleItemView.tsx | 8 +- .../FormComponents/SliderItem/SliderField.tsx | 49 ++++++----- .../FormComponents/SliderItem/SliderItem.tsx | 23 +++++- .../FormComponents/StringItem/StringItem.tsx | 7 +- .../FormComponents/TextItem/TextItem.tsx | 17 +++- .../FormComponents/TimeItem/TimeItem.tsx | 4 +- .../FormComponents/UrlItem/UrlItem.tsx | 7 +- .../src/hooks/useValidationFeedback.ts | 11 ++- .../interfaces/overrideComponent.interface.ts | 1 + .../src/interfaces/renderProps.interface.ts | 4 + 53 files changed, 596 insertions(+), 279 deletions(-) diff --git a/package-lock.json b/package-lock.json index 167ad38c7..9e47336ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40776,7 +40776,7 @@ }, "packages/smart-forms-renderer": { "name": "@aehrc/smart-forms-renderer", - "version": "1.0.0-alpha.23", + "version": "1.0.0-alpha.24", "license": "Apache-2.0", "dependencies": { "@aehrc/sdc-populate": "^3.0.1", diff --git a/packages/smart-forms-renderer/package.json b/packages/smart-forms-renderer/package.json index f9aebd43c..7a29003c1 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": "1.0.0-alpha.23", + "version": "1.0.0-alpha.24", "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/AttachmentItem/AttachmentField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx index e49a35c49..3f98dddff 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx @@ -25,10 +25,12 @@ import Stack from '@mui/material/Stack'; import type { AttachmentValues } from './AttachmentItem'; import AttachmentUrlField from './AttachmentUrlField'; import { useRendererStylingStore } from '../../../stores'; +import { StyledRequiredTypography } from '../Item.styles'; interface AttachmentFieldProps extends PropsWithIsTabledAttribute { linkId: string; attachmentValues: AttachmentValues; + feedback: string; readOnly: boolean; onUploadFile: (file: File | null) => void; onUrlChange: (url: string) => void; @@ -39,6 +41,7 @@ function AttachmentField(props: AttachmentFieldProps) { const { linkId, attachmentValues, + feedback, readOnly, isTabled, onUploadFile, @@ -96,6 +99,8 @@ function AttachmentField(props: AttachmentFieldProps) { ) : null} + + {feedback ? {feedback} : null} ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx index 0056ac190..a43ab833a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx @@ -33,6 +33,7 @@ interface AttachmentFieldWrapperProps PropsWithIsTabledAttribute { qItem: QuestionnaireItem; attachmentValues: AttachmentValues; + feedback: string; readOnly: boolean; onUploadFile: (file: File | null) => void; onUrlChange: (url: string) => void; @@ -43,6 +44,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) { const { qItem, attachmentValues, + feedback, readOnly, isRepeated, isTabled, @@ -58,6 +60,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) { - {feedback ? ( - {feedback} - ) : null} + {feedback ? {feedback} : null} ); }); 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 365a40e25..1aa3ebbac 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx @@ -17,6 +17,7 @@ import React from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -40,20 +41,29 @@ interface BooleanItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } function BooleanItem(props: BooleanItemProps) { - const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; + const { + qItem, + qrItem, + isRepeated, + isTabled, + parentIsReadOnly, + feedbackFromParent, + onQrItemChange + } = props; const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, ''); + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); // Init input value const answerKey = qrItem?.answer?.[0].id; 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 769420039..a47a621cd 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx @@ -23,6 +23,7 @@ import { FullWidthFormComponentBox } from '../../Box.styles'; import useDebounce from '../../../hooks/useDebounce'; import useTerminologyServerQuery from '../../../hooks/useTerminologyServerQuery'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -35,13 +36,15 @@ import ChoiceAutocompleteField from './ChoiceAutocompleteField'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import { useQuestionnaireStore } from '../../../stores'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface ChoiceAutocompleteItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, - PropsWithRenderingExtensionsAttribute { + PropsWithRenderingExtensionsAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -54,6 +57,7 @@ function ChoiceAutocompleteItem(props: ChoiceAutocompleteItemProps) { isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -68,19 +72,23 @@ function ChoiceAutocompleteItem(props: ChoiceAutocompleteItemProps) { valueCoding = qrChoice.answer[0].valueCoding; } - const readOnly = useReadOnly(qItem, parentIsReadOnly); - const maxList = 10; const [input, setInput] = useState(''); const debouncedInput = useDebounce(input, AUTOCOMPLETE_DEBOUNCE_DURATION); - const { options, loading, feedback } = useTerminologyServerQuery( - qItem, - maxList, - input, - debouncedInput - ); + const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const validationFeedback = useValidationFeedback(qItem, feedbackFromParent, ''); + + const { + options, + loading, + feedback: terminologyFeedback + } = useTerminologyServerQuery(qItem, maxList, input, debouncedInput); + + const feedback = terminologyFeedback ?? { message: validationFeedback, color: 'error' }; if (!qItem.answerValueSet) { return null; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx index 95f861011..88c886b39 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx @@ -24,31 +24,36 @@ import type { import { getChoiceOrientation } from '../../../utils/choice'; import { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import CheckboxOptionList from './CheckboxOptionList'; -import { StyledFormGroup } from '../Item.styles'; +import { StyledFormGroup, StyledRequiredTypography } from '../Item.styles'; interface ChoiceCheckboxAnswerOptionFieldsProps { qItem: QuestionnaireItem; options: QuestionnaireItemAnswerOption[]; answers: QuestionnaireResponseItemAnswer[]; + feedback: string; readOnly: boolean; onCheckedChange: (newValue: string) => void; } function ChoiceCheckboxAnswerOptionFields(props: ChoiceCheckboxAnswerOptionFieldsProps) { - const { qItem, options, answers, readOnly, onCheckedChange } = props; + const { qItem, options, answers, feedback, readOnly, onCheckedChange } = props; const orientation = getChoiceOrientation(qItem) ?? ChoiceItemOrientation.Vertical; if (options.length > 0) { return ( - - - + <> + + + + + {feedback ? {feedback} : null} + ); } 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 397855d2b..113d4ef6f 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx @@ -21,6 +21,7 @@ import { createEmptyQrItem } from '../../../utils/qrItem'; import { updateChoiceCheckboxAnswers } from '../../../utils/choice'; import { FullWidthFormComponentBox } from '../../Box.styles'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler, @@ -33,13 +34,15 @@ import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import { useQuestionnaireStore } from '../../../stores'; import ChoiceCheckboxAnswerOptionFields from './ChoiceCheckboxAnswerOptionFields'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface ChoiceCheckboxAnswerOptionItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithRenderingExtensionsAttribute, PropsWithShowMinimalViewAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -52,6 +55,7 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro renderingExtensions, showMinimalView = false, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -62,13 +66,17 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro const qrChoiceCheckbox = qrItem ?? createEmptyQrItem(qItem, answerKey); const answers = qrChoiceCheckbox.answer ?? []; - const readOnly = useReadOnly(qItem, parentIsReadOnly); const { displayInstructions } = renderingExtensions; // TODO Process calculated expressions // This requires its own hook, because in the case of multi-select, we need to check if the value is already checked to prevent an infinite loop // This will be done after the choice/open-choice refactoring + const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + const options = qItem.answerOption ?? []; // Event handlers @@ -99,6 +107,7 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro qItem={qItem} options={options} answers={answers} + feedback={feedback} readOnly={readOnly} onCheckedChange={handleCheckedChange} /> @@ -121,6 +130,7 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro qItem={qItem} options={options} answers={answers} + feedback={feedback} readOnly={readOnly} onCheckedChange={handleCheckedChange} /> diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx index 9f8919c96..b0676a38b 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx @@ -28,32 +28,37 @@ import type { TerminologyError } from '../../../hooks/useValueSetCodings'; import { getChoiceOrientation } from '../../../utils/choice'; import { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import CheckboxOptionList from './CheckboxOptionList'; -import { StyledFormGroup } from '../Item.styles'; +import { StyledFormGroup, StyledRequiredTypography } from '../Item.styles'; interface ChoiceCheckboxAnswerValueSetFieldsProps { qItem: QuestionnaireItem; options: QuestionnaireItemAnswerOption[]; answers: QuestionnaireResponseItemAnswer[]; + feedback: string; readOnly: boolean; terminologyError: TerminologyError; onCheckedChange: (newValue: string) => void; } function ChoiceCheckboxAnswerValueSetFields(props: ChoiceCheckboxAnswerValueSetFieldsProps) { - const { qItem, options, answers, readOnly, terminologyError, onCheckedChange } = props; + const { qItem, options, answers, feedback, readOnly, terminologyError, onCheckedChange } = props; const orientation = getChoiceOrientation(qItem) ?? ChoiceItemOrientation.Vertical; if (options.length > 0) { return ( - - - + <> + + + + + {feedback ? {feedback} : null} + ); } 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 11cdbdb58..68ec1a54e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx @@ -22,6 +22,7 @@ import useValueSetCodings from '../../../hooks/useValueSetCodings'; import { convertCodingsToAnswerOptions, updateChoiceCheckboxAnswers } from '../../../utils/choice'; import { FullWidthFormComponentBox } from '../../Box.styles'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler, @@ -34,13 +35,15 @@ import useReadOnly from '../../../hooks/useReadOnly'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import { useQuestionnaireStore } from '../../../stores'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface ChoiceCheckboxAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithRenderingExtensionsAttribute, PropsWithShowMinimalViewAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; showText?: boolean; @@ -54,6 +57,7 @@ function ChoiceCheckboxAnswerValueSetItem(props: ChoiceCheckboxAnswerValueSetIte renderingExtensions, showMinimalView = false, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -64,9 +68,13 @@ function ChoiceCheckboxAnswerValueSetItem(props: ChoiceCheckboxAnswerValueSetIte const qrChoiceCheckbox = qrItem ?? createEmptyQrItem(qItem, answerKey); const answers = qrChoiceCheckbox.answer ?? []; - const readOnly = useReadOnly(qItem, parentIsReadOnly); const { displayInstructions } = renderingExtensions; + const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + // Get codings/options from valueSet const { codings, terminologyError } = useValueSetCodings(qItem); @@ -104,6 +112,7 @@ function ChoiceCheckboxAnswerValueSetItem(props: ChoiceCheckboxAnswerValueSetIte qItem={qItem} options={options} answers={answers} + feedback={feedback} readOnly={readOnly} terminologyError={terminologyError} onCheckedChange={handleCheckedChange} @@ -127,6 +136,7 @@ function ChoiceCheckboxAnswerValueSetItem(props: ChoiceCheckboxAnswerValueSetIte qItem={qItem} options={options} answers={answers} + feedback={feedback} readOnly={readOnly} terminologyError={terminologyError} onCheckedChange={handleCheckedChange} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceItemSwitcher.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceItemSwitcher.tsx index 8984a2ddb..6a7248d71 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceItemSwitcher.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceItemSwitcher.tsx @@ -27,6 +27,7 @@ import { getChoiceControlType } from '../../../utils/choice'; import ChoiceRadioAnswerValueSetItem from './ChoiceRadioAnswerValueSetItem'; import ChoiceCheckboxAnswerValueSetItem from './ChoiceCheckboxAnswerValueSetItem'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -42,7 +43,8 @@ interface ChoiceItemSwitcherProps PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, PropsWithShowMinimalViewAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -56,6 +58,7 @@ function ChoiceItemSwitcher(props: ChoiceItemSwitcherProps) { renderingExtensions, showMinimalView, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -72,6 +75,7 @@ function ChoiceItemSwitcher(props: ChoiceItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -83,6 +87,7 @@ function ChoiceItemSwitcher(props: ChoiceItemSwitcherProps) { isRepeated={isRepeated} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -97,6 +102,7 @@ function ChoiceItemSwitcher(props: ChoiceItemSwitcherProps) { renderingExtensions={renderingExtensions} showMinimalView={showMinimalView} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -109,6 +115,7 @@ function ChoiceItemSwitcher(props: ChoiceItemSwitcherProps) { renderingExtensions={renderingExtensions} showMinimalView={showMinimalView} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -122,6 +129,7 @@ function ChoiceItemSwitcher(props: ChoiceItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -135,6 +143,7 @@ function ChoiceItemSwitcher(props: ChoiceItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -147,6 +156,7 @@ function ChoiceItemSwitcher(props: ChoiceItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx index 65fccf060..936cae7df 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx @@ -19,13 +19,12 @@ import React from 'react'; import { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; import RadioOptionList from '../ItemParts/RadioOptionList'; -import { StyledRadioGroup } from '../Item.styles'; +import { StyledRadioGroup, StyledRequiredTypography } from '../Item.styles'; import { getChoiceOrientation } from '../../../utils/choice'; import Box from '@mui/material/Box'; import FadingCheckIcon from '../ItemParts/FadingCheckIcon'; import ClearInputButton from '../ItemParts/ClearInputButton'; import { useRendererStylingStore } from '../../../stores'; -import Typography from '@mui/material/Typography'; interface ChoiceRadioAnswerOptionFieldsProps { qItem: QuestionnaireItem; @@ -85,9 +84,8 @@ function ChoiceRadioAnswerOptionFields(props: ChoiceRadioAnswerOptionFieldsProps )} - {feedback ? ( - {feedback} - ) : null} + + {feedback ? {feedback} : null} ); } 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 f51f9aa82..e36684ffb 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx @@ -24,6 +24,7 @@ import type { import { findInAnswerOptions, getChoiceControlType, getQrChoiceValue } from '../../../utils/choice'; import { createEmptyQrItem } from '../../../utils/qrItem'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -44,7 +45,8 @@ interface ChoiceRadioAnswerOptionItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -57,6 +59,7 @@ function ChoiceRadioAnswerOptionItem(props: ChoiceRadioAnswerOptionItemProps) { isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -70,7 +73,7 @@ function ChoiceRadioAnswerOptionItem(props: ChoiceRadioAnswerOptionItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, ''); + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); const options = qItem.answerOption ?? []; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx index 49ca89c43..fb918df22 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx @@ -19,7 +19,7 @@ import React from 'react'; import Typography from '@mui/material/Typography'; import { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; -import { StyledRadioGroup } from '../Item.styles'; +import { StyledRadioGroup, StyledRequiredTypography } from '../Item.styles'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import { StyledAlert } from '../../Alert.styles'; import type { TerminologyError } from '../../../hooks/useValueSetCodings'; @@ -92,11 +92,7 @@ function ChoiceRadioAnswerValueSetFields(props: ChoiceRadioAnswerValueSetFieldsP )} - {feedback ? ( - - {feedback} - - ) : null} + {feedback ? {feedback} : null} ); } 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 27bec2625..87863248a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx @@ -22,6 +22,7 @@ import { createEmptyQrItem } from '../../../utils/qrItem'; import { FullWidthFormComponentBox } from '../../Box.styles'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler, @@ -39,13 +40,14 @@ interface ChoiceRadioAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } function ChoiceRadioAnswerValueSetItem(props: ChoiceRadioAnswerValueSetItemProps) { - const { qItem, qrItem, isRepeated, parentIsReadOnly, onQrItemChange } = props; + const { qItem, qrItem, isRepeated, parentIsReadOnly, feedbackFromParent, onQrItemChange } = props; const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); @@ -67,7 +69,7 @@ function ChoiceRadioAnswerValueSetItem(props: ChoiceRadioAnswerValueSetItemProps const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, ''); + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); const options = useMemo(() => convertCodingsToAnswerOptions(codings), [codings]); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx index eb8010232..4479089b7 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx @@ -28,6 +28,7 @@ import { getAnswerOptionLabel } from '../../../utils/openChoice'; import { compareAnswerOptionValue } from '../../../utils/choice'; import { useRendererStylingStore } from '../../../stores'; import Typography from '@mui/material/Typography'; +import { StyledRequiredTypography } from '../Item.styles'; interface ChoiceSelectAnswerOptionFieldsProps extends PropsWithIsTabledAttribute, @@ -94,9 +95,8 @@ function ChoiceSelectAnswerOptionFields(props: ChoiceSelectAnswerOptionFieldsPro /> )} /> - {feedback ? ( - {feedback} - ) : null} + + {feedback ? {feedback} : null} ); } 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 754c43404..4dad44fec 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx @@ -25,6 +25,7 @@ import type { import { findInAnswerOptions, getQrChoiceValue } from '../../../utils/choice'; import { createEmptyQrItem } from '../../../utils/qrItem'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -43,7 +44,8 @@ interface ChoiceSelectAnswerOptionItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -56,6 +58,7 @@ function ChoiceSelectAnswerOptionItem(props: ChoiceSelectAnswerOptionItemProps) isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -64,7 +67,7 @@ function ChoiceSelectAnswerOptionItem(props: ChoiceSelectAnswerOptionItemProps) const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, ''); + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); // Init input value const answerKey = qrItem?.answer?.[0].id; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx index ffd5b9140..dd6eca7ef 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx @@ -29,6 +29,7 @@ import type { import type { TerminologyError } from '../../../hooks/useValueSetCodings'; import FadingCheckIcon from '../ItemParts/FadingCheckIcon'; import { useRendererStylingStore } from '../../../stores'; +import { StyledRequiredTypography } from '../Item.styles'; interface ChoiceSelectAnswerValueSetFieldsProps extends PropsWithIsTabledAttribute, @@ -37,6 +38,7 @@ interface ChoiceSelectAnswerValueSetFieldsProps codings: Coding[]; valueCoding: Coding | null; terminologyError: TerminologyError; + feedback: string; readOnly: boolean; calcExpUpdated: boolean; onSelectChange: (newValue: Coding | null) => void; @@ -48,6 +50,7 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField codings, valueCoding, terminologyError, + feedback, readOnly, calcExpUpdated, isTabled, @@ -61,40 +64,44 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField if (codings.length > 0) { return ( - option.display ?? `${option.code}`} - value={valueCoding ?? null} - onChange={(_, newValue) => onSelectChange(newValue)} - openOnFocus - autoHighlight - sx={{ maxWidth: !isTabled ? textFieldWidth : 3000, minWidth: 160, flexGrow: 1 }} - size="small" - disabled={readOnly} - renderInput={(params) => ( - - {params.InputProps.endAdornment} - - - {displayUnit} - - - ) - }} - data-test="q-item-choice-select-answer-value-set-field" - /> - )} - /> + <> + option.display ?? `${option.code}`} + value={valueCoding ?? null} + onChange={(_, newValue) => onSelectChange(newValue)} + openOnFocus + autoHighlight + sx={{ maxWidth: !isTabled ? textFieldWidth : 3000, minWidth: 160, flexGrow: 1 }} + size="small" + disabled={readOnly} + renderInput={(params) => ( + + {params.InputProps.endAdornment} + + + {displayUnit} + + + ) + }} + data-test="q-item-choice-select-answer-value-set-field" + /> + )} + /> + + {feedback ? {feedback} : null} + ); } 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 38ab2182c..23818d56c 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx @@ -22,6 +22,7 @@ import { createEmptyQrItem } from '../../../utils/qrItem'; import { FullWidthFormComponentBox } from '../../Box.styles'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -35,13 +36,15 @@ import { useQuestionnaireStore } from '../../../stores'; import useCodingCalculatedExpression from '../../../hooks/useCodingCalculatedExpression'; import { convertCodingsToAnswerOptions, findInAnswerOptions } from '../../../utils/choice'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface ChoiceSelectAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -54,13 +57,12 @@ function ChoiceSelectAnswerValueSetItem(props: ChoiceSelectAnswerValueSetItemPro isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); - const readOnly = useReadOnly(qItem, parentIsReadOnly); - // Init input value const answerKey = qrItem?.answer?.[0].id; const qrChoiceSelect = qrItem ?? createEmptyQrItem(qItem, answerKey); @@ -70,6 +72,11 @@ function ChoiceSelectAnswerValueSetItem(props: ChoiceSelectAnswerValueSetItemPro valueCoding = qrChoiceSelect.answer[0].valueCoding ?? null; } + const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + // Get codings/options from valueSet const { codings, terminologyError } = useValueSetCodings(qItem); @@ -134,6 +141,7 @@ function ChoiceSelectAnswerValueSetItem(props: ChoiceSelectAnswerValueSetItemPro codings={codings} valueCoding={valueCoding} terminologyError={terminologyError} + feedback={feedback} readOnly={readOnly} calcExpUpdated={calcExpUpdated} isTabled={isTabled} @@ -157,6 +165,7 @@ function ChoiceSelectAnswerValueSetItem(props: ChoiceSelectAnswerValueSetItemPro codings={codings} valueCoding={valueCoding} terminologyError={terminologyError} + feedback={feedback} readOnly={readOnly} calcExpUpdated={calcExpUpdated} isTabled={isTabled} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateItem.tsx index cb2921c30..2740724d3 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateItem.tsx @@ -17,6 +17,7 @@ import React, { useState } from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -43,7 +44,8 @@ interface CustomDateItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, - PropsWithRenderingExtensionsAttribute { + PropsWithRenderingExtensionsAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx index 5e8aab24a..61a12fa60 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx @@ -17,6 +17,7 @@ import React, { useState } from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -50,7 +51,8 @@ interface CustomDateTimeItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx index 16c7ed878..929a4701d 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx @@ -17,6 +17,7 @@ import React, { useCallback, useState } from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -47,7 +48,8 @@ interface DecimalItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -60,6 +62,7 @@ function DecimalItem(props: DecimalItemProps) { isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -88,8 +91,8 @@ function DecimalItem(props: DecimalItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); - // Perform validation checks - const feedback = useValidationFeedback(qItem, input); + // Perform validation checks - there's no string-based input here + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); // Process calculated expressions const { calcExpUpdated } = useDecimalCalculatedExpression({ 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 181a1d4f9..0c7e7bf1d 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx @@ -17,6 +17,7 @@ import React, { useCallback, useState } from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -42,7 +43,8 @@ interface IntegerItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -55,6 +57,7 @@ function IntegerItem(props: IntegerItemProps) { isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -83,7 +86,7 @@ function IntegerItem(props: IntegerItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, input); + const feedback = useValidationFeedback(qItem, feedbackFromParent, input); // Process calculated expressions const { calcExpUpdated } = useIntegerCalculatedExpression({ diff --git a/packages/smart-forms-renderer/src/components/FormComponents/Item.styles.ts b/packages/smart-forms-renderer/src/components/FormComponents/Item.styles.ts index 2c6286ac8..33206b6be 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/Item.styles.ts +++ b/packages/smart-forms-renderer/src/components/FormComponents/Item.styles.ts @@ -18,6 +18,7 @@ import FormGroup from '@mui/material/FormGroup'; import RadioGroup from '@mui/material/RadioGroup'; import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; export const StyledFormGroup = styled(FormGroup)(() => ({ marginBottom: 4 @@ -26,3 +27,9 @@ export const StyledFormGroup = styled(FormGroup)(() => ({ export const StyledRadioGroup = styled(RadioGroup)(() => ({ marginBottom: 4 })); + +export const StyledRequiredTypography = styled(Typography)(({ theme }) => ({ + color: theme.palette.error.main, + fontSize: '0.75rem', + marginTop: 4 +})); 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 ccf7e5ec5..827feaa5a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx @@ -24,6 +24,7 @@ import { FullWidthFormComponentBox } from '../../Box.styles'; import useDebounce from '../../../hooks/useDebounce'; import useTerminologyServerQuery from '../../../hooks/useTerminologyServerQuery'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -36,13 +37,15 @@ import useReadOnly from '../../../hooks/useReadOnly'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import { useQuestionnaireStore } from '../../../stores'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface OpenChoiceAutocompleteItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -54,14 +57,13 @@ function OpenChoiceAutocompleteItem(props: OpenChoiceAutocompleteItemProps) { isRepeated, isTabled, renderingExtensions, + feedbackFromParent, parentIsReadOnly, onQrItemChange } = props; const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); - const readOnly = useReadOnly(qItem, parentIsReadOnly); - const answerKey = qrItem?.answer?.[0].id; const qrOpenChoice = qrItem ?? createEmptyQrItem(qItem, answerKey); @@ -81,12 +83,18 @@ function OpenChoiceAutocompleteItem(props: OpenChoiceAutocompleteItemProps) { const [input, setInput] = useState(''); const debouncedInput = useDebounce(input, AUTOCOMPLETE_DEBOUNCE_DURATION); - const { options, loading, feedback } = useTerminologyServerQuery( - qItem, - maxList, - input, - debouncedInput - ); + const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const validationFeedback = useValidationFeedback(qItem, feedbackFromParent, ''); + + const { + options, + loading, + feedback: terminologyFeedback + } = useTerminologyServerQuery(qItem, maxList, input, debouncedInput); + + const feedback = terminologyFeedback ?? { message: validationFeedback, color: 'error' }; if (!qItem.answerValueSet) { return null; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx index edbbb2552..8a16549a3 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx @@ -24,7 +24,7 @@ import type { QuestionnaireResponseItemAnswer } from 'fhir/r4'; import { getChoiceOrientation } from '../../../utils/choice'; -import { StyledFormGroup } from '../Item.styles'; +import { StyledFormGroup, StyledRequiredTypography } from '../Item.styles'; import CheckboxOptionList from '../ChoiceItems/CheckboxOptionList'; interface OpenChoiceCheckboxAnswerOptionFieldsProps { @@ -34,6 +34,7 @@ interface OpenChoiceCheckboxAnswerOptionFieldsProps { openLabelText: string | null; openLabelValue: string; openLabelChecked: boolean; + feedback: string; readOnly: boolean; onOptionChange: (changedOptionValue: string) => void; onOpenLabelCheckedChange: (checked: boolean) => void; @@ -48,6 +49,7 @@ function OpenChoiceCheckboxAnswerOptionFields(props: OpenChoiceCheckboxAnswerOpt openLabelText, openLabelValue, openLabelChecked, + feedback, readOnly, onOptionChange, onOpenLabelCheckedChange, @@ -57,24 +59,28 @@ function OpenChoiceCheckboxAnswerOptionFields(props: OpenChoiceCheckboxAnswerOpt const orientation = getChoiceOrientation(qItem) ?? ChoiceItemOrientation.Vertical; return ( - - - - {openLabelText !== null ? ( - + + - ) : null} - + + {openLabelText !== null ? ( + + ) : null} + + + {feedback ? {feedback} : null} + ); } 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 500851e1a..e1000cb35 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx @@ -23,6 +23,7 @@ import { updateOpenLabelAnswer } from '../../../utils/openChoice'; import { FullWidthFormComponentBox } from '../../Box.styles'; import debounce from 'lodash.debounce'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler, @@ -38,13 +39,15 @@ import useOpenLabel from '../../../hooks/useOpenLabel'; import { updateChoiceCheckboxAnswers } from '../../../utils/choice'; import OpenChoiceCheckboxAnswerOptionFields from './OpenChoiceCheckboxAnswerOptionFields'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface OpenChoiceCheckboxAnswerOptionItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithShowMinimalViewAttribute, PropsWithParentIsReadOnlyAttribute, - PropsWithRenderingExtensionsAttribute { + PropsWithRenderingExtensionsAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -57,6 +60,7 @@ function OpenChoiceCheckboxAnswerOptionItem(props: OpenChoiceCheckboxAnswerOptio renderingExtensions, showMinimalView = false, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -68,6 +72,10 @@ function OpenChoiceCheckboxAnswerOptionItem(props: OpenChoiceCheckboxAnswerOptio const answers = qrOpenChoiceCheckbox.answer ?? []; const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + const { displayInstructions } = renderingExtensions; const openLabelText = getOpenLabelText(qItem); @@ -152,6 +160,7 @@ function OpenChoiceCheckboxAnswerOptionItem(props: OpenChoiceCheckboxAnswerOptio openLabelText={openLabelText} openLabelValue={openLabelValue} openLabelChecked={openLabelChecked} + feedback={feedback} readOnly={readOnly} onOptionChange={handleOptionChange} onOpenLabelCheckedChange={handleOpenLabelCheckedChange} @@ -179,6 +188,7 @@ function OpenChoiceCheckboxAnswerOptionItem(props: OpenChoiceCheckboxAnswerOptio openLabelText={openLabelText} openLabelValue={openLabelValue} openLabelChecked={openLabelChecked} + feedback={feedback} readOnly={readOnly} onOptionChange={handleOptionChange} onOpenLabelCheckedChange={handleOpenLabelCheckedChange} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetFields.tsx index 19f414cd6..a0767816b 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetFields.tsx @@ -24,7 +24,7 @@ import type { QuestionnaireResponseItemAnswer } from 'fhir/r4'; import { getChoiceOrientation } from '../../../utils/choice'; -import { StyledFormGroup } from '../Item.styles'; +import { StyledFormGroup, StyledRequiredTypography } from '../Item.styles'; import CheckboxOptionList from '../ChoiceItems/CheckboxOptionList'; import { StyledAlert } from '../../Alert.styles'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; @@ -38,6 +38,7 @@ interface OpenChoiceCheckboxFieldsProps { openLabelText: string | null; openLabelValue: string; openLabelChecked: boolean; + feedback: string; readOnly: boolean; terminologyError: TerminologyError; onOptionChange: (changedOptionValue: string) => void; @@ -53,6 +54,7 @@ function OpenChoiceCheckboxAnswerValueSetFields(props: OpenChoiceCheckboxFieldsP openLabelText, openLabelValue, openLabelChecked, + feedback, readOnly, terminologyError, onOptionChange, @@ -64,24 +66,28 @@ function OpenChoiceCheckboxAnswerValueSetFields(props: OpenChoiceCheckboxFieldsP if (options.length > 0) { return ( - - - - {openLabelText !== null ? ( - + + - ) : null} - + + {openLabelText !== null ? ( + + ) : null} + + + {feedback ? {feedback} : null} + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetItem.tsx index 7e9ac3a1b..4fa9e2954 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetItem.tsx @@ -23,6 +23,7 @@ import { updateOpenLabelAnswer } from '../../../utils/openChoice'; import { FullWidthFormComponentBox } from '../../Box.styles'; import debounce from 'lodash.debounce'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler, @@ -39,13 +40,15 @@ import useOpenLabel from '../../../hooks/useOpenLabel'; import { convertCodingsToAnswerOptions, updateChoiceCheckboxAnswers } from '../../../utils/choice'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface OpenChoiceCheckboxAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithShowMinimalViewAttribute, PropsWithParentIsReadOnlyAttribute, - PropsWithRenderingExtensionsAttribute { + PropsWithRenderingExtensionsAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -58,6 +61,7 @@ function OpenChoiceCheckboxAnswerValueSetItem(props: OpenChoiceCheckboxAnswerVal renderingExtensions, showMinimalView = false, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -69,6 +73,10 @@ function OpenChoiceCheckboxAnswerValueSetItem(props: OpenChoiceCheckboxAnswerVal const answers = qrOpenChoiceCheckbox.answer ?? []; const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + const { displayInstructions } = renderingExtensions; const openLabelText = getOpenLabelText(qItem); @@ -156,6 +164,7 @@ function OpenChoiceCheckboxAnswerValueSetItem(props: OpenChoiceCheckboxAnswerVal openLabelText={openLabelText} openLabelValue={openLabelValue} openLabelChecked={openLabelChecked} + feedback={feedback} readOnly={readOnly} terminologyError={terminologyError} onOptionChange={handleOptionChange} @@ -184,6 +193,7 @@ function OpenChoiceCheckboxAnswerValueSetItem(props: OpenChoiceCheckboxAnswerVal openLabelText={openLabelText} openLabelValue={openLabelValue} openLabelChecked={openLabelChecked} + feedback={feedback} readOnly={readOnly} terminologyError={terminologyError} onOptionChange={handleOptionChange} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceItemSwitcher.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceItemSwitcher.tsx index a3ed6867d..49c980957 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceItemSwitcher.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceItemSwitcher.tsx @@ -25,6 +25,7 @@ import { getOpenChoiceControlType } from '../../../utils/openChoice'; import OpenChoiceCheckboxAnswerOptionItem from './OpenChoiceCheckboxAnswerOptionItem'; import OpenChoiceRadioAnswerOptionItem from './OpenChoiceRadioAnswerOptionItem'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -41,7 +42,8 @@ interface OpenChoiceItemSwitcherProps PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, PropsWithShowMinimalViewAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -55,6 +57,7 @@ function OpenChoiceItemSwitcher(props: OpenChoiceItemSwitcherProps) { renderingExtensions, showMinimalView, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -69,6 +72,7 @@ function OpenChoiceItemSwitcher(props: OpenChoiceItemSwitcherProps) { renderingExtensions={renderingExtensions} showMinimalView={showMinimalView} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -81,6 +85,7 @@ function OpenChoiceItemSwitcher(props: OpenChoiceItemSwitcherProps) { renderingExtensions={renderingExtensions} showMinimalView={showMinimalView} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -94,6 +99,7 @@ function OpenChoiceItemSwitcher(props: OpenChoiceItemSwitcherProps) { isRepeated={qItem['repeats'] ?? false} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -105,6 +111,7 @@ function OpenChoiceItemSwitcher(props: OpenChoiceItemSwitcherProps) { isRepeated={qItem['repeats'] ?? false} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -118,6 +125,7 @@ function OpenChoiceItemSwitcher(props: OpenChoiceItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -131,6 +139,7 @@ function OpenChoiceItemSwitcher(props: OpenChoiceItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -143,6 +152,7 @@ function OpenChoiceItemSwitcher(props: OpenChoiceItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx index d00ac23b8..0b081b3a6 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx @@ -19,7 +19,7 @@ import type { ChangeEvent } from 'react'; import React from 'react'; import { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; -import { StyledRadioGroup } from '../Item.styles'; +import { StyledRadioGroup, StyledRequiredTypography } from '../Item.styles'; import RadioButtonWithOpenLabel from '../ItemParts/RadioButtonWithOpenLabel'; import RadioOptionList from '../ItemParts/RadioOptionList'; import { getChoiceOrientation } from '../../../utils/choice'; @@ -32,6 +32,7 @@ interface OpenChoiceRadioAnswerOptionFieldsProps { openLabelText: string | null; openLabelValue: string | null; openLabelSelected: boolean; + feedback: string; readOnly: boolean; onValueChange: (changedOptionValue: string | null, changedOpenLabelValue: string | null) => void; } @@ -44,6 +45,7 @@ function OpenChoiceRadioAnswerOptionFields(props: OpenChoiceRadioAnswerOptionFie openLabelText, openLabelValue, openLabelSelected, + feedback, readOnly, onValueChange } = props; @@ -53,26 +55,30 @@ function OpenChoiceRadioAnswerOptionFields(props: OpenChoiceRadioAnswerOptionFie const orientation = getChoiceOrientation(qItem) ?? ChoiceItemOrientation.Vertical; return ( - ) => onValueChange(e.target.value, null)} - value={valueRadio} - data-test="q-item-radio-group"> - + <> + ) => onValueChange(e.target.value, null)} + value={valueRadio} + data-test="q-item-radio-group"> + - {openLabelText ? ( - onValueChange(null, input)} - /> - ) : null} - + {openLabelText ? ( + onValueChange(null, input)} + /> + ) : null} + + + {feedback ? {feedback} : null} + ); } 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 14b464cbd..cd52003a4 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx @@ -23,6 +23,7 @@ import { getOldOpenLabelAnswer } from '../../../utils/openChoice'; import { FullWidthFormComponentBox } from '../../Box.styles'; import { findInAnswerOptions, getQrChoiceValue } from '../../../utils/choice'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler, @@ -33,18 +34,20 @@ import useReadOnly from '../../../hooks/useReadOnly'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import { useQuestionnaireStore } from '../../../stores'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface OpenChoiceRadioAnswerOptionItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, - PropsWithRenderingExtensionsAttribute { + PropsWithRenderingExtensionsAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } function OpenChoiceRadioAnswerOptionItem(props: OpenChoiceRadioAnswerOptionItemProps) { - const { qItem, qrItem, parentIsReadOnly, onQrItemChange } = props; + const { qItem, qrItem, parentIsReadOnly, feedbackFromParent, onQrItemChange } = props; const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); @@ -55,6 +58,10 @@ function OpenChoiceRadioAnswerOptionItem(props: OpenChoiceRadioAnswerOptionItemP const answers = qrOpenChoiceRadio.answer ?? []; const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + const openLabelText = getOpenLabelText(qItem); const options = qItem.answerOption ?? []; @@ -147,6 +154,7 @@ function OpenChoiceRadioAnswerOptionItem(props: OpenChoiceRadioAnswerOptionItemP openLabelText={openLabelText} openLabelValue={openLabelValue} openLabelSelected={openLabelSelected} + feedback={feedback} readOnly={readOnly} onValueChange={handleValueChange} /> diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetFields.tsx index 278e6423b..f53f63dea 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetFields.tsx @@ -19,7 +19,7 @@ import type { ChangeEvent } from 'react'; import React from 'react'; import { ChoiceItemOrientation } from '../../../interfaces/choice.enum'; import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; -import { StyledRadioGroup } from '../Item.styles'; +import { StyledRadioGroup, StyledRequiredTypography } from '../Item.styles'; import RadioButtonWithOpenLabel from '../ItemParts/RadioButtonWithOpenLabel'; import RadioOptionList from '../ItemParts/RadioOptionList'; import { getChoiceOrientation } from '../../../utils/choice'; @@ -36,6 +36,7 @@ interface OpenChoiceRadioAnswerValueSetFieldsProps { openLabelText: string | null; openLabelValue: string | null; openLabelSelected: boolean; + feedback: string; readOnly: boolean; terminologyError: TerminologyError; onValueChange: (changedOptionValue: string | null, changedOpenLabelValue: string | null) => void; @@ -49,6 +50,7 @@ function OpenChoiceRadioAnswerValueSetFields(props: OpenChoiceRadioAnswerValueSe openLabelText, openLabelValue, openLabelSelected, + feedback, readOnly, terminologyError, onValueChange @@ -60,26 +62,30 @@ function OpenChoiceRadioAnswerValueSetFields(props: OpenChoiceRadioAnswerValueSe if (options.length > 0) { return ( - ) => onValueChange(e.target.value, null)} - value={valueRadio} - data-test="q-item-radio-group"> - + <> + ) => onValueChange(e.target.value, null)} + value={valueRadio} + data-test="q-item-radio-group"> + - {openLabelText ? ( - onValueChange(null, input)} - /> - ) : null} - + {openLabelText ? ( + onValueChange(null, input)} + /> + ) : null} + + + {feedback ? {feedback} : null} + ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetItem.tsx index 5e5a45d47..90ce13e90 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetItem.tsx @@ -27,6 +27,7 @@ import { getQrChoiceValue } from '../../../utils/choice'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler, @@ -38,18 +39,20 @@ import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import { useQuestionnaireStore } from '../../../stores'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface OpenChoiceRadioAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, - PropsWithRenderingExtensionsAttribute { + PropsWithRenderingExtensionsAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } function OpenChoiceRadioAnswerValueSetItem(props: OpenChoiceRadioAnswerValueSetItemProps) { - const { qItem, qrItem, parentIsReadOnly, onQrItemChange } = props; + const { qItem, qrItem, parentIsReadOnly, feedbackFromParent, onQrItemChange } = props; const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); @@ -60,6 +63,10 @@ function OpenChoiceRadioAnswerValueSetItem(props: OpenChoiceRadioAnswerValueSetI const answers = qrOpenChoiceRadio.answer ?? []; const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + const openLabelText = getOpenLabelText(qItem); // Get codings/options from valueSet @@ -155,6 +162,7 @@ function OpenChoiceRadioAnswerValueSetItem(props: OpenChoiceRadioAnswerValueSetI openLabelText={openLabelText} openLabelValue={openLabelValue} openLabelSelected={openLabelSelected} + feedback={feedback} readOnly={readOnly} terminologyError={terminologyError} onValueChange={handleValueChange} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx index 4b54c6b09..d7fda4541 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx @@ -10,6 +10,7 @@ import type { } from '../../../interfaces/renderProps.interface'; import { useRendererStylingStore } from '../../../stores'; import Typography from '@mui/material/Typography'; +import { StyledRequiredTypography } from '../Item.styles'; interface OpenChoiceSelectAnswerOptionFieldProps extends PropsWithIsTabledAttribute, @@ -18,50 +19,64 @@ interface OpenChoiceSelectAnswerOptionFieldProps qItem: QuestionnaireItem; options: QuestionnaireItemAnswerOption[]; valueSelect: QuestionnaireItemAnswerOption | null; + feedback: string; readOnly: boolean; onChange: (newValue: QuestionnaireItemAnswerOption | string | null) => void; } function OpenChoiceSelectAnswerOptionField(props: OpenChoiceSelectAnswerOptionFieldProps) { - const { qItem, options, valueSelect, readOnly, isTabled, renderingExtensions, onChange } = props; + const { + qItem, + options, + valueSelect, + feedback, + readOnly, + isTabled, + renderingExtensions, + onChange + } = props; const textFieldWidth = useRendererStylingStore.use.textFieldWidth(); const { displayUnit, displayPrompt, entryFormat } = renderingExtensions; return ( - getAnswerOptionLabel(option)} - onChange={(_, newValue) => onChange(newValue)} - freeSolo - autoHighlight - sx={{ maxWidth: !isTabled ? textFieldWidth : 3000, minWidth: 160, flexGrow: 1 }} - disabled={readOnly} - size="small" - renderInput={(params) => ( - - {params.InputProps.endAdornment} - - {displayUnit} - - - ) - }} - /> - )} - /> + <> + getAnswerOptionLabel(option)} + onChange={(_, newValue) => onChange(newValue)} + freeSolo + autoHighlight + sx={{ maxWidth: !isTabled ? textFieldWidth : 3000, minWidth: 160, flexGrow: 1 }} + disabled={readOnly} + size="small" + renderInput={(params) => ( + + {params.InputProps.endAdornment} + + {displayUnit} + + + ) + }} + /> + )} + /> + + {feedback ? {feedback} : null} + ); } 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 b18cf9cd8..5e757b2ef 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx @@ -24,6 +24,7 @@ import type { import { createEmptyQrItem } from '../../../utils/qrItem'; import { FullWidthFormComponentBox } from '../../Box.styles'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -35,13 +36,15 @@ import useReadOnly from '../../../hooks/useReadOnly'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import { useQuestionnaireStore } from '../../../stores'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface OpenChoiceSelectAnswerOptionItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -54,6 +57,7 @@ function OpenChoiceSelectAnswerOptionItem(props: OpenChoiceSelectAnswerOptionIte isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -61,6 +65,9 @@ function OpenChoiceSelectAnswerOptionItem(props: OpenChoiceSelectAnswerOptionIte const readOnly = useReadOnly(qItem, parentIsReadOnly); + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + // Init input value const answerKey = qrItem?.answer?.[0].id; const answerOptions = qItem.answerOption; @@ -111,6 +118,7 @@ function OpenChoiceSelectAnswerOptionItem(props: OpenChoiceSelectAnswerOptionIte qItem={qItem} options={answerOptions} valueSelect={valueSelect} + feedback={feedback} readOnly={readOnly} isTabled={isTabled} renderingExtensions={renderingExtensions} @@ -133,6 +141,7 @@ function OpenChoiceSelectAnswerOptionItem(props: OpenChoiceSelectAnswerOptionIte qItem={qItem} options={answerOptions} valueSelect={valueSelect} + feedback={feedback} readOnly={readOnly} isTabled={isTabled} renderingExtensions={renderingExtensions} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx index 24343e9f9..363aea60e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx @@ -10,6 +10,7 @@ import type { import type { Coding, QuestionnaireItem } from 'fhir/r4'; import type { TerminologyError } from '../../../hooks/useValueSetCodings'; import { useRendererStylingStore } from '../../../stores'; +import { StyledRequiredTypography } from '../Item.styles'; interface OpenChoiceSelectAnswerValueSetFieldProps extends PropsWithIsTabledAttribute, @@ -19,6 +20,7 @@ interface OpenChoiceSelectAnswerValueSetFieldProps options: Coding[]; valueSelect: Coding | null; terminologyError: TerminologyError; + feedback: string; readOnly: boolean; onValueChange: (newValue: Coding | string | null) => void; } @@ -29,6 +31,7 @@ function OpenChoiceSelectAnswerValueSetField(props: OpenChoiceSelectAnswerValueS options, valueSelect, terminologyError, + feedback, readOnly, isTabled, renderingExtensions, @@ -82,6 +85,8 @@ function OpenChoiceSelectAnswerValueSetField(props: OpenChoiceSelectAnswerValueS {terminologyError.answerValueSet} ) : null} + + {feedback ? {feedback} : null} ); } 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 5ba91dc26..aabe3d441 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx @@ -21,6 +21,7 @@ import { createEmptyQrItem } from '../../../utils/qrItem'; import { FullWidthFormComponentBox } from '../../Box.styles'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -32,13 +33,15 @@ import useReadOnly from '../../../hooks/useReadOnly'; import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; import { useQuestionnaireStore } from '../../../stores'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface OpenChoiceSelectAnswerValueSetItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -51,6 +54,7 @@ function OpenChoiceSelectAnswerValueSetItem(props: OpenChoiceSelectAnswerValueSe isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -58,6 +62,9 @@ function OpenChoiceSelectAnswerValueSetItem(props: OpenChoiceSelectAnswerValueSe const readOnly = useReadOnly(qItem, parentIsReadOnly); + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + // Init input value const answerKey = qrItem?.answer?.[0].id; const qrOpenChoice = qrItem ?? createEmptyQrItem(qItem, answerKey); @@ -97,6 +104,7 @@ function OpenChoiceSelectAnswerValueSetItem(props: OpenChoiceSelectAnswerValueSe options={codings} valueSelect={valueSelect} terminologyError={terminologyError} + feedback={feedback} isTabled={isTabled} renderingExtensions={renderingExtensions} readOnly={readOnly} @@ -120,6 +128,7 @@ function OpenChoiceSelectAnswerValueSetItem(props: OpenChoiceSelectAnswerValueSe options={codings} valueSelect={valueSelect} terminologyError={terminologyError} + feedback={feedback} isTabled={isTabled} renderingExtensions={renderingExtensions} readOnly={readOnly} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx index 844c307a2..0f57b047e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -39,7 +40,8 @@ interface QuantityItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -52,6 +54,7 @@ function QuantityItem(props: QuantityItemProps) { isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -115,7 +118,7 @@ function QuantityItem(props: QuantityItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, valueInput); + const feedback = useValidationFeedback(qItem, feedbackFromParent, valueInput); // Process calculated expressions const { calcExpUpdated } = useQuantityCalculatedExpression({ diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItem.tsx index 2c86c07bc..7092628ee 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItem.tsx @@ -18,6 +18,7 @@ import React, { useCallback, useMemo } from 'react'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -37,7 +38,8 @@ interface SingleItemProps PropsWithIsTabledAttribute, PropsWithShowMinimalViewAttribute, PropsWithParentIsReadOnlyAttribute, - PropsWithParentIsRepeatGroupAttribute { + PropsWithParentIsRepeatGroupAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; groupCardElevation: number; @@ -58,6 +60,7 @@ function SingleItem(props: SingleItemProps) { groupCardElevation, showMinimalView, parentIsReadOnly, + feedbackFromParent, parentIsRepeatGroup, parentRepeatGroupIndex, onQrItemChange @@ -116,6 +119,7 @@ function SingleItem(props: SingleItemProps) { groupCardElevation={groupCardElevation} showMinimalView={showMinimalView} parentIsReadOnly={readOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={handleQrItemChange} onQrItemChangeWithNestedItems={handleQrItemChangeWithNestedItems} /> diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx index 417cb8aa5..f13bbe670 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx @@ -21,6 +21,7 @@ import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import OpenChoiceItemSwitcher from '../OpenChoiceItems/OpenChoiceItemSwitcher'; import Typography from '@mui/material/Typography'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -50,7 +51,8 @@ interface SingleItemSwitcherProps PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, PropsWithShowMinimalViewAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -64,6 +66,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { renderingExtensions, showMinimalView, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -81,6 +84,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} onQrRepeatGroupChange={() => {}} // Not needed for single items, use empty function /> @@ -100,6 +104,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -112,6 +117,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -125,6 +131,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -138,6 +145,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -150,6 +158,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -162,6 +171,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -174,6 +184,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -186,6 +197,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -197,6 +209,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isRepeated={isRepeated} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -209,6 +222,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -222,6 +236,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { renderingExtensions={renderingExtensions} showMinimalView={showMinimalView} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -235,6 +250,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { renderingExtensions={renderingExtensions} showMinimalView={showMinimalView} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -247,6 +263,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -260,6 +277,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); @@ -272,6 +290,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { isTabled={isTabled} renderingExtensions={renderingExtensions} parentIsReadOnly={parentIsReadOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemView.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemView.tsx index 5e3d3b093..b13b5a6a5 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemView.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemView.tsx @@ -18,6 +18,7 @@ import React from 'react'; import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -46,7 +47,8 @@ interface SingleItemViewProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithShowMinimalViewAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; itemIsHidden: boolean; @@ -66,6 +68,7 @@ function SingleItemView(props: SingleItemViewProps) { groupCardElevation, showMinimalView, parentIsReadOnly, + feedbackFromParent, onQrItemChange, onQrItemChangeWithNestedItems } = props; @@ -102,6 +105,7 @@ function SingleItemView(props: SingleItemViewProps) { renderingExtensions={renderingExtensions} showMinimalView={showMinimalView} parentIsReadOnly={readOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> @@ -141,6 +145,7 @@ function SingleItemView(props: SingleItemViewProps) { renderingExtensions={renderingExtensions} showMinimalView={showMinimalView} parentIsReadOnly={readOnly} + feedbackFromParent={feedbackFromParent} onQrItemChange={onQrItemChange} /> ); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx index 11aabd2f0..c6831ca22 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx @@ -23,6 +23,7 @@ import Stack from '@mui/material/Stack'; import SliderLabels from './SliderLabels'; import SliderDisplayValue from './SliderDisplayValue'; import { useRendererStylingStore } from '../../../stores'; +import { StyledRequiredTypography } from '../Item.styles'; interface SliderFieldProps extends PropsWithIsTabledAttribute { linkId: string; @@ -33,6 +34,7 @@ interface SliderFieldProps extends PropsWithIsTabledAttribute { maxLabel: string; stepValue: number; isInteracted: boolean; + feedback: string; readOnly: boolean; onValueChange: (newValue: number) => void; } @@ -47,6 +49,7 @@ function SliderField(props: SliderFieldProps) { minLabel, maxLabel, isInteracted, + feedback, readOnly, isTabled, onValueChange @@ -64,27 +67,31 @@ function SliderField(props: SliderFieldProps) { const hasLabels = !!(minLabel || maxLabel); return ( - - - {hasLabels ? : null} - { - if (typeof newValue === 'number') { - onValueChange(newValue); - } - }} - disabled={readOnly} - valueLabelDisplay="auto" - data-test="q-item-slider-field" - /> - + <> + + + {hasLabels ? : null} + { + if (typeof newValue === 'number') { + onValueChange(newValue); + } + }} + disabled={readOnly} + valueLabelDisplay="auto" + data-test="q-item-slider-field" + /> + + + {feedback ? {feedback} : null} + ); } 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 cda41a4ac..24c4aa84a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx @@ -17,6 +17,7 @@ import React from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -33,23 +34,32 @@ import useSliderExtensions from '../../../hooks/useSliderExtensions'; import Box from '@mui/material/Box'; import { useQuestionnaireStore } from '../../../stores'; import { ItemLabelWrapper } from '../ItemParts'; +import useValidationFeedback from '../../../hooks/useValidationFeedback'; interface SliderItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } function SliderItem(props: SliderItemProps) { - const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; + const { + qItem, + qrItem, + isRepeated, + isTabled, + parentIsReadOnly, + feedbackFromParent, + onQrItemChange + } = props; const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); - const readOnly = useReadOnly(qItem, parentIsReadOnly); const { minValue, maxValue, stepValue, minLabel, maxLabel } = useSliderExtensions(qItem); const isInteracted = !!qrItem?.answer; @@ -66,6 +76,11 @@ function SliderItem(props: SliderItemProps) { } } + const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Perform validation checks + const feedback = useValidationFeedback(qItem, feedbackFromParent, ''); + // Event handlers function handleValueChange(newValue: number) { onQrItemChange({ @@ -86,6 +101,7 @@ function SliderItem(props: SliderItemProps) { minLabel={minLabel} maxLabel={maxLabel} isInteracted={isInteracted} + feedback={feedback} readOnly={readOnly} isTabled={isTabled} onValueChange={handleValueChange} @@ -114,6 +130,7 @@ function SliderItem(props: SliderItemProps) { minLabel={minLabel} maxLabel={maxLabel} isInteracted={isInteracted} + feedback={feedback} readOnly={readOnly} isTabled={isTabled} onValueChange={handleValueChange} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx index 9dc654c96..e164fdf1d 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx @@ -17,6 +17,7 @@ import React, { useCallback, useState } from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -41,7 +42,8 @@ interface StringItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -53,6 +55,7 @@ function StringItem(props: StringItemProps) { isTabled, renderingExtensions, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -71,7 +74,7 @@ function StringItem(props: StringItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, input); + const feedback = useValidationFeedback(qItem, feedbackFromParent, input); // Process calculated expressions const { calcExpUpdated } = useStringCalculatedExpression({ diff --git a/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx index 0a352627f..03ef883b1 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx @@ -17,6 +17,7 @@ import React, { useCallback, useState } from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithParentIsReadOnlyAttribute, PropsWithQrItemChangeHandler, @@ -39,14 +40,22 @@ interface TextItemProps extends PropsWithQrItemChangeHandler, PropsWithIsRepeatedAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } function TextItem(props: TextItemProps) { - const { qItem, qrItem, isRepeated, renderingExtensions, parentIsReadOnly, onQrItemChange } = - props; + const { + qItem, + qrItem, + isRepeated, + renderingExtensions, + parentIsReadOnly, + feedbackFromParent, + onQrItemChange + } = props; const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); @@ -64,7 +73,7 @@ function TextItem(props: TextItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, input); + const feedback = useValidationFeedback(qItem, feedbackFromParent, input); // Process calculated expressions const { calcExpUpdated } = useStringCalculatedExpression({ diff --git a/packages/smart-forms-renderer/src/components/FormComponents/TimeItem/TimeItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/TimeItem/TimeItem.tsx index 42ae75168..79903517f 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/TimeItem/TimeItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/TimeItem/TimeItem.tsx @@ -17,6 +17,7 @@ import React from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -39,7 +40,8 @@ interface TimeItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx index d67f9774f..daf39bd07 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx @@ -17,6 +17,7 @@ import React, { useCallback, useState } from 'react'; import type { + PropsWithFeedbackFromParentAttribute, PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute, @@ -40,7 +41,8 @@ interface UrlItemProps PropsWithIsRepeatedAttribute, PropsWithIsTabledAttribute, PropsWithRenderingExtensionsAttribute, - PropsWithParentIsReadOnlyAttribute { + PropsWithParentIsReadOnlyAttribute, + PropsWithFeedbackFromParentAttribute { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem | null; } @@ -52,6 +54,7 @@ function UrlItem(props: UrlItemProps) { isRepeated, isTabled, parentIsReadOnly, + feedbackFromParent, onQrItemChange } = props; @@ -71,7 +74,7 @@ function UrlItem(props: UrlItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, input); + const feedback = useValidationFeedback(qItem, feedbackFromParent, input); // Event handlers function handleChange(newInput: string) { diff --git a/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts index 07310a321..1f1226968 100644 --- a/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts +++ b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts @@ -32,7 +32,11 @@ import { import { structuredDataCapture } from 'fhir-sdc-helpers'; import { useQuestionnaireResponseStore, useQuestionnaireStore } from '../stores'; -function useValidationFeedback(qItem: QuestionnaireItem, input: string): string { +function useValidationFeedback( + qItem: QuestionnaireItem, + feedbackFromParent: string | undefined, + input: string +): string { const invalidItems = useQuestionnaireResponseStore.use.invalidItems(); const requiredItemsIsHighlighted = useQuestionnaireResponseStore.use.requiredItemsIsHighlighted(); @@ -52,6 +56,11 @@ function useValidationFeedback(qItem: QuestionnaireItem, input: string): string } } + // Feedback from parent + if (feedbackFromParent) { + return feedbackFromParent; + } + // Required-based validation // User needs to manually invoke required items to be highlighted if (requiredItemsIsHighlighted) { diff --git a/packages/smart-forms-renderer/src/interfaces/overrideComponent.interface.ts b/packages/smart-forms-renderer/src/interfaces/overrideComponent.interface.ts index 2a514b998..6ae9355b7 100644 --- a/packages/smart-forms-renderer/src/interfaces/overrideComponent.interface.ts +++ b/packages/smart-forms-renderer/src/interfaces/overrideComponent.interface.ts @@ -27,6 +27,7 @@ export interface QItemOverrideComponentProps { renderingExtensions?: RenderingExtensions; groupCardElevation?: number; parentIsReadOnly?: boolean; + feedbackFromParent?: string; parentIsRepeatGroup?: boolean; parentRepeatGroupIndex?: number; onQrItemChange: (qrItem: QuestionnaireResponseItem) => unknown; diff --git a/packages/smart-forms-renderer/src/interfaces/renderProps.interface.ts b/packages/smart-forms-renderer/src/interfaces/renderProps.interface.ts index e567919f7..553522753 100644 --- a/packages/smart-forms-renderer/src/interfaces/renderProps.interface.ts +++ b/packages/smart-forms-renderer/src/interfaces/renderProps.interface.ts @@ -47,6 +47,10 @@ export interface PropsWithParentIsReadOnlyAttribute { parentIsReadOnly?: boolean; } +export interface PropsWithFeedbackFromParentAttribute { + feedbackFromParent?: string; +} + export interface PropsWithParentIsRepeatGroupAttribute { parentIsRepeatGroup?: boolean; parentRepeatGroupIndex?: number;