@openmrs/esm-patient-flags-app 11.3.1-pre.9533 → 11.3.1-pre.9541

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 (50) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/dist/4300.js +1 -1
  3. package/dist/4480.js +1 -0
  4. package/dist/4480.js.map +1 -0
  5. package/dist/597.js +1 -0
  6. package/dist/597.js.map +1 -0
  7. package/dist/7231.js +1 -0
  8. package/dist/7231.js.map +1 -0
  9. package/dist/8519.js +1 -1
  10. package/dist/8519.js.map +1 -1
  11. package/dist/8572.js +1 -0
  12. package/dist/8572.js.map +1 -0
  13. package/dist/9388.js +1 -0
  14. package/dist/9388.js.map +1 -0
  15. package/dist/main.js +1 -1
  16. package/dist/main.js.map +1 -1
  17. package/dist/openmrs-esm-patient-flags-app.js +1 -1
  18. package/dist/openmrs-esm-patient-flags-app.js.buildmanifest.json +129 -105
  19. package/dist/routes.json +1 -1
  20. package/package.json +2 -2
  21. package/src/config-schema.ts +13 -2
  22. package/src/flags/flags-list-extension/flags-list.extension.tsx +12 -0
  23. package/src/flags/flags-list.component.tsx +68 -0
  24. package/src/flags/flags-list.scss +43 -147
  25. package/src/flags/flags-list.test.tsx +50 -68
  26. package/src/flags/flags-risk-count-extension/extension-config-schema.ts +17 -0
  27. package/src/flags/flags-risk-count-extension/flags-risk-count.extension.tsx +84 -0
  28. package/src/flags/{flags-highlight-bar.scss → flags-risk-count-extension/flags-risk-count.scss} +14 -5
  29. package/src/flags/flags-risk-count-extension/flags-risk-count.test.tsx +99 -0
  30. package/src/flags/flags-workspace/flags-workspace.scss +167 -0
  31. package/src/flags/flags-workspace/flags-workspace.test.tsx +97 -0
  32. package/src/flags/flags-workspace/flags.workspace.tsx +255 -0
  33. package/src/flags/hooks/usePatientFlags.ts +19 -11
  34. package/src/index.ts +12 -7
  35. package/src/routes.json +18 -9
  36. package/translations/en.json +7 -15
  37. package/dist/1837.js +0 -1
  38. package/dist/1837.js.map +0 -1
  39. package/dist/5628.js +0 -1
  40. package/dist/5628.js.map +0 -1
  41. package/dist/6138.js +0 -1
  42. package/dist/6138.js.map +0 -1
  43. package/dist/6173.js +0 -1
  44. package/dist/6173.js.map +0 -1
  45. package/src/flags/flags-highlight-bar.component.tsx +0 -81
  46. package/src/flags/flags-highlight-bar.test.tsx +0 -85
  47. package/src/flags/flags.component.tsx +0 -82
  48. package/src/flags/flags.scss +0 -71
  49. package/src/flags/flags.test.tsx +0 -49
  50. package/src/flags/patient-flags.workspace.tsx +0 -234
@@ -1,82 +0,0 @@
1
- import React, { useCallback } from 'react';
2
- import { useTranslation } from 'react-i18next';
3
- import { Button, Tag, Toggletip, ToggletipButton, ToggletipContent } from '@carbon/react';
4
- import { CloseIcon, EditIcon, launchWorkspace2 } from '@openmrs/esm-framework';
5
- import { usePatientFlags } from './hooks/usePatientFlags';
6
- import styles from './flags.scss';
7
-
8
- interface FlagsProps {
9
- patientUuid: string;
10
- onHandleCloseHighlightBar: () => void;
11
- showHighlightBar: boolean;
12
- }
13
-
14
- type FlagWithPriority = ReturnType<typeof usePatientFlags>['flags'][0];
15
-
16
- const Flags: React.FC<FlagsProps> = ({ patientUuid, onHandleCloseHighlightBar, showHighlightBar }) => {
17
- const { t } = useTranslation();
18
- const { flags, isLoading, error } = usePatientFlags(patientUuid);
19
- const filteredFlags = flags.filter((flag: FlagWithPriority) => !flag.voided);
20
-
21
- const handleClickEditFlags = useCallback(() => launchWorkspace2('patient-flags-workspace'), []);
22
-
23
- const renderFlag = (flag: FlagWithPriority) => {
24
- const hasPriority = flag.flagWithPriority?.priority?.name;
25
- const priorityName = hasPriority ? hasPriority.toLowerCase() : '';
26
-
27
- const isInfoFlag = priorityName === 'info';
28
- const isRiskFlag = priorityName === 'risk';
29
-
30
- return (
31
- <Toggletip key={flag.uuid} align="bottom-start">
32
- <ToggletipButton label={isRiskFlag ? t('riskFlag', 'Risk flag') : t('infoFlag', 'Info flag')}>
33
- <Tag
34
- className={isInfoFlag ? styles.infoFlagTag : isRiskFlag ? styles.flagTag : undefined}
35
- type={isRiskFlag ? 'high-contrast' : undefined}
36
- >
37
- {isRiskFlag && <span className={styles.flagIcon}>&#128681;</span>}
38
- {flag.flag.display}
39
- </Tag>
40
- </ToggletipButton>
41
- <ToggletipContent>
42
- <div className={styles.content}>
43
- <p className={styles.title}>{flag.flag.display}</p>
44
- <p className={styles.message}>{flag.message}</p>
45
- </div>
46
- </ToggletipContent>
47
- </Toggletip>
48
- );
49
- };
50
-
51
- if (!isLoading && !error) {
52
- return (
53
- <div className={styles.container}>
54
- <div className={styles.flagsContainer}>{filteredFlags.map(renderFlag)}</div>
55
- {filteredFlags.length > 0 ? (
56
- <Button
57
- className={styles.actionButton}
58
- kind="ghost"
59
- renderIcon={EditIcon}
60
- onClick={handleClickEditFlags}
61
- iconDescription={t('editFlags', 'Edit flags')}
62
- >
63
- {t('edit', 'Edit')}
64
- </Button>
65
- ) : null}
66
- {showHighlightBar ? (
67
- <Button
68
- className={styles.actionButton}
69
- hasIconOnly
70
- iconDescription={t('closeFlagsBar', 'Close flags bar')}
71
- kind="ghost"
72
- renderIcon={CloseIcon}
73
- onClick={onHandleCloseHighlightBar}
74
- />
75
- ) : null}
76
- </div>
77
- );
78
- }
79
- return null;
80
- };
81
-
82
- export default Flags;
@@ -1,71 +0,0 @@
1
- @use '@carbon/colors';
2
- @use '@carbon/layout';
3
- @use '@carbon/type';
4
-
5
- .container {
6
- display: flex;
7
- flex-flow: row wrap;
8
- align-items: center;
9
- justify-content: flex-end;
10
- gap: layout.$spacing-03;
11
- }
12
-
13
- .flagsContainer {
14
- display: flex;
15
- flex-flow: row wrap;
16
- column-gap: layout.$spacing-02;
17
- row-gap: layout.$spacing-03;
18
- align-items: center;
19
- flex: 1;
20
- margin-right: auto;
21
- }
22
-
23
- .actionButton {
24
- max-height: layout.$spacing-08;
25
- flex-shrink: 0;
26
- align-self: center;
27
- }
28
-
29
- .flagIcon {
30
- margin-right: layout.$spacing-02;
31
- }
32
-
33
- .flagTag {
34
- padding: 0 layout.$spacing-03;
35
- span {
36
- display: flex;
37
- align-items: center;
38
- }
39
- }
40
-
41
- .infoFlagTag {
42
- padding: 0 layout.$spacing-03;
43
- background-color: colors.$orange-20;
44
- color: #934412;
45
- }
46
-
47
- .highlightBarContainer {
48
- display: flex;
49
- align-items: center;
50
- padding: layout.$spacing-03;
51
- border-bottom: 1px solid colors.$gray-20;
52
- }
53
-
54
- .tooltipPadding {
55
- padding: layout.$spacing-02;
56
- }
57
-
58
- .content {
59
- p {
60
- @include type.type-style('body-01');
61
- }
62
-
63
- .title {
64
- @include type.type-style('heading-compact-01');
65
- }
66
-
67
- .message {
68
- margin-top: layout.$spacing-03;
69
- color: colors.$gray-30;
70
- }
71
- }
@@ -1,49 +0,0 @@
1
- import React from 'react';
2
- import userEvent from '@testing-library/user-event';
3
- import { screen, render } from '@testing-library/react';
4
- import { launchWorkspace2 } from '@openmrs/esm-framework';
5
- import { mockPatient } from 'tools';
6
- import { mockPatientFlags } from '__mocks__';
7
- import { usePatientFlags } from './hooks/usePatientFlags';
8
- import Flags from './flags.component';
9
-
10
- type FlagWithPriority = ReturnType<typeof usePatientFlags>['flags'][0];
11
-
12
- const mockUsePatientFlags = jest.mocked(usePatientFlags);
13
- const mockLaunchWorkspace = jest.mocked(launchWorkspace2);
14
-
15
- jest.mock('./hooks/usePatientFlags', () => {
16
- const originalModule = jest.requireActual('./hooks/usePatientFlags');
17
-
18
- return {
19
- ...originalModule,
20
- usePatientFlags: jest.fn(),
21
- };
22
- });
23
-
24
- it('renders flags in the patient flags slot', async () => {
25
- const user = userEvent.setup();
26
-
27
- mockUsePatientFlags.mockReturnValue({
28
- error: null,
29
- flags: mockPatientFlags as FlagWithPriority[],
30
- isLoading: false,
31
- isValidating: false,
32
- mutate: jest.fn(),
33
- });
34
-
35
- render(<Flags patientUuid={mockPatient.id} onHandleCloseHighlightBar={jest.fn()} showHighlightBar={false} />);
36
-
37
- const flags = screen.getAllByRole('button', { name: /flag/i });
38
- expect(flags).toHaveLength(3);
39
- expect(screen.getByText(/patient needs to be followed up/i)).toBeInTheDocument();
40
- expect(screen.getByText(/diagnosis for the patient is unknown/i)).toBeInTheDocument();
41
- expect(screen.getByText(/patient has a future appointment scheduled/i)).toBeInTheDocument();
42
-
43
- const editButton = screen.getByRole('button', { name: /edit/i });
44
- expect(editButton).toBeInTheDocument();
45
-
46
- await user.click(editButton);
47
-
48
- expect(mockLaunchWorkspace).toHaveBeenCalledWith('patient-flags-workspace');
49
- });
@@ -1,234 +0,0 @@
1
- import React, { useMemo, useState } from 'react';
2
- import { orderBy } from 'lodash-es';
3
- import { useTranslation } from 'react-i18next';
4
- import { Button, ButtonSet, Dropdown, Form, InlineLoading, Search, Tile, Toggle, Stack } from '@carbon/react';
5
- import { type PatientWorkspace2DefinitionProps } from '@openmrs/esm-patient-common-lib';
6
- import { useLayoutType, showSnackbar, parseDate, formatDate, ResponsiveWrapper } from '@openmrs/esm-framework';
7
- import { usePatientFlags, enablePatientFlag, disablePatientFlag } from './hooks/usePatientFlags';
8
- import { getFlagType } from './utils';
9
- import styles from './flags-list.scss';
10
-
11
- type SortKey = 'alpha' | 'active' | 'retired';
12
-
13
- const FlagsList: React.FC<PatientWorkspace2DefinitionProps<{}, {}>> = ({
14
- closeWorkspace,
15
- groupProps: { patientUuid },
16
- }) => {
17
- const { t } = useTranslation();
18
- const { flags, isLoading, error, mutate } = usePatientFlags(patientUuid);
19
- const isTablet = useLayoutType() === 'tablet';
20
- const [isEnabling, setIsEnabling] = useState(false);
21
- const [isDisabling, setIsDisabling] = useState(false);
22
- const [searchTerm, setSearchTerm] = useState('');
23
- const [sortBy, setSortBy] = useState<SortKey>('alpha');
24
-
25
- const sortItems = useMemo(
26
- () => [
27
- { id: 'alpha' as const, label: t('alphabetically', 'A - Z') },
28
- { id: 'active' as const, label: t('activeFirst', 'Active first') },
29
- { id: 'retired' as const, label: t('retiredFirst', 'Retired first') },
30
- ],
31
- [t],
32
- );
33
-
34
- const sortedRows = useMemo(() => {
35
- if (!sortBy) {
36
- return flags;
37
- }
38
- if (sortBy === 'active') {
39
- return orderBy(flags, [(item) => Number(item.voided)], ['asc']);
40
- }
41
- if (sortBy === 'retired') {
42
- return orderBy(flags, [(item) => Number(item.voided)], ['desc']);
43
- }
44
- return orderBy(flags, [(f) => f.flag.display], ['asc']);
45
- }, [sortBy, flags]);
46
-
47
- const searchResults = useMemo(() => {
48
- const query = searchTerm.trim().toLowerCase();
49
- if (query) {
50
- return sortedRows.filter((f) => f.flag.display.toLowerCase().includes(query));
51
- }
52
- return sortedRows;
53
- }, [searchTerm, sortedRows]);
54
-
55
- const handleSortByChange = ({ selectedItem }) => setSortBy(selectedItem?.id as SortKey);
56
-
57
- const handleEnableFlag = async (flagUuid) => {
58
- setIsEnabling(true);
59
- try {
60
- const res = await enablePatientFlag(flagUuid);
61
- if (!res.ok) {
62
- throw new Error('Enable flag failed');
63
- }
64
- mutate();
65
- showSnackbar({
66
- isLowContrast: true,
67
- kind: 'success',
68
- subtitle: t('flagEnabledSuccessfully', 'Flag successfully enabled'),
69
- title: t('enabledFlag', 'Enabled flag'),
70
- });
71
- } catch (e) {
72
- showSnackbar({
73
- isLowContrast: false,
74
- kind: 'error',
75
- subtitle: t('flagEnableError', 'Error enabling flag'),
76
- title: t('enableFlagError', 'Enable flag error'),
77
- });
78
- } finally {
79
- setIsEnabling(false);
80
- }
81
- };
82
-
83
- const handleDisableFlag = async (flagUuid) => {
84
- setIsDisabling(true);
85
- try {
86
- const res = await disablePatientFlag(flagUuid);
87
- if (!res.ok) {
88
- throw new Error('Disable flag failed');
89
- }
90
- mutate();
91
- showSnackbar({
92
- isLowContrast: true,
93
- kind: 'success',
94
- subtitle: t('flagDisabledSuccessfully', 'Flag successfully disabled'),
95
- title: t('flagDisabled', 'Flag disabled'),
96
- });
97
- } catch (e) {
98
- showSnackbar({
99
- isLowContrast: false,
100
- kind: 'error',
101
- subtitle: t('flagDisableError', 'Error disabling the flag'),
102
- title: t('disableFlagError', 'Disable flag error'),
103
- });
104
- } finally {
105
- setIsDisabling(false);
106
- }
107
- };
108
-
109
- if (isLoading) {
110
- return <InlineLoading className={styles.loading} description={`${t('loading', 'Loading')} ...`} />;
111
- }
112
-
113
- if (error) {
114
- return <div>{error.message}</div>;
115
- }
116
-
117
- return (
118
- <Form className={styles.formWrapper}>
119
- {/* The <div> below is required to maintain the page layout styling */}
120
- <div>
121
- <ResponsiveWrapper>
122
- <Search
123
- labelText={t('searchForAFlag', 'Search for a flag')}
124
- placeholder={t('searchForAFlag', 'Search for a flag')}
125
- value={searchTerm}
126
- size={isTablet ? 'lg' : 'md'}
127
- onChange={(e) => setSearchTerm(e.target.value)}
128
- />
129
- </ResponsiveWrapper>
130
- <Stack gap={4}>
131
- <div className={styles.listWrapper}>
132
- <div className={styles.flagsHeaderInfo}>
133
- {searchResults.length > 0 ? (
134
- <>
135
- <span className={styles.resultsCount}>
136
- {t('matchesForSearchTerm', '{{count}} flags', {
137
- count: searchResults.length,
138
- })}
139
- </span>
140
- <Dropdown
141
- className={styles.sortDropdown}
142
- id="sortBy"
143
- label=""
144
- type="inline"
145
- items={sortItems}
146
- itemToString={(item) => item?.label ?? ''}
147
- selectedItem={sortItems.find((item) => item.id === sortBy)}
148
- onChange={handleSortByChange}
149
- titleText={t('sortBy', 'Sort by')}
150
- />
151
- </>
152
- ) : null}
153
- </div>
154
- {searchResults.length > 0
155
- ? searchResults.map((result) => (
156
- <div className={styles.flagTile} key={result.uuid}>
157
- <div className={styles.flagHeader}>
158
- <div className={styles.titleAndType}>
159
- <div className={styles.flagTitle}>{result.flag.display}</div>&middot;
160
- <span className={styles.type}>
161
- {(() => {
162
- const typeLabel = getFlagType(result.tags);
163
- return typeLabel ? t(typeLabel, typeLabel) : t('clinical', 'Clinical');
164
- })()}
165
- </span>
166
- </div>
167
- <Toggle
168
- className={styles.flagToggle}
169
- defaultToggled={!Boolean(result.voided)}
170
- id={result.uuid}
171
- labelA=""
172
- labelB=""
173
- onToggle={(on) => (on ? handleEnableFlag(result.uuid) : handleDisableFlag(result.uuid))}
174
- size="sm"
175
- />
176
- </div>
177
- {result.voided ? null : (
178
- <div className={styles.secondRow}>
179
- <div className={styles.metadata}>
180
- <span className={styles.label}>{t('assigned', 'Assigned')}</span>
181
- <span className={styles.value}>
182
- {formatDate(parseDate(result.auditInfo?.dateCreated), { time: false })}
183
- </span>
184
- </div>
185
- </div>
186
- )}
187
- </div>
188
- ))
189
- : null}
190
-
191
- {searchResults.length === 0 ? (
192
- <div className={styles.emptyState}>
193
- <Tile className={styles.tile}>
194
- <p className={styles.content}>{t('noFlagsFound', 'Sorry, no flags found matching your search')}</p>
195
- <p className={styles.helper}>
196
- <Button
197
- kind="ghost"
198
- size="sm"
199
- onClick={() => {
200
- setSearchTerm('');
201
- }}
202
- >
203
- {t('clearSearch', 'Clear search')}
204
- </Button>
205
- </p>
206
- </Tile>
207
- </div>
208
- ) : null}
209
- </div>
210
- </Stack>
211
- </div>
212
- <ButtonSet className={isTablet ? styles.tabletButtonSet : styles.desktopButtonSet}>
213
- <Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
214
- {t('discard', 'Discard')}
215
- </Button>
216
- <Button
217
- className={styles.button}
218
- disabled={isEnabling || isDisabling}
219
- kind="primary"
220
- type="submit"
221
- onClick={() => closeWorkspace({ discardUnsavedChanges: true })}
222
- >
223
- {(() => {
224
- if (isEnabling) return t('enablingFlag', 'Enabling flag...');
225
- if (isDisabling) return t('disablingFlag', 'Disabling flag...');
226
- return t('saveAndClose', 'Save & close');
227
- })()}
228
- </Button>
229
- </ButtonSet>
230
- </Form>
231
- );
232
- };
233
-
234
- export default FlagsList;