@peers-app/peers-ui 0.25.20 → 0.25.21
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.
|
@@ -167,7 +167,7 @@ const UIRouter = (props) => {
|
|
|
167
167
|
if (!matchingRoute) {
|
|
168
168
|
return undefined;
|
|
169
169
|
}
|
|
170
|
-
return UILoader
|
|
170
|
+
return (0, jsx_runtime_1.jsx)(UILoader, { peersUIId: matchingRoute.peersUIId, props: props.props });
|
|
171
171
|
};
|
|
172
172
|
exports.UIRouter = UIRouter;
|
|
173
173
|
const UILoader = (args) => {
|
|
@@ -177,22 +177,9 @@ const UILoader = (args) => {
|
|
|
177
177
|
const { props, peersUIId } = args;
|
|
178
178
|
const [lastReloadTime] = (0, hooks_1.useObservable)(globals_1.packageReloaded);
|
|
179
179
|
props.__lastPackageReload = lastReloadTime;
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const parseResult = peersUI.propsSchema.safeParse(props);
|
|
184
|
-
if (!parseResult.success) {
|
|
185
|
-
console.error("props did not match schema", {
|
|
186
|
-
props,
|
|
187
|
-
schema: peersUI.propsSchema,
|
|
188
|
-
errors: parseResult.error,
|
|
189
|
-
});
|
|
190
|
-
return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsxs)("pre", { children: ["Peers UI could not be loaded ", (0, jsx_runtime_1.jsx)("br", {}), "peersUIId: ", peersUI.peersUIId, " ", (0, jsx_runtime_1.jsx)("br", {}), "Component name: ", peersUI.content?.name, " ", (0, jsx_runtime_1.jsx)("br", {}), "Props did not match schema ", (0, jsx_runtime_1.jsx)("br", {}), "Errors: ", JSON.stringify(parseResult.error, null, 2), " ", (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("br", {}), "Props ", JSON.stringify((0, peers_sdk_1.toJSON)(props), null, 2), (0, jsx_runtime_1.jsx)("br", {})] }) }));
|
|
191
|
-
}
|
|
192
|
-
const Component = peersUI.content;
|
|
193
|
-
return ((0, jsx_runtime_1.jsx)(UIErrorBoundary, { peersUIId: peersUIId, componentName: peersUI.content?.name, children: (0, jsx_runtime_1.jsx)(Component, { ...args.props }) }));
|
|
194
|
-
}
|
|
195
|
-
return (0, jsx_runtime_1.jsx)(UIAsyncLoader, { peersUIId: peersUIId, props: props });
|
|
180
|
+
// Keep the same loader ancestor after the bundle enters the cache. Replacing it
|
|
181
|
+
// on a path change would unmount the screen before its navigation guard can run.
|
|
182
|
+
return (0, jsx_runtime_1.jsx)(UIAsyncLoader, { peersUIId: peersUIId, props: props }, peersUIId);
|
|
196
183
|
};
|
|
197
184
|
const uiLoadingPromises = {};
|
|
198
185
|
// Check if we're running in a React Native WebView (has injectUIBundle available)
|
|
@@ -273,6 +260,7 @@ function loadUIBundle(pkg, forceRefresh) {
|
|
|
273
260
|
}
|
|
274
261
|
const UIAsyncLoader = (props) => {
|
|
275
262
|
const { peersUIId } = props;
|
|
263
|
+
const peersUI = peersUIs[peersUIId];
|
|
276
264
|
const [loaded, setLoaded] = react_1.default.useState(false);
|
|
277
265
|
const [loadError, setLoadError] = react_1.default.useState(null);
|
|
278
266
|
const allRoutes = _window.getPeersUIRoutes();
|
|
@@ -280,7 +268,7 @@ const UIAsyncLoader = (props) => {
|
|
|
280
268
|
(0, hooks_1.useObservable)(routes_loader_1.allPackages);
|
|
281
269
|
const pkg = (0, routes_loader_1.allPackages)().find((p) => p.packageId === uiPackageId);
|
|
282
270
|
(0, react_1.useEffect)(() => {
|
|
283
|
-
if (!pkg)
|
|
271
|
+
if (!pkg || peersUIs[peersUIId]?.content)
|
|
284
272
|
return;
|
|
285
273
|
const pkgRef = pkg;
|
|
286
274
|
setLoadError(null);
|
|
@@ -290,11 +278,11 @@ const UIAsyncLoader = (props) => {
|
|
|
290
278
|
console.error(`Failed to load UI bundle for package ${pkgRef.name}:`, error);
|
|
291
279
|
setLoadError(error);
|
|
292
280
|
});
|
|
293
|
-
}, [pkg?.packageId, pkg?.name, pkg]);
|
|
294
|
-
if (!pkg) {
|
|
281
|
+
}, [peersUIId, pkg?.packageId, pkg?.name, pkg]);
|
|
282
|
+
if (!peersUI?.content && !pkg) {
|
|
295
283
|
return (0, jsx_runtime_1.jsxs)("div", { children: ["Could not find package that owns peersUIId ", peersUIId] });
|
|
296
284
|
}
|
|
297
|
-
if (loadError) {
|
|
285
|
+
if (!peersUI?.content && loadError) {
|
|
298
286
|
return ((0, jsx_runtime_1.jsxs)("div", { style: {
|
|
299
287
|
padding: "20px",
|
|
300
288
|
border: "2px solid #ff6b6b",
|
|
@@ -303,7 +291,7 @@ const UIAsyncLoader = (props) => {
|
|
|
303
291
|
color: "#721c24",
|
|
304
292
|
fontFamily: "monospace",
|
|
305
293
|
margin: "10px 0",
|
|
306
|
-
}, children: [(0, jsx_runtime_1.jsx)("h3", { style: { color: "#c53030", marginTop: 0 }, children: "Bundle Loading Error" }), (0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Peers UI ID:" }), " ", peersUIId] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package:" }), " ", pkg
|
|
294
|
+
}, children: [(0, jsx_runtime_1.jsx)("h3", { style: { color: "#c53030", marginTop: 0 }, children: "Bundle Loading Error" }), (0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Peers UI ID:" }), " ", peersUIId] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package:" }), " ", pkg?.name] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package ID:" }), " ", pkg?.packageId] })] }), (0, jsx_runtime_1.jsxs)("details", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsx)("summary", { style: { cursor: "pointer", fontWeight: "bold", marginBottom: "10px" }, children: "Error Details" }), (0, jsx_runtime_1.jsxs)("div", { style: {
|
|
307
295
|
backgroundColor: "#fed7d7",
|
|
308
296
|
padding: "10px",
|
|
309
297
|
borderRadius: "4px",
|
|
@@ -328,16 +316,15 @@ const UIAsyncLoader = (props) => {
|
|
|
328
316
|
cursor: "pointer",
|
|
329
317
|
}, children: "Retry Loading" })] }));
|
|
330
318
|
}
|
|
331
|
-
if (!loaded) {
|
|
319
|
+
if (!peersUI?.content && !loaded) {
|
|
332
320
|
return (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {});
|
|
333
321
|
}
|
|
334
|
-
const peersUI = peersUIs[peersUIId];
|
|
335
322
|
if (peersUI?.content) {
|
|
336
|
-
const _props =
|
|
323
|
+
const _props = props.props;
|
|
337
324
|
try {
|
|
338
325
|
peersUI.propsSchema.parse(_props);
|
|
339
326
|
const Component = peersUI.content;
|
|
340
|
-
return ((0, jsx_runtime_1.jsx)(UIErrorBoundary, { peersUIId: peersUIId, packageName: pkg
|
|
327
|
+
return ((0, jsx_runtime_1.jsx)(UIErrorBoundary, { peersUIId: peersUIId, packageName: pkg?.name, componentName: peersUI.content?.name, children: (0, jsx_runtime_1.jsx)(Component, { ..._props }) }));
|
|
341
328
|
}
|
|
342
329
|
catch (parseError) {
|
|
343
330
|
return ((0, jsx_runtime_1.jsxs)("div", { style: {
|
|
@@ -348,7 +335,7 @@ const UIAsyncLoader = (props) => {
|
|
|
348
335
|
color: "#721c24",
|
|
349
336
|
fontFamily: "monospace",
|
|
350
337
|
margin: "10px 0",
|
|
351
|
-
}, children: [(0, jsx_runtime_1.jsx)("h3", { style: { color: "#c53030", marginTop: 0 }, children: "Props Validation Error" }), (0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Peers UI ID:" }), " ", peersUIId] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package:" }), " ", pkg
|
|
338
|
+
}, children: [(0, jsx_runtime_1.jsx)("h3", { style: { color: "#c53030", marginTop: 0 }, children: "Props Validation Error" }), (0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Peers UI ID:" }), " ", peersUIId] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package:" }), " ", pkg?.name] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Component:" }), " ", peersUI.content?.name] })] }), (0, jsx_runtime_1.jsxs)("div", { style: {
|
|
352
339
|
backgroundColor: "#fed7d7",
|
|
353
340
|
padding: "10px",
|
|
354
341
|
borderRadius: "4px",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
const react_1 = require("@testing-library/react");
|
|
5
|
+
const react_2 = require("react");
|
|
6
|
+
const mockPackages = [];
|
|
7
|
+
const mockGetFileContents = jest.fn();
|
|
8
|
+
jest.mock("@peers-app/peers-sdk", () => ({
|
|
9
|
+
debounceByArgs: (fn) => fn,
|
|
10
|
+
getUserContext: () => new Promise(() => { }),
|
|
11
|
+
packagePrefsVar: () => Object.assign(() => ({}), { loadingPromise: Promise.resolve() }),
|
|
12
|
+
PackageVersions: () => ({ get: async () => ({ uiBundleFileId: "bundle" }) }),
|
|
13
|
+
rpcServerCalls: { getFileContents: (...args) => mockGetFileContents(...args) },
|
|
14
|
+
}));
|
|
15
|
+
jest.mock("../globals", () => ({ mainContentPath: () => "", packageReloaded: () => 0 }));
|
|
16
|
+
jest.mock("../hooks", () => ({ useObservable: (value) => [value()] }));
|
|
17
|
+
jest.mock("../ui-defaults", () => ({}));
|
|
18
|
+
jest.mock("../components/loading-indicator", () => ({
|
|
19
|
+
LoadingIndicator: () => (0, jsx_runtime_1.jsx)("p", { children: "Loading bundle" }),
|
|
20
|
+
}));
|
|
21
|
+
jest.mock("./routes-loader", () => ({ allPackages: () => mockPackages }));
|
|
22
|
+
const ui_loader_1 = require("./ui-loader");
|
|
23
|
+
test.each([
|
|
24
|
+
false,
|
|
25
|
+
true,
|
|
26
|
+
])("query changes retain screen state and props (preloaded: %s)", async (preloaded) => {
|
|
27
|
+
const packageId = `package-${preloaded}`;
|
|
28
|
+
const peersUIId = `ui-${preloaded}`;
|
|
29
|
+
const base = `package-nav/${packageId}/app`;
|
|
30
|
+
let path = `${base}?jobId=first`;
|
|
31
|
+
const listeners = new Set();
|
|
32
|
+
let mounts = 0;
|
|
33
|
+
let unmounts = 0;
|
|
34
|
+
function Content({ caller }) {
|
|
35
|
+
const [draft, setDraft] = (0, react_2.useState)("");
|
|
36
|
+
const currentPath = (0, react_2.useSyncExternalStore)((listener) => {
|
|
37
|
+
listeners.add(listener);
|
|
38
|
+
return () => {
|
|
39
|
+
listeners.delete(listener);
|
|
40
|
+
};
|
|
41
|
+
}, () => path);
|
|
42
|
+
(0, react_2.useEffect)(() => {
|
|
43
|
+
mounts++;
|
|
44
|
+
return () => {
|
|
45
|
+
unmounts++;
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { children: caller }), (0, jsx_runtime_1.jsx)("p", { children: currentPath }), (0, jsx_runtime_1.jsx)("input", { "aria-label": "Draft", value: draft, onChange: (event) => setDraft(event.target.value) })] }));
|
|
49
|
+
}
|
|
50
|
+
const schema = { parse: (value) => value, safeParse: () => ({ success: true }) };
|
|
51
|
+
const ui = { peersUIId, content: Content, propsSchema: schema };
|
|
52
|
+
const routeWindow = window;
|
|
53
|
+
routeWindow.registerPeersUIRoute({ packageId, peersUIId, path: base });
|
|
54
|
+
mockPackages.length = 0;
|
|
55
|
+
mockGetFileContents.mockReset();
|
|
56
|
+
let releaseBundle;
|
|
57
|
+
if (preloaded) {
|
|
58
|
+
await (0, ui_loader_1.registerInternalPeersUI)({ component: Content, peersUIId, propsSchema: schema });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
mockPackages.push({ packageId, name: "Test package", activePackageVersionId: "version" });
|
|
62
|
+
routeWindow.testUI = ui;
|
|
63
|
+
mockGetFileContents.mockImplementation(() => new Promise((resolve) => {
|
|
64
|
+
releaseBundle = resolve;
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(ui_loader_1.UIRouter, { path: path, props: { caller: "Caller props" } }));
|
|
68
|
+
if (!preloaded) {
|
|
69
|
+
expect(await react_1.screen.findByText("Loading bundle")).toBeInTheDocument();
|
|
70
|
+
await (0, react_1.act)(async () => {
|
|
71
|
+
releaseBundle("exportUIs({uis: [window.testUI]});");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
expect(await react_1.screen.findByText("Caller props")).toBeInTheDocument();
|
|
75
|
+
react_1.fireEvent.change(react_1.screen.getByLabelText("Draft"), { target: { value: "Unsaved edits" } });
|
|
76
|
+
await (0, react_1.act)(async () => {
|
|
77
|
+
path = `${base}?jobId=second`;
|
|
78
|
+
for (const listener of listeners)
|
|
79
|
+
listener();
|
|
80
|
+
view.rerender((0, jsx_runtime_1.jsx)(ui_loader_1.UIRouter, { path: path, props: { caller: "Caller props" } }));
|
|
81
|
+
});
|
|
82
|
+
expect(react_1.screen.getByLabelText("Draft")).toHaveValue("Unsaved edits");
|
|
83
|
+
expect(react_1.screen.getByText(path)).toBeInTheDocument();
|
|
84
|
+
expect({ mounts, unmounts, loads: mockGetFileContents.mock.calls.length }).toEqual({
|
|
85
|
+
mounts: 1,
|
|
86
|
+
unmounts: 0,
|
|
87
|
+
loads: preloaded ? 0 : 1,
|
|
88
|
+
});
|
|
89
|
+
view.unmount();
|
|
90
|
+
delete routeWindow.testUI;
|
|
91
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.21",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"lint:fix": "biome check --write ."
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@peers-app/peers-sdk": "^0.25.
|
|
33
|
+
"@peers-app/peers-sdk": "^0.25.21",
|
|
34
34
|
"bootstrap": "^5.3.3",
|
|
35
35
|
"react": "^18.0.0",
|
|
36
36
|
"react-dom": "^18.0.0"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@babel/preset-react": "^7.24.1",
|
|
42
42
|
"@babel/preset-typescript": "^7.27.1",
|
|
43
43
|
"@biomejs/biome": "2.3.11",
|
|
44
|
-
"@peers-app/peers-sdk": "0.25.
|
|
44
|
+
"@peers-app/peers-sdk": "0.25.21",
|
|
45
45
|
"@testing-library/dom": "^10.4.0",
|
|
46
46
|
"@testing-library/jest-dom": "^6.6.3",
|
|
47
47
|
"@testing-library/react": "^16.3.0",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
3
|
+
|
|
4
|
+
const mockPackages: Record<string, unknown>[] = [];
|
|
5
|
+
const mockGetFileContents = jest.fn();
|
|
6
|
+
jest.mock("@peers-app/peers-sdk", () => ({
|
|
7
|
+
debounceByArgs: (fn: unknown) => fn,
|
|
8
|
+
getUserContext: () => new Promise(() => {}),
|
|
9
|
+
packagePrefsVar: () => Object.assign(() => ({}), { loadingPromise: Promise.resolve() }),
|
|
10
|
+
PackageVersions: () => ({ get: async () => ({ uiBundleFileId: "bundle" }) }),
|
|
11
|
+
rpcServerCalls: { getFileContents: (...args: unknown[]) => mockGetFileContents(...args) },
|
|
12
|
+
}));
|
|
13
|
+
jest.mock("../globals", () => ({ mainContentPath: () => "", packageReloaded: () => 0 }));
|
|
14
|
+
jest.mock("../hooks", () => ({ useObservable: (value: () => unknown) => [value()] }));
|
|
15
|
+
jest.mock("../ui-defaults", () => ({}));
|
|
16
|
+
jest.mock("../components/loading-indicator", () => ({
|
|
17
|
+
LoadingIndicator: () => <p>Loading bundle</p>,
|
|
18
|
+
}));
|
|
19
|
+
jest.mock("./routes-loader", () => ({ allPackages: () => mockPackages }));
|
|
20
|
+
|
|
21
|
+
import { registerInternalPeersUI, UIRouter } from "./ui-loader";
|
|
22
|
+
|
|
23
|
+
test.each([
|
|
24
|
+
false,
|
|
25
|
+
true,
|
|
26
|
+
])("query changes retain screen state and props (preloaded: %s)", async (preloaded) => {
|
|
27
|
+
const packageId = `package-${preloaded}`;
|
|
28
|
+
const peersUIId = `ui-${preloaded}`;
|
|
29
|
+
const base = `package-nav/${packageId}/app`;
|
|
30
|
+
let path = `${base}?jobId=first`;
|
|
31
|
+
const listeners = new Set<() => void>();
|
|
32
|
+
let mounts = 0;
|
|
33
|
+
let unmounts = 0;
|
|
34
|
+
function Content({ caller }: { caller: string }) {
|
|
35
|
+
const [draft, setDraft] = useState("");
|
|
36
|
+
const currentPath = useSyncExternalStore(
|
|
37
|
+
(listener) => {
|
|
38
|
+
listeners.add(listener);
|
|
39
|
+
return () => {
|
|
40
|
+
listeners.delete(listener);
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
() => path,
|
|
44
|
+
);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
mounts++;
|
|
47
|
+
return () => {
|
|
48
|
+
unmounts++;
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<p>{caller}</p>
|
|
54
|
+
<p>{currentPath}</p>
|
|
55
|
+
<input
|
|
56
|
+
aria-label="Draft"
|
|
57
|
+
value={draft}
|
|
58
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
59
|
+
/>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const schema = { parse: (value: unknown) => value, safeParse: () => ({ success: true }) };
|
|
64
|
+
const ui = { peersUIId, content: Content, propsSchema: schema };
|
|
65
|
+
const routeWindow = window as unknown as {
|
|
66
|
+
registerPeersUIRoute: (route: object) => void;
|
|
67
|
+
testUI?: object;
|
|
68
|
+
};
|
|
69
|
+
routeWindow.registerPeersUIRoute({ packageId, peersUIId, path: base });
|
|
70
|
+
mockPackages.length = 0;
|
|
71
|
+
mockGetFileContents.mockReset();
|
|
72
|
+
let releaseBundle!: (code: string) => void;
|
|
73
|
+
if (preloaded) {
|
|
74
|
+
await registerInternalPeersUI({ component: Content, peersUIId, propsSchema: schema });
|
|
75
|
+
} else {
|
|
76
|
+
mockPackages.push({ packageId, name: "Test package", activePackageVersionId: "version" });
|
|
77
|
+
routeWindow.testUI = ui;
|
|
78
|
+
mockGetFileContents.mockImplementation(
|
|
79
|
+
() =>
|
|
80
|
+
new Promise<string>((resolve) => {
|
|
81
|
+
releaseBundle = resolve;
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const view = render(<UIRouter path={path} props={{ caller: "Caller props" }} />);
|
|
86
|
+
if (!preloaded) {
|
|
87
|
+
expect(await screen.findByText("Loading bundle")).toBeInTheDocument();
|
|
88
|
+
await act(async () => {
|
|
89
|
+
releaseBundle("exportUIs({uis: [window.testUI]});");
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
expect(await screen.findByText("Caller props")).toBeInTheDocument();
|
|
93
|
+
fireEvent.change(screen.getByLabelText("Draft"), { target: { value: "Unsaved edits" } });
|
|
94
|
+
await act(async () => {
|
|
95
|
+
path = `${base}?jobId=second`;
|
|
96
|
+
for (const listener of listeners) listener();
|
|
97
|
+
view.rerender(<UIRouter path={path} props={{ caller: "Caller props" }} />);
|
|
98
|
+
});
|
|
99
|
+
expect(screen.getByLabelText("Draft")).toHaveValue("Unsaved edits");
|
|
100
|
+
expect(screen.getByText(path)).toBeInTheDocument();
|
|
101
|
+
expect({ mounts, unmounts, loads: mockGetFileContents.mock.calls.length }).toEqual({
|
|
102
|
+
mounts: 1,
|
|
103
|
+
unmounts: 0,
|
|
104
|
+
loads: preloaded ? 0 : 1,
|
|
105
|
+
});
|
|
106
|
+
view.unmount();
|
|
107
|
+
delete routeWindow.testUI;
|
|
108
|
+
});
|
|
@@ -239,7 +239,7 @@ export const UIRouter = (props: IProps) => {
|
|
|
239
239
|
return undefined;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
return UILoader
|
|
242
|
+
return <UILoader peersUIId={matchingRoute.peersUIId} props={props.props} />;
|
|
243
243
|
};
|
|
244
244
|
|
|
245
245
|
const UILoader = (args: { peersUIId: string; props?: Record<string, unknown> }) => {
|
|
@@ -249,40 +249,9 @@ const UILoader = (args: { peersUIId: string; props?: Record<string, unknown> })
|
|
|
249
249
|
const { props, peersUIId } = args;
|
|
250
250
|
const [lastReloadTime] = useObservable(packageReloaded);
|
|
251
251
|
props.__lastPackageReload = lastReloadTime;
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const parseResult = peersUI.propsSchema.safeParse(props);
|
|
256
|
-
if (!parseResult.success) {
|
|
257
|
-
console.error("props did not match schema", {
|
|
258
|
-
props,
|
|
259
|
-
schema: peersUI.propsSchema,
|
|
260
|
-
errors: parseResult.error,
|
|
261
|
-
});
|
|
262
|
-
return (
|
|
263
|
-
<div>
|
|
264
|
-
<pre>
|
|
265
|
-
Peers UI could not be loaded <br />
|
|
266
|
-
peersUIId: {peersUI.peersUIId} <br />
|
|
267
|
-
Component name: {peersUI.content?.name} <br />
|
|
268
|
-
Props did not match schema <br />
|
|
269
|
-
Errors: {JSON.stringify(parseResult.error, null, 2)} <br />
|
|
270
|
-
<br />
|
|
271
|
-
Props {JSON.stringify(toJSON(props), null, 2)}
|
|
272
|
-
<br />
|
|
273
|
-
</pre>
|
|
274
|
-
</div>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
const Component = peersUI.content as React.ComponentType<Record<string, unknown>>;
|
|
278
|
-
return (
|
|
279
|
-
<UIErrorBoundary peersUIId={peersUIId} componentName={peersUI.content?.name}>
|
|
280
|
-
<Component {...args.props} />
|
|
281
|
-
</UIErrorBoundary>
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return <UIAsyncLoader peersUIId={peersUIId} props={props} />;
|
|
252
|
+
// Keep the same loader ancestor after the bundle enters the cache. Replacing it
|
|
253
|
+
// on a path change would unmount the screen before its navigation guard can run.
|
|
254
|
+
return <UIAsyncLoader key={peersUIId} peersUIId={peersUIId} props={props} />;
|
|
286
255
|
};
|
|
287
256
|
|
|
288
257
|
const uiLoadingPromises: Record<string, Promise<void>> = {};
|
|
@@ -379,6 +348,7 @@ function loadUIBundle(pkg: IPackage, forceRefresh?: boolean) {
|
|
|
379
348
|
|
|
380
349
|
const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown> }) => {
|
|
381
350
|
const { peersUIId } = props;
|
|
351
|
+
const peersUI = peersUIs[peersUIId];
|
|
382
352
|
const [loaded, setLoaded] = React.useState(false);
|
|
383
353
|
const [loadError, setLoadError] = React.useState<Error | null>(null);
|
|
384
354
|
|
|
@@ -389,7 +359,7 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
|
|
|
389
359
|
const pkg = allPackages().find((p) => p.packageId === uiPackageId);
|
|
390
360
|
|
|
391
361
|
useEffect(() => {
|
|
392
|
-
if (!pkg) return;
|
|
362
|
+
if (!pkg || peersUIs[peersUIId]?.content) return;
|
|
393
363
|
const pkgRef = pkg;
|
|
394
364
|
setLoadError(null);
|
|
395
365
|
loadUIBundle(pkgRef)
|
|
@@ -398,13 +368,13 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
|
|
|
398
368
|
console.error(`Failed to load UI bundle for package ${pkgRef.name}:`, error);
|
|
399
369
|
setLoadError(error);
|
|
400
370
|
});
|
|
401
|
-
}, [pkg?.packageId, pkg?.name, pkg]);
|
|
371
|
+
}, [peersUIId, pkg?.packageId, pkg?.name, pkg]);
|
|
402
372
|
|
|
403
|
-
if (!pkg) {
|
|
373
|
+
if (!peersUI?.content && !pkg) {
|
|
404
374
|
return <div>Could not find package that owns peersUIId {peersUIId}</div>;
|
|
405
375
|
}
|
|
406
376
|
|
|
407
|
-
if (loadError) {
|
|
377
|
+
if (!peersUI?.content && loadError) {
|
|
408
378
|
return (
|
|
409
379
|
<div
|
|
410
380
|
style={{
|
|
@@ -423,10 +393,10 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
|
|
|
423
393
|
<strong>Peers UI ID:</strong> {peersUIId}
|
|
424
394
|
</div>
|
|
425
395
|
<div>
|
|
426
|
-
<strong>Package:</strong> {pkg
|
|
396
|
+
<strong>Package:</strong> {pkg?.name}
|
|
427
397
|
</div>
|
|
428
398
|
<div>
|
|
429
|
-
<strong>Package ID:</strong> {pkg
|
|
399
|
+
<strong>Package ID:</strong> {pkg?.packageId}
|
|
430
400
|
</div>
|
|
431
401
|
</div>
|
|
432
402
|
|
|
@@ -484,20 +454,19 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
|
|
|
484
454
|
);
|
|
485
455
|
}
|
|
486
456
|
|
|
487
|
-
if (!loaded) {
|
|
457
|
+
if (!peersUI?.content && !loaded) {
|
|
488
458
|
return <LoadingIndicator />;
|
|
489
459
|
}
|
|
490
460
|
|
|
491
|
-
const peersUI = peersUIs[peersUIId];
|
|
492
461
|
if (peersUI?.content) {
|
|
493
|
-
const _props =
|
|
462
|
+
const _props = props.props;
|
|
494
463
|
try {
|
|
495
464
|
peersUI.propsSchema.parse(_props);
|
|
496
465
|
const Component = peersUI.content as React.ComponentType<Record<string, unknown>>;
|
|
497
466
|
return (
|
|
498
467
|
<UIErrorBoundary
|
|
499
468
|
peersUIId={peersUIId}
|
|
500
|
-
packageName={pkg
|
|
469
|
+
packageName={pkg?.name}
|
|
501
470
|
componentName={peersUI.content?.name}
|
|
502
471
|
>
|
|
503
472
|
<Component {..._props} />
|
|
@@ -522,7 +491,7 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
|
|
|
522
491
|
<strong>Peers UI ID:</strong> {peersUIId}
|
|
523
492
|
</div>
|
|
524
493
|
<div>
|
|
525
|
-
<strong>Package:</strong> {pkg
|
|
494
|
+
<strong>Package:</strong> {pkg?.name}
|
|
526
495
|
</div>
|
|
527
496
|
<div>
|
|
528
497
|
<strong>Component:</strong> {peersUI.content?.name}
|