-
Notifications
You must be signed in to change notification settings - Fork 249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(fix) O3-2807: Quantity Units should be required when a quantity to dispense is specified #1636
Changes from 17 commits
844de36
772d757
7d91e9e
08ea98b
46413eb
61b35d9
cdeb02a
c4eeb10
068fcb3
12a1aac
cb9b02c
b4e9648
282d82d
fb1adb5
f380d44
c087559
624173c
0315e57
936a825
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,7 @@ import { | |
import { Add, ArrowLeft, Subtract } from '@carbon/react/icons'; | ||
import { z } from 'zod'; | ||
import { zodResolver } from '@hookform/resolvers/zod'; | ||
import { Controller, useController, useForm } from 'react-hook-form'; | ||
import { type Control, Controller, useController, useForm } from 'react-hook-form'; | ||
import { age, formatDate, parseDate, useConfig, useLayoutType, usePatient } from '@openmrs/esm-framework'; | ||
import { useOrderConfig } from '../api/order-config'; | ||
import { type ConfigObject } from '../config-schema'; | ||
|
@@ -68,21 +68,34 @@ const schemaFields = { | |
frequency: z.object({ ...comboSchema }, { invalid_type_error: 'Please select a frequency' }), | ||
}; | ||
|
||
const medicationOrderFormSchema = z.discriminatedUnion('isFreeTextDosage', [ | ||
z.object({ | ||
...schemaFields, | ||
isFreeTextDosage: z.literal(false), | ||
freeTextDosage: z.string().optional(), | ||
}), | ||
z.object({ | ||
...schemaFields, | ||
isFreeTextDosage: z.literal(true), | ||
dosage: z.number().nullable(), | ||
unit: z.object({ ...comboSchema }).nullable(), | ||
route: z.object({ ...comboSchema }).nullable(), | ||
frequency: z.object({ ...comboSchema }).nullable(), | ||
}), | ||
]); | ||
const medicationOrderFormSchema = z | ||
.discriminatedUnion('isFreeTextDosage', [ | ||
z.object({ | ||
...schemaFields, | ||
isFreeTextDosage: z.literal(false), | ||
freeTextDosage: z.string().optional(), | ||
}), | ||
z.object({ | ||
...schemaFields, | ||
isFreeTextDosage: z.literal(true), | ||
dosage: z.number().nullable(), | ||
unit: z.object({ ...comboSchema }).nullable(), | ||
route: z.object({ ...comboSchema }).nullable(), | ||
frequency: z.object({ ...comboSchema }).nullable(), | ||
}), | ||
]) | ||
.refine( | ||
(formValues) => { | ||
if (formValues.pillsDispensed > 0) { | ||
return Boolean(formValues.quantityUnits); | ||
} | ||
return true; | ||
}, | ||
{ | ||
message: 'Please select Quantity unit', | ||
path: ['quantityUnits'], | ||
}, | ||
); | ||
|
||
type MedicationOrderFormData = z.infer<typeof medicationOrderFormSchema>; | ||
|
||
|
@@ -143,7 +156,14 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel }: Drug | |
return initialOrderBasketItem?.startDate as Date; | ||
}, [initialOrderBasketItem?.startDate]); | ||
|
||
const { handleSubmit, control, watch, setValue } = useForm<MedicationOrderFormData>({ | ||
const { | ||
handleSubmit, | ||
control, | ||
watch, | ||
getValues, | ||
setValue, | ||
formState: { errors }, | ||
} = useForm<MedicationOrderFormData>({ | ||
mode: 'all', | ||
resolver: zodResolver(medicationOrderFormSchema), | ||
defaultValues: { | ||
|
@@ -166,6 +186,15 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel }: Drug | |
}, | ||
}); | ||
|
||
const handleUnitAfterChange = useCallback( | ||
(newValue: MedicationOrderFormData['unit'], prevValue: MedicationOrderFormData['unit']) => { | ||
if (prevValue?.valueCoded === getValues('quantityUnits')?.valueCoded) { | ||
setValue('quantityUnits', newValue, { shouldValidate: true }); | ||
} | ||
}, | ||
[setValue], | ||
); | ||
|
||
const routeValue = watch('route')?.value; | ||
const unitValue = watch('unit')?.value; | ||
const dosage = watch('dosage'); | ||
|
@@ -277,7 +306,6 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel }: Drug | |
</span> | ||
</div> | ||
)} | ||
|
||
<Form className={styles.orderForm} onSubmit={handleSubmit(handleFormSubmission)} id="drugOrderForm"> | ||
<div> | ||
{errorFetchingOrderConfig && ( | ||
|
@@ -368,12 +396,14 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel }: Drug | |
control={control} | ||
name="unit" | ||
type="comboBox" | ||
getValues={getValues} | ||
size={isTablet ? 'lg' : 'md'} | ||
id="dosingUnits" | ||
items={drugDosingUnits} | ||
placeholder={t('editDosageUnitsPlaceholder', 'Unit')} | ||
titleText={t('editDosageUnitsTitle', 'Dose unit')} | ||
itemToString={(item) => item?.value} | ||
handleAfterChange={handleUnitAfterChange} | ||
/> | ||
</InputWrapper> | ||
</Column> | ||
|
@@ -675,31 +705,58 @@ const CustomNumberInput = ({ setValue, control, name, labelText, ...inputProps } | |
); | ||
}; | ||
|
||
const ControlledFieldInput = ({ name, control, type, ...restProps }) => { | ||
interface ControlledFieldInputProps { | ||
name: keyof MedicationOrderFormData; | ||
type: 'toggle' | 'checkbox' | 'number' | 'textArea' | 'textInput' | 'comboBox'; | ||
handleAfterChange?: ( | ||
newValue: MedicationOrderFormData[keyof MedicationOrderFormData], | ||
prevValue: MedicationOrderFormData[keyof MedicationOrderFormData], | ||
) => void; | ||
control: Control<MedicationOrderFormData>; | ||
[x: string]: any; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we get a better type here than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Properly typing this requires a discriminated union, since the values of type ControlledFieldInputProps = BaseControlledFieldInputProps & (
ToggleFieldProps | CheckboxFieldProps | NumberFieldProps | TextAreaProps | TextInputProps | ComboBoxProps
)
interface BaseControlledFieldInputProps {
name: keyof MedicationOrderFormData;
handleAfterChange?: (
newValue: MedicationOrderFormData[keyof MedicationOrderFormData],
prevValue: MedicationOrderFormData[keyof MedicationOrderFormData],
) => void;
control: Control<MedicationOrderFormData>;
}
interface ToggleFieldProps extends ToggleProps /* part of Carbon */ {
type: 'toggle',
}
// repeat for other types And that should give us something fully typed. |
||
} | ||
|
||
const ControlledFieldInput = ({ | ||
name, | ||
type, | ||
control, | ||
getValues, | ||
handleAfterChange, | ||
...restProps | ||
}: ControlledFieldInputProps) => { | ||
const { | ||
field: { onBlur, onChange, value, ref }, | ||
fieldState, | ||
} = useController<MedicationOrderFormData>({ name: name, control }); | ||
|
||
const handleChange = useCallback( | ||
(newValue: MedicationOrderFormData[keyof MedicationOrderFormData]) => { | ||
const prevValue = getValues?.(name); | ||
onChange(newValue); | ||
handleAfterChange?.(newValue, prevValue); | ||
}, | ||
[getValues, onChange, handleAfterChange], | ||
); | ||
|
||
const component = useMemo(() => { | ||
if (type === 'toggle') | ||
return ( | ||
<Toggle | ||
toggled={value} | ||
onChange={() => {} /* Required by the typings, but we don't need it. */} | ||
onToggle={(value) => onChange(value)} | ||
onToggle={(value) => handleChange(value)} | ||
{...restProps} | ||
/> | ||
); | ||
|
||
if (type === 'checkbox') | ||
return <Checkbox checked={value} onChange={(e, { checked, id }) => onChange(checked)} {...restProps} />; | ||
return <Checkbox checked={value} onChange={(e, { checked, id }) => handleChange(checked)} {...restProps} />; | ||
|
||
if (type === 'number') | ||
return ( | ||
<NumberInput | ||
value={!!value ? value : 0} | ||
onChange={(e, { value }) => onChange(parseFloat(value))} | ||
onChange={(e, { value }) => handleChange(parseFloat(value))} | ||
className={fieldState?.error?.message && styles.fieldError} | ||
onBlur={onBlur} | ||
ref={ref} | ||
|
@@ -711,7 +768,7 @@ const ControlledFieldInput = ({ name, control, type, ...restProps }) => { | |
return ( | ||
<TextArea | ||
value={value} | ||
onChange={(e) => onChange(e.target.value)} | ||
onChange={(e) => handleChange(e.target.value)} | ||
onBlur={onBlur} | ||
ref={ref} | ||
className={fieldState?.error?.message && styles.fieldError} | ||
|
@@ -723,7 +780,7 @@ const ControlledFieldInput = ({ name, control, type, ...restProps }) => { | |
return ( | ||
<TextInput | ||
value={value} | ||
onChange={(e) => onChange(e.target.value)} | ||
onChange={(e) => handleChange(e.target.value)} | ||
ref={ref} | ||
onBlur={onBlur} | ||
className={fieldState?.error?.message && styles.fieldError} | ||
|
@@ -735,7 +792,7 @@ const ControlledFieldInput = ({ name, control, type, ...restProps }) => { | |
return ( | ||
<ComboBox | ||
selectedItem={value} | ||
onChange={({ selectedItem }) => onChange(selectedItem)} | ||
onChange={({ selectedItem }) => handleChange(selectedItem)} | ||
onBlur={onBlur} | ||
ref={ref} | ||
className={fieldState?.error?.message && styles.fieldError} | ||
|
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.
@vasharma05, could you guide @mccarthyaaron on how to make these error messages translatable? We should probably add an example to our Coding Conventions.
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.
Will be eager to learn how to do that. Thanks
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 @mccarthyaaron!
You can refer #1652.
Thanks!