@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
@@ -0,0 +1,167 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .listWrapper {
6
+ margin: layout.$spacing-05;
7
+ }
8
+
9
+ .flagTile {
10
+ display: flex;
11
+ flex-direction: column;
12
+ justify-content: space-between;
13
+ border: 1px solid #ededed;
14
+ background-color: colors.$gray-10;
15
+ padding: layout.$spacing-03;
16
+ }
17
+
18
+ :global(.omrs-breakpoint-lt-desktop) {
19
+ .flagTile {
20
+ margin-bottom: layout.$spacing-03;
21
+ }
22
+ }
23
+
24
+ :global(.omrs-breakpoint-gt-tablet) {
25
+ .flagTile {
26
+ margin-bottom: layout.$spacing-02;
27
+ }
28
+ }
29
+
30
+ .flagHeader {
31
+ display: flex;
32
+ justify-content: space-between;
33
+ align-items: center;
34
+ margin-bottom: layout.$spacing-03;
35
+ font-size: 0.875rem;
36
+ }
37
+
38
+ .titleAndType {
39
+ display: flex;
40
+ align-items: baseline;
41
+ }
42
+
43
+ .flagTitle {
44
+ @include type.type-style('body-01');
45
+ color: colors.$gray-100;
46
+ margin-right: layout.$spacing-02;
47
+ }
48
+
49
+ .type {
50
+ @include type.type-style('label-01');
51
+ color: colors.$gray-70;
52
+ margin-left: layout.$spacing-02;
53
+ }
54
+
55
+ .secondRow {
56
+ font-size: 0.875rem;
57
+ display: flex;
58
+ align-items: center;
59
+ }
60
+
61
+ .metadata {
62
+ display: flex;
63
+ flex-direction: column;
64
+ }
65
+
66
+ .label {
67
+ @include type.type-style('label-01');
68
+ color: colors.$gray-70;
69
+ margin-bottom: layout.$spacing-02;
70
+ }
71
+
72
+ .value {
73
+ @include type.type-style('body-01');
74
+ }
75
+
76
+ .flagToggle {
77
+ :global(.cds--toggle__switch--checked) {
78
+ background-color: colors.$green-50;
79
+ }
80
+ }
81
+
82
+ .flagsHeaderInfo {
83
+ display: flex;
84
+ justify-content: space-between;
85
+ align-items: center;
86
+ }
87
+
88
+ .resultsCount {
89
+ @include type.type-style('label-01');
90
+ }
91
+
92
+ .formWrapper {
93
+ display: flex;
94
+ flex-direction: column;
95
+ justify-content: space-between;
96
+ height: 100%;
97
+ background-color: colors.$white;
98
+ }
99
+
100
+ .button {
101
+ height: layout.$spacing-10;
102
+ display: flex;
103
+ align-content: flex-start;
104
+ align-items: baseline;
105
+ min-width: 50%;
106
+ }
107
+
108
+ .tabletButtonSet {
109
+ padding: layout.$spacing-06 layout.$spacing-05;
110
+ background-color: colors.$white;
111
+ }
112
+
113
+ .desktopButtonSet {
114
+ padding: 0;
115
+ }
116
+
117
+ .emptyText {
118
+ color: colors.$gray-70;
119
+ font-size: 0.875rem;
120
+ text-align: center;
121
+ }
122
+
123
+ .sortDropdown {
124
+ @include type.type-style('body-compact-01');
125
+ color: colors.$gray-100;
126
+ gap: 0;
127
+
128
+ :global(.cds--dropdown--inline .cds--list-box__menu) {
129
+ min-width: layout.$spacing-13;
130
+ max-width: 12rem;
131
+ left: -4.375rem;
132
+ }
133
+ }
134
+
135
+ .emptyState {
136
+ display: flex;
137
+ justify-content: center;
138
+ align-items: center;
139
+ padding: layout.$spacing-05;
140
+ text-align: center;
141
+ }
142
+
143
+ .tile {
144
+ margin: auto;
145
+ width: 18.75rem;
146
+ display: flex;
147
+ flex-direction: column;
148
+ align-items: center;
149
+ justify-content: center;
150
+ }
151
+
152
+ .content {
153
+ max-width: layout.$spacing-13;
154
+ @include type.type-style('heading-compact-02');
155
+ margin-bottom: layout.$spacing-05;
156
+ }
157
+
158
+ .helper {
159
+ @include type.type-style('body-compact-01');
160
+ }
161
+
162
+ .loader {
163
+ display: flex;
164
+ justify-content: center;
165
+ background-color: colors.$gray-10;
166
+ min-height: layout.$spacing-09;
167
+ }
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { mockPatient } from 'tools';
5
+ import { mockPatientFlags } from '__mocks__';
6
+ import { usePatientFlags } from '../hooks/usePatientFlags';
7
+ import FlagsWorkspace from './flags.workspace';
8
+
9
+ const mockUsePatientFlags = usePatientFlags as jest.Mock;
10
+
11
+ jest.mock('../hooks/usePatientFlags', () => {
12
+ const originalModule = jest.requireActual('../hooks/usePatientFlags');
13
+
14
+ return {
15
+ ...originalModule,
16
+ usePatientFlags: jest.fn(),
17
+ };
18
+ });
19
+
20
+ it('renders an Edit form that enables users to toggle flags on or off', async () => {
21
+ mockUsePatientFlags.mockReturnValue({
22
+ flags: mockPatientFlags,
23
+ isLoading: false,
24
+ error: null,
25
+ isValidating: false,
26
+ mutate: jest.fn(),
27
+ });
28
+
29
+ render(
30
+ <FlagsWorkspace
31
+ closeWorkspace={jest.fn()}
32
+ groupProps={{
33
+ patientUuid: mockPatient.id,
34
+ patient: mockPatient,
35
+ visitContext: null,
36
+ mutateVisitContext: null,
37
+ }}
38
+ workspaceProps={{}}
39
+ windowProps={{}}
40
+ workspaceName=""
41
+ launchChildWorkspace={null}
42
+ windowName={''}
43
+ isRootWorkspace={false}
44
+ />,
45
+ );
46
+
47
+ const searchbox = screen.getByRole('searchbox', { name: /search for a flag/i });
48
+ const clearSearchInputButton = screen.getByRole('button', { name: /clear search input/i });
49
+ const discardButton = screen.getByRole('button', { name: /discard/i });
50
+ const saveButton = screen.getByRole('button', { name: /save & close/i });
51
+
52
+ expect(searchbox).toBeInTheDocument();
53
+ expect(clearSearchInputButton).toBeInTheDocument();
54
+ expect(discardButton).toBeInTheDocument();
55
+ expect(saveButton).toBeInTheDocument();
56
+ expect(screen.getByText(/patient has a future appointment scheduled/i)).toBeInTheDocument();
57
+ expect(screen.getByText(/patient needs to be followed up/i)).toBeInTheDocument();
58
+ expect(screen.getByText(/social/i)).toBeInTheDocument();
59
+ });
60
+
61
+ it('sorts by active and retired correctly via controlled dropdown', async () => {
62
+ mockUsePatientFlags.mockReturnValue({
63
+ flags: mockPatientFlags,
64
+ isLoading: false,
65
+ error: null,
66
+ isValidating: false,
67
+ mutate: jest.fn(),
68
+ });
69
+
70
+ render(
71
+ <FlagsWorkspace
72
+ closeWorkspace={jest.fn()}
73
+ groupProps={{
74
+ patientUuid: mockPatient.id,
75
+ patient: mockPatient,
76
+ visitContext: null,
77
+ mutateVisitContext: null,
78
+ }}
79
+ workspaceProps={{}}
80
+ windowProps={{}}
81
+ workspaceName=""
82
+ launchChildWorkspace={null}
83
+ windowName={''}
84
+ isRootWorkspace={false}
85
+ />,
86
+ );
87
+
88
+ const user = userEvent.setup();
89
+ const sortDropdown = screen.getByRole('combobox');
90
+ expect(sortDropdown).toBeInTheDocument();
91
+
92
+ // select "Retired first" then "Active first" to exercise both flows
93
+ await user.click(sortDropdown);
94
+ await user.click(screen.getByText(/Retired first/i));
95
+ await user.click(sortDropdown);
96
+ await user.click(screen.getByText(/Active first/i));
97
+ });
@@ -0,0 +1,255 @@
1
+ import React, { useCallback, 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 {
7
+ useLayoutType,
8
+ showSnackbar,
9
+ parseDate,
10
+ formatDate,
11
+ ResponsiveWrapper,
12
+ Workspace2,
13
+ } from '@openmrs/esm-framework';
14
+ import { usePatientFlags, enablePatientFlag, disablePatientFlag } from '../hooks/usePatientFlags';
15
+ import { getFlagType } from '../utils';
16
+ import styles from './flags-workspace.scss';
17
+
18
+ type SortKey = 'alpha' | 'active' | 'retired';
19
+
20
+ const FlagsWorkspace: React.FC<PatientWorkspace2DefinitionProps<{}, {}>> = ({
21
+ closeWorkspace,
22
+ groupProps: { patientUuid },
23
+ }) => {
24
+ const { t } = useTranslation();
25
+ const { flags, isLoading, error, mutate } = usePatientFlags(patientUuid);
26
+ const isTablet = useLayoutType() === 'tablet';
27
+ const [isSaving, setIsSaving] = useState(false);
28
+ const [searchTerm, setSearchTerm] = useState('');
29
+ const [sortBy, setSortBy] = useState<SortKey>('alpha');
30
+ // Track pending changes: Map of flagUuid -> desired active state
31
+ const [pendingActiveStates, setPendingActiveStates] = useState<Map<string, boolean>>(new Map());
32
+
33
+ const hasUnsavedChanges = pendingActiveStates.size > 0;
34
+
35
+ const sortItems = useMemo(
36
+ () => [
37
+ { id: 'alpha' as const, label: t('alphabetically', 'A - Z') },
38
+ { id: 'active' as const, label: t('activeFirst', 'Active first') },
39
+ { id: 'retired' as const, label: t('retiredFirst', 'Retired first') },
40
+ ],
41
+ [t],
42
+ );
43
+
44
+ const sortedRows = useMemo(() => {
45
+ if (!sortBy) {
46
+ return flags;
47
+ }
48
+ if (sortBy === 'active') {
49
+ return orderBy(flags, [(item) => Number(item.voided)], ['asc']);
50
+ }
51
+ if (sortBy === 'retired') {
52
+ return orderBy(flags, [(item) => Number(item.voided)], ['desc']);
53
+ }
54
+ return orderBy(flags, [(f) => f.message], ['asc']);
55
+ }, [sortBy, flags]);
56
+
57
+ const searchResults = useMemo(() => {
58
+ const query = searchTerm.trim().toLowerCase();
59
+ if (query) {
60
+ return sortedRows.filter((f) => f.message.toLowerCase().includes(query));
61
+ }
62
+ return sortedRows;
63
+ }, [searchTerm, sortedRows]);
64
+
65
+ const handleSortByChange = ({ selectedItem }) => setSortBy(selectedItem?.id as SortKey);
66
+
67
+ // Get the effective active state for a flag (considering pending changes)
68
+ const isActive = useCallback(
69
+ (flagUuid: string, originallyActive: boolean) => {
70
+ return pendingActiveStates.has(flagUuid) ? pendingActiveStates.get(flagUuid) : originallyActive;
71
+ },
72
+ [pendingActiveStates],
73
+ );
74
+
75
+ const handleToggle = useCallback((flagUuid: string, originallyActive: boolean, isNowActive: boolean) => {
76
+ setPendingActiveStates((prev) => {
77
+ const next = new Map(prev);
78
+ if (isNowActive === originallyActive) {
79
+ next.delete(flagUuid);
80
+ } else {
81
+ next.set(flagUuid, isNowActive);
82
+ }
83
+ return next;
84
+ });
85
+ }, []);
86
+
87
+ const handleSave = useCallback(async () => {
88
+ if (pendingActiveStates.size === 0) {
89
+ closeWorkspace({ discardUnsavedChanges: true });
90
+ return;
91
+ }
92
+
93
+ setIsSaving(true);
94
+ const promises: Promise<{ flagUuid: string; success: boolean }>[] = [];
95
+
96
+ pendingActiveStates.forEach((active, flagUuid) => {
97
+ const promise = (active ? enablePatientFlag(flagUuid) : disablePatientFlag(flagUuid))
98
+ .then((res) => ({ flagUuid, success: res.ok }))
99
+ .catch(() => ({ flagUuid, success: false }));
100
+ promises.push(promise);
101
+ });
102
+
103
+ const results = await Promise.all(promises);
104
+ const failures = results.filter((r) => !r.success);
105
+
106
+ if (failures.length === 0) {
107
+ showSnackbar({
108
+ isLowContrast: true,
109
+ kind: 'success',
110
+ subtitle: t('flagsUpdatedSuccessfully', 'Flags updated successfully'),
111
+ title: t('flagsUpdated', 'Flags updated'),
112
+ });
113
+ mutate();
114
+ closeWorkspace({ discardUnsavedChanges: true });
115
+ } else {
116
+ showSnackbar({
117
+ isLowContrast: false,
118
+ kind: 'error',
119
+ subtitle: t('flagsUpdateError', 'Some flags failed to update'),
120
+ title: t('updateError', 'Update error'),
121
+ });
122
+ // Clear successful changes from pending, keep failures
123
+ setPendingActiveStates((prev) => {
124
+ const next = new Map(prev);
125
+ results.filter((r) => r.success).forEach((r) => next.delete(r.flagUuid));
126
+ return next;
127
+ });
128
+ mutate();
129
+ }
130
+
131
+ setIsSaving(false);
132
+ }, [pendingActiveStates, closeWorkspace, mutate, t]);
133
+
134
+ if (isLoading) {
135
+ return <InlineLoading className={styles.loading} description={`${t('loading', 'Loading')} ...`} />;
136
+ }
137
+
138
+ if (error) {
139
+ return <div>{error.message}</div>;
140
+ }
141
+
142
+ return (
143
+ <Workspace2 title={t('editPatientFlags', 'Edit patient flags')} hasUnsavedChanges={hasUnsavedChanges}>
144
+ <Form className={styles.formWrapper}>
145
+ {/* The <div> below is required to maintain the page layout styling */}
146
+ <div>
147
+ <ResponsiveWrapper>
148
+ <Search
149
+ labelText={t('searchForAFlag', 'Search for a flag')}
150
+ placeholder={t('searchForAFlag', 'Search for a flag')}
151
+ value={searchTerm}
152
+ size={isTablet ? 'lg' : 'md'}
153
+ onChange={(e) => setSearchTerm(e.target.value)}
154
+ />
155
+ </ResponsiveWrapper>
156
+ <Stack gap={4}>
157
+ <div className={styles.listWrapper}>
158
+ <div className={styles.flagsHeaderInfo}>
159
+ {searchResults.length > 0 ? (
160
+ <>
161
+ <span className={styles.resultsCount}>
162
+ {t('matchesForSearchTerm', '{{count}} flags', {
163
+ count: searchResults.length,
164
+ })}
165
+ </span>
166
+ <Dropdown
167
+ className={styles.sortDropdown}
168
+ id="sortBy"
169
+ label=""
170
+ type="inline"
171
+ items={sortItems}
172
+ itemToString={(item) => item?.label ?? ''}
173
+ selectedItem={sortItems.find((item) => item.id === sortBy)}
174
+ onChange={handleSortByChange}
175
+ titleText={t('sortBy', 'Sort by')}
176
+ />
177
+ </>
178
+ ) : null}
179
+ </div>
180
+ {searchResults.length > 0
181
+ ? searchResults.map((result) => {
182
+ const originallyActive = !result.voided;
183
+ const active = isActive(result.uuid, originallyActive);
184
+ return (
185
+ <div className={styles.flagTile} key={result.uuid}>
186
+ <div className={styles.flagHeader}>
187
+ <div className={styles.titleAndType}>
188
+ <div className={styles.flagTitle}>{result.message}</div>&middot;
189
+ <span className={styles.type}>
190
+ {(() => {
191
+ const typeLabel = getFlagType(result.tags);
192
+ return typeLabel ? t(typeLabel, typeLabel) : t('clinical', 'Clinical');
193
+ })()}
194
+ </span>
195
+ </div>
196
+ <Toggle
197
+ className={styles.flagToggle}
198
+ toggled={active}
199
+ id={result.uuid}
200
+ labelA=""
201
+ labelB=""
202
+ onToggle={(isNowActive) => handleToggle(result.uuid, originallyActive, isNowActive)}
203
+ size="sm"
204
+ />
205
+ </div>
206
+ {active && (
207
+ <div className={styles.secondRow}>
208
+ <div className={styles.metadata}>
209
+ <span className={styles.label}>{t('assigned', 'Assigned')}</span>
210
+ <span className={styles.value}>
211
+ {formatDate(parseDate(result.auditInfo?.dateCreated), { time: false })}
212
+ </span>
213
+ </div>
214
+ </div>
215
+ )}
216
+ </div>
217
+ );
218
+ })
219
+ : null}
220
+
221
+ {searchResults.length === 0 ? (
222
+ <div className={styles.emptyState}>
223
+ <Tile className={styles.tile}>
224
+ <p className={styles.content}>{t('noFlagsFound', 'Sorry, no flags found matching your search')}</p>
225
+ <p className={styles.helper}>
226
+ <Button
227
+ kind="ghost"
228
+ size="sm"
229
+ onClick={() => {
230
+ setSearchTerm('');
231
+ }}
232
+ >
233
+ {t('clearSearch', 'Clear search')}
234
+ </Button>
235
+ </p>
236
+ </Tile>
237
+ </div>
238
+ ) : null}
239
+ </div>
240
+ </Stack>
241
+ </div>
242
+ <ButtonSet className={isTablet ? styles.tabletButtonSet : styles.desktopButtonSet}>
243
+ <Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
244
+ {t('discard', 'Discard')}
245
+ </Button>
246
+ <Button className={styles.button} disabled={isSaving} kind="primary" type="button" onClick={handleSave}>
247
+ {isSaving ? t('saving', 'Saving...') : t('saveAndClose', 'Save & close')}
248
+ </Button>
249
+ </ButtonSet>
250
+ </Form>
251
+ </Workspace2>
252
+ );
253
+ };
254
+
255
+ export default FlagsWorkspace;
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
2
2
  import useSWR from 'swr';
3
3
  import { openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
4
4
 
5
- interface FlagFetchResponse {
5
+ export interface FlagFetchResponse {
6
6
  uuid: string;
7
7
  message: string;
8
8
  voided: boolean;
@@ -12,23 +12,23 @@ interface FlagFetchResponse {
12
12
  auditInfo: { dateCreated: string };
13
13
  }
14
14
 
15
- interface FlagDefinition {
15
+ export interface FlagDefinition {
16
16
  uuid: string;
17
17
  display: string;
18
18
  priority: { uuid: string; name: string };
19
19
  tags: Array<{ uuid: string; display: string }>;
20
20
  }
21
21
 
22
- interface FlagsFetchResponse {
22
+ export interface FlagsFetchResponse {
23
23
  results: Array<FlagFetchResponse>;
24
24
  }
25
25
 
26
- interface FlagDefinitionsFetchResponse {
26
+ export interface FlagDefinitionsFetchResponse {
27
27
  results: Array<FlagDefinition>;
28
28
  }
29
29
 
30
- interface FlagWithPriority extends FlagFetchResponse {
31
- flagWithPriority?: FlagDefinition;
30
+ export interface FlagWithPriority extends FlagFetchResponse {
31
+ flagDefinition?: FlagDefinition;
32
32
  }
33
33
 
34
34
  /**
@@ -44,6 +44,7 @@ export function usePatientFlags(patientUuid: string) {
44
44
  data: patientFlagsData,
45
45
  error: patientFlagsError,
46
46
  isLoading: isLoadingPatientFlags,
47
+ mutate,
47
48
  } = useSWR<FetchResponse<FlagsFetchResponse>, Error>(patientUuid ? patientFlagsUrl : null, openmrsFetch);
48
49
 
49
50
  const patientFlags = patientFlagsData?.data?.results ?? [];
@@ -60,17 +61,24 @@ export function usePatientFlags(patientUuid: string) {
60
61
  const flagDefinitions = flagDefinitionsData?.data?.results ?? [];
61
62
 
62
63
  // Merge patient flags with flag definitions to get priority information
63
- const flagsWithPriority: FlagWithPriority[] = patientFlags.map((pf) => ({
64
- ...pf,
65
- flagWithPriority: flagDefinitions.find((f) => f.uuid === pf.flag.uuid),
66
- }));
64
+ const flagsWithPriority: FlagWithPriority[] = patientFlags.map((pf) => {
65
+ const flagDefinition = flagDefinitions.find((f) => f.uuid === pf.flag.uuid);
66
+ if (!flagDefinition) {
67
+ console.warn(`Flag definition not found for flag ${pf.flag.display}. Tag will be missing priority information.`);
68
+ return pf;
69
+ }
70
+ return {
71
+ ...pf,
72
+ flagDefinition,
73
+ };
74
+ });
67
75
 
68
76
  const result = {
69
77
  flags: flagsWithPriority,
70
78
  error: patientFlagsError || flagDefinitionsError,
71
79
  isLoading: isLoadingPatientFlags || isLoadingFlagDefinitions,
72
80
  isValidating: false,
73
- mutate: () => {},
81
+ mutate,
74
82
  };
75
83
 
76
84
  return result;
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { defineConfigSchema, getAsyncLifecycle } from '@openmrs/esm-framework';
1
+ import { defineConfigSchema, defineExtensionConfigSchema, getAsyncLifecycle } from '@openmrs/esm-framework';
2
2
  import { configSchema } from './config-schema';
3
+ import { riskCountExtensionConfigSchema } from './flags/flags-risk-count-extension/extension-config-schema';
3
4
 
4
5
  const moduleName = '@openmrs/esm-patient-flags-app';
5
6
 
@@ -7,19 +8,23 @@ export const importTranslation = require.context('../translations', false, /.jso
7
8
 
8
9
  export function startupApp() {
9
10
  defineConfigSchema(moduleName, configSchema);
11
+ defineExtensionConfigSchema('patient-flags-risk-count', riskCountExtensionConfigSchema);
10
12
  }
11
13
 
12
- export const flagTags = getAsyncLifecycle(() => import('./flags/flags-highlight-bar.component'), {
13
- featureName: 'patient-flag-tags',
14
- moduleName,
15
- });
14
+ export const flagsRiskCountExtension = getAsyncLifecycle(
15
+ () => import('./flags/flags-risk-count-extension/flags-risk-count.extension'),
16
+ {
17
+ featureName: 'patient-flags-risk-count',
18
+ moduleName,
19
+ },
20
+ );
16
21
 
17
- export const flagsOverview = getAsyncLifecycle(() => import('./flags/flags.component'), {
22
+ export const flagsListExtension = getAsyncLifecycle(() => import('./flags/flags-list-extension/flags-list.extension'), {
18
23
  featureName: 'patient-flags-overview',
19
24
  moduleName,
20
25
  });
21
26
 
22
- export const patientFlagsWorkspace = getAsyncLifecycle(() => import('./flags/patient-flags.workspace'), {
27
+ export const patientFlagsWorkspace = getAsyncLifecycle(() => import('./flags/flags-workspace/flags.workspace'), {
23
28
  featureName: 'patient-flags-workspace',
24
29
  moduleName,
25
30
  });
package/src/routes.json CHANGED
@@ -5,16 +5,19 @@
5
5
  },
6
6
  "extensions": [
7
7
  {
8
- "name": "patient-flag-tags",
9
- "slot": "patient-highlights-bar-slot",
10
- "component": "flagTags",
8
+ "name": "patient-flags-risk-count",
9
+ "slot": "patient-info-slot",
10
+ "component": "flagsRiskCountExtension",
11
11
  "order": 0,
12
- "featureFlag": "patient-flags"
12
+ "featureFlag": "patient-flags",
13
+ "meta": {
14
+ "fullWidth": true
15
+ }
13
16
  },
14
17
  {
15
- "name": "patient-flags-overview",
18
+ "name": "patient-flags-list",
16
19
  "slot": "patient-chart-summary-dashboard-slot",
17
- "component": "flagsOverview",
20
+ "component": "flagsListExtension",
18
21
  "order": 0,
19
22
  "featureFlag": "patient-flags",
20
23
  "meta": {
@@ -29,11 +32,17 @@
29
32
  "description": "Visual components that enable healthcare providers to see relevant patient information with a glance in the Patient chart. Flags are displayed in the Patient Summary, just below the patient banner, and can link users to other areas of the chart to perform relevant actions during a visit."
30
33
  }
31
34
  ],
32
- "workspaces": [
35
+ "workspaces2": [
33
36
  {
34
37
  "name": "patient-flags-workspace",
35
- "title": "editPatientFlags",
36
- "component": "patientFlagsWorkspace"
38
+ "component": "patientFlagsWorkspace",
39
+ "window": "patient-flags"
40
+ }
41
+ ],
42
+ "workspaceWindows2": [
43
+ {
44
+ "name": "patient-flags",
45
+ "group": "patient-chart"
37
46
  }
38
47
  ]
39
48
  }