Skip to content
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

(feat) Implement the inline date picker with showDate and shownDateOptions #285

Merged
merged 7 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 76 additions & 31 deletions src/components/encounter/encounter-form.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import type {
import FormPage from '../page/form-page.component';
import { FormContext } from '../../form-context';
import {
cascadeVisibityToChildFields,
evalConditionalRequired,
evaluateDisabled,
evaluateFieldReadonlyProp,
evaluateHide,
findConceptByReference,
findPagesWithErrors,
} from '../../utils/form-helper';
Expand Down Expand Up @@ -98,6 +99,7 @@ const EncounterForm: React.FC<EncounterFormProps> = ({
const [invalidFields, setInvalidFields] = useState([]);
const [initValues, setInitValues] = useState({});
const { isLoading: isLoadingPatientPrograms, patientPrograms } = usePatientPrograms(patient?.id, formJson);
const getFormField = useCallback((id: string) => fields.find((field) => field.id === id), [fields]);

const layoutType = useLayoutType();
const { encounterContext, isLoadingContextDependencies } = useMemo(() => {
Expand All @@ -118,6 +120,7 @@ const EncounterForm: React.FC<EncounterFormProps> = ({
setEncounterProvider,
setEncounterLocation,
setEncounterRole,
getFormField,
};
return {
encounterContext: contextObject,
Expand Down Expand Up @@ -228,7 +231,14 @@ const EncounterForm: React.FC<EncounterFormProps> = ({
setFields(
flattenedFields.map((field) => {
if (field.hide) {
evalHide({ value: field, type: 'field' }, flattenedFields, tempInitialValues);
evaluateHide(
{ value: field, type: 'field' },
flattenedFields,
tempInitialValues,
sessionMode,
patient,
evaluateExpression,
);
} else {
field.isHidden = false;
}
Expand All @@ -237,6 +247,18 @@ const EncounterForm: React.FC<EncounterFormProps> = ({
} else {
field.isRequired = isTrue(field.required);
}
if (field.disabled) {
field.isDisabled = field.disabled?.disableWhenExpression
? evaluateDisabled(
{ value: field, type: 'field' },
flattenedFields,
tempInitialValues,
sessionMode,
patient,
evaluateExpression,
)
: isTrue(field.disabled);
}

field.questionOptions.answers
?.filter((answer) => !isEmpty(answer.hide?.hideWhenExpression))
Expand Down Expand Up @@ -336,13 +358,27 @@ const EncounterForm: React.FC<EncounterFormProps> = ({

form?.pages?.forEach((page) => {
if (page.hide) {
evalHide({ value: page, type: 'page' }, flattenedFields, tempInitialValues);
evaluateHide(
{ value: page, type: 'page' },
flattenedFields,
tempInitialValues,
sessionMode,
patient,
evaluateExpression,
);
} else {
page.isHidden = false;
}
page?.sections?.forEach((section) => {
if (section.hide) {
evalHide({ value: section, type: 'section' }, flattenedFields, tempInitialValues);
evaluateHide(
{ value: section, type: 'section' },
flattenedFields,
tempInitialValues,
sessionMode,
patient,
evaluateExpression,
);
} else {
section.isHidden = false;
}
Expand Down Expand Up @@ -373,30 +409,6 @@ const EncounterForm: React.FC<EncounterFormProps> = ({
}
}, [isLoadingEncounter, isLoadingPreviousEncounter]);

const evalHide = (node, allFields: FormField[], allValues: Record<string, any>) => {
const { value, type } = node;
const isHidden = evaluateExpression(value['hide']?.hideWhenExpression, node, allFields, allValues, {
mode: sessionMode,
patient,
});
node.value.isHidden = isHidden;
if (type == 'field' && node.value?.questions?.length) {
node.value?.questions.forEach((question) => {
question.isParentHidden = isHidden;
});
}
// cascade visibility
if (type == 'page') {
value['sections'].forEach((section) => {
section.isParentHidden = isHidden;
cascadeVisibityToChildFields(isHidden, section, allFields);
});
}
if (type == 'section') {
cascadeVisibityToChildFields(isHidden, value, allFields);
}
};

useEffect(() => {
if (invalidFields?.length) {
setPagesWithErrors(findPagesWithErrors(scrollablePages, invalidFields));
Expand Down Expand Up @@ -608,7 +620,26 @@ const EncounterForm: React.FC<EncounterFormProps> = ({
}
// evaluate hide
if (dependant.hide) {
evalHide({ value: dependant, type: 'field' }, fields, { ...values, [fieldName]: value });
evaluateHide(
{ value: dependant, type: 'field' },
fields,
{ ...values, [fieldName]: value },
sessionMode,
patient,
evaluateExpression,
);
}

// evaluate disabled
if (typeof dependant.disabled === 'object' && dependant.disabled.disableWhenExpression) {
dependant.isDisabled = evaluateDisabled(
{ value: dependant, type: 'field' },
fields,
{ ...values, [fieldName]: value },
sessionMode,
patient,
evaluateExpression,
);
}
// evaluate conditional required
if (typeof dependant.required === 'object' && dependant.required?.type === 'conditionalRequired') {
Expand Down Expand Up @@ -697,7 +728,14 @@ const EncounterForm: React.FC<EncounterFormProps> = ({
for (let i = 0; i < form.pages.length; i++) {
const section = form.pages[i].sections.find((section, _sectionIndex) => section.label == dependant);
if (section) {
evalHide({ value: section, type: 'section' }, fields, { ...values, [fieldName]: value });
evaluateHide(
{ value: section, type: 'section' },
fields,
{ ...values, [fieldName]: value },
sessionMode,
patient,
evaluateExpression,
);
if (isTrue(section.isHidden)) {
section.questions.forEach((field) => {
field.isParentHidden = true;
Expand All @@ -711,7 +749,14 @@ const EncounterForm: React.FC<EncounterFormProps> = ({
if (field.pageDependants) {
field.pageDependants?.forEach((dep) => {
const dependant = form.pages.find((f) => f.label == dep);
evalHide({ value: dependant, type: 'page' }, fields, { ...values, [fieldName]: value });
evaluateHide(
{ value: dependant, type: 'page' },
fields,
{ ...values, [fieldName]: value },
sessionMode,
patient,
evaluateExpression,
);
if (isTrue(dependant.isHidden)) {
dependant.sections.forEach((section) => {
section.questions.forEach((field) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const ContentSwitcher: React.FC<FormFieldProps> = ({ question, onChange, handler
name={option.concept || option.value}
text={option.label}
key={index}
disabled={question.disabled}
disabled={question.isDisabled}
/>
))}
</CdsContentSwitcher>
Expand Down
2 changes: 1 addition & 1 deletion src/components/inputs/date/date.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ const DateField: React.FC<FormFieldProps> = ({ question, onChange, handler, prev
// When we manually trigger an onchange event using the 'fireEvent' lib, the handler below will
// be triggered as opposed to the former handler that only gets triggered at runtime.
onChange={(e) => onDateChange([dayjs(e.target.value, placeholder.toUpperCase()).toDate()])}
disabled={question.disabled}
disabled={question.isDisabled}
invalid={errors.length > 0}
invalidText={errors[0]?.message}
warn={warnings.length > 0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const mockOpenmrsFetch = jest.fn();
global.ResizeObserver = require('resize-observer-polyfill');
const visit = mockVisit;
const patientUUID = '8673ee4f-e2ab-4077-ba55-4980f408773e';
const locale = window.i18next.language == 'en' ? 'en-GB' : window.i18next.language;

jest.mock('@openmrs/esm-framework', () => {
const originalModule = jest.requireActual('@openmrs/esm-framework');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const MultiSelect: React.FC<FormFieldProps> = ({ question, onChange, handler, pr
}
key={counter}
itemToString={(item) => (item ? item.label : ' ')}
disabled={question.disabled}
disabled={question.isDisabled}
invalid={errors.length > 0}
invalidText={errors[0]?.message}
warn={warnings.length > 0}
Expand Down
2 changes: 1 addition & 1 deletion src/components/inputs/number/number.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const NumberField: React.FC<FormFieldProps> = ({ question, onChange, handler, pr
size="lg"
hideSteppers={true}
onWheel={(e) => e.target.blur()}
disabled={question.disabled}
disabled={question.isDisabled}
readOnly={question.readonly}
className={classNames(styles.controlWidthConstrained, styles.boldedLabel)}
warn={warnings.length > 0}
Expand Down
2 changes: 1 addition & 1 deletion src/components/inputs/radio/radio.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const Radio: React.FC<FormFieldProps> = ({ question, onChange, handler, previous
question.isRequired ? <RequiredFieldLabel label={t(question.label)} /> : <span>{t(question.label)}</span>
}
className={styles.boldedLegend}
disabled={question.disabled}
disabled={question.isDisabled}
invalid={errors.length > 0}>
<RadioButtonGroup name={question.id} valueSelected={field.value} onChange={handleChange} orientation="vertical">
{question.questionOptions.answers
Expand Down
2 changes: 1 addition & 1 deletion src/components/inputs/select/dropdown.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const Dropdown: React.FC<FormFieldProps> = ({ question, onChange, handler, previ
itemToString={itemToString}
selectedItem={field.value}
onChange={({ selectedItem }) => handleChange(selectedItem)}
disabled={question.disabled}
disabled={question.isDisabled}
readOnly={question.readonly}
invalid={errors.length > 0}
invalidText={errors[0]?.message}
Expand Down
2 changes: 1 addition & 1 deletion src/components/inputs/text-area/text-area.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const TextArea: React.FC<FormFieldProps> = ({ question, onChange, handler, previ
value={field.value || ''}
onFocus={() => setPreviousValue(field.value)}
rows={question.questionOptions.rows || 4}
disabled={question.disabled}
disabled={question.isDisabled}
readOnly={question.readonly}
invalid={errors.length > 0}
invalidText={errors[0]?.message}
Expand Down
2 changes: 1 addition & 1 deletion src/components/inputs/text/text.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const TextField: React.FC<FormFieldProps> = ({ question, onChange, handler, prev
}
name={question.id}
value={field.value || ''}
disabled={question.disabled}
disabled={question.isDisabled}
readOnly={Boolean(question.readonly)}
invalid={errors.length > 0}
invalidText={errors[0]?.message}
Expand Down
2 changes: 1 addition & 1 deletion src/components/inputs/text/text.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, fireEvent, screen, cleanup, act } from '@testing-library/react';
import { render, fireEvent, screen, act } from '@testing-library/react';
import { Formik } from 'formik';
import { type EncounterContext, FormContext } from '../../../form-context';
import { type FormField } from '../../../types';
Expand Down
2 changes: 1 addition & 1 deletion src/components/inputs/toggle/toggle.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const Toggle: React.FC<FormFieldProps> = ({ question, onChange, handler, previou
labelB={question.questionOptions.toggleOptions.labelTrue}
onToggle={handleChange}
toggled={!!field.value}
disabled={question.disabled}
disabled={question.isDisabled}
readOnly={question.readonly}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ const UiSelectExtended: React.FC<FormFieldProps> = ({ question, handler, onChang
isProcessingSelection.current = true;
handleChange(selectedItem?.uuid);
}}
disabled={question.disabled}
disabled={question.isDisabled}
readOnly={question.readonly}
invalid={errors.length > 0}
invalidText={errors.length && errors[0].message}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const UnspecifiedField: React.FC<FormFieldProps> = ({ question, onChange, handle
value={t('unspecified', 'Unspecified')}
onChange={handleOnChange}
checked={field.value}
disabled={question.disabled}
disabled={question.isDisabled}
/>
</div>
)
Expand Down
1 change: 1 addition & 0 deletions src/components/repeat/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function cloneRepeatField(srcField: FormField, value: OpenmrsResource, id
originalGroupMembersIds,
);
}

if (childField.validators?.length) {
childField.validators.forEach((validator) => {
if (validator.type === 'js_expression') {
Expand Down
1 change: 1 addition & 0 deletions src/form-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface EncounterContext {
initValues?: Record<string, any>;
patientIdentifier?: PatientIdentifier;
patientPrograms?: Array<PatientProgram>;
getFormField?: (id: string) => FormField;
}

export const FormContext = React.createContext<FormContextProps | undefined>(undefined);
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TestOrderSubmissionHandler } from '../../submission-handlers/testOrderH
import { ObsSubmissionHandler } from '../../submission-handlers/obsHandler';
import { EncounterRoleHandler } from '../../submission-handlers/encounterRoleHandler';
import { ProgramStateHandler } from '../../submission-handlers/programStateHandler';
import { InlineDateHandler } from '../../submission-handlers/inlineDateHandler';

/**
* @internal
Expand Down Expand Up @@ -44,6 +45,11 @@ export const inbuiltFieldSubmissionHandlers: Array<RegistryItem<SubmissionHandle
component: PatientIdentifierHandler,
type: 'patientIdentifier',
},
{
name: 'InlineDateHandler',
component: InlineDateHandler,
type: 'inlineDate',
},
{
name: 'controlHandler',
component: ControlHandler,
Expand Down
41 changes: 41 additions & 0 deletions src/submission-handlers/inlineDateHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type EncounterContext, type FormField, type OpenmrsEncounter, type SubmissionHandler } from '..';
import { hasSubmission } from '../utils/common-utils';
import { isEmpty } from '../validators/form-validator';

export const InlineDateHandler: SubmissionHandler = {
handleFieldSubmission(field: FormField, value: any, context: EncounterContext) {
const targetField = context.getFormField(field.meta.targetField);
if (hasSubmission(targetField) && !isEmpty(value)) {
const refinedDate = value instanceof Date ? new Date(value.getTime() - value.getTimezoneOffset() * 60000) : value;
targetField.meta.submission.newValue.obsDatetime = refinedDate;
}
return targetField;
},
getInitialValue: (
encounter: OpenmrsEncounter,
field: FormField,
allFormFields: Array<FormField>,
context: EncounterContext,
) => {
if (encounter) {
const dateField = field.id.split('-');
const correspondingQuestion = allFormFields.find(field => field.id === dateField[0]);
const dateValue = correspondingQuestion?.meta?.previousValue?.obsDatetime;

return new Date(dateValue);
}
return null;
},
getDisplayValue: (field: FormField, value: any) => {
return value;
},
getPreviousValue: (field: FormField, encounter: OpenmrsEncounter, allFormFields: Array<FormField>) => {
if (encounter) {
const dateField = field.id.split('-');
const correspondingQuestion = allFormFields.find(field => field.id === dateField[0]);
const dateValue = correspondingQuestion?.meta?.previousValue?.obsDatetime;
return new Date(dateValue);
}
return null;
},
}
Loading