forked from aehrc/smart-forms
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Quantity Item Form Component * Update package version * Update packages version
- Loading branch information
Showing
7 changed files
with
449 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityField.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import React from 'react'; | ||
import InputAdornment from '@mui/material/InputAdornment'; | ||
import FadingCheckIcon from '../ItemParts/FadingCheckIcon'; | ||
import { StandardTextField } from '../Textfield.styles'; | ||
import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface'; | ||
|
||
interface QuantityFieldProps extends PropsWithIsTabledAttribute { | ||
linkId: string; | ||
input: string; | ||
feedback: string; | ||
displayPrompt: string; | ||
displayUnit: string; | ||
entryFormat: string; | ||
readOnly: boolean; | ||
calcExpUpdated: boolean; | ||
onInputChange: (value: string) => void; | ||
} | ||
|
||
function QuantityField(props: QuantityFieldProps) { | ||
const { | ||
linkId, | ||
input, | ||
feedback, | ||
displayPrompt, | ||
displayUnit, | ||
entryFormat, | ||
readOnly, | ||
calcExpUpdated, | ||
isTabled, | ||
onInputChange | ||
} = props; | ||
|
||
return ( | ||
<StandardTextField | ||
id={linkId} | ||
value={input} | ||
error={!!feedback} | ||
onChange={(event) => onInputChange(event.target.value)} | ||
disabled={readOnly} | ||
label={displayPrompt} | ||
placeholder={entryFormat === '' ? '0.0' : entryFormat} | ||
fullWidth | ||
isTabled={isTabled} | ||
size="small" | ||
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }} | ||
InputProps={{ | ||
endAdornment: ( | ||
<InputAdornment position={'end'}> | ||
<FadingCheckIcon fadeIn={calcExpUpdated} disabled={readOnly} /> | ||
{displayUnit} | ||
</InputAdornment> | ||
) | ||
}} | ||
helperText={feedback} | ||
data-test="q-item-quantity-field" | ||
/> | ||
); | ||
} | ||
|
||
export default QuantityField; |
243 changes: 243 additions & 0 deletions
243
packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
import React, { useCallback, useState } from 'react'; | ||
import type { | ||
PropsWithIsRepeatedAttribute, | ||
PropsWithIsTabledAttribute, | ||
PropsWithParentIsReadOnlyAttribute, | ||
PropsWithQrItemChangeHandler | ||
} from '../../../interfaces/renderProps.interface'; | ||
import type { | ||
Extension, | ||
Quantity, | ||
QuestionnaireItem, | ||
QuestionnaireItemAnswerOption, | ||
QuestionnaireResponseItem | ||
} from 'fhir/r4'; | ||
import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; | ||
import { FullWidthFormComponentBox } from '../../Box.styles'; | ||
import useValidationFeedback from '../../../hooks/useValidationFeedback'; | ||
import debounce from 'lodash.debounce'; | ||
import { DEBOUNCE_DURATION } from '../../../utils/debounce'; | ||
import { createEmptyQrItem } from '../../../utils/qrItem'; | ||
import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; | ||
import { | ||
parseDecimalStringToFloat, | ||
parseDecimalStringWithPrecision | ||
} from '../../../utils/parseInputs'; | ||
import { getDecimalPrecision } from '../../../utils/itemControl'; | ||
import useDecimalCalculatedExpression from '../../../hooks/useDecimalCalculatedExpression'; | ||
import useStringInput from '../../../hooks/useStringInput'; | ||
import useReadOnly from '../../../hooks/useReadOnly'; | ||
import { useQuestionnaireStore } from '../../../stores'; | ||
import Box from '@mui/material/Box'; | ||
import QuantityField from './QuantityField'; | ||
import QuantityUnitField from './QuantityUnitField'; | ||
import Grid from '@mui/material/Grid'; | ||
|
||
interface QuantityItemProps | ||
extends PropsWithQrItemChangeHandler, | ||
PropsWithIsRepeatedAttribute, | ||
PropsWithIsTabledAttribute, | ||
PropsWithParentIsReadOnlyAttribute { | ||
qItem: QuestionnaireItem; | ||
qrItem: QuestionnaireResponseItem | null; | ||
} | ||
|
||
function QuantityItem(props: QuantityItemProps) { | ||
const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; | ||
|
||
const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId(); | ||
|
||
const readOnly = useReadOnly(qItem, parentIsReadOnly); | ||
const precision = getDecimalPrecision(qItem); | ||
const { displayUnit, displayPrompt, entryFormat } = useRenderingExtensions(qItem); | ||
|
||
// Init input value | ||
let valueQuantity: Quantity = {}; | ||
let initialInput = ''; | ||
if (qrItem?.answer) { | ||
if (qrItem?.answer[0].valueQuantity) { | ||
valueQuantity = qrItem.answer[0].valueQuantity; | ||
} | ||
|
||
initialInput = | ||
(precision ? valueQuantity.value?.toFixed(precision) : valueQuantity.value?.toString()) || ''; | ||
} | ||
const [input, setInput] = useStringInput(initialInput); | ||
|
||
// Init unit input value | ||
const answerOptions = qItem.extension?.filter( | ||
(f) => f.url === 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption' | ||
); | ||
const isShowAnswerOptions = answerOptions?.length || false; | ||
const [unitInput, setUnitInput] = useState<QuestionnaireItemAnswerOption | null>( | ||
(answerOptions?.at(0) ?? null) as Extension | null | ||
); | ||
|
||
// Perform validation checks | ||
const feedback = useValidationFeedback(qItem, input); | ||
|
||
// Process calculated expressions | ||
const { calcExpUpdated } = useDecimalCalculatedExpression({ | ||
qItem: qItem, | ||
inputValue: input, | ||
precision: precision, | ||
onChangeByCalcExpressionDecimal: (newValueDecimal: number) => { | ||
setInput( | ||
typeof precision === 'number' | ||
? newValueDecimal.toFixed(precision) | ||
: newValueDecimal.toString() | ||
); | ||
onQrItemChange({ | ||
...createEmptyQrItem(qItem), | ||
answer: [ | ||
{ | ||
valueQuantity: { | ||
value: newValueDecimal, | ||
system: unitInput?.valueCoding?.system, | ||
code: unitInput?.valueCoding?.code | ||
} | ||
} | ||
] | ||
}); | ||
}, | ||
onChangeByCalcExpressionNull: () => { | ||
setInput(''); | ||
onQrItemChange(createEmptyQrItem(qItem)); | ||
} | ||
}); | ||
|
||
// Event handlers | ||
function handleInputChange(newInput: string) { | ||
const parsedNewInput: string = parseDecimalStringWithPrecision(newInput, precision); | ||
|
||
setInput(parsedNewInput); | ||
updateQrItemWithDebounce(parsedNewInput); | ||
} | ||
|
||
function handleUnitInputChange(newInput: QuestionnaireItemAnswerOption | null) { | ||
setUnitInput(newInput); | ||
|
||
if (!input) return; | ||
|
||
onQrItemChange({ | ||
...createEmptyQrItem(qItem), | ||
answer: precision | ||
? [ | ||
{ | ||
valueQuantity: { | ||
value: parseDecimalStringToFloat(input, precision), | ||
system: newInput?.valueCoding?.system, | ||
code: newInput?.valueCoding?.code | ||
} | ||
} | ||
] | ||
: [ | ||
{ | ||
valueQuantity: { | ||
value: parseFloat(input), | ||
system: newInput?.valueCoding?.system, | ||
code: newInput?.valueCoding?.code | ||
} | ||
} | ||
] | ||
}); | ||
} | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
const updateQrItemWithDebounce = useCallback( | ||
debounce((parsedNewInput: string) => { | ||
if (parsedNewInput === '') { | ||
onQrItemChange(createEmptyQrItem(qItem)); | ||
} else { | ||
onQrItemChange({ | ||
...createEmptyQrItem(qItem), | ||
answer: precision | ||
? [ | ||
{ | ||
valueQuantity: { | ||
value: parseDecimalStringToFloat(parsedNewInput, precision), | ||
system: unitInput?.valueCoding?.system, | ||
code: unitInput?.valueCoding?.code | ||
} | ||
} | ||
] | ||
: [ | ||
{ | ||
valueQuantity: { | ||
value: parseFloat(parsedNewInput), | ||
system: unitInput?.valueCoding?.system, | ||
code: unitInput?.valueCoding?.code | ||
} | ||
} | ||
] | ||
}); | ||
} | ||
}, DEBOUNCE_DURATION), | ||
[onQrItemChange, qItem, displayUnit, precision, unitInput] | ||
); // Dependencies are tested, debounce is causing eslint to not recognise dependencies | ||
|
||
if (isRepeated) { | ||
return ( | ||
<Box data-test="q-item-quantity-box" display="flex" gap={1}> | ||
<QuantityField | ||
linkId={qItem.linkId} | ||
input={input} | ||
feedback={feedback} | ||
displayPrompt={displayPrompt} | ||
displayUnit={displayUnit} | ||
entryFormat={entryFormat} | ||
readOnly={readOnly} | ||
calcExpUpdated={calcExpUpdated} | ||
isTabled={isTabled} | ||
onInputChange={handleInputChange} | ||
/> | ||
{answerOptions?.length ? ( | ||
<QuantityUnitField | ||
qItem={qItem} | ||
options={answerOptions} | ||
valueSelect={unitInput} | ||
readOnly={readOnly} | ||
isTabled={isTabled} | ||
onChange={handleUnitInputChange} | ||
/> | ||
) : null} | ||
</Box> | ||
); | ||
} | ||
|
||
return ( | ||
<FullWidthFormComponentBox | ||
data-test="q-item-quantity-box" | ||
data-linkid={qItem.linkId} | ||
onClick={() => onFocusLinkId(qItem.linkId)}> | ||
<ItemFieldGrid qItem={qItem} readOnly={readOnly}> | ||
<Box display="flex" gap={1}> | ||
<QuantityField | ||
linkId={qItem.linkId} | ||
input={input} | ||
feedback={feedback} | ||
displayPrompt={displayPrompt} | ||
displayUnit={displayUnit} | ||
entryFormat={entryFormat} | ||
readOnly={readOnly} | ||
calcExpUpdated={calcExpUpdated} | ||
isTabled={isTabled} | ||
onInputChange={handleInputChange} | ||
/> | ||
{answerOptions?.length ? ( | ||
<QuantityUnitField | ||
qItem={qItem} | ||
options={answerOptions} | ||
valueSelect={unitInput} | ||
readOnly={readOnly} | ||
isTabled={isTabled} | ||
onChange={handleUnitInputChange} | ||
/> | ||
) : null} | ||
</Box> | ||
</ItemFieldGrid> | ||
</FullWidthFormComponentBox> | ||
); | ||
} | ||
|
||
export default QuantityItem; |
60 changes: 60 additions & 0 deletions
60
...ges/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityUnitField.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import React from 'react'; | ||
import { getAnswerOptionLabel } from '../../../utils/openChoice'; | ||
import { StandardTextField, TEXT_FIELD_WIDTH } from '../Textfield.styles'; | ||
import Autocomplete from '@mui/material/Autocomplete'; | ||
import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; | ||
import type { | ||
PropsWithIsTabledAttribute, | ||
PropsWithParentIsReadOnlyAttribute | ||
} from '../../../interfaces/renderProps.interface'; | ||
import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; | ||
|
||
interface QuantityUnitFieldProps | ||
extends PropsWithIsTabledAttribute, | ||
PropsWithParentIsReadOnlyAttribute { | ||
qItem: QuestionnaireItem; | ||
options: QuestionnaireItemAnswerOption[]; | ||
valueSelect: QuestionnaireItemAnswerOption | null; | ||
readOnly: boolean; | ||
onChange: (newValue: QuestionnaireItemAnswerOption | null) => void; | ||
} | ||
|
||
function QuantityUnitField(props: QuantityUnitFieldProps) { | ||
const { qItem, options, valueSelect, readOnly, isTabled, onChange } = props; | ||
|
||
const { displayUnit, displayPrompt, entryFormat } = useRenderingExtensions(qItem); | ||
|
||
return ( | ||
<Autocomplete | ||
id={qItem.linkId} | ||
value={valueSelect ?? null} | ||
options={options} | ||
getOptionLabel={(option) => getAnswerOptionLabel(option)} | ||
onChange={(_, newValue) => onChange(newValue as QuestionnaireItemAnswerOption | null)} | ||
freeSolo | ||
autoHighlight | ||
sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160, flexGrow: 1 }} | ||
disabled={readOnly} | ||
size="small" | ||
renderInput={(params) => ( | ||
<StandardTextField | ||
isTabled={isTabled} | ||
label={displayPrompt} | ||
placeholder={entryFormat} | ||
{...params} | ||
InputProps={{ | ||
...params.InputProps, | ||
endAdornment: ( | ||
<> | ||
{params.InputProps.endAdornment} | ||
{displayUnit} | ||
</> | ||
) | ||
}} | ||
/> | ||
)} | ||
/> | ||
); | ||
} | ||
|
||
export default QuantityUnitField; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
af8b8f8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @ryuuzake, is there any chance we can collaborate on this with a pull request? It looks well made.
Just got a feature request for "Quantity": aehrc#639
af8b8f8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, let me make a pull request to the main repo @fongsean