@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,104 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import classnames from 'classnames';
3
+ import { useLocation } from 'react-router-dom';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Tab, Tabs, TabList } from '@carbon/react';
6
+ import { navigate } from '@openmrs/esm-framework';
7
+ import { type PatientListFilter, PatientListType } from '../api/types';
8
+ import { useAllPatientLists } from '../api/hooks';
9
+ import CreateEditPatientList from '../create-edit-patient-list/create-edit-list.component';
10
+ import Header from '../header/header.component';
11
+ import ListsTable from '../lists-table/lists-table.component';
12
+ import styles from './lists-dashboard.scss';
13
+
14
+ const TabIndices = {
15
+ STARRED_LISTS: 0,
16
+ SYSTEM_LISTS: 1,
17
+ MY_LISTS: 2,
18
+ ALL_LISTS: 3,
19
+ } as const;
20
+
21
+ function usePatientListFilterForCurrentTab(selectedTab: number) {
22
+ const { t } = useTranslation();
23
+
24
+ return useMemo<PatientListFilter>(() => {
25
+ switch (selectedTab) {
26
+ case TabIndices.STARRED_LISTS:
27
+ return { isStarred: true, label: t('starred', 'starred') };
28
+ case TabIndices.SYSTEM_LISTS:
29
+ return { type: PatientListType.SYSTEM, label: t('systemDefined', 'system-defined') };
30
+ case TabIndices.MY_LISTS:
31
+ return { type: PatientListType.USER, label: t('userDefined', 'user-defined') };
32
+ case TabIndices.ALL_LISTS:
33
+ default:
34
+ return { label: '' };
35
+ }
36
+ }, [selectedTab, t]);
37
+ }
38
+
39
+ const ListsDashboard: React.FC = () => {
40
+ const { t } = useTranslation();
41
+ const [selectedTab, setSelectedTab] = useState(TabIndices.STARRED_LISTS);
42
+ const patientListFilter = usePatientListFilterForCurrentTab(selectedTab);
43
+ const { patientLists, isLoading, error, mutate } = useAllPatientLists(patientListFilter);
44
+ const { search } = useLocation();
45
+
46
+ const isCreatingPatientList =
47
+ Object.fromEntries(
48
+ search
49
+ .slice(1)
50
+ .split('&')
51
+ ?.map((searchParam) => searchParam?.split('=')),
52
+ )['new_cohort'] === 'true';
53
+
54
+ const handleHideNewListOverlay = () => {
55
+ navigate({
56
+ to: window.getOpenmrsSpaBase() + 'home/patient-lists',
57
+ });
58
+ };
59
+
60
+ const tableHeaders = [
61
+ { id: 1, key: 'display', header: t('listName', 'List name') },
62
+ { id: 2, key: 'type', header: t('listType', 'List type') },
63
+ { id: 3, key: 'size', header: t('noOfPatients', 'No. of patients') },
64
+ { id: 4, key: 'isStarred', header: '' },
65
+ ];
66
+
67
+ return (
68
+ <main className={classnames('omrs-main-content', styles.dashboardContainer)}>
69
+ <section className={styles.dashboard}>
70
+ <Header />
71
+ <Tabs
72
+ className={styles.tabs}
73
+ onChange={({ selectedIndex }) => {
74
+ setSelectedTab(selectedIndex);
75
+ }}
76
+ selectedIndex={selectedTab}
77
+ tabContentClassName={styles.hiddenTabsContent}>
78
+ <TabList className={styles.tablist} aria-label="List tabs" contained>
79
+ <Tab className={styles.tab}>{t('starredLists', 'Starred lists')}</Tab>
80
+ <Tab className={styles.tab}>{t('systemLists', 'System lists')}</Tab>
81
+ <Tab className={styles.tab}>{t('myLists', 'My lists')}</Tab>
82
+ <Tab className={styles.tab}>{t('allLists', 'All lists')}</Tab>
83
+ </TabList>
84
+ </Tabs>
85
+ <div className={styles.listsTableContainer}>
86
+ <ListsTable
87
+ error={error}
88
+ headers={tableHeaders}
89
+ isLoading={isLoading}
90
+ key={patientListFilter.label}
91
+ listType={patientListFilter.label}
92
+ patientLists={patientLists}
93
+ refetch={mutate}
94
+ />
95
+ </div>
96
+ </section>
97
+ <section>
98
+ {isCreatingPatientList && <CreateEditPatientList close={handleHideNewListOverlay} onSuccess={() => mutate()} />}
99
+ </section>
100
+ </main>
101
+ );
102
+ };
103
+
104
+ export default ListsDashboard;
@@ -0,0 +1,110 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/styles/scss/spacing';
3
+ @use '@carbon/styles/scss/type';
4
+ @import '../style.scss';
5
+
6
+ .container {
7
+ margin: 1rem 0;
8
+ }
9
+
10
+ .dashboardContainer {
11
+ display: flex;
12
+ background-color: $ui-02;
13
+ }
14
+
15
+ .dashboard {
16
+ height: 100%;
17
+ width: 100%;
18
+ background-color: $ui-02;
19
+ height: calc(100vh - 3rem);
20
+ }
21
+
22
+ .tabs {
23
+ grid-column: 'span 2';
24
+ }
25
+
26
+ .tablist {
27
+ padding: 0 spacing.$spacing-05;
28
+ }
29
+
30
+ .tab {
31
+ min-width: 8.875rem;
32
+
33
+ &:active,
34
+ &:focus {
35
+ outline: 2px solid var(--brand-03) !important;
36
+ }
37
+
38
+ &[aria-selected='true'] {
39
+ box-shadow: inset 0 2px 0 0 var(--brand-03) !important;
40
+ }
41
+ }
42
+
43
+ .hiddenTabsContent,
44
+ .tabs .hiddenTabsContent {
45
+ display: none;
46
+ }
47
+
48
+ .listsTableContainer {
49
+ padding: spacing.$spacing-05;
50
+ background-color: $ui-01;
51
+ height: calc(100vh - 12rem);
52
+ }
53
+
54
+ .tableBody {
55
+ background-color: $ui-01;
56
+ }
57
+
58
+ .tableCell,
59
+ .interactiveText01 {
60
+ color: $interactive-01;
61
+ }
62
+
63
+ .desktopRow,
64
+ .desktopHeader {
65
+ height: spacing.$spacing-07 !important;
66
+ }
67
+
68
+ .tabletRow,
69
+ .tabletHeader {
70
+ height: spacing.$spacing-09 !important;
71
+ }
72
+
73
+ // Search Overlay
74
+ .searchOverlay {
75
+ z-index: 1;
76
+ grid-row: 2 / 4;
77
+ grid-column: 1 / 2;
78
+ background-color: $ui-04;
79
+ opacity: 0.7;
80
+ }
81
+
82
+ .patientListResultsContainer {
83
+ z-index: 1;
84
+ grid-row: 2 / 4;
85
+ grid-column: 1 / 2;
86
+ background-color: $ui-01;
87
+ padding: spacing.$spacing-05;
88
+ }
89
+
90
+ // Results Overlay
91
+
92
+ .resultCount {
93
+ @include type.type-style('body-compact-01');
94
+ color: $text-02;
95
+ border-bottom: solid 1px #e0e0e0;
96
+ padding: spacing.$spacing-04 0;
97
+ }
98
+
99
+ .patientListResultsTableContainer {
100
+ margin: spacing.$spacing-03;
101
+ }
102
+
103
+ .starButton {
104
+ :global(.cds--popover) {
105
+ display: none;
106
+ }
107
+ :global(.cds--btn--ghost:not([disabled])) svg {
108
+ fill: $interactive-01;
109
+ }
110
+ }
@@ -0,0 +1,129 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { useLocation } from 'react-router-dom';
5
+ import { openmrsFetch, restBaseUrl, useSession } from '@openmrs/esm-framework';
6
+ import { mockSession } from '__mocks__';
7
+ import ListsDashboard from './lists-dashboard.component';
8
+
9
+ const mockedUseLocation = jest.mocked(useLocation);
10
+ const mockedUseSession = jest.mocked(useSession);
11
+ const mockedOpenmrsFetch = openmrsFetch as jest.Mock;
12
+
13
+ jest.mock('react-router-dom', () => ({
14
+ ...jest.requireActual('react-router-dom'),
15
+ useLocation: jest.fn(),
16
+ }));
17
+
18
+ describe('ListsDashboard', () => {
19
+ beforeEach(() => {
20
+ mockedUseLocation.mockReturnValue({
21
+ pathname: '/',
22
+ search: '',
23
+ hash: '',
24
+ state: null,
25
+ key: 'default',
26
+ });
27
+
28
+ mockedUseSession.mockReturnValue(mockSession.data);
29
+
30
+ mockedOpenmrsFetch.mockResolvedValue({
31
+ data: {
32
+ results: [
33
+ {
34
+ uuid: 'ffff37ca-872f-4ede-9a19-bb6692c5ff98',
35
+ name: 'Test List',
36
+ description: 'Test List',
37
+ display: 'Test List',
38
+ size: 1,
39
+ attributes: [],
40
+ cohortType: {
41
+ name: 'My List',
42
+ description: 'A user-generated patient list',
43
+ uuid: 'e71857cb-33af-4f2c-86ab-7223bcfa37ad',
44
+ display: 'My List',
45
+ links: [
46
+ {
47
+ rel: 'self',
48
+ uri: `http://dev3.openmrs.org/openmrs/${restBaseUrl}/cohortm/cohorttype/e71857cb-33af-4f2c-86ab-7223bcfa37ad`,
49
+ resourceAlias: 'cohorttype',
50
+ },
51
+ {
52
+ rel: 'full',
53
+ uri: `http://dev3.openmrs.org/openmrs/${restBaseUrl}/cohortm/cohorttype/e71857cb-33af-4f2c-86ab-7223bcfa37ad?v=full`,
54
+ resourceAlias: 'cohorttype',
55
+ },
56
+ ],
57
+ resourceVersion: '1.8',
58
+ },
59
+ },
60
+ {
61
+ uuid: '94ee4943-8dcc-409a-86d5-8ab6631a511c',
62
+ name: '2.13.0',
63
+ description: 'Testing',
64
+ display: '2.13.0',
65
+ size: 0,
66
+ attributes: [],
67
+ cohortType: {
68
+ name: 'My List',
69
+ description: 'A user-generated patient list',
70
+ uuid: 'e71857cb-33af-4f2c-86ab-7223bcfa37ad',
71
+ display: 'My List',
72
+ links: [
73
+ {
74
+ rel: 'self',
75
+ uri: `http://dev3.openmrs.org/openmrs/${restBaseUrl}/cohortm/cohorttype/e71857cb-33af-4f2c-86ab-7223bcfa37ad`,
76
+ resourceAlias: 'cohorttype',
77
+ },
78
+ {
79
+ rel: 'full',
80
+ uri: `http://dev3.openmrs.org/openmrs/${restBaseUrl}/cohortm/cohorttype/e71857cb-33af-4f2c-86ab-7223bcfa37ad?v=full`,
81
+ resourceAlias: 'cohorttype',
82
+ },
83
+ ],
84
+ resourceVersion: '1.8',
85
+ },
86
+ },
87
+ ],
88
+ },
89
+ });
90
+ });
91
+
92
+ it('renders the patient list page UI correctly', async () => {
93
+ const user = userEvent.setup();
94
+ render(<ListsDashboard />);
95
+
96
+ await screen.findByRole('button', { name: /new list/i });
97
+ expect(screen.getByRole('tablist', { name: /list tabs/i })).toBeInTheDocument();
98
+
99
+ const tabs = ['Starred lists', 'System lists', 'My lists', 'All lists'];
100
+
101
+ tabs.forEach((tab) => {
102
+ expect(screen.getByRole('tab', { name: tab })).toBeInTheDocument();
103
+ });
104
+ expect(screen.getByRole('tab', { name: /starred lists/i })).toHaveAttribute('aria-selected', 'true');
105
+
106
+ await user.click(screen.getByRole('tab', { name: 'All lists' }));
107
+ await screen.findByRole('searchbox');
108
+ expect(screen.getByRole('table')).toBeInTheDocument();
109
+
110
+ const columnHeaders = [/List name/, /List type/, /No. of patients/];
111
+
112
+ columnHeaders.forEach((header) => {
113
+ expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
114
+ });
115
+ });
116
+
117
+ it('clicking a tab switches the page content to the selected tab', async () => {
118
+ const user = userEvent.setup();
119
+
120
+ render(<ListsDashboard />);
121
+
122
+ const systemListsTab = screen.getByRole('tab', { name: /system lists/i });
123
+ expect(systemListsTab).toHaveAttribute('aria-selected', 'false');
124
+
125
+ await user.click(systemListsTab);
126
+
127
+ expect(systemListsTab).toHaveAttribute('aria-selected', 'true');
128
+ });
129
+ });
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { Pagination } from '@carbon/react';
3
+ import { useLayoutType } from '@openmrs/esm-framework';
4
+ import { usePaginationInfo } from './use-pagination-info.component';
5
+ import styles from './custom-pagination.scss';
6
+
7
+ interface CustomPaginationProps {
8
+ currentItems: number;
9
+ totalItems: number;
10
+ pageNumber: number;
11
+ pageSize: number;
12
+ onPageNumberChange?: ({ page }: { page: number }) => void;
13
+ }
14
+
15
+ export const CustomPagination: React.FC<CustomPaginationProps> = ({
16
+ totalItems,
17
+ pageSize,
18
+ onPageNumberChange,
19
+ pageNumber,
20
+ currentItems,
21
+ }) => {
22
+ const { itemsDisplayed, pageSizes } = usePaginationInfo(pageSize, totalItems, pageNumber, currentItems);
23
+ const isTablet = useLayoutType() === 'tablet';
24
+
25
+ return (
26
+ <>
27
+ {totalItems > 0 && (
28
+ <div className={isTablet ? styles.tablet : styles.desktop}>
29
+ <div>{itemsDisplayed}</div>
30
+ <Pagination
31
+ className={styles.pagination}
32
+ page={pageNumber}
33
+ pageSize={pageSize}
34
+ pageSizes={pageSizes}
35
+ totalItems={totalItems}
36
+ onChange={onPageNumberChange}
37
+ size={isTablet ? 'lg' : 'sm'}
38
+ />
39
+ </div>
40
+ )}
41
+ </>
42
+ );
43
+ };
@@ -0,0 +1,67 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .bodyShort01 {
6
+ @include type.type-style('body-compact-01');
7
+ }
8
+
9
+ .desktop,
10
+ .tablet {
11
+ @include type.type-style('body-compact-01');
12
+ display: flex;
13
+ justify-content: space-between;
14
+ color: colors.$gray-70;
15
+ background-color: colors.$white-0;
16
+ padding-left: 1rem;
17
+ align-items: center;
18
+ border: 1px solid colors.$gray-20;
19
+ border-top: none;
20
+ }
21
+
22
+ .desktop :global(.cds--pagination) {
23
+ min-height: 0rem;
24
+ height: 2rem;
25
+ width: auto;
26
+ border: none;
27
+
28
+ & :global(.cds--select-input),
29
+ :global(.cds--btn),
30
+ :global(.cds--pagination__right) {
31
+ min-height: 0rem;
32
+ height: 2rem;
33
+ }
34
+ }
35
+
36
+ .tablet :global(.cds--pagination) {
37
+ min-height: 0rem;
38
+ height: 3rem;
39
+ width: auto;
40
+ border: none;
41
+
42
+ & :global(.cds--select-input),
43
+ :global(.cds--btn),
44
+ :global(.cds--pagination__right) {
45
+ min-height: 0rem;
46
+ height: 3rem;
47
+ }
48
+ }
49
+
50
+ .configurableLink {
51
+ text-decoration: none;
52
+ @extend .bodyShort01;
53
+ padding: 0 layout.$spacing-03;
54
+ }
55
+
56
+ .pagination {
57
+ @include type.type-style('body-compact-01');
58
+ background-color: colors.$white-0;
59
+ color: colors.$gray-70;
60
+ display: flex;
61
+ }
62
+
63
+ div.pagination {
64
+ & > :global(.cds--pagination__left) {
65
+ display: none;
66
+ }
67
+ }