@openmrs/esm-patient-flags-app 11.3.1-pre.9537 → 11.3.1-pre.9545

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,68 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, Tag } from '@carbon/react';
4
+ import { type ConfigObject, EditIcon, launchWorkspace2, useConfig } from '@openmrs/esm-framework';
5
+ import { type FlagWithPriority, usePatientFlags } from './hooks/usePatientFlags';
6
+ import styles from './flags-list.scss';
7
+
8
+ interface FlagsListProps {
9
+ patientUuid: string;
10
+ }
11
+
12
+ const FlagsList: React.FC<FlagsListProps> = ({ patientUuid }) => {
13
+ const { t } = useTranslation();
14
+ const { flags, isLoading, error } = usePatientFlags(patientUuid);
15
+ const filteredFlags = flags.filter((flag) => !flag.voided);
16
+ const config = useConfig<ConfigObject>();
17
+
18
+ const handleClickEditFlags = useCallback(() => launchWorkspace2('patient-flags-workspace'), []);
19
+
20
+ if (!isLoading && !error) {
21
+ return (
22
+ <div className={styles.container}>
23
+ <ul className={styles.flagsList}>
24
+ {filteredFlags.map((flag) => (
25
+ <li key={flag.uuid}>
26
+ <Flag flag={flag} />
27
+ </li>
28
+ ))}
29
+ </ul>
30
+ {config.allowFlagDeletion && filteredFlags.length > 0 ? (
31
+ <Button
32
+ className={styles.actionButton}
33
+ hasIconOnly
34
+ kind="ghost"
35
+ size="sm"
36
+ renderIcon={EditIcon}
37
+ onClick={handleClickEditFlags}
38
+ iconDescription={t('editFlags', 'Edit flags')}
39
+ />
40
+ ) : null}
41
+ </div>
42
+ );
43
+ }
44
+ return null;
45
+ };
46
+
47
+ interface FlagProps {
48
+ flag: FlagWithPriority;
49
+ }
50
+
51
+ const Flag: React.FC<FlagProps> = ({ flag }) => {
52
+ const priorityName = flag.flagDefinition?.priority?.name?.toLowerCase() ?? '';
53
+ const isInfoFlag = priorityName === 'info';
54
+ const isRiskFlag = priorityName === 'risk';
55
+
56
+ return (
57
+ <Tag
58
+ key={flag.uuid}
59
+ className={isInfoFlag ? styles.infoFlagTag : isRiskFlag ? styles.flagTag : undefined}
60
+ type={isRiskFlag ? 'high-contrast' : undefined}
61
+ >
62
+ {isRiskFlag && <span className={styles.flagIcon}>&#128681;</span>}
63
+ {flag.message}
64
+ </Tag>
65
+ );
66
+ };
67
+
68
+ export default FlagsList;
@@ -1,175 +1,71 @@
1
+ @use '@openmrs/esm-styleguide/src/vars' as *;
1
2
  @use '@carbon/colors';
2
3
  @use '@carbon/layout';
3
4
  @use '@carbon/type';
4
5
 
5
- .listWrapper {
6
- margin: layout.$spacing-05;
7
- }
8
-
9
- .flagTile {
6
+ .container {
10
7
  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;
8
+ flex-flow: row wrap;
33
9
  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;
10
+ justify-content: flex-end;
11
+ gap: layout.$spacing-03;
53
12
  }
54
13
 
55
- .secondRow {
56
- font-size: 0.875rem;
14
+ .flagsList {
57
15
  display: flex;
16
+ flex-flow: row wrap;
17
+ column-gap: layout.$spacing-02;
18
+ row-gap: layout.$spacing-03;
58
19
  align-items: center;
20
+ flex: 1;
21
+ margin: 0;
22
+ margin-right: auto;
23
+ padding: 0;
24
+ list-style: none;
59
25
  }
60
26
 
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;
27
+ .actionButton {
28
+ max-height: layout.$spacing-08;
29
+ flex-shrink: 0;
30
+ align-self: center;
31
+ svg {
32
+ fill: $interactive-01 !important;
79
33
  }
80
34
  }
81
35
 
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
- .button {
93
- height: layout.$spacing-10;
94
- display: flex;
95
- align-content: flex-start;
96
- align-items: baseline;
97
- min-width: 50%;
98
- }
99
-
100
- .formWrapper {
101
- display: flex;
102
- flex-direction: column;
103
- justify-content: space-between;
104
- height: calc(100vh - layout.$spacing-12);
105
- background-color: colors.$white;
106
- }
107
-
108
- .button {
109
- height: layout.$spacing-10;
110
- display: flex;
111
- align-content: flex-start;
112
- align-items: baseline;
113
- min-width: 50%;
114
- }
115
-
116
- .tabletButtonSet {
117
- padding: layout.$spacing-06 layout.$spacing-05;
118
- background-color: colors.$white;
119
- }
120
-
121
- .desktopButtonSet {
122
- padding: 0;
123
- }
124
-
125
- .emptyText {
126
- color: colors.$gray-70;
127
- font-size: 0.875rem;
128
- text-align: center;
36
+ .flagIcon {
37
+ margin-right: layout.$spacing-02;
129
38
  }
130
39
 
131
- .sortDropdown {
132
- @include type.type-style('body-compact-01');
133
- color: colors.$gray-100;
134
- gap: 0;
135
-
136
- :global(.cds--dropdown--inline .cds--list-box__menu) {
137
- min-width: layout.$spacing-13;
138
- max-width: 12rem;
139
- left: -4.375rem;
40
+ .flagTag {
41
+ padding: 0 layout.$spacing-03;
42
+ span {
43
+ display: flex;
44
+ align-items: center;
140
45
  }
141
46
  }
142
47
 
143
- .emptyState {
144
- display: flex;
145
- justify-content: center;
146
- align-items: center;
147
- padding: layout.$spacing-05;
148
- text-align: center;
48
+ .infoFlagTag {
49
+ padding: 0 layout.$spacing-03;
50
+ background-color: colors.$orange-20;
51
+ color: #934412;
149
52
  }
150
53
 
151
- .tile {
152
- margin: auto;
153
- width: 18.75rem;
154
- display: flex;
155
- flex-direction: column;
156
- align-items: center;
157
- justify-content: center;
54
+ .tooltipPadding {
55
+ padding: layout.$spacing-02;
158
56
  }
159
57
 
160
58
  .content {
161
- max-width: layout.$spacing-13;
162
- @include type.type-style('heading-compact-02');
163
- margin-bottom: layout.$spacing-05;
164
- }
59
+ p {
60
+ @include type.type-style('body-01');
61
+ }
165
62
 
166
- .helper {
167
- @include type.type-style('body-compact-01');
168
- }
63
+ .title {
64
+ @include type.type-style('heading-compact-01');
65
+ }
169
66
 
170
- .loader {
171
- display: flex;
172
- justify-content: center;
173
- background-color: colors.$gray-10;
174
- min-height: layout.$spacing-09;
67
+ .message {
68
+ margin-top: layout.$spacing-03;
69
+ color: colors.$gray-30;
70
+ }
175
71
  }
@@ -1,12 +1,18 @@
1
1
  import React from 'react';
2
- import { render, screen } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
3
+ import { screen, render } from '@testing-library/react';
4
+ import { getDefaultsFromConfigSchema, launchWorkspace2, useConfig } from '@openmrs/esm-framework';
4
5
  import { mockPatient } from 'tools';
5
6
  import { mockPatientFlags } from '__mocks__';
7
+ import { type ConfigObject, configSchema } from '../config-schema';
6
8
  import { usePatientFlags } from './hooks/usePatientFlags';
7
- import FlagsList from './patient-flags.workspace';
9
+ import FlagsList from './flags-list.component';
8
10
 
9
- const mockUsePatientFlags = usePatientFlags as jest.Mock;
11
+ type FlagWithPriority = ReturnType<typeof usePatientFlags>['flags'][0];
12
+
13
+ const mockUsePatientFlags = jest.mocked(usePatientFlags);
14
+ const mockLaunchWorkspace = jest.mocked(launchWorkspace2);
15
+ const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
10
16
 
11
17
  jest.mock('./hooks/usePatientFlags', () => {
12
18
  const originalModule = jest.requireActual('./hooks/usePatientFlags');
@@ -17,77 +23,53 @@ jest.mock('./hooks/usePatientFlags', () => {
17
23
  };
18
24
  });
19
25
 
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
- });
26
+ describe('flags list', () => {
27
+ it('flags list displays flags and edit button', async () => {
28
+ const user = userEvent.setup();
26
29
 
27
- render(
28
- <FlagsList
29
- closeWorkspace={jest.fn()}
30
- groupProps={{
31
- patientUuid: mockPatient.id,
32
- patient: mockPatient,
33
- visitContext: null,
34
- mutateVisitContext: null,
35
- }}
36
- workspaceProps={{}}
37
- windowProps={{}}
38
- workspaceName=""
39
- launchChildWorkspace={null}
40
- windowName={''}
41
- isRootWorkspace={false}
42
- />,
43
- );
30
+ mockUseConfig.mockReturnValue(getDefaultsFromConfigSchema(configSchema));
31
+ mockUsePatientFlags.mockReturnValue({
32
+ error: null,
33
+ flags: mockPatientFlags as FlagWithPriority[],
34
+ isLoading: false,
35
+ isValidating: false,
36
+ mutate: jest.fn(),
37
+ });
44
38
 
45
- const searchbox = screen.getByRole('searchbox', { name: /search for a flag/i });
46
- const clearSearchInputButton = screen.getByRole('button', { name: /clear search input/i });
47
- const discardButton = screen.getByRole('button', { name: /discard/i });
48
- const saveButton = screen.getByRole('button', { name: /save & close/i });
39
+ render(<FlagsList patientUuid={mockPatient.id} />);
49
40
 
50
- expect(searchbox).toBeInTheDocument();
51
- expect(clearSearchInputButton).toBeInTheDocument();
52
- expect(discardButton).toBeInTheDocument();
53
- expect(saveButton).toBeInTheDocument();
54
- expect(screen.getByText(/future appointment/i)).toBeInTheDocument();
55
- expect(screen.getByText(/needs follow up/i)).toBeInTheDocument();
56
- expect(screen.getByText(/social/i)).toBeInTheDocument();
57
- });
41
+ const flags = screen.getAllByRole('listitem');
42
+ expect(flags).toHaveLength(3);
43
+ expect(screen.getByText(/patient needs to be followed up/i)).toBeInTheDocument();
44
+ expect(screen.getByText(/diagnosis for the patient is unknown/i)).toBeInTheDocument();
45
+ expect(screen.getByText(/patient has a future appointment scheduled/i)).toBeInTheDocument();
46
+
47
+ const editButton = screen.getByRole('button', { name: /edit flags/i });
48
+ expect(editButton).toBeInTheDocument();
58
49
 
59
- it('sorts by active and retired correctly via controlled dropdown', async () => {
60
- mockUsePatientFlags.mockReturnValue({
61
- flags: mockPatientFlags,
62
- isLoading: false,
63
- error: null,
50
+ await user.click(editButton);
51
+
52
+ expect(mockLaunchWorkspace).toHaveBeenCalledWith('patient-flags-workspace');
64
53
  });
65
54
 
66
- render(
67
- <FlagsList
68
- closeWorkspace={jest.fn()}
69
- groupProps={{
70
- patientUuid: mockPatient.id,
71
- patient: mockPatient,
72
- visitContext: null,
73
- mutateVisitContext: null,
74
- }}
75
- workspaceProps={{}}
76
- windowProps={{}}
77
- workspaceName=""
78
- launchChildWorkspace={null}
79
- windowName={''}
80
- isRootWorkspace={false}
81
- />,
82
- );
55
+ it('hides the Edit button when allowFlagDeletion config is false', () => {
56
+ mockUseConfig.mockReturnValue({
57
+ ...getDefaultsFromConfigSchema(configSchema),
58
+ allowFlagDeletion: false,
59
+ });
60
+ mockUsePatientFlags.mockReturnValue({
61
+ error: null,
62
+ flags: mockPatientFlags as FlagWithPriority[],
63
+ isLoading: false,
64
+ isValidating: false,
65
+ mutate: jest.fn(),
66
+ });
67
+
68
+ render(<FlagsList patientUuid={mockPatient.id} />);
83
69
 
84
- const user = userEvent.setup();
85
- const sortDropdown = screen.getByRole('combobox');
86
- expect(sortDropdown).toBeInTheDocument();
70
+ const flags = screen.getAllByRole('listitem');
71
+ expect(flags).toHaveLength(3);
87
72
 
88
- // select "Retired first" then "Active first" to exercise both flows
89
- await user.click(sortDropdown);
90
- await user.click(screen.getByText(/Retired first/i));
91
- await user.click(sortDropdown);
92
- await user.click(screen.getByText(/Active first/i));
73
+ expect(screen.queryByRole('button', { name: /edit flags/i })).not.toBeInTheDocument();
74
+ });
93
75
  });
@@ -0,0 +1,17 @@
1
+ import { Type } from '@openmrs/esm-framework';
2
+
3
+ export const riskCountExtensionConfigSchema = {
4
+ hideOnPages: {
5
+ _type: Type.Array,
6
+ _default: ['Patient Summary'],
7
+ _description:
8
+ 'List of page names where the risk count badge should not be displayed. Matches against the last segment of the URL path (e.g., "Patient Summary", "Vitals", "Medications").',
9
+ _elements: {
10
+ _type: Type.String,
11
+ },
12
+ },
13
+ };
14
+
15
+ export interface FlagsRiskCountExtensionConfig {
16
+ hideOnPages: Array<string>;
17
+ }
@@ -0,0 +1,84 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { Button, InlineLoading, Tag } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { ArrowRightIcon, CloseIcon, useConfig } from '@openmrs/esm-framework';
5
+ import { useCurrentPath, usePatientFlags } from '../hooks/usePatientFlags';
6
+ import FlagsList from '../flags-list.component';
7
+ import styles from './flags-risk-count.scss';
8
+ import { type FlagsRiskCountExtensionConfig } from './extension-config-schema';
9
+
10
+ interface FlagsRiskCountExtensionProps {
11
+ patientUuid: string;
12
+ }
13
+
14
+ const FlagsRiskCountExtension: React.FC<FlagsRiskCountExtensionProps> = ({ patientUuid }) => {
15
+ const path = useCurrentPath();
16
+ const { t } = useTranslation();
17
+ const { flags, isLoading, error } = usePatientFlags(patientUuid);
18
+ const filteredFlags = flags.filter((f) => !f.voided);
19
+ const config = useConfig<FlagsRiskCountExtensionConfig>();
20
+
21
+ const riskFlags = useMemo(() => {
22
+ return filteredFlags.filter((f) => f.flagDefinition?.priority?.name?.toLowerCase() === 'risk');
23
+ }, [filteredFlags]);
24
+
25
+ const [showFlagsList, setShowFlagsList] = useState(false);
26
+
27
+ const lastSegment = decodeURI(path).split('/').filter(Boolean).pop();
28
+ if (lastSegment && config.hideOnPages.includes(lastSegment)) {
29
+ return null;
30
+ }
31
+
32
+ if (isLoading) {
33
+ return <InlineLoading className={styles.loader} description={`${t('loading', 'Loading')} ...`} />;
34
+ }
35
+
36
+ if (error) {
37
+ return <div>{error.message}</div>;
38
+ }
39
+
40
+ if (riskFlags.length === 0) {
41
+ return null;
42
+ }
43
+
44
+ return (
45
+ <div className={styles.flagSummary}>
46
+ <Tag
47
+ className={styles.flagsCountTag}
48
+ type={showFlagsList ? 'outline' : 'high-contrast'}
49
+ onClick={() => setShowFlagsList(!showFlagsList)}
50
+ role="button"
51
+ tabIndex={0}
52
+ onKeyDown={(e) => {
53
+ if (e.key === 'Enter' || e.key === ' ') {
54
+ e.preventDefault();
55
+ setShowFlagsList(!showFlagsList);
56
+ }
57
+ }}
58
+ >
59
+ <span className={styles.flagIcon}>&#128681;</span>
60
+ {t('flagCount', '{{count}} risk flags', { count: riskFlags.length })}
61
+ {!showFlagsList && <ArrowRightIcon className={styles.arrowInTag} size={16} />}
62
+ </Tag>
63
+ {showFlagsList && (
64
+ <>
65
+ <ArrowRightIcon className={styles.arrow} size={16} />
66
+ <div className={styles.flagsListContainer}>
67
+ <FlagsList patientUuid={patientUuid} />
68
+ </div>
69
+ <Button
70
+ className={styles.actionButton}
71
+ hasIconOnly
72
+ iconDescription={t('closeFlagsList', 'Close flags list')}
73
+ kind="ghost"
74
+ size="sm"
75
+ renderIcon={CloseIcon}
76
+ onClick={() => setShowFlagsList(false)}
77
+ />
78
+ </>
79
+ )}
80
+ </div>
81
+ );
82
+ };
83
+
84
+ export default FlagsRiskCountExtension;
@@ -2,8 +2,10 @@
2
2
  @use '@carbon/layout';
3
3
 
4
4
  .flagSummary {
5
- padding: layout.$spacing-03;
6
- border-bottom: 1px solid colors.$gray-20;
5
+ padding: layout.$spacing-01;
6
+ display: flex;
7
+ flex-direction: row; /* needed if attaching extension outside highlights bar */
8
+ align-items: center;
7
9
  }
8
10
 
9
11
  .flagIcon {
@@ -15,9 +17,11 @@
15
17
  fill: currentColor;
16
18
  }
17
19
 
18
- .flagsHighlightTag {
20
+ .flagsCountTag {
21
+ margin: layout.$spacing-02 0;
19
22
  padding: 0 layout.$spacing-03;
20
23
  border: none;
24
+ cursor: pointer;
21
25
  span {
22
26
  display: flex;
23
27
  justify-content: space-between;
@@ -25,9 +29,14 @@
25
29
  }
26
30
  }
27
31
 
28
- .highlightBarContainer {
32
+ .arrowInTag {
33
+ margin-left: layout.$spacing-03;
34
+ fill: currentColor;
35
+ }
36
+
37
+ .flagsListContainer {
29
38
  padding-left: layout.$spacing-03;
30
- border-bottom: 1px solid colors.$gray-20;
39
+ flex-grow: 1;
31
40
  }
32
41
 
33
42
  .loader {
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { getDefaultsFromConfigSchema, launchWorkspace2, useConfig } from '@openmrs/esm-framework';
5
+ import { mockPatient } from 'tools';
6
+ import { mockPatientFlags } from '__mocks__';
7
+ import { configSchema } from '../../config-schema';
8
+ import { riskCountExtensionConfigSchema } from './extension-config-schema';
9
+ import { useCurrentPath, usePatientFlags } from '../hooks/usePatientFlags';
10
+ import FlagsRiskCountExtension from './flags-risk-count.extension';
11
+ import { type ConfigObject } from '../../config-schema';
12
+ import { type FlagsRiskCountExtensionConfig } from './extension-config-schema';
13
+
14
+ const mockUsePatientFlags = jest.mocked(usePatientFlags);
15
+ const mockUseCurrentPath = jest.mocked(useCurrentPath);
16
+ const mockLaunchWorkspace = jest.mocked(launchWorkspace2);
17
+ const mockUseConfig = jest.mocked(useConfig);
18
+
19
+ jest.mock('../hooks/usePatientFlags', () => {
20
+ const originalModule = jest.requireActual('../hooks/usePatientFlags');
21
+
22
+ return {
23
+ ...originalModule,
24
+ usePatientFlags: jest.fn(),
25
+ useCurrentPath: jest.fn(),
26
+ };
27
+ });
28
+
29
+ beforeEach(() => {
30
+ mockUseCurrentPath.mockReturnValue('/patient/123/chart');
31
+ mockUseConfig.mockReturnValue({
32
+ ...getDefaultsFromConfigSchema<ConfigObject>(configSchema),
33
+ ...getDefaultsFromConfigSchema<FlagsRiskCountExtensionConfig>(riskCountExtensionConfigSchema),
34
+ });
35
+ });
36
+
37
+ describe('flags risk count', () => {
38
+ it('displays a single clickable tag', async () => {
39
+ const user = userEvent.setup();
40
+
41
+ mockUsePatientFlags.mockReturnValue({
42
+ flags: mockPatientFlags,
43
+ error: null,
44
+ isLoading: false,
45
+ isValidating: false,
46
+ mutate: jest.fn(),
47
+ } as unknown as ReturnType<typeof usePatientFlags>);
48
+
49
+ render(<FlagsRiskCountExtension patientUuid={mockPatient.id} />);
50
+
51
+ const riskFlag = screen.getByRole('button', { name: /risk flag/i });
52
+ expect(riskFlag).toBeInTheDocument();
53
+ expect(screen.getByText('🚩')).toBeInTheDocument();
54
+ expect(screen.getByText(/risk flag/)).toBeInTheDocument();
55
+
56
+ await user.click(riskFlag);
57
+
58
+ const flags = screen.getAllByRole('listitem');
59
+ expect(flags).toHaveLength(3);
60
+ expect(screen.getByText(/patient needs to be followed up/i)).toBeInTheDocument();
61
+ expect(screen.getByText(/diagnosis for the patient is unknown/i)).toBeInTheDocument();
62
+ expect(screen.getByText(/patient has a future appointment scheduled/i)).toBeInTheDocument();
63
+
64
+ const editButton = screen.getByRole('button', { name: /edit flags/i });
65
+ expect(editButton).toBeInTheDocument();
66
+
67
+ await user.click(editButton);
68
+
69
+ expect(mockLaunchWorkspace).toHaveBeenCalledWith('patient-flags-workspace');
70
+
71
+ const closeButton = screen.getByRole('button', { name: /close flags list/i });
72
+
73
+ await user.click(closeButton);
74
+
75
+ expect(screen.queryAllByRole('listitem')).toHaveLength(0);
76
+ });
77
+
78
+ it('respects the hideOnPages config', () => {
79
+ mockUseCurrentPath.mockReturnValue('/patient/123/FooBarBaz');
80
+ mockUseConfig.mockReturnValue({
81
+ ...getDefaultsFromConfigSchema<ConfigObject>(configSchema),
82
+ ...getDefaultsFromConfigSchema<FlagsRiskCountExtensionConfig>(riskCountExtensionConfigSchema),
83
+ hideOnPages: ['FooBarBaz'],
84
+ });
85
+
86
+ mockUsePatientFlags.mockReturnValue({
87
+ flags: mockPatientFlags,
88
+ error: null,
89
+ isLoading: false,
90
+ isValidating: false,
91
+ mutate: jest.fn(),
92
+ } as unknown as ReturnType<typeof usePatientFlags>);
93
+
94
+ render(<FlagsRiskCountExtension patientUuid={mockPatient.id} />);
95
+
96
+ // No flags risk count should be shown on the FooBarBaz route
97
+ expect(screen.queryByText(/risk flags/i)).not.toBeInTheDocument();
98
+ });
99
+ });