@bathiran212/esm-patient-notes-app 2.0.0
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/README.md +4 -0
- package/dist/1076.js +1 -0
- package/dist/1076.js.map +1 -0
- package/dist/1339.js +1 -0
- package/dist/1339.js.map +1 -0
- package/dist/1480.js +1 -0
- package/dist/1480.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1646.js.map +1 -0
- package/dist/1789.js +1 -0
- package/dist/1789.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1869.js.map +1 -0
- package/dist/1871.js +1 -0
- package/dist/1871.js.map +1 -0
- package/dist/1877.js +1 -0
- package/dist/1877.js.map +1 -0
- package/dist/2153.js +1 -0
- package/dist/2153.js.map +1 -0
- package/dist/2317.js +1 -0
- package/dist/2317.js.map +1 -0
- package/dist/2416.js +1 -0
- package/dist/2416.js.map +1 -0
- package/dist/2544.js +27 -0
- package/dist/2544.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/282.js.map +1 -0
- package/dist/2824.js +1 -0
- package/dist/2824.js.map +1 -0
- package/dist/2842.js +1 -0
- package/dist/2842.js.map +1 -0
- package/dist/2881.js +1 -0
- package/dist/2881.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3720.js +1 -0
- package/dist/3720.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/3963.js.map +1 -0
- package/dist/3989.js +1 -0
- package/dist/3989.js.map +1 -0
- package/dist/4106.js +1 -0
- package/dist/4106.js.map +1 -0
- package/dist/4111.js +1 -0
- package/dist/4111.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/434.js.map +1 -0
- package/dist/4348.js +1 -0
- package/dist/4348.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4383.js.map +1 -0
- package/dist/4658.js +1 -0
- package/dist/4658.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/4928.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5132.js +1 -0
- package/dist/5132.js.map +1 -0
- package/dist/5145.js +1 -0
- package/dist/5145.js.map +1 -0
- package/dist/5503.js +1 -0
- package/dist/5503.js.map +1 -0
- package/dist/556.js +1 -0
- package/dist/556.js.map +1 -0
- package/dist/5644.js +1 -0
- package/dist/5644.js.map +1 -0
- package/dist/5697.js +1 -0
- package/dist/5697.js.map +1 -0
- package/dist/5861.js +1 -0
- package/dist/5861.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5940.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6047.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6371.js.map +1 -0
- package/dist/6377.js +1 -0
- package/dist/6377.js.map +1 -0
- package/dist/6444.js +1 -0
- package/dist/6444.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6724.js +1 -0
- package/dist/6724.js.map +1 -0
- package/dist/6904.js +1 -0
- package/dist/6904.js.map +1 -0
- package/dist/7045.js +1 -0
- package/dist/7045.js.map +1 -0
- package/dist/7103.js +1 -0
- package/dist/7103.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7175.js.map +1 -0
- package/dist/7182.js +1 -0
- package/dist/7182.js.map +1 -0
- package/dist/7205.js +11 -0
- package/dist/7205.js.map +1 -0
- package/dist/7646.js +17 -0
- package/dist/7646.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7742.js.map +1 -0
- package/dist/7912.js +1 -0
- package/dist/7912.js.map +1 -0
- package/dist/8358.js +1 -0
- package/dist/8358.js.map +1 -0
- package/dist/8359.js +1 -0
- package/dist/8359.js.map +1 -0
- package/dist/8369.js +1 -0
- package/dist/8369.js.map +1 -0
- package/dist/8695.js +1 -0
- package/dist/8695.js.map +1 -0
- package/dist/8722.js +1 -0
- package/dist/8722.js.map +1 -0
- package/dist/903.js +1 -0
- package/dist/903.js.map +1 -0
- package/dist/9061.js +1 -0
- package/dist/9061.js.map +1 -0
- package/dist/9072.js +1 -0
- package/dist/9072.js.map +1 -0
- package/dist/9105.js +1 -0
- package/dist/9105.js.map +1 -0
- package/dist/9712.js +1 -0
- package/dist/9712.js.map +1 -0
- package/dist/9771.js +1 -0
- package/dist/9771.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/main.js +16 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-patient-notes-app.js +6 -0
- package/dist/openmrs-esm-patient-notes-app.js.buildmanifest.json +1760 -0
- package/dist/openmrs-esm-patient-notes-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +58 -0
- package/rspack.config.js +1 -0
- package/src/config-schema.ts +28 -0
- package/src/dashboard.meta.ts +7 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +45 -0
- package/src/notes/notes-overview.extension.tsx +74 -0
- package/src/notes/notes-overview.scss +40 -0
- package/src/notes/notes-overview.test.tsx +101 -0
- package/src/notes/paginated-notes.component.tsx +182 -0
- package/src/notes/visit-note-config-schema.ts +38 -0
- package/src/notes/visit-notes-form.scss +219 -0
- package/src/notes/visit-notes-form.test.tsx +523 -0
- package/src/notes/visit-notes-form.workspace.tsx +853 -0
- package/src/notes/visit-notes.resource.ts +113 -0
- package/src/routes.json +48 -0
- package/src/sticky-notes/delete-sticky-note-button.component.tsx +39 -0
- package/src/sticky-notes/delete-sticky-note-button.scss +13 -0
- package/src/sticky-notes/delete-sticky-note.modal.test.tsx +72 -0
- package/src/sticky-notes/delete-sticky-note.modal.tsx +62 -0
- package/src/sticky-notes/edit-sticky-note-button.component.tsx +20 -0
- package/src/sticky-notes/sticky-note-header-button.component.tsx +100 -0
- package/src/sticky-notes/sticky-note-header-button.scss +38 -0
- package/src/sticky-notes/sticky-note-header-button.test.tsx +182 -0
- package/src/sticky-notes/sticky-note-panel.component.tsx +88 -0
- package/src/sticky-notes/sticky-note-panel.scss +54 -0
- package/src/sticky-notes/sticky-note-panel.test.tsx +66 -0
- package/src/sticky-notes/sticky-note.modal.scss +3 -0
- package/src/sticky-notes/sticky-note.modal.test.tsx +115 -0
- package/src/sticky-notes/sticky-note.modal.tsx +93 -0
- package/src/sticky-notes/sticky-note.resource.test.ts +24 -0
- package/src/sticky-notes/sticky-note.resource.ts +82 -0
- package/src/sticky-notes/utils.test.ts +36 -0
- package/src/sticky-notes/utils.ts +9 -0
- package/src/types/index.ts +203 -0
- package/src/visit-note-action-button.extension.tsx +28 -0
- package/src/visit-note-action-button.test.tsx +42 -0
- package/translations/am.json +55 -0
- package/translations/ar.json +55 -0
- package/translations/ar_SY.json +55 -0
- package/translations/bn.json +55 -0
- package/translations/cs.json +55 -0
- package/translations/de.json +55 -0
- package/translations/en.json +55 -0
- package/translations/en_US.json +55 -0
- package/translations/es.json +55 -0
- package/translations/es_MX.json +55 -0
- package/translations/fr.json +55 -0
- package/translations/he.json +55 -0
- package/translations/hi.json +55 -0
- package/translations/hi_IN.json +55 -0
- package/translations/id.json +55 -0
- package/translations/it.json +55 -0
- package/translations/ka.json +55 -0
- package/translations/km.json +55 -0
- package/translations/ku.json +55 -0
- package/translations/ky.json +55 -0
- package/translations/lg.json +55 -0
- package/translations/ne.json +55 -0
- package/translations/pl.json +55 -0
- package/translations/pt.json +55 -0
- package/translations/pt_BR.json +55 -0
- package/translations/qu.json +55 -0
- package/translations/ro_RO.json +55 -0
- package/translations/ru_RU.json +55 -0
- package/translations/si.json +55 -0
- package/translations/sq.json +55 -0
- package/translations/sw.json +55 -0
- package/translations/sw_KE.json +55 -0
- package/translations/tr.json +55 -0
- package/translations/tr_TR.json +55 -0
- package/translations/uk.json +55 -0
- package/translations/uz.json +55 -0
- package/translations/uz@Latn.json +55 -0
- package/translations/uz_UZ.json +55 -0
- package/translations/vi.json +55 -0
- package/translations/zh.json +55 -0
- package/translations/zh_CN.json +55 -0
- package/translations/zh_TW.json +55 -0
- package/tsconfig.json +4 -0
- package/vitest.config.ts +4 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import useSWR from 'swr';
|
|
2
|
+
import useSWRInfinite from 'swr/infinite';
|
|
3
|
+
import { openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import { type ConfigObject } from '../config-schema';
|
|
5
|
+
import type {
|
|
6
|
+
Concept,
|
|
7
|
+
DiagnosisPayload,
|
|
8
|
+
EncountersFetchResponse,
|
|
9
|
+
PatientNote,
|
|
10
|
+
RESTPatientNote,
|
|
11
|
+
VisitNotePayload,
|
|
12
|
+
} from '../types';
|
|
13
|
+
|
|
14
|
+
interface UseVisitNotes {
|
|
15
|
+
visitNotes: Array<PatientNote> | null;
|
|
16
|
+
error: Error;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
isValidating?: boolean;
|
|
19
|
+
mutateVisitNotes: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useVisitNotes(patientUuid: string): UseVisitNotes {
|
|
23
|
+
const {
|
|
24
|
+
visitNoteConfig: { encounterNoteTextConceptUuid, visitDiagnosesConceptUuid },
|
|
25
|
+
} = useConfig<ConfigObject>();
|
|
26
|
+
|
|
27
|
+
const customRepresentation =
|
|
28
|
+
'custom:(uuid,display,encounterDatetime,patient,obs,' +
|
|
29
|
+
'encounterProviders:(uuid,display,' +
|
|
30
|
+
'encounterRole:(uuid,display),' +
|
|
31
|
+
'provider:(uuid,person:(uuid,display))),' +
|
|
32
|
+
'diagnoses';
|
|
33
|
+
const encountersApiUrl = `${restBaseUrl}/encounter?patient=${patientUuid}&obs=${visitDiagnosesConceptUuid}&v=${customRepresentation}`;
|
|
34
|
+
|
|
35
|
+
const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: EncountersFetchResponse }, Error>(
|
|
36
|
+
encountersApiUrl,
|
|
37
|
+
openmrsFetch,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const mapNoteProperties = (note: RESTPatientNote, index: number): PatientNote => ({
|
|
41
|
+
id: `${index}`,
|
|
42
|
+
diagnoses: note.diagnoses
|
|
43
|
+
.filter((diagnosis) => !diagnosis.voided)
|
|
44
|
+
.map((diagnosisData) => diagnosisData.display)
|
|
45
|
+
.filter((val) => val)
|
|
46
|
+
.join(', '),
|
|
47
|
+
encounterDate: note.encounterDatetime,
|
|
48
|
+
encounterNote: note.obs.find((observation) => observation.concept.uuid === encounterNoteTextConceptUuid)?.value,
|
|
49
|
+
encounterNoteRecordedAt: note.obs.find((observation) => observation.concept.uuid === encounterNoteTextConceptUuid)
|
|
50
|
+
?.obsDatetime,
|
|
51
|
+
encounterProvider: note?.encounterProviders[0]?.provider?.person?.display,
|
|
52
|
+
encounterProviderRole: note?.encounterProviders[0]?.encounterRole?.display,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const formattedVisitNotes = data?.data?.results
|
|
56
|
+
?.map(mapNoteProperties)
|
|
57
|
+
?.sort((noteA, noteB) => new Date(noteB.encounterDate).getTime() - new Date(noteA.encounterDate).getTime());
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
visitNotes: data ? formattedVisitNotes : null,
|
|
61
|
+
error,
|
|
62
|
+
isLoading,
|
|
63
|
+
isValidating,
|
|
64
|
+
mutateVisitNotes: mutate,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function fetchDiagnosisConceptsByName(searchTerm: string, diagnosisConceptClass: string) {
|
|
69
|
+
const customRepresentation = 'custom:(uuid,display)';
|
|
70
|
+
const url = `${restBaseUrl}/concept?name=${searchTerm}&searchType=fuzzy&class=${diagnosisConceptClass}&v=${customRepresentation}`;
|
|
71
|
+
|
|
72
|
+
return openmrsFetch<Array<Concept>>(url).then(({ data }) => Promise.resolve(data['results']));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function saveVisitNote(abortController: AbortController, payload: VisitNotePayload) {
|
|
76
|
+
return openmrsFetch(`${restBaseUrl}/encounter`, {
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
},
|
|
80
|
+
method: 'POST',
|
|
81
|
+
body: payload,
|
|
82
|
+
signal: abortController.signal,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function updateVisitNote(abortController: AbortController, encounterUuid: string, payload: VisitNotePayload) {
|
|
87
|
+
return openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, {
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
},
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: payload,
|
|
93
|
+
signal: abortController.signal,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function savePatientDiagnosis(abortController: AbortController, payload: DiagnosisPayload) {
|
|
98
|
+
return openmrsFetch(`${restBaseUrl}/patientdiagnoses`, {
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
},
|
|
102
|
+
method: 'POST',
|
|
103
|
+
body: payload,
|
|
104
|
+
signal: abortController.signal,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function deletePatientDiagnosis(abortController: AbortController, diagnosisUuid: string) {
|
|
109
|
+
return openmrsFetch(`${restBaseUrl}/patientdiagnoses/${diagnosisUuid}`, {
|
|
110
|
+
method: 'DELETE',
|
|
111
|
+
signal: abortController.signal,
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/routes.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {
|
|
4
|
+
"fhir2": ">=1.2",
|
|
5
|
+
"webservices.rest": ">=2.2.0"
|
|
6
|
+
},
|
|
7
|
+
"extensions": [
|
|
8
|
+
{
|
|
9
|
+
"name": "notes-overview-widget",
|
|
10
|
+
"component": "notesOverview",
|
|
11
|
+
"meta": {
|
|
12
|
+
"fullWidth": false
|
|
13
|
+
},
|
|
14
|
+
"order": 5
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "sticky-notes-button",
|
|
18
|
+
"component": "stickyNotesButton",
|
|
19
|
+
"slot": "patient-info-slot",
|
|
20
|
+
"order": 1
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"modals": [
|
|
24
|
+
{
|
|
25
|
+
"name": "delete-sticky-note-modal",
|
|
26
|
+
"component": "deleteStickyNoteModal"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "sticky-note-modal",
|
|
30
|
+
"component": "stickyNoteModal"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"workspaces2": [
|
|
34
|
+
{
|
|
35
|
+
"name": "visit-notes-form-workspace",
|
|
36
|
+
"component": "visitNotesFormWorkspace",
|
|
37
|
+
"window": "visit-note"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"workspaceWindows2": [
|
|
41
|
+
{
|
|
42
|
+
"name": "visit-note",
|
|
43
|
+
"icon": "visitNotesActionButton",
|
|
44
|
+
"group": "patient-chart",
|
|
45
|
+
"order": 2
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IconButton } from '@carbon/react';
|
|
3
|
+
import { showModal, TrashCanIcon } from '@openmrs/esm-framework';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import styles from './delete-sticky-note-button.scss';
|
|
6
|
+
|
|
7
|
+
interface DeleteStickyNoteProps {
|
|
8
|
+
noteUuid: string;
|
|
9
|
+
mutate: () => void;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DeleteStickyNote: React.FC<DeleteStickyNoteProps> = ({ noteUuid, mutate, onClose }) => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
|
|
16
|
+
const handleDelete = () => {
|
|
17
|
+
const dispose = showModal('delete-sticky-note-modal', {
|
|
18
|
+
close: () => {
|
|
19
|
+
dispose();
|
|
20
|
+
},
|
|
21
|
+
noteUuid,
|
|
22
|
+
mutate,
|
|
23
|
+
onClose,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
return (
|
|
27
|
+
<IconButton
|
|
28
|
+
label={t('deleteStickyNote', 'Delete sticky note')}
|
|
29
|
+
onClick={handleDelete}
|
|
30
|
+
kind="ghost"
|
|
31
|
+
size="sm"
|
|
32
|
+
align="top-end"
|
|
33
|
+
>
|
|
34
|
+
<TrashCanIcon className={styles.deleteIcon} />
|
|
35
|
+
</IconButton>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default DeleteStickyNote;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { vi, describe, it, expect } from 'vitest';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
6
|
+
import { deleteStickyNote } from './sticky-note.resource';
|
|
7
|
+
import DeleteStickyNoteModal from './delete-sticky-note.modal';
|
|
8
|
+
|
|
9
|
+
vi.mock('./sticky-note.resource', () => ({
|
|
10
|
+
deleteStickyNote: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockDeleteStickyNote = vi.mocked(deleteStickyNote);
|
|
14
|
+
const mockShowSnackbar = vi.mocked(showSnackbar);
|
|
15
|
+
|
|
16
|
+
const defaultProps = {
|
|
17
|
+
close: vi.fn(),
|
|
18
|
+
noteUuid: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
|
|
19
|
+
mutate: vi.fn(),
|
|
20
|
+
onClose: vi.fn(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('DeleteStickyNoteModal', () => {
|
|
24
|
+
it('renders modal with correct elements', () => {
|
|
25
|
+
render(<DeleteStickyNoteModal {...defaultProps} />);
|
|
26
|
+
expect(screen.getByText(/confirm delete sticky note/i)).toBeInTheDocument();
|
|
27
|
+
expect(screen.getByText(/are you sure you want to delete this sticky note/i)).toBeInTheDocument();
|
|
28
|
+
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
29
|
+
expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('clicking Cancel button closes modal', async () => {
|
|
33
|
+
const user = userEvent.setup();
|
|
34
|
+
render(<DeleteStickyNoteModal {...defaultProps} />);
|
|
35
|
+
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
36
|
+
await user.click(cancelButton);
|
|
37
|
+
expect(defaultProps.close).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('clicking Delete button deletes sticky note and shows success', async () => {
|
|
41
|
+
const user = userEvent.setup();
|
|
42
|
+
mockDeleteStickyNote.mockResolvedValue(undefined);
|
|
43
|
+
render(<DeleteStickyNoteModal {...defaultProps} />);
|
|
44
|
+
const deleteButton = screen.getByRole('button', { name: /delete/i });
|
|
45
|
+
await user.click(deleteButton);
|
|
46
|
+
|
|
47
|
+
expect(mockDeleteStickyNote).toHaveBeenCalledWith(defaultProps.noteUuid);
|
|
48
|
+
expect(mockDeleteStickyNote).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith({
|
|
50
|
+
subtitle: 'The sticky note was deleted successfully',
|
|
51
|
+
title: 'Sticky note deleted',
|
|
52
|
+
kind: 'success',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('shows error when deletion fails', async () => {
|
|
57
|
+
const user = userEvent.setup();
|
|
58
|
+
const error = { message: 'Server error' };
|
|
59
|
+
mockDeleteStickyNote.mockRejectedValue(error);
|
|
60
|
+
|
|
61
|
+
render(<DeleteStickyNoteModal {...defaultProps} />);
|
|
62
|
+
const deleteButton = screen.getByRole('button', { name: /delete/i });
|
|
63
|
+
await user.click(deleteButton);
|
|
64
|
+
|
|
65
|
+
expect(mockDeleteStickyNote).toHaveBeenCalledWith(defaultProps.noteUuid);
|
|
66
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith({
|
|
67
|
+
title: 'Error deleting sticky note',
|
|
68
|
+
subtitle: 'An error occurred while deleting the sticky note',
|
|
69
|
+
kind: 'error',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { Button, InlineLoading, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { getCoreTranslation, showSnackbar } from '@openmrs/esm-framework';
|
|
5
|
+
import { deleteStickyNote } from './sticky-note.resource';
|
|
6
|
+
|
|
7
|
+
interface DeleteStickyNoteModalProps {
|
|
8
|
+
close: () => void;
|
|
9
|
+
noteUuid: string;
|
|
10
|
+
mutate: () => void;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DeleteStickyNoteModal = ({ close, noteUuid, mutate, onClose }: DeleteStickyNoteModalProps) => {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
17
|
+
|
|
18
|
+
const confirmDelete = useCallback(async () => {
|
|
19
|
+
setIsDeleting(true);
|
|
20
|
+
try {
|
|
21
|
+
await deleteStickyNote(noteUuid);
|
|
22
|
+
showSnackbar({
|
|
23
|
+
kind: 'success',
|
|
24
|
+
title: t('stickyNoteDeleted', 'Sticky note deleted'),
|
|
25
|
+
subtitle: t('stickyNoteDeletedSuccessfully', 'The sticky note was deleted successfully'),
|
|
26
|
+
});
|
|
27
|
+
mutate();
|
|
28
|
+
close();
|
|
29
|
+
onClose();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
showSnackbar({
|
|
32
|
+
kind: 'error',
|
|
33
|
+
title: t('errorDeletingStickyNote', 'Error deleting sticky note'),
|
|
34
|
+
subtitle: t('errorDeletingStickyNoteMessage', 'An error occurred while deleting the sticky note'),
|
|
35
|
+
});
|
|
36
|
+
} finally {
|
|
37
|
+
setIsDeleting(false);
|
|
38
|
+
}
|
|
39
|
+
}, [noteUuid, mutate, close, onClose, t]);
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<ModalHeader closeModal={close}>{t('confirmDeleteStickyNote', 'Confirm delete sticky note')}</ModalHeader>
|
|
43
|
+
<ModalBody>
|
|
44
|
+
<p>{t('confirmDeleteStickyNoteMessage', 'Are you sure you want to delete this sticky note?')}</p>
|
|
45
|
+
</ModalBody>
|
|
46
|
+
<ModalFooter>
|
|
47
|
+
<Button kind="secondary" onClick={close} disabled={isDeleting}>
|
|
48
|
+
{getCoreTranslation('cancel')}
|
|
49
|
+
</Button>
|
|
50
|
+
<Button kind="danger" onClick={confirmDelete} disabled={isDeleting}>
|
|
51
|
+
{isDeleting ? (
|
|
52
|
+
<InlineLoading description={t('deleting', 'Deleting') + '...'} />
|
|
53
|
+
) : (
|
|
54
|
+
getCoreTranslation('delete')
|
|
55
|
+
)}
|
|
56
|
+
</Button>
|
|
57
|
+
</ModalFooter>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default DeleteStickyNoteModal;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IconButton } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { EditIcon } from '@openmrs/esm-framework';
|
|
5
|
+
|
|
6
|
+
interface EditStickyNoteProps {
|
|
7
|
+
onEdit: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const EditStickyNote: React.FC<EditStickyNoteProps> = ({ onEdit }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<IconButton label={t('editStickyNote', 'Edit sticky note')} onClick={onEdit} kind="ghost" size="sm" align="top-end">
|
|
15
|
+
<EditIcon />
|
|
16
|
+
</IconButton>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default EditStickyNote;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { Button } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { DocumentIcon, showModal, useConfig, useOnClickOutside } from '@openmrs/esm-framework';
|
|
5
|
+
import { type ConfigObject } from '../config-schema';
|
|
6
|
+
import { useStickyNote } from './sticky-note.resource';
|
|
7
|
+
import StickyNotePanel from './sticky-note-panel.component';
|
|
8
|
+
import styles from './sticky-note-header-button.scss';
|
|
9
|
+
|
|
10
|
+
interface StickyNoteHeaderButtonProps {
|
|
11
|
+
patientUuid: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const StickyNoteHeaderButton: React.FC<StickyNoteHeaderButtonProps> = ({ patientUuid }) => {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const [showPanel, setShowPanel] = useState(false);
|
|
17
|
+
const { note, isLoading, error, mutate } = useStickyNote(patientUuid);
|
|
18
|
+
|
|
19
|
+
const handleClose = useCallback(() => {
|
|
20
|
+
setShowPanel(false);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const handleOutsideClick = useCallback((event: MouseEvent) => {
|
|
24
|
+
// Ignore clicks inside any open Carbon modal so opening the edit/delete modal doesn't dismiss the panel.
|
|
25
|
+
if ((event.target as HTMLElement | null)?.closest('[role="dialog"]')) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setShowPanel(false);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const containerRef = useOnClickOutside<HTMLDivElement>(handleOutsideClick, showPanel);
|
|
32
|
+
|
|
33
|
+
const handleClick = useCallback(() => {
|
|
34
|
+
// Any state other than "definitely no note" shows the panel, which already renders skeleton / error / note.
|
|
35
|
+
if (isLoading || error || note) {
|
|
36
|
+
setShowPanel((prev) => !prev);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const dispose = showModal('sticky-note-modal', {
|
|
40
|
+
close: () => dispose(),
|
|
41
|
+
mutate,
|
|
42
|
+
patientUuid,
|
|
43
|
+
});
|
|
44
|
+
}, [error, isLoading, mutate, note, patientUuid]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!showPanel) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
51
|
+
if (event.key === 'Escape') {
|
|
52
|
+
setShowPanel(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
56
|
+
return () => {
|
|
57
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
58
|
+
};
|
|
59
|
+
}, [showPanel]);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={styles.content} ref={containerRef}>
|
|
63
|
+
<Button
|
|
64
|
+
kind="ghost"
|
|
65
|
+
size="sm"
|
|
66
|
+
renderIcon={(props) => (
|
|
67
|
+
<div>
|
|
68
|
+
<DocumentIcon {...props} />
|
|
69
|
+
{note && <span className={styles.notificationBadge}>1</span>}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
onClick={handleClick}
|
|
73
|
+
>
|
|
74
|
+
{t('stickyNote', 'Sticky note')}
|
|
75
|
+
</Button>
|
|
76
|
+
{showPanel && (
|
|
77
|
+
<StickyNotePanel
|
|
78
|
+
error={error}
|
|
79
|
+
isLoading={isLoading}
|
|
80
|
+
mutate={mutate}
|
|
81
|
+
note={note}
|
|
82
|
+
onClose={handleClose}
|
|
83
|
+
patientUuid={patientUuid}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Gate the button on config so distros that don't configure a concept don't see a feature that
|
|
91
|
+
// would silently fail on save. When unconfigured, nothing renders — no button, no SWR request.
|
|
92
|
+
const StickyNoteHeaderButtonGate: React.FC<StickyNoteHeaderButtonProps> = (props) => {
|
|
93
|
+
const { stickyNoteConceptUuid } = useConfig<ConfigObject>();
|
|
94
|
+
if (!stickyNoteConceptUuid) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return <StickyNoteHeaderButton {...props} />;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default StickyNoteHeaderButtonGate;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/colors';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
+
|
|
6
|
+
// When the gate returns null (no concept configured) the framework still leaves an empty
|
|
7
|
+
// <div data-extension-id="sticky-notes-button"> wrapper in the patient-info-slot. The slot's
|
|
8
|
+
// own `> div:not(:last-child) { border-bottom }` rule paints a border on that empty wrapper,
|
|
9
|
+
// stacking a second 1px line just below the banner's own border-bottom. Collapse the empty
|
|
10
|
+
// wrapper so it takes no space and paints nothing.
|
|
11
|
+
:global([data-extension-id='sticky-notes-button']):empty {
|
|
12
|
+
display: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.content {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
width: fit-content;
|
|
19
|
+
margin-inline-start: auto;
|
|
20
|
+
position: relative;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.notificationBadge {
|
|
24
|
+
position: absolute;
|
|
25
|
+
top: calc(layout.$spacing-02 * -1);
|
|
26
|
+
right: layout.$spacing-03;
|
|
27
|
+
background-color: $danger;
|
|
28
|
+
color: white;
|
|
29
|
+
border-radius: 50%;
|
|
30
|
+
width: layout.$spacing-05;
|
|
31
|
+
height: layout.$spacing-05;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
font-size: 10px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
border: 1px solid white;
|
|
38
|
+
}
|