@kenyaemr/esm-patient-list-management-app 7.0.2-pre.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/dist/130.js +2 -0
  3. package/dist/130.js.LICENSE.txt +3 -0
  4. package/dist/130.js.map +1 -0
  5. package/dist/139.js +1 -0
  6. package/dist/139.js.map +1 -0
  7. package/dist/255.js +2 -0
  8. package/dist/255.js.LICENSE.txt +9 -0
  9. package/dist/255.js.map +1 -0
  10. package/dist/271.js +1 -0
  11. package/dist/319.js +1 -0
  12. package/dist/382.js +1 -0
  13. package/dist/382.js.map +1 -0
  14. package/dist/443.js +1 -0
  15. package/dist/443.js.map +1 -0
  16. package/dist/460.js +1 -0
  17. package/dist/548.js +1 -0
  18. package/dist/548.js.map +1 -0
  19. package/dist/574.js +1 -0
  20. package/dist/591.js +2 -0
  21. package/dist/591.js.LICENSE.txt +32 -0
  22. package/dist/591.js.map +1 -0
  23. package/dist/635.js +1 -0
  24. package/dist/635.js.map +1 -0
  25. package/dist/644.js +1 -0
  26. package/dist/729.js +1 -0
  27. package/dist/729.js.map +1 -0
  28. package/dist/757.js +1 -0
  29. package/dist/784.js +2 -0
  30. package/dist/784.js.LICENSE.txt +9 -0
  31. package/dist/784.js.map +1 -0
  32. package/dist/788.js +1 -0
  33. package/dist/807.js +1 -0
  34. package/dist/833.js +1 -0
  35. package/dist/930.js +2 -0
  36. package/dist/930.js.LICENSE.txt +35 -0
  37. package/dist/930.js.map +1 -0
  38. package/dist/kenyaemr-esm-patient-list-management-app.js +1 -0
  39. package/dist/kenyaemr-esm-patient-list-management-app.js.buildmanifest.json +580 -0
  40. package/dist/kenyaemr-esm-patient-list-management-app.js.map +1 -0
  41. package/dist/main.js +2 -0
  42. package/dist/main.js.LICENSE.txt +45 -0
  43. package/dist/main.js.map +1 -0
  44. package/dist/routes.json +1 -0
  45. package/jest.config.js +3 -0
  46. package/package.json +56 -0
  47. package/src/add-patient/add-patient.component.tsx +271 -0
  48. package/src/add-patient/add-patient.scss +51 -0
  49. package/src/add-patient-to-patient-list-menu-item.component.tsx +48 -0
  50. package/src/add-patient-to-patient-list-menu-item.test.tsx +33 -0
  51. package/src/api/api-remote.ts +211 -0
  52. package/src/api/hooks.ts +150 -0
  53. package/src/api/types.ts +102 -0
  54. package/src/config-schema.ts +25 -0
  55. package/src/constants.ts +5 -0
  56. package/src/create-edit-patient-list/create-edit-list.component.tsx +170 -0
  57. package/src/create-edit-patient-list/create-edit-patient-list.scss +31 -0
  58. package/src/createDashboardLink.component.tsx +40 -0
  59. package/src/dashboard.meta.ts +5 -0
  60. package/src/declarations.d.ts +5 -0
  61. package/src/empty-state/empty-data-illustration.component.tsx +42 -0
  62. package/src/empty-state/empty-state.component.tsx +41 -0
  63. package/src/empty-state/empty-state.scss +24 -0
  64. package/src/error-state/error-state.component.tsx +35 -0
  65. package/src/error-state/error-state.scss +50 -0
  66. package/src/header/header.component.tsx +51 -0
  67. package/src/header/header.scss +52 -0
  68. package/src/illo.component.tsx +25 -0
  69. package/src/index.ts +41 -0
  70. package/src/list-details/list-details.component.tsx +201 -0
  71. package/src/list-details/list-details.scss +47 -0
  72. package/src/list-details/list-details.test.tsx +112 -0
  73. package/src/list-details/patient-list-detail.test.tsx +105 -0
  74. package/src/list-details-table/list-details-table.component.tsx +402 -0
  75. package/src/list-details-table/list-details-table.scss +143 -0
  76. package/src/list-details-table/list-details-table.test.tsx +94 -0
  77. package/src/lists-dashboard/lists-dashboard.component.tsx +104 -0
  78. package/src/lists-dashboard/lists-dashboard.scss +110 -0
  79. package/src/lists-dashboard/lists-dashboard.test.tsx +129 -0
  80. package/src/lists-table/custom-pagination.component.tsx +43 -0
  81. package/src/lists-table/custom-pagination.scss +67 -0
  82. package/src/lists-table/lists-table.component.tsx +317 -0
  83. package/src/lists-table/lists-table.scss +100 -0
  84. package/src/lists-table/lists-table.test.tsx +189 -0
  85. package/src/lists-table/use-pagination-info.component.tsx +35 -0
  86. package/src/offline.ts +31 -0
  87. package/src/overlay.component.tsx +49 -0
  88. package/src/overlay.scss +89 -0
  89. package/src/overlay.test.tsx +60 -0
  90. package/src/root.component.tsx +19 -0
  91. package/src/routes.json +36 -0
  92. package/src/style.scss +46 -0
  93. package/translations/am.json +84 -0
  94. package/translations/ar.json +84 -0
  95. package/translations/en.json +84 -0
  96. package/translations/es.json +84 -0
  97. package/translations/fr.json +84 -0
  98. package/translations/he.json +84 -0
  99. package/translations/km.json +84 -0
  100. package/translations/zh.json +84 -0
  101. package/translations/zh_CN.json +84 -0
  102. package/tsconfig.json +5 -0
  103. package/webpack.config.js +1 -0
@@ -0,0 +1,50 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .errorMessage {
6
+ @include type.type-style('heading-compact-02');
7
+
8
+ margin-top: 2.25rem;
9
+ margin-bottom: layout.$spacing-03;
10
+ }
11
+
12
+ .errorCopy {
13
+ margin-bottom: layout.$spacing-03;
14
+ @include type.type-style('body-01');
15
+ color: colors.$gray-70;
16
+ }
17
+
18
+ .desktopHeading {
19
+ h4 {
20
+ @include type.type-style('heading-compact-02');
21
+ color: colors.$gray-70;
22
+ }
23
+ }
24
+
25
+ .tabletHeading {
26
+ h4 {
27
+ @include type.type-style('heading-03');
28
+ color: colors.$gray-70;
29
+ }
30
+ }
31
+
32
+ .desktopHeading,
33
+ .tabletHeading {
34
+ text-align: left;
35
+ text-transform: capitalize;
36
+ margin-bottom: layout.$spacing-05;
37
+
38
+ h4:after {
39
+ content: '';
40
+ display: block;
41
+ width: 2rem;
42
+ padding-top: 0.188rem;
43
+ border-bottom: 0.375rem solid var(--brand-03);
44
+ }
45
+ }
46
+
47
+ .tile {
48
+ text-align: center;
49
+ border: 1px solid colors.$gray-20;
50
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button } from '@carbon/react';
4
+ import { Add, Calendar } from '@carbon/react/icons';
5
+ import { formatDate, navigate } from '@openmrs/esm-framework';
6
+ import Illustration from '../illo.component';
7
+ import styles from './header.scss';
8
+
9
+ const Header: React.FC = () => {
10
+ const { t } = useTranslation();
11
+ const newCohortUrl = window.getOpenmrsSpaBase() + 'home/patient-lists?new_cohort=true';
12
+
13
+ const handleShowNewListOverlay = () => {
14
+ // URL navigation is in place to know either to open the create list overlay or not
15
+ // The url /patient-list?new_cohort=true is being used in the "Add patient to list" widget
16
+ // in the patient chart. The button in the above mentioned widget "Create new list", navigates
17
+ // to /patient-list?new_cohort=true to open the overlay directly.
18
+ navigate({
19
+ to: newCohortUrl,
20
+ });
21
+ };
22
+
23
+ return (
24
+ <div className={styles.patientListHeader}>
25
+ <div className={styles.leftJustifiedItems}>
26
+ <Illustration />
27
+ <div className={styles.pageLabels}>
28
+ <p>{t('patientLists', 'Patient lists')}</p>
29
+ <p className={styles.pageName}>{t('home', 'Home')}</p>
30
+ </div>
31
+ </div>
32
+ <div className={styles.rightJustifiedItems}>
33
+ <div className={styles.date}>
34
+ <Calendar size={16} />
35
+ <span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
36
+ </div>
37
+ <Button
38
+ className={styles.newListButton}
39
+ kind="ghost"
40
+ iconDescription="Add"
41
+ renderIcon={(props) => <Add {...props} size={16} />}
42
+ onClick={handleShowNewListOverlay}
43
+ size="sm">
44
+ {t('newList', 'New list')}
45
+ </Button>
46
+ </div>
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export default Header;
@@ -0,0 +1,52 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '../style.scss';
4
+
5
+ .patientListHeader {
6
+ @include type.type-style('body-compact-02');
7
+ color: $text-02;
8
+ height: spacing.$spacing-12;
9
+ display: flex;
10
+ justify-content: space-between;
11
+ }
12
+
13
+ .leftJustifiedItems {
14
+ display: flex;
15
+ flex-direction: row;
16
+ align-items: center;
17
+ }
18
+
19
+ .rightJustifiedItems {
20
+ @include type.type-style('body-compact-02');
21
+ display: flex;
22
+ flex-direction: column;
23
+ color: $text-02;
24
+ padding: 1rem 0;
25
+ justify-content: space-between;
26
+ }
27
+
28
+ .date {
29
+ display: flex;
30
+ justify-content: flex-end;
31
+ align-items: center;
32
+ margin-right: 1rem;
33
+
34
+ svg {
35
+ margin-right: 0.5rem;
36
+ }
37
+ }
38
+
39
+ .pageName {
40
+ @include type.type-style('heading-04');
41
+ }
42
+
43
+ .pageLabels {
44
+ p:first-of-type {
45
+ margin-bottom: 0.25rem;
46
+ }
47
+ }
48
+
49
+ .newListButton {
50
+ align-self: flex-end;
51
+ width: fit-content;
52
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+
4
+ const Illustration: React.FC = () => {
5
+ const { t } = useTranslation();
6
+
7
+ return (
8
+ <svg width="92" height="94" viewBox="0 0 92 94" xmlns="http://www.w3.org/2000/svg">
9
+ <title>{t('emptyStateIllustration', 'Empty state illustration')}</title>
10
+ <g fill="none" fillRule="evenodd">
11
+ <path fill="#FFF" d="M0 0h92v94H0z" />
12
+ <path
13
+ d="M40 32c.84-.602 1.12-1.797 1-3 .12-5.005-3.96-9-9-9s-9.12 3.995-9 9c-.12 3.572 2.1 6.706 5 8-6.76 1.741-12 7.91-12 15v15h28V32h-4zM76 67V52c0-7.09-5.24-13.278-12-15 2.9-1.294 5.12-4.428 5-8 .12-5.005-3.96-9-9-9s-9.12 3.995-9 9c-.12 1.203.14 2.398 1 3h-4v35h28z"
14
+ fill="#CEE6E5"
15
+ />
16
+ <path
17
+ d="M32 75V60.312c0-7.402 5.24-13.59 12.3-15.216-3.2-1.39-5.42-4.523-5.42-8.166 0-4.935 4.08-8.93 9.12-8.93 5.04 0 9.12 3.995 9.12 8.93 0 3.642-2.22 6.776-5.42 8.166C58.76 46.741 64 52.91 64 60.313V75"
18
+ fill="#7BBCB9"
19
+ />
20
+ </g>
21
+ </svg>
22
+ );
23
+ };
24
+
25
+ export default Illustration;
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework';
2
+ import { configSchema } from './config-schema';
3
+ import { createDashboardLink } from './createDashboardLink.component';
4
+ import { dashboardMeta } from './dashboard.meta';
5
+ import { setupOffline } from './offline';
6
+ import rootComponent from './root.component';
7
+ import listDetailsTableComponent from './list-details-table/list-details-table.component';
8
+ import addPatientToPatientListMenuItemComponent from './add-patient-to-patient-list-menu-item.component';
9
+
10
+ const moduleName = '@kenyaemr/esm-patient-list-management-app';
11
+
12
+ const options = {
13
+ featureName: 'patient list',
14
+ moduleName,
15
+ };
16
+
17
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
18
+
19
+ export function startupApp() {
20
+ setupOffline();
21
+ defineConfigSchema(moduleName, configSchema);
22
+ }
23
+
24
+ export const root = getSyncLifecycle(rootComponent, options);
25
+
26
+ export const addPatientToListModal = getAsyncLifecycle(() => import('./add-patient/add-patient.component'), {
27
+ featureName: 'patient-actions-modal',
28
+ moduleName,
29
+ });
30
+
31
+ export const addPatientToPatientListMenuItem = getSyncLifecycle(addPatientToPatientListMenuItemComponent, {
32
+ featureName: 'patient-actions-slot',
33
+ moduleName,
34
+ });
35
+
36
+ export const patientListDashboardLink = getSyncLifecycle(createDashboardLink(dashboardMeta), options);
37
+
38
+ export const listDetailsTable = getSyncLifecycle(listDetailsTableComponent, {
39
+ featureName: 'patient-table',
40
+ moduleName,
41
+ });
@@ -0,0 +1,201 @@
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
+ import classNames from 'classnames';
3
+ import { useParams } from 'react-router-dom';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { OverflowMenuItem, Modal } from '@carbon/react';
6
+ import { OverflowMenuVertical } from '@carbon/react/icons';
7
+ import { navigate, formatDate, parseDate, showSnackbar, CustomOverflowMenu } from '@openmrs/esm-framework';
8
+ import { deletePatientList } from '../api/api-remote';
9
+ import { usePatientListDetails, usePatientListMembers } from '../api/hooks';
10
+ import CreateEditPatientList from '../create-edit-patient-list/create-edit-list.component';
11
+ import ListDetailsTable from '../list-details-table/list-details-table.component';
12
+ import styles from './list-details.scss';
13
+
14
+ interface ListDetails {
15
+ name: string;
16
+ identifier: string;
17
+ sex: string;
18
+ startDate: string;
19
+ uuid: string;
20
+ }
21
+
22
+ const ListDetails = () => {
23
+ const { t } = useTranslation();
24
+ const params = useParams();
25
+ const patientListUuid = params.patientListUuid;
26
+ const [currentPage, setPageCount] = useState(1);
27
+ const [currentPageSize, setCurrentPageSize] = useState(10);
28
+ const [searchString, setSearchString] = useState('');
29
+ const { listDetails, mutateListDetails } = usePatientListDetails(patientListUuid);
30
+ const { listMembers, isLoadingListMembers, mutateListMembers } = usePatientListMembers(
31
+ patientListUuid,
32
+ searchString,
33
+ (currentPage - 1) * currentPageSize,
34
+ currentPageSize,
35
+ );
36
+
37
+ const [showEditPatientListDetailOverlay, setEditPatientListDetailOverlay] = useState(false);
38
+ const [showDeleteConfirmationModal, setShowDeleteConfirmationModal] = useState(false);
39
+
40
+ const patients: Array<ListDetails> = useMemo(
41
+ () =>
42
+ listMembers
43
+ ? listMembers?.length
44
+ ? listMembers?.map((member) => ({
45
+ name: member?.patient?.person?.display,
46
+ identifier: member?.patient?.identifiers[0]?.identifier ?? null,
47
+ sex: member?.patient?.person?.gender,
48
+ startDate: member?.startDate ? formatDate(parseDate(member.startDate)) : null,
49
+ uuid: `${member?.patient?.uuid}`,
50
+ membershipUuid: member?.uuid,
51
+ }))
52
+ : []
53
+ : [],
54
+ [listMembers],
55
+ );
56
+
57
+ const headers = useMemo(
58
+ () => [
59
+ {
60
+ key: 'name',
61
+ header: t('name', 'Name'),
62
+ link: {
63
+ getUrl: (patient) =>
64
+ patient?.uuid ? `${window.spaBase}/patient/${patient?.uuid}/chart/` : window?.location?.href,
65
+ },
66
+ },
67
+ {
68
+ key: 'identifier',
69
+ header: t('identifier', 'Identifier'),
70
+ },
71
+ {
72
+ key: 'sex',
73
+ header: t('sex', 'Sex'),
74
+ },
75
+ {
76
+ key: 'startDate',
77
+ header: t('startDate', 'Start Date'),
78
+ },
79
+ ],
80
+ [t],
81
+ );
82
+
83
+ const handleDelete = useCallback(() => {
84
+ setShowDeleteConfirmationModal(true);
85
+ }, []);
86
+
87
+ const confirmDeletePatientList = useCallback(() => {
88
+ deletePatientList(patientListUuid)
89
+ .then(() => {
90
+ showSnackbar({
91
+ title: t('deleted', 'Deleted'),
92
+ subtitle: `${t('deletedPatientList', 'Deleted patient list')}: ${listDetails?.name}`,
93
+ kind: 'success',
94
+ isLowContrast: true,
95
+ });
96
+
97
+ navigate({ to: window.getOpenmrsSpaBase() + 'home/patient-lists' });
98
+ })
99
+ .catch((e) =>
100
+ showSnackbar({
101
+ title: t('errorDeletingList', 'Error deleting patient list'),
102
+ subtitle: e?.message,
103
+ kind: 'error',
104
+ }),
105
+ )
106
+ .finally(() => setShowDeleteConfirmationModal(false));
107
+ }, [patientListUuid, listDetails, t]);
108
+
109
+ return (
110
+ <main className={styles.container}>
111
+ <section className={styles.cohortHeader}>
112
+ <div data-testid="patientListHeader">
113
+ <h1 className={styles.productiveHeading03}>{listDetails?.name ?? '--'}</h1>
114
+ <h4 className={classNames(styles.bodyShort02, styles.marginTop)}>{listDetails?.description ?? '--'}</h4>
115
+ <div className={classNames(styles.text02, styles.bodyShort01, styles.marginTop)}>
116
+ {listDetails?.size} {t('patients', 'patients')} &middot;{' '}
117
+ <span className={styles.label01}>{t('createdOn', 'Created on')}:</span>{' '}
118
+ {listDetails?.startDate ? formatDate(parseDate(listDetails.startDate)) : null}
119
+ </div>
120
+ </div>
121
+ <div className={styles.overflowMenu}>
122
+ <CustomOverflowMenu
123
+ menuTitle={
124
+ <>
125
+ <span className={styles.actionsButtonText}>{t('actions', 'Actions')}</span>{' '}
126
+ <OverflowMenuVertical size={16} style={{ marginLeft: '0.5rem' }} />
127
+ </>
128
+ }>
129
+ <OverflowMenuItem
130
+ className={styles.menuItem}
131
+ itemText={t('editNameDescription', 'Edit name or description')}
132
+ onClick={() => setEditPatientListDetailOverlay(true)}
133
+ />
134
+ <OverflowMenuItem
135
+ className={styles.menuItem}
136
+ itemText={t('deletePatientList', 'Delete patient list')}
137
+ onClick={handleDelete}
138
+ isDelete
139
+ />
140
+ </CustomOverflowMenu>
141
+ </div>
142
+ </section>
143
+ <section>
144
+ <div className={styles.tableContainer}>
145
+ <ListDetailsTable
146
+ patients={patients}
147
+ columns={headers}
148
+ isLoading={isLoadingListMembers}
149
+ isFetching={!listMembers}
150
+ mutateListMembers={mutateListMembers}
151
+ mutateListDetails={mutateListDetails}
152
+ pagination={{
153
+ usePagination: listDetails?.size > currentPageSize,
154
+ currentPage,
155
+ onChange: ({ page, pageSize }) => {
156
+ setPageCount(page);
157
+ setCurrentPageSize(pageSize);
158
+ },
159
+ pageSize: 10,
160
+ totalItems: listDetails?.size,
161
+ pagesUnknown: true,
162
+ lastPage: patients?.length < currentPageSize || currentPage * currentPageSize === listDetails?.size,
163
+ }}
164
+ />
165
+ </div>
166
+ {showEditPatientListDetailOverlay && (
167
+ <CreateEditPatientList
168
+ close={() => setEditPatientListDetailOverlay(false)}
169
+ isEditing
170
+ patientListDetails={listDetails}
171
+ onSuccess={mutateListDetails}
172
+ />
173
+ )}
174
+ {showDeleteConfirmationModal && (
175
+ <Modal
176
+ open
177
+ danger
178
+ modalHeading={t('confirmDeletePatientList', 'Are you sure you want to delete this patient list?')}
179
+ primaryButtonText="Delete"
180
+ secondaryButtonText="Cancel"
181
+ onRequestClose={() => setShowDeleteConfirmationModal(false)}
182
+ onRequestSubmit={confirmDeletePatientList}
183
+ primaryButtonDisabled={false}>
184
+ {listDetails?.size > 0 ? (
185
+ <p>
186
+ {t('patientListMemberCount', 'This list has {{count}} patients', {
187
+ count: listDetails.size,
188
+ })}
189
+ .
190
+ </p>
191
+ ) : (
192
+ <p>{t('emptyList', 'This list has no patients')}</p>
193
+ )}
194
+ </Modal>
195
+ )}
196
+ </section>
197
+ </main>
198
+ );
199
+ };
200
+
201
+ export default ListDetails;
@@ -0,0 +1,47 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '../style.scss';
4
+
5
+ .container {
6
+ display: flex;
7
+ flex-direction: column;
8
+ justify-content: space-between;
9
+ background-color: #ededed;
10
+ }
11
+
12
+ .cohortHeader {
13
+ display: flex;
14
+ justify-content: space-between;
15
+ border-bottom: 1px solid $ui-03;
16
+ padding: spacing.$spacing-04 spacing.$spacing-05;
17
+ padding-right: 0;
18
+ background-color: $ui-01;
19
+ }
20
+
21
+ .overflowMenu {
22
+ display: flex;
23
+ align-items: flex-start;
24
+ margin-top: spacing.$spacing-04;
25
+ }
26
+
27
+ .marginTop {
28
+ margin-top: spacing.$spacing-03;
29
+ }
30
+
31
+ .menuItem {
32
+ max-width: none;
33
+ }
34
+
35
+ .actionsButtonText {
36
+ @include type.type-style('body-compact-01');
37
+ color: $interactive-01;
38
+ }
39
+
40
+ .tableContainer {
41
+ padding: 0 spacing.$spacing-05 spacing.$spacing-05;
42
+ height: calc(100vh - 11rem);
43
+ }
44
+
45
+ .menuItem {
46
+ max-width: none;
47
+ }
@@ -0,0 +1,112 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { usePatientListDetails, usePatientListMembers } from '../api/hooks';
5
+ import { deletePatientList } from '../api/api-remote';
6
+ import { getByTextWithMarkup } from 'tools';
7
+ import ListDetails from './list-details.component';
8
+
9
+ const mockedUsePatientListDetails = usePatientListDetails as jest.Mock;
10
+ const mockedUsePatientListMembers = usePatientListMembers as jest.Mock;
11
+ const mockedDeletePatientList = deletePatientList as jest.Mock;
12
+
13
+ jest.mock('../api/hooks', () => ({
14
+ usePatientListDetails: jest.fn(),
15
+ usePatientListMembers: jest.fn(),
16
+ }));
17
+
18
+ jest.mock('../api/api-remote');
19
+
20
+ jest.mock('@openmrs/esm-framework', () => ({
21
+ ...jest.requireActual('@openmrs/esm-framework'),
22
+ showSnackbar: jest.fn(),
23
+ navigate: jest.fn(),
24
+ ExtensionSlot: jest.fn(),
25
+ }));
26
+
27
+ const mockedPatientListDetails = {
28
+ name: 'Test Patient List',
29
+ description: 'This is a test patient list',
30
+ size: 1,
31
+ startDate: '2023-08-14',
32
+ };
33
+
34
+ const mockedPatientListMembers = [
35
+ {
36
+ patient: {
37
+ person: {
38
+ display: 'John Doe',
39
+ gender: 'Male',
40
+ },
41
+ identifiers: [
42
+ {
43
+ identifier: '100GEJ',
44
+ },
45
+ ],
46
+ uuid: '7cd38a6d-377e-491b-8284-b04cf8b8c6d8',
47
+ },
48
+ startDate: '2023-08-10',
49
+ uuid: 'ce7d26fa-e1b4-4e78-a1f5-3a7a5de9c0db',
50
+ },
51
+ ];
52
+
53
+ describe('ListDetails', () => {
54
+ beforeEach(() => {
55
+ mockedUsePatientListDetails.mockReturnValue({
56
+ listDetails: mockedPatientListDetails,
57
+ });
58
+
59
+ mockedUsePatientListMembers.mockReturnValue({
60
+ listMembers: mockedPatientListMembers,
61
+ });
62
+
63
+ mockedDeletePatientList.mockResolvedValue({});
64
+ });
65
+
66
+ it('renders patient list details page', async () => {
67
+ render(<ListDetails />);
68
+
69
+ expect(screen.getByRole('heading', { name: /^test patient list$/i })).toBeInTheDocument();
70
+ expect(screen.getByRole('heading', { name: /this is a test patient list/i })).toBeInTheDocument();
71
+ expect(screen.getByRole('button', { name: /actions/i })).toBeInTheDocument();
72
+ expect(screen.getByRole('link', { name: /back to lists page/i })).toBeInTheDocument();
73
+ expect(screen.getByText(/1 patient/)).toBeInTheDocument();
74
+ expect(getByTextWithMarkup('Created on: 14-Aug-2023')).toBeInTheDocument();
75
+ expect(screen.getByText(/edit name or description/i)).toBeInTheDocument();
76
+ expect(screen.getByText(/delete patient list/i)).toBeInTheDocument();
77
+ });
78
+
79
+ it('renders an empty state view if a list has no patients', async () => {
80
+ mockedUsePatientListMembers.mockReturnValue({
81
+ listMembers: [],
82
+ });
83
+
84
+ render(<ListDetails />);
85
+
86
+ expect(screen.getByTitle(/empty state illustration/i)).toBeInTheDocument();
87
+ expect(screen.getByText(/there are no patients in this list/i)).toBeInTheDocument();
88
+ });
89
+
90
+ it('opens overlay with a form when the "Edit name or description" button is clicked', () => {
91
+ render(<ListDetails />);
92
+
93
+ userEvent.click(screen.getByText('Actions'));
94
+ const editBtn = screen.getByText('Edit name or description');
95
+ userEvent.click(editBtn);
96
+ });
97
+
98
+ it('deletes patient list and navigates back to the list page', async () => {
99
+ render(<ListDetails />);
100
+
101
+ await userEvent.click(screen.getByText('Actions'));
102
+ await userEvent.click(screen.getByText(/delete patient list/i));
103
+
104
+ expect(screen.getByText(/Are you sure you want to delete this patient list/i)).toBeInTheDocument();
105
+ expect(screen.getByText('Delete')).toBeInTheDocument();
106
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
107
+
108
+ expect(screen.getByText('Delete').closest('button')).not.toHaveAttribute('disabled');
109
+
110
+ await userEvent.click(screen.getByText('Cancel'));
111
+ });
112
+ });
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { usePatientListDetails, usePatientListMembers } from '../api/hooks';
5
+ import { showToast } from '@openmrs/esm-framework';
6
+ import { deletePatientList } from '../api/api-remote';
7
+ import PatientListDetailComponent from './patient-list-detail.component';
8
+
9
+ const mockedUsePatientListDetails = usePatientListDetails as jest.Mock;
10
+ const mockedUsePatientListMembers = usePatientListMembers as jest.Mock;
11
+ const mockedDeletePatientList = deletePatientList as jest.Mock;
12
+
13
+ jest.mock('../api/hooks', () => ({
14
+ usePatientListDetails: jest.fn(),
15
+ usePatientListMembers: jest.fn(),
16
+ }));
17
+
18
+ jest.mock('../api/api-remote');
19
+
20
+ jest.mock('@openmrs/esm-framework', () => ({
21
+ ...jest.requireActual('@openmrs/esm-framework'),
22
+ showToast: jest.fn(),
23
+ navigate: jest.fn(),
24
+ ExtensionSlot: jest.fn(),
25
+ }));
26
+
27
+ const mockedPatientListDetails = {
28
+ name: 'Test Patient List',
29
+ description: 'This is a test patient list',
30
+ size: 5,
31
+ startDate: '2023-08-14',
32
+ };
33
+
34
+ const mockedPatientListMembers = [
35
+ {
36
+ patient: {
37
+ person: {
38
+ display: 'John Doe',
39
+ gender: 'Male',
40
+ },
41
+ identifiers: [
42
+ {
43
+ identifier: '100GEJ',
44
+ },
45
+ ],
46
+ uuid: '7cd38a6d-377e-491b-8284-b04cf8b8c6d8',
47
+ },
48
+ startDate: '2023-08-10',
49
+ },
50
+ ];
51
+
52
+ describe('PatientListDetailComponent', () => {
53
+ beforeEach(() => {
54
+ jest.clearAllMocks();
55
+ mockedUsePatientListDetails.mockReturnValue({
56
+ data: mockedPatientListDetails,
57
+ });
58
+
59
+ mockedUsePatientListMembers.mockReturnValue({
60
+ data: mockedPatientListMembers,
61
+ });
62
+
63
+ mockedDeletePatientList.mockResolvedValue({});
64
+ });
65
+
66
+ it('renders patient list details', async () => {
67
+ render(<PatientListDetailComponent />);
68
+
69
+ await waitFor(() => {
70
+ expect(screen.getByText('Test Patient List')).toBeInTheDocument();
71
+ expect(screen.getByText('This is a test patient list')).toBeInTheDocument();
72
+ });
73
+ });
74
+
75
+ it('displays patient list members', async () => {
76
+ render(<PatientListDetailComponent />);
77
+
78
+ await waitFor(() => {
79
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
80
+ expect(screen.getByText('Male')).toBeInTheDocument();
81
+ expect(screen.getByText('100GEJ')).toBeInTheDocument();
82
+ });
83
+ });
84
+
85
+ it('opens edit overlay when "Edit Name/ Description" is clicked', () => {
86
+ render(<PatientListDetailComponent />);
87
+
88
+ userEvent.click(screen.getByText('Actions'));
89
+ const editBtn = screen.getByText('Edit Name/ Description');
90
+ userEvent.click(editBtn);
91
+ });
92
+
93
+ it('deletes patient list and navigates on successful delete', async () => {
94
+ render(<PatientListDetailComponent />);
95
+
96
+ await waitFor(() => {
97
+ userEvent.click(screen.getByText('Delete'));
98
+ });
99
+
100
+ await waitFor(() => {
101
+ expect(mockedDeletePatientList).toHaveBeenCalledTimes(1);
102
+ expect(showToast).toHaveBeenCalledTimes(1);
103
+ });
104
+ });
105
+ });