@openmrs/esm-styleguide 11.0.1-pre.5799 → 11.0.1-pre.5825

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.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 117 files, copied 171 files with swc (274.55ms)
1
+ [0] Successfully compiled: 117 files, copied 171 files with swc (257.79ms)
2
2
  [0] swc --strip-leading-paths --copy-files src -d dist && svgo -rf dist --quiet exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -1 +1 @@
1
- {"version":3,"file":"active-workspace-window.component.d.ts","sourceRoot":"","sources":["../../src/workspaces2/active-workspace-window.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuD,MAAM,OAAO,CAAC;AAM5E,OAAO,EACL,KAAK,YAAY,EAKlB,MAAM,yBAAyB,CAAC;AAOjC,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAGzE,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACvC,OAAO,EAAE,sBAAsB,CAAC;IAChC;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;CACzB;AAKD;;;GAGG;AACH,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA4DzD,CAAC;AA0RF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"active-workspace-window.component.d.ts","sourceRoot":"","sources":["../../src/workspaces2/active-workspace-window.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+D,MAAM,OAAO,CAAC;AAapF,OAAO,EACL,KAAK,YAAY,EAKlB,MAAM,yBAAyB,CAAC;AAQjC,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAGzE,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IACvC,OAAO,EAAE,sBAAsB,CAAC;IAChC;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;CACzB;AAKD;;;GAGG;AACH,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAuEzD,CAAC;AAmSF,eAAe,qBAAqB,CAAC"}
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React, { useEffect, useMemo, useState } from "react";
2
+ import React, { useEffect, useMemo, useRef, useState } from "react";
3
3
  import classNames from "classnames";
4
4
  import Parcel from "single-spa-react/parcel";
5
- import { Header, HeaderGlobalAction, HeaderGlobalBar, HeaderName, InlineLoading } from "@carbon/react";
5
+ import { Header, HeaderGlobalAction, HeaderGlobalBar, HeaderName, InlineLoading, InlineNotification } from "@carbon/react";
6
6
  import { DownToBottom, Maximize, Minimize } from "@carbon/react/icons";
7
7
  import { createParcelMounter, workspace2Store } from "@openmrs/esm-extensions";
8
8
  import { isDesktop, useLayoutType, useStore } from "@openmrs/esm-react-utils";
9
+ import { reportError } from "@openmrs/esm-error-handling";
9
10
  import { loadLifeCycles } from "@openmrs/esm-routes";
10
11
  import { getCoreTranslation } from "@openmrs/esm-translations";
11
12
  import { ArrowRightIcon, CloseIcon } from "../icons/index.js";
@@ -28,6 +29,9 @@ const selectRegisteredWorkspacesByName = (state)=>state.registeredWorkspacesByNa
28
29
  const [lifeCyclesByUuid, setLifeCyclesByUuid] = useState({});
29
30
  // Reactive so the effect retries when a workspace registers after mount.
30
31
  const registeredWorkspacesByName = useStore(workspace2Store, selectRegisteredWorkspacesByName);
32
+ // The effect reloads every workspace whenever any of them changes (for example, its title), so
33
+ // track which workspaces have already reported a load failure to report each one only once.
34
+ const reportedFailureUuids = useRef(new Set());
31
35
  useEffect(()=>{
32
36
  let cancelled = false;
33
37
  Promise.all(openedWorkspaces.map(async (openedWorkspace)=>{
@@ -36,15 +40,26 @@ const selectRegisteredWorkspacesByName = (state)=>state.registeredWorkspacesByNa
36
40
  return null;
37
41
  }
38
42
  const { moduleName, component } = workspaceDef;
39
- return [
40
- openedWorkspace.uuid,
41
- await loadLifeCycles(moduleName, component)
42
- ];
43
+ try {
44
+ return [
45
+ openedWorkspace.uuid,
46
+ await loadLifeCycles(moduleName, component)
47
+ ];
48
+ } catch (error) {
49
+ if (!cancelled && !reportedFailureUuids.current.has(openedWorkspace.uuid)) {
50
+ reportedFailureUuids.current.add(openedWorkspace.uuid);
51
+ reportError(error);
52
+ }
53
+ return [
54
+ openedWorkspace.uuid,
55
+ null
56
+ ];
57
+ }
43
58
  })).then((entries)=>{
44
59
  if (!cancelled) {
45
60
  setLifeCyclesByUuid(Object.fromEntries(entries.filter((entry)=>entry != null)));
46
61
  }
47
- });
62
+ }, reportError);
48
63
  return ()=>{
49
64
  cancelled = true;
50
65
  };
@@ -141,7 +156,17 @@ const ActiveWorkspace = ({ lifeCycle, openedWorkspace, openedWindow, groupProps,
141
156
  [styles.hiddenExtraWorkspace]: hideBehindLeaf
142
157
  }),
143
158
  ...props
144
- }, openedWorkspace.workspaceName) : null, [
159
+ }, openedWorkspace.workspaceName) : lifeCycle === null ? /*#__PURE__*/ _jsx("div", {
160
+ className: classNames(styles.workspaceContent, {
161
+ [styles.hiddenExtraWorkspace]: hideBehindLeaf
162
+ }),
163
+ children: /*#__PURE__*/ _jsx(InlineNotification, {
164
+ kind: "error",
165
+ title: getCoreTranslation('error'),
166
+ subtitle: getCoreTranslation('somethingWentWrongTryReloading'),
167
+ hideCloseButton: true
168
+ })
169
+ }) : null, [
145
170
  lifeCycle,
146
171
  openedWorkspace.workspaceName,
147
172
  props,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-styleguide",
3
- "version": "11.0.1-pre.5799",
3
+ "version": "11.0.1-pre.5825",
4
4
  "license": "MPL-2.0",
5
5
  "description": "The styleguide for OpenMRS SPA",
6
6
  "module": "dist/internal.js",
@@ -79,18 +79,18 @@
79
79
  "uuid": "^11.1.1"
80
80
  },
81
81
  "peerDependencies": {
82
- "@openmrs/esm-api": "^11.0.1-pre.5799",
83
- "@openmrs/esm-config": "^11.0.1-pre.5799",
84
- "@openmrs/esm-emr-api": "^11.0.1-pre.5799",
85
- "@openmrs/esm-error-handling": "^11.0.1-pre.5799",
86
- "@openmrs/esm-extensions": "^11.0.1-pre.5799",
87
- "@openmrs/esm-globals": "^11.0.1-pre.5799",
88
- "@openmrs/esm-navigation": "^11.0.1-pre.5799",
89
- "@openmrs/esm-react-utils": "^11.0.1-pre.5799",
90
- "@openmrs/esm-routes": "^11.0.1-pre.5799",
91
- "@openmrs/esm-state": "^11.0.1-pre.5799",
92
- "@openmrs/esm-translations": "^11.0.1-pre.5799",
93
- "@openmrs/esm-utils": "^11.0.1-pre.5799",
82
+ "@openmrs/esm-api": "^11.0.1-pre.5825",
83
+ "@openmrs/esm-config": "^11.0.1-pre.5825",
84
+ "@openmrs/esm-emr-api": "^11.0.1-pre.5825",
85
+ "@openmrs/esm-error-handling": "^11.0.1-pre.5825",
86
+ "@openmrs/esm-extensions": "^11.0.1-pre.5825",
87
+ "@openmrs/esm-globals": "^11.0.1-pre.5825",
88
+ "@openmrs/esm-navigation": "^11.0.1-pre.5825",
89
+ "@openmrs/esm-react-utils": "^11.0.1-pre.5825",
90
+ "@openmrs/esm-routes": "^11.0.1-pre.5825",
91
+ "@openmrs/esm-state": "^11.0.1-pre.5825",
92
+ "@openmrs/esm-translations": "^11.0.1-pre.5825",
93
+ "@openmrs/esm-utils": "^11.0.1-pre.5825",
94
94
  "dayjs": "1.x",
95
95
  "i18next": "25.x",
96
96
  "react": "18.x",
@@ -99,18 +99,18 @@
99
99
  "swr": "2.x"
100
100
  },
101
101
  "devDependencies": {
102
- "@openmrs/esm-api": "11.0.1-pre.5799",
103
- "@openmrs/esm-config": "11.0.1-pre.5799",
104
- "@openmrs/esm-emr-api": "11.0.1-pre.5799",
105
- "@openmrs/esm-error-handling": "11.0.1-pre.5799",
106
- "@openmrs/esm-extensions": "11.0.1-pre.5799",
107
- "@openmrs/esm-globals": "11.0.1-pre.5799",
108
- "@openmrs/esm-navigation": "11.0.1-pre.5799",
109
- "@openmrs/esm-react-utils": "11.0.1-pre.5799",
110
- "@openmrs/esm-routes": "11.0.1-pre.5799",
111
- "@openmrs/esm-state": "11.0.1-pre.5799",
112
- "@openmrs/esm-translations": "11.0.1-pre.5799",
113
- "@openmrs/esm-utils": "11.0.1-pre.5799",
102
+ "@openmrs/esm-api": "11.0.1-pre.5825",
103
+ "@openmrs/esm-config": "11.0.1-pre.5825",
104
+ "@openmrs/esm-emr-api": "11.0.1-pre.5825",
105
+ "@openmrs/esm-error-handling": "11.0.1-pre.5825",
106
+ "@openmrs/esm-extensions": "11.0.1-pre.5825",
107
+ "@openmrs/esm-globals": "11.0.1-pre.5825",
108
+ "@openmrs/esm-navigation": "11.0.1-pre.5825",
109
+ "@openmrs/esm-react-utils": "11.0.1-pre.5825",
110
+ "@openmrs/esm-routes": "11.0.1-pre.5825",
111
+ "@openmrs/esm-state": "11.0.1-pre.5825",
112
+ "@openmrs/esm-translations": "11.0.1-pre.5825",
113
+ "@openmrs/esm-utils": "11.0.1-pre.5825",
114
114
  "@swc/cli": "0.8.1",
115
115
  "@swc/core": "1.15.21",
116
116
  "@types/geopattern": "^1.2.9",
@@ -1,8 +1,15 @@
1
- import React, { type ReactNode, useEffect, useMemo, useState } from 'react';
1
+ import React, { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import Parcel from 'single-spa-react/parcel';
4
4
  import { type ParcelConfig } from 'single-spa';
5
- import { Header, HeaderGlobalAction, HeaderGlobalBar, HeaderName, InlineLoading } from '@carbon/react';
5
+ import {
6
+ Header,
7
+ HeaderGlobalAction,
8
+ HeaderGlobalBar,
9
+ HeaderName,
10
+ InlineLoading,
11
+ InlineNotification,
12
+ } from '@carbon/react';
6
13
  import { DownToBottom, Maximize, Minimize } from '@carbon/react/icons';
7
14
  import {
8
15
  type OpenedWindow,
@@ -12,6 +19,7 @@ import {
12
19
  type WorkspaceStoreState2,
13
20
  } from '@openmrs/esm-extensions';
14
21
  import { isDesktop, useLayoutType, useStore } from '@openmrs/esm-react-utils';
22
+ import { reportError } from '@openmrs/esm-error-handling';
15
23
  import { loadLifeCycles } from '@openmrs/esm-routes';
16
24
  import { getCoreTranslation } from '@openmrs/esm-translations';
17
25
  import { ArrowRightIcon, CloseIcon } from '../icons';
@@ -55,28 +63,39 @@ const ActiveWorkspaceWindow: React.FC<WorkspaceWindowProps> = ({
55
63
  // the previous workspace's component until the reload resolves. The parcel
56
64
  // mounts whatever config it first receives, so the wrong component would then
57
65
  // render with the new workspace's props.
58
- const [lifeCyclesByUuid, setLifeCyclesByUuid] = useState<Record<string, ParcelConfig>>({});
66
+ const [lifeCyclesByUuid, setLifeCyclesByUuid] = useState<Record<string, ParcelConfig | null>>({});
59
67
  // Reactive so the effect retries when a workspace registers after mount.
60
68
  const registeredWorkspacesByName = useStore(workspace2Store, selectRegisteredWorkspacesByName);
69
+ // The effect reloads every workspace whenever any of them changes (for example, its title), so
70
+ // track which workspaces have already reported a load failure to report each one only once.
71
+ const reportedFailureUuids = useRef(new Set<string>());
61
72
 
62
73
  useEffect(() => {
63
74
  let cancelled = false;
64
75
  Promise.all(
65
- openedWorkspaces.map(async (openedWorkspace): Promise<[string, ParcelConfig] | null> => {
76
+ openedWorkspaces.map(async (openedWorkspace): Promise<[string, ParcelConfig | null] | null> => {
66
77
  const workspaceDef = registeredWorkspacesByName[openedWorkspace.workspaceName];
67
78
  if (!workspaceDef) {
68
79
  return null;
69
80
  }
70
81
  const { moduleName, component } = workspaceDef;
71
- return [openedWorkspace.uuid, await loadLifeCycles(moduleName, component)];
82
+ try {
83
+ return [openedWorkspace.uuid, await loadLifeCycles(moduleName, component)];
84
+ } catch (error) {
85
+ if (!cancelled && !reportedFailureUuids.current.has(openedWorkspace.uuid)) {
86
+ reportedFailureUuids.current.add(openedWorkspace.uuid);
87
+ reportError(error);
88
+ }
89
+ return [openedWorkspace.uuid, null];
90
+ }
72
91
  }),
73
92
  ).then((entries) => {
74
93
  if (!cancelled) {
75
94
  setLifeCyclesByUuid(
76
- Object.fromEntries(entries.filter((entry): entry is [string, ParcelConfig] => entry != null)),
95
+ Object.fromEntries(entries.filter((entry): entry is [string, ParcelConfig | null] => entry != null)),
77
96
  );
78
97
  }
79
- });
98
+ }, reportError);
80
99
  return () => {
81
100
  cancelled = true;
82
101
  };
@@ -103,7 +122,7 @@ const ActiveWorkspaceWindow: React.FC<WorkspaceWindowProps> = ({
103
122
  };
104
123
 
105
124
  interface ActiveWorkspaceProps {
106
- lifeCycle: ParcelConfig | undefined;
125
+ lifeCycle: ParcelConfig | null | undefined;
107
126
  openedWorkspace: OpenedWorkspace;
108
127
  openedWindow: OpenedWindow;
109
128
  groupProps: Record<string, any> | null;
@@ -207,6 +226,15 @@ const ActiveWorkspace: React.FC<ActiveWorkspaceProps> = ({
207
226
  wrapClassName={classNames(styles.workspaceContent, { [styles.hiddenExtraWorkspace]: hideBehindLeaf })}
208
227
  {...props}
209
228
  />
229
+ ) : lifeCycle === null ? (
230
+ <div className={classNames(styles.workspaceContent, { [styles.hiddenExtraWorkspace]: hideBehindLeaf })}>
231
+ <InlineNotification
232
+ kind="error"
233
+ title={getCoreTranslation('error')}
234
+ subtitle={getCoreTranslation('somethingWentWrongTryReloading')}
235
+ hideCloseButton
236
+ />
237
+ </div>
210
238
  ) : null,
211
239
  [lifeCycle, openedWorkspace.workspaceName, props, hideBehindLeaf],
212
240
  );
@@ -2,12 +2,15 @@ import React from 'react';
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  import { act, render, screen } from '@testing-library/react';
4
4
  import { workspace2Store, type OpenedWindow, type OpenedWorkspace } from '@openmrs/esm-extensions';
5
+ import { reportError } from '@openmrs/esm-error-handling';
5
6
  import { loadLifeCycles } from '@openmrs/esm-routes';
6
7
  import ActiveWorkspaceWindow from './active-workspace-window.component';
7
8
  import styles from './workspace2.module.scss';
8
9
  import { type Workspace2InternalProps } from './workspace2.component';
9
10
  import { type WorkspaceWindowActions } from './workspace-window-actions';
10
11
 
12
+ vi.mock('@openmrs/esm-error-handling', () => ({ reportError: vi.fn() }));
13
+
11
14
  vi.mock('@openmrs/esm-routes', () => ({
12
15
  loadLifeCycles: vi.fn(),
13
16
  }));
@@ -186,6 +189,82 @@ describe('ActiveWorkspaceWindow', () => {
186
189
  expect(screen.getByTestId('parcel')).toHaveTextContent('admit-lifecycle');
187
190
  });
188
191
 
192
+ it.each([false, true])(
193
+ 'reports failed loading with renderChrome=%s while rendering other workspaces',
194
+ async (renderChrome) => {
195
+ const error = new Error('Module failed to load');
196
+ mockLoadLifeCycles.mockImplementation((_moduleName, component) =>
197
+ component === 'form' ? Promise.reject(error) : Promise.resolve({ name: 'admit-lifecycle' } as never),
198
+ );
199
+ render(
200
+ renderWindow(
201
+ makeOpenedWindow([
202
+ makeOpenedWorkspace('form-workspace', 'uuid-form'),
203
+ makeOpenedWorkspace('admit-workspace', 'uuid-admit'),
204
+ ]),
205
+ makeActions(),
206
+ renderChrome,
207
+ ),
208
+ );
209
+
210
+ await flushLifeCycles();
211
+
212
+ expect(reportError).toHaveBeenCalledWith(error);
213
+ expect(screen.getByText('Something went wrong. Please try reloading.')).toBeInTheDocument();
214
+ expect(screen.getByTestId('parcel')).toHaveTextContent('admit-lifecycle');
215
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
216
+ },
217
+ );
218
+
219
+ it('does not report a failed load for a workspace that has since been replaced', async () => {
220
+ // The form lifecycle rejects only when we release it, after the admit workspace has replaced it.
221
+ let rejectForm: (error: Error) => void;
222
+ mockLoadLifeCycles.mockImplementation((_moduleName, component) => {
223
+ if (component === 'form') {
224
+ return new Promise((_resolve, reject) => {
225
+ rejectForm = reject;
226
+ }) as never;
227
+ }
228
+ return Promise.resolve({ name: 'admit-lifecycle' } as never);
229
+ });
230
+
231
+ const actions = makeActions();
232
+ const { rerender } = render(
233
+ renderWindow(makeOpenedWindow([makeOpenedWorkspace('form-workspace', 'uuid-form')]), actions),
234
+ );
235
+ rerender(renderWindow(makeOpenedWindow([makeOpenedWorkspace('admit-workspace', 'uuid-admit')]), actions));
236
+ await flushLifeCycles();
237
+
238
+ await act(async () => {
239
+ rejectForm(new Error('Module failed to load'));
240
+ await Promise.resolve();
241
+ });
242
+
243
+ expect(reportError).not.toHaveBeenCalled();
244
+ expect(screen.queryByText('Something went wrong. Please try reloading.')).not.toBeInTheDocument();
245
+ expect(screen.getByTestId('parcel')).toHaveTextContent('admit-lifecycle');
246
+ });
247
+
248
+ it('reports a failed load only once when the opened workspaces change', async () => {
249
+ const error = new Error('Module failed to load');
250
+ mockLoadLifeCycles.mockImplementation((_moduleName, component) =>
251
+ component === 'form' ? Promise.reject(error) : Promise.resolve({ name: 'admit-lifecycle' } as never),
252
+ );
253
+ const actions = makeActions();
254
+ const formWorkspace = makeOpenedWorkspace('form-workspace', 'uuid-form');
255
+ const admitWorkspace = makeOpenedWorkspace('admit-workspace', 'uuid-admit');
256
+
257
+ const { rerender } = render(renderWindow(makeOpenedWindow([formWorkspace, admitWorkspace]), actions));
258
+ await flushLifeCycles();
259
+
260
+ // A title change creates a new openedWorkspaces array, which reloads every workspace.
261
+ rerender(renderWindow(makeOpenedWindow([formWorkspace, { ...admitWorkspace, title: 'Admit' }]), actions));
262
+ await flushLifeCycles();
263
+
264
+ expect(mockLoadLifeCycles).toHaveBeenCalledTimes(4);
265
+ expect(reportError).toHaveBeenCalledTimes(1);
266
+ });
267
+
189
268
  it("passes the workspace's registered meta to the parcel via workspaceMeta", async () => {
190
269
  mockLoadLifeCycles.mockResolvedValue({ name: 'form-lifecycle' } as never);
191
270