Skip to content

(feat)O3-2971: Adding conformation modal for an empty form. #464

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
42 changes: 42 additions & 0 deletions src/empty-form-conformation-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { ComposedModal, ModalHeader, ModalBody, ModalFooter, Button } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import styles from './empty-formconformation-modal.scss'

interface EmptyFormConfirmationModalProps {
onDiscard: () => void;
onConfirmation: () => void;
open: boolean;
}

const IncompleteFormConfirmationModal: React.FC<EmptyFormConfirmationModalProps> = ({
onDiscard,
onConfirmation,
open,
}) => {
const { t } = useTranslation();

return (
<ComposedModal open={open} onClose={onDiscard} className={styles.customModal} size="small">
<ModalHeader title={t('EmptyForm', 'Empty Form ')} />
<ModalBody>
<p >
{t(
'EmptyFormConfirmation',
'All fields are Empty. Are you sure you want to submit the form?',
)}
</p>
</ModalBody>
<ModalFooter >
<Button kind="secondary" onClick={onDiscard}>
{t('cancel', 'Cancel')}
</Button>
<Button kind="danger" onClick={onConfirmation}>
{t('confirm', 'Confirm')}
</Button>
</ModalFooter>
</ComposedModal>
);
};

export default IncompleteFormConfirmationModal;
13 changes: 13 additions & 0 deletions src/empty-formconformation-modal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@use '@carbon/react/scss/colors';


.customModal {
:global(.cds--modal-close) {
position: fixed;
}

:global( .cds--btn--primary) {
background-color: #f4f4f4;
}

}
19 changes: 19 additions & 0 deletions src/provider/form-factory-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { evaluatePostSubmissionExpression } from '../utils/post-submission-actio
import { type PostSubmissionActionMeta } from '../hooks/usePostSubmissionActions';
import { type TFunction } from 'react-i18next';
import { type SessionMode } from '../types';
import { Console } from 'console';

export function validateForm(context: FormContextProps) {
const {
Expand Down Expand Up @@ -46,6 +47,24 @@ export function validateForm(context: FormContextProps) {
return errors.length === 0;
}

export function validateEmptyFields(context: FormContextProps){
const {
formFields,
formFieldValidators,
patient,
sessionMode,
addInvalidField,
updateFormField,
methods: { getValues, trigger },
} = context;
const values = getValues();
return Object.values(values).every(value =>
value === null ||
value === undefined ||
(Array.isArray(value) && value.length === 0)
);
}

export async function processPostSubmissionActions(
postSubmissionHandlers: PostSubmissionActionMeta[],
submissionResults: OpenmrsResource[],
Expand Down
145 changes: 94 additions & 51 deletions src/provider/form-factory-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { type FormField, type FormSchema, type SessionMode } from '../types';
import { EncounterFormProcessor } from '../processors/encounter/encounter-form-processor';
import {
Expand All @@ -11,9 +11,10 @@ import {
} from '@openmrs/esm-framework';
import { type FormProcessorConstructor } from '../processors/form-processor';
import { type FormContextProps } from './form-provider';
import { processPostSubmissionActions, validateForm } from './form-factory-helper';
import { processPostSubmissionActions, validateForm, validateEmptyFields } from './form-factory-helper';
import { useTranslation } from 'react-i18next';
import { usePostSubmissionActions } from '../hooks/usePostSubmissionActions';
import IncompleteFormConfirmationModal from '../empty-form-conformation-modal';

interface FormFactoryProviderContextProps {
patient: fhir.Patient;
Expand Down Expand Up @@ -57,7 +58,7 @@ interface FormFactoryProviderProps {

const FormFactoryProviderContext = createContext<FormFactoryProviderContextProps | undefined>(undefined);

export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = React.memo(({
patient,
sessionMode,
sessionDate,
Expand All @@ -79,9 +80,72 @@ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
const layoutType = useLayoutType();
const { isSubmitting, setIsSubmitting, onSubmit, onError, handleClose } = formSubmissionProps;
const postSubmissionHandlers = usePostSubmissionActions(formJson.postSubmissionActions);
const [isEmptyFormModalOpen, setIsEmptyFormModalOpen] = useState(false);

const abortController = new AbortController();

const handleFormSubmission = useCallback(
async (forms: FormContextProps[]) => {
try {
const results = await Promise.all(
forms.map((formContext) => formContext.processor.processSubmission(formContext, abortController)),
);

formSubmissionProps.setIsSubmitting(false);

if (sessionMode === 'edit') {
showSnackbar({
title: t('updatedRecord', 'Record updated'),
subtitle: t('updatedRecordDescription', 'The patient encounter was updated'),
kind: 'success',
isLowContrast: true,
});
} else {
showSnackbar({
title: t('submittedForm', 'Form submitted'),
subtitle: t('submittedFormDescription', 'Form submitted successfully'),
kind: 'success',
isLowContrast: true,
});
}

if (postSubmissionHandlers) {
await processPostSubmissionActions(postSubmissionHandlers, results, patient, sessionMode, t);
}

hideFormCollapseToggle();
if (onSubmit) {
onSubmit(results);
} else {
handleClose();
}
} catch (errorObject: Error | ToastDescriptor | any) {
setIsSubmitting(false);
if (errorObject instanceof Error) {
showToast({
title: t('errorProcessingFormSubmission', 'Error processing form submission'),
kind: 'error',
description: errorObject.message,
critical: true,
});
} else {
showToast(errorObject);
}
}
},
[
abortController,
formSubmissionProps,
sessionMode,
t,
postSubmissionHandlers,
patient,
hideFormCollapseToggle,
onSubmit,
handleClose,
],
);

const registerForm = useCallback((formId: string, isSubForm: boolean, context: FormContextProps) => {
if (isSubForm) {
subForms.current[formId] = context;
Expand All @@ -95,62 +159,34 @@ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
EncounterFormProcessor: EncounterFormProcessor,
});

const handleIncompleteFormConfirmation = useCallback(() => {
const forms = [rootForm.current, ...Object.values(subForms.current)];
handleFormSubmission(forms); // To Use the reusable function
setIsEmptyFormModalOpen(false);
}, [handleFormSubmission]);

const handleIncompleteFormDiscard = useCallback(() => {
setIsEmptyFormModalOpen(false);
setIsSubmitting(false);
}, [setIsSubmitting]);

useEffect(() => {
if (isSubmitting) {
// TODO: find a dynamic way of managing the form processing order
const forms = [rootForm.current, ...Object.values(subForms.current)];
// validate all forms
const isValid = forms.every((formContext) => validateForm(formContext));
const isEmpty = forms.some((formContext) => validateEmptyFields(formContext));

if (isValid) {
Promise.all(forms.map((formContext) => formContext.processor.processSubmission(formContext, abortController)))
.then(async (results) => {
formSubmissionProps.setIsSubmitting(false);
if (sessionMode === 'edit') {
showSnackbar({
title: t('updatedRecord', 'Record updated'),
subtitle: t('updatedRecordDescription', 'The patient encounter was updated'),
kind: 'success',
isLowContrast: true,
});
} else {
showSnackbar({
title: t('submittedForm', 'Form submitted'),
subtitle: t('submittedFormDescription', 'Form submitted successfully'),
kind: 'success',
isLowContrast: true,
});
}
if (postSubmissionHandlers) {
await processPostSubmissionActions(postSubmissionHandlers, results, patient, sessionMode, t);
}
hideFormCollapseToggle();
if (onSubmit) {
onSubmit(results);
} else {
handleClose();
}
})
.catch((errorObject: Error | ToastDescriptor) => {
setIsSubmitting(false);
if (errorObject instanceof Error) {
showToast({
title: t('errorProcessingFormSubmission', 'Error processing form submission'),
kind: 'error',
description: errorObject.message,
critical: true,
});
} else {
showToast(errorObject);
}
});
if (isEmpty) {
setIsEmptyFormModalOpen(true);
} else {
handleFormSubmission(forms);
}
} else {
setIsSubmitting(false);
}
}
return () => {
abortController.abort();
};
}, [isSubmitting]);
}, [isSubmitting, handleFormSubmission]);

return (
<FormFactoryProviderContext.Provider
Expand All @@ -170,10 +206,17 @@ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
handleConfirmQuestionDeletion,
setIsFormDirty,
}}>
{isEmptyFormModalOpen && (
<IncompleteFormConfirmationModal
open={isEmptyFormModalOpen}
onDiscard={handleIncompleteFormDiscard}
onConfirmation={handleIncompleteFormConfirmation}
/>
)}
{formProcessors.current && children}
</FormFactoryProviderContext.Provider>
);
};
});

export const useFormFactory = () => {
const context = useContext(FormFactoryProviderContext);
Expand Down
Loading