@tangle-network/sandbox-ui 0.116.8 → 0.117.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.
@@ -1,9 +1,363 @@
1
- import { n as normalizeProviderId, r as providerLogoCandidates, t as ProviderIcon } from "./provider-logo-DjqmConF.js";
2
- import { Button, Card, CardContent, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, EmptyState } from "@tangle-network/ui/primitives";
3
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { n as normalizeProviderId, r as providerLogoCandidates, t as ProviderIcon } from "./provider-logo-BiuiO_c2.js";
2
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, EmptyState } from "@tangle-network/ui/primitives";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import * as React$1 from "react";
5
5
  import { cn, focusFieldWithin, focusRing } from "@tangle-network/ui/utils";
6
6
  import { Check, ExternalLink, MoreVertical, Search, Unplug } from "lucide-react";
7
+ //#region src/integrations/integrations-catalog.tsx
8
+ const TONES = {
9
+ neutral: "text-muted-foreground",
10
+ success: "text-[var(--surface-success-text)]",
11
+ warning: "text-[var(--surface-warning-text)]",
12
+ error: "text-destructive"
13
+ };
14
+ function catalogRowActive(row) {
15
+ return row.kind === "app" ? row.installedCount > 0 : row.connections.length > 0;
16
+ }
17
+ /** A controlled selector shared by the catalog and detail; never picks an account. */
18
+ function ConnectionSelector({ connections, value, onChange, label }) {
19
+ const selected = connections.some((connection) => connection.id === value) ? value : "";
20
+ return /* @__PURE__ */ jsxs("select", {
21
+ "aria-label": label,
22
+ value: selected ?? "",
23
+ onChange: (event) => onChange(event.target.value || null),
24
+ className: cn("h-8 w-full min-w-0 truncate rounded-md border border-border bg-background px-2 text-sm text-foreground", focusRing),
25
+ children: [/* @__PURE__ */ jsx("option", {
26
+ value: "",
27
+ children: "Select an account"
28
+ }), connections.map((connection) => /* @__PURE__ */ jsx("option", {
29
+ value: connection.id,
30
+ children: connection.accountDisplay ? `${connection.accountDisplay} · ${connection.id}` : connection.id
31
+ }, connection.id))]
32
+ });
33
+ }
34
+ function ConnectionMenu({ row, connection, onManage, onDisconnect, compact }) {
35
+ const manage = connection.capabilities.manage && (connection.manageHref || onManage);
36
+ const disconnect = connection.capabilities.disconnect && onDisconnect;
37
+ const actions = connection.actions ?? [];
38
+ if (!manage && !disconnect && actions.length === 0) return null;
39
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
40
+ asChild: true,
41
+ children: /* @__PURE__ */ jsx("button", {
42
+ type: "button",
43
+ "data-testid": `menu-${row.providerId}`,
44
+ "aria-label": `More actions for ${row.title}`,
45
+ title: "More actions",
46
+ className: cn("relative z-20 flex shrink-0 items-center justify-center rounded-md transition-colors", compact ? "h-6 w-6 text-muted-foreground/70 hover:bg-background hover:text-foreground focus-visible:bg-background focus-visible:text-foreground" : "h-7 w-7 text-muted-foreground hover:bg-accent hover:text-foreground", focusRing),
47
+ children: /* @__PURE__ */ jsx(MoreVertical, { className: compact ? "h-3.5 w-3.5" : "h-4 w-4" })
48
+ })
49
+ }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
50
+ side: "right",
51
+ align: "start",
52
+ className: "min-w-[160px] rounded-md",
53
+ children: [
54
+ manage && (connection.manageHref ? /* @__PURE__ */ jsx(DropdownMenuItem, {
55
+ asChild: true,
56
+ children: /* @__PURE__ */ jsxs("a", {
57
+ href: connection.manageHref,
58
+ target: connection.manageInNewWindow ? "_blank" : void 0,
59
+ rel: connection.manageInNewWindow ? "noopener noreferrer" : void 0,
60
+ "data-testid": `manage-item-${row.providerId}`,
61
+ children: [/* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Manage"]
62
+ })
63
+ }) : /* @__PURE__ */ jsxs(DropdownMenuItem, {
64
+ "data-testid": `manage-item-${row.providerId}`,
65
+ onSelect: () => onManage?.(connection, row),
66
+ children: [/* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Manage"]
67
+ })),
68
+ actions.map((action) => /* @__PURE__ */ jsx(DropdownMenuItem, {
69
+ disabled: action.disabled,
70
+ onSelect: action.onSelect,
71
+ children: action.label
72
+ }, action.id)),
73
+ disconnect && (manage || actions.length > 0) ? /* @__PURE__ */ jsx(DropdownMenuSeparator, {}) : null,
74
+ disconnect ? /* @__PURE__ */ jsxs(DropdownMenuItem, {
75
+ "data-testid": `disconnect-${row.providerId}`,
76
+ className: "text-destructive focus:text-destructive",
77
+ onSelect: () => onDisconnect?.(connection, row),
78
+ children: [/* @__PURE__ */ jsx(Unplug, { className: "mr-2 h-4 w-4" }), " Disconnect"]
79
+ }) : null
80
+ ]
81
+ })] });
82
+ }
83
+ /** Platform's catalog presentation, with app-owned state, destinations and effects. */
84
+ function IntegrationsCatalog({ rows, query, onQueryChange, categoryFilter = "", onCategoryFilterChange, sort, onSortChange, onSelectConnection, onConnect, onManage, onDisconnect, onRequestIntegration, onRetry, loading = false, error, actionError, connectError, busyProviderId, reserveConnectionContext = false, skeletonCount, emptyCatalogLabel = "No integrations are available to connect yet.", title, description, layout = "cards", className }) {
85
+ const compact = layout === "tiles";
86
+ const categories = [...new Set(rows.flatMap((row) => row.category ? [row.category] : []))].sort();
87
+ const q = query.trim().toLowerCase();
88
+ const filtered = rows.filter((row) => (!categoryFilter || row.category === categoryFilter) && (!q || `${row.title} ${row.providerId} ${row.description ?? ""}`.toLowerCase().includes(q)));
89
+ const hiddenActive = rows.filter((row) => catalogRowActive(row) && !filtered.includes(row)).length;
90
+ const state = error ? "error" : loading && rows.length === 0 ? "loading" : rows.length === 0 ? "empty" : "loaded";
91
+ const gridClass = compact ? "grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6" : "grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3";
92
+ const boxClass = cn(compact ? "flex min-w-0 aspect-square flex-col items-center justify-center gap-2 rounded-xl border p-3 text-center" : "flex min-w-0 min-h-[200px] flex-col gap-3 rounded-xl border p-4 text-left", compact && reserveConnectionContext && "w-full min-h-[136px]");
93
+ const headerClass = cn("flex w-full min-w-0 gap-2", compact ? "flex-col items-center" : "items-start gap-3");
94
+ const clearFilters = () => {
95
+ onQueryChange("");
96
+ onCategoryFilterChange?.("");
97
+ };
98
+ return /* @__PURE__ */ jsxs("section", {
99
+ className: cn("w-full min-w-0 space-y-4", className),
100
+ "aria-label": title ?? "Integrations",
101
+ "aria-busy": loading,
102
+ "data-testid": state === "loading" ? "integrations-skeleton" : "integrations-panel",
103
+ "data-catalog-state": state,
104
+ children: [
105
+ title ? /* @__PURE__ */ jsxs("header", { children: [/* @__PURE__ */ jsx("h2", {
106
+ className: "text-lg font-semibold",
107
+ children: title
108
+ }), description ? /* @__PURE__ */ jsx("p", {
109
+ className: "mt-1 text-sm text-muted-foreground",
110
+ children: description
111
+ }) : null] }) : null,
112
+ /* @__PURE__ */ jsxs("div", {
113
+ className: "flex min-w-0 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between",
114
+ children: [
115
+ /* @__PURE__ */ jsxs("div", {
116
+ className: cn("flex min-w-0 flex-1 items-center gap-2 rounded-lg border bg-card px-3 py-2", focusFieldWithin),
117
+ children: [/* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("input", {
118
+ type: "text",
119
+ value: query,
120
+ onChange: (event) => onQueryChange(event.target.value),
121
+ "aria-label": "Search integrations",
122
+ placeholder: "Search integrations...",
123
+ autoFocus: compact,
124
+ "data-testid": "integration-search",
125
+ className: "min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
126
+ })]
127
+ }),
128
+ onCategoryFilterChange ? /* @__PURE__ */ jsxs("select", {
129
+ "aria-label": "Filter by category",
130
+ value: categoryFilter,
131
+ onChange: (event) => onCategoryFilterChange(event.target.value),
132
+ className: cn("h-10 min-w-0 rounded-lg border bg-card px-3 text-sm", focusRing),
133
+ children: [
134
+ /* @__PURE__ */ jsx("option", {
135
+ value: "",
136
+ children: "All categories"
137
+ }),
138
+ categoryFilter && !categories.includes(categoryFilter) ? /* @__PURE__ */ jsx("option", {
139
+ value: categoryFilter,
140
+ children: categoryFilter
141
+ }) : null,
142
+ categories.map((category) => /* @__PURE__ */ jsx("option", {
143
+ value: category,
144
+ children: category
145
+ }, category))
146
+ ]
147
+ }) : null,
148
+ sort && onSortChange ? /* @__PURE__ */ jsx("div", {
149
+ role: "group",
150
+ "aria-label": "Sort integrations",
151
+ className: "flex shrink-0 items-center gap-1 rounded-lg border border-border bg-card p-1",
152
+ children: [["featured", "Featured"], ["alpha", "A–Z"]].map(([value, label]) => /* @__PURE__ */ jsx("button", {
153
+ type: "button",
154
+ "aria-pressed": sort === value,
155
+ onClick: () => onSortChange(value),
156
+ "data-testid": `sort-${value}`,
157
+ className: cn("rounded-md px-2.5 py-1 text-xs font-medium transition-colors", focusRing, sort === value ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"),
158
+ children: label
159
+ }, value))
160
+ }) : null,
161
+ onRequestIntegration ? /* @__PURE__ */ jsx(Button, {
162
+ variant: "outline",
163
+ onClick: () => onRequestIntegration(query.trim()),
164
+ children: "Request an integration"
165
+ }) : null
166
+ ]
167
+ }),
168
+ actionError ? /* @__PURE__ */ jsx("p", {
169
+ role: "alert",
170
+ className: "text-sm text-destructive",
171
+ children: actionError
172
+ }) : null,
173
+ connectError ? /* @__PURE__ */ jsx("p", {
174
+ role: "alert",
175
+ "data-testid": "integration-connect-error",
176
+ className: "rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive",
177
+ children: connectError
178
+ }) : null,
179
+ (q || categoryFilter) && state === "loaded" ? /* @__PURE__ */ jsxs("span", {
180
+ role: "status",
181
+ className: "sr-only",
182
+ children: [filtered.length, " matching integrations"]
183
+ }) : null,
184
+ /* @__PURE__ */ jsx("div", {
185
+ className: gridClass,
186
+ "data-testid": "integrations-catalog-grid",
187
+ children: state === "loading" ? Array.from({ length: skeletonCount ?? (compact ? 12 : 6) }, (_, i) => /* @__PURE__ */ jsxs("div", {
188
+ "aria-hidden": "true",
189
+ className: cn(boxClass, "animate-pulse border-border bg-muted/40"),
190
+ children: [/* @__PURE__ */ jsxs("div", {
191
+ className: headerClass,
192
+ children: [/* @__PURE__ */ jsx("div", {
193
+ className: "shrink-0 rounded-2xl bg-muted",
194
+ style: {
195
+ width: 48,
196
+ height: 48
197
+ }
198
+ }), /* @__PURE__ */ jsx("div", {
199
+ className: "flex h-8 w-full items-center justify-center",
200
+ children: /* @__PURE__ */ jsx("div", { className: "h-2.5 w-3/4 rounded bg-muted" })
201
+ })]
202
+ }), !compact ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { className: "h-8 w-full rounded bg-muted" }), /* @__PURE__ */ jsx("div", { className: "mt-auto h-9 border-t border-border" })] }) : null]
203
+ }, i)) : state === "error" ? /* @__PURE__ */ jsxs("div", {
204
+ role: "alert",
205
+ className: "col-span-full flex min-h-[200px] min-w-0 flex-col items-center justify-center gap-3 rounded-xl border border-destructive/40 bg-card p-6",
206
+ children: [/* @__PURE__ */ jsx("p", {
207
+ className: "break-words text-sm text-destructive",
208
+ children: error
209
+ }), onRetry ? /* @__PURE__ */ jsx(Button, {
210
+ variant: "outline",
211
+ onClick: onRetry,
212
+ children: "Retry"
213
+ }) : null]
214
+ }) : filtered.length === 0 ? /* @__PURE__ */ jsxs("div", {
215
+ className: "col-span-full min-h-[200px] min-w-0 rounded-xl border border-border bg-card p-4",
216
+ children: [/* @__PURE__ */ jsx(EmptyState, {
217
+ title: state === "empty" ? "No integrations" : "No matches",
218
+ description: state === "empty" ? emptyCatalogLabel : `No integrations match "${query.trim()}".`
219
+ }), q || categoryFilter ? /* @__PURE__ */ jsxs("div", {
220
+ className: "flex flex-col items-center gap-2",
221
+ children: [hiddenActive > 0 ? /* @__PURE__ */ jsxs("p", {
222
+ className: "text-sm text-muted-foreground",
223
+ children: [hiddenActive, " active integrations are hidden by your filters."]
224
+ }) : null, /* @__PURE__ */ jsx(Button, {
225
+ variant: "outline",
226
+ onClick: clearFilters,
227
+ children: "Clear filters"
228
+ })]
229
+ }) : null]
230
+ }) : filtered.map((row) => {
231
+ const provider = row.kind === "provider" ? row : void 0;
232
+ const connection = provider?.connections.find((item) => item.id === provider.selectedConnectionId);
233
+ const active = catalogRowActive(row);
234
+ const canConnect = provider?.canConnect && onConnect;
235
+ const connecting = busyProviderId === row.providerId;
236
+ const wholeButton = !active && !!canConnect && !row.to;
237
+ const Box = wholeButton ? "button" : "div";
238
+ const manageHref = connection?.capabilities.manage ? connection.manageHref : void 0;
239
+ const header = /* @__PURE__ */ jsxs("div", {
240
+ className: headerClass,
241
+ children: [/* @__PURE__ */ jsx(ProviderIcon, {
242
+ id: row.kind === "app" ? row.logoProviderId : row.providerId,
243
+ iconUrl: row.iconUrl,
244
+ displayName: row.title,
245
+ size: 48,
246
+ className: "rounded-2xl"
247
+ }), /* @__PURE__ */ jsxs("div", {
248
+ className: "flex h-8 w-full min-w-0 flex-col justify-center leading-4",
249
+ children: [row.to ? /* @__PURE__ */ jsx("a", {
250
+ href: row.to,
251
+ className: cn("relative z-20 truncate text-sm font-medium hover:underline", focusRing),
252
+ children: row.title
253
+ }) : /* @__PURE__ */ jsx("span", {
254
+ className: cn("w-full font-medium text-foreground", compact ? "text-xs" : "text-sm", connection?.accountDisplay ? "truncate" : "line-clamp-2"),
255
+ children: row.title
256
+ }), connection?.accountDisplay ? /* @__PURE__ */ jsx("span", {
257
+ className: "w-full truncate text-[11px] leading-4 text-muted-foreground",
258
+ "data-testid": `account-${row.providerId}`,
259
+ children: connection.accountDisplay
260
+ }) : null]
261
+ })]
262
+ });
263
+ return /* @__PURE__ */ jsxs(Box, {
264
+ type: wholeButton ? "button" : void 0,
265
+ "data-testid": `integration-${row.providerId}`,
266
+ "data-connected": active ? "true" : "false",
267
+ "data-connecting": connecting ? "true" : void 0,
268
+ "aria-busy": connecting || void 0,
269
+ disabled: wholeButton ? connecting : void 0,
270
+ onClick: wholeButton ? () => provider && onConnect?.(provider) : void 0,
271
+ title: connection?.accountDisplay ? `${row.title} — ${connection.accountDisplay}` : row.description ?? row.title,
272
+ className: cn(boxClass, "group relative", compact && active ? "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)]" : "border-border bg-card", wholeButton && cn("transition-all hover:border-[var(--border-strong)] hover:bg-accent/40 hover:shadow-sm", focusRing), connection?.detail && compact && "w-full min-h-[136px]"),
273
+ children: [
274
+ compact && manageHref ? /* @__PURE__ */ jsx("a", {
275
+ href: manageHref,
276
+ target: connection?.manageInNewWindow ? "_blank" : void 0,
277
+ rel: connection?.manageInNewWindow ? "noopener noreferrer" : void 0,
278
+ "aria-label": `Manage ${row.title}${connection?.manageInNewWindow ? " (opens in new window)" : ""}`,
279
+ "data-testid": `manage-${row.providerId}`,
280
+ className: cn("absolute inset-0 z-10 rounded-xl", focusRing)
281
+ }) : null,
282
+ compact && active ? /* @__PURE__ */ jsx("span", {
283
+ className: "pointer-events-none absolute left-2 top-2 flex h-4 w-4 items-center justify-center rounded-full bg-[var(--surface-success-text)] text-white",
284
+ children: /* @__PURE__ */ jsx(Check, {
285
+ className: "h-3 w-3",
286
+ strokeWidth: 3
287
+ })
288
+ }) : null,
289
+ header,
290
+ provider && (provider.connections.length > 1 || active && !connection) ? /* @__PURE__ */ jsx("div", {
291
+ className: "relative z-20 w-full min-w-0",
292
+ children: /* @__PURE__ */ jsx(ConnectionSelector, {
293
+ connections: provider.connections,
294
+ value: provider.selectedConnectionId,
295
+ onChange: (id) => onSelectConnection(row.providerId, id),
296
+ label: `Account for ${row.title}`
297
+ })
298
+ }) : null,
299
+ connection?.detail ? /* @__PURE__ */ jsx("span", {
300
+ className: "w-full truncate text-[11px] leading-4 text-muted-foreground",
301
+ title: connection.detail,
302
+ children: connection.detail
303
+ }) : null,
304
+ !compact ? /* @__PURE__ */ jsx("p", {
305
+ className: cn("min-h-8 text-sm", TONES[connection?.statusTone ?? "neutral"]),
306
+ children: row.kind === "app" ? row.installedCount ? `Installed · ${row.installedCount} ${row.installedCount === 1 ? "account" : "accounts"}` : "Not installed" : connection?.statusLabel ?? (active ? `${provider?.connections.length} accounts · select one to manage` : "Not connected")
307
+ }) : null,
308
+ !compact ? /* @__PURE__ */ jsxs("div", {
309
+ className: "mt-auto flex min-h-9 items-center gap-2 border-t border-border pt-3",
310
+ children: [row.kind === "app" ? /* @__PURE__ */ jsx("a", {
311
+ href: row.to,
312
+ className: cn("text-sm font-medium hover:underline", focusRing),
313
+ children: row.installedCount ? "Manage" : "Set up"
314
+ }) : !active && canConnect ? wholeButton ? /* @__PURE__ */ jsx("span", {
315
+ className: "text-sm font-medium",
316
+ children: connecting ? "Connecting…" : "Connect"
317
+ }) : /* @__PURE__ */ jsx(Button, {
318
+ variant: "outline",
319
+ disabled: connecting,
320
+ onClick: () => onConnect?.(row),
321
+ children: connecting ? "Connecting…" : "Connect"
322
+ }) : manageHref ? /* @__PURE__ */ jsx("a", {
323
+ href: manageHref,
324
+ target: connection?.manageInNewWindow ? "_blank" : void 0,
325
+ rel: connection?.manageInNewWindow ? "noopener noreferrer" : void 0,
326
+ "data-testid": `manage-${row.providerId}`,
327
+ className: cn("text-sm font-medium hover:underline", focusRing),
328
+ children: "Manage"
329
+ }) : connection?.capabilities.manage && onManage ? /* @__PURE__ */ jsx(Button, {
330
+ variant: "outline",
331
+ onClick: () => onManage(connection, row),
332
+ children: "Manage"
333
+ }) : null, provider && connection ? /* @__PURE__ */ jsx("div", {
334
+ className: "ml-auto",
335
+ children: /* @__PURE__ */ jsx(ConnectionMenu, {
336
+ compact,
337
+ row: provider,
338
+ connection,
339
+ onManage,
340
+ onDisconnect
341
+ }, connection.id)
342
+ }) : null]
343
+ }) : provider && connection ? /* @__PURE__ */ jsx("div", {
344
+ className: "absolute right-1.5 top-1.5 z-20",
345
+ children: /* @__PURE__ */ jsx(ConnectionMenu, {
346
+ compact,
347
+ row: provider,
348
+ connection,
349
+ onManage,
350
+ onDisconnect
351
+ }, connection.id)
352
+ }) : null
353
+ ]
354
+ }, `${row.kind}:${row.providerId}`);
355
+ })
356
+ })
357
+ ]
358
+ });
359
+ }
360
+ //#endregion
7
361
  //#region src/integrations/integrations-panel.tsx
8
362
  const DEFAULT_FEATURED_IDS = [
9
363
  "gmail",
@@ -43,419 +397,573 @@ const DEFAULT_FEATURED_IDS = [
43
397
  function defaultConnectorOf(provider) {
44
398
  return provider.connectors?.[0]?.connectorId ?? provider.providerId;
45
399
  }
46
- const TILE_GRID_CLASS = "grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6";
47
- const DEFAULT_SKELETON_COUNT = 12;
48
- const TILE_BOX_CLASS = "flex min-w-0 aspect-square flex-col items-center justify-center gap-2 rounded-xl border p-3 text-center";
49
- /**
50
- * Search + sort row. Rendered by the skeleton AND the loaded panel from this
51
- * one component, which is the whole point: the row is ~74px tall with a 16px
52
- * gap under it, and a skeleton that omitted it under-reserved the page by that
53
- * much on every single load — the panel visibly dropped when data arrived.
54
- * Height parity here is structural, not a matched pair of magic numbers.
55
- *
56
- * The controls stay live while loading. They drive local state only (`query`,
57
- * `sort`), so typing during the fetch is applied the moment the catalog lands
58
- * instead of being thrown away — and keeping the same input element mounted
59
- * across the transition is what preserves its autofocus.
60
- */
61
- function IntegrationsToolbar({ query, onQueryChange, sort, onSortChange }) {
62
- return /* @__PURE__ */ jsxs("div", {
63
- className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between",
64
- children: [/* @__PURE__ */ jsxs("div", {
65
- className: `flex flex-1 items-center gap-2 rounded-lg border bg-card px-3 py-2 ${focusFieldWithin}`,
66
- children: [/* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("input", {
67
- type: "text",
68
- value: query,
69
- onChange: (e) => onQueryChange(e.target.value),
70
- "aria-label": "Search integrations",
71
- placeholder: "Search integrations...",
72
- autoFocus: true,
73
- "data-testid": "integration-search",
74
- className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
75
- })]
76
- }), /* @__PURE__ */ jsx("div", {
77
- role: "group",
78
- "aria-label": "Sort integrations",
79
- className: "flex shrink-0 items-center gap-1 rounded-lg border border-border bg-card p-1",
80
- children: [["featured", "Featured"], ["alpha", "A–Z"]].map(([value, label]) => /* @__PURE__ */ jsx("button", {
81
- type: "button",
82
- "aria-pressed": sort === value,
83
- onClick: () => onSortChange(value),
84
- "data-testid": `sort-${value}`,
85
- className: cn("rounded-md px-2.5 py-1 text-xs font-medium transition-colors", focusRing, sort === value ? "bg-accent text-foreground" : "text-muted-foreground hover:text-foreground"),
86
- children: label
87
- }, value))
88
- })]
89
- });
90
- }
91
- const LOGO_SIZE = 48;
92
- function buildConnectionIndex(connections) {
93
- const index = /* @__PURE__ */ new Map();
94
- for (const conn of connections) {
95
- if (conn.status === "revoked") continue;
96
- index.set(`${conn.providerId}:${conn.connectorId ?? conn.providerId}`, conn);
97
- }
98
- return index;
400
+ function displayNameOf(provider) {
401
+ return provider.displayName ?? provider.title ?? provider.providerId.replace(/[-_]/g, " ");
99
402
  }
100
- function makeFeaturedRank(featuredIds) {
403
+ function makeFeaturedRank(ids) {
101
404
  const rank = /* @__PURE__ */ new Map();
102
- featuredIds.forEach((id, i) => {
103
- const norm = normalizeProviderId(id);
405
+ ids.forEach((id, i) => {
104
406
  if (!rank.has(id)) rank.set(id, i);
105
- if (!rank.has(norm)) rank.set(norm, i);
407
+ const normalized = normalizeProviderId(id);
408
+ if (!rank.has(normalized)) rank.set(normalized, i);
106
409
  });
107
410
  return rank;
108
411
  }
109
412
  function rankOf(provider, rank) {
110
413
  const raw = provider.providerId.toLowerCase();
111
- const direct = rank.get(raw);
112
- if (direct !== void 0) return direct;
113
- return rank.get(normalizeProviderId(raw)) ?? Number.MAX_SAFE_INTEGER;
114
- }
115
- function displayNameOf(provider) {
116
- return provider.displayName ?? provider.title ?? provider.providerId.replace(/[-_]/g, " ");
117
- }
118
- function matchesQuery(provider, q) {
119
- return `${displayNameOf(provider)} ${provider.providerId} ${provider.description ?? ""}`.toLowerCase().includes(q);
414
+ return rank.get(raw) ?? rank.get(normalizeProviderId(raw)) ?? Number.MAX_SAFE_INTEGER;
120
415
  }
121
- function IntegrationsPanel({ catalog, connections, healthByConnectionId, isLoading, error, onConnect, onDisconnect, getManageHref, onManage, getConnectionContext, getConnectionActions, emptyCatalogLabel = "No integrations available yet.", featuredIds = DEFAULT_FEATURED_IDS, defaultSort = "featured", skeletonCount = DEFAULT_SKELETON_COUNT, className }) {
122
- const tileBoxClass = cn(TILE_BOX_CLASS, getConnectionContext && "w-full min-h-[136px]");
416
+ /** Compatibility facade: legacy effects/sorting, one controlled catalog renderer. */
417
+ function IntegrationsPanel({ catalog, connections, healthByConnectionId, isLoading, error, onConnect, onDisconnect, getManageHref, onManage, getConnectionContext, getConnectionActions, emptyCatalogLabel = "No integrations available yet.", featuredIds = DEFAULT_FEATURED_IDS, defaultSort = "featured", skeletonCount = 12, className }) {
418
+ const [query, setQuery] = React$1.useState("");
419
+ const [sort, setSort] = React$1.useState(defaultSort);
420
+ const [selected, setSelected] = React$1.useState({});
123
421
  const [actionError, setActionError] = React$1.useState(null);
124
422
  const [pendingAction, setPendingAction] = React$1.useState(false);
423
+ const [connectingId, setConnectingId] = React$1.useState(null);
424
+ const [connectError, setConnectError] = React$1.useState(null);
425
+ const activeConnect = React$1.useRef(0);
426
+ const [disconnectTarget, setDisconnectTarget] = React$1.useState(null);
427
+ const [isDisconnecting, setIsDisconnecting] = React$1.useState(false);
428
+ const [disconnectError, setDisconnectError] = React$1.useState(null);
429
+ const activeDisconnect = React$1.useRef(0);
430
+ React$1.useEffect(() => () => {
431
+ activeConnect.current += 1;
432
+ activeDisconnect.current += 1;
433
+ }, []);
125
434
  const runConnectionAction = async (action) => {
126
435
  setActionError(null);
127
436
  setPendingAction(true);
128
437
  try {
129
438
  await action.onSelect();
130
- } catch (error) {
131
- setActionError(error instanceof Error ? error.message : "Could not update the connection.");
439
+ } catch (failure) {
440
+ setActionError(failure instanceof Error ? failure.message : "Could not update the connection.");
132
441
  } finally {
133
442
  setPendingAction(false);
134
443
  }
135
444
  };
136
- const [query, setQuery] = React$1.useState("");
137
- const [sort, setSort] = React$1.useState(defaultSort);
138
- const [disconnectTarget, setDisconnectTarget] = React$1.useState(null);
139
- const [isDisconnecting, setIsDisconnecting] = React$1.useState(false);
140
- const [disconnectError, setDisconnectError] = React$1.useState(null);
141
- const activeDisconnect = React$1.useRef(0);
142
- const connectionIndex = React$1.useMemo(() => buildConnectionIndex(connections), [connections]);
143
- const featuredRank = React$1.useMemo(() => makeFeaturedRank(featuredIds), [featuredIds]);
144
- const visible = React$1.useMemo(() => {
145
- const q = query.trim().toLowerCase();
146
- const sorted = [...q ? catalog.filter((p) => matchesQuery(p, q)) : catalog];
147
- sorted.sort((a, b) => {
148
- if (sort === "featured") {
149
- const ra = rankOf(a, featuredRank);
150
- const rb = rankOf(b, featuredRank);
151
- if (ra !== rb) return ra - rb;
152
- }
153
- return displayNameOf(a).localeCompare(displayNameOf(b));
154
- });
155
- return sorted;
156
- }, [
157
- catalog,
158
- query,
159
- sort,
160
- featuredRank
161
- ]);
162
- const closeDisconnect = React$1.useCallback(() => {
163
- activeDisconnect.current += 1;
164
- setIsDisconnecting(false);
165
- setDisconnectTarget(null);
166
- setDisconnectError(null);
167
- }, []);
168
- const [connectingId, setConnectingId] = React$1.useState(null);
169
- const [connectError, setConnectError] = React$1.useState(null);
170
- const activeConnect = React$1.useRef(0);
171
- const startConnect = React$1.useCallback(async (input) => {
172
- const reqId = activeConnect.current += 1;
445
+ const startConnect = async (provider) => {
446
+ const request = ++activeConnect.current;
173
447
  setConnectError(null);
174
448
  let pending;
175
449
  try {
176
- pending = onConnect(input);
177
- } catch (e) {
178
- setConnectError(e instanceof Error ? e.message : "Failed to start the connection.");
450
+ pending = onConnect({
451
+ providerId: provider.providerId,
452
+ connectorId: defaultConnectorOf(provider)
453
+ });
454
+ } catch (failure) {
455
+ setConnectError(failure instanceof Error ? failure.message : "Failed to start the connection.");
179
456
  return;
180
457
  }
181
458
  if (typeof pending?.then !== "function") return;
182
- setConnectingId(input.providerId);
459
+ setConnectingId(provider.providerId);
183
460
  try {
184
461
  await pending;
185
- } catch (e) {
186
- if (activeConnect.current !== reqId) return;
187
- setConnectError(e instanceof Error ? e.message : "Failed to start the connection.");
462
+ } catch (failure) {
463
+ if (activeConnect.current === request) setConnectError(failure instanceof Error ? failure.message : "Failed to start the connection.");
188
464
  } finally {
189
- if (activeConnect.current === reqId) setConnectingId(null);
465
+ if (activeConnect.current === request) setConnectingId(null);
190
466
  }
191
- }, [onConnect]);
192
- const confirmDisconnect = React$1.useCallback(async () => {
467
+ };
468
+ const closeDisconnect = React$1.useCallback(() => {
469
+ activeDisconnect.current += 1;
470
+ setIsDisconnecting(false);
471
+ setDisconnectTarget(null);
472
+ setDisconnectError(null);
473
+ }, []);
474
+ const confirmDisconnect = async () => {
193
475
  if (!disconnectTarget) return;
194
- const reqId = activeDisconnect.current += 1;
476
+ const request = ++activeDisconnect.current;
195
477
  setIsDisconnecting(true);
196
478
  setDisconnectError(null);
197
479
  try {
198
- await Promise.resolve(onDisconnect(disconnectTarget.connectionId));
199
- if (activeDisconnect.current !== reqId) return;
200
- setDisconnectTarget(null);
201
- } catch (e) {
202
- if (activeDisconnect.current !== reqId) return;
203
- setDisconnectError(e instanceof Error ? e.message : "Failed to disconnect.");
480
+ await onDisconnect(disconnectTarget.connection.id);
481
+ if (activeDisconnect.current === request) setDisconnectTarget(null);
482
+ } catch (failure) {
483
+ if (activeDisconnect.current === request) setDisconnectError(failure instanceof Error ? failure.message : "Failed to disconnect.");
204
484
  } finally {
205
- if (activeDisconnect.current === reqId) setIsDisconnecting(false);
485
+ if (activeDisconnect.current === request) setIsDisconnecting(false);
206
486
  }
207
- }, [disconnectTarget, onDisconnect]);
208
- if (error) return /* @__PURE__ */ jsx(Card, {
209
- className: cn("border-destructive/50", className),
210
- children: /* @__PURE__ */ jsx(CardContent, {
211
- className: "py-6",
212
- children: /* @__PURE__ */ jsxs("p", {
213
- className: "text-sm text-destructive",
214
- children: ["Failed to load integrations: ", error.message]
487
+ };
488
+ const rank = makeFeaturedRank(featuredIds);
489
+ const rows = [...catalog].sort((a, b) => {
490
+ if (sort === "featured") {
491
+ const delta = rankOf(a, rank) - rankOf(b, rank);
492
+ if (delta) return delta;
493
+ }
494
+ return displayNameOf(a).localeCompare(displayNameOf(b));
495
+ }).map((provider) => {
496
+ const live = connections.filter((connection) => connection.providerId === provider.providerId && connection.status !== "revoked");
497
+ const selectedId = Object.hasOwn(selected, provider.providerId) ? selected[provider.providerId] ?? null : live.length === 1 ? live[0].id : null;
498
+ return {
499
+ kind: "provider",
500
+ providerId: provider.providerId,
501
+ title: displayNameOf(provider),
502
+ description: provider.description,
503
+ iconUrl: provider.iconUrl,
504
+ canConnect: true,
505
+ selectedConnectionId: live.some((connection) => connection.id === selectedId) ? selectedId : null,
506
+ connections: live.map((connection) => {
507
+ const health = healthByConnectionId?.[connection.id];
508
+ const manageHref = getManageHref?.(connection);
509
+ return {
510
+ id: connection.id,
511
+ accountDisplay: connection.accountDisplay,
512
+ statusLabel: health?.message ?? connection.status.replace(/_/g, " "),
513
+ detail: getConnectionContext?.(connection),
514
+ manageHref,
515
+ manageInNewWindow: true,
516
+ capabilities: {
517
+ manage: Boolean(manageHref || onManage),
518
+ disconnect: true
519
+ },
520
+ actions: (getConnectionActions?.(connection) ?? []).map((action) => ({
521
+ id: action.id,
522
+ label: action.label,
523
+ disabled: action.disabled || pendingAction,
524
+ onSelect: () => {
525
+ runConnectionAction(action);
526
+ }
527
+ }))
528
+ };
215
529
  })
216
- })
530
+ };
217
531
  });
218
- if (isLoading && catalog.length === 0) return /* @__PURE__ */ jsxs("div", {
219
- className: cn("space-y-4", className),
220
- "data-testid": "integrations-skeleton",
221
- children: [/* @__PURE__ */ jsx(IntegrationsToolbar, {
222
- query,
223
- onQueryChange: setQuery,
224
- sort,
225
- onSortChange: setSort
226
- }), /* @__PURE__ */ jsx("div", {
227
- className: TILE_GRID_CLASS,
228
- children: Array.from({ length: skeletonCount }).map((_, i) => /* @__PURE__ */ jsxs("div", {
229
- "aria-hidden": true,
230
- className: cn(tileBoxClass, "animate-pulse border-border bg-muted/40"),
231
- children: [/* @__PURE__ */ jsx("div", {
232
- className: "shrink-0 rounded-2xl bg-muted",
233
- style: {
234
- width: LOGO_SIZE,
235
- height: LOGO_SIZE
236
- }
237
- }), /* @__PURE__ */ jsx("div", {
238
- className: "flex h-8 w-full items-center justify-center",
239
- children: /* @__PURE__ */ jsx("div", { className: "h-2.5 w-3/4 rounded bg-muted" })
532
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IntegrationsCatalog, {
533
+ rows,
534
+ query,
535
+ onQueryChange: setQuery,
536
+ sort,
537
+ onSortChange: setSort,
538
+ layout: "tiles",
539
+ loading: isLoading,
540
+ reserveConnectionContext: Boolean(getConnectionContext),
541
+ error: error ? `Failed to load integrations: ${error.message}` : null,
542
+ actionError,
543
+ connectError,
544
+ busyProviderId: connectingId,
545
+ skeletonCount,
546
+ emptyCatalogLabel,
547
+ className,
548
+ onSelectConnection: (providerId, id) => setSelected((current) => ({
549
+ ...current,
550
+ [providerId]: id
551
+ })),
552
+ onConnect: (row) => {
553
+ const provider = catalog.find((item) => item.providerId === row.providerId);
554
+ if (provider) startConnect(provider);
555
+ },
556
+ onManage: onManage ? (connection, row) => {
557
+ runConnectionAction({
558
+ id: "manage",
559
+ label: "Manage",
560
+ onSelect: () => onManage({
561
+ connectionId: connection.id,
562
+ providerId: row.providerId
563
+ })
564
+ });
565
+ } : void 0,
566
+ onDisconnect: (display, row) => {
567
+ const connection = connections.find((item) => item.id === display.id && item.providerId === row.providerId && item.status !== "revoked");
568
+ if (connection) setDisconnectTarget({
569
+ connection,
570
+ name: row.title
571
+ });
572
+ }
573
+ }), /* @__PURE__ */ jsx(Dialog, {
574
+ open: !!disconnectTarget,
575
+ onOpenChange: (open) => {
576
+ if (!open) closeDisconnect();
577
+ },
578
+ children: /* @__PURE__ */ jsxs(DialogContent, {
579
+ className: "max-w-sm",
580
+ onCloseAutoFocus: (event) => event.preventDefault(),
581
+ children: [
582
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
583
+ "Disconnect ",
584
+ disconnectTarget?.name ?? "integration",
585
+ "?"
586
+ ] }), /* @__PURE__ */ jsxs(DialogDescription, { children: [
587
+ "This removes Sandbox's access to your ",
588
+ disconnectTarget?.name ?? "this",
589
+ " account",
590
+ disconnectTarget?.connection.accountDisplay ? ` (${disconnectTarget.connection.accountDisplay})` : "",
591
+ ". You can reconnect anytime."
592
+ ] })] }),
593
+ disconnectError ? /* @__PURE__ */ jsx("p", {
594
+ className: "text-sm text-destructive",
595
+ role: "alert",
596
+ children: disconnectError
597
+ }) : null,
598
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
599
+ variant: "outline",
600
+ onClick: closeDisconnect,
601
+ "data-testid": "cancel-disconnect",
602
+ children: "Cancel"
603
+ }), /* @__PURE__ */ jsx(Button, {
604
+ variant: "destructive",
605
+ loading: isDisconnecting,
606
+ onClick: confirmDisconnect,
607
+ "data-testid": "confirm-disconnect",
608
+ children: "Disconnect"
609
+ })] })
610
+ ]
611
+ })
612
+ })] });
613
+ }
614
+ //#endregion
615
+ //#region src/integrations/connection-detail.tsx
616
+ /** Platform's permission layout, without its fetching, defaults or persistence. */
617
+ function IntegrationConnectionDetail({ provider, connections, selectedConnectionId, onSelectConnection, detailsByConnectionId, loading, error, backHref, description, onRetry, onTestConnection, onDisconnect, onDecisionChange, onResetDecision, className }) {
618
+ const connection = connections.find((item) => item.id === selectedConnectionId);
619
+ const details = connection && Object.hasOwn(detailsByConnectionId, connection.id) ? detailsByConnectionId[connection.id] : void 0;
620
+ const busy = !!details?.busy;
621
+ const groups = details?.permissionGroups;
622
+ const failure = error ?? details?.error;
623
+ const renderRows = (group) => /* @__PURE__ */ jsx("ul", {
624
+ className: "divide-y divide-border",
625
+ children: group.rows.map((row) => {
626
+ const known = row.decision !== null && row.decisionOptions.some((option) => option.value === row.decision);
627
+ const editable = connection?.capabilities.editPermissions && onDecisionChange;
628
+ return /* @__PURE__ */ jsxs("li", {
629
+ className: "flex min-w-0 flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between",
630
+ children: [/* @__PURE__ */ jsxs("div", {
631
+ className: "min-w-0",
632
+ children: [/* @__PURE__ */ jsxs("div", {
633
+ className: "flex min-w-0 flex-wrap items-center gap-2",
634
+ children: [/* @__PURE__ */ jsx("span", {
635
+ className: cn("rounded border border-border px-2 py-0.5 text-xs", row.riskTone === "error" ? "text-destructive" : "text-muted-foreground"),
636
+ children: row.riskLabel
637
+ }), /* @__PURE__ */ jsx("span", {
638
+ className: "min-w-0 break-words text-sm font-medium",
639
+ children: row.title
640
+ })]
641
+ }), /* @__PURE__ */ jsx("p", {
642
+ className: "mt-1 break-all font-mono text-xs text-muted-foreground",
643
+ children: row.actionPath
644
+ })]
645
+ }), /* @__PURE__ */ jsxs("div", {
646
+ className: "flex flex-wrap items-center gap-3 sm:shrink-0",
647
+ children: [
648
+ /* @__PURE__ */ jsx("span", {
649
+ className: "text-xs text-muted-foreground",
650
+ children: row.sourceLabel
651
+ }),
652
+ editable ? /* @__PURE__ */ jsxs("select", {
653
+ "aria-label": `Decision for ${row.actionPath}`,
654
+ value: known ? row.decision : "",
655
+ disabled: busy || row.disabled,
656
+ onChange: (event) => {
657
+ if (connection && row.decisionOptions.some((option) => option.value === event.target.value)) onDecisionChange?.(connection.id, row.actionPath, event.target.value);
658
+ },
659
+ className: cn("h-9 rounded-md border border-border bg-background px-2 text-sm", focusRing),
660
+ children: [!known ? /* @__PURE__ */ jsx("option", {
661
+ value: "",
662
+ disabled: true,
663
+ children: "Not available"
664
+ }) : null, row.decisionOptions.map((option) => /* @__PURE__ */ jsx("option", {
665
+ value: option.value,
666
+ children: option.label
667
+ }, option.value))]
668
+ }) : /* @__PURE__ */ jsx("span", {
669
+ className: "text-sm",
670
+ children: known ? row.decisionOptions.find((option) => option.value === row.decision)?.label : "Not available"
671
+ }),
672
+ row.canReset && connection?.capabilities.resetPermissions && onResetDecision ? /* @__PURE__ */ jsx(Button, {
673
+ variant: "outline",
674
+ disabled: busy || row.disabled,
675
+ "aria-label": `Reset ${row.actionPath}`,
676
+ onClick: () => onResetDecision(connection.id, row.actionPath),
677
+ children: "Reset"
678
+ }) : null
679
+ ]
240
680
  })]
241
- }, i))
242
- })]
243
- });
244
- if (catalog.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
245
- title: "No integrations",
246
- description: emptyCatalogLabel,
247
- className
681
+ }, row.actionPath);
682
+ })
248
683
  });
249
- const disconnectAccountLabel = disconnectTarget?.accountDisplay;
250
- return /* @__PURE__ */ jsxs("div", {
251
- className: cn("space-y-4", className),
252
- "data-testid": "integrations-panel",
684
+ return /* @__PURE__ */ jsxs("section", {
685
+ className: cn("w-full min-w-0 space-y-4", className),
686
+ "aria-label": `${provider.title} connection settings`,
687
+ "data-testid": "integration-connection-detail",
688
+ "aria-busy": loading || details?.loading || void 0,
253
689
  children: [
254
- /* @__PURE__ */ jsx(IntegrationsToolbar, {
255
- query,
256
- onQueryChange: setQuery,
257
- sort,
258
- onSortChange: setSort
259
- }),
260
- actionError ? /* @__PURE__ */ jsx("p", {
261
- role: "alert",
262
- className: "text-sm text-destructive",
263
- children: actionError
690
+ backHref ? /* @__PURE__ */ jsx("a", {
691
+ href: backHref,
692
+ className: cn("inline-flex text-sm text-muted-foreground hover:text-foreground", focusRing),
693
+ children: "← Integrations"
264
694
  }) : null,
265
- connectError ? /* @__PURE__ */ jsx("div", {
266
- role: "alert",
267
- "data-testid": "integration-connect-error",
268
- className: "mb-3 rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive",
269
- children: connectError
270
- }) : null,
271
- visible.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
272
- title: "No matches",
273
- description: `No integrations match "${query.trim()}".`
274
- }) : /* @__PURE__ */ jsx("div", {
275
- className: TILE_GRID_CLASS,
276
- children: visible.map((provider) => {
277
- const connectorId = defaultConnectorOf(provider);
278
- const live = connectionIndex.get(`${provider.providerId}:${connectorId}`);
279
- const name = displayNameOf(provider);
280
- if (Boolean(live) && live) {
281
- const connectionContext = getConnectionContext?.(live);
282
- const connectionActions = getConnectionActions?.(live) ?? [];
283
- const manageHref = getManageHref?.(live);
284
- const canManage = Boolean(manageHref) || Boolean(onManage);
285
- return /* @__PURE__ */ jsxs("div", {
286
- "data-testid": `integration-${provider.providerId}`,
287
- "data-connected": "true",
288
- title: live.accountDisplay ? `${name} — ${live.accountDisplay}` : name,
289
- className: cn(tileBoxClass, "group relative", "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)]"),
695
+ /* @__PURE__ */ jsxs("header", {
696
+ className: "flex min-w-0 items-start gap-3",
697
+ children: [/* @__PURE__ */ jsx(ProviderIcon, {
698
+ id: provider.providerId,
699
+ iconUrl: provider.iconUrl,
700
+ displayName: provider.title,
701
+ size: 48
702
+ }), /* @__PURE__ */ jsxs("div", {
703
+ className: "min-w-0",
704
+ children: [/* @__PURE__ */ jsxs("h2", {
705
+ className: "break-words text-xl font-semibold",
706
+ children: [provider.title, " connection settings"]
707
+ }), description ? /* @__PURE__ */ jsx("p", {
708
+ className: "mt-1 text-sm text-muted-foreground",
709
+ children: description
710
+ }) : null]
711
+ })]
712
+ }),
713
+ /* @__PURE__ */ jsx(ConnectionSelector, {
714
+ connections,
715
+ value: selectedConnectionId,
716
+ onChange: onSelectConnection,
717
+ label: `Account for ${provider.title}`
718
+ }),
719
+ /* @__PURE__ */ jsxs("div", {
720
+ className: "min-h-[200px] min-w-0 space-y-4",
721
+ children: [failure ? /* @__PURE__ */ jsxs("div", {
722
+ role: "alert",
723
+ className: "rounded-xl border border-destructive/40 bg-card p-4 text-sm text-destructive",
724
+ children: [/* @__PURE__ */ jsx("p", { children: failure }), onRetry ? /* @__PURE__ */ jsx(Button, {
725
+ variant: "outline",
726
+ onClick: () => onRetry(connection?.id ?? null),
727
+ children: "Retry"
728
+ }) : null]
729
+ }) : null, loading && !connection ? /* @__PURE__ */ jsx("p", {
730
+ role: "status",
731
+ className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
732
+ children: "Loading connections…"
733
+ }) : !connection ? /* @__PURE__ */ jsx("p", {
734
+ className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
735
+ children: connections.length ? "Select a connected account to view its settings." : "No connected accounts are available."
736
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
737
+ /* @__PURE__ */ jsxs("div", {
738
+ className: "flex flex-wrap items-start justify-between gap-3 rounded-xl border border-border bg-card p-4",
739
+ children: [/* @__PURE__ */ jsxs("div", {
740
+ className: "min-w-0",
290
741
  children: [
291
- manageHref ? /* @__PURE__ */ jsx("a", {
292
- href: manageHref,
293
- target: "_blank",
294
- rel: "noopener noreferrer",
295
- "aria-label": `Manage ${name} (opens in new window)`,
296
- "data-testid": `manage-${provider.providerId}`,
297
- className: `absolute inset-0 z-10 rounded-xl ${focusRing}`
298
- }) : null,
299
- /* @__PURE__ */ jsx("span", {
300
- className: "pointer-events-none absolute left-2 top-2 flex h-4 w-4 items-center justify-center rounded-full bg-[var(--surface-success-text)] text-white",
301
- children: /* @__PURE__ */ jsx(Check, {
302
- className: "h-3 w-3",
303
- strokeWidth: 3
304
- })
742
+ /* @__PURE__ */ jsx("p", {
743
+ className: "break-words text-sm font-medium",
744
+ children: connection.accountDisplay ?? connection.id
305
745
  }),
306
- /* @__PURE__ */ jsxs("div", {
307
- className: "pointer-events-none flex w-full min-w-0 flex-col items-center gap-2",
308
- children: [/* @__PURE__ */ jsx(ProviderIcon, {
309
- id: provider.providerId,
310
- iconUrl: provider.iconUrl,
311
- displayName: displayNameOf(provider),
312
- size: LOGO_SIZE,
313
- className: "rounded-2xl"
314
- }), /* @__PURE__ */ jsxs("div", {
315
- className: "flex h-8 w-full flex-col justify-center leading-4",
316
- children: [/* @__PURE__ */ jsx("span", {
317
- className: cn("w-full text-xs font-medium text-foreground", live.accountDisplay ? "truncate" : "line-clamp-2"),
318
- children: name
319
- }), live.accountDisplay ? /* @__PURE__ */ jsx("span", {
320
- className: "w-full truncate text-[11px] leading-4 text-muted-foreground",
321
- "data-testid": `account-${provider.providerId}`,
322
- children: live.accountDisplay
323
- }) : null]
324
- })]
746
+ /* @__PURE__ */ jsx("p", {
747
+ className: "text-sm text-muted-foreground",
748
+ children: connection.statusLabel
325
749
  }),
326
- connectionContext ? /* @__PURE__ */ jsx("span", {
327
- className: "pointer-events-none w-full truncate text-[11px] leading-4 text-muted-foreground",
328
- title: connectionContext,
329
- children: connectionContext
330
- }) : null,
331
- /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
332
- asChild: true,
333
- children: /* @__PURE__ */ jsx("button", {
334
- type: "button",
335
- "data-testid": `menu-${provider.providerId}`,
336
- "aria-label": `More actions for ${name}`,
337
- title: "More actions",
338
- className: `absolute right-1.5 top-1.5 z-20 flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground focus-visible:bg-background focus-visible:text-foreground ${focusRing}`,
339
- children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-3.5 w-3.5" })
340
- })
341
- }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
342
- side: "right",
343
- align: "start",
344
- className: "min-w-[160px] rounded-md",
345
- children: [
346
- manageHref ? /* @__PURE__ */ jsx(DropdownMenuItem, {
347
- asChild: true,
348
- children: /* @__PURE__ */ jsxs("a", {
349
- href: manageHref,
350
- target: "_blank",
351
- rel: "noopener noreferrer",
352
- "data-testid": `manage-item-${provider.providerId}`,
353
- children: [/* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Manage"]
354
- })
355
- }) : onManage ? /* @__PURE__ */ jsxs(DropdownMenuItem, {
356
- "data-testid": `manage-item-${provider.providerId}`,
357
- onClick: () => onManage({
358
- connectionId: live.id,
359
- providerId: provider.providerId
360
- }),
361
- children: [/* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Manage"]
362
- }) : null,
363
- connectionActions.map((action) => /* @__PURE__ */ jsx(DropdownMenuItem, {
364
- disabled: action.disabled || pendingAction,
365
- onSelect: () => {
366
- runConnectionAction(action);
367
- },
368
- children: action.label
369
- }, action.id)),
370
- canManage || connectionActions.length > 0 ? /* @__PURE__ */ jsx(DropdownMenuSeparator, {}) : null,
371
- /* @__PURE__ */ jsxs(DropdownMenuItem, {
372
- "data-testid": `disconnect-${provider.providerId}`,
373
- className: "text-destructive focus:text-destructive",
374
- onClick: () => setDisconnectTarget({
375
- connectionId: live.id,
376
- name,
377
- accountDisplay: live.accountDisplay
378
- }),
379
- children: [/* @__PURE__ */ jsx(Unplug, { className: "mr-2 h-4 w-4" }), " Disconnect"]
380
- })
381
- ]
382
- })] })
750
+ connection.detail ? /* @__PURE__ */ jsx("p", {
751
+ className: "mt-1 text-sm text-muted-foreground",
752
+ children: connection.detail
753
+ }) : null
383
754
  ]
384
- }, `${provider.providerId}:${connectorId}`);
385
- }
386
- return /* @__PURE__ */ jsxs("button", {
387
- type: "button",
388
- "data-testid": `integration-${provider.providerId}`,
389
- "data-connected": "false",
390
- "data-connecting": connectingId === provider.providerId ? "true" : void 0,
391
- disabled: connectingId === provider.providerId,
392
- "aria-busy": connectingId === provider.providerId,
393
- onClick: () => {
394
- startConnect({
395
- providerId: provider.providerId,
396
- connectorId
397
- });
398
- },
399
- title: provider.description ? provider.description : `Connect ${name}`,
400
- className: cn(tileBoxClass, "group border-border bg-card transition-all", "hover:border-[var(--border-strong)] hover:bg-accent/40 hover:shadow-sm", focusRing),
401
- children: [/* @__PURE__ */ jsx(ProviderIcon, {
402
- id: provider.providerId,
403
- iconUrl: provider.iconUrl,
404
- displayName: displayNameOf(provider),
405
- size: LOGO_SIZE,
406
- className: "rounded-2xl"
407
- }), /* @__PURE__ */ jsx("span", {
408
- className: "line-clamp-2 h-8 w-full text-xs font-medium leading-4 text-foreground",
409
- children: name
755
+ }), /* @__PURE__ */ jsxs("div", {
756
+ className: "flex flex-wrap gap-2",
757
+ children: [connection.capabilities.test && onTestConnection ? /* @__PURE__ */ jsx(Button, {
758
+ variant: "outline",
759
+ disabled: busy,
760
+ onClick: () => onTestConnection(connection.id),
761
+ children: "Test connection"
762
+ }) : null, connection.capabilities.disconnect && onDisconnect ? /* @__PURE__ */ jsx(Button, {
763
+ variant: "destructive",
764
+ disabled: busy,
765
+ onClick: () => onDisconnect(connection.id),
766
+ children: "Disconnect"
767
+ }) : null]
410
768
  })]
411
- }, `${provider.providerId}:${connectorId}`);
412
- })
413
- }),
414
- /* @__PURE__ */ jsx(Dialog, {
415
- open: !!disconnectTarget,
416
- onOpenChange: (open) => {
417
- if (!open) closeDisconnect();
418
- },
419
- children: /* @__PURE__ */ jsxs(DialogContent, {
420
- className: "max-w-sm",
421
- onCloseAutoFocus: (e) => e.preventDefault(),
422
- children: [
423
- /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
424
- "Disconnect ",
425
- disconnectTarget?.name ?? "integration",
426
- "?"
427
- ] }), /* @__PURE__ */ jsxs(DialogDescription, { children: [
428
- "This removes Sandbox's access to your",
429
- " ",
430
- disconnectTarget?.name ?? "this",
431
- " account",
432
- disconnectAccountLabel ? ` (${disconnectAccountLabel})` : "",
433
- ". You can reconnect anytime."
434
- ] })] }),
435
- disconnectError ? /* @__PURE__ */ jsx("p", {
436
- className: "text-sm text-destructive",
437
- role: "alert",
438
- children: disconnectError
439
- }) : null,
440
- /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
441
- variant: "outline",
442
- onClick: closeDisconnect,
443
- "data-testid": "cancel-disconnect",
444
- children: "Cancel"
445
- }), /* @__PURE__ */ jsx(Button, {
446
- variant: "destructive",
447
- loading: isDisconnecting,
448
- onClick: confirmDisconnect,
449
- "data-testid": "confirm-disconnect",
450
- children: "Disconnect"
451
- })] })
452
- ]
453
- })
454
- })
769
+ }),
770
+ details?.testResult ? /* @__PURE__ */ jsx("p", {
771
+ role: details.testResult.tone === "error" ? "alert" : "status",
772
+ className: cn("text-sm", details.testResult.tone === "error" ? "text-destructive" : "text-muted-foreground"),
773
+ children: details.testResult.message
774
+ }) : null,
775
+ details?.loading ? /* @__PURE__ */ jsx("p", {
776
+ role: "status",
777
+ className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
778
+ children: "Loading connection settings…"
779
+ }) : !groups ? /* @__PURE__ */ jsx("p", {
780
+ className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
781
+ children: "Connection settings are not available yet."
782
+ }) : groups.every((group) => group.rows.length === 0) ? /* @__PURE__ */ jsx("p", {
783
+ className: "rounded-xl border border-border bg-card p-6 text-sm text-muted-foreground",
784
+ children: "No actions are available for this connection."
785
+ }) : groups.map((group) => group.collapsed ? /* @__PURE__ */ jsxs("details", {
786
+ className: "min-w-0 rounded-xl border border-border bg-card",
787
+ children: [
788
+ /* @__PURE__ */ jsxs("summary", {
789
+ className: cn("cursor-pointer rounded-xl p-4 font-medium", focusRing),
790
+ children: [
791
+ group.title,
792
+ " (",
793
+ group.rows.length,
794
+ ")"
795
+ ]
796
+ }),
797
+ group.description ? /* @__PURE__ */ jsx("p", {
798
+ className: "px-4 pb-4 text-sm text-muted-foreground",
799
+ children: group.description
800
+ }) : null,
801
+ renderRows(group)
802
+ ]
803
+ }, group.id) : /* @__PURE__ */ jsxs("section", {
804
+ className: "min-w-0 rounded-xl border border-border bg-card",
805
+ children: [/* @__PURE__ */ jsxs("div", {
806
+ className: "border-b border-border p-4",
807
+ children: [/* @__PURE__ */ jsx("h3", {
808
+ className: "font-medium",
809
+ children: group.title
810
+ }), group.description ? /* @__PURE__ */ jsx("p", {
811
+ className: "mt-1 text-sm text-muted-foreground",
812
+ children: group.description
813
+ }) : null]
814
+ }), renderRows(group)]
815
+ }, group.id))
816
+ ] })]
817
+ }, connection?.id ?? "unselected")
455
818
  ]
456
819
  });
457
820
  }
458
821
  //#endregion
822
+ //#region src/integrations/connection-dialogs.tsx
823
+ const fieldClass = cn("h-10 w-full min-w-0 rounded-md border border-border bg-background px-3 text-sm disabled:opacity-50", focusRing);
824
+ function ConnectionDialog({ children, complete, submitLabel, ...props }) {
825
+ const opener = React$1.useRef(null);
826
+ const errorId = React$1.useId();
827
+ return /* @__PURE__ */ jsx(Dialog, {
828
+ open: props.open,
829
+ onOpenChange: props.onOpenChange,
830
+ children: /* @__PURE__ */ jsxs(DialogContent, {
831
+ className: "max-h-[90dvh] max-w-md overflow-y-auto",
832
+ onOpenAutoFocus: () => {
833
+ opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
834
+ },
835
+ onCloseAutoFocus: (event) => {
836
+ event.preventDefault();
837
+ if (opener.current?.isConnected) opener.current.focus();
838
+ },
839
+ onEscapeKeyDown: (event) => event.stopPropagation(),
840
+ children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: ["Connect ", props.title] }), /* @__PURE__ */ jsx(DialogDescription, { children: props.description ?? "Enter the connection details requested by this application." })] }), /* @__PURE__ */ jsxs("form", {
841
+ className: "min-w-0 space-y-4",
842
+ "aria-busy": props.busy || void 0,
843
+ "aria-describedby": props.error ? errorId : void 0,
844
+ onSubmit: (event) => {
845
+ event.preventDefault();
846
+ if (complete && !props.busy && props.canSubmit !== false) props.onSubmit();
847
+ },
848
+ children: [
849
+ children,
850
+ props.error ? /* @__PURE__ */ jsx("p", {
851
+ id: errorId,
852
+ role: "alert",
853
+ className: "text-sm text-destructive",
854
+ children: props.error
855
+ }) : null,
856
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
857
+ type: "button",
858
+ variant: "outline",
859
+ onClick: () => props.onOpenChange(false),
860
+ children: "Cancel"
861
+ }), /* @__PURE__ */ jsx(Button, {
862
+ type: "submit",
863
+ disabled: !complete || props.busy || props.canSubmit === false,
864
+ children: props.busy ? "Connecting…" : submitLabel
865
+ })] })
866
+ ]
867
+ })]
868
+ })
869
+ });
870
+ }
871
+ function ApiKeyField({ value, onValueChange, busy, error }) {
872
+ const id = React$1.useId();
873
+ const [revealed, setRevealed] = React$1.useState(false);
874
+ return /* @__PURE__ */ jsxs("div", {
875
+ className: "space-y-2",
876
+ children: [/* @__PURE__ */ jsx("label", {
877
+ htmlFor: id,
878
+ className: "block text-sm font-medium",
879
+ children: "API key"
880
+ }), /* @__PURE__ */ jsxs("div", {
881
+ className: "flex min-w-0 gap-2",
882
+ children: [/* @__PURE__ */ jsx("input", {
883
+ id,
884
+ type: revealed ? "text" : "password",
885
+ value,
886
+ required: true,
887
+ onChange: (event) => onValueChange(event.target.value),
888
+ disabled: busy,
889
+ autoComplete: "off",
890
+ autoCapitalize: "none",
891
+ spellCheck: false,
892
+ "aria-invalid": !!error || void 0,
893
+ className: cn(fieldClass, "font-mono")
894
+ }), /* @__PURE__ */ jsx(Button, {
895
+ type: "button",
896
+ variant: "outline",
897
+ "aria-label": revealed ? "Hide API key" : "Show API key",
898
+ "aria-pressed": revealed,
899
+ onClick: () => setRevealed((current) => !current),
900
+ disabled: busy,
901
+ children: revealed ? "Hide" : "Show"
902
+ })]
903
+ })]
904
+ });
905
+ }
906
+ /** Key bytes remain in owner-controlled value/onValueChange; no request or storage. */
907
+ function ApiKeyConnectDialog(props) {
908
+ return /* @__PURE__ */ jsxs(ConnectionDialog, {
909
+ ...props,
910
+ complete: props.value.trim().length > 0,
911
+ submitLabel: "Connect",
912
+ children: [/* @__PURE__ */ jsx(ApiKeyField, {
913
+ value: props.value,
914
+ onValueChange: props.onValueChange,
915
+ busy: props.busy,
916
+ error: props.error
917
+ }, props.providerId), props.children]
918
+ });
919
+ }
920
+ function ParameterFields({ parameters, values, onValueChange, busy, error }) {
921
+ const prefix = React$1.useId();
922
+ return /* @__PURE__ */ jsx(Fragment, { children: parameters.map((parameter) => {
923
+ const id = `${prefix}-${parameter.key}`;
924
+ return /* @__PURE__ */ jsxs("div", {
925
+ className: "space-y-2",
926
+ children: [
927
+ /* @__PURE__ */ jsx("label", {
928
+ htmlFor: id,
929
+ className: "block text-sm font-medium",
930
+ children: parameter.label
931
+ }),
932
+ /* @__PURE__ */ jsx("input", {
933
+ id,
934
+ type: "text",
935
+ value: values[parameter.key] ?? "",
936
+ onChange: (event) => onValueChange(parameter.key, event.target.value),
937
+ required: parameter.required,
938
+ disabled: busy,
939
+ autoComplete: "off",
940
+ autoCapitalize: "none",
941
+ spellCheck: false,
942
+ placeholder: parameter.placeholder,
943
+ "aria-invalid": !!error || void 0,
944
+ "aria-describedby": parameter.description ? `${id}-hint` : void 0,
945
+ className: fieldClass
946
+ }),
947
+ parameter.description ? /* @__PURE__ */ jsx("p", {
948
+ id: `${id}-hint`,
949
+ className: "text-xs text-muted-foreground",
950
+ children: parameter.description
951
+ }) : null
952
+ ]
953
+ }, parameter.key);
954
+ }) });
955
+ }
956
+ /** Parameter schema/values and all auth behavior belong to the owner. */
957
+ function OAuthConnectionParameterDialog(props) {
958
+ const complete = props.parameters.every((parameter) => !parameter.required || !!props.values[parameter.key]?.trim());
959
+ return /* @__PURE__ */ jsxs(ConnectionDialog, {
960
+ ...props,
961
+ complete,
962
+ submitLabel: "Continue",
963
+ children: [/* @__PURE__ */ jsx(ParameterFields, { ...props }, props.providerId), props.children]
964
+ });
965
+ }
966
+ //#endregion
459
967
  //#region src/integrations/use-integrations.ts
460
968
  function unwrap(json) {
461
969
  if (json && typeof json === "object" && "data" in json && json.data !== void 0) return json.data;
@@ -652,4 +1160,4 @@ function useIntegrations({ apiBaseUrl, fetchImpl, autoLoad = true, refreshOnRetu
652
1160
  };
653
1161
  }
654
1162
  //#endregion
655
- export { IntegrationsPanel, ProviderIcon, providerLogoCandidates, useIntegrations };
1163
+ export { ApiKeyConnectDialog, ApiKeyConnectDialog as ApiKeyConnectModal, IntegrationConnectionDetail, IntegrationsCatalog, IntegrationsPanel, OAuthConnectionParameterDialog, OAuthConnectionParameterDialog as OAuthConnectionParameterModal, ProviderIcon, catalogRowActive, providerLogoCandidates, useIntegrations };