@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,402 @@
1
+ import React, { type CSSProperties, type HTMLAttributes, useCallback, useId, useMemo, useState } from 'react';
2
+ import fuzzy from 'fuzzy';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {
5
+ Button,
6
+ DataTable,
7
+ type DataTableRow,
8
+ DataTableSkeleton,
9
+ InlineLoading,
10
+ Layer,
11
+ Modal,
12
+ Pagination,
13
+ Search,
14
+ Table,
15
+ TableBody,
16
+ TableCell,
17
+ TableContainer,
18
+ TableHead,
19
+ TableHeader,
20
+ TableRow,
21
+ Tile,
22
+ } from '@carbon/react';
23
+ import { ArrowLeft, TrashCan } from '@carbon/react/icons';
24
+ import { ConfigurableLink, useLayoutType, isDesktop, showSnackbar, useDebounce } from '@openmrs/esm-framework';
25
+ import { removePatientFromList } from '../api/api-remote';
26
+ import { EmptyDataIllustration } from '../empty-state/empty-data-illustration.component';
27
+ import styles from './list-details-table.scss';
28
+
29
+ // FIXME Temporarily included types from Carbon
30
+ type InputPropsBase = Omit<HTMLAttributes<HTMLInputElement>, 'onChange'>;
31
+
32
+ interface SearchProps extends InputPropsBase {
33
+ /**
34
+ * Specify an optional value for the `autocomplete` property on the underlying
35
+ * `<input>`, defaults to "off"
36
+ */
37
+ autoComplete?: string;
38
+
39
+ /**
40
+ * Specify an optional className to be applied to the container node
41
+ */
42
+ className?: string;
43
+
44
+ /**
45
+ * Specify a label to be read by screen readers on the "close" button
46
+ */
47
+ closeButtonLabelText?: string;
48
+
49
+ /**
50
+ * Optionally provide the default value of the `<input>`
51
+ */
52
+ defaultValue?: string | number;
53
+
54
+ /**
55
+ * Specify whether the `<input>` should be disabled
56
+ */
57
+ disabled?: boolean;
58
+
59
+ /**
60
+ * Specify whether or not ExpandableSearch should render expanded or not
61
+ */
62
+ isExpanded?: boolean;
63
+
64
+ /**
65
+ * Specify a custom `id` for the input
66
+ */
67
+ id?: string;
68
+
69
+ /**
70
+ * Provide the label text for the Search icon
71
+ */
72
+ labelText: React.ReactNode;
73
+
74
+ /**
75
+ * Optional callback called when the search value changes.
76
+ */
77
+ onChange?(e: { target: HTMLInputElement; type: 'change' }): void;
78
+
79
+ /**
80
+ * Optional callback called when the search value is cleared.
81
+ */
82
+ onClear?(): void;
83
+
84
+ /**
85
+ * Optional callback called when the magnifier icon is clicked in ExpandableSearch.
86
+ */
87
+ onExpand?(e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void;
88
+
89
+ /**
90
+ * Provide an optional placeholder text for the Search.
91
+ * Note: if the label and placeholder differ,
92
+ * VoiceOver on Mac will read both
93
+ */
94
+ placeholder?: string;
95
+
96
+ /**
97
+ * Rendered icon for the Search.
98
+ * Can be a React component class
99
+ */
100
+ renderIcon?: React.ComponentType | React.FunctionComponent;
101
+
102
+ /**
103
+ * Specify the role for the underlying `<input>`, defaults to `searchbox`
104
+ */
105
+ role?: string;
106
+
107
+ /**
108
+ * Specify the size of the Search
109
+ */
110
+ size?: 'sm' | 'md' | 'lg';
111
+
112
+ /**
113
+ * Optional prop to specify the type of the `<input>`
114
+ */
115
+ type?: string;
116
+
117
+ /**
118
+ * Specify the value of the `<input>`
119
+ */
120
+ value?: string | number;
121
+ }
122
+
123
+ interface ListDetailsTableProps {
124
+ autoFocus?: boolean;
125
+ columns: Array<PatientTableColumn>;
126
+ isFetching?: boolean;
127
+ isLoading: boolean;
128
+ mutateListDetails: () => void;
129
+ mutateListMembers: () => void;
130
+ pagination: {
131
+ usePagination: boolean;
132
+ currentPage: number;
133
+ onChange(props: any): any;
134
+ pageSize: number;
135
+ totalItems: number;
136
+ pagesUnknown?: boolean;
137
+ lastPage?: boolean;
138
+ };
139
+ patients;
140
+ style?: CSSProperties;
141
+ }
142
+
143
+ interface PatientTableColumn {
144
+ key: string;
145
+ header: string;
146
+ getValue?(patient: any): any;
147
+ link?: {
148
+ getUrl(patient: any): string;
149
+ };
150
+ }
151
+
152
+ const ListDetailsTable: React.FC<ListDetailsTableProps> = ({
153
+ columns,
154
+ isFetching,
155
+ isLoading,
156
+ mutateListDetails,
157
+ mutateListMembers,
158
+ pagination,
159
+ patients,
160
+ }) => {
161
+ const { t } = useTranslation();
162
+ const id = useId();
163
+ const layout = useLayoutType();
164
+ const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
165
+ const patientListsPath = window.getOpenmrsSpaBase() + 'home/patient-lists';
166
+
167
+ const [isDeleting, setIsDeleting] = useState(false);
168
+ const [membershipUuid, setMembershipUuid] = useState('');
169
+ const [patientName, setPatientName] = useState('');
170
+ const [searchTerm, setSearchTerm] = useState('');
171
+ const [showConfirmationModal, setShowConfirmationModal] = useState(false);
172
+ const debouncedSearchTerm = useDebounce(searchTerm);
173
+
174
+ const filteredPatients = useMemo(() => {
175
+ if (!debouncedSearchTerm) {
176
+ return patients;
177
+ }
178
+
179
+ return debouncedSearchTerm
180
+ ? fuzzy
181
+ .filter(debouncedSearchTerm, patients, {
182
+ extract: (patient: any) => `${patient.name} ${patient.identifier} ${patient.sex}`,
183
+ })
184
+ .sort((r1, r2) => r1.score - r2.score)
185
+ .map((result) => result.original)
186
+ : patients;
187
+ }, [debouncedSearchTerm, patients]);
188
+
189
+ const tableRows: Array<typeof DataTableRow> = useMemo(
190
+ () =>
191
+ filteredPatients?.map((patient) => ({
192
+ id: patient.identifier,
193
+ identifier: patient.identifier,
194
+ membershipUuid: patient.membershipUuid,
195
+ name: columns.find((column) => column.key === 'name')?.link ? (
196
+ <ConfigurableLink
197
+ className={styles.link}
198
+ to={columns.find((column) => column.key === 'name')?.link?.getUrl(patient)}>
199
+ {patient.name}
200
+ </ConfigurableLink>
201
+ ) : (
202
+ patient.name
203
+ ),
204
+ sex: patient.sex,
205
+ startDate: patient.startDate,
206
+ })) ?? [],
207
+ [columns, filteredPatients],
208
+ );
209
+
210
+ const handleRemovePatientFromList = useCallback(async () => {
211
+ setIsDeleting(true);
212
+
213
+ try {
214
+ await removePatientFromList(membershipUuid);
215
+ mutateListMembers();
216
+ mutateListDetails();
217
+
218
+ showSnackbar({
219
+ isLowContrast: true,
220
+ kind: 'success',
221
+ subtitle: t('listUpToDate', 'The list is now up to date'),
222
+ title: t('patientRemovedFromList', 'Patient removed from list'),
223
+ });
224
+ } catch (error) {
225
+ showSnackbar({
226
+ kind: 'error',
227
+ subtitle: error?.message,
228
+ title: t('errorRemovingPatientFromList', 'Failed to remove patient from list'),
229
+ });
230
+ }
231
+
232
+ setIsDeleting(false);
233
+ setShowConfirmationModal(false);
234
+ }, [membershipUuid, mutateListDetails, mutateListMembers, t]);
235
+
236
+ const BackButton = () => (
237
+ <div className={styles.backButton}>
238
+ <ConfigurableLink to={patientListsPath}>
239
+ <Button
240
+ kind="ghost"
241
+ renderIcon={(props) => <ArrowLeft size={24} {...props} />}
242
+ iconDescription="Return to lists page"
243
+ size="sm"
244
+ onClick={() => {}}>
245
+ <span>{t('backToListsPage', 'Back to lists page')}</span>
246
+ </Button>
247
+ </ConfigurableLink>
248
+ </div>
249
+ );
250
+
251
+ if (isLoading) {
252
+ return (
253
+ <div className={styles.skeletonContainer}>
254
+ <DataTableSkeleton
255
+ data-testid="data-table-skeleton"
256
+ className={styles.dataTableSkeleton}
257
+ rowCount={5}
258
+ columnCount={5}
259
+ zebra
260
+ />
261
+ </div>
262
+ );
263
+ }
264
+
265
+ if (patients.length > 0) {
266
+ return (
267
+ <>
268
+ <BackButton />
269
+ <div className={styles.tableOverride}>
270
+ <div className={styles.searchContainer}>
271
+ <div>{isFetching && <InlineLoading />}</div>
272
+ <div>
273
+ <Layer>
274
+ <Search
275
+ className={styles.searchOverrides}
276
+ id={`${id}-search`}
277
+ labelText=""
278
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
279
+ placeholder={t('searchThisList', 'Search this list')}
280
+ size={responsiveSize}
281
+ />
282
+ </Layer>
283
+ </div>
284
+ </div>
285
+ <DataTable rows={tableRows} headers={columns} isSortable size={responsiveSize} useZebraStyles>
286
+ {({ rows, headers, getHeaderProps, getTableProps, getRowProps }) => (
287
+ <TableContainer>
288
+ <Table className={styles.table} {...getTableProps()} data-testid="patientsTable">
289
+ <TableHead>
290
+ <TableRow>
291
+ {headers.map((header) => (
292
+ <TableHeader
293
+ {...getHeaderProps({
294
+ header,
295
+ isSortable: header.isSortable,
296
+ })}
297
+ className={isDesktop(layout) ? styles.desktopHeader : styles.tabletHeader}>
298
+ {header.header?.content ?? header.header}
299
+ </TableHeader>
300
+ ))}
301
+ </TableRow>
302
+ </TableHead>
303
+ <TableBody>
304
+ {rows.map((row) => {
305
+ const currentPatient = patients.find((patient) => patient.identifier === row.id);
306
+
307
+ return (
308
+ <TableRow
309
+ {...getRowProps({ row })}
310
+ className={isDesktop(layout) ? styles.desktopRow : styles.tabletRow}
311
+ key={row.id}>
312
+ {row.cells.map((cell) => (
313
+ <TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell>
314
+ ))}
315
+ <TableCell className="cds--table-column-menu">
316
+ <Button
317
+ kind="ghost"
318
+ hasIconOnly
319
+ renderIcon={TrashCan}
320
+ iconDescription={t('removeFromList', 'Remove from list')}
321
+ size={responsiveSize}
322
+ tooltipPosition="left"
323
+ onClick={() => {
324
+ setMembershipUuid(currentPatient.membershipUuid);
325
+ setPatientName(currentPatient.name);
326
+ setShowConfirmationModal(true);
327
+ }}
328
+ />
329
+ </TableCell>
330
+ </TableRow>
331
+ );
332
+ })}
333
+ </TableBody>
334
+ </Table>
335
+ </TableContainer>
336
+ )}
337
+ </DataTable>
338
+ {filteredPatients?.length === 0 && (
339
+ <div className={styles.filterEmptyState}>
340
+ <Layer level={0}>
341
+ <Tile className={styles.filterEmptyStateTile}>
342
+ <p className={styles.filterEmptyStateContent}>
343
+ {t('noMatchingPatients', 'No matching patients to display')}
344
+ </p>
345
+ <p className={styles.filterEmptyStateHelper}>{t('checkFilters', 'Check the filters above')}</p>
346
+ </Tile>
347
+ </Layer>
348
+ </div>
349
+ )}
350
+ {pagination.usePagination && (
351
+ <Pagination
352
+ backwardText={t('nextPage', 'Next page')}
353
+ className={styles.paginationOverride}
354
+ forwardText={t('previousPage', 'Previous page')}
355
+ isLastPage={pagination.lastPage}
356
+ onChange={pagination.onChange}
357
+ page={pagination.currentPage}
358
+ pageSize={pagination.pageSize}
359
+ pageSizes={[10, 20, 30, 40, 50]}
360
+ pagesUnknown={pagination?.pagesUnknown}
361
+ totalItems={pagination.totalItems}
362
+ />
363
+ )}
364
+ </div>
365
+ {showConfirmationModal && (
366
+ <Modal
367
+ open
368
+ danger
369
+ modalHeading={t(
370
+ 'removePatientFromListConfirmation',
371
+ 'Are you sure you want to remove {{patientName}} from this list?',
372
+ {
373
+ patientName: patientName,
374
+ },
375
+ )}
376
+ primaryButtonText={t('removeFromList', 'Remove from list')}
377
+ secondaryButtonText={t('cancel', 'Cancel')}
378
+ onRequestClose={() => setShowConfirmationModal(false)}
379
+ onRequestSubmit={handleRemovePatientFromList}
380
+ primaryButtonDisabled={isDeleting}
381
+ />
382
+ )}
383
+ </>
384
+ );
385
+ }
386
+
387
+ return (
388
+ <>
389
+ <BackButton />
390
+ <Layer>
391
+ <Tile className={styles.tile}>
392
+ <div className={styles.illo}>
393
+ <EmptyDataIllustration />
394
+ </div>
395
+ <p className={styles.content}>{t('noPatientsInList', 'There are no patients in this list')}</p>
396
+ </Tile>
397
+ </Layer>
398
+ </>
399
+ );
400
+ };
401
+
402
+ export default ListDetailsTable;
@@ -0,0 +1,143 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .searchContainer {
6
+ display: grid;
7
+ grid-template-columns: 1fr 1fr;
8
+ margin-top: spacing.$spacing-05;
9
+ background-color: #ededed;
10
+ }
11
+
12
+ .searchContainer > div {
13
+ align-self: center;
14
+ justify-self: end;
15
+ }
16
+
17
+ .searchOverrides {
18
+ width: 100%;
19
+ max-width: 250px;
20
+ border: 0px !important;
21
+ }
22
+
23
+ div.tableOverride {
24
+ background-color: $ui-01;
25
+ }
26
+
27
+ div.tableOverride > div {
28
+ white-space: nowrap;
29
+ }
30
+
31
+ div#table-tool-bar {
32
+ margin-top: spacing.$spacing-09;
33
+ }
34
+
35
+ .table {
36
+ background-color: $ui-03;
37
+ }
38
+
39
+ .desktopRow,
40
+ .desktopHeader {
41
+ height: spacing.$spacing-07 !important;
42
+ }
43
+
44
+ .tabletRow,
45
+ .tabletHeader {
46
+ height: spacing.$spacing-09 !important;
47
+ }
48
+
49
+ .paginationOverride {
50
+ background-color: $ui-background;
51
+ border-top: 1px solid $ui-03 !important;
52
+ border-bottom: 1px solid $ui-03 !important;
53
+ overflow-x: hidden;
54
+ }
55
+
56
+ .paginationOverride > div {
57
+ background-color: $ui-background;
58
+ border-bottom: 0px !important;
59
+ border-top: 0px !important;
60
+ }
61
+
62
+ .link {
63
+ text-decoration: none;
64
+ }
65
+
66
+ .dataTableSkeleton {
67
+ background-color: transparent;
68
+ padding: 0rem;
69
+ }
70
+
71
+ .skeletonContainer {
72
+ margin: spacing.$spacing-05;
73
+ }
74
+
75
+ .tile {
76
+ text-align: center;
77
+ border: 1px solid $ui-03;
78
+ margin: spacing.$spacing-05 0;
79
+ }
80
+
81
+ .illo {
82
+ margin-top: spacing.$spacing-05;
83
+ }
84
+
85
+ .content {
86
+ @include type.type-style('heading-compact-01');
87
+ color: $text-02;
88
+ margin-top: spacing.$spacing-05;
89
+ margin-bottom: spacing.$spacing-03;
90
+ }
91
+
92
+ .backButton {
93
+ padding: spacing.$spacing-03 0;
94
+ max-width: fit-content;
95
+
96
+ a {
97
+ text-decoration: none;
98
+ }
99
+
100
+ button {
101
+ display: flex;
102
+ padding-left: 0 !important;
103
+
104
+ svg {
105
+ order: 1;
106
+ margin: 0 spacing.$spacing-03;
107
+ }
108
+
109
+ span {
110
+ order: 2;
111
+ }
112
+ }
113
+ }
114
+
115
+ .tile {
116
+ text-align: center;
117
+ border-bottom: 1px solid $ui-03;
118
+ }
119
+
120
+ .filterEmptyState {
121
+ display: flex;
122
+ justify-content: center;
123
+ align-items: center;
124
+ padding: spacing.$spacing-09 !important;
125
+ text-align: center;
126
+ border: 1px solid $ui-03;
127
+ background-color: white;
128
+ }
129
+
130
+ .filterEmptyStateTile {
131
+ margin: auto;
132
+ }
133
+
134
+ .filterEmptyStateContent {
135
+ @include type.type-style('heading-compact-02');
136
+ color: $text-02;
137
+ margin-bottom: 0.5rem;
138
+ }
139
+
140
+ .filterEmptyStateHelper {
141
+ @include type.type-style('body-compact-01');
142
+ color: $text-02;
143
+ }
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import ListDetailsTable from './list-details-table.component';
4
+
5
+ jest.mock('@openmrs/esm-framework', () => ({
6
+ ...jest.requireActual('@openmrs/esm-framework'),
7
+ isDesktop: jest.fn(() => true),
8
+ }));
9
+
10
+ describe('ListDetailsTable Component', () => {
11
+ const patients = [
12
+ {
13
+ identifier: '123abced',
14
+ firstName: 'John',
15
+ lastName: 'Doe',
16
+ age: 30,
17
+ sex: 'Male',
18
+ startDate: '2023-08-10',
19
+ membershipUuid: 'ce7d26fa-e1b4-4e78-a1f5-3a7a5de9c0db',
20
+ },
21
+ {
22
+ identifier: '123abcedfg',
23
+ firstName: 'Jane',
24
+ lastName: 'Smith',
25
+ age: 25,
26
+ sex: 'Female',
27
+ startDate: '2023-08-10',
28
+ membershipUuid: 'ce7d26fa-e1b4-4e78-a1f5-3a7a5de9c0db',
29
+ },
30
+ ];
31
+
32
+ const columns = [
33
+ {
34
+ key: 'firstName',
35
+ header: 'First Name',
36
+ },
37
+ {
38
+ key: 'lastName',
39
+ header: 'Last Name',
40
+ link: {
41
+ getUrl: (patient) => `/patient/${patient.id}`,
42
+ },
43
+ },
44
+ {
45
+ key: 'age',
46
+ header: 'Age',
47
+ getValue: (patient) => `${patient.age} years`,
48
+ },
49
+ ];
50
+
51
+ const mockedOnChange = jest.fn();
52
+
53
+ let pagination = {
54
+ usePagination: true,
55
+ currentPage: 1,
56
+ onChange: mockedOnChange,
57
+ pageSize: 10,
58
+ totalItems: 100,
59
+ pagesUnknown: false,
60
+ };
61
+
62
+ it('renders table with patient data', () => {
63
+ render(
64
+ <ListDetailsTable
65
+ patients={patients}
66
+ columns={columns}
67
+ pagination={pagination}
68
+ isLoading={false}
69
+ autoFocus={false}
70
+ isFetching={true}
71
+ mutateListDetails={jest.fn()}
72
+ mutateListMembers={jest.fn()}
73
+ />,
74
+ );
75
+ expect(screen.getByTestId('patientsTable')).toBeInTheDocument();
76
+ });
77
+
78
+ it('renders loading skeleton when loading', () => {
79
+ render(
80
+ <ListDetailsTable
81
+ patients={patients}
82
+ columns={columns}
83
+ pagination={pagination}
84
+ isLoading={true}
85
+ autoFocus={false}
86
+ isFetching={false}
87
+ mutateListDetails={jest.fn()}
88
+ mutateListMembers={jest.fn()}
89
+ />,
90
+ );
91
+
92
+ expect(screen.getByTestId('data-table-skeleton')).toBeInTheDocument();
93
+ });
94
+ });