@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.
- package/.turbo/turbo-build.log +41 -0
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +3 -0
- package/dist/130.js.map +1 -0
- package/dist/139.js +1 -0
- package/dist/139.js.map +1 -0
- package/dist/255.js +2 -0
- package/dist/255.js.LICENSE.txt +9 -0
- package/dist/255.js.map +1 -0
- package/dist/271.js +1 -0
- package/dist/319.js +1 -0
- package/dist/382.js +1 -0
- package/dist/382.js.map +1 -0
- package/dist/443.js +1 -0
- package/dist/443.js.map +1 -0
- package/dist/460.js +1 -0
- package/dist/548.js +1 -0
- package/dist/548.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +32 -0
- package/dist/591.js.map +1 -0
- package/dist/635.js +1 -0
- package/dist/635.js.map +1 -0
- package/dist/644.js +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/784.js +2 -0
- package/dist/784.js.LICENSE.txt +9 -0
- package/dist/784.js.map +1 -0
- package/dist/788.js +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/930.js +2 -0
- package/dist/930.js.LICENSE.txt +35 -0
- package/dist/930.js.map +1 -0
- package/dist/kenyaemr-esm-patient-list-management-app.js +1 -0
- package/dist/kenyaemr-esm-patient-list-management-app.js.buildmanifest.json +580 -0
- package/dist/kenyaemr-esm-patient-list-management-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +45 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +56 -0
- package/src/add-patient/add-patient.component.tsx +271 -0
- package/src/add-patient/add-patient.scss +51 -0
- package/src/add-patient-to-patient-list-menu-item.component.tsx +48 -0
- package/src/add-patient-to-patient-list-menu-item.test.tsx +33 -0
- package/src/api/api-remote.ts +211 -0
- package/src/api/hooks.ts +150 -0
- package/src/api/types.ts +102 -0
- package/src/config-schema.ts +25 -0
- package/src/constants.ts +5 -0
- package/src/create-edit-patient-list/create-edit-list.component.tsx +170 -0
- package/src/create-edit-patient-list/create-edit-patient-list.scss +31 -0
- package/src/createDashboardLink.component.tsx +40 -0
- package/src/dashboard.meta.ts +5 -0
- package/src/declarations.d.ts +5 -0
- package/src/empty-state/empty-data-illustration.component.tsx +42 -0
- package/src/empty-state/empty-state.component.tsx +41 -0
- package/src/empty-state/empty-state.scss +24 -0
- package/src/error-state/error-state.component.tsx +35 -0
- package/src/error-state/error-state.scss +50 -0
- package/src/header/header.component.tsx +51 -0
- package/src/header/header.scss +52 -0
- package/src/illo.component.tsx +25 -0
- package/src/index.ts +41 -0
- package/src/list-details/list-details.component.tsx +201 -0
- package/src/list-details/list-details.scss +47 -0
- package/src/list-details/list-details.test.tsx +112 -0
- package/src/list-details/patient-list-detail.test.tsx +105 -0
- package/src/list-details-table/list-details-table.component.tsx +402 -0
- package/src/list-details-table/list-details-table.scss +143 -0
- package/src/list-details-table/list-details-table.test.tsx +94 -0
- package/src/lists-dashboard/lists-dashboard.component.tsx +104 -0
- package/src/lists-dashboard/lists-dashboard.scss +110 -0
- package/src/lists-dashboard/lists-dashboard.test.tsx +129 -0
- package/src/lists-table/custom-pagination.component.tsx +43 -0
- package/src/lists-table/custom-pagination.scss +67 -0
- package/src/lists-table/lists-table.component.tsx +317 -0
- package/src/lists-table/lists-table.scss +100 -0
- package/src/lists-table/lists-table.test.tsx +189 -0
- package/src/lists-table/use-pagination-info.component.tsx +35 -0
- package/src/offline.ts +31 -0
- package/src/overlay.component.tsx +49 -0
- package/src/overlay.scss +89 -0
- package/src/overlay.test.tsx +60 -0
- package/src/root.component.tsx +19 -0
- package/src/routes.json +36 -0
- package/src/style.scss +46 -0
- package/translations/am.json +84 -0
- package/translations/ar.json +84 -0
- package/translations/en.json +84 -0
- package/translations/es.json +84 -0
- package/translations/fr.json +84 -0
- package/translations/he.json +84 -0
- package/translations/km.json +84 -0
- package/translations/zh.json +84 -0
- package/translations/zh_CN.json +84 -0
- package/tsconfig.json +5 -0
- 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
|
+
});
|