@openmrs/esm-styleguide 10.0.1-pre.4943 → 10.0.1-pre.4966

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: 123 files, copied 164 files with swc (295.7ms)
1
+ [0] Successfully compiled: 123 files, copied 164 files with swc (211.09ms)
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,KAAuC,MAAM,OAAO,CAAC;AAK5D,OAAO,EAAE,KAAK,YAAY,EAAyC,MAAM,yBAAyB,CAAC;AAOnG,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;CACzB;AACD;;GAEG;AACH,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA6BzD,CAAC;AAyIF,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,KAAuC,MAAM,OAAO,CAAC;AAK5D,OAAO,EAAE,KAAK,YAAY,EAAyC,MAAM,yBAAyB,CAAC;AAOnG,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;CACzB;AACD;;GAEG;AACH,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA4CzD,CAAC;AAyIF,eAAe,qBAAqB,CAAC"}
@@ -13,13 +13,31 @@ import styles from "./workspace2.module.scss";
13
13
  * Renders an opened workspace window.
14
14
  */ const ActiveWorkspaceWindow = ({ openedWindow, showActionMenu })=>{
15
15
  const { openedWorkspaces } = openedWindow;
16
- const [lifeCycles, setLifeCycles] = useState();
16
+ // Keyed by opened workspace uuid rather than stack index. The lifecycles load
17
+ // asynchronously, so when the stack changes (a workspace at some position is
18
+ // replaced by another, for example closing a child workspace and immediately
19
+ // opening a different one), an index-based lookup can hand the new workspace
20
+ // the previous workspace's component until the reload resolves. The parcel
21
+ // mounts whatever config it first receives, so the wrong component would then
22
+ // render with the new workspace's props.
23
+ const [lifeCyclesByUuid, setLifeCyclesByUuid] = useState({});
17
24
  const { registeredWorkspacesByName } = workspace2Store.getState();
18
25
  useEffect(()=>{
19
- Promise.all(openedWorkspaces.map((openedWorkspace)=>{
26
+ let cancelled = false;
27
+ Promise.all(openedWorkspaces.map(async (openedWorkspace)=>{
20
28
  const { moduleName, component } = registeredWorkspacesByName[openedWorkspace.workspaceName];
21
- return loadLifeCycles(moduleName, component);
22
- })).then(setLifeCycles);
29
+ return [
30
+ openedWorkspace.uuid,
31
+ await loadLifeCycles(moduleName, component)
32
+ ];
33
+ })).then((entries)=>{
34
+ if (!cancelled) {
35
+ setLifeCyclesByUuid(Object.fromEntries(entries));
36
+ }
37
+ });
38
+ return ()=>{
39
+ cancelled = true;
40
+ };
23
41
  }, [
24
42
  openedWorkspaces
25
43
  ]);
@@ -27,7 +45,7 @@ import styles from "./workspace2.module.scss";
27
45
  children: openedWorkspaces.map((openedWorkspace, i)=>/*#__PURE__*/ _jsx(ActiveWorkspace, {
28
46
  openedWorkspace: openedWorkspace,
29
47
  openedWindow: openedWindow,
30
- lifeCycle: lifeCycles && lifeCycles[i] ? lifeCycles[i] : undefined,
48
+ lifeCycle: lifeCyclesByUuid[openedWorkspace.uuid],
31
49
  isRootWorkspace: i === 0,
32
50
  isLeafWorkspace: i === openedWorkspaces.length - 1,
33
51
  showActionMenu: showActionMenu
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-styleguide",
3
- "version": "10.0.1-pre.4943",
3
+ "version": "10.0.1-pre.4966",
4
4
  "license": "MPL-2.0",
5
5
  "description": "The styleguide for OpenMRS SPA",
6
6
  "module": "dist/internal.js",
@@ -77,18 +77,18 @@
77
77
  "react-aria-components": "^1.7.1"
78
78
  },
79
79
  "peerDependencies": {
80
- "@openmrs/esm-api": "^10.0.1-pre.4943",
81
- "@openmrs/esm-config": "^10.0.1-pre.4943",
82
- "@openmrs/esm-emr-api": "^10.0.1-pre.4943",
83
- "@openmrs/esm-error-handling": "^10.0.1-pre.4943",
84
- "@openmrs/esm-extensions": "^10.0.1-pre.4943",
85
- "@openmrs/esm-globals": "^10.0.1-pre.4943",
86
- "@openmrs/esm-navigation": "^10.0.1-pre.4943",
87
- "@openmrs/esm-react-utils": "^10.0.1-pre.4943",
88
- "@openmrs/esm-routes": "^10.0.1-pre.4943",
89
- "@openmrs/esm-state": "^10.0.1-pre.4943",
90
- "@openmrs/esm-translations": "^10.0.1-pre.4943",
91
- "@openmrs/esm-utils": "^10.0.1-pre.4943",
80
+ "@openmrs/esm-api": "^10.0.1-pre.4966",
81
+ "@openmrs/esm-config": "^10.0.1-pre.4966",
82
+ "@openmrs/esm-emr-api": "^10.0.1-pre.4966",
83
+ "@openmrs/esm-error-handling": "^10.0.1-pre.4966",
84
+ "@openmrs/esm-extensions": "^10.0.1-pre.4966",
85
+ "@openmrs/esm-globals": "^10.0.1-pre.4966",
86
+ "@openmrs/esm-navigation": "^10.0.1-pre.4966",
87
+ "@openmrs/esm-react-utils": "^10.0.1-pre.4966",
88
+ "@openmrs/esm-routes": "^10.0.1-pre.4966",
89
+ "@openmrs/esm-state": "^10.0.1-pre.4966",
90
+ "@openmrs/esm-translations": "^10.0.1-pre.4966",
91
+ "@openmrs/esm-utils": "^10.0.1-pre.4966",
92
92
  "dayjs": "1.x",
93
93
  "i18next": "25.x",
94
94
  "react": "18.x",
@@ -98,18 +98,18 @@
98
98
  "swr": "2.x"
99
99
  },
100
100
  "devDependencies": {
101
- "@openmrs/esm-api": "10.0.1-pre.4943",
102
- "@openmrs/esm-config": "10.0.1-pre.4943",
103
- "@openmrs/esm-emr-api": "10.0.1-pre.4943",
104
- "@openmrs/esm-error-handling": "10.0.1-pre.4943",
105
- "@openmrs/esm-extensions": "10.0.1-pre.4943",
106
- "@openmrs/esm-globals": "10.0.1-pre.4943",
107
- "@openmrs/esm-navigation": "10.0.1-pre.4943",
108
- "@openmrs/esm-react-utils": "10.0.1-pre.4943",
109
- "@openmrs/esm-routes": "10.0.1-pre.4943",
110
- "@openmrs/esm-state": "10.0.1-pre.4943",
111
- "@openmrs/esm-translations": "10.0.1-pre.4943",
112
- "@openmrs/esm-utils": "10.0.1-pre.4943",
101
+ "@openmrs/esm-api": "10.0.1-pre.4966",
102
+ "@openmrs/esm-config": "10.0.1-pre.4966",
103
+ "@openmrs/esm-emr-api": "10.0.1-pre.4966",
104
+ "@openmrs/esm-error-handling": "10.0.1-pre.4966",
105
+ "@openmrs/esm-extensions": "10.0.1-pre.4966",
106
+ "@openmrs/esm-globals": "10.0.1-pre.4966",
107
+ "@openmrs/esm-navigation": "10.0.1-pre.4966",
108
+ "@openmrs/esm-react-utils": "10.0.1-pre.4966",
109
+ "@openmrs/esm-routes": "10.0.1-pre.4966",
110
+ "@openmrs/esm-state": "10.0.1-pre.4966",
111
+ "@openmrs/esm-translations": "10.0.1-pre.4966",
112
+ "@openmrs/esm-utils": "10.0.1-pre.4966",
113
113
  "@swc/cli": "0.8.1",
114
114
  "@swc/core": "1.15.21",
115
115
  "@types/geopattern": "^1.2.9",
@@ -19,16 +19,31 @@ interface WorkspaceWindowProps {
19
19
  */
20
20
  const ActiveWorkspaceWindow: React.FC<WorkspaceWindowProps> = ({ openedWindow, showActionMenu }) => {
21
21
  const { openedWorkspaces } = openedWindow;
22
- const [lifeCycles, setLifeCycles] = useState<ParcelConfig[]>();
22
+ // Keyed by opened workspace uuid rather than stack index. The lifecycles load
23
+ // asynchronously, so when the stack changes (a workspace at some position is
24
+ // replaced by another, for example closing a child workspace and immediately
25
+ // opening a different one), an index-based lookup can hand the new workspace
26
+ // the previous workspace's component until the reload resolves. The parcel
27
+ // mounts whatever config it first receives, so the wrong component would then
28
+ // render with the new workspace's props.
29
+ const [lifeCyclesByUuid, setLifeCyclesByUuid] = useState<Record<string, ParcelConfig>>({});
23
30
  const { registeredWorkspacesByName } = workspace2Store.getState();
24
31
 
25
32
  useEffect(() => {
33
+ let cancelled = false;
26
34
  Promise.all(
27
- openedWorkspaces.map((openedWorkspace) => {
35
+ openedWorkspaces.map(async (openedWorkspace): Promise<[string, ParcelConfig]> => {
28
36
  const { moduleName, component } = registeredWorkspacesByName[openedWorkspace.workspaceName];
29
- return loadLifeCycles(moduleName, component);
37
+ return [openedWorkspace.uuid, await loadLifeCycles(moduleName, component)];
30
38
  }),
31
- ).then(setLifeCycles);
39
+ ).then((entries) => {
40
+ if (!cancelled) {
41
+ setLifeCyclesByUuid(Object.fromEntries(entries));
42
+ }
43
+ });
44
+ return () => {
45
+ cancelled = true;
46
+ };
32
47
  }, [openedWorkspaces]);
33
48
 
34
49
  return (
@@ -38,7 +53,7 @@ const ActiveWorkspaceWindow: React.FC<WorkspaceWindowProps> = ({ openedWindow, s
38
53
  key={openedWorkspace.uuid}
39
54
  openedWorkspace={openedWorkspace}
40
55
  openedWindow={openedWindow}
41
- lifeCycle={lifeCycles && lifeCycles[i] ? lifeCycles[i] : undefined}
56
+ lifeCycle={lifeCyclesByUuid[openedWorkspace.uuid]}
42
57
  isRootWorkspace={i === 0}
43
58
  isLeafWorkspace={i === openedWorkspaces.length - 1}
44
59
  showActionMenu={showActionMenu}
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { act, render, screen } from '@testing-library/react';
4
+ import { workspace2Store, type OpenedWindow, type OpenedWorkspace } from '@openmrs/esm-extensions';
5
+ import { loadLifeCycles } from '@openmrs/esm-routes';
6
+ import ActiveWorkspaceWindow from './active-workspace-window.component';
7
+
8
+ vi.mock('@openmrs/esm-routes', () => ({
9
+ loadLifeCycles: vi.fn(),
10
+ }));
11
+
12
+ vi.mock('single-spa-react/parcel', () => ({
13
+ default: ({ config }: { config: { name: string } }) => <div data-testid="parcel">{config.name}</div>,
14
+ }));
15
+
16
+ const mockLoadLifeCycles = vi.mocked(loadLifeCycles);
17
+
18
+ function makeOpenedWorkspace(workspaceName: string, uuid: string): OpenedWorkspace {
19
+ return { workspaceName, uuid, props: {}, hasUnsavedChanges: false } as OpenedWorkspace;
20
+ }
21
+
22
+ function makeOpenedWindow(openedWorkspaces: Array<OpenedWorkspace>): OpenedWindow {
23
+ return {
24
+ windowName: 'test-window',
25
+ openedWorkspaces,
26
+ props: {},
27
+ maximized: false,
28
+ } as OpenedWindow;
29
+ }
30
+
31
+ describe('ActiveWorkspaceWindow', () => {
32
+ beforeEach(() => {
33
+ workspace2Store.setState({
34
+ registeredWorkspacesByName: {
35
+ 'form-workspace': {
36
+ name: 'form-workspace',
37
+ component: 'form',
38
+ window: 'test-window',
39
+ moduleName: 'test-module',
40
+ },
41
+ 'admit-workspace': {
42
+ name: 'admit-workspace',
43
+ component: 'admit',
44
+ window: 'test-window',
45
+ moduleName: 'test-module',
46
+ },
47
+ },
48
+ registeredWindowsByName: {
49
+ 'test-window': { name: 'test-window', group: 'test-group' },
50
+ },
51
+ workspaceTitleByWorkspaceName: {},
52
+ openedGroup: null,
53
+ } as never);
54
+ });
55
+
56
+ afterEach(() => {
57
+ vi.clearAllMocks();
58
+ });
59
+
60
+ it('does not render a replaced workspace with the previous workspace lifecycle at the same stack position', async () => {
61
+ // The form lifecycle resolves immediately. The admit lifecycle resolves only
62
+ // when we release it, keeping the reload from the stack change pending, which
63
+ // is the window where an index-based lookup served the stale component.
64
+ let resolveAdmit: (config: { name: string }) => void;
65
+ mockLoadLifeCycles.mockImplementation((moduleName, component) => {
66
+ if (component === 'form') {
67
+ return Promise.resolve({ name: 'form-lifecycle' } as never);
68
+ }
69
+ return new Promise((resolve) => {
70
+ resolveAdmit = resolve as never;
71
+ }) as never;
72
+ });
73
+
74
+ const formWorkspace = makeOpenedWorkspace('form-workspace', 'uuid-form');
75
+ const { rerender } = render(
76
+ <ActiveWorkspaceWindow openedWindow={makeOpenedWindow([formWorkspace])} showActionMenu={false} />,
77
+ );
78
+
79
+ await act(async () => {
80
+ await Promise.resolve();
81
+ });
82
+ expect(screen.getByTestId('parcel')).toHaveTextContent('form-lifecycle');
83
+
84
+ // Replace the workspace at index 0, as happens when a workspace is closed and
85
+ // another is opened in quick succession.
86
+ const admitWorkspace = makeOpenedWorkspace('admit-workspace', 'uuid-admit');
87
+ rerender(<ActiveWorkspaceWindow openedWindow={makeOpenedWindow([admitWorkspace])} showActionMenu={false} />);
88
+
89
+ // While the admit lifecycle is still loading, the stale form lifecycle must not
90
+ // be mounted for the admit workspace.
91
+ expect(screen.queryByTestId('parcel')).not.toBeInTheDocument();
92
+
93
+ await act(async () => {
94
+ resolveAdmit({ name: 'admit-lifecycle' });
95
+ await Promise.resolve();
96
+ });
97
+ expect(screen.getByTestId('parcel')).toHaveTextContent('admit-lifecycle');
98
+ });
99
+ });