@peers-app/peers-ui 0.25.20 → 0.26.0
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/dist/mention-configs.d.ts +7 -0
- package/dist/mention-configs.js +8 -2
- package/dist/screens/assistants/assistant-config.d.ts +1 -0
- package/dist/screens/assistants/assistant-config.js +20 -3
- package/dist/screens/assistants/assistant-config.test.d.ts +1 -0
- package/dist/screens/assistants/assistant-config.test.js +57 -0
- package/dist/screens/settings/settings-page.js +1 -59
- package/dist/screens/settings/settings-page.test.d.ts +1 -0
- package/dist/screens/settings/settings-page.test.js +19 -0
- package/dist/screens/tools/tool-catalog-ipc.d.ts +118 -0
- package/dist/screens/tools/tool-catalog-ipc.js +48 -0
- package/dist/screens/tools/tool-catalog-ipc.test.d.ts +1 -0
- package/dist/screens/tools/tool-catalog-ipc.test.js +46 -0
- package/dist/screens/tools/tool-details.d.ts +3 -4
- package/dist/screens/tools/tool-details.js +14 -33
- package/dist/screens/tools/tool-info.d.ts +5 -4
- package/dist/screens/tools/tool-info.js +10 -30
- package/dist/screens/tools/tool-list.d.ts +1 -0
- package/dist/screens/tools/tool-list.js +2 -26
- package/dist/screens/tools/tool-schema.d.ts +5 -3
- package/dist/screens/tools/tool-schema.js +5 -14
- package/dist/ui-router/ui-loader.js +14 -27
- package/dist/ui-router/ui-loader.test.d.ts +1 -0
- package/dist/ui-router/ui-loader.test.js +91 -0
- package/package.json +3 -3
- package/src/mention-configs.ts +9 -5
- package/src/screens/assistants/assistant-config.test.tsx +54 -0
- package/src/screens/assistants/assistant-config.tsx +42 -11
- package/src/screens/settings/settings-page.test.tsx +24 -0
- package/src/screens/settings/settings-page.tsx +1 -255
- package/src/screens/tools/tool-catalog-ipc.test.ts +46 -0
- package/src/screens/tools/tool-catalog-ipc.ts +51 -0
- package/src/screens/tools/tool-details.tsx +18 -67
- package/src/screens/tools/tool-info.tsx +39 -53
- package/src/screens/tools/tool-list.tsx +4 -37
- package/src/screens/tools/tool-schema.tsx +40 -45
- package/src/ui-router/ui-loader.test.tsx +108 -0
- package/src/ui-router/ui-loader.tsx +15 -46
- package/dist/screens/tools/tool-code.d.ts +0 -4
- package/dist/screens/tools/tool-code.js +0 -28
- package/src/screens/tools/tool-code.tsx +0 -30
|
@@ -3,35 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ToolInfo = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
(0, react_1.useEffect)(() => {
|
|
17
|
-
const sub = markdownUsageObs.subscribe(() => {
|
|
18
|
-
tool.usageDescription = markdownUsageObs();
|
|
19
|
-
});
|
|
20
|
-
const sub2 = tool.qs.name.subscribe(() => {
|
|
21
|
-
tool.name = tool.qs
|
|
22
|
-
.name()
|
|
23
|
-
.replaceAll(/\s/g, "_")
|
|
24
|
-
.replaceAll(/[^a-zA-Z0-9_-]/g, "");
|
|
25
|
-
});
|
|
26
|
-
const sub3 = markdownDetailedObs.subscribe(() => {
|
|
27
|
-
tool.detailedDescription = markdownDetailedObs();
|
|
28
|
-
});
|
|
29
|
-
return () => {
|
|
30
|
-
sub.dispose();
|
|
31
|
-
sub2.dispose();
|
|
32
|
-
sub3.dispose();
|
|
33
|
-
};
|
|
34
|
-
}, [tool, markdownDetailedObs, markdownUsageObs]);
|
|
35
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("small", { children: "Name:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: tool.qs.name, className: "form-control mb-3 p-0 ps-2", placeholder: "Tool name", title: "Tool name" }), (0, jsx_runtime_1.jsx)(access_level_field_1.AccessLevelField, { value: tool.qs.accessLevel }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small mt-2 me-2", children: "Is Assistant Runner:" }), (0, jsx_runtime_1.jsx)(checkbox_1.Checkbox, { className: "form-check-input mt-2", checked: tool.qs.isAssistantRunner })] }), (0, jsx_runtime_1.jsx)("small", { children: "Usage Description:" }), (0, jsx_runtime_1.jsx)(editor_inline_1.MarkdownEditorInline, { value: markdownUsageObs }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("small", { children: "Detailed Description:" }), (0, jsx_runtime_1.jsx)(editor_inline_1.MarkdownEditorInline, { value: markdownDetailedObs })] }));
|
|
6
|
+
const markdown_with_mentions_1 = require("../../components/markdown-with-mentions");
|
|
7
|
+
/** Display permanent identity and the host’s current dispatch and access projection. */
|
|
8
|
+
const ToolInfo = ({ tool, entry }) => {
|
|
9
|
+
const target = entry?.target;
|
|
10
|
+
const level = entry?.access?.accessLevel;
|
|
11
|
+
return ((0, jsx_runtime_1.jsxs)("div", { children: [tool.isAssistantRunner && (0, jsx_runtime_1.jsx)("span", { className: "badge bg-secondary", children: "Assistant runner" }), (0, jsx_runtime_1.jsxs)("dl", { children: [(0, jsx_runtime_1.jsx)("dt", { children: "ID" }), (0, jsx_runtime_1.jsx)("dd", { children: tool.toolId }), (0, jsx_runtime_1.jsx)("dt", { children: "Aliases" }), (0, jsx_runtime_1.jsx)("dd", { children: entry?.aliases.join(", ") ?? tool.toolId }), (0, jsx_runtime_1.jsx)("dt", { children: "Contract" }), (0, jsx_runtime_1.jsx)("dd", { children: target
|
|
12
|
+
? `${target.contractId}@${target.version}`
|
|
13
|
+
: tool.contractId
|
|
14
|
+
? `${tool.contractId}@${tool.contractVersion}`
|
|
15
|
+
: "Unresolved" }), (0, jsx_runtime_1.jsx)("dt", { children: "Member" }), (0, jsx_runtime_1.jsx)("dd", { children: target?.toolName ?? tool.toolName ?? tool.name }), (0, jsx_runtime_1.jsx)("dt", { children: "Provider" }), (0, jsx_runtime_1.jsx)("dd", { children: entry?.provider?.activePackageId ?? "Unavailable" }), (0, jsx_runtime_1.jsx)("dt", { children: "Dispatch" }), (0, jsx_runtime_1.jsxs)("dd", { children: [entry?.availability.dispatch ?? "Host catalog unavailable", entry?.availability.message && `: ${entry.availability.message}`] }), (0, jsx_runtime_1.jsx)("dt", { children: "Live access" }), (0, jsx_runtime_1.jsx)("dd", { children: level === undefined ? "Unavailable" : `${(0, peers_sdk_1.getAccessLevelLabel)(level)} (${level})` }), (0, jsx_runtime_1.jsx)("dt", { children: "Workflow" }), (0, jsx_runtime_1.jsxs)("dd", { children: [entry ? (entry.workflowInvocable ? "Available" : "Excluded or unavailable") : "Unknown", entry?.availability.requiresWorkflowRun && "; workflow run required"] })] }), (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: entry?.usageDescription ?? tool.usageDescription }), tool.detailedDescription && (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: tool.detailedDescription })] }));
|
|
36
16
|
};
|
|
37
17
|
exports.ToolInfo = ToolInfo;
|
|
@@ -10,6 +10,7 @@ const loading_indicator_1 = require("../../components/loading-indicator");
|
|
|
10
10
|
const markdown_with_mentions_1 = require("../../components/markdown-with-mentions");
|
|
11
11
|
const globals_1 = require("../../globals");
|
|
12
12
|
const hooks_1 = require("../../hooks");
|
|
13
|
+
/** Browse permanent tool catalog rows. */
|
|
13
14
|
function ToolList() {
|
|
14
15
|
const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(""));
|
|
15
16
|
const [searchText] = (0, hooks_1.useObservable)(searchTextObs);
|
|
@@ -35,31 +36,6 @@ function ToolList() {
|
|
|
35
36
|
});
|
|
36
37
|
return () => sub.dispose();
|
|
37
38
|
}, [cursorId, cursorObs, searchTextObs.subscribe]);
|
|
38
|
-
async function searchSubmit(evt) {
|
|
39
|
-
if (evt.key !== "Enter")
|
|
40
|
-
return;
|
|
41
|
-
if (!searchText.trim())
|
|
42
|
-
return;
|
|
43
|
-
const toolName = searchText
|
|
44
|
-
.trim()
|
|
45
|
-
.replace(/\s+/g, "_")
|
|
46
|
-
.replaceAll(/[^a-zA-Z0-9_]/g, "");
|
|
47
|
-
const tool = await (0, peers_sdk_1.Tools)().initRecord({
|
|
48
|
-
name: toolName,
|
|
49
|
-
usageDescription: searchText.trim(),
|
|
50
|
-
detailedDescription: "",
|
|
51
|
-
inputSchema: {
|
|
52
|
-
type: peers_sdk_1.IOSchemaType.simple,
|
|
53
|
-
fields: [{ name: "input", type: peers_sdk_1.FieldType.string, description: "" }],
|
|
54
|
-
},
|
|
55
|
-
outputSchema: {
|
|
56
|
-
type: peers_sdk_1.IOSchemaType.simple,
|
|
57
|
-
fields: [{ name: "output", type: peers_sdk_1.FieldType.string, description: "" }],
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
await (0, peers_sdk_1.Tools)().insert(tool);
|
|
61
|
-
(0, globals_1.mainContentPath)(`tools/${tool.toolId}`);
|
|
62
|
-
}
|
|
63
39
|
async function loadMore(existingTools) {
|
|
64
40
|
const moreMatches = [];
|
|
65
41
|
const cursor = cursorObs() || (await newCursor());
|
|
@@ -77,7 +53,7 @@ function ToolList() {
|
|
|
77
53
|
}
|
|
78
54
|
return moreMatches;
|
|
79
55
|
}
|
|
80
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control mt-3", style: { marginBottom: "10px" }, placeholder: "Search
|
|
56
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control mt-3", style: { marginBottom: "10px" }, placeholder: "Search tools", autoFocus: !!(0, globals_1.isDesktop)() }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { loadMore: loadMore, scrollThreshold: 0.6, renderItems: (tools) => {
|
|
81
57
|
return tools.map((tool) => {
|
|
82
58
|
let markdownContent = `${tool.usageDescription}\n\n---\n\n${tool.detailedDescription ?? ""}`;
|
|
83
59
|
const maxLen = 500;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ITool, IToolCatalogEntry } from "@peers-app/peers-sdk";
|
|
2
|
+
/** Read-only contract field tables, plus persisted assistant runner configuration. */
|
|
3
|
+
export declare const ToolSchema: ({ tool, entry }: {
|
|
4
|
+
tool: ITool;
|
|
5
|
+
entry?: IToolCatalogEntry;
|
|
4
6
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,18 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ToolSchema = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
(0, hooks_1.useObservable)(tool.qs.isAssistantRunner);
|
|
11
|
-
if (tool.isAssistantRunner && !tool.configSchema) {
|
|
12
|
-
tool.configSchema = {
|
|
13
|
-
type: peers_sdk_1.IOSchemaType.complex,
|
|
14
|
-
fields: [],
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [tool.isAssistantRunner && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { style: { width: "130px", display: "inline-block" }, className: "fw-bold", children: "Config Schema:" }), (0, jsx_runtime_1.jsx)(io_schema_1.IOSchemaEditor, { ioSchema: tool.qs.configSchema, simpleValueName: "input" }), (0, jsx_runtime_1.jsx)("br", {})] })), (0, jsx_runtime_1.jsx)("span", { style: { width: "130px", display: "inline-block" }, className: "fw-bold", children: "Input Schema:" }), (0, jsx_runtime_1.jsx)(io_schema_1.IOSchemaEditor, { ioSchema: tool.qs.inputSchema, simpleValueName: "input" }), (0, jsx_runtime_1.jsx)("span", { style: { width: "130px", display: "inline-block" }, className: "mt-4 fw-bold", children: "Output Schema:" }), (0, jsx_runtime_1.jsx)(io_schema_1.IOSchemaEditor, { ioSchema: tool.qs.outputSchema, simpleValueName: "output" })] }));
|
|
18
|
-
};
|
|
5
|
+
function FieldTable({ label, schema }) {
|
|
6
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h6", { children: label }), !schema?.fields.length ? ((0, jsx_runtime_1.jsx)("p", { children: "No fields" })) : ((0, jsx_runtime_1.jsxs)("table", { className: "table table-sm", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "Name" }), (0, jsx_runtime_1.jsx)("th", { children: "Type" }), (0, jsx_runtime_1.jsx)("th", { children: "Required" }), (0, jsx_runtime_1.jsx)("th", { children: "Description" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: schema.fields.map((field) => ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { children: field.name }), (0, jsx_runtime_1.jsxs)("td", { children: [field.type, field.isArray ? "[]" : ""] }), (0, jsx_runtime_1.jsx)("td", { children: field.optional ? "No" : "Yes" }), (0, jsx_runtime_1.jsx)("td", { children: field.description })] }, field.name))) })] }))] }));
|
|
7
|
+
}
|
|
8
|
+
/** Read-only contract field tables, plus persisted assistant runner configuration. */
|
|
9
|
+
const ToolSchema = ({ tool, entry }) => ((0, jsx_runtime_1.jsxs)("div", { children: [tool.isAssistantRunner && (0, jsx_runtime_1.jsx)(FieldTable, { label: "Config Schema", schema: tool.configSchema }), (0, jsx_runtime_1.jsx)(FieldTable, { label: "Input Schema", schema: entry?.inputSchema ?? tool.inputSchema }), (0, jsx_runtime_1.jsx)(FieldTable, { label: "Output Schema", schema: entry?.outputSchema ?? tool.outputSchema })] }));
|
|
19
10
|
exports.ToolSchema = ToolSchema;
|
|
@@ -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.
|
|
3
|
+
"version": "0.26.0",
|
|
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.
|
|
33
|
+
"@peers-app/peers-sdk": "^0.26.0",
|
|
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.
|
|
44
|
+
"@peers-app/peers-sdk": "0.26.0",
|
|
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",
|
package/src/mention-configs.ts
CHANGED
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
Workflows,
|
|
9
9
|
} from "@peers-app/peers-sdk";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import { listWorkflowMentionTools } from "./screens/tools/tool-catalog-ipc";
|
|
12
12
|
|
|
13
13
|
const MENTIONS_MAX_RESULTS = 10;
|
|
14
14
|
|
|
15
|
+
/** Editor and renderer behavior for one persisted mention kind. */
|
|
15
16
|
export interface IMentionConfig {
|
|
16
17
|
iconClass: string;
|
|
17
18
|
styleClass?: string;
|
|
@@ -22,15 +23,13 @@ export interface IMentionConfig {
|
|
|
22
23
|
onClick?: (data: IMentionData) => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** Discover workflow-invocable tools while keeping historical mention IDs readable. */
|
|
25
27
|
export const toolMentionConfig: IMentionConfig = {
|
|
26
28
|
kind: "tool",
|
|
27
29
|
iconClass: "bi bi-tools",
|
|
28
30
|
styleClass: "mention-tool",
|
|
29
31
|
query: async (search: string) => {
|
|
30
|
-
const results = await
|
|
31
|
-
{ name: { $matchWords: search } },
|
|
32
|
-
{ pageSize: MENTIONS_MAX_RESULTS },
|
|
33
|
-
);
|
|
32
|
+
const results = await listWorkflowMentionTools(search, MENTIONS_MAX_RESULTS);
|
|
34
33
|
return results.map((p) => ({
|
|
35
34
|
kind: "tool",
|
|
36
35
|
id: p.toolId,
|
|
@@ -50,6 +49,7 @@ export const toolMentionConfig: IMentionConfig = {
|
|
|
50
49
|
),
|
|
51
50
|
};
|
|
52
51
|
|
|
52
|
+
/** Discover users and resolve persisted user mentions. */
|
|
53
53
|
export const userMentionConfig: IMentionConfig = {
|
|
54
54
|
kind: "user",
|
|
55
55
|
iconClass: "bi-person-fill",
|
|
@@ -78,6 +78,7 @@ export const userMentionConfig: IMentionConfig = {
|
|
|
78
78
|
),
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
/** Discover assistants and resolve persisted assistant mentions. */
|
|
81
82
|
export const assistantMentionConfig: IMentionConfig = {
|
|
82
83
|
kind: "assistant",
|
|
83
84
|
iconClass: "bi bi-person-fill-gear",
|
|
@@ -106,6 +107,7 @@ export const assistantMentionConfig: IMentionConfig = {
|
|
|
106
107
|
),
|
|
107
108
|
};
|
|
108
109
|
|
|
110
|
+
/** Discover workflows and resolve persisted workflow mentions. */
|
|
109
111
|
export const workflowMentionConfig: IMentionConfig = {
|
|
110
112
|
kind: "workflow",
|
|
111
113
|
iconClass: "bi bi-database-fill-gear",
|
|
@@ -134,6 +136,7 @@ export const workflowMentionConfig: IMentionConfig = {
|
|
|
134
136
|
),
|
|
135
137
|
};
|
|
136
138
|
|
|
139
|
+
/** Discover data types and navigate to their definitions. */
|
|
137
140
|
export const valueTypeMentionConfig: IMentionConfig = {
|
|
138
141
|
kind: "type",
|
|
139
142
|
iconClass: "bi bi-code-square",
|
|
@@ -172,6 +175,7 @@ export const valueTypeMentionConfig: IMentionConfig = {
|
|
|
172
175
|
// }))),
|
|
173
176
|
// picture = <i className="bi bi-list-task pe-1" />;
|
|
174
177
|
|
|
178
|
+
/** Shared mention definitions used by editors and rendered messages. */
|
|
175
179
|
export const mentionConfigs = [
|
|
176
180
|
toolMentionConfig,
|
|
177
181
|
userMentionConfig,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type IAssistant, type IDoc, observable } from "@peers-app/peers-sdk";
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { AssistantConfig } from "./assistant-config";
|
|
4
|
+
|
|
5
|
+
const mockRunners = [
|
|
6
|
+
{ toolId: "selected", name: "Retired", catalogState: "quarantined" },
|
|
7
|
+
{ toolId: "hidden", name: "Other unavailable", catalogState: "quarantined" },
|
|
8
|
+
{ toolId: "available", name: "Available" },
|
|
9
|
+
];
|
|
10
|
+
const mockCatalog = jest.fn();
|
|
11
|
+
jest.mock("@peers-app/peers-sdk", () => ({
|
|
12
|
+
...jest.requireActual("@peers-app/peers-sdk"),
|
|
13
|
+
Tools: () => ({
|
|
14
|
+
get: async () => mockRunners[0],
|
|
15
|
+
list: async () => mockRunners,
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
jest.mock("../tools/tool-catalog-ipc", () => ({
|
|
19
|
+
getToolCatalogConsumer: () => mockCatalog(),
|
|
20
|
+
}));
|
|
21
|
+
jest.mock("../../components/io-schema-values", () => ({ IOSchemaValues: () => null }));
|
|
22
|
+
|
|
23
|
+
test.each([
|
|
24
|
+
"catalog",
|
|
25
|
+
"browser fallback",
|
|
26
|
+
])("runner selection retains a disabled current runner in %s", async (mode) => {
|
|
27
|
+
mockCatalog.mockReset();
|
|
28
|
+
if (mode === "catalog")
|
|
29
|
+
mockCatalog.mockResolvedValue({
|
|
30
|
+
tools: {
|
|
31
|
+
getTool: async ({ toolIdOrName }: { toolIdOrName: string }) => ({
|
|
32
|
+
tool: { workflowInvocable: toolIdOrName === "available" },
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
else mockCatalog.mockRejectedValue(new Error("No host transport"));
|
|
37
|
+
const selected = observable("selected");
|
|
38
|
+
const assistant = {
|
|
39
|
+
assistantRunnerToolId: "selected",
|
|
40
|
+
qs: { assistantRunnerToolId: selected },
|
|
41
|
+
} as unknown as IDoc<IAssistant>;
|
|
42
|
+
const { container } = render(<AssistantConfig assistant={assistant} />);
|
|
43
|
+
await screen.findByRole("button", { name: "Available" });
|
|
44
|
+
const retired = container.querySelector<HTMLButtonElement>(".dropdown-menu button:disabled");
|
|
45
|
+
expect(retired).toHaveTextContent("Retired (unavailable)");
|
|
46
|
+
expect(container.querySelector(".dropdown-toggle")).toHaveTextContent("Retired (unavailable)");
|
|
47
|
+
expect(screen.queryByText("Other unavailable")).not.toBeInTheDocument();
|
|
48
|
+
if (!retired) throw new Error("Missing unavailable runner option");
|
|
49
|
+
fireEvent.click(retired);
|
|
50
|
+
expect(selected()).toBe("selected");
|
|
51
|
+
fireEvent.click(screen.getByRole("button", { name: "Available" }));
|
|
52
|
+
await waitFor(() => expect(selected()).toBe("available"));
|
|
53
|
+
expect(screen.queryByText("Retired (unavailable)")).not.toBeInTheDocument();
|
|
54
|
+
});
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
import { IOSchemaValues } from "../../components/io-schema-values";
|
|
9
9
|
import { LoadingIndicator } from "../../components/loading-indicator";
|
|
10
10
|
import { useObservable, usePromise } from "../../hooks";
|
|
11
|
+
import { getToolCatalogConsumer } from "../tools/tool-catalog-ipc";
|
|
11
12
|
|
|
13
|
+
/** Edit assistant runner selection and its configuration. */
|
|
12
14
|
export const AssistantConfig = (props: { assistant: IDoc<IAssistant> }) => {
|
|
13
15
|
const { assistant } = props;
|
|
14
16
|
|
|
@@ -57,7 +59,25 @@ const AssistantRunnersDropdown = (props: { selectedToolId: Observable<string> })
|
|
|
57
59
|
|
|
58
60
|
const modelTools = usePromise(async () => {
|
|
59
61
|
const modelTools = await Tools().list({ isAssistantRunner: true });
|
|
60
|
-
|
|
62
|
+
let availableIds: Set<string>;
|
|
63
|
+
try {
|
|
64
|
+
const catalog = await getToolCatalogConsumer();
|
|
65
|
+
const entries = await Promise.all(
|
|
66
|
+
modelTools.map((tool) =>
|
|
67
|
+
catalog.tools.getTool({ toolIdOrName: tool.toolId, runnableOnly: true }),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
availableIds = new Set(
|
|
71
|
+
modelTools
|
|
72
|
+
.filter((_tool, index) => entries[index].tool?.workflowInvocable)
|
|
73
|
+
.map((tool) => tool.toolId),
|
|
74
|
+
);
|
|
75
|
+
} catch {
|
|
76
|
+
availableIds = new Set(
|
|
77
|
+
modelTools.filter((tool) => tool.catalogState !== "quarantined").map((tool) => tool.toolId),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return modelTools.map((tool) => ({ tool, available: availableIds.has(tool.toolId) }));
|
|
61
81
|
});
|
|
62
82
|
|
|
63
83
|
if (!modelTools) {
|
|
@@ -72,18 +92,29 @@ const AssistantRunnersDropdown = (props: { selectedToolId: Observable<string> })
|
|
|
72
92
|
data-bs-toggle="dropdown"
|
|
73
93
|
aria-expanded="false"
|
|
74
94
|
>
|
|
75
|
-
{modelTools.find((tool) => tool.toolId === selectedToolId)?.name ?? "Select Tool"}
|
|
95
|
+
{modelTools.find(({ tool }) => tool.toolId === selectedToolId)?.tool.name ?? "Select Tool"}
|
|
96
|
+
{modelTools.some(({ tool, available }) => tool.toolId === selectedToolId && !available)
|
|
97
|
+
? " (unavailable)"
|
|
98
|
+
: ""}
|
|
76
99
|
</button>
|
|
77
100
|
<ul className="dropdown-menu">
|
|
78
|
-
{modelTools
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
{modelTools
|
|
102
|
+
.filter(({ tool, available }) => available || tool.toolId === selectedToolId)
|
|
103
|
+
.map(({ tool, available }) => {
|
|
104
|
+
return (
|
|
105
|
+
<li key={tool.toolId}>
|
|
106
|
+
<button
|
|
107
|
+
className="dropdown-item"
|
|
108
|
+
type="button"
|
|
109
|
+
disabled={!available}
|
|
110
|
+
onClick={() => props.selectedToolId(tool.toolId)}
|
|
111
|
+
>
|
|
112
|
+
{tool.name}
|
|
113
|
+
{available ? "" : " (unavailable)"}
|
|
114
|
+
</button>
|
|
115
|
+
</li>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
87
118
|
</ul>
|
|
88
119
|
</div>
|
|
89
120
|
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { SettingsPage } from "./settings-page";
|
|
3
|
+
|
|
4
|
+
jest.mock("../../components/tooltip", () => ({ Tooltip: () => null }));
|
|
5
|
+
jest.mock("../../hooks", () => ({ usePromise: () => undefined, useObservable: () => [false] }));
|
|
6
|
+
jest.mock("../../components/tabs", () => ({
|
|
7
|
+
Tabs: ({ tabs }: any) => (
|
|
8
|
+
<>
|
|
9
|
+
{tabs.map((tab: any) => (
|
|
10
|
+
<div key={tab.name}>{tab.content}</div>
|
|
11
|
+
))}
|
|
12
|
+
</>
|
|
13
|
+
),
|
|
14
|
+
}));
|
|
15
|
+
jest.mock("../../root-layout/layout-picker", () => ({ LayoutPicker: () => <div>Layout</div> }));
|
|
16
|
+
jest.mock("./color-mode-dropdown", () => ({ ColorModeDropdown: () => <div>Theme</div> }));
|
|
17
|
+
jest.mock("../../globals", () => ({ me: {}, isDesktop: () => false }));
|
|
18
|
+
|
|
19
|
+
test("Settings renders advanced controls without the retired Tasks importer", () => {
|
|
20
|
+
const html = renderToStaticMarkup(<SettingsPage />);
|
|
21
|
+
expect(html).toContain("Advanced Settings");
|
|
22
|
+
expect(html).not.toContain("Import Old Peers");
|
|
23
|
+
expect(html).not.toContain("Dry Run");
|
|
24
|
+
});
|