@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.
- package/.turbo/turbo-build.log +10 -10
- package/dist/4300.js +1 -1
- package/dist/4480.js +1 -0
- package/dist/4480.js.map +1 -0
- package/dist/597.js +1 -0
- package/dist/597.js.map +1 -0
- package/dist/7231.js +1 -0
- package/dist/7231.js.map +1 -0
- package/dist/8519.js +1 -1
- package/dist/8519.js.map +1 -1
- package/dist/8572.js +1 -0
- package/dist/8572.js.map +1 -0
- package/dist/9388.js +1 -0
- package/dist/9388.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-flags-app.js +1 -1
- package/dist/openmrs-esm-patient-flags-app.js.buildmanifest.json +129 -105
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/config-schema.ts +13 -2
- package/src/flags/flags-list-extension/flags-list.extension.tsx +12 -0
- package/src/flags/flags-list.component.tsx +68 -0
- package/src/flags/flags-list.scss +43 -147
- package/src/flags/flags-list.test.tsx +50 -68
- package/src/flags/flags-risk-count-extension/extension-config-schema.ts +17 -0
- package/src/flags/flags-risk-count-extension/flags-risk-count.extension.tsx +84 -0
- package/src/flags/{flags-highlight-bar.scss → flags-risk-count-extension/flags-risk-count.scss} +14 -5
- package/src/flags/flags-risk-count-extension/flags-risk-count.test.tsx +99 -0
- package/src/flags/flags-workspace/flags-workspace.scss +167 -0
- package/src/flags/flags-workspace/flags-workspace.test.tsx +97 -0
- package/src/flags/flags-workspace/flags.workspace.tsx +255 -0
- package/src/flags/hooks/usePatientFlags.ts +19 -11
- package/src/index.ts +12 -7
- package/src/routes.json +18 -9
- package/translations/en.json +7 -15
- package/dist/1837.js +0 -1
- package/dist/1837.js.map +0 -1
- package/dist/5628.js +0 -1
- package/dist/5628.js.map +0 -1
- package/dist/6138.js +0 -1
- package/dist/6138.js.map +0 -1
- package/dist/6173.js +0 -1
- package/dist/6173.js.map +0 -1
- package/src/flags/flags-highlight-bar.component.tsx +0 -81
- package/src/flags/flags-highlight-bar.test.tsx +0 -85
- package/src/flags/flags.component.tsx +0 -82
- package/src/flags/flags.scss +0 -71
- package/src/flags/flags.test.tsx +0 -49
- 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}>🚩</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
|
-
.
|
|
6
|
-
margin: layout.$spacing-05;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.flagTile {
|
|
6
|
+
.container {
|
|
10
7
|
display: flex;
|
|
11
|
-
flex-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
62
|
-
|
|
63
|
-
flex-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
.
|
|
83
|
-
|
|
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
|
-
.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
.
|
|
152
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
59
|
+
p {
|
|
60
|
+
@include type.type-style('body-01');
|
|
61
|
+
}
|
|
165
62
|
|
|
166
|
-
.
|
|
167
|
-
|
|
168
|
-
}
|
|
63
|
+
.title {
|
|
64
|
+
@include type.type-style('heading-compact-01');
|
|
65
|
+
}
|
|
169
66
|
|
|
170
|
-
.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 './
|
|
9
|
+
import FlagsList from './flags-list.component';
|
|
8
10
|
|
|
9
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
flags
|
|
62
|
-
isLoading: false,
|
|
63
|
-
error: null,
|
|
50
|
+
await user.click(editButton);
|
|
51
|
+
|
|
52
|
+
expect(mockLaunchWorkspace).toHaveBeenCalledWith('patient-flags-workspace');
|
|
64
53
|
});
|
|
65
54
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
expect(sortDropdown).toBeInTheDocument();
|
|
70
|
+
const flags = screen.getAllByRole('listitem');
|
|
71
|
+
expect(flags).toHaveLength(3);
|
|
87
72
|
|
|
88
|
-
|
|
89
|
-
|
|
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}>🚩</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;
|
package/src/flags/{flags-highlight-bar.scss → flags-risk-count-extension/flags-risk-count.scss}
RENAMED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
@use '@carbon/layout';
|
|
3
3
|
|
|
4
4
|
.flagSummary {
|
|
5
|
-
padding: layout.$spacing-
|
|
6
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
32
|
+
.arrowInTag {
|
|
33
|
+
margin-left: layout.$spacing-03;
|
|
34
|
+
fill: currentColor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.flagsListContainer {
|
|
29
38
|
padding-left: layout.$spacing-03;
|
|
30
|
-
|
|
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
|
+
});
|