@agent-native/dispatch 0.40.30-nightly-20261021064625 → 0.40.30
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 +7 -47
- package/dist/actions/connect-external-agent.d.ts.map +1 -1
- package/dist/actions/connect-external-agent.js +12 -13
- package/dist/actions/connect-external-agent.js.map +1 -1
- package/dist/actions/create-workspace-resource.d.ts +13 -13
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/import-agent.d.ts +13 -13
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/inspect-connect-agent-card.d.ts +5 -0
- package/dist/actions/inspect-connect-agent-card.d.ts.map +1 -0
- package/dist/actions/inspect-connect-agent-card.js +15 -0
- package/dist/actions/inspect-connect-agent-card.js.map +1 -0
- package/dist/actions/install-curated-community-app.d.ts +36 -0
- package/dist/actions/install-curated-community-app.d.ts.map +1 -0
- package/dist/actions/install-curated-community-app.js +26 -0
- package/dist/actions/install-curated-community-app.js.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts +3 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.js +15 -0
- package/dist/actions/list-connect-marketplace-apps.js.map +1 -0
- package/dist/actions/list-connected-agents.d.ts +2 -0
- package/dist/actions/list-curated-community-apps.d.ts +3 -0
- package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
- package/dist/actions/list-curated-community-apps.js +10 -0
- package/dist/actions/list-curated-community-apps.js.map +1 -0
- package/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/actions/remix-workspace-template.d.ts +6 -0
- package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
- package/dist/actions/update-workspace-app-metadata.js +26 -5
- package/dist/actions/update-workspace-app-metadata.js.map +1 -1
- package/dist/components/community-apps-section.d.ts +2 -0
- package/dist/components/community-apps-section.d.ts.map +1 -0
- package/dist/components/community-apps-section.js +90 -0
- package/dist/components/community-apps-section.js.map +1 -0
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +63 -9
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/lib/connect-apps.d.ts +1 -2
- package/dist/lib/connect-apps.d.ts.map +1 -1
- package/dist/lib/connect-apps.js +41 -65
- package/dist/lib/connect-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -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 +2 -1
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/connect.d.ts.map +1 -1
- package/dist/routes/pages/connect.js +5 -7
- package/dist/routes/pages/connect.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +11 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +258 -39
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.d.ts +45 -0
- package/dist/server/lib/community-apps.d.ts.map +1 -0
- package/dist/server/lib/community-apps.js +267 -0
- package/dist/server/lib/community-apps.js.map +1 -0
- package/dist/server/lib/community-source.d.ts +19 -0
- package/dist/server/lib/community-source.d.ts.map +1 -0
- package/dist/server/lib/community-source.js +118 -0
- package/dist/server/lib/community-source.js.map +1 -0
- package/dist/server/lib/connect-apps.d.ts +4 -0
- package/dist/server/lib/connect-apps.d.ts.map +1 -0
- package/dist/server/lib/connect-apps.js +99 -0
- package/dist/server/lib/connect-apps.js.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.js +44 -0
- package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/labs.d.ts +2 -0
- package/dist/shared/labs.d.ts.map +1 -0
- package/dist/shared/labs.js +8 -0
- package/dist/shared/labs.js.map +1 -0
- package/package.json +5 -4
- package/src/actions/connect-external-agent.spec.ts +101 -3
- package/src/actions/connect-external-agent.ts +12 -11
- package/src/actions/index.spec.ts +13 -0
- package/src/actions/index.ts +8 -0
- package/src/actions/inspect-connect-agent-card.ts +17 -0
- package/src/actions/install-curated-community-app.ts +32 -0
- package/src/actions/list-connect-marketplace-apps.ts +17 -0
- package/src/actions/list-curated-community-apps.ts +12 -0
- package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
- package/src/actions/update-workspace-app-metadata.ts +48 -24
- package/src/components/community-apps-section.tsx +224 -0
- package/src/components/workspace-app-card.spec.tsx +226 -1
- package/src/components/workspace-app-card.tsx +115 -12
- package/src/lib/connect-apps.spec.ts +118 -66
- package/src/lib/connect-apps.ts +47 -82
- package/src/lib/workspace-apps.ts +1 -0
- package/src/routes/pages/apps.spec.tsx +201 -1
- package/src/routes/pages/apps.tsx +3 -0
- package/src/routes/pages/connect.spec.tsx +53 -6
- package/src/routes/pages/connect.tsx +14 -11
- package/src/server/index.ts +9 -0
- package/src/server/lib/app-creation-store.spec.ts +539 -11
- package/src/server/lib/app-creation-store.ts +332 -41
- package/src/server/lib/community-apps.spec.ts +367 -0
- package/src/server/lib/community-apps.ts +372 -0
- package/src/server/lib/community-source.spec.ts +151 -0
- package/src/server/lib/community-source.ts +186 -0
- package/src/server/lib/connect-apps.ts +149 -0
- package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
- package/src/server/lib/org-apps-directory-cache.ts +66 -0
- package/src/server/plugins/integrations.spec.ts +60 -0
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/labs.ts +9 -0
|
@@ -1,33 +1,57 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core/action";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getWorkspaceAppIdValidationError,
|
|
4
|
+
parseAgentProfileMetadata,
|
|
5
|
+
} from "@agent-native/core/shared";
|
|
3
6
|
import { z } from "zod";
|
|
4
7
|
|
|
5
8
|
import { updateWorkspaceAppMetadata } from "../server/lib/app-creation-store.js";
|
|
6
9
|
|
|
7
10
|
export default defineAction({
|
|
8
11
|
description:
|
|
9
|
-
"Update
|
|
10
|
-
schema: z
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
"Update a workspace app's display name, description, and when-to-use profile. Only organization owners and admins can edit these shared details.",
|
|
13
|
+
schema: z
|
|
14
|
+
.object({
|
|
15
|
+
appId: z
|
|
16
|
+
.string()
|
|
17
|
+
.max(64)
|
|
18
|
+
.refine((appId) => !getWorkspaceAppIdValidationError(appId), {
|
|
19
|
+
message:
|
|
20
|
+
"Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
|
|
21
|
+
})
|
|
22
|
+
.describe("Workspace app id, matching the apps/<id> folder"),
|
|
23
|
+
name: z
|
|
24
|
+
.string()
|
|
25
|
+
.max(120)
|
|
26
|
+
.optional()
|
|
27
|
+
.nullable()
|
|
28
|
+
.describe("Human-readable app name"),
|
|
29
|
+
description: z
|
|
30
|
+
.string()
|
|
31
|
+
.max(1000)
|
|
32
|
+
.optional()
|
|
33
|
+
.nullable()
|
|
34
|
+
.describe("Human-readable app description, up to 1,000 characters"),
|
|
35
|
+
whenToUse: z
|
|
36
|
+
.array(z.string())
|
|
37
|
+
.max(6)
|
|
38
|
+
.optional()
|
|
39
|
+
.nullable()
|
|
40
|
+
.describe("Example requests that indicate when this app is useful"),
|
|
41
|
+
})
|
|
42
|
+
.superRefine(({ description, whenToUse }, context) => {
|
|
43
|
+
if (description == null && whenToUse == null) return;
|
|
44
|
+
const profile = parseAgentProfileMetadata({
|
|
45
|
+
...(description != null ? { description } : {}),
|
|
46
|
+
...(whenToUse != null ? { whenToUse } : {}),
|
|
47
|
+
});
|
|
48
|
+
if (!profile) {
|
|
49
|
+
context.addIssue({
|
|
50
|
+
code: "custom",
|
|
51
|
+
message:
|
|
52
|
+
"Use a description up to 1,000 characters and up to 6 non-empty examples, each no longer than 140 characters.",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
32
56
|
run: async (args) => updateWorkspaceAppMetadata(args),
|
|
33
57
|
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
|
|
2
|
+
import {
|
|
3
|
+
useActionMutation,
|
|
4
|
+
useActionQuery,
|
|
5
|
+
} from "@agent-native/core/client/hooks";
|
|
6
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
7
|
+
import { useLab } from "@agent-native/core/client/labs";
|
|
8
|
+
import {
|
|
9
|
+
IconArrowUpRight,
|
|
10
|
+
IconCircleCheck,
|
|
11
|
+
IconPlus,
|
|
12
|
+
} from "@tabler/icons-react";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
|
|
16
|
+
import { COMMUNITY_APPS_LAB } from "../shared/labs.js";
|
|
17
|
+
import { ActionQueryError } from "./action-query-error";
|
|
18
|
+
import { AppIcon } from "./app-icon";
|
|
19
|
+
import { AppList, AppListRow } from "./app-list-row";
|
|
20
|
+
import { Badge } from "./ui/badge";
|
|
21
|
+
import { Button } from "./ui/button";
|
|
22
|
+
import { Skeleton } from "./ui/skeleton";
|
|
23
|
+
import { Spinner } from "./ui/spinner";
|
|
24
|
+
|
|
25
|
+
interface CommunityAppSource {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
appId: string;
|
|
30
|
+
repo: string;
|
|
31
|
+
ref: string;
|
|
32
|
+
license: string;
|
|
33
|
+
provenance: string;
|
|
34
|
+
templatePath: string;
|
|
35
|
+
installed: boolean;
|
|
36
|
+
pending: boolean;
|
|
37
|
+
installable: boolean;
|
|
38
|
+
installUnavailableReason?:
|
|
39
|
+
| "workspace-required"
|
|
40
|
+
| "app-id-conflict"
|
|
41
|
+
| "builder-install-unverified";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pinnedSourceUrl(source: CommunityAppSource): string {
|
|
45
|
+
return (
|
|
46
|
+
"https://github.com/" +
|
|
47
|
+
source.repo +
|
|
48
|
+
"/tree/" +
|
|
49
|
+
source.ref +
|
|
50
|
+
"/" +
|
|
51
|
+
source.templatePath
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function CommunityAppsSection() {
|
|
56
|
+
const t = useT();
|
|
57
|
+
const enabled = useLab(COMMUNITY_APPS_LAB);
|
|
58
|
+
const [installingId, setInstallingId] = useState<string | null>(null);
|
|
59
|
+
const catalogQuery = useActionQuery<CommunityAppSource[]>(
|
|
60
|
+
"list-curated-community-apps" as never,
|
|
61
|
+
{},
|
|
62
|
+
{ enabled },
|
|
63
|
+
);
|
|
64
|
+
const workspaceAppsQuery = useActionQuery("list-workspace-apps", {
|
|
65
|
+
includeAgentCards: false,
|
|
66
|
+
includeArchived: true,
|
|
67
|
+
});
|
|
68
|
+
const install = useActionMutation("install-curated-community-app", {
|
|
69
|
+
onSuccess: (result) => {
|
|
70
|
+
const mode = (result as { mode?: string } | null)?.mode;
|
|
71
|
+
const message = (result as { message?: string } | null)?.message;
|
|
72
|
+
if (mode === "builder") {
|
|
73
|
+
toast.success(t("dispatch.pages.communityAppsInstallStarted"));
|
|
74
|
+
} else if (mode === "pending") {
|
|
75
|
+
toast.info(t("dispatch.pages.communityAppsPending"));
|
|
76
|
+
} else if (mode === "already-installed") {
|
|
77
|
+
toast.success(t("dispatch.pages.communityAppsAlreadyInstalled"));
|
|
78
|
+
} else if (mode === "builder-unavailable" || mode === "app-id-taken") {
|
|
79
|
+
toast.error(message || t("dispatch.pages.communityAppsInstallFailed"));
|
|
80
|
+
} else if (mode === "local-agent") {
|
|
81
|
+
sendToAgentChat({
|
|
82
|
+
message:
|
|
83
|
+
(result as { prompt?: string } | null)?.prompt ||
|
|
84
|
+
t("dispatch.pages.communityAppsLocalRequestReady"),
|
|
85
|
+
attachments: (
|
|
86
|
+
result as {
|
|
87
|
+
attachments?: Array<{
|
|
88
|
+
type: "file";
|
|
89
|
+
name: string;
|
|
90
|
+
contentType: string;
|
|
91
|
+
text: string;
|
|
92
|
+
}>;
|
|
93
|
+
} | null
|
|
94
|
+
)?.attachments,
|
|
95
|
+
submit: true,
|
|
96
|
+
type: "code",
|
|
97
|
+
newTab: true,
|
|
98
|
+
reuseEmptyTab: true,
|
|
99
|
+
});
|
|
100
|
+
toast.info(t("dispatch.pages.communityAppsLocalRequestReady"));
|
|
101
|
+
} else {
|
|
102
|
+
toast.error(t("dispatch.pages.communityAppsInstallFailed"));
|
|
103
|
+
}
|
|
104
|
+
setInstallingId(null);
|
|
105
|
+
void catalogQuery.refetch();
|
|
106
|
+
void workspaceAppsQuery.refetch();
|
|
107
|
+
},
|
|
108
|
+
onError: () => {
|
|
109
|
+
setInstallingId(null);
|
|
110
|
+
toast.error(t("dispatch.pages.communityAppsInstallFailed"));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!enabled) return null;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<section className="space-y-3 border-t pt-6">
|
|
118
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
119
|
+
{t("dispatch.pages.communityApps")}
|
|
120
|
+
</h2>
|
|
121
|
+
{catalogQuery.isError ? (
|
|
122
|
+
<ActionQueryError
|
|
123
|
+
error={catalogQuery.error}
|
|
124
|
+
onRetry={() => void catalogQuery.refetch()}
|
|
125
|
+
/>
|
|
126
|
+
) : catalogQuery.isLoading ? (
|
|
127
|
+
<AppList className="divide-y rounded-xl border">
|
|
128
|
+
<div className="flex items-center gap-3 px-4 py-3.5">
|
|
129
|
+
<Skeleton className="size-8 rounded-lg" />
|
|
130
|
+
<Skeleton className="h-4 w-40" />
|
|
131
|
+
</div>
|
|
132
|
+
</AppList>
|
|
133
|
+
) : catalogQuery.data?.length ? (
|
|
134
|
+
<AppList className="divide-y rounded-xl border">
|
|
135
|
+
{catalogQuery.data.map((source) => (
|
|
136
|
+
<AppListRow key={source.id} className="border-b-0">
|
|
137
|
+
<AppIcon id={source.id} name={source.name} size="sm" />
|
|
138
|
+
<div className="min-w-0 flex-1">
|
|
139
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
140
|
+
<h3 className="truncate text-sm font-semibold text-foreground">
|
|
141
|
+
{source.name}
|
|
142
|
+
</h3>
|
|
143
|
+
{source.installed ? (
|
|
144
|
+
<Badge variant="secondary" className="gap-1">
|
|
145
|
+
<IconCircleCheck size={13} />
|
|
146
|
+
{t("dispatch.pages.communityAppsInstalled")}
|
|
147
|
+
</Badge>
|
|
148
|
+
) : source.pending ? (
|
|
149
|
+
<Badge variant="secondary">
|
|
150
|
+
{t("dispatch.pages.communityAppsPending")}
|
|
151
|
+
</Badge>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
{source.description ? (
|
|
155
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
156
|
+
{source.description}
|
|
157
|
+
</p>
|
|
158
|
+
) : null}
|
|
159
|
+
<div className="mt-1 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
|
|
160
|
+
<span>
|
|
161
|
+
{t("dispatch.pages.communityAppsLicense")}: {source.license}
|
|
162
|
+
</span>
|
|
163
|
+
<a
|
|
164
|
+
href={pinnedSourceUrl(source)}
|
|
165
|
+
target="_blank"
|
|
166
|
+
rel="noreferrer"
|
|
167
|
+
className="inline-flex items-center gap-1 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
168
|
+
>
|
|
169
|
+
{t("dispatch.pages.communityAppsTemplateSource")}
|
|
170
|
+
<IconArrowUpRight size={12} aria-hidden="true" />
|
|
171
|
+
</a>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
{source.installable ? (
|
|
175
|
+
<Button
|
|
176
|
+
type="button"
|
|
177
|
+
size="sm"
|
|
178
|
+
variant="outline"
|
|
179
|
+
disabled={install.isPending}
|
|
180
|
+
onClick={() => {
|
|
181
|
+
setInstallingId(source.id);
|
|
182
|
+
install.mutate({ sourceId: source.id });
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{install.isPending && installingId === source.id ? (
|
|
186
|
+
<Spinner className="mr-1.5" />
|
|
187
|
+
) : (
|
|
188
|
+
<IconPlus size={14} className="mr-1.5" />
|
|
189
|
+
)}
|
|
190
|
+
{install.isPending && installingId === source.id
|
|
191
|
+
? t("dispatch.pages.communityAppsInstalling")
|
|
192
|
+
: t("dispatch.pages.communityAppsInstall")}
|
|
193
|
+
</Button>
|
|
194
|
+
) : !source.installed &&
|
|
195
|
+
!source.pending &&
|
|
196
|
+
source.installUnavailableReason === "workspace-required" ? (
|
|
197
|
+
<span className="max-w-48 text-right text-xs text-muted-foreground">
|
|
198
|
+
{t("dispatch.pages.communityAppsWorkspaceRequired")}
|
|
199
|
+
</span>
|
|
200
|
+
) : !source.installed &&
|
|
201
|
+
!source.pending &&
|
|
202
|
+
source.installUnavailableReason === "app-id-conflict" ? (
|
|
203
|
+
<span className="max-w-48 text-right text-xs text-muted-foreground">
|
|
204
|
+
{t("dispatch.pages.communityAppsIdConflict")}
|
|
205
|
+
</span>
|
|
206
|
+
) : !source.installed &&
|
|
207
|
+
!source.pending &&
|
|
208
|
+
source.installUnavailableReason ===
|
|
209
|
+
"builder-install-unverified" ? (
|
|
210
|
+
<span className="max-w-48 text-right text-xs text-muted-foreground">
|
|
211
|
+
{t("dispatch.pages.communityAppsBuilderInstallUnavailable")}
|
|
212
|
+
</span>
|
|
213
|
+
) : null}
|
|
214
|
+
</AppListRow>
|
|
215
|
+
))}
|
|
216
|
+
</AppList>
|
|
217
|
+
) : (
|
|
218
|
+
<p className="text-sm text-muted-foreground">
|
|
219
|
+
{t("dispatch.pages.communityAppsEmpty")}
|
|
220
|
+
</p>
|
|
221
|
+
)}
|
|
222
|
+
</section>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -34,7 +34,13 @@ vi.mock("../../../core/dist/client/i18n.js", () => ({
|
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
36
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
37
|
-
useActionMutation: () => ({
|
|
37
|
+
useActionMutation: (action: string) => ({
|
|
38
|
+
mutate:
|
|
39
|
+
action === "update-workspace-app-metadata"
|
|
40
|
+
? metadataMutationState.mutate
|
|
41
|
+
: vi.fn(),
|
|
42
|
+
isPending: false,
|
|
43
|
+
}),
|
|
38
44
|
useActionQuery: () => ({
|
|
39
45
|
data: { resources: [], counts: {} },
|
|
40
46
|
isLoading: false,
|
|
@@ -53,6 +59,8 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
53
59
|
}));
|
|
54
60
|
|
|
55
61
|
const frameState = vi.hoisted(() => ({ inBuilderFrame: false }));
|
|
62
|
+
const orgRoleState = vi.hoisted(() => ({ role: "owner" as string }));
|
|
63
|
+
const metadataMutationState = vi.hoisted(() => ({ mutate: vi.fn() }));
|
|
56
64
|
|
|
57
65
|
function LocationProbe() {
|
|
58
66
|
const location = useLocation();
|
|
@@ -64,6 +72,24 @@ vi.mock("@agent-native/core/client/host", () => ({
|
|
|
64
72
|
isInBuilderFrame: () => frameState.inBuilderFrame,
|
|
65
73
|
}));
|
|
66
74
|
|
|
75
|
+
vi.mock("@agent-native/core/client/org", () => ({
|
|
76
|
+
useOrgRole: () => ({
|
|
77
|
+
org: { orgId: "org-123" },
|
|
78
|
+
role: orgRoleState.role,
|
|
79
|
+
isLoading: false,
|
|
80
|
+
error: null,
|
|
81
|
+
}),
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
function setTextareaValue(textarea: HTMLTextAreaElement, value: string) {
|
|
85
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
86
|
+
HTMLTextAreaElement.prototype,
|
|
87
|
+
"value",
|
|
88
|
+
)?.set;
|
|
89
|
+
setter?.call(textarea, value);
|
|
90
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
describe("WorkspaceAppCard", () => {
|
|
68
94
|
let container: HTMLDivElement;
|
|
69
95
|
let root: Root;
|
|
@@ -71,6 +97,8 @@ describe("WorkspaceAppCard", () => {
|
|
|
71
97
|
|
|
72
98
|
beforeEach(() => {
|
|
73
99
|
frameState.inBuilderFrame = false;
|
|
100
|
+
orgRoleState.role = "owner";
|
|
101
|
+
metadataMutationState.mutate.mockClear();
|
|
74
102
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
75
103
|
container = document.createElement("div");
|
|
76
104
|
document.body.appendChild(container);
|
|
@@ -90,6 +118,203 @@ describe("WorkspaceAppCard", () => {
|
|
|
90
118
|
vi.unstubAllGlobals();
|
|
91
119
|
});
|
|
92
120
|
|
|
121
|
+
it("edits the app's when-to-use examples with the existing details form", async () => {
|
|
122
|
+
await act(async () => {
|
|
123
|
+
root.render(
|
|
124
|
+
<MemoryRouter>
|
|
125
|
+
<TooltipProvider>
|
|
126
|
+
<WorkspaceAppCard
|
|
127
|
+
app={{
|
|
128
|
+
id: "analytics",
|
|
129
|
+
name: "Analytics",
|
|
130
|
+
path: "/analytics",
|
|
131
|
+
status: "ready",
|
|
132
|
+
whenToUse: [
|
|
133
|
+
"Explain this week's activation trend",
|
|
134
|
+
"Compare signup sources",
|
|
135
|
+
],
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
</TooltipProvider>
|
|
139
|
+
</MemoryRouter>,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
144
|
+
'button[aria-label="Settings for Analytics"]',
|
|
145
|
+
);
|
|
146
|
+
await act(async () => {
|
|
147
|
+
settingsButton?.dispatchEvent(
|
|
148
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
const editItem = Array.from(
|
|
152
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
153
|
+
).find((item) => item.textContent?.includes("Edit details"));
|
|
154
|
+
expect(editItem).not.toBeUndefined();
|
|
155
|
+
|
|
156
|
+
await act(async () => editItem?.click());
|
|
157
|
+
|
|
158
|
+
const whenToUse = document.querySelector<HTMLTextAreaElement>(
|
|
159
|
+
"#app-when-to-use-analytics",
|
|
160
|
+
);
|
|
161
|
+
expect(whenToUse?.value).toBe(
|
|
162
|
+
"Explain this week's activation trend\nCompare signup sources",
|
|
163
|
+
);
|
|
164
|
+
expect(document.body.textContent).toContain("When to use");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("validates profile examples and normalizes description line breaks", async () => {
|
|
168
|
+
await act(async () => {
|
|
169
|
+
root.render(
|
|
170
|
+
<MemoryRouter>
|
|
171
|
+
<TooltipProvider>
|
|
172
|
+
<WorkspaceAppCard
|
|
173
|
+
app={{
|
|
174
|
+
id: "analytics",
|
|
175
|
+
name: "Analytics",
|
|
176
|
+
path: "/analytics",
|
|
177
|
+
status: "ready",
|
|
178
|
+
whenToUse: ["Compare signup sources"],
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
</TooltipProvider>
|
|
182
|
+
</MemoryRouter>,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
187
|
+
'button[aria-label="Settings for Analytics"]',
|
|
188
|
+
);
|
|
189
|
+
await act(async () => {
|
|
190
|
+
settingsButton?.dispatchEvent(
|
|
191
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
const editItem = Array.from(
|
|
195
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
196
|
+
).find((item) => item.textContent?.includes("Edit details"));
|
|
197
|
+
await act(async () => editItem?.click());
|
|
198
|
+
|
|
199
|
+
const examples = document.querySelector<HTMLTextAreaElement>(
|
|
200
|
+
"#app-when-to-use-analytics",
|
|
201
|
+
);
|
|
202
|
+
const description = document.querySelector<HTMLTextAreaElement>(
|
|
203
|
+
"#app-description-analytics",
|
|
204
|
+
);
|
|
205
|
+
const save = Array.from(
|
|
206
|
+
document.querySelectorAll<HTMLButtonElement>('button[type="submit"]'),
|
|
207
|
+
).find((button) => button.textContent?.includes("Save"));
|
|
208
|
+
expect(examples).not.toBeNull();
|
|
209
|
+
expect(description).not.toBeNull();
|
|
210
|
+
expect(save).not.toBeNull();
|
|
211
|
+
|
|
212
|
+
await act(async () => {
|
|
213
|
+
setTextareaValue(
|
|
214
|
+
examples!,
|
|
215
|
+
Array.from({ length: 7 }, (_, i) => `Example ${i + 1}`).join("\n"),
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
expect(save?.disabled).toBe(true);
|
|
219
|
+
|
|
220
|
+
await act(async () => {
|
|
221
|
+
setTextareaValue(examples!, `${"x".repeat(141)}\nValid example`);
|
|
222
|
+
});
|
|
223
|
+
expect(save?.disabled).toBe(true);
|
|
224
|
+
|
|
225
|
+
await act(async () => {
|
|
226
|
+
setTextareaValue(examples!, "Safe\u0001 example");
|
|
227
|
+
setTextareaValue(description!, "First line\nSecond line");
|
|
228
|
+
});
|
|
229
|
+
expect(examples?.value).toBe("Safe example");
|
|
230
|
+
expect(description?.value).toBe("First line Second line");
|
|
231
|
+
expect(save?.disabled).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("submits only the profile field that changed", async () => {
|
|
235
|
+
await act(async () => {
|
|
236
|
+
root.render(
|
|
237
|
+
<MemoryRouter>
|
|
238
|
+
<TooltipProvider>
|
|
239
|
+
<WorkspaceAppCard
|
|
240
|
+
app={{
|
|
241
|
+
id: "analytics",
|
|
242
|
+
name: "Analytics",
|
|
243
|
+
description: "Original description",
|
|
244
|
+
path: "/analytics",
|
|
245
|
+
status: "ready",
|
|
246
|
+
whenToUse: ["Compare signup sources"],
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
</TooltipProvider>
|
|
250
|
+
</MemoryRouter>,
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
255
|
+
'button[aria-label="Settings for Analytics"]',
|
|
256
|
+
);
|
|
257
|
+
await act(async () => {
|
|
258
|
+
settingsButton?.dispatchEvent(
|
|
259
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
const editItem = Array.from(
|
|
263
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
264
|
+
).find((item) => item.textContent?.includes("Edit details"));
|
|
265
|
+
await act(async () => editItem?.click());
|
|
266
|
+
|
|
267
|
+
const examples = document.querySelector<HTMLTextAreaElement>(
|
|
268
|
+
"#app-when-to-use-analytics",
|
|
269
|
+
);
|
|
270
|
+
const form = examples?.closest("form");
|
|
271
|
+
await act(async () => {
|
|
272
|
+
setTextareaValue(examples!, "Track acquisition by channel");
|
|
273
|
+
form?.dispatchEvent(
|
|
274
|
+
new Event("submit", { bubbles: true, cancelable: true }),
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(metadataMutationState.mutate).toHaveBeenCalledWith({
|
|
279
|
+
appId: "analytics",
|
|
280
|
+
whenToUse: ["Track acquisition by channel"],
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("hides Edit details from members", async () => {
|
|
285
|
+
orgRoleState.role = "member";
|
|
286
|
+
await act(async () => {
|
|
287
|
+
root.render(
|
|
288
|
+
<MemoryRouter>
|
|
289
|
+
<TooltipProvider>
|
|
290
|
+
<WorkspaceAppCard
|
|
291
|
+
app={{
|
|
292
|
+
id: "analytics",
|
|
293
|
+
name: "Analytics",
|
|
294
|
+
path: "/analytics",
|
|
295
|
+
status: "ready",
|
|
296
|
+
}}
|
|
297
|
+
/>
|
|
298
|
+
</TooltipProvider>
|
|
299
|
+
</MemoryRouter>,
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
304
|
+
'button[aria-label="Settings for Analytics"]',
|
|
305
|
+
);
|
|
306
|
+
await act(async () => {
|
|
307
|
+
settingsButton?.dispatchEvent(
|
|
308
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
expect(
|
|
312
|
+
Array.from(
|
|
313
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
314
|
+
).some((item) => item.textContent?.includes("Edit details")),
|
|
315
|
+
).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
93
318
|
it("opens ready apps inline and keeps new-tab opening in the chevron menu", async () => {
|
|
94
319
|
await act(async () => {
|
|
95
320
|
root.render(
|