@agent-native/dispatch 0.41.1-nightly-20261104110058 → 0.42.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/CHANGELOG.md +26 -55
- package/dist/actions/get-hosted-apps-settings.d.ts +5 -0
- package/dist/actions/get-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/get-hosted-apps-settings.js +25 -0
- package/dist/actions/get-hosted-apps-settings.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +4 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-connected-agents.d.ts +1 -0
- package/dist/actions/list-connected-agents.js +11 -10
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list_apps.js +3 -2
- package/dist/actions/list_apps.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/actions/set-hosted-apps-settings.d.ts +7 -0
- package/dist/actions/set-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/set-hosted-apps-settings.js +35 -0
- package/dist/actions/set-hosted-apps-settings.js.map +1 -0
- package/dist/components/app-card-tiers.d.ts +13 -0
- package/dist/components/app-card-tiers.d.ts.map +1 -0
- package/dist/components/app-card-tiers.js +22 -0
- package/dist/components/app-card-tiers.js.map +1 -0
- package/dist/components/app-list-row.d.ts +12 -4
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +19 -4
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +4 -0
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +9 -0
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/app-pin-button.d.ts +9 -0
- package/dist/components/app-pin-button.d.ts.map +1 -0
- package/dist/components/app-pin-button.js +29 -0
- package/dist/components/app-pin-button.js.map +1 -0
- package/dist/components/app-tier-sections.d.ts +27 -0
- package/dist/components/app-tier-sections.d.ts.map +1 -0
- package/dist/components/app-tier-sections.js +44 -0
- package/dist/components/app-tier-sections.js.map +1 -0
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +19 -26
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/connected-app-card.d.ts +3 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +6 -3
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.d.ts +4 -0
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +6 -0
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +25 -35
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/hosted-apps-settings-group.d.ts +6 -0
- package/dist/components/hosted-apps-settings-group.d.ts.map +1 -0
- package/dist/components/hosted-apps-settings-group.js +76 -0
- package/dist/components/hosted-apps-settings-group.js.map +1 -0
- package/dist/components/layout/Layout.d.ts +6 -4
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +97 -80
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +9 -20
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-layout-notice.d.ts +9 -0
- package/dist/components/workspace-app-layout-notice.d.ts.map +1 -0
- package/dist/components/workspace-app-layout-notice.js +27 -0
- package/dist/components/workspace-app-layout-notice.js.map +1 -0
- package/dist/components/workspace-app-search.d.ts.map +1 -1
- package/dist/components/workspace-app-search.js +11 -5
- package/dist/components/workspace-app-search.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +7 -2
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/lib/other-apps.d.ts +10 -1
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +7 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-app-layout.d.ts +62 -6
- package/dist/lib/workspace-app-layout.d.ts.map +1 -1
- package/dist/lib/workspace-app-layout.js +196 -41
- package/dist/lib/workspace-app-layout.js.map +1 -1
- package/dist/lib/workspace-app-lists.d.ts +17 -0
- package/dist/lib/workspace-app-lists.d.ts.map +1 -0
- package/dist/lib/workspace-app-lists.js +21 -0
- package/dist/lib/workspace-app-lists.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +3 -1
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +5 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +23 -35
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/settings-page.d.ts.map +1 -1
- package/dist/routes/pages/settings-page.js +24 -3
- package/dist/routes/pages/settings-page.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +13 -18
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/app-roles.d.ts +2 -0
- package/dist/server/lib/app-roles.d.ts.map +1 -1
- package/dist/server/lib/app-roles.js +26 -7
- package/dist/server/lib/app-roles.js.map +1 -1
- package/dist/server/lib/hosted-apps-settings-schema.d.ts +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings-schema.js +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.js.map +1 -0
- package/dist/server/lib/hosted-apps-settings.d.ts +27 -0
- package/dist/server/lib/hosted-apps-settings.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings.js +72 -0
- package/dist/server/lib/hosted-apps-settings.js.map +1 -0
- package/dist/server/plugins/agent-chat.js +6 -3
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/hosted-apps-settings.d.ts +3 -0
- package/dist/shared/hosted-apps-settings.d.ts.map +1 -0
- package/dist/shared/hosted-apps-settings.js +3 -0
- package/dist/shared/hosted-apps-settings.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/get-hosted-apps-settings.ts +29 -0
- package/src/actions/hosted-apps-settings.spec.ts +100 -0
- package/src/actions/index.ts +4 -0
- package/src/actions/list-connected-agents.spec.ts +67 -0
- package/src/actions/list-connected-agents.ts +14 -15
- package/src/actions/list_apps.spec.ts +16 -0
- package/src/actions/list_apps.ts +5 -2
- package/src/actions/set-hosted-apps-settings.ts +42 -0
- package/src/components/app-card-tiers.tsx +85 -0
- package/src/components/app-list-row.spec.tsx +54 -0
- package/src/components/app-list-row.tsx +50 -8
- package/src/components/app-open-actions.tsx +16 -0
- package/src/components/app-pin-button.spec.tsx +68 -0
- package/src/components/app-pin-button.tsx +66 -0
- package/src/components/app-tier-sections.spec.tsx +155 -0
- package/src/components/app-tier-sections.tsx +158 -0
- package/src/components/chat-home-apps.spec.tsx +135 -29
- package/src/components/chat-home-apps.tsx +115 -111
- package/src/components/connected-app-card.spec.tsx +18 -2
- package/src/components/connected-app-card.tsx +18 -1
- package/src/components/create-app-popover.tsx +22 -0
- package/src/components/dispatch-control-plane.spec.tsx +89 -20
- package/src/components/dispatch-control-plane.tsx +91 -125
- package/src/components/hosted-apps-settings-group.spec.tsx +218 -0
- package/src/components/hosted-apps-settings-group.tsx +189 -0
- package/src/components/layout/Layout.spec.tsx +356 -49
- package/src/components/layout/Layout.tsx +189 -142
- package/src/components/workspace-app-card.spec.tsx +33 -29
- package/src/components/workspace-app-card.tsx +13 -31
- package/src/components/workspace-app-layout-notice.spec.tsx +106 -0
- package/src/components/workspace-app-layout-notice.tsx +65 -0
- package/src/components/workspace-app-search.spec.tsx +53 -0
- package/src/components/workspace-app-search.tsx +39 -32
- package/src/components/workspace-template-card.spec.tsx +10 -0
- package/src/components/workspace-template-card.tsx +7 -2
- package/src/db/index.spec.ts +9 -0
- package/src/db/index.ts +1 -0
- package/src/lib/other-apps.spec.ts +25 -0
- package/src/lib/other-apps.ts +9 -0
- package/src/lib/workspace-app-layout.spec.ts +61 -3
- package/src/lib/workspace-app-layout.store.spec.tsx +261 -0
- package/src/lib/workspace-app-layout.ts +265 -48
- package/src/lib/workspace-app-lists.ts +32 -0
- package/src/lib/workspace-apps.ts +10 -2
- package/src/routes/pages/apps.spec.tsx +133 -5
- package/src/routes/pages/apps.tsx +79 -140
- package/src/routes/pages/chat.spec.tsx +66 -49
- package/src/routes/pages/settings-page.tsx +31 -3
- package/src/routes/pages/settings.spec.tsx +37 -0
- package/src/server/lib/app-creation-store.spec.ts +14 -0
- package/src/server/lib/app-creation-store.ts +16 -29
- package/src/server/lib/app-creation-store.write-observation.spec.ts +216 -0
- package/src/server/lib/app-roles.spec.ts +48 -1
- package/src/server/lib/app-roles.ts +44 -12
- package/src/server/lib/hosted-apps-settings-schema.ts +6 -0
- package/src/server/lib/hosted-apps-settings.spec.ts +154 -0
- package/src/server/lib/hosted-apps-settings.ts +108 -0
- package/src/server/plugins/agent-chat.spec.ts +16 -0
- package/src/server/plugins/agent-chat.ts +7 -2
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/hosted-apps-settings.ts +3 -0
- package/src/styles/dispatch.css +40 -11
- package/dist/components/available-apps-section.d.ts +0 -9
- package/dist/components/available-apps-section.d.ts.map +0 -1
- package/dist/components/available-apps-section.js +0 -97
- package/dist/components/available-apps-section.js.map +0 -1
- package/dist/components/other-apps-section.d.ts +0 -35
- package/dist/components/other-apps-section.d.ts.map +0 -1
- package/dist/components/other-apps-section.js +0 -79
- package/dist/components/other-apps-section.js.map +0 -1
- package/src/components/available-apps-section.tsx +0 -239
- package/src/components/other-apps-section.spec.ts +0 -50
- package/src/components/other-apps-section.tsx +0 -254
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
IconEyeOff,
|
|
17
17
|
IconFileText,
|
|
18
18
|
IconKey,
|
|
19
|
-
IconPin,
|
|
20
19
|
IconShare3,
|
|
21
20
|
IconSettings,
|
|
22
21
|
IconTrash,
|
|
@@ -44,7 +43,8 @@ import { ActionQueryError } from "./action-query-error";
|
|
|
44
43
|
import { AppIcon } from "./app-icon";
|
|
45
44
|
import { AppKeysPanel } from "./app-keys-popover";
|
|
46
45
|
import { AppListRow } from "./app-list-row";
|
|
47
|
-
import { AppOpenActions } from "./app-open-actions";
|
|
46
|
+
import { AppOpenActions, appOpenActionLabels } from "./app-open-actions";
|
|
47
|
+
import { AppPinButton } from "./app-pin-button";
|
|
48
48
|
import { Badge } from "./ui/badge";
|
|
49
49
|
import { Button } from "./ui/button";
|
|
50
50
|
import {
|
|
@@ -248,13 +248,6 @@ export function WorkspaceAppCard({
|
|
|
248
248
|
...(whenToUseChanged ? { whenToUse: draftWhenToUseValues } : {}),
|
|
249
249
|
});
|
|
250
250
|
};
|
|
251
|
-
const pinLabel = t(
|
|
252
|
-
isPinned ? "dispatch.pages.unpinApp" : "dispatch.pages.pinApp",
|
|
253
|
-
{
|
|
254
|
-
name: app.name,
|
|
255
|
-
defaultValue: isPinned ? "Unpin this app" : "Pin this app",
|
|
256
|
-
},
|
|
257
|
-
);
|
|
258
251
|
|
|
259
252
|
return (
|
|
260
253
|
<>
|
|
@@ -297,16 +290,21 @@ export function WorkspaceAppCard({
|
|
|
297
290
|
</div>
|
|
298
291
|
</div>
|
|
299
292
|
<div className="flex shrink-0 items-center gap-2">
|
|
293
|
+
{onTogglePinned ? (
|
|
294
|
+
<AppPinButton
|
|
295
|
+
appId={app.id}
|
|
296
|
+
name={app.name}
|
|
297
|
+
pinned={isPinned}
|
|
298
|
+
onToggle={onTogglePinned}
|
|
299
|
+
/>
|
|
300
|
+
) : null}
|
|
300
301
|
<WorkspaceAppOpenActions
|
|
301
302
|
app={app}
|
|
302
303
|
href={directHref ?? href}
|
|
303
304
|
openDirectly={Boolean(
|
|
304
305
|
directHref && shouldOpenWorkspaceAppInTopWindow(),
|
|
305
306
|
)}
|
|
306
|
-
isPinned={isPinned}
|
|
307
|
-
pinLabel={pinLabel}
|
|
308
307
|
pendingOpenLabel={pendingOpenLabel}
|
|
309
|
-
onTogglePinned={onTogglePinned}
|
|
310
308
|
/>
|
|
311
309
|
<WorkspaceAppSettings
|
|
312
310
|
app={app}
|
|
@@ -432,19 +430,14 @@ function WorkspaceAppOpenActions({
|
|
|
432
430
|
app,
|
|
433
431
|
href,
|
|
434
432
|
openDirectly,
|
|
435
|
-
isPinned,
|
|
436
|
-
pinLabel,
|
|
437
433
|
pendingOpenLabel,
|
|
438
|
-
onTogglePinned,
|
|
439
434
|
}: {
|
|
440
435
|
app: WorkspaceAppSummary;
|
|
441
436
|
href: string | null;
|
|
442
437
|
openDirectly: boolean;
|
|
443
|
-
isPinned: boolean;
|
|
444
|
-
pinLabel: string;
|
|
445
438
|
pendingOpenLabel: string;
|
|
446
|
-
onTogglePinned?: () => void;
|
|
447
439
|
}) {
|
|
440
|
+
const t = useT();
|
|
448
441
|
const navigate = useNavigate();
|
|
449
442
|
const appRoute = workspaceAppRoute(app.id);
|
|
450
443
|
const isPending = app.status === "pending";
|
|
@@ -466,7 +459,7 @@ function WorkspaceAppOpenActions({
|
|
|
466
459
|
if (!href) {
|
|
467
460
|
return (
|
|
468
461
|
<Button size="sm" variant="outline" disabled>
|
|
469
|
-
Open
|
|
462
|
+
{t("dispatch.pages.openApp", { defaultValue: "Open" })}
|
|
470
463
|
</Button>
|
|
471
464
|
);
|
|
472
465
|
}
|
|
@@ -476,22 +469,11 @@ function WorkspaceAppOpenActions({
|
|
|
476
469
|
name={app.name}
|
|
477
470
|
href={href}
|
|
478
471
|
showNewTabOption
|
|
472
|
+
labels={appOpenActionLabels(t, app.name)}
|
|
479
473
|
onOpen={() => {
|
|
480
474
|
if (openDirectly && navigateToWorkspaceApp(href)) return;
|
|
481
475
|
void navigate(appRoute);
|
|
482
476
|
}}
|
|
483
|
-
menuItems={
|
|
484
|
-
onTogglePinned
|
|
485
|
-
? [
|
|
486
|
-
{
|
|
487
|
-
id: "pin",
|
|
488
|
-
label: pinLabel,
|
|
489
|
-
icon: <IconPin size={14} strokeWidth={isPinned ? 2.2 : 1.7} />,
|
|
490
|
-
onSelect: onTogglePinned,
|
|
491
|
-
},
|
|
492
|
-
]
|
|
493
|
-
: undefined
|
|
494
|
-
}
|
|
495
477
|
/>
|
|
496
478
|
);
|
|
497
479
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const appState = vi.hoisted(() => ({
|
|
7
|
+
read: vi.fn(),
|
|
8
|
+
write: vi.fn(),
|
|
9
|
+
deviceOk: true,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("@agent-native/core/client/application-state", () => ({
|
|
13
|
+
readClientAppState: appState.read,
|
|
14
|
+
writeClientAppState: appState.write,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("@agent-native/core/client/agent-chat", async (importOriginal) => ({
|
|
18
|
+
...(await importOriginal<
|
|
19
|
+
typeof import("@agent-native/core/client/agent-chat")
|
|
20
|
+
>()),
|
|
21
|
+
writeChatFirstAppLayout: () =>
|
|
22
|
+
appState.deviceOk ? { ok: true } : { ok: false, reason: "write-failed" },
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
26
|
+
useT: () => (key: string) => key,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
resetWorkspaceAppLayoutStoreForTests,
|
|
31
|
+
updateWorkspaceAppLayout,
|
|
32
|
+
} from "../lib/workspace-app-layout";
|
|
33
|
+
import { WorkspaceAppLayoutNotice } from "./workspace-app-layout-notice";
|
|
34
|
+
|
|
35
|
+
const pin = (id: string) =>
|
|
36
|
+
updateWorkspaceAppLayout((layout) => ({
|
|
37
|
+
...layout,
|
|
38
|
+
pinnedIds: [id, ...layout.pinnedIds],
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe("WorkspaceAppLayoutNotice", () => {
|
|
42
|
+
let container: HTMLDivElement;
|
|
43
|
+
let root: Root;
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
47
|
+
resetWorkspaceAppLayoutStoreForTests();
|
|
48
|
+
appState.deviceOk = true;
|
|
49
|
+
appState.read.mockReset().mockResolvedValue(null);
|
|
50
|
+
appState.write.mockReset().mockResolvedValue(null);
|
|
51
|
+
container = document.createElement("div");
|
|
52
|
+
document.body.appendChild(container);
|
|
53
|
+
root = createRoot(container);
|
|
54
|
+
await act(async () => root.render(<WorkspaceAppLayoutNotice />));
|
|
55
|
+
// Let the first change read workspace state, so later ones write it.
|
|
56
|
+
await act(async () => pin("seed"));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
act(() => root.unmount());
|
|
61
|
+
container.remove();
|
|
62
|
+
vi.unstubAllGlobals();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const notice = () =>
|
|
66
|
+
container.querySelector('[role="status"] [data-app-notice-message]');
|
|
67
|
+
const dismiss = () =>
|
|
68
|
+
container.querySelector<HTMLButtonElement>('[role="status"] button')!;
|
|
69
|
+
|
|
70
|
+
it.each([
|
|
71
|
+
[false, true, "dispatch.pages.appPinSaveFailed"],
|
|
72
|
+
[false, false, "dispatch.pages.appPinSaveFailed"],
|
|
73
|
+
[true, false, "dispatch.pages.appPinSavedToWorkspaceOnly"],
|
|
74
|
+
])(
|
|
75
|
+
"says where pins were saved (workspace %s, device %s)",
|
|
76
|
+
async (workspaceOk, deviceOk, message) => {
|
|
77
|
+
appState.deviceOk = deviceOk;
|
|
78
|
+
appState.write.mockImplementation(() =>
|
|
79
|
+
workspaceOk ? Promise.resolve(null) : Promise.reject(new Error("x")),
|
|
80
|
+
);
|
|
81
|
+
await act(async () => pin("mail"));
|
|
82
|
+
|
|
83
|
+
expect(notice()?.textContent).toBe(message);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
it("shows nothing once a later save succeeds", async () => {
|
|
88
|
+
appState.write.mockRejectedValueOnce(new Error("offline"));
|
|
89
|
+
await act(async () => pin("mail"));
|
|
90
|
+
expect(notice()).not.toBeNull();
|
|
91
|
+
|
|
92
|
+
await act(async () => pin("plan"));
|
|
93
|
+
expect(notice()).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("shows again for a new failure after being dismissed", async () => {
|
|
97
|
+
appState.write.mockRejectedValue(new Error("offline"));
|
|
98
|
+
await act(async () => pin("mail"));
|
|
99
|
+
expect(dismiss().textContent).toBe("agentChat.chatFirst.dismiss");
|
|
100
|
+
await act(async () => dismiss().click());
|
|
101
|
+
expect(notice()).toBeNull();
|
|
102
|
+
|
|
103
|
+
await act(async () => pin("plan"));
|
|
104
|
+
expect(notice()?.textContent).toBe("dispatch.pages.appPinSaveFailed");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { useState, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
useWorkspaceAppLayout,
|
|
6
|
+
type WorkspaceAppLayoutPersistenceError,
|
|
7
|
+
} from "../lib/workspace-app-layout";
|
|
8
|
+
import { Alert, AlertDescription } from "./ui/alert";
|
|
9
|
+
import { Button } from "./ui/button";
|
|
10
|
+
|
|
11
|
+
const NOTICE_KEYS: Record<WorkspaceAppLayoutPersistenceError, string> = {
|
|
12
|
+
both: "dispatch.pages.appPinSaveFailed",
|
|
13
|
+
workspace: "dispatch.pages.appPinSaveFailed",
|
|
14
|
+
device: "dispatch.pages.appPinSavedToWorkspaceOnly",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** A dismissible notice above the page content. */
|
|
18
|
+
export function AppNotice({
|
|
19
|
+
children,
|
|
20
|
+
onDismiss,
|
|
21
|
+
}: {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
onDismiss: () => void;
|
|
24
|
+
}) {
|
|
25
|
+
const t = useT();
|
|
26
|
+
return (
|
|
27
|
+
<Alert
|
|
28
|
+
role="status"
|
|
29
|
+
aria-live="polite"
|
|
30
|
+
data-app-notice
|
|
31
|
+
className="flex items-center justify-between gap-3 rounded-none border-x-0 border-t-0 px-3 py-2"
|
|
32
|
+
>
|
|
33
|
+
<AlertDescription
|
|
34
|
+
data-app-notice-message
|
|
35
|
+
className="min-w-0 text-xs text-muted-foreground"
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</AlertDescription>
|
|
39
|
+
<Button
|
|
40
|
+
type="button"
|
|
41
|
+
variant="ghost"
|
|
42
|
+
size="sm"
|
|
43
|
+
className="shrink-0"
|
|
44
|
+
onClick={onDismiss}
|
|
45
|
+
>
|
|
46
|
+
{t("agentChat.chatFirst.dismiss")}
|
|
47
|
+
</Button>
|
|
48
|
+
</Alert>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The one place a failed app pin save is shown, on every route. */
|
|
53
|
+
export function WorkspaceAppLayoutNotice() {
|
|
54
|
+
const t = useT();
|
|
55
|
+
const { persistenceError } = useWorkspaceAppLayout({ hydrate: false });
|
|
56
|
+
const [dismissedWriteId, setDismissedWriteId] = useState<number | null>(null);
|
|
57
|
+
if (!persistenceError || persistenceError.writeId === dismissedWriteId) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return (
|
|
61
|
+
<AppNotice onDismiss={() => setDismissedWriteId(persistenceError.writeId)}>
|
|
62
|
+
{t(NOTICE_KEYS[persistenceError.scope])}
|
|
63
|
+
</AppNotice>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
7
|
+
useT: () => (key: string) => key,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { WorkspaceAppSearch } from "./workspace-app-search";
|
|
11
|
+
|
|
12
|
+
describe("WorkspaceAppSearch", () => {
|
|
13
|
+
let container: HTMLDivElement;
|
|
14
|
+
let root: Root;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
18
|
+
container = document.createElement("div");
|
|
19
|
+
document.body.appendChild(container);
|
|
20
|
+
root = createRoot(container);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
act(() => root.unmount());
|
|
25
|
+
container.remove();
|
|
26
|
+
vi.unstubAllGlobals();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("clears from a button inside the field, not beside it", async () => {
|
|
30
|
+
const onQueryChange = vi.fn();
|
|
31
|
+
await act(async () =>
|
|
32
|
+
root.render(
|
|
33
|
+
<WorkspaceAppSearch query="" onQueryChange={onQueryChange} />,
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
const group = container.querySelector('[data-slot="input-group"]');
|
|
37
|
+
expect(group?.querySelector("input")).not.toBeNull();
|
|
38
|
+
expect(container.querySelector("button")).toBeNull();
|
|
39
|
+
|
|
40
|
+
await act(async () =>
|
|
41
|
+
root.render(
|
|
42
|
+
<WorkspaceAppSearch query="mail" onQueryChange={onQueryChange} />,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
const clear = container.querySelector<HTMLButtonElement>(
|
|
46
|
+
'button[aria-label="dispatch.pages.clearAppSearch"]',
|
|
47
|
+
);
|
|
48
|
+
expect(clear?.closest('[data-slot="input-group"]')).toBe(group);
|
|
49
|
+
await act(async () => clear?.click());
|
|
50
|
+
expect(onQueryChange).toHaveBeenCalledWith("");
|
|
51
|
+
expect(document.activeElement).toBe(group?.querySelector("input"));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import {
|
|
3
|
+
InputGroup,
|
|
4
|
+
InputGroupAddon,
|
|
5
|
+
InputGroupButton,
|
|
6
|
+
InputGroupInput,
|
|
7
|
+
} from "@agent-native/toolkit/ui/input-group";
|
|
2
8
|
import { IconSearch, IconX } from "@tabler/icons-react";
|
|
3
|
-
import { useId } from "react";
|
|
9
|
+
import { useId, useRef } from "react";
|
|
4
10
|
|
|
5
11
|
import { cn } from "../lib/utils";
|
|
6
12
|
import { Button } from "./ui/button";
|
|
7
|
-
import { Input } from "./ui/input";
|
|
8
13
|
|
|
9
14
|
export function WorkspaceAppSearch({
|
|
10
15
|
query,
|
|
@@ -17,6 +22,7 @@ export function WorkspaceAppSearch({
|
|
|
17
22
|
}) {
|
|
18
23
|
const t = useT();
|
|
19
24
|
const inputId = useId();
|
|
25
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
20
26
|
const label = t("dispatch.pages.searchApps", {
|
|
21
27
|
defaultValue: "Search apps",
|
|
22
28
|
});
|
|
@@ -24,43 +30,44 @@ export function WorkspaceAppSearch({
|
|
|
24
30
|
defaultValue: "Search apps",
|
|
25
31
|
});
|
|
26
32
|
|
|
33
|
+
const clearLabel = t("dispatch.pages.clearAppSearch", {
|
|
34
|
+
defaultValue: "Clear search",
|
|
35
|
+
});
|
|
36
|
+
|
|
27
37
|
return (
|
|
28
|
-
<div role="search" className={cn("flex items-center
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<Input
|
|
38
|
+
<div role="search" className={cn("flex items-center", className)}>
|
|
39
|
+
<label htmlFor={inputId} className="sr-only">
|
|
40
|
+
{label}
|
|
41
|
+
</label>
|
|
42
|
+
<InputGroup size="sm" className="min-w-0 flex-1">
|
|
43
|
+
<InputGroupAddon>
|
|
44
|
+
<IconSearch aria-hidden="true" />
|
|
45
|
+
</InputGroupAddon>
|
|
46
|
+
<InputGroupInput
|
|
47
|
+
ref={inputRef}
|
|
39
48
|
id={inputId}
|
|
40
|
-
size="sm"
|
|
41
49
|
value={query}
|
|
42
50
|
onChange={(event) => onQueryChange(event.target.value)}
|
|
43
51
|
placeholder={placeholder}
|
|
44
|
-
className="bg-background ps-9"
|
|
45
52
|
autoComplete="off"
|
|
46
53
|
/>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
{query ? (
|
|
55
|
+
<InputGroupAddon align="inline-end">
|
|
56
|
+
<InputGroupButton
|
|
57
|
+
size="icon-xs"
|
|
58
|
+
aria-label={clearLabel}
|
|
59
|
+
title={clearLabel}
|
|
60
|
+
onClick={() => {
|
|
61
|
+
onQueryChange("");
|
|
62
|
+
// The button unmounts once the query is empty.
|
|
63
|
+
inputRef.current?.focus();
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<IconX aria-hidden="true" />
|
|
67
|
+
</InputGroupButton>
|
|
68
|
+
</InputGroupAddon>
|
|
69
|
+
) : null}
|
|
70
|
+
</InputGroup>
|
|
64
71
|
</div>
|
|
65
72
|
);
|
|
66
73
|
}
|
|
@@ -32,6 +32,11 @@ vi.mock("@agent-native/core/client/hooks", () => ({
|
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
34
|
vi.mock("sonner", () => ({ toast }));
|
|
35
|
+
vi.mock("@agent-native/core/client/i18n", async (importOriginal) => ({
|
|
36
|
+
...(await importOriginal<typeof import("@agent-native/core/client/i18n")>()),
|
|
37
|
+
useT: () => (key: string, values?: { name?: string }) =>
|
|
38
|
+
values?.name ? `${key}:${values.name}` : key,
|
|
39
|
+
}));
|
|
35
40
|
|
|
36
41
|
const template: CuratedWorkspaceTemplate = {
|
|
37
42
|
id: "weekly-report",
|
|
@@ -194,6 +199,11 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
194
199
|
expect(
|
|
195
200
|
container.querySelector('a[href="https://reports.example.test"]'),
|
|
196
201
|
).not.toBeNull();
|
|
202
|
+
expect(
|
|
203
|
+
container
|
|
204
|
+
.querySelector(".app-open-actions__menu")
|
|
205
|
+
?.getAttribute("aria-label"),
|
|
206
|
+
).toBe("extensions.optionsFor:Weekly report");
|
|
197
207
|
});
|
|
198
208
|
|
|
199
209
|
it("accepts the list action envelope and renders installed state", async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
3
|
import { normalizeWorkspaceAppId } from "@agent-native/core/shared";
|
|
3
4
|
import {
|
|
4
5
|
IconArrowUpRight,
|
|
@@ -12,7 +13,7 @@ import { toast } from "sonner";
|
|
|
12
13
|
import { cn } from "../lib/utils";
|
|
13
14
|
import { AppIcon } from "./app-icon";
|
|
14
15
|
import { AppListRow } from "./app-list-row";
|
|
15
|
-
import { AppOpenActions } from "./app-open-actions";
|
|
16
|
+
import { AppOpenActions, appOpenActionLabels } from "./app-open-actions";
|
|
16
17
|
import { AppRowSettings } from "./app-row-settings";
|
|
17
18
|
import { useNotifyBuildInBuilder } from "./build-in-builder";
|
|
18
19
|
import { Button } from "./ui/button";
|
|
@@ -131,6 +132,7 @@ export function WorkspaceTemplateCard({
|
|
|
131
132
|
catalog = false,
|
|
132
133
|
onRemixSuccess,
|
|
133
134
|
}: WorkspaceTemplateCardProps) {
|
|
135
|
+
const t = useT();
|
|
134
136
|
const labels = useMemo(() => mergeLabels(labelOverrides), [labelOverrides]);
|
|
135
137
|
const [open, setOpen] = useState(false);
|
|
136
138
|
const [appId, setAppId] = useState(() =>
|
|
@@ -218,7 +220,10 @@ export function WorkspaceTemplateCard({
|
|
|
218
220
|
name={template.name}
|
|
219
221
|
href={liveUrl}
|
|
220
222
|
rel="noreferrer"
|
|
221
|
-
labels={{
|
|
223
|
+
labels={{
|
|
224
|
+
...appOpenActionLabels(t, template.name),
|
|
225
|
+
openApp: labels.openApp,
|
|
226
|
+
}}
|
|
222
227
|
showNewTabOption
|
|
223
228
|
/>
|
|
224
229
|
) : null}
|
package/src/db/index.spec.ts
CHANGED
|
@@ -62,6 +62,15 @@ describe("Dispatch workspace app share access", () => {
|
|
|
62
62
|
).resolves.toBe(false);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("shares one manager answer across a request org's apps", () => {
|
|
66
|
+
const key = mocks.getRegisteredResource().canManageAccessKey as (resource: {
|
|
67
|
+
orgId?: string | null;
|
|
68
|
+
}) => string | undefined;
|
|
69
|
+
|
|
70
|
+
expect(key({ orgId: "org-a" })).toBe("org-a");
|
|
71
|
+
expect(key({ orgId: null })).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
65
74
|
it("does not resolve a role for a resource outside the request org", async () => {
|
|
66
75
|
await expect(
|
|
67
76
|
canManageAccess()(
|
package/src/db/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ registerShareableResource({
|
|
|
21
21
|
allowPublic: false,
|
|
22
22
|
requireOrgMemberForUserShares: true,
|
|
23
23
|
supportsGroupShares: true,
|
|
24
|
+
canManageAccessKey: (resource) => resource.orgId ?? undefined,
|
|
24
25
|
canManageAccess: async (resource, ctx) => {
|
|
25
26
|
if (!ctx.userEmail || !ctx.orgId || resource?.orgId !== ctx.orgId) {
|
|
26
27
|
return false;
|
|
@@ -50,6 +50,31 @@ describe("filterBuiltInApps", () => {
|
|
|
50
50
|
]);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it("leaves out hosted apps the organization hides unless asked to keep them", () => {
|
|
54
|
+
const apps = [
|
|
55
|
+
{
|
|
56
|
+
id: "slides",
|
|
57
|
+
name: "Slides",
|
|
58
|
+
url: "https://slides.agent-native.com",
|
|
59
|
+
source: "builtin" as const,
|
|
60
|
+
hiddenFromLaunchers: true,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "clips",
|
|
64
|
+
name: "Clips",
|
|
65
|
+
url: "https://clips.agent-native.com",
|
|
66
|
+
source: "builtin" as const,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
expect(filterBuiltInApps(apps, []).map((app) => app.id)).toEqual(["clips"]);
|
|
71
|
+
expect(
|
|
72
|
+
filterBuiltInApps(apps, [], { includeHiddenFromLaunchers: true }).map(
|
|
73
|
+
(app) => app.id,
|
|
74
|
+
),
|
|
75
|
+
).toEqual(["clips", "slides"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
53
78
|
it("keeps built-in app launches on the current beta lane", () => {
|
|
54
79
|
vi.stubGlobal("window", {
|
|
55
80
|
location: { hostname: "beta.agent-workspace.builder.io" },
|
package/src/lib/other-apps.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ConnectedAppSummary {
|
|
|
11
11
|
homeUrl?: string;
|
|
12
12
|
color?: string;
|
|
13
13
|
source?: "builtin" | "custom" | "workspace";
|
|
14
|
+
/** Set by list-connected-agents when the organization hides hosted apps. */
|
|
15
|
+
hiddenFromLaunchers?: boolean;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface WorkspaceAppId {
|
|
@@ -58,9 +60,15 @@ export function filterOtherApps(
|
|
|
58
60
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Hosted apps for launcher surfaces. Rows the organization hides are left out
|
|
65
|
+
* unless `includeHiddenFromLaunchers` is set, which callers that resolve an
|
|
66
|
+
* app id to open (pane restore, `open_app`) need.
|
|
67
|
+
*/
|
|
61
68
|
export function filterBuiltInApps(
|
|
62
69
|
connectedApps: ConnectedAppSummary[],
|
|
63
70
|
workspaceApps: WorkspaceAppId[],
|
|
71
|
+
{ includeHiddenFromLaunchers = false } = {},
|
|
64
72
|
): ConnectedAppSummary[] {
|
|
65
73
|
const workspaceAppIds = new Set([
|
|
66
74
|
"dispatch",
|
|
@@ -75,6 +83,7 @@ export function filterBuiltInApps(
|
|
|
75
83
|
if (
|
|
76
84
|
!id ||
|
|
77
85
|
app.source !== "builtin" ||
|
|
86
|
+
(app.hiddenFromLaunchers && !includeHiddenFromLaunchers) ||
|
|
78
87
|
isDefaultWorkspaceAppHiddenId(id) ||
|
|
79
88
|
workspaceAppIds.has(id) ||
|
|
80
89
|
seen.has(id) ||
|
|
@@ -2,9 +2,12 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
normalizeWorkspaceAppLayout,
|
|
5
|
+
setWorkspaceAppPinned,
|
|
5
6
|
orderWorkspaceApps,
|
|
6
|
-
|
|
7
|
+
tierWorkspaceApps,
|
|
7
8
|
workspaceAppMatchesQuery,
|
|
9
|
+
workspaceAppsStatus,
|
|
10
|
+
appListsStatus,
|
|
8
11
|
} from "./workspace-app-layout";
|
|
9
12
|
import { mergeChatFirstWorkspaceApps } from "./workspace-apps";
|
|
10
13
|
|
|
@@ -51,15 +54,70 @@ describe("workspace app layout", () => {
|
|
|
51
54
|
expect(workspaceAppMatchesQuery({ name: "Mail" }, "calendar")).toBe(false);
|
|
52
55
|
});
|
|
53
56
|
|
|
54
|
-
it("normalizes persisted layout ids and
|
|
57
|
+
it("normalizes persisted layout ids and sets pin state", () => {
|
|
55
58
|
const layout = normalizeWorkspaceAppLayout({
|
|
56
59
|
pinnedIds: ["Mail", "mail", 42],
|
|
57
60
|
orderedIds: ["Calendar", "", "calendar"],
|
|
58
61
|
});
|
|
59
62
|
|
|
60
|
-
expect(
|
|
63
|
+
expect(setWorkspaceAppPinned(layout, "CALENDAR", true)).toEqual({
|
|
61
64
|
pinnedIds: ["calendar", "mail"],
|
|
62
65
|
orderedIds: ["calendar"],
|
|
63
66
|
});
|
|
67
|
+
// Already in the asked-for state: nothing moves.
|
|
68
|
+
expect(
|
|
69
|
+
setWorkspaceAppPinned(
|
|
70
|
+
{ pinnedIds: ["a", "b"], orderedIds: [] },
|
|
71
|
+
"b",
|
|
72
|
+
true,
|
|
73
|
+
),
|
|
74
|
+
).toEqual({ pinnedIds: ["a", "b"], orderedIds: [] });
|
|
75
|
+
expect(setWorkspaceAppPinned(layout, "mail", false).pinnedIds).toEqual([]);
|
|
76
|
+
expect(setWorkspaceAppPinned(layout, "plan", false).pinnedIds).toEqual([
|
|
77
|
+
"mail",
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("tiers pinned apps in pin order, then apps and hosted apps each A to Z", () => {
|
|
82
|
+
const apps = [
|
|
83
|
+
{ id: "gamma", name: "Gamma" },
|
|
84
|
+
{ id: "zeta", name: "Zeta" },
|
|
85
|
+
{ id: "mail", name: "Mail", source: "builtin" },
|
|
86
|
+
{ id: "Alpha", name: "alpha" },
|
|
87
|
+
{ id: "calendar", name: "Calendar", source: "builtin" },
|
|
88
|
+
{ id: "clips", name: "Clips", source: "builtin" },
|
|
89
|
+
{ id: "beta", name: "Beta", source: "connected" },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const tiers = tierWorkspaceApps(apps, {
|
|
93
|
+
pinnedIds: ["clips", "zeta", "missing"],
|
|
94
|
+
orderedIds: ["beta", "alpha"],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(tiers.pinned.map((app) => app.id)).toEqual(["clips", "zeta"]);
|
|
98
|
+
expect(tiers.apps.map((app) => app.id)).toEqual(["Alpha", "beta", "gamma"]);
|
|
99
|
+
expect(tiers.hosted.map((app) => app.id)).toEqual(["calendar", "mail"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it.each([
|
|
103
|
+
[{ data: undefined, isError: false }, "loading"],
|
|
104
|
+
[{ data: undefined, isError: true }, "error"],
|
|
105
|
+
[{ data: [], isError: false }, "ready"],
|
|
106
|
+
// A failed refresh keeps the list it already had.
|
|
107
|
+
[{ data: [], isError: true }, "ready"],
|
|
108
|
+
] as const)("reports %o as %s", (query, status) => {
|
|
109
|
+
expect(workspaceAppsStatus(query)).toBe(status);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("keeps the app lists loading until the hosted list settles", () => {
|
|
113
|
+
const ready = { data: [], isError: false };
|
|
114
|
+
const pending = { data: undefined, isError: false };
|
|
115
|
+
const failed = { data: undefined, isError: true };
|
|
116
|
+
expect(appListsStatus(pending, ready)).toBe("loading");
|
|
117
|
+
expect(appListsStatus(failed, ready)).toBe("error");
|
|
118
|
+
expect(appListsStatus(ready, pending)).toBe("loading");
|
|
119
|
+
// A failed hosted list is reported on its own; workspace apps still show.
|
|
120
|
+
expect(appListsStatus(ready, failed)).toBe("ready");
|
|
121
|
+
expect(appListsStatus(ready, ready)).toBe("ready");
|
|
64
122
|
});
|
|
65
123
|
});
|