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) O3-4040: Add delete ability to program enrollments #2133

Open
wants to merge 71 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
cbbff83
(chore) Created a program actions menu component that displays the "e…
PiusKariuki Dec 5, 2024
b81e251
feat: created a program actions menu test component testing edit and …
PiusKariuki Dec 5, 2024
80c62ed
feat: created a delete modal test component
PiusKariuki Dec 5, 2024
14e044a
(feat) registered the delete confirmation dialog
PiusKariuki Dec 5, 2024
2ede494
(chore) generated translation keys
PiusKariuki Dec 5, 2024
a66f460
(feat) created a HTTP request function to delete programs
PiusKariuki Dec 5, 2024
a6684fa
(feat): created a dialog that shows up when deleting a program
PiusKariuki Dec 5, 2024
576c2ad
(feat) added a React component to render the edit-program button
PiusKariuki Dec 5, 2024
ac33454
(feat) swapped the edit button for an Action menu that houses the edi…
PiusKariuki Dec 5, 2024
8ddee66
(feat) added styles for the action table cell
PiusKariuki Dec 5, 2024
4ee37bb
(chore) edited the modal prop name to ensure the test runs correctly
PiusKariuki Dec 5, 2024
a683be1
(chore) included a name for the overflow menu item for testing purposes
PiusKariuki Dec 5, 2024
470f200
(chore) typo fix
PiusKariuki Dec 5, 2024
5ea6b91
(chore) edited the test component to accommodate the extra step invol…
PiusKariuki Dec 5, 2024
f4b550f
(chore) optimize imports
PiusKariuki Dec 5, 2024
3236b0d
(chore) removed styling that caused overflow
PiusKariuki Dec 5, 2024
7ab780f
chore: moved the delete enrollment component to the modals array
PiusKariuki Dec 16, 2024
de252d4
chore: removed useless styling
PiusKariuki Dec 16, 2024
9d444b6
chore: renamed Dialog to Modal
PiusKariuki Dec 16, 2024
49e52d3
chore: more descriptive text for a successful deletion on the snackbar
PiusKariuki Dec 16, 2024
847261c
chore: more descriptive text for an error during deletion on the snac…
PiusKariuki Dec 16, 2024
4352fe8
chore: more descriptive modal header for deleting a program
PiusKariuki Dec 16, 2024
17a7079
chore: more descriptive modal body for deleting a program
PiusKariuki Dec 16, 2024
7840ee8
chore: updated tests
PiusKariuki Dec 16, 2024
9869444
chore: updated tests
PiusKariuki Dec 16, 2024
421d61f
chore: using getCoreTranslation
PiusKariuki Dec 16, 2024
f871f46
chore: updated the modal name
PiusKariuki Dec 16, 2024
cc0cfb0
chore: updated the translation strings
PiusKariuki Dec 16, 2024
6762df0
chore: updated the tests
PiusKariuki Dec 16, 2024
db768d8
chore: removed unnecessary mock for react-i18
PiusKariuki Dec 19, 2024
7223a13
chore: used mockPatient instead of arbitrary values
PiusKariuki Dec 19, 2024
04842e4
chore: using a11y roles to make tests more robust
PiusKariuki Jan 3, 2025
dfe5356
chore: using userEvent instead of fireEvent
PiusKariuki Jan 3, 2025
05f8b3d
refactor: renderDeleteProgramModal reused on all tests
PiusKariuki Jan 3, 2025
1f17b78
refactor: refactored the 'clicking the delete button deletes the prog…
PiusKariuki Jan 3, 2025
3a64344
refactor: refactored the 'handles delete action error' test
PiusKariuki Jan 3, 2025
a0bc18a
refactor: removed redundant abortController for the "delete program" …
PiusKariuki Jan 3, 2025
bdae7c6
refactor: removed redundant abortController for the "delete program" …
PiusKariuki Jan 3, 2025
b1a153b
chore: added an asynchronous step to the delete function to mutate th…
PiusKariuki Jan 3, 2025
17c07ea
chore: moved setting state outside the try/catch block
PiusKariuki Jan 3, 2025
70f7a04
chore: changed to lowercase
PiusKariuki Jan 3, 2025
f1498e9
chore: sentence case for modal header
PiusKariuki Jan 3, 2025
fb93d08
chore: setting up the "userEvent" in the "Calls closeDeleteModal when…
PiusKariuki Jan 3, 2025
9dd408f
chore: removed redundant translation mock
PiusKariuki Jan 3, 2025
0cacc23
chore: using "userEvent" in place of "fireEvent"
PiusKariuki Jan 3, 2025
06413b5
chore: mocking the core translation
PiusKariuki Jan 3, 2025
c7b7962
chore: import optimization
PiusKariuki Jan 3, 2025
3b7127e
chore: import mockPatient and userEvent
PiusKariuki Jan 3, 2025
1654142
chore: removed this simulation
PiusKariuki Jan 3, 2025
6f98879
chore: overriding useLayoutType
PiusKariuki Jan 3, 2025
92d545c
chore: returned the classname in the "action" table cell
PiusKariuki Jan 3, 2025
26d2a49
fix: fixed failing e2e tests
PiusKariuki Jan 4, 2025
1876c76
Merge branch 'main' into feat/delete-programs-feature
denniskigen Jan 6, 2025
53bbbe2
chore: regex of matching case instead of strict typing it.
PiusKariuki Jan 8, 2025
6da388d
Merge remote-tracking branch 'origin/feat/delete-programs-feature' in…
PiusKariuki Jan 8, 2025
529b542
chore: removed redundant block to clear all mocks
PiusKariuki Jan 8, 2025
4dd1699
chore: removed partial mock
PiusKariuki Jan 8, 2025
fbc226a
chore: removed partial mock
PiusKariuki Jan 8, 2025
5262ad8
chore: using jest.mocked for type safety
PiusKariuki Jan 8, 2025
4990847
chore: ensure type safety in mock implementation for useEnrollments
PiusKariuki Jan 8, 2025
f9db4ff
refactor: removed repeated lines of code
PiusKariuki Jan 9, 2025
bd588b9
refactor: renamed the "program-actions-menu.test.tsx"
PiusKariuki Jan 9, 2025
4363f81
refactor: renamed the spec to a friendlier name
PiusKariuki Jan 9, 2025
cf60eeb
refactor: renamed the "overflowMenuButton" and added a name argument …
PiusKariuki Jan 9, 2025
902454b
refactor: renamed the "ProgramsActionMenu" component
PiusKariuki Jan 9, 2025
be6cfb9
refactor: passing down row props to the datatable rows
PiusKariuki Jan 9, 2025
d71d7f3
refactor: remove unnecessary classname
PiusKariuki Jan 9, 2025
72826b1
refactor: added necessary classname
PiusKariuki Jan 9, 2025
7dad9e1
refactor: renamed test file for consistency
PiusKariuki Jan 9, 2025
218b024
refactor: renamed test file for consistency
PiusKariuki Jan 9, 2025
e7db186
Merge branch 'main' into feat/delete-programs-feature
denniskigen Jan 10, 2025
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
3 changes: 2 additions & 1 deletion e2e/pages/program-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export class ProgramsPage {
constructor(readonly page: Page) {}

readonly programsTable = () => this.page.getByRole('table', { name: /program enrollments/i });
readonly editProgramButton = () => this.page.getByRole('button', { name: /edit program/i });
readonly overflowButton = () => this.page.getByRole('button', { name: /options/i });
readonly editProgramButton = () => this.page.getByRole('menuitem', { name: /edit/i });

async goTo(patientUuid: string) {
await this.page.goto(`patient/${patientUuid}/chart/Programs`);
Expand Down
1 change: 1 addition & 0 deletions e2e/specs/program-enrollment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ test('Add and edit a program enrollment', async ({ page }) => {
});

await test.step('When I click on the `Edit` button of the created program', async () => {
await programsPage.overflowButton().click();
await programsPage.editProgramButton().click();
});

Expand Down
5 changes: 5 additions & 0 deletions packages/esm-patient-programs-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ export const programsDashboardLink =

// t('programEnrollmentWorkspaceTitle', 'Record program enrollment')
export const programsFormWorkspace = getAsyncLifecycle(() => import('./programs/programs-form.workspace'), options);

export const deleteProgramConfirmationModal = getAsyncLifecycle(
() => import('./programs/delete-program.modal'),
options,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { deleteProgramEnrollment, useEnrollments } from './programs.resource';
import DeleteProgramModal from './delete-program.modal';
import { type FetchResponse, showSnackbar } from '@openmrs/esm-framework';
import { mockPatient } from 'tools';

jest.mock('./programs.resource', () => ({
deleteProgramEnrollment: jest.fn(),
useEnrollments: jest.fn(),
}));

const mockDeleteProgramEnrollment = jest.mocked(deleteProgramEnrollment);
const mockShowSnackbar = jest.mocked(showSnackbar);

// for type safety
const mockUseEnrollments = jest.mocked(useEnrollments);

// a mock for mutation
const mockMutateEnrollments = jest.fn();

// mock the implementation
mockUseEnrollments.mockImplementation(
() =>
({
mutateEnrollments: mockMutateEnrollments,
}) as unknown as ReturnType<typeof useEnrollments>,
);

const testProps = {
programEnrollmentId: '123',
patientUuid: mockPatient.id,
};

denniskigen marked this conversation as resolved.
Show resolved Hide resolved
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',
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useCallback, useState } from 'react';
import { Button, InlineLoading, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { deleteProgramEnrollment, useEnrollments } from './programs.resource';
import { showSnackbar, getCoreTranslation } from '@openmrs/esm-framework';

interface DeleteProgramProps {
closeDeleteModal: () => void;
programEnrollmentId: string;
patientUuid: string;
}

const DeleteProgramModal: React.FC<DeleteProgramProps> = ({ closeDeleteModal, programEnrollmentId, patientUuid }) => {
const { t } = useTranslation();
const [isDeleting, setIsDeleting] = useState(false);
const { mutateEnrollments } = useEnrollments(patientUuid);

const handleDelete = useCallback(async () => {
setIsDeleting(true);
try {
denniskigen marked this conversation as resolved.
Show resolved Hide resolved
await deleteProgramEnrollment(programEnrollmentId);
await mutateEnrollments();
closeDeleteModal();
showSnackbar({
isLowContrast: true,
kind: 'success',
title: t('programDeleted', 'Program enrollment deleted'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: t('programDeleted', 'Program enrollment deleted'),
title: t('programEnrollmentDeleted', 'Program enrollment deleted'),

});
} catch (error) {
showSnackbar({
isLowContrast: false,
kind: 'error',
title: t('errorDeletingProgram', 'Error deleting program enrollment'),
subtitle: error?.message,
});
} finally {
setIsDeleting(false);
}
}, [closeDeleteModal, programEnrollmentId, t, mutateEnrollments]);
denniskigen marked this conversation as resolved.
Show resolved Hide resolved
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 kind="danger" onClick={handleDelete} disabled={isDeleting}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Button kind="danger" onClick={handleDelete} disabled={isDeleting}>
<Button className={styles.deleteButton} kind="danger" onClick={handleDelete} disabled={isDeleting}>

And the styles for that would be:

@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');
  }
}

{isDeleting ? (
<InlineLoading description={t('deleting', 'Deleting') + '...'} />
) : (
<span>{getCoreTranslation('confirm', 'Confirm')}</span>
)}
</Button>
</ModalFooter>
</div>
);
};

export default DeleteProgramModal;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and all it's related files should be renamed to programs-action-menu for consistency.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { showModal, useLayoutType } from '@openmrs/esm-framework';
import { ProgramsActionsMenu } from './programs-actions-menu.component';
import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
import { mockPatient } from 'tools';
import userEvent from '@testing-library/user-event';

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(
<ProgramsActionsMenu 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,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useTranslation } from 'react-i18next';
import { showModal, useLayoutType } from '@openmrs/esm-framework';
import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react';
import React, { useCallback } from 'react';
import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';

interface ProgramActionsProps {
patientUuid: string;
programEnrollmentId: string;
}

export const ProgramsActionsMenu = ({ patientUuid, programEnrollmentId }: ProgramActionsProps) => {
const { t } = useTranslation();
const isTablet = useLayoutType() === 'tablet';

const launchEditProgramsForm = useCallback(
() => launchPatientWorkspace('programs-form-workspace', { programEnrollmentId }),
[programEnrollmentId],
);

const launchDeleteProgramDialog = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can wrap this in a useCallback.

const dispose = showModal('program-delete-confirmation-modal', {
closeDeleteModal: () => dispose(),
programEnrollmentId,
patientUuid,
});
};

return (
<Layer>
Copy link
Member

@denniskigen denniskigen Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Layer>
<Layer className={styles.layer}>

And then the associated styles would look something like this:

.layer {
  height: 100%;

  :global(.cds--overflow-menu) {
    min-height: unset;
  }
}

.menuItem {
  max-width: none;
}

This will make your UI visually consistent with other flows like the Conditions edit experience.

<OverflowMenu
name={t('editOrDeleteProgram', 'Edit or delete program')}
aria-label={t('editOrDeleteProgram', 'Edit or delete program')}
size={isTablet ? 'lg' : 'sm'}
flipped
>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<OverflowMenu
name={t('editOrDeleteProgram', 'Edit or delete program')}
aria-label={t('editOrDeleteProgram', 'Edit or delete program')}
size={isTablet ? 'lg' : 'sm'}
flipped
>
<OverflowMenu
align="left"
aria-label={t('editOrDeleteProgram', 'Edit or delete program')}
flipped
size={isTablet ? 'lg' : 'sm'}
>

The OverflowMenu should be left-aligned so it's content does not flow over the width of the datatable. Additionally, we should remove the name prop as it's not a known prop of the OverflowMenu component.

<OverflowMenuItem id="editProgram" onClick={launchEditProgramsForm} itemText={t('edit', 'Edit')} />
<OverflowMenuItem id="deleteProgam" onClick={launchDeleteProgramDialog} itemText={t('delete', 'Delete')} />
Copy link
Member

@denniskigen denniskigen Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<OverflowMenuItem id="editProgram" onClick={launchEditProgramsForm} itemText={t('edit', 'Edit')} />
<OverflowMenuItem id="deleteProgam" onClick={launchDeleteProgramDialog} itemText={t('delete', 'Delete')} />
<OverflowMenuItem
className={styles.menuItem}
id="editProgram"
onClick={launchEditProgramsForm}
itemText={t('edit', 'Edit')}
/>
<OverflowMenuItem
className={styles.menuItem}
hasDivider
id="deleteProgam"
isDelete
onClick={launchDeleteProgramDialog}
itemText={t('delete', 'Delete')}
/>

</OverflowMenu>
</Layer>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { type ComponentProps, useCallback, useMemo } from 'react';
import classNames from 'classnames';
import { type TFunction, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import {
Button,
DataTable,
Expand All @@ -19,7 +19,6 @@ import {
import {
AddIcon,
type ConfigObject,
EditIcon,
formatDate,
formatDatetime,
useConfig,
Expand All @@ -29,16 +28,12 @@ import {
import { CardHeader, EmptyState, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
import { findLastState, usePrograms } from './programs.resource';
import styles from './programs-detailed-summary.scss';
import { ProgramsActionsMenu } from './programs-actions-menu.component';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the correct name for this component and file should be ProgramsActionMenu and programs-action-menu.component respectively.


interface ProgramsDetailedSummaryProps {
patientUuid: string;
}

interface ProgramEditButtonProps {
programEnrollmentId: string;
t: TFunction;
}

const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = ({ patientUuid }) => {
const { t } = useTranslation();
const { hideAddProgramButton, showProgramStatusField } = useConfig<ConfigObject>();
Expand Down Expand Up @@ -158,10 +153,12 @@ const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = ({ patie
{rows.map((row, i) => (
<TableRow key={row.id}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<TableRow key={row.id}>
<TableRow key={row.id} {...getRowProps({ row })}>

{row.cells.map((cell) => (
<TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell>
<TableCell className="cds--table-column-menu" key={cell.id}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<TableCell className="cds--table-column-menu" key={cell.id}>
<TableCell key={cell.id}>

cds--table-column-menu isn't strictly necessary here.

{cell.value?.content ?? cell.value}
</TableCell>
))}
<TableCell className="cds--table-column-menu">
PiusKariuki marked this conversation as resolved.
Show resolved Hide resolved
<ProgramEditButton programEnrollmentId={enrollments[i]?.uuid} t={t} />
<TableCell>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is here to ensure the OverflowMenu takes up the appropriate amount of space.

Suggested change
<TableCell>
<TableCell className="cds--table-column-menu">

<ProgramsActionsMenu patientUuid={patientUuid} programEnrollmentId={enrollments[i]?.uuid} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add this to the ProgramsOverview component as well for consistency.

</TableCell>
</TableRow>
))}
Expand All @@ -176,25 +173,4 @@ const ProgramsDetailedSummary: React.FC<ProgramsDetailedSummaryProps> = ({ patie
return <EmptyState displayText={displayText} headerTitle={headerTitle} launchForm={launchProgramsForm} />;
};

function ProgramEditButton({ programEnrollmentId, t }: ProgramEditButtonProps) {
const isTablet = useLayoutType() === 'tablet';
const launchEditProgramsForm = useCallback(
() => launchPatientWorkspace('programs-form-workspace', { programEnrollmentId }),
[programEnrollmentId],
);

return (
<Button
aria-label="edit program"
kind="ghost"
renderIcon={(props: ComponentProps<typeof EditIcon>) => <EditIcon size={16} {...props} />}
iconDescription={t('editProgram', 'Edit Program')}
onClick={launchEditProgramsForm}
hasIconOnly
tooltipPosition="left"
size={isTablet ? 'lg' : 'sm'}
/>
);
}

export default ProgramsDetailedSummary;
Loading
Loading