@openmrs/esm-metadataexport-app 4.4.1-pre.799 → 4.4.1-pre.803
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 +1 -1
- package/dist/{5739.css.map → 1118.css.map} +1 -1
- package/dist/1118.js +1 -0
- package/dist/1118.js.map +1 -0
- package/dist/3566.css +1 -0
- package/dist/3566.css.map +1 -0
- package/dist/3566.js +1 -0
- package/dist/3566.js.map +1 -0
- package/dist/5065.css +1 -0
- package/dist/5065.css.map +1 -0
- package/dist/5065.js +1 -0
- package/dist/5065.js.map +1 -0
- package/dist/5117.js +1 -1
- package/dist/5117.js.map +1 -1
- package/dist/6813.js +1 -1
- package/dist/6813.js.map +1 -1
- package/dist/9503.css +1 -0
- package/dist/9503.css.map +1 -0
- package/dist/9503.js +1 -0
- package/dist/9503.js.map +1 -0
- package/dist/openmrs-esm-metadataexport-app.js +2 -2
- package/dist/openmrs-esm-metadataexport-app.js.buildmanifest.json +94 -94
- package/dist/openmrs-esm-metadataexport-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/components/metadata-export-header/metadata-export-header.component.tsx +2 -2
- package/src/components/{new-package → metadata-package-form}/add-package-action-button/add-package-action-button.component.tsx +6 -6
- package/src/components/{new-package → metadata-package-form}/add-package-action-button/add-package-action-button.test.tsx +12 -12
- package/src/components/metadata-package-form/metadata-package-form-utils.ts +9 -0
- package/src/components/{new-package/new-package.workspace.test.tsx → metadata-package-form/metadata-package-form.workspace.test.tsx} +147 -6
- package/src/components/{new-package/new-package.workspace.tsx → metadata-package-form/metadata-package-form.workspace.tsx} +111 -22
- package/src/components/packages-table/packages-table.component.tsx +3 -3
- package/src/components/packages-table/packages-table.test.tsx +10 -10
- package/src/components/{view-package/delete-package.modal.test.tsx → view-metadata-package/delete-metadata-package.modal.test.tsx} +4 -2
- package/src/components/{view-package/delete-package.modal.tsx → view-metadata-package/delete-metadata-package.modal.tsx} +3 -3
- package/src/components/{view-package/view-package-utils.ts → view-metadata-package/view-metadata-package-utils.ts} +2 -2
- package/src/components/{view-package/view-package.workspace.scss → view-metadata-package/view-metadata-package.workspace.scss} +14 -2
- package/src/components/{view-package/view-package.workspace.test.tsx → view-metadata-package/view-metadata-package.workspace.test.tsx} +52 -2
- package/src/components/{view-package/view-package.workspace.tsx → view-metadata-package/view-metadata-package.workspace.tsx} +28 -7
- package/src/components/{view-package → view-metadata-package}/view-package-action-button/view-package-action-button.component.test.tsx +4 -4
- package/src/components/{view-package → view-metadata-package}/view-package-action-button/view-package-action-button.component.tsx +2 -2
- package/src/index.ts +6 -6
- package/src/packages/packages.resource.ts +42 -19
- package/src/routes.json +6 -6
- package/translations/en.json +8 -0
- package/dist/4974.css +0 -1
- package/dist/4974.css.map +0 -1
- package/dist/4974.js +0 -1
- package/dist/4974.js.map +0 -1
- package/dist/5309.css +0 -1
- package/dist/5309.css.map +0 -1
- package/dist/5309.js +0 -1
- package/dist/5309.js.map +0 -1
- package/dist/5739.js +0 -1
- package/dist/5739.js.map +0 -1
- package/dist/827.css +0 -1
- package/dist/827.css.map +0 -1
- package/dist/827.js +0 -1
- package/dist/827.js.map +0 -1
- package/src/components/new-package/new-package-utils.ts +0 -8
- /package/dist/{5739.css → 1118.css} +0 -0
- /package/src/components/{new-package/new-package.workspace.scss → metadata-package-form/metadata-package-form.workspace.scss} +0 -0
- /package/src/components/{view-package/delete-package.modal.scss → view-metadata-package/delete-metadata-package.modal.scss} +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type TFunction } from 'i18next';
|
|
2
|
+
import { launchWorkspace } from '@openmrs/esm-framework';
|
|
3
|
+
|
|
4
|
+
export const launchPackageFormWorkspace = (t: TFunction, uuid?: string) => {
|
|
5
|
+
launchWorkspace('metadata-package-form-workspace', {
|
|
6
|
+
workspaceTitle: uuid ? t('editpackage', 'Edit package') : t('newpackage', 'New package'),
|
|
7
|
+
uuid,
|
|
8
|
+
});
|
|
9
|
+
};
|
|
@@ -4,8 +4,8 @@ import userEvent from '@testing-library/user-event';
|
|
|
4
4
|
import { OpenmrsFetchError, showSnackbar } from '@openmrs/esm-framework';
|
|
5
5
|
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
6
6
|
import { useDomains } from '../../domain-lookups/domain-lookups.resource';
|
|
7
|
-
import { createPackage } from '../../packages/packages.resource';
|
|
8
|
-
import
|
|
7
|
+
import { createPackage, editPackage, usePackage } from '../../packages/packages.resource';
|
|
8
|
+
import MetadataPackageFormWorkspace from './metadata-package-form.workspace';
|
|
9
9
|
|
|
10
10
|
vi.mock('../../domain-lookups/domain-lookups.resource', async (importOriginal) => ({
|
|
11
11
|
...(await importOriginal<object>()),
|
|
@@ -14,10 +14,14 @@ vi.mock('../../domain-lookups/domain-lookups.resource', async (importOriginal) =
|
|
|
14
14
|
|
|
15
15
|
vi.mock('../../packages/packages.resource', () => ({
|
|
16
16
|
createPackage: vi.fn(),
|
|
17
|
+
editPackage: vi.fn(),
|
|
18
|
+
usePackage: vi.fn(),
|
|
17
19
|
}));
|
|
18
20
|
|
|
19
21
|
const mockUseDomains = useDomains as Mock;
|
|
20
22
|
const mockCreatePackage = createPackage as Mock;
|
|
23
|
+
const mockEditPackage = editPackage as Mock;
|
|
24
|
+
const mockUsePackage = usePackage as Mock;
|
|
21
25
|
const mockShowSnackbar = showSnackbar as Mock;
|
|
22
26
|
const mockCloseWorkspace = vi.fn();
|
|
23
27
|
const mockCloseWorkspaceWithSavedChanges = vi.fn();
|
|
@@ -35,9 +39,10 @@ const fetchError = (responseBody: unknown) =>
|
|
|
35
39
|
new Error(),
|
|
36
40
|
);
|
|
37
41
|
|
|
38
|
-
function renderWorkspace() {
|
|
39
|
-
render(
|
|
40
|
-
<
|
|
42
|
+
function renderWorkspace(uuid?: string) {
|
|
43
|
+
return render(
|
|
44
|
+
<MetadataPackageFormWorkspace
|
|
45
|
+
uuid={uuid}
|
|
41
46
|
closeWorkspace={mockCloseWorkspace}
|
|
42
47
|
closeWorkspaceWithSavedChanges={mockCloseWorkspaceWithSavedChanges}
|
|
43
48
|
promptBeforeClosing={mockPromptBeforeClosing}
|
|
@@ -48,10 +53,12 @@ function renderWorkspace() {
|
|
|
48
53
|
);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
describe('
|
|
56
|
+
describe('PackageFormWorkspace', () => {
|
|
52
57
|
beforeEach(() => {
|
|
53
58
|
mockUseDomains.mockReturnValue({ domains, isLoading: false, error: undefined });
|
|
59
|
+
mockUsePackage.mockReturnValue({ exportPackage: undefined, isLoading: false, error: undefined });
|
|
54
60
|
mockCreatePackage.mockResolvedValue({ data: {} });
|
|
61
|
+
mockEditPackage.mockResolvedValue({ data: {} });
|
|
55
62
|
});
|
|
56
63
|
|
|
57
64
|
it('renders a checkbox for each domain with a human-readable label', () => {
|
|
@@ -203,4 +210,138 @@ describe('NewPackageWorkspace', () => {
|
|
|
203
210
|
|
|
204
211
|
expect(mockCloseWorkspace).toHaveBeenCalled();
|
|
205
212
|
});
|
|
213
|
+
|
|
214
|
+
describe('edit mode', () => {
|
|
215
|
+
const uuid = 'package-uuid';
|
|
216
|
+
|
|
217
|
+
it('shows a loading indicator while the package is being fetched', () => {
|
|
218
|
+
mockUsePackage.mockReturnValue({ exportPackage: undefined, isLoading: true, error: undefined });
|
|
219
|
+
renderWorkspace(uuid);
|
|
220
|
+
|
|
221
|
+
expect(screen.getByText('Loading package…')).toBeInTheDocument();
|
|
222
|
+
expect(screen.queryByRole('textbox', { name: 'Package name' })).not.toBeInTheDocument();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('shows an error notification when the package request fails', () => {
|
|
226
|
+
mockUsePackage.mockReturnValue({ exportPackage: undefined, isLoading: false, error: new Error('Not found') });
|
|
227
|
+
renderWorkspace(uuid);
|
|
228
|
+
|
|
229
|
+
expect(screen.getByText('Error loading package')).toBeInTheDocument();
|
|
230
|
+
expect(screen.getByText('Not found')).toBeInTheDocument();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('seeds the form from the fetched package', () => {
|
|
234
|
+
mockUsePackage.mockReturnValue({
|
|
235
|
+
exportPackage: {
|
|
236
|
+
name: 'Core reference data',
|
|
237
|
+
description: 'Some notes',
|
|
238
|
+
entries: [{ domain: 'CONCEPTS' }, { domain: 'ENCOUNTER_TYPES' }],
|
|
239
|
+
},
|
|
240
|
+
isLoading: false,
|
|
241
|
+
error: undefined,
|
|
242
|
+
});
|
|
243
|
+
renderWorkspace(uuid);
|
|
244
|
+
|
|
245
|
+
expect(screen.getByRole('textbox', { name: 'Package name' })).toHaveValue('Core reference data');
|
|
246
|
+
expect(screen.getByRole('textbox', { name: 'Description' })).toHaveValue('Some notes');
|
|
247
|
+
expect(screen.getByRole('checkbox', { name: 'Concepts' })).toBeChecked();
|
|
248
|
+
expect(screen.getByRole('checkbox', { name: 'Encounter types' })).toBeChecked();
|
|
249
|
+
expect(screen.getByRole('checkbox', { name: 'Attribute types' })).not.toBeChecked();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('does not overwrite in-progress edits when the package or domains revalidate', async () => {
|
|
253
|
+
// A fresh object each call mimics SWR handing back a new reference on revalidation.
|
|
254
|
+
const serverPackage = () => ({
|
|
255
|
+
exportPackage: { name: 'Original name', description: 'Original notes', entries: [{ domain: 'CONCEPTS' }] },
|
|
256
|
+
isLoading: false,
|
|
257
|
+
error: undefined,
|
|
258
|
+
});
|
|
259
|
+
mockUsePackage.mockReturnValue(serverPackage());
|
|
260
|
+
const user = userEvent.setup();
|
|
261
|
+
const { rerender } = renderWorkspace(uuid);
|
|
262
|
+
|
|
263
|
+
const nameInput = screen.getByRole('textbox', { name: 'Package name' });
|
|
264
|
+
await user.clear(nameInput);
|
|
265
|
+
await user.type(nameInput, 'My edited name');
|
|
266
|
+
|
|
267
|
+
// Simulate a background revalidation returning the original server values again.
|
|
268
|
+
mockUsePackage.mockReturnValue(serverPackage());
|
|
269
|
+
rerender(
|
|
270
|
+
<MetadataPackageFormWorkspace
|
|
271
|
+
uuid={uuid}
|
|
272
|
+
closeWorkspace={mockCloseWorkspace}
|
|
273
|
+
closeWorkspaceWithSavedChanges={mockCloseWorkspaceWithSavedChanges}
|
|
274
|
+
promptBeforeClosing={mockPromptBeforeClosing}
|
|
275
|
+
setTitle={vi.fn()}
|
|
276
|
+
// @ts-expect-error - the workspace only uses closeWorkspace and promptBeforeClosing from the default props
|
|
277
|
+
additionalProps={{}}
|
|
278
|
+
/>,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(screen.getByRole('textbox', { name: 'Package name' })).toHaveValue('My edited name');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('selects every domain when the package has no entries', () => {
|
|
285
|
+
mockUsePackage.mockReturnValue({
|
|
286
|
+
exportPackage: { name: 'Everything', description: '', entries: [] },
|
|
287
|
+
isLoading: false,
|
|
288
|
+
error: undefined,
|
|
289
|
+
});
|
|
290
|
+
renderWorkspace(uuid);
|
|
291
|
+
|
|
292
|
+
for (const label of ['Attribute types', 'Concepts', 'Encounter types']) {
|
|
293
|
+
expect(screen.getByRole('checkbox', { name: label })).toBeChecked();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('updates the package via editPackage, shows a snackbar, and closes the workspace', async () => {
|
|
298
|
+
mockUsePackage.mockReturnValue({
|
|
299
|
+
exportPackage: {
|
|
300
|
+
name: 'Core reference data',
|
|
301
|
+
description: 'Some notes',
|
|
302
|
+
entries: [{ domain: 'CONCEPTS' }],
|
|
303
|
+
},
|
|
304
|
+
isLoading: false,
|
|
305
|
+
error: undefined,
|
|
306
|
+
});
|
|
307
|
+
const user = userEvent.setup();
|
|
308
|
+
renderWorkspace(uuid);
|
|
309
|
+
|
|
310
|
+
await user.click(screen.getByRole('checkbox', { name: 'Encounter types' }));
|
|
311
|
+
await user.click(screen.getByRole('button', { name: 'Save changes' }));
|
|
312
|
+
|
|
313
|
+
expect(mockEditPackage).toHaveBeenCalledWith(uuid, {
|
|
314
|
+
name: 'Core reference data',
|
|
315
|
+
description: 'Some notes',
|
|
316
|
+
entries: [{ domain: 'CONCEPTS' }, { domain: 'ENCOUNTER_TYPES' }],
|
|
317
|
+
});
|
|
318
|
+
expect(mockCreatePackage).not.toHaveBeenCalled();
|
|
319
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
320
|
+
expect.objectContaining({ title: 'Package updated', kind: 'success' }),
|
|
321
|
+
);
|
|
322
|
+
expect(mockCloseWorkspaceWithSavedChanges).toHaveBeenCalled();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('shows an update-failure snackbar and keeps the workspace open on error', async () => {
|
|
326
|
+
mockUsePackage.mockReturnValue({
|
|
327
|
+
exportPackage: { name: 'Core reference data', description: '', entries: [{ domain: 'CONCEPTS' }] },
|
|
328
|
+
isLoading: false,
|
|
329
|
+
error: undefined,
|
|
330
|
+
});
|
|
331
|
+
mockEditPackage.mockRejectedValue(new Error('Server exploded'));
|
|
332
|
+
const user = userEvent.setup();
|
|
333
|
+
renderWorkspace(uuid);
|
|
334
|
+
|
|
335
|
+
await user.click(screen.getByRole('button', { name: 'Save changes' }));
|
|
336
|
+
|
|
337
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
338
|
+
expect.objectContaining({
|
|
339
|
+
title: 'Failed to update package',
|
|
340
|
+
subtitle: 'An unexpected error occurred',
|
|
341
|
+
kind: 'error',
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
expect(mockCloseWorkspaceWithSavedChanges).not.toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
206
347
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
@@ -21,13 +21,18 @@ import {
|
|
|
21
21
|
OpenmrsFetchError,
|
|
22
22
|
} from '@openmrs/esm-framework';
|
|
23
23
|
import { formatDomainLabel, useDomains } from '../../domain-lookups/domain-lookups.resource';
|
|
24
|
-
import { createPackage } from '../../packages/packages.resource';
|
|
25
|
-
import styles from './
|
|
24
|
+
import { createPackage, editPackage, usePackage } from '../../packages/packages.resource';
|
|
25
|
+
import styles from './metadata-package-form.workspace.scss';
|
|
26
26
|
|
|
27
27
|
const packagesUrl = `${restBaseUrl}/metadataexport/packages`;
|
|
28
28
|
const isPackagesCacheKey = (key: unknown) => typeof key === 'string' && key.startsWith(packagesUrl);
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
interface PackageFormWorkspaceProps extends DefaultWorkspaceProps {
|
|
31
|
+
uuid?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MetadataPackageFormWorkspace: React.FC<PackageFormWorkspaceProps> = ({
|
|
35
|
+
uuid,
|
|
31
36
|
closeWorkspace,
|
|
32
37
|
closeWorkspaceWithSavedChanges,
|
|
33
38
|
promptBeforeClosing,
|
|
@@ -35,6 +40,7 @@ const NewPackageWorkspace: React.FC<DefaultWorkspaceProps> = ({
|
|
|
35
40
|
const { t } = useTranslation();
|
|
36
41
|
const { mutate } = useSWRConfig();
|
|
37
42
|
const isTablet = useLayoutType() === 'tablet';
|
|
43
|
+
const isEditMode = Boolean(uuid);
|
|
38
44
|
const { domains, isLoading, error } = useDomains();
|
|
39
45
|
|
|
40
46
|
const [packageName, setPackageName] = useState('');
|
|
@@ -45,6 +51,8 @@ const NewPackageWorkspace: React.FC<DefaultWorkspaceProps> = ({
|
|
|
45
51
|
const allSelected = domains.length > 0 && selectedDomains.size === domains.length;
|
|
46
52
|
const someSelected = selectedDomains.size > 0 && !allSelected;
|
|
47
53
|
|
|
54
|
+
const { exportPackage, isLoading: isLoadingPackage, error: packageError } = usePackage(uuid ?? '');
|
|
55
|
+
|
|
48
56
|
const toggleSelectAll = useCallback(
|
|
49
57
|
(checked: boolean) => {
|
|
50
58
|
setSelectedDomains(checked ? new Set(domains) : new Set());
|
|
@@ -70,6 +78,28 @@ const NewPackageWorkspace: React.FC<DefaultWorkspaceProps> = ({
|
|
|
70
78
|
promptBeforeClosing(() => hasUnsavedChanges);
|
|
71
79
|
}, [hasUnsavedChanges, promptBeforeClosing]);
|
|
72
80
|
|
|
81
|
+
// Seed the form from the fetched package exactly once, so a later SWR revalidation
|
|
82
|
+
// of the package or domain list doesn't clobber the user's in-progress edits.
|
|
83
|
+
// An empty entries list means the package includes every registered domain.
|
|
84
|
+
const hasPrefilledFromPackage = useRef(false);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!isEditMode || hasPrefilledFromPackage.current || !exportPackage) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Wait for the domain list before expanding an "all domains" package.
|
|
90
|
+
if (exportPackage.entries.length === 0 && domains.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
hasPrefilledFromPackage.current = true;
|
|
94
|
+
setPackageName(exportPackage.name);
|
|
95
|
+
setDescription(exportPackage.description ?? '');
|
|
96
|
+
setSelectedDomains(
|
|
97
|
+
exportPackage.entries.length === 0
|
|
98
|
+
? new Set(domains)
|
|
99
|
+
: new Set(exportPackage.entries.map((entry) => entry.domain)),
|
|
100
|
+
);
|
|
101
|
+
}, [isEditMode, exportPackage, domains]);
|
|
102
|
+
|
|
73
103
|
const handleSubmit = useCallback(
|
|
74
104
|
async (event: React.FormEvent<HTMLFormElement>) => {
|
|
75
105
|
event.preventDefault();
|
|
@@ -77,17 +107,32 @@ const NewPackageWorkspace: React.FC<DefaultWorkspaceProps> = ({
|
|
|
77
107
|
|
|
78
108
|
// An explicit empty array tells the server to include every registered domain.
|
|
79
109
|
const entries = allSelected ? [] : Array.from(selectedDomains, (domain) => ({ domain }));
|
|
110
|
+
const payload = { name: packageName.trim(), description: description.trim(), entries };
|
|
80
111
|
|
|
81
112
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
const name = packageName.trim();
|
|
114
|
+
|
|
115
|
+
if (isEditMode && uuid) {
|
|
116
|
+
await editPackage(uuid, payload);
|
|
117
|
+
// Revalidate the packages list so the table reflects the change without a refresh.
|
|
118
|
+
await mutate(isPackagesCacheKey);
|
|
119
|
+
showSnackbar({
|
|
120
|
+
title: t('packageUpdated', 'Package updated'),
|
|
121
|
+
subtitle: t('packageUpdatedSubtitle', '{{name}} was updated successfully', { name }),
|
|
122
|
+
kind: 'success',
|
|
123
|
+
isLowContrast: true,
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
await createPackage(payload);
|
|
127
|
+
// Revalidate the packages list so the table reflects the change without a refresh.
|
|
128
|
+
await mutate(isPackagesCacheKey);
|
|
129
|
+
showSnackbar({
|
|
130
|
+
title: t('packageCreated', 'Package created'),
|
|
131
|
+
subtitle: t('packageCreatedSubtitle', '{{name}} was created successfully', { name }),
|
|
132
|
+
kind: 'success',
|
|
133
|
+
isLowContrast: true,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
91
136
|
// Bypass the "unsaved changes" prompt now that the package is persisted.
|
|
92
137
|
closeWorkspaceWithSavedChanges();
|
|
93
138
|
} catch (submitError) {
|
|
@@ -96,21 +141,63 @@ const NewPackageWorkspace: React.FC<DefaultWorkspaceProps> = ({
|
|
|
96
141
|
typeof responseBody === 'object' && responseBody !== null
|
|
97
142
|
? Object.values(responseBody.fieldErrors ?? {})[0] ?? responseBody.error
|
|
98
143
|
: null;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
144
|
+
const subtitle = reason ?? t('unexpectedError', 'An unexpected error occurred');
|
|
145
|
+
|
|
146
|
+
if (isEditMode) {
|
|
147
|
+
showSnackbar({
|
|
148
|
+
title: t('packageUpdateFailed', 'Failed to update package'),
|
|
149
|
+
subtitle,
|
|
150
|
+
kind: 'error',
|
|
151
|
+
isLowContrast: false,
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
showSnackbar({
|
|
155
|
+
title: t('packageCreationFailed', 'Failed to create package'),
|
|
156
|
+
subtitle,
|
|
157
|
+
kind: 'error',
|
|
158
|
+
isLowContrast: false,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
105
161
|
} finally {
|
|
106
162
|
setIsSubmitting(false);
|
|
107
163
|
}
|
|
108
164
|
},
|
|
109
|
-
[
|
|
165
|
+
[
|
|
166
|
+
allSelected,
|
|
167
|
+
closeWorkspaceWithSavedChanges,
|
|
168
|
+
description,
|
|
169
|
+
isEditMode,
|
|
170
|
+
mutate,
|
|
171
|
+
packageName,
|
|
172
|
+
selectedDomains,
|
|
173
|
+
t,
|
|
174
|
+
uuid,
|
|
175
|
+
],
|
|
110
176
|
);
|
|
111
177
|
|
|
112
178
|
const isSubmitDisabled = packageName.trim().length === 0 || selectedDomains.size === 0 || isSubmitting;
|
|
113
179
|
|
|
180
|
+
if (isEditMode && isLoadingPackage) {
|
|
181
|
+
return (
|
|
182
|
+
<div className={styles.form}>
|
|
183
|
+
<InlineLoading description={t('loadingPackage', 'Loading package…')} />
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (packageError) {
|
|
189
|
+
return (
|
|
190
|
+
<div className={styles.form}>
|
|
191
|
+
<InlineNotification
|
|
192
|
+
kind="error"
|
|
193
|
+
lowContrast
|
|
194
|
+
title={t('errorLoadingPackage', 'Error loading package')}
|
|
195
|
+
subtitle={packageError.message}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
114
201
|
return (
|
|
115
202
|
<Form className={styles.form} onSubmit={handleSubmit}>
|
|
116
203
|
<div className={styles.grid}>
|
|
@@ -184,7 +271,9 @@ const NewPackageWorkspace: React.FC<DefaultWorkspaceProps> = ({
|
|
|
184
271
|
</Button>
|
|
185
272
|
<Button className={styles.button} kind="primary" type="submit" disabled={isSubmitDisabled}>
|
|
186
273
|
{isSubmitting ? (
|
|
187
|
-
<InlineLoading description={t('creating', 'Creating') + '…'} />
|
|
274
|
+
<InlineLoading description={(isEditMode ? t('saving', 'Saving') : t('creating', 'Creating')) + '…'} />
|
|
275
|
+
) : isEditMode ? (
|
|
276
|
+
t('saveChanges', 'Save changes')
|
|
188
277
|
) : (
|
|
189
278
|
t('createPackage', 'Create package')
|
|
190
279
|
)}
|
|
@@ -194,4 +283,4 @@ const NewPackageWorkspace: React.FC<DefaultWorkspaceProps> = ({
|
|
|
194
283
|
);
|
|
195
284
|
};
|
|
196
285
|
|
|
197
|
-
export default
|
|
286
|
+
export default MetadataPackageFormWorkspace;
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
} from '@openmrs/esm-framework';
|
|
23
23
|
import { formatDomainLabel } from '../../domain-lookups/domain-lookups.resource';
|
|
24
24
|
import { useAllPackages } from '../../packages/packages.resource';
|
|
25
|
-
import {
|
|
26
|
-
import ViewPackageActionButton from '../view-package/view-package-action-button/view-package-action-button.component';
|
|
25
|
+
import { launchPackageFormWorkspace } from '../metadata-package-form/metadata-package-form-utils';
|
|
26
|
+
import ViewPackageActionButton from '../view-metadata-package/view-package-action-button/view-package-action-button.component';
|
|
27
27
|
import styles from './packages-table.scss';
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -97,7 +97,7 @@ const PackagesTable: React.FC = () => {
|
|
|
97
97
|
<EmptyCard
|
|
98
98
|
displayText={t('packages__lower', 'packages')}
|
|
99
99
|
headerTitle={headerTitle}
|
|
100
|
-
launchForm={canManage ? () =>
|
|
100
|
+
launchForm={canManage ? () => launchPackageFormWorkspace(t) : undefined}
|
|
101
101
|
/>
|
|
102
102
|
</div>
|
|
103
103
|
);
|
|
@@ -5,8 +5,8 @@ import { type Session, useSession, userHasAccess } from '@openmrs/esm-framework'
|
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { type ExportPackage } from '../../types/index';
|
|
7
7
|
import { useAllPackages } from '../../packages/packages.resource';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { launchPackageFormWorkspace } from '../metadata-package-form/metadata-package-form-utils';
|
|
9
|
+
import { launchViewMetadataPackageWorkspace } from '../view-metadata-package/view-metadata-package-utils';
|
|
10
10
|
import PackagesTable from './packages-table.component';
|
|
11
11
|
|
|
12
12
|
vi.mock('../../packages/packages.resource', async (importOriginal) => ({
|
|
@@ -14,19 +14,19 @@ vi.mock('../../packages/packages.resource', async (importOriginal) => ({
|
|
|
14
14
|
useAllPackages: vi.fn(),
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
-
vi.mock('../
|
|
18
|
-
|
|
17
|
+
vi.mock('../metadata-package-form/metadata-package-form-utils', () => ({
|
|
18
|
+
launchPackageFormWorkspace: vi.fn(),
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
-
vi.mock('../view-package/view-package-utils', () => ({
|
|
22
|
-
|
|
21
|
+
vi.mock('../view-metadata-package/view-metadata-package-utils', () => ({
|
|
22
|
+
launchViewMetadataPackageWorkspace: vi.fn(),
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
25
|
const mockUseAllPackages = vi.mocked(useAllPackages);
|
|
26
26
|
const mockUseSession = vi.mocked(useSession);
|
|
27
27
|
const mockUserHasAccess = vi.mocked(userHasAccess);
|
|
28
|
-
const
|
|
29
|
-
const mockLaunchViewPackageWorkspace = vi.mocked(
|
|
28
|
+
const mockLaunchPackageFormWorkspace = vi.mocked(launchPackageFormWorkspace);
|
|
29
|
+
const mockLaunchViewPackageWorkspace = vi.mocked(launchViewMetadataPackageWorkspace);
|
|
30
30
|
|
|
31
31
|
const sessionWithUser = () => ({ user: { uuid: 'cc8507b8-7c9a-486b-85dc-b8f25ad1e4cc' } }) as unknown as Session;
|
|
32
32
|
|
|
@@ -81,7 +81,7 @@ describe('PackagesTable', () => {
|
|
|
81
81
|
await user.click(screen.getByRole('button', { name: 'Record packages' }));
|
|
82
82
|
|
|
83
83
|
expect(mockUserHasAccess).toHaveBeenCalledWith('Manage Metadata Export Packages', expect.anything());
|
|
84
|
-
expect(
|
|
84
|
+
expect(mockLaunchPackageFormWorkspace).toHaveBeenCalledTimes(1);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
it('does not offer the empty-state action to users without the manage privilege', () => {
|
|
@@ -89,7 +89,7 @@ describe('PackagesTable', () => {
|
|
|
89
89
|
render(<PackagesTable />);
|
|
90
90
|
|
|
91
91
|
expect(screen.queryByRole('button', { name: 'Record packages' })).not.toBeInTheDocument();
|
|
92
|
-
expect(
|
|
92
|
+
expect(mockLaunchPackageFormWorkspace).not.toHaveBeenCalled();
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
it('renders the build status per package and launches the view workspace with the package for the clicked row', async () => {
|
|
@@ -3,7 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import { describe, it, expect, vi, type Mock } from 'vitest';
|
|
5
5
|
import * as esmFramework from '@openmrs/esm-framework';
|
|
6
|
-
import
|
|
6
|
+
import DeleteMetadataPackageModal from './delete-metadata-package.modal';
|
|
7
7
|
import { deletePackage } from '../../packages/packages.resource';
|
|
8
8
|
import type { ExportPackage } from '../../types';
|
|
9
9
|
|
|
@@ -28,7 +28,9 @@ const mockCloseModal = vi.fn();
|
|
|
28
28
|
const mockOnDeleted = vi.fn();
|
|
29
29
|
|
|
30
30
|
function renderModal() {
|
|
31
|
-
render(
|
|
31
|
+
render(
|
|
32
|
+
<DeleteMetadataPackageModal closeModal={mockCloseModal} exportPackage={exportPackage} onDeleted={mockOnDeleted} />,
|
|
33
|
+
);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
describe('DeletePackageModal', () => {
|
|
@@ -5,7 +5,7 @@ import { useSWRConfig } from 'swr';
|
|
|
5
5
|
import { OpenmrsFetchError, restBaseUrl, showSnackbar } from '@openmrs/esm-framework';
|
|
6
6
|
import { deletePackage } from '../../packages/packages.resource';
|
|
7
7
|
import type { ExportPackage } from '../../types';
|
|
8
|
-
import styles from './delete-package.modal.scss';
|
|
8
|
+
import styles from './delete-metadata-package.modal.scss';
|
|
9
9
|
|
|
10
10
|
const packagesUrl = `${restBaseUrl}/metadataexport/packages`;
|
|
11
11
|
const isPackagesCacheKey = (key: unknown) => typeof key === 'string' && key.startsWith(packagesUrl);
|
|
@@ -16,7 +16,7 @@ interface DeletePackageModalProps {
|
|
|
16
16
|
onDeleted: () => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const DeleteMetadataPackageModal: React.FC<DeletePackageModalProps> = ({ closeModal, exportPackage, onDeleted }) => {
|
|
20
20
|
const { t } = useTranslation();
|
|
21
21
|
const { mutate } = useSWRConfig();
|
|
22
22
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
@@ -79,4 +79,4 @@ const DeletePackageModal: React.FC<DeletePackageModalProps> = ({ closeModal, exp
|
|
|
79
79
|
);
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
export default
|
|
82
|
+
export default DeleteMetadataPackageModal;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { launchWorkspace } from '@openmrs/esm-framework';
|
|
2
2
|
import { type ExportPackage } from '../../types/index';
|
|
3
3
|
|
|
4
|
-
export const
|
|
5
|
-
launchWorkspace('view-package-workspace', {
|
|
4
|
+
export const launchViewMetadataPackageWorkspace = (exportPackage: ExportPackage) => {
|
|
5
|
+
launchWorkspace('view-metadata-package-workspace', {
|
|
6
6
|
workspaceTitle: exportPackage.name,
|
|
7
7
|
exportPackage,
|
|
8
8
|
});
|
|
@@ -25,12 +25,24 @@
|
|
|
25
25
|
|
|
26
26
|
.actionRow {
|
|
27
27
|
display: flex;
|
|
28
|
-
flex-direction:
|
|
28
|
+
flex-direction: column;
|
|
29
29
|
gap: layout.$spacing-04;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
.triggerButton {
|
|
33
|
-
|
|
33
|
+
width: 100%;
|
|
34
|
+
max-width: none;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.secondaryActions {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: row;
|
|
41
|
+
gap: layout.$spacing-04;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.secondaryButton {
|
|
45
|
+
flex: 1 1 0;
|
|
34
46
|
max-width: none;
|
|
35
47
|
justify-content: center;
|
|
36
48
|
}
|
|
@@ -3,7 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
5
5
|
import * as esmFramework from '@openmrs/esm-framework';
|
|
6
|
-
import
|
|
6
|
+
import ViewMetadataPackageWorkspace from './view-metadata-package.workspace';
|
|
7
7
|
import { usePackageBuilds, triggerBuild } from '../../packages/packages.resource';
|
|
8
8
|
import type { ExportPackage, ExportPackageBuild } from '../../types';
|
|
9
9
|
|
|
@@ -75,7 +75,7 @@ function mockBuilds(overrides: Partial<ReturnType<typeof usePackageBuilds>> = {}
|
|
|
75
75
|
|
|
76
76
|
function renderWorkspace(pkg: ExportPackage = exportPackage) {
|
|
77
77
|
render(
|
|
78
|
-
<
|
|
78
|
+
<ViewMetadataPackageWorkspace
|
|
79
79
|
exportPackage={pkg}
|
|
80
80
|
closeWorkspace={mockCloseWorkspace}
|
|
81
81
|
closeWorkspaceWithSavedChanges={mockCloseWorkspaceWithSavedChanges}
|
|
@@ -224,4 +224,54 @@ describe('ViewPackageWorkspace', () => {
|
|
|
224
224
|
}),
|
|
225
225
|
);
|
|
226
226
|
});
|
|
227
|
+
|
|
228
|
+
it('triggers a build and revalidates the builds list', async () => {
|
|
229
|
+
const user = userEvent.setup();
|
|
230
|
+
const mutate = vi.fn();
|
|
231
|
+
mockBuilds({ mutate });
|
|
232
|
+
mockTriggerBuild.mockResolvedValue({});
|
|
233
|
+
renderWorkspace();
|
|
234
|
+
|
|
235
|
+
await user.click(screen.getByRole('button', { name: 'Trigger new build' }));
|
|
236
|
+
|
|
237
|
+
expect(mockTriggerBuild).toHaveBeenCalledWith(exportPackage.uuid);
|
|
238
|
+
await waitFor(() => expect(mutate).toHaveBeenCalled());
|
|
239
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('shows an error snackbar when triggering a build fails', async () => {
|
|
243
|
+
const user = userEvent.setup();
|
|
244
|
+
const error = new esmFramework.OpenmrsFetchError(
|
|
245
|
+
'/url',
|
|
246
|
+
{} as Response,
|
|
247
|
+
{ error: 'A build is already running for this package' },
|
|
248
|
+
new Error(),
|
|
249
|
+
);
|
|
250
|
+
mockTriggerBuild.mockRejectedValue(error);
|
|
251
|
+
renderWorkspace();
|
|
252
|
+
|
|
253
|
+
await user.click(screen.getByRole('button', { name: 'Trigger new build' }));
|
|
254
|
+
|
|
255
|
+
await waitFor(() =>
|
|
256
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
257
|
+
expect.objectContaining({ kind: 'error', subtitle: 'A build is already running for this package' }),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('launches the delete confirmation modal wired to close the workspace on success', async () => {
|
|
263
|
+
const user = userEvent.setup();
|
|
264
|
+
renderWorkspace();
|
|
265
|
+
|
|
266
|
+
await user.click(screen.getByRole('button', { name: /delete/i }));
|
|
267
|
+
|
|
268
|
+
expect(mockShowModal).toHaveBeenCalledWith(
|
|
269
|
+
'delete-package-modal',
|
|
270
|
+
expect.objectContaining({
|
|
271
|
+
exportPackage,
|
|
272
|
+
onDeleted: mockCloseWorkspace,
|
|
273
|
+
closeModal: expect.any(Function),
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
});
|
|
227
277
|
});
|