-
Notifications
You must be signed in to change notification settings - Fork 251
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(feat) O3-4040: Enable deleting program enrollments
This PR makes it possible to delete program enrollments. It adds a delete button to the program overview and detailed summary components' overflow menus. When clicked, a modal is opened to confirm the deletion.
- Loading branch information
1 parent
bad2f7a
commit c8f8885
Showing
16 changed files
with
393 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
packages/esm-patient-programs-app/src/programs/delete-program.modal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import React, { useCallback, useState } from 'react'; | ||
import { Button, InlineLoading, ModalBody, ModalFooter, ModalHeader } from '@carbon/react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { showSnackbar, getCoreTranslation } from '@openmrs/esm-framework'; | ||
import { deleteProgramEnrollment, useEnrollments } from './programs.resource'; | ||
import styles from './delete-program.scss'; | ||
|
||
interface DeleteProgramProps { | ||
closeDeleteModal: () => void; | ||
programEnrollmentId: string; | ||
patientUuid: string; | ||
} | ||
|
||
const DeleteProgramModal: React.FC<DeleteProgramProps> = ({ closeDeleteModal, programEnrollmentId, patientUuid }) => { | ||
const { t } = useTranslation(); | ||
const { mutateEnrollments } = useEnrollments(patientUuid); | ||
const [isDeleting, setIsDeleting] = useState(false); | ||
|
||
const handleDelete = useCallback(async () => { | ||
setIsDeleting(true); | ||
try { | ||
await deleteProgramEnrollment(programEnrollmentId); | ||
await mutateEnrollments(); | ||
closeDeleteModal(); | ||
showSnackbar({ | ||
isLowContrast: true, | ||
kind: 'success', | ||
title: t('programEnrollmentDeleted', 'Program enrollment deleted'), | ||
}); | ||
} catch (error) { | ||
showSnackbar({ | ||
isLowContrast: false, | ||
kind: 'error', | ||
title: t('errorDeletingProgram', 'Error deleting program enrollment'), | ||
subtitle: error?.responseBody?.message ?? error?.message, | ||
}); | ||
} finally { | ||
setIsDeleting(false); | ||
} | ||
}, [closeDeleteModal, programEnrollmentId, t, mutateEnrollments]); | ||
|
||
return ( | ||
<div> | ||
<ModalHeader | ||
closeModal={closeDeleteModal} | ||
title={t('deletePatientProgramEnrollment', 'Delete program enrollment')} | ||
/> | ||
<ModalBody> | ||
<p>{t('deleteModalConfirmationText', 'Are you sure you want to delete this program enrollment?')}</p> | ||
</ModalBody> | ||
<ModalFooter> | ||
<Button kind="secondary" onClick={closeDeleteModal}> | ||
{getCoreTranslation('cancel', 'Cancel')} | ||
</Button> | ||
<Button className={styles.deleteButton} kind="danger" onClick={handleDelete} disabled={isDeleting}> | ||
{isDeleting ? ( | ||
<InlineLoading description={t('deleting', 'Deleting') + '...'} /> | ||
) : ( | ||
<span>{getCoreTranslation('confirm', 'Confirm')}</span> | ||
)} | ||
</Button> | ||
</ModalFooter> | ||
</div> | ||
); | ||
}; | ||
|
||
export default DeleteProgramModal; |
12 changes: 12 additions & 0 deletions
12
packages/esm-patient-programs-app/src/programs/delete-program.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
@use '@carbon/layout'; | ||
@use '@carbon/type'; | ||
|
||
.deleteButton { | ||
:global(.cds--inline-loading) { | ||
min-height: layout.$spacing-05; | ||
} | ||
|
||
:global(.cds--inline-loading__text) { | ||
@include type.type-style('body-01'); | ||
} | ||
} |
98 changes: 98 additions & 0 deletions
98
packages/esm-patient-programs-app/src/programs/delete-program.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { type FetchResponse, showSnackbar } from '@openmrs/esm-framework'; | ||
import { mockPatient } from 'tools'; | ||
import { deleteProgramEnrollment, useEnrollments } from './programs.resource'; | ||
import DeleteProgramModal from './delete-program.modal'; | ||
|
||
jest.mock('./programs.resource', () => ({ | ||
deleteProgramEnrollment: jest.fn(), | ||
useEnrollments: jest.fn(), | ||
})); | ||
|
||
const mockDeleteProgramEnrollment = jest.mocked(deleteProgramEnrollment); | ||
const mockShowSnackbar = jest.mocked(showSnackbar); | ||
const mockUseEnrollments = jest.mocked(useEnrollments); | ||
const mockMutateEnrollments = jest.fn(); | ||
|
||
mockUseEnrollments.mockImplementation( | ||
() => | ||
({ | ||
mutateEnrollments: mockMutateEnrollments, | ||
}) as unknown as ReturnType<typeof useEnrollments>, | ||
); | ||
|
||
const testProps = { | ||
programEnrollmentId: '123', | ||
patientUuid: mockPatient.id, | ||
}; | ||
|
||
const closeDeleteModalMock = jest.fn(); | ||
|
||
const renderDeleteProgramModal = () => { | ||
return render( | ||
<DeleteProgramModal | ||
closeDeleteModal={closeDeleteModalMock} | ||
programEnrollmentId={testProps.programEnrollmentId} | ||
patientUuid={testProps.patientUuid} | ||
/>, | ||
); | ||
}; | ||
|
||
describe('DeleteProgramModal', () => { | ||
it('renders modal with delete confirmation text ', () => { | ||
renderDeleteProgramModal(); | ||
|
||
expect(screen.getByRole('heading', { name: /delete program enrollment/i })).toBeInTheDocument(); | ||
expect(screen.getByText(/are you sure you want to delete this program enrollment?/i)).toBeInTheDocument(); | ||
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); | ||
expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument(); | ||
}); | ||
|
||
it('Calls closeDeleteModal when cancel button is clicked', async () => { | ||
const user = userEvent.setup(); | ||
|
||
renderDeleteProgramModal(); | ||
|
||
await user.click(screen.getByRole('button', { name: /cancel/i })); | ||
|
||
expect(closeDeleteModalMock).toHaveBeenCalled(); | ||
}); | ||
|
||
it('clicking the delete button deletes the program enrollment', async () => { | ||
const user = userEvent.setup(); | ||
mockDeleteProgramEnrollment.mockResolvedValue({ ok: true } as unknown as FetchResponse); | ||
|
||
renderDeleteProgramModal(); | ||
|
||
await user.click(screen.getByRole('button', { name: /confirm/i })); | ||
|
||
expect(mockDeleteProgramEnrollment).toHaveBeenCalledTimes(1); | ||
expect(mockDeleteProgramEnrollment).toHaveBeenCalledWith(testProps.programEnrollmentId); | ||
expect(mockShowSnackbar).toHaveBeenCalledWith({ | ||
isLowContrast: true, | ||
kind: 'success', | ||
title: expect.stringMatching(/program enrollment deleted/i), | ||
}); | ||
}); | ||
|
||
it('renders an error notification when the delete action fails', async () => { | ||
const user = userEvent.setup(); | ||
mockDeleteProgramEnrollment.mockRejectedValue(new Error('Internal server error')); | ||
|
||
renderDeleteProgramModal(); | ||
|
||
await user.click(screen.getByRole('button', { name: /confirm/i })); | ||
|
||
expect(mockDeleteProgramEnrollment).toHaveBeenCalledTimes(1); | ||
expect(mockDeleteProgramEnrollment).toHaveBeenCalledWith(testProps.programEnrollmentId); | ||
expect(mockMutateEnrollments).not.toHaveBeenCalled(); | ||
expect(mockShowSnackbar).toHaveBeenCalledWith({ | ||
isLowContrast: false, | ||
kind: 'error', | ||
title: expect.stringMatching(/error deleting program enrollment/i), | ||
subtitle: 'Internal server error', | ||
}); | ||
}); | ||
}); |
56 changes: 56 additions & 0 deletions
56
packages/esm-patient-programs-app/src/programs/programs-action-menu.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import React, { useCallback } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react'; | ||
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; | ||
import { showModal, useLayoutType } from '@openmrs/esm-framework'; | ||
import styles from './programs-action-menu.scss'; | ||
|
||
interface ProgramActionsProps { | ||
patientUuid: string; | ||
programEnrollmentId: string; | ||
} | ||
|
||
export const ProgramsActionMenu = ({ patientUuid, programEnrollmentId }: ProgramActionsProps) => { | ||
const { t } = useTranslation(); | ||
const isTablet = useLayoutType() === 'tablet'; | ||
|
||
const launchEditProgramsForm = useCallback( | ||
() => launchPatientWorkspace('programs-form-workspace', { programEnrollmentId }), | ||
[programEnrollmentId], | ||
); | ||
|
||
const launchDeleteProgramDialog = useCallback(() => { | ||
const dispose = showModal('program-delete-confirmation-modal', { | ||
closeDeleteModal: () => dispose(), | ||
programEnrollmentId, | ||
patientUuid, | ||
size: 'sm', | ||
}); | ||
}, [programEnrollmentId, patientUuid]); | ||
|
||
return ( | ||
<Layer className={styles.layer}> | ||
<OverflowMenu | ||
align="left" | ||
aria-label={t('editOrDeleteProgram', 'Edit or delete program')} | ||
flipped | ||
size={isTablet ? 'lg' : 'sm'} | ||
> | ||
<OverflowMenuItem | ||
className={styles.menuItem} | ||
id="editProgram" | ||
itemText={t('edit', 'Edit')} | ||
onClick={launchEditProgramsForm} | ||
/> | ||
<OverflowMenuItem | ||
className={styles.menuItem} | ||
id="deleteProgam" | ||
hasDivider | ||
isDelete | ||
itemText={t('delete', 'Delete')} | ||
onClick={launchDeleteProgramDialog} | ||
/> | ||
</OverflowMenu> | ||
</Layer> | ||
); | ||
}; |
11 changes: 11 additions & 0 deletions
11
packages/esm-patient-programs-app/src/programs/programs-action-menu.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
.layer { | ||
height: 100%; | ||
|
||
:global(.cds--overflow-menu) { | ||
min-height: unset; | ||
} | ||
} | ||
|
||
.menuItem { | ||
max-width: none; | ||
} |
73 changes: 73 additions & 0 deletions
73
packages/esm-patient-programs-app/src/programs/programs-action-menu.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import React from 'react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { render, screen, waitFor } from '@testing-library/react'; | ||
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; | ||
import { showModal, useLayoutType } from '@openmrs/esm-framework'; | ||
import { mockPatient } from 'tools'; | ||
import { ProgramsActionMenu } from './programs-action-menu.component'; | ||
|
||
const mockShowModal = jest.mocked(showModal); | ||
const mockUseLayoutType = jest.mocked(useLayoutType); | ||
|
||
jest.mock('@openmrs/esm-patient-common-lib', () => ({ | ||
launchPatientWorkspace: jest.fn(), | ||
})); | ||
|
||
const testProps = { | ||
programEnrollmentId: '123', | ||
patientUuid: mockPatient.id, | ||
}; | ||
|
||
const renderProgramActionsMenu = () => { | ||
return render( | ||
<ProgramsActionMenu patientUuid={testProps.patientUuid} programEnrollmentId={testProps.programEnrollmentId} />, | ||
); | ||
}; | ||
|
||
describe('ProgramActionsMenu', () => { | ||
beforeEach(() => { | ||
mockUseLayoutType.mockReturnValue('small-desktop'); // or 'large-desktop' or 'tablet' | ||
}); | ||
|
||
it('renders an overflow menu with edit and delete actions', async () => { | ||
const user = userEvent.setup(); | ||
renderProgramActionsMenu(); | ||
|
||
const overflowMenuButton = screen.getByRole('button', { name: /options/i }); | ||
await user.click(overflowMenuButton); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText('Edit')).toBeInTheDocument(); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText('Delete')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('launches edit program form when edit button is clicked', async () => { | ||
const user = userEvent.setup(); | ||
renderProgramActionsMenu(); | ||
await user.click(screen.getByRole('button')); | ||
await user.click(screen.getByText('Edit')); | ||
|
||
expect(launchPatientWorkspace).toHaveBeenCalledWith('programs-form-workspace', { | ||
programEnrollmentId: testProps.programEnrollmentId, | ||
}); | ||
}); | ||
|
||
it('launches delete program dialog when delete option is clicked', async () => { | ||
const user = userEvent.setup(); | ||
renderProgramActionsMenu(); | ||
|
||
await user.click(screen.getByRole('button')); | ||
await user.click(screen.getByText('Delete')); | ||
|
||
expect(mockShowModal).toHaveBeenCalledWith('program-delete-confirmation-modal', { | ||
closeDeleteModal: expect.any(Function), | ||
patientUuid: testProps.patientUuid, | ||
programEnrollmentId: testProps.programEnrollmentId, | ||
size: 'sm', | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.