Skip to content

Commit

Permalink
- Add support for required-based validation to all item types
Browse files Browse the repository at this point in the history
- Implement feedbackFromParent to manually pass feedback from a wrapper of a single item
- Update renderer to v1.0.0-alpha.24
  • Loading branch information
fongsean committed Feb 26, 2025
1 parent 28ae59c commit 478dd25
Show file tree
Hide file tree
Showing 53 changed files with 596 additions and 279 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/smart-forms-renderer/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,6 +41,7 @@ function AttachmentField(props: AttachmentFieldProps) {
const {
linkId,
attachmentValues,
feedback,
readOnly,
isTabled,
onUploadFile,
Expand Down Expand Up @@ -96,6 +99,8 @@ function AttachmentField(props: AttachmentFieldProps) {
</Typography>
) : null}
</Stack>

{feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface AttachmentFieldWrapperProps
PropsWithIsTabledAttribute {
qItem: QuestionnaireItem;
attachmentValues: AttachmentValues;
feedback: string;
readOnly: boolean;
onUploadFile: (file: File | null) => void;
onUrlChange: (url: string) => void;
Expand All @@ -43,6 +44,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) {
const {
qItem,
attachmentValues,
feedback,
readOnly,
isRepeated,
isTabled,
Expand All @@ -58,6 +60,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) {
<AttachmentField
linkId={qItem.linkId}
attachmentValues={attachmentValues}
feedback={feedback}
readOnly={readOnly}
isTabled={isTabled}
onUploadFile={onUploadFile}
Expand All @@ -80,6 +83,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) {
<AttachmentField
linkId={qItem.linkId}
attachmentValues={attachmentValues}
feedback={feedback}
readOnly={readOnly}
isTabled={isTabled}
onUploadFile={onUploadFile}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import React, { useCallback, useState } from 'react';
import type {
PropsWithFeedbackFromParentAttribute,
PropsWithIsRepeatedAttribute,
PropsWithIsTabledAttribute,
PropsWithParentIsReadOnlyAttribute,
Expand All @@ -32,6 +33,7 @@ import AttachmentFieldWrapper from './AttachmentFieldWrapper';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { createAttachmentAnswer } from '../../../utils/fileUtils';
import useValidationFeedback from '../../../hooks/useValidationFeedback';

export interface AttachmentValues {
uploadedFile: File | null;
Expand All @@ -44,15 +46,22 @@ interface AttachmentItemProps
PropsWithIsRepeatedAttribute,
PropsWithIsTabledAttribute,
PropsWithRenderingExtensionsAttribute,
PropsWithParentIsReadOnlyAttribute {
PropsWithParentIsReadOnlyAttribute,
PropsWithFeedbackFromParentAttribute {
qItem: QuestionnaireItem;
qrItem: QuestionnaireResponseItem | null;
}

function AttachmentItem(props: AttachmentItemProps) {
const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props;

const readOnly = useReadOnly(qItem, parentIsReadOnly);
const {
qItem,
qrItem,
isRepeated,
isTabled,
parentIsReadOnly,
feedbackFromParent,
onQrItemChange
} = props;

// Init input value
const answerKey = qrItem?.answer?.[0].id;
Expand All @@ -65,6 +74,11 @@ function AttachmentItem(props: AttachmentItemProps) {
const [url, setUrl] = useState(valueString);
const [fileName, setFileName] = useState(valueString);

const readOnly = useReadOnly(qItem, parentIsReadOnly);

// Perform validation checks
const feedback = useValidationFeedback(qItem, feedbackFromParent, '');

// Event handlers
async function handleUploadFile(newUploadedFile: File | null) {
setUploadedFile(newUploadedFile);
Expand Down Expand Up @@ -112,6 +126,7 @@ function AttachmentItem(props: AttachmentItemProps) {
<AttachmentFieldWrapper
qItem={qItem}
attachmentValues={{ uploadedFile: uploadedFile, url: url, fileName: fileName }}
feedback={feedback}
readOnly={readOnly}
isRepeated={isRepeated}
isTabled={isTabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Box from '@mui/material/Box';
import { ChoiceItemOrientation } from '../../../interfaces/choice.enum';
import type { QuestionnaireItem } from 'fhir/r4';
import ChoiceRadioSingle from '../ChoiceItems/ChoiceRadioSingle';
import { StyledRadioGroup } from '../Item.styles';
import { StyledRadioGroup, StyledRequiredTypography } from '../Item.styles';
import { getChoiceOrientation } from '../../../utils/choice';
import FadingCheckIcon from '../ItemParts/FadingCheckIcon';
import Checkbox from '@mui/material/Checkbox';
Expand All @@ -29,7 +29,6 @@ import { isSpecificItemControl } from '../../../utils';
import ClearInputButton from '../ItemParts/ClearInputButton';
import { useRendererStylingStore } from '../../../stores';
import { findCalculatedExpressionsInExtensions } from '../../../utils/getExpressionsFromItem';
import Typography from '@mui/material/Typography';

interface BooleanFieldProps {
qItem: QuestionnaireItem;
Expand Down Expand Up @@ -138,9 +137,7 @@ const BooleanField = memo(function BooleanField(props: BooleanFieldProps) {
)}
</Box>

{feedback ? (
<Typography sx={{ color: 'error.main', fontSize: '0.75rem', mt: 1 }}>{feedback}</Typography>
) : null}
{feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
</>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import React from 'react';
import type {
PropsWithFeedbackFromParentAttribute,
PropsWithIsRepeatedAttribute,
PropsWithIsTabledAttribute,
PropsWithParentIsReadOnlyAttribute,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand All @@ -54,6 +57,7 @@ function ChoiceAutocompleteItem(props: ChoiceAutocompleteItemProps) {
isTabled,
renderingExtensions,
parentIsReadOnly,
feedbackFromParent,
onQrItemChange
} = props;

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<StyledFormGroup row={orientation === ChoiceItemOrientation.Horizontal}>
<CheckboxOptionList
options={options}
answers={answers}
readOnly={readOnly}
onCheckedChange={onCheckedChange}
/>
</StyledFormGroup>
<>
<StyledFormGroup row={orientation === ChoiceItemOrientation.Horizontal}>
<CheckboxOptionList
options={options}
answers={answers}
readOnly={readOnly}
onCheckedChange={onCheckedChange}
/>
</StyledFormGroup>

{feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand All @@ -52,6 +55,7 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro
renderingExtensions,
showMinimalView = false,
parentIsReadOnly,
feedbackFromParent,
onQrItemChange
} = props;

Expand All @@ -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
Expand Down Expand Up @@ -99,6 +107,7 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro
qItem={qItem}
options={options}
answers={answers}
feedback={feedback}
readOnly={readOnly}
onCheckedChange={handleCheckedChange}
/>
Expand All @@ -121,6 +130,7 @@ function ChoiceCheckboxAnswerOptionItem(props: ChoiceCheckboxAnswerOptionItemPro
qItem={qItem}
options={options}
answers={answers}
feedback={feedback}
readOnly={readOnly}
onCheckedChange={handleCheckedChange}
/>
Expand Down
Loading

0 comments on commit 478dd25

Please sign in to comment.