Skip to content
Open
29 changes: 28 additions & 1 deletion src/components/inputs/number/number.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ import { useTranslation } from 'react-i18next';
import { useFormProviderContext } from '../../../provider/form-provider';
import FieldLabel from '../../field-label/field-label.component';
import { isEmpty } from '../../../validators/form-validator';
import { Concept } from '@openmrs/esm-framework';


const extractFieldUnitsAndRange = (concept?: Concept): string => {
if (!concept) {
return '';
}

const { hiAbsolute, lowAbsolute, units } = concept;
const displayUnits = units ? ` ${units}` : '';
const hasLowerLimit = lowAbsolute != null;
const hasUpperLimit = hiAbsolute != null;

if (hasLowerLimit && hasUpperLimit) {
return `(${lowAbsolute} - ${hiAbsolute}${displayUnits})`;
} else if (hasUpperLimit) {
return `(<= ${hiAbsolute}${displayUnits})`;
} else if (hasLowerLimit) {
return `(>= ${lowAbsolute}${displayUnits})`;
}
return units ? `(${units})` : '';
};

const NumberField: React.FC<FormFieldInputProps> = ({ field, value, errors, warnings, setFieldValue }) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -61,7 +83,12 @@ const NumberField: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
id={field.id}
invalid={errors.length > 0}
invalidText={errors[0]?.message}
label={<FieldLabel field={field} />}
label={<FieldLabel field={field} customLabel={t('{{fieldDescription}} {{unitsAndRange}}',
{
fieldDescription: t(field.label),
unitsAndRange: extractFieldUnitsAndRange(field.meta?.concept),
interpolation: { escapeValue: false }
})}/>}
max={Number(field.questionOptions.max) || undefined}
min={Number(field.questionOptions.min) || undefined}
name={field.id}
Expand Down
102 changes: 102 additions & 0 deletions src/components/inputs/number/number.test.tsx
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really sure about jest.mock('react-i18next', () => ({...
it seemed needed for the new i18n label t('{{fieldDescription}} {{unitsAndRange}}'... but I'm not sure if that's correct

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import { act, render, screen, fireEvent } from '@testing-library/react';
import { useFormProviderContext } from 'src/provider/form-provider';
import NumberField from './number.component';

jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key, options) => {
if (options && 'fieldDescription' in options) {
return `${options.fieldDescription} ${options.unitsAndRange || ''}`.trim();
}
return key;
}
})
}));

jest.mock('src/provider/form-provider', () => ({
useFormProviderContext: jest.fn(),
}));
Expand All @@ -22,6 +33,53 @@ const numberFieldMock = {
readonly: false,
};

const numberFieldMockWithUnitsAndRange = {
label: 'Weight',
type: 'obs',
id: 'weight',
questionOptions: {
rendering: 'number',
},
meta: {
concept: {
units: 'kg',
lowAbsolute: 0,
hiAbsolute: 200,
}
},
isHidden: false,
isDisabled: false,
readonly: false,
};

const numberFieldMockWithUnitsOnly = {
...numberFieldMockWithUnitsAndRange,
meta: {
concept: {
units: 'kg',
}
},
};

const numberFieldMockWithRangeOnly = {
...numberFieldMockWithUnitsAndRange,
meta: {
concept: {
lowAbsolute: 0,
hiAbsolute: 200,
}
},
};

const numberFieldMockWithHiAbsoluteOnly = {
...numberFieldMockWithUnitsAndRange,
meta: {
concept: {
hiAbsolute: 200,
}
},
};

const renderNumberField = async (props) => {
await act(() => render(<NumberField {...props} />));
};
Expand Down Expand Up @@ -104,4 +162,48 @@ describe('NumberField Component', () => {
const inputElement = screen.getByLabelText('Weight(kg):') as HTMLInputElement;
expect(inputElement).toBeDisabled();
});

it('renders units and range', async () => {
await renderNumberField({
field: numberFieldMockWithUnitsAndRange,
value: '',
errors: [],
warnings: [],
setFieldValue: jest.fn(),
});
expect(screen.getByLabelText('Weight (0 - 200 kg)')).toBeInTheDocument();
});

it('renders units only', async () => {
await renderNumberField({
field: numberFieldMockWithUnitsOnly,
value: '',
errors: [],
warnings: [],
setFieldValue: jest.fn(),
});
expect(screen.getByLabelText('Weight (kg)')).toBeInTheDocument();
});

it('renders range only', async () => {
await renderNumberField({
field: numberFieldMockWithRangeOnly,
value: '',
errors: [],
warnings: [],
setFieldValue: jest.fn(),
});
expect(screen.getByLabelText('Weight (0 - 200)')).toBeInTheDocument();
});

it('renders hiAbsolute only', async () => {
await renderNumberField({
field: numberFieldMockWithHiAbsoluteOnly,
value: '',
errors: [],
warnings: [],
setFieldValue: jest.fn(),
});
expect(screen.getByLabelText('Weight (<= 200)')).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion src/hooks/useConcepts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useRestApiMaxResults } from './useRestApiMaxResults';
type ConceptFetchResponse = FetchResponse<{ results: Array<OpenmrsResource> }>;

const conceptRepresentation =
'custom:(uuid,display,conceptClass:(uuid,display),answers:(uuid,display),conceptMappings:(conceptReferenceTerm:(conceptSource:(name),code)))';
'custom:(units,lowAbsolute,hiAbsolute,uuid,display,conceptClass:(uuid,display),answers:(uuid,display),conceptMappings:(conceptReferenceTerm:(conceptSource:(name),code)))';

export function useConcepts(references: Set<string>): {
concepts: Array<OpenmrsResource> | undefined;
Expand Down
14 changes: 14 additions & 0 deletions src/hooks/useFormFieldsMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ export function useFormFieldsMeta(rawFormFields: FormField[], concepts: OpenmrsR
const matchingConcept = findConceptByReference(field.questionOptions.concept, concepts);
field.questionOptions.concept = matchingConcept ? matchingConcept.uuid : field.questionOptions.concept;
field.label = field.label ? field.label : matchingConcept?.display;

if (matchingConcept) {
if (matchingConcept.lowAbsolute != undefined && matchingConcept.hiAbsolute != undefined) {
field.questionOptions.min = matchingConcept.lowAbsolute;
field.questionOptions.max = matchingConcept.hiAbsolute;
}
else if (matchingConcept.lowAbsolute != undefined) {
field.questionOptions.min = matchingConcept.lowAbsolute;
}
else if (matchingConcept.hiAbsolute != undefined) {
field.questionOptions.max = matchingConcept.hiAbsolute;
}
}

if (
codedTypes.includes(field.questionOptions.rendering) &&
!field.questionOptions.answers?.length &&
Expand Down