@openmrs/esm-metadataexport-app 4.4.1-pre.877 → 4.4.1-pre.879

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 (29) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/2173.js +1 -1
  3. package/dist/2173.js.map +1 -1
  4. package/dist/5065.js +1 -1
  5. package/dist/5065.js.map +1 -1
  6. package/dist/5117.js +1 -1
  7. package/dist/5117.js.map +1 -1
  8. package/dist/7121.js +1 -1
  9. package/dist/7121.js.map +1 -1
  10. package/dist/9503.js +1 -1
  11. package/dist/9503.js.map +1 -1
  12. package/dist/openmrs-esm-metadataexport-app.js +1 -1
  13. package/dist/openmrs-esm-metadataexport-app.js.buildmanifest.json +16 -16
  14. package/dist/routes.json +1 -1
  15. package/package.json +1 -1
  16. package/src/admin-card-link.component.tsx +1 -1
  17. package/src/components/dashboard/home-dashboard.component.tsx +1 -2
  18. package/src/components/metadata-package-form/add-package-action-button/add-package-action-button.component.tsx +3 -3
  19. package/src/components/metadata-package-form/add-package-action-button/add-package-action-button.test.tsx +5 -5
  20. package/src/components/metadata-package-form/metadata-package-form-utils.ts +3 -7
  21. package/src/components/metadata-package-form/metadata-package-form.workspace.test.tsx +58 -30
  22. package/src/components/metadata-package-form/metadata-package-form.workspace.tsx +104 -106
  23. package/src/components/packages-table/packages-table.component.tsx +1 -1
  24. package/src/components/view-metadata-package/view-metadata-package-utils.ts +2 -5
  25. package/src/components/view-metadata-package/view-metadata-package.workspace.test.tsx +31 -6
  26. package/src/components/view-metadata-package/view-metadata-package.workspace.tsx +103 -93
  27. package/src/root.component.tsx +1 -1
  28. package/src/routes.json +25 -10
  29. package/translations/en.json +2 -1
@@ -71,7 +71,7 @@
71
71
  "auxiliaryFiles": [
72
72
  "openmrs-esm-metadataexport-app.js.map"
73
73
  ],
74
- "hash": "65053bd4657da087",
74
+ "hash": "6234fa494254ef21",
75
75
  "childrenByOrder": {}
76
76
  },
77
77
  {
@@ -225,10 +225,10 @@
225
225
  "rendered": true,
226
226
  "initial": false,
227
227
  "entry": false,
228
- "size": 28842,
228
+ "size": 28540,
229
229
  "sizes": {
230
230
  "css/mini-extract": 1647,
231
- "javascript": 27195
231
+ "javascript": 26893
232
232
  },
233
233
  "names": [],
234
234
  "idHints": [],
@@ -243,7 +243,7 @@
243
243
  "2173.css.map",
244
244
  "2173.js.map"
245
245
  ],
246
- "hash": "e0e3b016c62a47f6",
246
+ "hash": "c226db14bbdbbf59",
247
247
  "childrenByOrder": {}
248
248
  },
249
249
  {
@@ -763,10 +763,10 @@
763
763
  "rendered": true,
764
764
  "initial": false,
765
765
  "entry": false,
766
- "size": 17286,
766
+ "size": 17618,
767
767
  "sizes": {
768
768
  "css/mini-extract": 1915,
769
- "javascript": 15371
769
+ "javascript": 15703
770
770
  },
771
771
  "names": [],
772
772
  "idHints": [],
@@ -781,7 +781,7 @@
781
781
  "5065.css.map",
782
782
  "5065.js.map"
783
783
  ],
784
- "hash": "c6856ed1af3aef06",
784
+ "hash": "c013e3ec5d865d0a",
785
785
  "childrenByOrder": {}
786
786
  },
787
787
  {
@@ -789,9 +789,9 @@
789
789
  "rendered": true,
790
790
  "initial": false,
791
791
  "entry": false,
792
- "size": 2063,
792
+ "size": 2092,
793
793
  "sizes": {
794
- "javascript": 2063
794
+ "javascript": 2092
795
795
  },
796
796
  "names": [],
797
797
  "idHints": [],
@@ -804,7 +804,7 @@
804
804
  "auxiliaryFiles": [
805
805
  "5117.js.map"
806
806
  ],
807
- "hash": "d2addaad0ac571f2",
807
+ "hash": "b94bb6b8ac5d28e2",
808
808
  "childrenByOrder": {}
809
809
  },
810
810
  {
@@ -1323,9 +1323,9 @@
1323
1323
  "rendered": true,
1324
1324
  "initial": false,
1325
1325
  "entry": false,
1326
- "size": 1375,
1326
+ "size": 1376,
1327
1327
  "sizes": {
1328
- "javascript": 1375
1328
+ "javascript": 1376
1329
1329
  },
1330
1330
  "names": [],
1331
1331
  "idHints": [],
@@ -1338,7 +1338,7 @@
1338
1338
  "auxiliaryFiles": [
1339
1339
  "7121.js.map"
1340
1340
  ],
1341
- "hash": "0c31c11c68a25b81",
1341
+ "hash": "75c22d2c49b9924c",
1342
1342
  "childrenByOrder": {}
1343
1343
  },
1344
1344
  {
@@ -1625,10 +1625,10 @@
1625
1625
  "rendered": true,
1626
1626
  "initial": false,
1627
1627
  "entry": false,
1628
- "size": 19058,
1628
+ "size": 19275,
1629
1629
  "sizes": {
1630
1630
  "css/mini-extract": 4201,
1631
- "javascript": 14857
1631
+ "javascript": 15074
1632
1632
  },
1633
1633
  "names": [],
1634
1634
  "idHints": [],
@@ -1643,7 +1643,7 @@
1643
1643
  "9503.css.map",
1644
1644
  "9503.js.map"
1645
1645
  ],
1646
- "hash": "bf22bbb807ea720f",
1646
+ "hash": "10d839d0fb1604b5",
1647
1647
  "childrenByOrder": {}
1648
1648
  },
1649
1649
  {
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"metadataexport":">=1.0.0-SNAPSHOT"},"pages":[{"route":"metadataexport","component":"root","online":true}],"extensions":[{"name":"metadata-export-card-link","slot":"system-admin-page-card-link-slot","component":"metadataExportCardLink","privileges":["Get Metadata Export Packages"]}],"modals":[{"name":"delete-metadata-package-modal","component":"deleteMetadataPackageModal"}],"workspaces":[{"name":"metadata-package-form-workspace","component":"metadataPackageFormWorkspace","title":"New package","type":"form","canMaximize":true,"width":"narrow"},{"name":"view-metadata-package-workspace","component":"viewMetadataPackageWorkspace","title":"View package","type":"form","canMaximize":true,"width":"narrow"}],"version":"4.4.1-pre.877"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"metadataexport":">=1.0.0-SNAPSHOT"},"pages":[{"route":"metadata-export","component":"root","online":true}],"extensions":[{"name":"metadata-export-card-link","slot":"system-admin-page-card-link-slot","component":"metadataExportCardLink","privileges":["Get Metadata Export Packages"]}],"modals":[{"name":"delete-metadata-package-modal","component":"deleteMetadataPackageModal"}],"workspaceGroups2":[{"name":"metadata-export-group","scopePattern":"^/metadata-export","overlay":true}],"workspaceWindows2":[{"name":"metadata-package-form-window","group":"metadata-export-group","canMaximize":true,"width":"narrow"},{"name":"view-metadata-package-window","group":"metadata-export-group","canMaximize":true,"width":"narrow"}],"workspaces2":[{"name":"metadata-package-form-workspace","component":"metadataPackageFormWorkspace","window":"metadata-package-form-window"},{"name":"view-metadata-package-workspace","component":"viewMetadataPackageWorkspace","window":"view-metadata-package-window"}],"version":"4.4.1-pre.879"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-metadataexport-app",
3
- "version": "4.4.1-pre.877",
3
+ "version": "4.4.1-pre.879",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Metadata export admin dashboard for O3",
6
6
  "browser": "dist/openmrs-esm-metadataexport-app.js",
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
3
3
  import { Layer, ClickableTile } from '@carbon/react';
4
4
  import { ArrowRightIcon, navigate } from '@openmrs/esm-framework';
5
5
 
6
- const metadataExportUrl = `${window.spaBase}/metadataexport`;
6
+ const metadataExportUrl = `${window.spaBase}/metadata-export`;
7
7
 
8
8
  // Navigate client-side on a plain left click; modified clicks fall through
9
9
  // to the anchor so they open a new tab or window as usual.
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { useLayoutType, isDesktop, WorkspaceContainer } from '@openmrs/esm-framework';
2
+ import { useLayoutType, isDesktop } from '@openmrs/esm-framework';
3
3
  import DashboardView from './dashboard-view.component';
4
4
  import styles from './home-dashboard.scss';
5
5
 
@@ -11,7 +11,6 @@ export default function Dashboard() {
11
11
  <section className={isDesktop(layout) ? styles.dashboardContainer : styles.dashboardContainerTablet}>
12
12
  <DashboardView />
13
13
  </section>
14
- <WorkspaceContainer overlay contextKey="metadata-export" />
15
14
  </div>
16
15
  );
17
16
  }
@@ -12,8 +12,8 @@ const AddPackageActionButton: React.FC = () => {
12
12
  const canManage = session.user ? userHasAccess('Manage Metadata Export Packages', session.user) : false;
13
13
 
14
14
  const handleLaunchPackageForm = useCallback(() => {
15
- launchPackageFormWorkspace(t);
16
- }, [t]);
15
+ launchPackageFormWorkspace();
16
+ }, []);
17
17
 
18
18
  if (!canManage) {
19
19
  return null;
@@ -26,7 +26,7 @@ const AddPackageActionButton: React.FC = () => {
26
26
  kind="primary"
27
27
  renderIcon={(props) => <Add size={16} {...props} />}
28
28
  >
29
- {t('newpackage', 'New Package')}
29
+ {t('newPackage', 'New package')}
30
30
  </Button>
31
31
  );
32
32
  };
@@ -23,17 +23,17 @@ describe('AddPackageActionButton', () => {
23
23
  mockUserHasAccess.mockReturnValue(true);
24
24
  });
25
25
 
26
- it('renders a "New Package" button', () => {
26
+ it('renders a "New package" button', () => {
27
27
  render(<AddPackageActionButton />);
28
28
 
29
- expect(screen.getByRole('button', { name: 'New Package' })).toBeInTheDocument();
29
+ expect(screen.getByRole('button', { name: 'New package' })).toBeInTheDocument();
30
30
  });
31
31
 
32
32
  it('launches the new package workspace when a privileged user clicks', async () => {
33
33
  const user = userEvent.setup();
34
34
  render(<AddPackageActionButton />);
35
35
 
36
- await user.click(screen.getByRole('button', { name: 'New Package' }));
36
+ await user.click(screen.getByRole('button', { name: 'New package' }));
37
37
 
38
38
  expect(mockUserHasAccess).toHaveBeenCalledWith('Manage Metadata Export Packages', expect.anything());
39
39
  expect(mockLaunchPackageFormWorkspace).toHaveBeenCalledTimes(1);
@@ -43,7 +43,7 @@ describe('AddPackageActionButton', () => {
43
43
  mockUserHasAccess.mockReturnValue(false);
44
44
  render(<AddPackageActionButton />);
45
45
 
46
- expect(screen.queryByRole('button', { name: 'New Package' })).not.toBeInTheDocument();
46
+ expect(screen.queryByRole('button', { name: 'New package' })).not.toBeInTheDocument();
47
47
  expect(mockLaunchPackageFormWorkspace).not.toHaveBeenCalled();
48
48
  });
49
49
 
@@ -51,7 +51,7 @@ describe('AddPackageActionButton', () => {
51
51
  mockUseSession.mockReturnValue({ authenticated: false } as unknown as Session);
52
52
  render(<AddPackageActionButton />);
53
53
 
54
- expect(screen.queryByRole('button', { name: 'New Package' })).not.toBeInTheDocument();
54
+ expect(screen.queryByRole('button', { name: 'New package' })).not.toBeInTheDocument();
55
55
  expect(mockUserHasAccess).not.toHaveBeenCalled();
56
56
  });
57
57
  });
@@ -1,9 +1,5 @@
1
- import { type TFunction } from 'i18next';
2
- import { launchWorkspace } from '@openmrs/esm-framework';
1
+ import { launchWorkspace2 } from '@openmrs/esm-framework';
3
2
 
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
- });
3
+ export const launchPackageFormWorkspace = (uuid?: string) => {
4
+ launchWorkspace2('metadata-package-form-workspace', { uuid });
9
5
  };
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
- import { OpenmrsFetchError, showSnackbar } from '@openmrs/esm-framework';
4
+ import { OpenmrsFetchError, showSnackbar, Workspace2, type Workspace2DefinitionProps } 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
7
  import { createPackage, editPackage, usePackage } from '../../packages/packages.resource';
@@ -25,8 +25,7 @@ const mockEditPackage = editPackage as Mock;
25
25
  const mockUsePackage = usePackage as Mock;
26
26
  const mockShowSnackbar = showSnackbar as Mock;
27
27
  const mockCloseWorkspace = vi.fn();
28
- const mockCloseWorkspaceWithSavedChanges = vi.fn();
29
- const mockPromptBeforeClosing = vi.fn();
28
+ const mockWorkspace2 = vi.mocked(Workspace2);
30
29
 
31
30
  const domains = ['ATTRIBUTE_TYPES', 'CONCEPTS', 'ENCOUNTER_TYPES'];
32
31
 
@@ -40,18 +39,22 @@ const fetchError = (responseBody: unknown) =>
40
39
  new Error(),
41
40
  );
42
41
 
42
+ function workspaceProps(uuid?: string): Workspace2DefinitionProps<{ uuid?: string }> {
43
+ return {
44
+ workspaceProps: { uuid },
45
+ windowProps: null,
46
+ groupProps: null,
47
+ closeWorkspace: mockCloseWorkspace,
48
+ launchChildWorkspace: vi.fn(),
49
+ workspaceName: 'metadata-package-form-workspace',
50
+ windowName: 'metadata-package-form-window',
51
+ isRootWorkspace: true,
52
+ showActionMenu: false,
53
+ };
54
+ }
55
+
43
56
  function renderWorkspace(uuid?: string) {
44
- return render(
45
- <MetadataPackageFormWorkspace
46
- uuid={uuid}
47
- closeWorkspace={mockCloseWorkspace}
48
- closeWorkspaceWithSavedChanges={mockCloseWorkspaceWithSavedChanges}
49
- promptBeforeClosing={mockPromptBeforeClosing}
50
- setTitle={vi.fn()}
51
- // @ts-expect-error - the workspace only uses closeWorkspace and promptBeforeClosing from the default props
52
- additionalProps={{}}
53
- />,
54
- );
57
+ return render(<MetadataPackageFormWorkspace {...workspaceProps(uuid)} />);
55
58
  }
56
59
 
57
60
  describe('PackageFormWorkspace', () => {
@@ -135,8 +138,7 @@ describe('PackageFormWorkspace', () => {
135
138
  expect.objectContaining({ title: 'Package created', kind: 'success' }),
136
139
  );
137
140
  // Closes without triggering the "unsaved changes" prompt.
138
- expect(mockCloseWorkspaceWithSavedChanges).toHaveBeenCalled();
139
- expect(mockCloseWorkspace).not.toHaveBeenCalled();
141
+ expect(mockCloseWorkspace).toHaveBeenCalledWith({ discardUnsavedChanges: true });
140
142
  });
141
143
 
142
144
  it('sends an empty entries array when every domain is selected', async () => {
@@ -209,7 +211,24 @@ describe('PackageFormWorkspace', () => {
209
211
 
210
212
  await user.click(screen.getByRole('button', { name: 'Cancel' }));
211
213
 
212
- expect(mockCloseWorkspace).toHaveBeenCalled();
214
+ expect(mockCloseWorkspace).toHaveBeenCalledWith();
215
+ });
216
+
217
+ it('marks the workspace as having unsaved changes once the user starts filling the form', async () => {
218
+ const user = userEvent.setup();
219
+ renderWorkspace();
220
+
221
+ expect(mockWorkspace2).toHaveBeenLastCalledWith(
222
+ expect.objectContaining({ title: 'New package', hasUnsavedChanges: false }),
223
+ expect.anything(),
224
+ );
225
+
226
+ await user.type(screen.getByRole('textbox', { name: 'Package name' }), 'Core reference data');
227
+
228
+ expect(mockWorkspace2).toHaveBeenLastCalledWith(
229
+ expect.objectContaining({ hasUnsavedChanges: true }),
230
+ expect.anything(),
231
+ );
213
232
  });
214
233
 
215
234
  describe('edit mode', () => {
@@ -231,6 +250,25 @@ describe('PackageFormWorkspace', () => {
231
250
  expect(screen.getByText('Not found')).toBeInTheDocument();
232
251
  });
233
252
 
253
+ it('still reports unsaved changes when a refetch fails after the user has edited the package', async () => {
254
+ const exportPackage = { name: 'Original name', description: '', entries: [{ domain: 'CONCEPTS' }] };
255
+ mockUsePackage.mockReturnValue({ exportPackage, isLoading: false, error: undefined });
256
+ const user = userEvent.setup();
257
+ const { rerender } = renderWorkspace(uuid);
258
+
259
+ await user.type(screen.getByRole('textbox', { name: 'Package name' }), ' edited');
260
+
261
+ // SWR keeps the cached package but reports the error from a failed background revalidation.
262
+ mockUsePackage.mockReturnValue({ exportPackage, isLoading: false, error: new Error('Network error') });
263
+ rerender(<MetadataPackageFormWorkspace {...workspaceProps(uuid)} />);
264
+
265
+ expect(screen.getByText('Error loading package')).toBeInTheDocument();
266
+ expect(mockWorkspace2).toHaveBeenLastCalledWith(
267
+ expect.objectContaining({ hasUnsavedChanges: true }),
268
+ expect.anything(),
269
+ );
270
+ });
271
+
234
272
  it('seeds the form from the fetched package', () => {
235
273
  mockUsePackage.mockReturnValue({
236
274
  exportPackage: {
@@ -267,17 +305,7 @@ describe('PackageFormWorkspace', () => {
267
305
 
268
306
  // Simulate a background revalidation returning the original server values again.
269
307
  mockUsePackage.mockReturnValue(serverPackage());
270
- rerender(
271
- <MetadataPackageFormWorkspace
272
- uuid={uuid}
273
- closeWorkspace={mockCloseWorkspace}
274
- closeWorkspaceWithSavedChanges={mockCloseWorkspaceWithSavedChanges}
275
- promptBeforeClosing={mockPromptBeforeClosing}
276
- setTitle={vi.fn()}
277
- // @ts-expect-error - the workspace only uses closeWorkspace and promptBeforeClosing from the default props
278
- additionalProps={{}}
279
- />,
280
- );
308
+ rerender(<MetadataPackageFormWorkspace {...workspaceProps(uuid)} />);
281
309
 
282
310
  expect(screen.getByRole('textbox', { name: 'Package name' })).toHaveValue('My edited name');
283
311
  });
@@ -320,7 +348,7 @@ describe('PackageFormWorkspace', () => {
320
348
  expect(mockShowSnackbar).toHaveBeenCalledWith(
321
349
  expect.objectContaining({ title: 'Package updated', kind: 'success' }),
322
350
  );
323
- expect(mockCloseWorkspaceWithSavedChanges).toHaveBeenCalled();
351
+ expect(mockCloseWorkspace).toHaveBeenCalledWith({ discardUnsavedChanges: true });
324
352
  });
325
353
 
326
354
  it('shows an update-failure snackbar and keeps the workspace open on error', async () => {
@@ -342,7 +370,7 @@ describe('PackageFormWorkspace', () => {
342
370
  kind: 'error',
343
371
  }),
344
372
  );
345
- expect(mockCloseWorkspaceWithSavedChanges).not.toHaveBeenCalled();
373
+ expect(mockCloseWorkspace).not.toHaveBeenCalled();
346
374
  });
347
375
  });
348
376
  });
@@ -13,21 +13,26 @@ import {
13
13
  TextInput,
14
14
  } from '@carbon/react';
15
15
  import { useSWRConfig } from 'swr';
16
- import { type DefaultWorkspaceProps, showSnackbar, useLayoutType, OpenmrsFetchError } from '@openmrs/esm-framework';
16
+ import {
17
+ showSnackbar,
18
+ useLayoutType,
19
+ OpenmrsFetchError,
20
+ Workspace2,
21
+ type Workspace2DefinitionProps,
22
+ } from '@openmrs/esm-framework';
17
23
  import { formatDomainLabel, useDomains } from '../../domain-lookups/domain-lookups.resource';
18
24
  import { createPackage, editPackage, isPackagesCacheKey, usePackage } from '../../packages/packages.resource';
19
25
  import styles from './metadata-package-form.workspace.scss';
20
26
 
21
- interface PackageFormWorkspaceProps extends DefaultWorkspaceProps {
27
+ interface PackageFormWorkspaceProps {
22
28
  uuid?: string;
23
29
  }
24
30
 
25
- const MetadataPackageFormWorkspace: React.FC<PackageFormWorkspaceProps> = ({
26
- uuid,
31
+ const MetadataPackageFormWorkspace: React.FC<Workspace2DefinitionProps<PackageFormWorkspaceProps>> = ({
32
+ workspaceProps,
27
33
  closeWorkspace,
28
- closeWorkspaceWithSavedChanges,
29
- promptBeforeClosing,
30
34
  }) => {
35
+ const uuid = workspaceProps?.uuid;
31
36
  const { t } = useTranslation();
32
37
  const { mutate } = useSWRConfig();
33
38
  const isTablet = useLayoutType() === 'tablet';
@@ -65,10 +70,6 @@ const MetadataPackageFormWorkspace: React.FC<PackageFormWorkspaceProps> = ({
65
70
 
66
71
  const hasUnsavedChanges = packageName.trim().length > 0 || description.trim().length > 0 || selectedDomains.size > 0;
67
72
 
68
- useEffect(() => {
69
- promptBeforeClosing(() => hasUnsavedChanges);
70
- }, [hasUnsavedChanges, promptBeforeClosing]);
71
-
72
73
  // Seed the form from the fetched package exactly once, so a later SWR revalidation
73
74
  // of the package or domain list doesn't clobber the user's in-progress edits.
74
75
  // An empty entries list means the package includes every registered domain.
@@ -125,7 +126,7 @@ const MetadataPackageFormWorkspace: React.FC<PackageFormWorkspaceProps> = ({
125
126
  });
126
127
  }
127
128
  // Bypass the "unsaved changes" prompt now that the package is persisted.
128
- closeWorkspaceWithSavedChanges();
129
+ closeWorkspace({ discardUnsavedChanges: true });
129
130
  } catch (submitError) {
130
131
  const responseBody = submitError instanceof OpenmrsFetchError ? submitError.responseBody : null;
131
132
  const reason =
@@ -153,124 +154,121 @@ const MetadataPackageFormWorkspace: React.FC<PackageFormWorkspaceProps> = ({
153
154
  setIsSubmitting(false);
154
155
  }
155
156
  },
156
- [
157
- allSelected,
158
- closeWorkspaceWithSavedChanges,
159
- description,
160
- isEditMode,
161
- mutate,
162
- packageName,
163
- selectedDomains,
164
- t,
165
- uuid,
166
- ],
157
+ [allSelected, closeWorkspace, description, isEditMode, mutate, packageName, selectedDomains, t, uuid],
167
158
  );
168
159
 
169
160
  const isSubmitDisabled = packageName.trim().length === 0 || selectedDomains.size === 0 || isSubmitting;
161
+ const title = isEditMode ? t('editPackage', 'Edit package') : t('newPackage', 'New package');
170
162
 
171
163
  if (isEditMode && isLoadingPackage) {
172
164
  return (
173
- <div className={styles.form}>
174
- <InlineLoading description={t('loadingPackage', 'Loading package…')} />
175
- </div>
165
+ <Workspace2 title={title}>
166
+ <div className={styles.form}>
167
+ <InlineLoading description={t('loadingPackage', 'Loading package…')} />
168
+ </div>
169
+ </Workspace2>
176
170
  );
177
171
  }
178
172
 
179
173
  if (packageError) {
180
174
  return (
181
- <div className={styles.form}>
182
- <InlineNotification
183
- kind="error"
184
- lowContrast
185
- title={t('errorLoadingPackage', 'Error loading package')}
186
- subtitle={packageError.message}
187
- />
188
- </div>
175
+ <Workspace2 title={title} hasUnsavedChanges={hasUnsavedChanges}>
176
+ <div className={styles.form}>
177
+ <InlineNotification
178
+ kind="error"
179
+ lowContrast
180
+ title={t('errorLoadingPackage', 'Error loading package')}
181
+ subtitle={packageError.message}
182
+ />
183
+ </div>
184
+ </Workspace2>
189
185
  );
190
186
  }
191
187
 
192
188
  return (
193
- <Form className={styles.form} onSubmit={handleSubmit}>
194
- <div className={styles.grid}>
195
- <Stack gap={6}>
196
- <section className={styles.section}>
197
- <TextInput
198
- id="packageName"
199
- labelText={t('packageName', 'Package name')}
200
- placeholder={t('packageNamePlaceholder', 'e.g. Core reference data')}
201
- value={packageName}
202
- onChange={(event) => setPackageName(event.target.value)}
203
- />
204
- </section>
189
+ <Workspace2 title={title} hasUnsavedChanges={hasUnsavedChanges}>
190
+ <Form className={styles.form} onSubmit={handleSubmit}>
191
+ <div className={styles.grid}>
192
+ <Stack gap={6}>
193
+ <section className={styles.section}>
194
+ <TextInput
195
+ id="packageName"
196
+ labelText={t('packageName', 'Package name')}
197
+ placeholder={t('packageNamePlaceholder', 'e.g. Core reference data')}
198
+ value={packageName}
199
+ onChange={(event) => setPackageName(event.target.value)}
200
+ />
201
+ </section>
205
202
 
206
- <section className={styles.section}>
207
- <TextArea
208
- id="packageDescription"
209
- labelText={t('description', 'Description')}
210
- placeholder={t('descriptionPlaceholder', 'Briefly describe what this package contains')}
211
- value={description}
212
- onChange={(event) => setDescription(event.target.value)}
213
- />
214
- </section>
203
+ <section className={styles.section}>
204
+ <TextArea
205
+ id="packageDescription"
206
+ labelText={t('description', 'Description')}
207
+ placeholder={t('descriptionPlaceholder', 'Briefly describe what this package contains')}
208
+ value={description}
209
+ onChange={(event) => setDescription(event.target.value)}
210
+ />
211
+ </section>
215
212
 
216
- <section className={styles.section}>
217
- <span className={styles.sectionLabel}>{t('domains', 'Domains')}</span>
213
+ <section className={styles.section}>
214
+ <span className={styles.sectionLabel}>{t('domains', 'Domains')}</span>
218
215
 
219
- {error ? (
220
- <InlineNotification
221
- kind="error"
222
- lowContrast
223
- title={t('errorLoadingDomains', 'Error loading domains')}
224
- subtitle={error?.message}
225
- />
226
- ) : isLoading ? (
227
- <div className={styles.domainList}>
228
- {Array.from({ length: 8 }, (_, index) => (
229
- <CheckboxSkeleton key={index} />
230
- ))}
231
- </div>
232
- ) : (
233
- <>
234
- <Checkbox
235
- id="select-all-domains"
236
- className={styles.selectAll}
237
- labelText={t('selectAll', 'Select all')}
238
- checked={allSelected}
239
- indeterminate={someSelected}
240
- onChange={(_event, { checked }) => toggleSelectAll(checked)}
216
+ {error ? (
217
+ <InlineNotification
218
+ kind="error"
219
+ lowContrast
220
+ title={t('errorLoadingDomains', 'Error loading domains')}
221
+ subtitle={error?.message}
241
222
  />
223
+ ) : isLoading ? (
242
224
  <div className={styles.domainList}>
243
- {domains.map((domain) => (
244
- <Checkbox
245
- key={domain}
246
- id={`domain-${domain}`}
247
- labelText={formatDomainLabel(domain)}
248
- checked={selectedDomains.has(domain)}
249
- onChange={(_event, { checked }) => toggleDomain(domain, checked)}
250
- />
225
+ {Array.from({ length: 8 }, (_, index) => (
226
+ <CheckboxSkeleton key={index} />
251
227
  ))}
252
228
  </div>
253
- </>
254
- )}
255
- </section>
256
- </Stack>
257
- </div>
229
+ ) : (
230
+ <>
231
+ <Checkbox
232
+ id="select-all-domains"
233
+ className={styles.selectAll}
234
+ labelText={t('selectAll', 'Select all')}
235
+ checked={allSelected}
236
+ indeterminate={someSelected}
237
+ onChange={(_event, { checked }) => toggleSelectAll(checked)}
238
+ />
239
+ <div className={styles.domainList}>
240
+ {domains.map((domain) => (
241
+ <Checkbox
242
+ key={domain}
243
+ id={`domain-${domain}`}
244
+ labelText={formatDomainLabel(domain)}
245
+ checked={selectedDomains.has(domain)}
246
+ onChange={(_event, { checked }) => toggleDomain(domain, checked)}
247
+ />
248
+ ))}
249
+ </div>
250
+ </>
251
+ )}
252
+ </section>
253
+ </Stack>
254
+ </div>
258
255
 
259
- <ButtonSet className={isTablet ? styles.tabletButtonSet : styles.desktopButtonSet}>
260
- <Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
261
- {t('cancel', 'Cancel')}
262
- </Button>
263
- <Button className={styles.button} kind="primary" type="submit" disabled={isSubmitDisabled}>
264
- {isSubmitting ? (
265
- <InlineLoading description={(isEditMode ? t('saving', 'Saving') : t('creating', 'Creating')) + '…'} />
266
- ) : isEditMode ? (
267
- t('saveChanges', 'Save changes')
268
- ) : (
269
- t('createPackage', 'Create package')
270
- )}
271
- </Button>
272
- </ButtonSet>
273
- </Form>
256
+ <ButtonSet className={isTablet ? styles.tabletButtonSet : styles.desktopButtonSet}>
257
+ <Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
258
+ {t('cancel', 'Cancel')}
259
+ </Button>
260
+ <Button className={styles.button} kind="primary" type="submit" disabled={isSubmitDisabled}>
261
+ {isSubmitting ? (
262
+ <InlineLoading description={(isEditMode ? t('saving', 'Saving') : t('creating', 'Creating')) + '…'} />
263
+ ) : isEditMode ? (
264
+ t('saveChanges', 'Save changes')
265
+ ) : (
266
+ t('createPackage', 'Create package')
267
+ )}
268
+ </Button>
269
+ </ButtonSet>
270
+ </Form>
271
+ </Workspace2>
274
272
  );
275
273
  };
276
274
 
@@ -101,7 +101,7 @@ const PackagesTable: React.FC = () => {
101
101
  <EmptyCard
102
102
  displayText={t('packages__lower', 'packages')}
103
103
  headerTitle={headerTitle}
104
- launchForm={canManage ? () => launchPackageFormWorkspace(t) : undefined}
104
+ launchForm={canManage ? () => launchPackageFormWorkspace() : undefined}
105
105
  />
106
106
  </div>
107
107
  );