@agent-native/dispatch 0.19.1 → 0.20.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/actions/provider-api-register.d.ts +9 -9
- package/dist/components/admin-navigation.d.ts +2 -1
- package/dist/components/admin-navigation.d.ts.map +1 -1
- package/dist/components/admin-navigation.js +10 -7
- package/dist/components/admin-navigation.js.map +1 -1
- package/dist/components/app-icon.d.ts +9 -0
- package/dist/components/app-icon.d.ts.map +1 -0
- package/dist/components/app-icon.js +85 -0
- package/dist/components/app-icon.js.map +1 -0
- package/dist/components/app-list-row.d.ts +12 -0
- package/dist/components/app-list-row.d.ts.map +1 -0
- package/dist/components/app-list-row.js +11 -0
- package/dist/components/app-list-row.js.map +1 -0
- package/dist/components/app-open-actions.d.ts +17 -0
- package/dist/components/app-open-actions.d.ts.map +1 -0
- package/dist/components/app-open-actions.js +20 -0
- package/dist/components/app-open-actions.js.map +1 -0
- package/dist/components/connected-app-card.d.ts +2 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +8 -8
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +21 -14
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +40 -19
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
- package/dist/components/layout/workspace-apps-rail.js +10 -10
- package/dist/components/layout/workspace-apps-rail.js.map +1 -1
- package/dist/components/other-apps-section.d.ts +31 -0
- package/dist/components/other-apps-section.d.ts.map +1 -0
- package/dist/components/other-apps-section.js +59 -0
- package/dist/components/other-apps-section.js.map +1 -0
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +38 -28
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts +8 -2
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +17 -13
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +5 -1
- package/dist/lib/other-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 +3 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +0 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/admin._index.d.ts.map +1 -1
- package/dist/routes/pages/admin._index.js +32 -7
- package/dist/routes/pages/admin._index.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +62 -13
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +21 -30
- package/dist/routes/pages/apps.js.map +1 -1
- package/package.json +3 -3
- package/src/components/admin-navigation.tsx +21 -16
- package/src/components/app-icon.tsx +149 -0
- package/src/components/app-list-row.tsx +40 -0
- package/src/components/app-open-actions.tsx +111 -0
- package/src/components/connected-app-card.tsx +30 -34
- package/src/components/dispatch-control-plane.spec.tsx +28 -1
- package/src/components/dispatch-control-plane.tsx +122 -48
- package/src/components/layout/Layout.spec.tsx +4 -4
- package/src/components/layout/Layout.tsx +86 -55
- package/src/components/layout/workspace-apps-rail.tsx +20 -21
- package/src/components/other-apps-section.tsx +195 -0
- package/src/components/workspace-app-card.spec.tsx +58 -32
- package/src/components/workspace-app-card.tsx +222 -158
- package/src/components/workspace-template-card.spec.tsx +45 -7
- package/src/components/workspace-template-card.tsx +74 -81
- package/src/lib/other-apps.spec.ts +31 -0
- package/src/lib/other-apps.ts +9 -1
- package/src/lib/workspace-apps.ts +4 -0
- package/src/routes/index.spec.ts +2 -1
- package/src/routes/index.ts +0 -2
- package/src/routes/pages/admin._index.tsx +85 -17
- package/src/routes/pages/apps.$appId.tsx +144 -57
- package/src/routes/pages/apps.tsx +76 -147
|
@@ -8,15 +8,16 @@ import {
|
|
|
8
8
|
IconChevronDown,
|
|
9
9
|
IconChevronRight,
|
|
10
10
|
IconClockHour4,
|
|
11
|
-
IconDots,
|
|
12
11
|
IconEdit,
|
|
13
12
|
IconEye,
|
|
14
13
|
IconEyeOff,
|
|
15
14
|
IconFileText,
|
|
16
|
-
|
|
15
|
+
IconKey,
|
|
16
|
+
IconSettings,
|
|
17
17
|
IconTrash,
|
|
18
18
|
IconUser,
|
|
19
19
|
IconUsersGroup,
|
|
20
|
+
IconWorld,
|
|
20
21
|
} from "@tabler/icons-react";
|
|
21
22
|
import { useEffect, useState, type FormEvent } from "react";
|
|
22
23
|
import { toast } from "sonner";
|
|
@@ -28,7 +29,10 @@ import {
|
|
|
28
29
|
type WorkspaceAppSummary,
|
|
29
30
|
} from "../lib/workspace-apps";
|
|
30
31
|
import { ActionQueryError } from "./action-query-error";
|
|
31
|
-
import {
|
|
32
|
+
import { AppIcon } from "./app-icon";
|
|
33
|
+
import { AppKeysPanel } from "./app-keys-popover";
|
|
34
|
+
import { AppListRow } from "./app-list-row";
|
|
35
|
+
import { AppOpenActions } from "./app-open-actions";
|
|
32
36
|
import { Badge } from "./ui/badge";
|
|
33
37
|
import { Button } from "./ui/button";
|
|
34
38
|
import {
|
|
@@ -38,7 +42,6 @@ import {
|
|
|
38
42
|
DialogFooter,
|
|
39
43
|
DialogHeader,
|
|
40
44
|
DialogTitle,
|
|
41
|
-
DialogTrigger,
|
|
42
45
|
} from "./ui/dialog";
|
|
43
46
|
import {
|
|
44
47
|
DropdownMenu,
|
|
@@ -67,12 +70,13 @@ export function WorkspaceAppCard({
|
|
|
67
70
|
const t = useT();
|
|
68
71
|
const { formatDate } = useFormatters();
|
|
69
72
|
const href = workspaceAppHref(app);
|
|
70
|
-
const openInNewTab = isPendingBuilderHref(app);
|
|
71
73
|
const isPending = app.status === "pending";
|
|
72
74
|
const pendingLabel = app.statusLabel || "Builder branch";
|
|
73
75
|
const isArchived = !!app.archived;
|
|
74
76
|
const audience = app.audience ?? "internal";
|
|
75
77
|
const [editOpen, setEditOpen] = useState(false);
|
|
78
|
+
const [keysOpen, setKeysOpen] = useState(false);
|
|
79
|
+
const [resourcesOpen, setResourcesOpen] = useState(false);
|
|
76
80
|
const [draftName, setDraftName] = useState(app.name);
|
|
77
81
|
const [draftDescription, setDraftDescription] = useState(
|
|
78
82
|
app.description || "",
|
|
@@ -132,145 +136,71 @@ export function WorkspaceAppCard({
|
|
|
132
136
|
};
|
|
133
137
|
|
|
134
138
|
return (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<a
|
|
145
|
-
href={href}
|
|
146
|
-
target={openInNewTab ? "_blank" : undefined}
|
|
147
|
-
rel={openInNewTab ? "noreferrer" : undefined}
|
|
148
|
-
aria-label={`Open ${app.name}`}
|
|
149
|
-
className="absolute inset-0 z-0 rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
150
|
-
/>
|
|
151
|
-
) : null}
|
|
152
|
-
|
|
153
|
-
<div className="pointer-events-none relative z-10 flex h-full min-w-0 items-start justify-between gap-3">
|
|
139
|
+
<>
|
|
140
|
+
<AppListRow
|
|
141
|
+
className={cn(
|
|
142
|
+
!href && "opacity-60",
|
|
143
|
+
isArchived && "opacity-70",
|
|
144
|
+
className,
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
<AppIcon id={app.id} name={app.name} size="sm" />
|
|
154
148
|
<div className="min-w-0 flex-1">
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
149
|
+
<h3 className="truncate text-sm font-semibold text-foreground">
|
|
150
|
+
{app.name}
|
|
151
|
+
</h3>
|
|
152
|
+
{app.description || isPending ? (
|
|
153
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
154
|
+
{app.description || pendingLabel}
|
|
155
|
+
</p>
|
|
156
|
+
) : null}
|
|
157
|
+
<div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
|
|
159
158
|
{isPending ? (
|
|
160
|
-
<
|
|
161
|
-
variant="outline"
|
|
162
|
-
className="shrink-0 gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
|
163
|
-
>
|
|
159
|
+
<span className="inline-flex min-w-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5">
|
|
164
160
|
<IconClockHour4 size={12} />
|
|
165
|
-
{pendingLabel}
|
|
166
|
-
</
|
|
161
|
+
<span className="truncate">{pendingLabel}</span>
|
|
162
|
+
</span>
|
|
167
163
|
) : null}
|
|
168
164
|
{isArchived ? (
|
|
169
|
-
<
|
|
165
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5">
|
|
170
166
|
<IconEyeOff size={12} />
|
|
171
167
|
Hidden
|
|
172
|
-
</
|
|
168
|
+
</span>
|
|
173
169
|
) : null}
|
|
174
170
|
{audience === "public" ? (
|
|
175
|
-
<
|
|
171
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5">
|
|
176
172
|
<IconWorld size={12} />
|
|
177
173
|
Public
|
|
178
|
-
</
|
|
174
|
+
</span>
|
|
175
|
+
) : null}
|
|
176
|
+
{isPending && app.branchName ? (
|
|
177
|
+
<span className="min-w-0 truncate">{app.branchName}</span>
|
|
179
178
|
) : null}
|
|
180
179
|
</div>
|
|
181
|
-
<p className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
182
|
-
{app.path}
|
|
183
|
-
</p>
|
|
184
|
-
{isPending && app.branchName ? (
|
|
185
|
-
<p className="mt-1 truncate text-xs text-muted-foreground">
|
|
186
|
-
Builder branch: {app.branchName}
|
|
187
|
-
</p>
|
|
188
|
-
) : null}
|
|
189
|
-
{app.description ? (
|
|
190
|
-
<p className="mt-2 min-h-10 line-clamp-2 text-[13px] leading-5 text-muted-foreground">
|
|
191
|
-
{app.description}
|
|
192
|
-
</p>
|
|
193
|
-
) : null}
|
|
194
180
|
</div>
|
|
195
|
-
<div className="flex shrink-0 items-center gap-
|
|
196
|
-
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"inline-flex cursor-pointer items-center justify-center",
|
|
217
|
-
)}
|
|
218
|
-
onClick={(e) => e.stopPropagation()}
|
|
219
|
-
>
|
|
220
|
-
<IconDots size={15} />
|
|
221
|
-
</button>
|
|
222
|
-
</DropdownMenuTrigger>
|
|
223
|
-
</TooltipTrigger>
|
|
224
|
-
<TooltipContent>More actions</TooltipContent>
|
|
225
|
-
</Tooltip>
|
|
226
|
-
<DropdownMenuContent align="end" className="w-72">
|
|
227
|
-
<DropdownMenuItem
|
|
228
|
-
onSelect={(event) => {
|
|
229
|
-
event.preventDefault();
|
|
230
|
-
setEditOpen(true);
|
|
231
|
-
}}
|
|
232
|
-
>
|
|
233
|
-
<IconEdit size={14} className="mr-2" />
|
|
234
|
-
Edit details
|
|
235
|
-
</DropdownMenuItem>
|
|
236
|
-
{isPending ? (
|
|
237
|
-
<DropdownMenuItem
|
|
238
|
-
onSelect={handleRemovePending}
|
|
239
|
-
className="text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
|
|
240
|
-
>
|
|
241
|
-
<IconTrash size={14} className="mr-2" />
|
|
242
|
-
Remove from list
|
|
243
|
-
</DropdownMenuItem>
|
|
244
|
-
) : isArchived ? (
|
|
245
|
-
<DropdownMenuItem onSelect={handleUnarchive}>
|
|
246
|
-
<IconEye size={14} className="mr-2" />
|
|
247
|
-
Restore to list
|
|
248
|
-
</DropdownMenuItem>
|
|
249
|
-
) : (
|
|
250
|
-
<DropdownMenuItem onSelect={handleArchive}>
|
|
251
|
-
<IconEyeOff size={14} className="mr-2" />
|
|
252
|
-
Hide from list
|
|
253
|
-
</DropdownMenuItem>
|
|
254
|
-
)}
|
|
255
|
-
<DropdownMenuSeparator />
|
|
256
|
-
<DropdownMenuLabel className="font-normal">
|
|
257
|
-
<WorkspaceAppMetadata
|
|
258
|
-
app={app}
|
|
259
|
-
formatDate={formatDate}
|
|
260
|
-
labels={{
|
|
261
|
-
created: t("agents.dashboardMetadataCreated"),
|
|
262
|
-
createdBy: t("agents.dashboardMetadataCreatedBy"),
|
|
263
|
-
owner: t("dispatch.pages.appMetadataOwner"),
|
|
264
|
-
teams: t("dispatch.pages.appMetadataTeams"),
|
|
265
|
-
notTracked: t("agents.notTracked"),
|
|
266
|
-
}}
|
|
267
|
-
/>
|
|
268
|
-
</DropdownMenuLabel>
|
|
269
|
-
</DropdownMenuContent>
|
|
270
|
-
</DropdownMenu>
|
|
271
|
-
</div>
|
|
181
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
182
|
+
<WorkspaceAppOpenActions app={app} href={href} />
|
|
183
|
+
<WorkspaceAppSettings
|
|
184
|
+
app={app}
|
|
185
|
+
formatDate={formatDate}
|
|
186
|
+
isArchived={isArchived}
|
|
187
|
+
isPending={isPending}
|
|
188
|
+
onEdit={() => setEditOpen(true)}
|
|
189
|
+
onKeys={() => setKeysOpen(true)}
|
|
190
|
+
onResources={() => setResourcesOpen(true)}
|
|
191
|
+
onArchive={handleArchive}
|
|
192
|
+
onUnarchive={handleUnarchive}
|
|
193
|
+
onRemovePending={handleRemovePending}
|
|
194
|
+
labels={{
|
|
195
|
+
created: t("agents.dashboardMetadataCreated"),
|
|
196
|
+
createdBy: t("agents.dashboardMetadataCreatedBy"),
|
|
197
|
+
owner: t("dispatch.pages.appMetadataOwner"),
|
|
198
|
+
teams: t("dispatch.pages.appMetadataTeams"),
|
|
199
|
+
notTracked: t("agents.notTracked"),
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
272
202
|
</div>
|
|
273
|
-
</
|
|
203
|
+
</AppListRow>
|
|
274
204
|
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
|
275
205
|
<DialogContent>
|
|
276
206
|
<DialogHeader>
|
|
@@ -311,7 +241,157 @@ export function WorkspaceAppCard({
|
|
|
311
241
|
</form>
|
|
312
242
|
</DialogContent>
|
|
313
243
|
</Dialog>
|
|
314
|
-
|
|
244
|
+
<Dialog open={keysOpen} onOpenChange={setKeysOpen}>
|
|
245
|
+
<DialogContent className="max-w-md">
|
|
246
|
+
<DialogHeader>
|
|
247
|
+
<DialogTitle>Manage keys for {app.name}</DialogTitle>
|
|
248
|
+
<DialogDescription>
|
|
249
|
+
Choose which workspace credentials this app can use.
|
|
250
|
+
</DialogDescription>
|
|
251
|
+
</DialogHeader>
|
|
252
|
+
<AppKeysPanel appId={app.id} appName={app.name} />
|
|
253
|
+
<DialogFooter>
|
|
254
|
+
<Button type="button" onClick={() => setKeysOpen(false)}>
|
|
255
|
+
Done
|
|
256
|
+
</Button>
|
|
257
|
+
</DialogFooter>
|
|
258
|
+
</DialogContent>
|
|
259
|
+
</Dialog>
|
|
260
|
+
<AppResourcesDialog
|
|
261
|
+
app={app}
|
|
262
|
+
open={resourcesOpen}
|
|
263
|
+
onOpenChange={setResourcesOpen}
|
|
264
|
+
/>
|
|
265
|
+
</>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function WorkspaceAppOpenActions({
|
|
270
|
+
app,
|
|
271
|
+
href,
|
|
272
|
+
}: {
|
|
273
|
+
app: WorkspaceAppSummary;
|
|
274
|
+
href: string | null;
|
|
275
|
+
}) {
|
|
276
|
+
const openInNewTab = isPendingBuilderHref(app);
|
|
277
|
+
|
|
278
|
+
if (!href) {
|
|
279
|
+
return (
|
|
280
|
+
<Button size="sm" variant="outline" disabled>
|
|
281
|
+
Open app
|
|
282
|
+
</Button>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<AppOpenActions
|
|
288
|
+
name={app.name}
|
|
289
|
+
href={href}
|
|
290
|
+
target={openInNewTab ? "_blank" : undefined}
|
|
291
|
+
rel={openInNewTab ? "noreferrer" : undefined}
|
|
292
|
+
showInlineOption
|
|
293
|
+
showNewTabOption
|
|
294
|
+
/>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
type WorkspaceAppMetadataLabels = {
|
|
299
|
+
created: string;
|
|
300
|
+
createdBy: string;
|
|
301
|
+
owner: string;
|
|
302
|
+
teams: string;
|
|
303
|
+
notTracked: string;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
function WorkspaceAppSettings({
|
|
307
|
+
app,
|
|
308
|
+
formatDate,
|
|
309
|
+
isArchived,
|
|
310
|
+
isPending,
|
|
311
|
+
onEdit,
|
|
312
|
+
onKeys,
|
|
313
|
+
onResources,
|
|
314
|
+
onArchive,
|
|
315
|
+
onUnarchive,
|
|
316
|
+
onRemovePending,
|
|
317
|
+
labels,
|
|
318
|
+
}: {
|
|
319
|
+
app: WorkspaceAppSummary;
|
|
320
|
+
formatDate: ReturnType<typeof useFormatters>["formatDate"];
|
|
321
|
+
isArchived: boolean;
|
|
322
|
+
isPending: boolean;
|
|
323
|
+
onEdit: () => void;
|
|
324
|
+
onKeys: () => void;
|
|
325
|
+
onResources: () => void;
|
|
326
|
+
onArchive: () => void;
|
|
327
|
+
onUnarchive: () => void;
|
|
328
|
+
onRemovePending: () => void;
|
|
329
|
+
labels: WorkspaceAppMetadataLabels;
|
|
330
|
+
}) {
|
|
331
|
+
return (
|
|
332
|
+
<DropdownMenu>
|
|
333
|
+
<Tooltip>
|
|
334
|
+
<TooltipTrigger asChild>
|
|
335
|
+
<DropdownMenuTrigger asChild>
|
|
336
|
+
<Button
|
|
337
|
+
type="button"
|
|
338
|
+
variant="ghost"
|
|
339
|
+
size="sm"
|
|
340
|
+
aria-label={`Settings for ${app.name}`}
|
|
341
|
+
className={APP_CARD_ACTION_CLASS}
|
|
342
|
+
>
|
|
343
|
+
<IconSettings size={15} />
|
|
344
|
+
</Button>
|
|
345
|
+
</DropdownMenuTrigger>
|
|
346
|
+
</TooltipTrigger>
|
|
347
|
+
<TooltipContent>App settings</TooltipContent>
|
|
348
|
+
</Tooltip>
|
|
349
|
+
<DropdownMenuContent align="end" className="w-72">
|
|
350
|
+
<DropdownMenuItem onSelect={onEdit}>
|
|
351
|
+
<IconEdit size={14} className="mr-2" />
|
|
352
|
+
Edit details
|
|
353
|
+
</DropdownMenuItem>
|
|
354
|
+
{!isPending && !isArchived ? (
|
|
355
|
+
<>
|
|
356
|
+
<DropdownMenuItem onSelect={onKeys}>
|
|
357
|
+
<IconKey size={14} className="mr-2" />
|
|
358
|
+
Manage keys
|
|
359
|
+
</DropdownMenuItem>
|
|
360
|
+
<DropdownMenuItem onSelect={onResources}>
|
|
361
|
+
<IconFileText size={14} className="mr-2" />
|
|
362
|
+
Agent resources
|
|
363
|
+
</DropdownMenuItem>
|
|
364
|
+
</>
|
|
365
|
+
) : null}
|
|
366
|
+
{isPending ? (
|
|
367
|
+
<DropdownMenuItem
|
|
368
|
+
onSelect={onRemovePending}
|
|
369
|
+
className="text-destructive focus:text-destructive"
|
|
370
|
+
>
|
|
371
|
+
<IconTrash size={14} className="mr-2" />
|
|
372
|
+
Remove from list
|
|
373
|
+
</DropdownMenuItem>
|
|
374
|
+
) : isArchived ? (
|
|
375
|
+
<DropdownMenuItem onSelect={onUnarchive}>
|
|
376
|
+
<IconEye size={14} className="mr-2" />
|
|
377
|
+
Restore to list
|
|
378
|
+
</DropdownMenuItem>
|
|
379
|
+
) : (
|
|
380
|
+
<DropdownMenuItem onSelect={onArchive}>
|
|
381
|
+
<IconEyeOff size={14} className="mr-2" />
|
|
382
|
+
Hide from list
|
|
383
|
+
</DropdownMenuItem>
|
|
384
|
+
)}
|
|
385
|
+
<DropdownMenuSeparator />
|
|
386
|
+
<DropdownMenuLabel className="font-normal">
|
|
387
|
+
<WorkspaceAppMetadata
|
|
388
|
+
app={app}
|
|
389
|
+
formatDate={formatDate}
|
|
390
|
+
labels={labels}
|
|
391
|
+
/>
|
|
392
|
+
</DropdownMenuLabel>
|
|
393
|
+
</DropdownMenuContent>
|
|
394
|
+
</DropdownMenu>
|
|
315
395
|
);
|
|
316
396
|
}
|
|
317
397
|
|
|
@@ -322,13 +402,7 @@ function WorkspaceAppMetadata({
|
|
|
322
402
|
}: {
|
|
323
403
|
app: WorkspaceAppSummary;
|
|
324
404
|
formatDate: ReturnType<typeof useFormatters>["formatDate"];
|
|
325
|
-
labels:
|
|
326
|
-
created: string;
|
|
327
|
-
createdBy: string;
|
|
328
|
-
owner: string;
|
|
329
|
-
teams: string;
|
|
330
|
-
notTracked: string;
|
|
331
|
-
};
|
|
405
|
+
labels: WorkspaceAppMetadataLabels;
|
|
332
406
|
}) {
|
|
333
407
|
function formatMetadataDate(value: string | null | undefined): string {
|
|
334
408
|
if (!value) return labels.notTracked;
|
|
@@ -389,8 +463,15 @@ function WorkspaceAppMetadataRow({
|
|
|
389
463
|
);
|
|
390
464
|
}
|
|
391
465
|
|
|
392
|
-
function AppResourcesDialog({
|
|
393
|
-
|
|
466
|
+
function AppResourcesDialog({
|
|
467
|
+
app,
|
|
468
|
+
open,
|
|
469
|
+
onOpenChange,
|
|
470
|
+
}: {
|
|
471
|
+
app: WorkspaceAppSummary;
|
|
472
|
+
open: boolean;
|
|
473
|
+
onOpenChange: (open: boolean) => void;
|
|
474
|
+
}) {
|
|
394
475
|
const [inspectedResourceId, setInspectedResourceId] = useState<string | null>(
|
|
395
476
|
null,
|
|
396
477
|
);
|
|
@@ -408,27 +489,10 @@ function AppResourcesDialog({ app }: { app: WorkspaceAppSummary }) {
|
|
|
408
489
|
<Dialog
|
|
409
490
|
open={open}
|
|
410
491
|
onOpenChange={(nextOpen) => {
|
|
411
|
-
|
|
492
|
+
onOpenChange(nextOpen);
|
|
412
493
|
if (!nextOpen) setInspectedResourceId(null);
|
|
413
494
|
}}
|
|
414
495
|
>
|
|
415
|
-
<Tooltip>
|
|
416
|
-
<TooltipTrigger asChild>
|
|
417
|
-
<DialogTrigger asChild>
|
|
418
|
-
<Button
|
|
419
|
-
type="button"
|
|
420
|
-
variant="ghost"
|
|
421
|
-
size="sm"
|
|
422
|
-
aria-label={`View agent resources for ${app.name}`}
|
|
423
|
-
className={APP_CARD_ACTION_CLASS}
|
|
424
|
-
onClick={(e) => e.stopPropagation()}
|
|
425
|
-
>
|
|
426
|
-
<IconFileText size={14} />
|
|
427
|
-
</Button>
|
|
428
|
-
</DialogTrigger>
|
|
429
|
-
</TooltipTrigger>
|
|
430
|
-
<TooltipContent>View agent resources</TooltipContent>
|
|
431
|
-
</Tooltip>
|
|
432
496
|
<DialogContent className="max-w-2xl">
|
|
433
497
|
<DialogHeader>
|
|
434
498
|
<DialogTitle>{app.name} agent resources</DialogTitle>
|
|
@@ -546,7 +610,7 @@ function AppResourcesDialog({ app }: { app: WorkspaceAppSummary }) {
|
|
|
546
610
|
)}
|
|
547
611
|
</div>
|
|
548
612
|
<DialogFooter>
|
|
549
|
-
<Button type="button" onClick={() =>
|
|
613
|
+
<Button type="button" onClick={() => onOpenChange(false)}>
|
|
550
614
|
Done
|
|
551
615
|
</Button>
|
|
552
616
|
</DialogFooter>
|
|
@@ -66,14 +66,23 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
66
66
|
vi.unstubAllGlobals();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it("
|
|
69
|
+
it("keeps template cards concise and moves setup context into the add flow", async () => {
|
|
70
70
|
await act(async () => {
|
|
71
71
|
root.render(<WorkspaceTemplateCard template={template} />);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
expect(container.textContent).toContain("Weekly report");
|
|
75
|
-
expect(container.textContent).toContain("RevOps");
|
|
76
|
-
expect(container.textContent).toContain(
|
|
75
|
+
expect(container.textContent).not.toContain("RevOps");
|
|
76
|
+
expect(container.textContent).not.toContain(
|
|
77
|
+
"Connect your CRM after installing the app.",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const trigger = Array.from(container.querySelectorAll("button")).find(
|
|
81
|
+
(button) => button.textContent?.includes("Add app"),
|
|
82
|
+
);
|
|
83
|
+
expect(trigger).not.toBeUndefined();
|
|
84
|
+
await act(async () => trigger?.click());
|
|
85
|
+
expect(document.body.textContent).toContain(
|
|
77
86
|
"Connect your CRM after installing the app.",
|
|
78
87
|
);
|
|
79
88
|
|
|
@@ -103,7 +112,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
103
112
|
});
|
|
104
113
|
|
|
105
114
|
const trigger = Array.from(container.querySelectorAll("button")).find(
|
|
106
|
-
(button) => button.textContent?.includes("
|
|
115
|
+
(button) => button.textContent?.includes("Add app"),
|
|
107
116
|
);
|
|
108
117
|
expect(trigger).not.toBeUndefined();
|
|
109
118
|
|
|
@@ -144,6 +153,35 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
144
153
|
);
|
|
145
154
|
});
|
|
146
155
|
|
|
156
|
+
it("keeps add app inside the catalog split-button menu", async () => {
|
|
157
|
+
await act(async () => {
|
|
158
|
+
root.render(<WorkspaceTemplateCard template={template} catalog />);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(
|
|
162
|
+
Array.from(container.querySelectorAll("button")).some((button) =>
|
|
163
|
+
button.textContent?.includes("Add app"),
|
|
164
|
+
),
|
|
165
|
+
).toBe(false);
|
|
166
|
+
|
|
167
|
+
const optionsButton = container.querySelector<HTMLButtonElement>(
|
|
168
|
+
'button[aria-label="Open options for Weekly report"]',
|
|
169
|
+
);
|
|
170
|
+
expect(optionsButton).not.toBeNull();
|
|
171
|
+
await act(async () => {
|
|
172
|
+
optionsButton?.dispatchEvent(
|
|
173
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const addItem = Array.from(
|
|
178
|
+
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
179
|
+
).find((item) => item.textContent?.includes("Add app"));
|
|
180
|
+
expect(addItem).not.toBeUndefined();
|
|
181
|
+
await act(async () => addItem?.click());
|
|
182
|
+
expect(document.body.textContent).toContain("Choose the URL-safe id");
|
|
183
|
+
});
|
|
184
|
+
|
|
147
185
|
it("accepts the list action envelope and renders installed state", async () => {
|
|
148
186
|
await act(async () => {
|
|
149
187
|
root.render(
|
|
@@ -166,9 +204,9 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
166
204
|
expect(container.textContent).toContain("Curated templates");
|
|
167
205
|
expect(container.textContent).toContain("Installed");
|
|
168
206
|
expect(container.querySelector('a[href^="https://"]')).toBeNull();
|
|
169
|
-
const
|
|
170
|
-
(button) => button.textContent?.includes("
|
|
207
|
+
const addButton = Array.from(container.querySelectorAll("button")).find(
|
|
208
|
+
(button) => button.textContent?.includes("Add app"),
|
|
171
209
|
);
|
|
172
|
-
expect(
|
|
210
|
+
expect(addButton).not.toHaveProperty("disabled", true);
|
|
173
211
|
});
|
|
174
212
|
});
|