Skip to content

Commit

Permalink
(feat) O3-3165: Prompt user when deleting repeated question (#267)
Browse files Browse the repository at this point in the history
* (feat): add modal for confirmation when deleting question

* update test cases

* rename components to convention

* remove style sheet and update carbon version

* (fix): rename selectedQuestion state

* (fix): change primary button color of modal

* (refactor) Move modal to patient chart and have fallback for no modal

* (refactor): add confirmation modal handler as prop

* (fix): add error handler

* (fix): Replace global store with react context

* (refactor): Changed context name to be more generic

* Slightly cleaner @carbon/react version pinning

---------

Co-authored-by: Ian <ian.c.bacher@gmail.com>
  • Loading branch information
2 people authored and vasharma05 committed May 27, 2024
1 parent 437f39e commit 451dbc1
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 220 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,8 @@
"*.{ts,tsx}": "eslint --cache --fix --max-warnings 0",
"*.{css,scss,ts,tsx}": "prettier --write --list-different"
},
"resolutions": {
"@carbon/react": "1.37.0"
},
"packageManager": "yarn@4.2.2"
}
46 changes: 35 additions & 11 deletions src/components/repeat/repeat.component.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { FormGroup } from '@carbon/react';
import { useFormikContext } from 'formik';
import { useTranslation } from 'react-i18next';
import { type FormField, type FormFieldProps, type RenderType } from '../../types';
import type { FormField, FormFieldProps, RenderType } from '../../types';
import { evaluateAsyncExpression, evaluateExpression } from '../../utils/expression-runner';
import { isEmpty } from '../../validators/form-validator';
import styles from './repeat.scss';
Expand All @@ -11,6 +11,8 @@ import { FormContext } from '../../form-context';
import { getFieldControlWithFallback } from '../section/helpers';
import { clearSubmission } from '../../utils/common-utils';
import RepeatControls from './repeat-controls.component';
import { createErrorHandler } from '@openmrs/esm-framework';
import { ExternalFunctionContext } from '../../external-function-context';

const renderingByTypeMap: Record<string, RenderType> = {
obsGroup: 'group',
Expand All @@ -26,6 +28,8 @@ const Repeat: React.FC<FormFieldProps> = ({ question, onChange, handler }) => {
const [rows, setRows] = useState([]);
const [fieldComponent, setFieldComponent] = useState(null);

const { handleConfirmQuestionDeletion } = useContext(ExternalFunctionContext);

useEffect(() => {
const repeatedFields = allFormFields.filter(
(field) =>
Expand Down Expand Up @@ -105,6 +109,21 @@ const Repeat: React.FC<FormFieldProps> = ({ question, onChange, handler }) => {
setRows(rows.filter((q) => q.id !== question.id));
};

const onClickDeleteQuestion = (question: Readonly<FormField>) => {
if (handleConfirmQuestionDeletion && typeof handleConfirmQuestionDeletion === 'function') {
const result = handleConfirmQuestionDeletion(question);
if (result && typeof result.then === 'function' && typeof result.catch === 'function') {
result.then(() => removeNthRow(question)).catch(() => createErrorHandler());
} else if (typeof result === 'boolean') {
result && removeNthRow(question);
} else {
removeNthRow(question);
}
} else {
removeNthRow(question);
}
};

const nodes = useMemo(() => {
return fieldComponent
? rows.map((question, index) => {
Expand All @@ -122,7 +141,7 @@ const Repeat: React.FC<FormFieldProps> = ({ question, onChange, handler }) => {
rows={rows}
questionIndex={index}
handleDelete={() => {
removeNthRow(question);
onClickDeleteQuestion(question);
}}
handleAdd={() => {
const nextCount = counter + 1;
Expand All @@ -139,14 +158,19 @@ const Repeat: React.FC<FormFieldProps> = ({ question, onChange, handler }) => {
if (question.isHidden || !nodes || !hasVisibleField(question)) {
return null;
}
return isGrouped ? (
<div className={styles.container}>
<FormGroup legendText={t(question.label)} className={styles.boldLegend}>
{nodes}
</FormGroup>
</div>
) : (
<div>{nodes}</div>

return (
<React.Fragment>
{isGrouped ? (
<div className={styles.container}>
<FormGroup legendText={t(question.label)} className={styles.boldLegend}>
{nodes}
</FormGroup>
</div>
) : (
<div>{nodes}</div>
)}
</React.Fragment>
);
};

Expand Down
8 changes: 8 additions & 0 deletions src/external-function-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import type { FormField } from './types';

export type ExternalFunctionContextProps = {
handleConfirmQuestionDeletion: (question: Readonly<FormField>) => Promise<void>;
};

export const ExternalFunctionContext = React.createContext<ExternalFunctionContextProps | undefined>(undefined);
165 changes: 85 additions & 80 deletions src/form-engine.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { I18nextProvider, useTranslation } from 'react-i18next';
import * as Yup from 'yup';
import { showSnackbar, useSession, type Visit } from '@openmrs/esm-framework';
import { init, teardown } from './lifecycle';
import type { FormPage as FormPageProps, FormSchema, SessionMode } from './types';
import type { FormField, FormPage as FormPageProps, FormSchema, SessionMode } from './types';
import { extractErrorMessagesFromResponse, reportError } from './utils/error-utils';
import { useFormJson } from './hooks/useFormJson';
import { usePostSubmissionAction } from './hooks/usePostSubmissionAction';
Expand All @@ -21,6 +21,7 @@ import Loader from './components/loaders/loader.component';
import MarkdownWrapper from './components/inputs/markdown/markdown-wrapper.component';
import PatientBanner from './components/patient-banner/patient-banner.component';
import Sidebar from './components/sidebar/sidebar.component';
import { ExternalFunctionContext } from './external-function-context';
import styles from './form-engine.scss';

interface FormProps {
Expand All @@ -33,6 +34,7 @@ interface FormProps {
onSubmit?: () => void;
onCancel?: () => void;
handleClose?: () => void;
handleConfirmQuestionDeletion?: (question: Readonly<FormField>) => Promise<void>;
mode?: SessionMode;
meta?: {
/**
Expand Down Expand Up @@ -79,6 +81,7 @@ const FormEngine: React.FC<FormProps> = ({
onSubmit,
onCancel,
handleClose,
handleConfirmQuestionDeletion,
formSessionIntent,
meta,
encounterUuid,
Expand Down Expand Up @@ -257,91 +260,93 @@ const FormEngine: React.FC<FormProps> = ({
setIsFormDirty(props.dirty);

return (
<Form className={classNames('cds--form', styles.formEngine)} ref={ref}>
{isLoadingPatient || isLoadingFormJson ? (
<Loader />
) : (
<div className={styles.container}>
{isLoadingFormDependencies && (
<div className={styles.linearActivity}>
<div className={styles.indeterminate}></div>
</div>
)}
<div className={styles.body}>
{showSidebar && (
<Sidebar
isFormSubmitting={isSubmitting}
pagesWithErrors={pagesWithErrors}
scrollablePages={scrollablePages}
selectedPage={selectedPage}
mode={mode}
onCancel={onCancel}
handleClose={handleClose}
values={props.values}
setValues={props.setValues}
allowUnspecifiedAll={formJson.allowUnspecifiedAll}
defaultPage={formJson.defaultPage}
hideFormCollapseToggle={hideFormCollapseToggle}
/>
<ExternalFunctionContext.Provider value={{ handleConfirmQuestionDeletion }}>
<Form className={classNames('cds--form', styles.formEngine)} ref={ref}>
{isLoadingPatient || isLoadingFormJson ? (
<Loader />
) : (
<div className={styles.container}>
{isLoadingFormDependencies && (
<div className={styles.linearActivity}>
<div className={styles.indeterminate}></div>
</div>
)}
<div className={styles.content}>
{showPatientBanner && <PatientBanner patient={patient} hideActionsOverflow />}
{refinedFormJson.markdown && (
<div className={styles.markdownContainer}>
<MarkdownWrapper markdown={refinedFormJson.markdown} />
</div>
)}
<div className={styles.contentBody}>
<EncounterForm
formJson={refinedFormJson}
patient={patient}
formSessionDate={formSessionDate}
provider={currentProvider}
role={encounterRole?.uuid}
location={location}
visit={visit}
values={props.values}
isFormExpanded={isFormExpanded}
sessionMode={sessionMode}
<div className={styles.body}>
{showSidebar && (
<Sidebar
isFormSubmitting={isSubmitting}
pagesWithErrors={pagesWithErrors}
scrollablePages={scrollablePages}
setAllInitialValues={setInitialValues}
allInitialValues={initialValues}
setScrollablePages={setScrollablePages}
setPagesWithErrors={setPagesWithErrors}
setIsLoadingFormDependencies={setIsLoadingFormDependencies}
setFieldValue={props.setFieldValue}
setSelectedPage={setSelectedPage}
handlers={handlers}
workspaceLayout={workspaceLayout}
isSubmitting={isSubmitting}
setIsSubmitting={setIsSubmitting}
selectedPage={selectedPage}
mode={mode}
onCancel={onCancel}
handleClose={handleClose}
values={props.values}
setValues={props.setValues}
allowUnspecifiedAll={formJson.allowUnspecifiedAll}
defaultPage={formJson.defaultPage}
hideFormCollapseToggle={hideFormCollapseToggle}
/>
</div>
{showButtonSet && (
<ButtonSet className={styles.minifiedButtons}>
<Button
kind="secondary"
onClick={() => {
onCancel && onCancel();
handleClose && handleClose();
hideFormCollapseToggle();
}}>
{mode === 'view' ? t('close', 'Close') : t('cancel', 'Cancel')}
</Button>
<Button type="submit" disabled={mode === 'view' || isSubmitting}>
{isSubmitting ? (
<InlineLoading description={t('submitting', 'Submitting') + '...'} />
) : (
<span>{`${t('save', 'Save')}`}</span>
)}
</Button>
</ButtonSet>
)}
<div className={styles.content}>
{showPatientBanner && <PatientBanner patient={patient} hideActionsOverflow />}
{refinedFormJson.markdown && (
<div className={styles.markdownContainer}>
<MarkdownWrapper markdown={refinedFormJson.markdown} />
</div>
)}
<div className={styles.contentBody}>
<EncounterForm
formJson={refinedFormJson}
patient={patient}
formSessionDate={formSessionDate}
provider={currentProvider}
role={encounterRole?.uuid}
location={location}
visit={visit}
values={props.values}
isFormExpanded={isFormExpanded}
sessionMode={sessionMode}
scrollablePages={scrollablePages}
setAllInitialValues={setInitialValues}
allInitialValues={initialValues}
setScrollablePages={setScrollablePages}
setPagesWithErrors={setPagesWithErrors}
setIsLoadingFormDependencies={setIsLoadingFormDependencies}
setFieldValue={props.setFieldValue}
setSelectedPage={setSelectedPage}
handlers={handlers}
workspaceLayout={workspaceLayout}
isSubmitting={isSubmitting}
setIsSubmitting={setIsSubmitting}
/>
</div>
{showButtonSet && (
<ButtonSet className={styles.minifiedButtons}>
<Button
kind="secondary"
onClick={() => {
onCancel && onCancel();
handleClose && handleClose();
hideFormCollapseToggle();
}}>
{mode === 'view' ? t('close', 'Close') : t('cancel', 'Cancel')}
</Button>
<Button type="submit" disabled={mode === 'view' || isSubmitting}>
{isSubmitting ? (
<InlineLoading description={t('submitting', 'Submitting') + '...'} />
) : (
<span>{`${t('save', 'Save')}`}</span>
)}
</Button>
</ButtonSet>
)}
</div>
</div>
</div>
</div>
)}
</Form>
)}
</Form>
</ExternalFunctionContext.Provider>
);
}}
</Formik>
Expand Down
Loading

0 comments on commit 451dbc1

Please sign in to comment.