From 035c318b9b10ec5268a570625466a491824505dc Mon Sep 17 00:00:00 2001 From: D-matz Date: Sat, 21 Dec 2024 16:02:58 -0600 Subject: [PATCH 1/7] get units, absolute high, absolute low from concept api, add (units) (min-max) to label and min/max to form --- src/hooks/useConcepts.ts | 2 +- src/hooks/useFormFieldsMeta.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/hooks/useConcepts.ts b/src/hooks/useConcepts.ts index d6f2d217..e844c2cc 100644 --- a/src/hooks/useConcepts.ts +++ b/src/hooks/useConcepts.ts @@ -5,7 +5,7 @@ import { type FetchResponse, type OpenmrsResource, openmrsFetch, restBaseUrl } f type ConceptFetchResponse = FetchResponse<{ results: Array }>; 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): { concepts: Array | undefined; diff --git a/src/hooks/useFormFieldsMeta.ts b/src/hooks/useFormFieldsMeta.ts index 973b7ae0..fd360fee 100644 --- a/src/hooks/useFormFieldsMeta.ts +++ b/src/hooks/useFormFieldsMeta.ts @@ -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.units) + { + field.label = field.label + " (" + matchingConcept.units + ")"; + } + if(matchingConcept.lowAbsolute && matchingConcept.hiAbsolute) + { + field.label = field.label + " (" + matchingConcept.lowAbsolute + "-" + matchingConcept.hiAbsolute + ")"; + field.questionOptions.min = matchingConcept.lowAbsolute; + field.questionOptions.max = matchingConcept.hiAbsolute; + } + } + if ( codedTypes.includes(field.questionOptions.rendering) && !field.questionOptions.answers?.length && From a8f625a1c002d3cb26a091711f961adc6277eaaf Mon Sep 17 00:00:00 2001 From: D-matz Date: Wed, 8 Jan 2025 23:23:40 -0600 Subject: [PATCH 2/7] display units and min/max from number component instead of field label, add test concept with units and range --- .../inputs/number/number.component.tsx | 26 +++++++++++++++- src/components/inputs/number/number.test.tsx | 30 +++++++++++++++++++ src/hooks/useFormFieldsMeta.ts | 16 +++++----- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/components/inputs/number/number.component.tsx b/src/components/inputs/number/number.component.tsx index f33364ca..09df9e81 100644 --- a/src/components/inputs/number/number.component.tsx +++ b/src/components/inputs/number/number.component.tsx @@ -11,6 +11,30 @@ import { useFormProviderContext } from '../../../provider/form-provider'; import FieldLabel from '../../field-label/field-label.component'; import { isEmpty } from '../../../validators/form-validator'; +const extractFieldUnitsAndRange = (concept) => { + if (!concept) { + return ''; + } + + let unitsDisplay = ''; + if (concept.units) { + unitsDisplay = ` (${concept.units})`; + } + + let rangeDisplay = ''; + if (concept.lowAbsolute != null && concept.hiAbsolute != null) { + rangeDisplay = ` (${concept.lowAbsolute}-${concept.hiAbsolute})`; + } + else if (concept.lowAbsolute != null) { + rangeDisplay = ` (Min ${concept.lowAbsolute})`; + } + else if (concept.hiAbsolute != null) { + rangeDisplay = ` (Max ${concept.hiAbsolute})`; + } + + return unitsDisplay + rangeDisplay; +}; + const NumberField: React.FC = ({ field, value, errors, warnings, setFieldValue }) => { const { t } = useTranslation(); const [lastBlurredValue, setLastBlurredValue] = useState(value); @@ -61,7 +85,7 @@ const NumberField: React.FC = ({ field, value, errors, warn id={field.id} invalid={errors.length > 0} invalidText={errors[0]?.message} - label={} + label={} max={Number(field.questionOptions.max) || undefined} min={Number(field.questionOptions.min) || undefined} name={field.id} diff --git a/src/components/inputs/number/number.test.tsx b/src/components/inputs/number/number.test.tsx index f1f04dac..f8795704 100644 --- a/src/components/inputs/number/number.test.tsx +++ b/src/components/inputs/number/number.test.tsx @@ -22,6 +22,25 @@ 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 renderNumberField = async (props) => { await act(() => render()); }; @@ -104,4 +123,15 @@ 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 (kg) (0-200)')).toBeInTheDocument(); + }); }); diff --git a/src/hooks/useFormFieldsMeta.ts b/src/hooks/useFormFieldsMeta.ts index fd360fee..3f3e44ab 100644 --- a/src/hooks/useFormFieldsMeta.ts +++ b/src/hooks/useFormFieldsMeta.ts @@ -11,16 +11,16 @@ 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.units) - { - field.label = field.label + " (" + matchingConcept.units + ")"; + + if (matchingConcept) { + if (matchingConcept.lowAbsolute != undefined && matchingConcept.hiAbsolute != undefined) { + field.questionOptions.min = matchingConcept.lowAbsolute; + field.questionOptions.max = matchingConcept.hiAbsolute; } - if(matchingConcept.lowAbsolute && matchingConcept.hiAbsolute) - { - field.label = field.label + " (" + matchingConcept.lowAbsolute + "-" + matchingConcept.hiAbsolute + ")"; + else if (matchingConcept.lowAbsolute != undefined) { field.questionOptions.min = matchingConcept.lowAbsolute; + } + else if (matchingConcept.hiAbsolute != undefined) { field.questionOptions.max = matchingConcept.hiAbsolute; } } From 9248fd24fcf49987b9239bded1ec41304b782b7f Mon Sep 17 00:00:00 2001 From: D-matz Date: Thu, 9 Jan 2025 19:17:28 -0600 Subject: [PATCH 3/7] make unit/range format consistent with lab result --- .../inputs/number/number.component.tsx | 28 +++++++++---------- src/components/inputs/number/number.test.tsx | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/components/inputs/number/number.component.tsx b/src/components/inputs/number/number.component.tsx index 09df9e81..ecccc1dc 100644 --- a/src/components/inputs/number/number.component.tsx +++ b/src/components/inputs/number/number.component.tsx @@ -11,28 +11,26 @@ import { useFormProviderContext } from '../../../provider/form-provider'; import FieldLabel from '../../field-label/field-label.component'; import { isEmpty } from '../../../validators/form-validator'; + const extractFieldUnitsAndRange = (concept) => { if (!concept) { return ''; } - let unitsDisplay = ''; - if (concept.units) { - unitsDisplay = ` (${concept.units})`; - } + const { hiAbsolute, lowAbsolute, units } = concept; + const displayUnit = units ? ` ${units}` : ''; - let rangeDisplay = ''; - if (concept.lowAbsolute != null && concept.hiAbsolute != null) { - rangeDisplay = ` (${concept.lowAbsolute}-${concept.hiAbsolute})`; - } - else if (concept.lowAbsolute != null) { - rangeDisplay = ` (Min ${concept.lowAbsolute})`; - } - else if (concept.hiAbsolute != null) { - rangeDisplay = ` (Max ${concept.hiAbsolute})`; + const hasLowerLimit = lowAbsolute != null; + const hasUpperLimit = hiAbsolute != null; + + if (hasLowerLimit && hasUpperLimit) { + return ` (${lowAbsolute} - ${hiAbsolute} ${displayUnit})`; + } else if (hasUpperLimit) { + return ` (<= ${hiAbsolute} ${displayUnit})`; + } else if (hasLowerLimit) { + return ` (>= ${lowAbsolute} ${displayUnit})`; } - - return unitsDisplay + rangeDisplay; + return units ? ` (${displayUnit})` : ''; }; const NumberField: React.FC = ({ field, value, errors, warnings, setFieldValue }) => { diff --git a/src/components/inputs/number/number.test.tsx b/src/components/inputs/number/number.test.tsx index f8795704..c28b4f8f 100644 --- a/src/components/inputs/number/number.test.tsx +++ b/src/components/inputs/number/number.test.tsx @@ -132,6 +132,6 @@ describe('NumberField Component', () => { warnings: [], setFieldValue: jest.fn(), }); - expect(screen.getByLabelText('Weight (kg) (0-200)')).toBeInTheDocument(); + expect(screen.getByLabelText('Weight (0 - 200 kg)')).toBeInTheDocument(); }); }); From 43ee1f0008077104daad387d4dd3b4c25780b443 Mon Sep 17 00:00:00 2001 From: D-matz Date: Sun, 23 Mar 2025 10:45:14 -0500 Subject: [PATCH 4/7] concept type from esm-framwork, spacing for unit/range cases, field label i18n, unit/range test cases --- .../inputs/number/number.component.tsx | 21 +++--- src/components/inputs/number/number.test.tsx | 72 +++++++++++++++++++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/components/inputs/number/number.component.tsx b/src/components/inputs/number/number.component.tsx index ecccc1dc..296c830d 100644 --- a/src/components/inputs/number/number.component.tsx +++ b/src/components/inputs/number/number.component.tsx @@ -10,27 +10,27 @@ 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) => { +const extractFieldUnitsAndRange = (concept?: Concept): string => { if (!concept) { return ''; } const { hiAbsolute, lowAbsolute, units } = concept; - const displayUnit = units ? ` ${units}` : ''; - + const displayUnits = units ? ` ${units}` : ''; const hasLowerLimit = lowAbsolute != null; const hasUpperLimit = hiAbsolute != null; if (hasLowerLimit && hasUpperLimit) { - return ` (${lowAbsolute} - ${hiAbsolute} ${displayUnit})`; + return `(${lowAbsolute} - ${hiAbsolute}${displayUnits})`; } else if (hasUpperLimit) { - return ` (<= ${hiAbsolute} ${displayUnit})`; + return `(<= ${hiAbsolute}${displayUnits})`; } else if (hasLowerLimit) { - return ` (>= ${lowAbsolute} ${displayUnit})`; + return `(>= ${lowAbsolute}${displayUnits})`; } - return units ? ` (${displayUnit})` : ''; + return units ? `(${units})` : ''; }; const NumberField: React.FC = ({ field, value, errors, warnings, setFieldValue }) => { @@ -83,7 +83,12 @@ const NumberField: React.FC = ({ field, value, errors, warn id={field.id} invalid={errors.length > 0} invalidText={errors[0]?.message} - label={} + label={} max={Number(field.questionOptions.max) || undefined} min={Number(field.questionOptions.min) || undefined} name={field.id} diff --git a/src/components/inputs/number/number.test.tsx b/src/components/inputs/number/number.test.tsx index c28b4f8f..20faef7d 100644 --- a/src/components/inputs/number/number.test.tsx +++ b/src/components/inputs/number/number.test.tsx @@ -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(), })); @@ -41,6 +52,34 @@ const numberFieldMockWithUnitsAndRange = { 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()); }; @@ -134,4 +173,37 @@ describe('NumberField Component', () => { }); 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(); + }); }); From 3cc336f90ef373fc5536467ecd3089fae139b857 Mon Sep 17 00:00:00 2001 From: D-matz Date: Wed, 26 Mar 2025 01:07:46 -0500 Subject: [PATCH 5/7] import isNil and type concept, default for translation and in test, only min test, cleaner hi/low from matchingConcept --- .../inputs/number/number.component.tsx | 10 ++++--- src/components/inputs/number/number.test.tsx | 29 +++++++++++++++++-- src/hooks/useFormFieldsMeta.ts | 17 +++++------ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/components/inputs/number/number.component.tsx b/src/components/inputs/number/number.component.tsx index 296c830d..00d31364 100644 --- a/src/components/inputs/number/number.component.tsx +++ b/src/components/inputs/number/number.component.tsx @@ -1,6 +1,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import { Layer, NumberInput } from '@carbon/react'; +import { type Concept } from '@openmrs/esm-framework'; import classNames from 'classnames'; +import { isNil } from 'lodash'; import { isTrue } from '../../../utils/boolean-utils'; import { shouldUseInlineLayout } from '../../../utils/form-helper'; import FieldValueView from '../../value/view/field-value-view.component'; @@ -10,7 +12,6 @@ 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 => { @@ -20,8 +21,8 @@ const extractFieldUnitsAndRange = (concept?: Concept): string => { const { hiAbsolute, lowAbsolute, units } = concept; const displayUnits = units ? ` ${units}` : ''; - const hasLowerLimit = lowAbsolute != null; - const hasUpperLimit = hiAbsolute != null; + const hasLowerLimit = !isNil(lowAbsolute); + const hasUpperLimit = !isNil(hiAbsolute); if (hasLowerLimit && hasUpperLimit) { return `(${lowAbsolute} - ${hiAbsolute}${displayUnits})`; @@ -83,7 +84,8 @@ const NumberField: React.FC = ({ field, value, errors, warn id={field.id} invalid={errors.length > 0} invalidText={errors[0]?.message} - label={ ({ useTranslation: () => ({ - t: (key, options) => { - if (options && 'fieldDescription' in options) { - return `${options.fieldDescription} ${options.unitsAndRange || ''}`.trim(); + t: (key, defaultValueOrOptions, options) => { + + if (typeof options === 'object' && 'unitsAndRange' in options) { + return `${options.fieldDescription} ${options.unitsAndRange}`; } + return key; } }) @@ -80,6 +82,16 @@ const numberFieldMockWithHiAbsoluteOnly = { }, }; +const numberFieldMockWithLowAbsoluteOnly = { + ...numberFieldMockWithUnitsAndRange, + meta: { + concept: { + lowAbsolute: 0, + } + }, +}; + + const renderNumberField = async (props) => { await act(() => render()); }; @@ -206,4 +218,15 @@ describe('NumberField Component', () => { }); expect(screen.getByLabelText('Weight (<= 200)')).toBeInTheDocument(); }); + + it('renders lowAbsolute only', async () => { + await renderNumberField({ + field: numberFieldMockWithLowAbsoluteOnly, + value: '', + errors: [], + warnings: [], + setFieldValue: jest.fn(), + }); + expect(screen.getByLabelText('Weight (>= 0)')).toBeInTheDocument(); + }); }); diff --git a/src/hooks/useFormFieldsMeta.ts b/src/hooks/useFormFieldsMeta.ts index 3f3e44ab..0f629e8a 100644 --- a/src/hooks/useFormFieldsMeta.ts +++ b/src/hooks/useFormFieldsMeta.ts @@ -11,17 +11,16 @@ 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; + const { lowAbsolute, hiAbsolute } = matchingConcept; + + if (lowAbsolute !== undefined) { + field.questionOptions.min = lowAbsolute; } - else if (matchingConcept.hiAbsolute != undefined) { - field.questionOptions.max = matchingConcept.hiAbsolute; + + if (hiAbsolute !== undefined) { + field.questionOptions.max = hiAbsolute; } } From 3e9a51c5384a7714eb222fee221c76745186b00c Mon Sep 17 00:00:00 2001 From: D-matz Date: Wed, 26 Mar 2025 01:10:46 -0500 Subject: [PATCH 6/7] allow test to work with default value object? --- src/components/inputs/number/number.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/inputs/number/number.test.tsx b/src/components/inputs/number/number.test.tsx index fd5b022a..9e174e61 100644 --- a/src/components/inputs/number/number.test.tsx +++ b/src/components/inputs/number/number.test.tsx @@ -7,7 +7,10 @@ jest.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key, defaultValueOrOptions, options) => { - if (typeof options === 'object' && 'unitsAndRange' in options) { + if (typeof defaultValueOrOptions === 'object' && 'fieldDescription' in defaultValueOrOptions) { + return `${defaultValueOrOptions.fieldDescription} ${defaultValueOrOptions.unitsAndRange}`; + } + else if (typeof options === 'object' && 'unitsAndRange' in options) { return `${options.fieldDescription} ${options.unitsAndRange}`; } From 9342a90dd343062d6a33ab9da326d236d971abee Mon Sep 17 00:00:00 2001 From: D-matz Date: Wed, 26 Mar 2025 01:16:02 -0500 Subject: [PATCH 7/7] recommended external modules second --- src/components/inputs/number/number.component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/inputs/number/number.component.tsx b/src/components/inputs/number/number.component.tsx index 00d31364..a056d379 100644 --- a/src/components/inputs/number/number.component.tsx +++ b/src/components/inputs/number/number.component.tsx @@ -1,14 +1,14 @@ import React, { useCallback, useMemo, useState } from 'react'; +import { isNil } from 'lodash'; +import { useTranslation } from 'react-i18next'; import { Layer, NumberInput } from '@carbon/react'; import { type Concept } from '@openmrs/esm-framework'; import classNames from 'classnames'; -import { isNil } from 'lodash'; import { isTrue } from '../../../utils/boolean-utils'; import { shouldUseInlineLayout } from '../../../utils/form-helper'; import FieldValueView from '../../value/view/field-value-view.component'; import { type FormFieldInputProps } from '../../../types'; import styles from './number.scss'; -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';