@greatapps/greatagents-ui 0.3.9 → 0.3.11
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/index.d.ts +13 -0
- package/dist/index.js +1059 -1001
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/capabilities/advanced-tab.tsx +1 -18
- package/src/components/capabilities/capabilities-tab.tsx +11 -47
- package/src/components/capabilities/integration-card.tsx +79 -10
- package/src/components/capabilities/integration-wizard.tsx +9 -4
- package/src/components/capabilities/integrations-tab.tsx +60 -8
- package/src/hooks/use-integrations.ts +69 -22
- package/src/types/index.ts +2 -0
package/package.json
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import type { GagentsHookConfig } from "../../hooks/types";
|
|
5
|
-
import { useToolCredentials } from "../../hooks";
|
|
6
5
|
import { ToolsTable } from "../tools/tools-table";
|
|
7
|
-
import { ToolCredentialsForm } from "../tools/tool-credentials-form";
|
|
8
6
|
import { ToolFormDialog } from "../tools/tool-form-dialog";
|
|
9
7
|
import type { Tool } from "../../types";
|
|
10
8
|
import { Info } from "lucide-react";
|
|
@@ -24,10 +22,6 @@ export interface AdvancedTabProps {
|
|
|
24
22
|
// ---------------------------------------------------------------------------
|
|
25
23
|
|
|
26
24
|
export function AdvancedTab({ config, agentId, gagentsApiUrl }: AdvancedTabProps) {
|
|
27
|
-
const { data: credentialsData, isLoading: isLoadingCredentials } =
|
|
28
|
-
useToolCredentials(config);
|
|
29
|
-
const credentials = credentialsData?.data ?? [];
|
|
30
|
-
|
|
31
25
|
const [editingTool, setEditingTool] = useState<Tool | null>(null);
|
|
32
26
|
const [showToolForm, setShowToolForm] = useState(false);
|
|
33
27
|
|
|
@@ -49,7 +43,7 @@ export function AdvancedTab({ config, agentId, gagentsApiUrl }: AdvancedTabProps
|
|
|
49
43
|
<p className="text-sm text-blue-800 dark:text-blue-300">
|
|
50
44
|
Use as abas <strong>Capacidades</strong> e <strong>Integrações</strong> para
|
|
51
45
|
configuração simplificada. Esta aba oferece controlo manual avançado sobre
|
|
52
|
-
ferramentas
|
|
46
|
+
ferramentas. As credenciais são geridas dentro de cada ferramenta.
|
|
53
47
|
</p>
|
|
54
48
|
</div>
|
|
55
49
|
|
|
@@ -59,17 +53,6 @@ export function AdvancedTab({ config, agentId, gagentsApiUrl }: AdvancedTabProps
|
|
|
59
53
|
<ToolsTable onEdit={handleEditTool} config={config} />
|
|
60
54
|
</section>
|
|
61
55
|
|
|
62
|
-
{/* Credenciais section */}
|
|
63
|
-
<section className="space-y-3">
|
|
64
|
-
<h3 className="text-sm font-medium">Credenciais</h3>
|
|
65
|
-
<ToolCredentialsForm
|
|
66
|
-
credentials={credentials}
|
|
67
|
-
isLoading={isLoadingCredentials}
|
|
68
|
-
config={config}
|
|
69
|
-
gagentsApiUrl={gagentsApiUrl}
|
|
70
|
-
/>
|
|
71
|
-
</section>
|
|
72
|
-
|
|
73
56
|
{/* Tool edit dialog */}
|
|
74
57
|
<ToolFormDialog
|
|
75
58
|
open={showToolForm}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback,
|
|
3
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
4
4
|
import type { GagentsHookConfig } from "../../hooks/types";
|
|
5
5
|
import {
|
|
6
6
|
useCapabilities,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
HeartHandshake,
|
|
32
32
|
Package,
|
|
33
33
|
ChevronDown,
|
|
34
|
+
Loader2,
|
|
34
35
|
} from "lucide-react";
|
|
35
36
|
import { toast } from "sonner";
|
|
36
37
|
|
|
@@ -129,7 +130,6 @@ export function CapabilitiesTab({ config, agentId }: CapabilitiesTabProps) {
|
|
|
129
130
|
const [localState, setLocalState] = useState<CapabilityState>(new Map());
|
|
130
131
|
const [serverState, setServerState] = useState<CapabilityState>(new Map());
|
|
131
132
|
const [initialized, setInitialized] = useState(false);
|
|
132
|
-
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
133
133
|
|
|
134
134
|
// Sync server data into local state on first load / refetch
|
|
135
135
|
useEffect(() => {
|
|
@@ -151,41 +151,12 @@ export function CapabilitiesTab({ config, agentId }: CapabilitiesTabProps) {
|
|
|
151
151
|
[localState, serverState, initialized],
|
|
152
152
|
);
|
|
153
153
|
|
|
154
|
-
// ------ Debounced save ------
|
|
155
|
-
const scheduleSave = useCallback(
|
|
156
|
-
(nextState: CapabilityState) => {
|
|
157
|
-
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
158
|
-
debounceRef.current = setTimeout(() => {
|
|
159
|
-
const payload = stateToPayload(nextState);
|
|
160
|
-
updateMutation.mutate(
|
|
161
|
-
{ agentId, payload },
|
|
162
|
-
{
|
|
163
|
-
onSuccess: () => {
|
|
164
|
-
setServerState(cloneState(nextState));
|
|
165
|
-
toast.success("Capacidades salvas");
|
|
166
|
-
},
|
|
167
|
-
onError: () => {
|
|
168
|
-
// Rollback to server state
|
|
169
|
-
setLocalState(cloneState(serverState));
|
|
170
|
-
toast.error("Erro ao salvar capacidades");
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
);
|
|
174
|
-
}, 500);
|
|
175
|
-
},
|
|
176
|
-
[agentId, updateMutation, serverState],
|
|
177
|
-
);
|
|
178
|
-
|
|
179
154
|
// ------ State mutation helpers ------
|
|
180
155
|
const updateState = useCallback(
|
|
181
156
|
(updater: (prev: CapabilityState) => CapabilityState) => {
|
|
182
|
-
setLocalState((prev) =>
|
|
183
|
-
const next = updater(prev);
|
|
184
|
-
scheduleSave(next);
|
|
185
|
-
return next;
|
|
186
|
-
});
|
|
157
|
+
setLocalState((prev) => updater(prev));
|
|
187
158
|
},
|
|
188
|
-
[
|
|
159
|
+
[],
|
|
189
160
|
);
|
|
190
161
|
|
|
191
162
|
const toggleModule = useCallback(
|
|
@@ -242,12 +213,10 @@ export function CapabilitiesTab({ config, agentId }: CapabilitiesTabProps) {
|
|
|
242
213
|
}, [updateState]);
|
|
243
214
|
|
|
244
215
|
const discard = useCallback(() => {
|
|
245
|
-
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
246
216
|
setLocalState(cloneState(serverState));
|
|
247
217
|
}, [serverState]);
|
|
248
218
|
|
|
249
219
|
const saveNow = useCallback(() => {
|
|
250
|
-
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
251
220
|
const payload = stateToPayload(localState);
|
|
252
221
|
updateMutation.mutate(
|
|
253
222
|
{ agentId, payload },
|
|
@@ -368,20 +337,15 @@ export function CapabilitiesTab({ config, agentId }: CapabilitiesTabProps) {
|
|
|
368
337
|
|
|
369
338
|
{/* Save bar */}
|
|
370
339
|
{hasChanges && (
|
|
371
|
-
<div className="sticky bottom-0
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<div className="flex items-center gap-2">
|
|
376
|
-
<Button variant="outline" size="sm" onClick={discard}>
|
|
340
|
+
<div className="sticky bottom-0 z-10 flex items-center justify-between gap-2 rounded-lg border bg-background p-3 shadow-sm">
|
|
341
|
+
<p className="text-sm text-muted-foreground">Você tem alterações não salvas.</p>
|
|
342
|
+
<div className="flex gap-2">
|
|
343
|
+
<Button variant="ghost" size="sm" onClick={discard} disabled={updateMutation.isPending}>
|
|
377
344
|
Descartar
|
|
378
345
|
</Button>
|
|
379
|
-
<Button
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
disabled={updateMutation.isPending}
|
|
383
|
-
>
|
|
384
|
-
{updateMutation.isPending ? "Salvando..." : "Salvar"}
|
|
346
|
+
<Button size="sm" onClick={saveNow} disabled={updateMutation.isPending}>
|
|
347
|
+
{updateMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
348
|
+
Salvar
|
|
385
349
|
</Button>
|
|
386
350
|
</div>
|
|
387
351
|
</div>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
RefreshCw,
|
|
10
10
|
Users,
|
|
11
11
|
Clock,
|
|
12
|
+
Plus,
|
|
12
13
|
} from "lucide-react";
|
|
13
14
|
import type { LucideIcon } from "lucide-react";
|
|
14
15
|
import { cn } from "../../lib";
|
|
@@ -24,6 +25,7 @@ const ICON_MAP: Record<string, LucideIcon> = {
|
|
|
24
25
|
RefreshCw,
|
|
25
26
|
Users,
|
|
26
27
|
Clock,
|
|
28
|
+
Plus,
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
function resolveIcon(name: string): LucideIcon {
|
|
@@ -58,8 +60,9 @@ const STATE_BADGES: Record<IntegrationCardState, BadgeVariant> = {
|
|
|
58
60
|
},
|
|
59
61
|
};
|
|
60
62
|
|
|
61
|
-
function getActionLabel(
|
|
62
|
-
|
|
63
|
+
function getActionLabel(card: IntegrationCardData): string {
|
|
64
|
+
if (card.isAddNew) return "Conectar";
|
|
65
|
+
switch (card.state) {
|
|
63
66
|
case "available":
|
|
64
67
|
return "Conectar";
|
|
65
68
|
case "connected":
|
|
@@ -81,11 +84,71 @@ export interface IntegrationCardProps {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
export function IntegrationCard({ card, onConnect }: IntegrationCardProps) {
|
|
84
|
-
const { definition, state, sharedByAgentsCount } = card;
|
|
87
|
+
const { definition, state, sharedByAgentsCount, isAddNew, accountLabel } = card;
|
|
85
88
|
const Icon = resolveIcon(definition.icon);
|
|
86
|
-
const badge = STATE_BADGES[state];
|
|
87
|
-
const actionLabel = getActionLabel(state);
|
|
88
89
|
const isComingSoon = state === "coming_soon";
|
|
90
|
+
const actionLabel = getActionLabel(card);
|
|
91
|
+
|
|
92
|
+
// "Add new" card uses a muted/outlined style
|
|
93
|
+
if (isAddNew) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
className={cn(
|
|
97
|
+
"group relative flex flex-col gap-3 rounded-xl border border-dashed bg-card/50 p-5 transition-shadow",
|
|
98
|
+
"hover:shadow-md hover:border-solid hover:bg-card cursor-pointer",
|
|
99
|
+
)}
|
|
100
|
+
role="button"
|
|
101
|
+
tabIndex={0}
|
|
102
|
+
aria-label={`Adicionar conta ${definition.name}`}
|
|
103
|
+
onClick={() => onConnect(card)}
|
|
104
|
+
onKeyDown={(e) => {
|
|
105
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
onConnect(card);
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{/* Header row */}
|
|
112
|
+
<div className="flex items-start justify-between gap-2">
|
|
113
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/5 text-primary/60">
|
|
114
|
+
<Icon className="h-5 w-5" />
|
|
115
|
+
</div>
|
|
116
|
+
<Badge variant="outline" className="text-xs bg-muted text-muted-foreground">
|
|
117
|
+
Adicionar
|
|
118
|
+
</Badge>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Name + description */}
|
|
122
|
+
<div className="space-y-1">
|
|
123
|
+
<h3 className="text-sm font-semibold leading-tight text-muted-foreground">
|
|
124
|
+
{definition.name}
|
|
125
|
+
</h3>
|
|
126
|
+
<p className="text-xs text-muted-foreground/70 leading-relaxed flex items-center gap-1">
|
|
127
|
+
<Plus className="h-3 w-3" />
|
|
128
|
+
Adicionar conta
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Footer */}
|
|
133
|
+
<div className="mt-auto flex items-center justify-end pt-1">
|
|
134
|
+
<Button
|
|
135
|
+
variant="outline"
|
|
136
|
+
size="sm"
|
|
137
|
+
className="text-xs"
|
|
138
|
+
onClick={(e) => {
|
|
139
|
+
e.stopPropagation();
|
|
140
|
+
onConnect(card);
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{actionLabel}
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Connected / expired / available card
|
|
151
|
+
const badge = STATE_BADGES[state];
|
|
89
152
|
|
|
90
153
|
return (
|
|
91
154
|
<div
|
|
@@ -97,7 +160,7 @@ export function IntegrationCard({ card, onConnect }: IntegrationCardProps) {
|
|
|
97
160
|
)}
|
|
98
161
|
role="button"
|
|
99
162
|
tabIndex={isComingSoon ? -1 : 0}
|
|
100
|
-
aria-label={`${definition.name} — ${badge.label}`}
|
|
163
|
+
aria-label={`${definition.name}${accountLabel ? ` — ${accountLabel}` : ""} — ${badge.label}`}
|
|
101
164
|
aria-disabled={isComingSoon}
|
|
102
165
|
onClick={() => !isComingSoon && onConnect(card)}
|
|
103
166
|
onKeyDown={(e) => {
|
|
@@ -117,12 +180,18 @@ export function IntegrationCard({ card, onConnect }: IntegrationCardProps) {
|
|
|
117
180
|
</Badge>
|
|
118
181
|
</div>
|
|
119
182
|
|
|
120
|
-
{/* Name +
|
|
183
|
+
{/* Name + account label */}
|
|
121
184
|
<div className="space-y-1">
|
|
122
185
|
<h3 className="text-sm font-semibold leading-tight">{definition.name}</h3>
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
|
|
186
|
+
{accountLabel ? (
|
|
187
|
+
<p className="text-xs text-muted-foreground leading-relaxed truncate" title={accountLabel}>
|
|
188
|
+
{accountLabel}
|
|
189
|
+
</p>
|
|
190
|
+
) : (
|
|
191
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
192
|
+
{definition.description}
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
126
195
|
</div>
|
|
127
196
|
|
|
128
197
|
{/* Footer */}
|
|
@@ -225,10 +225,15 @@ export function IntegrationWizard({
|
|
|
225
225
|
|
|
226
226
|
try {
|
|
227
227
|
// 1. Get auth URL from backend
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
228
|
+
// If reconnecting an existing credential, pass credential_id so the
|
|
229
|
+
// backend updates that specific credential instead of creating a new one.
|
|
230
|
+
let authorizeUrl = `${gagentsApiUrl}/v1/${language}/${idWl}/accounts/${accountId}/oauth/authorize/${integration.slug}`;
|
|
231
|
+
if (existingCredentialId) {
|
|
232
|
+
authorizeUrl += `?credential_id=${existingCredentialId}`;
|
|
233
|
+
}
|
|
234
|
+
const response = await fetch(authorizeUrl, {
|
|
235
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
236
|
+
});
|
|
232
237
|
const result = await response.json();
|
|
233
238
|
|
|
234
239
|
if (result.status !== 1 || !result.data?.auth_url) {
|
|
@@ -17,6 +17,20 @@ export interface IntegrationsTabProps {
|
|
|
17
17
|
onConnect: (card: IntegrationCardData) => void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function getCardKey(card: IntegrationCardData): string {
|
|
25
|
+
if (card.credentialId) {
|
|
26
|
+
return `${card.definition.slug}-cred-${card.credentialId}`;
|
|
27
|
+
}
|
|
28
|
+
if (card.isAddNew) {
|
|
29
|
+
return `${card.definition.slug}-add-new`;
|
|
30
|
+
}
|
|
31
|
+
return card.definition.slug;
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
// ---------------------------------------------------------------------------
|
|
21
35
|
// Component
|
|
22
36
|
// ---------------------------------------------------------------------------
|
|
@@ -47,15 +61,53 @@ export function IntegrationsTab({
|
|
|
47
61
|
);
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
// Split into connected/expired cards and add-new/coming-soon cards
|
|
65
|
+
const connectedCards = cards.filter(
|
|
66
|
+
(c) => !c.isAddNew && (c.state === "connected" || c.state === "expired"),
|
|
67
|
+
);
|
|
68
|
+
const otherCards = cards.filter(
|
|
69
|
+
(c) => c.isAddNew || c.state === "coming_soon",
|
|
70
|
+
);
|
|
71
|
+
|
|
50
72
|
return (
|
|
51
|
-
<div className="
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
<div className="space-y-6">
|
|
74
|
+
{/* Connected accounts */}
|
|
75
|
+
{connectedCards.length > 0 && (
|
|
76
|
+
<div>
|
|
77
|
+
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
78
|
+
Contas conectadas
|
|
79
|
+
</h3>
|
|
80
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
81
|
+
{connectedCards.map((card) => (
|
|
82
|
+
<IntegrationCard
|
|
83
|
+
key={getCardKey(card)}
|
|
84
|
+
card={card}
|
|
85
|
+
onConnect={onConnect}
|
|
86
|
+
/>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{/* Add new / coming soon */}
|
|
93
|
+
{otherCards.length > 0 && (
|
|
94
|
+
<div>
|
|
95
|
+
{connectedCards.length > 0 && (
|
|
96
|
+
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
97
|
+
Adicionar integração
|
|
98
|
+
</h3>
|
|
99
|
+
)}
|
|
100
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
101
|
+
{otherCards.map((card) => (
|
|
102
|
+
<IntegrationCard
|
|
103
|
+
key={getCardKey(card)}
|
|
104
|
+
card={card}
|
|
105
|
+
onConnect={onConnect}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
59
111
|
</div>
|
|
60
112
|
);
|
|
61
113
|
}
|
|
@@ -32,6 +32,12 @@ export interface IntegrationCardData {
|
|
|
32
32
|
sharedByAgentsCount: number;
|
|
33
33
|
/** Whether this agent has a linked agent_tool for this integration */
|
|
34
34
|
linkedToAgent: boolean;
|
|
35
|
+
/** Credential ID — set for connected/expired cards, undefined for "add new" */
|
|
36
|
+
credentialId?: number;
|
|
37
|
+
/** Account label (email or label from credential) */
|
|
38
|
+
accountLabel?: string;
|
|
39
|
+
/** Whether this is the "add new account" card */
|
|
40
|
+
isAddNew?: boolean;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
// ---------------------------------------------------------------------------
|
|
@@ -42,6 +48,11 @@ export interface IntegrationCardData {
|
|
|
42
48
|
* Cross-references the static integrations registry with live data
|
|
43
49
|
* (tools, tool_credentials, agent_tools) to produce card state for each
|
|
44
50
|
* integration entry.
|
|
51
|
+
*
|
|
52
|
+
* Returns:
|
|
53
|
+
* - One card per existing credential (connected/expired)
|
|
54
|
+
* - One "add new" card per integration type
|
|
55
|
+
* - Coming soon cards as before
|
|
45
56
|
*/
|
|
46
57
|
export function useIntegrationState(
|
|
47
58
|
config: GagentsHookConfig,
|
|
@@ -62,52 +73,88 @@ export function useIntegrationState(
|
|
|
62
73
|
const tools: Tool[] = toolsData?.data ?? [];
|
|
63
74
|
const agentTools: AgentTool[] = agentToolsData?.data ?? [];
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
const result: IntegrationCardData[] = [];
|
|
77
|
+
|
|
78
|
+
for (const def of INTEGRATIONS_REGISTRY) {
|
|
66
79
|
// coming_soon short-circuit
|
|
67
80
|
if (def.status === "coming_soon") {
|
|
68
|
-
|
|
81
|
+
result.push({
|
|
69
82
|
definition: def,
|
|
70
83
|
state: "coming_soon" as const,
|
|
71
84
|
credential: null,
|
|
72
85
|
tool: null,
|
|
73
86
|
sharedByAgentsCount: 0,
|
|
74
87
|
linkedToAgent: false,
|
|
75
|
-
};
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
// Find tool record matching registry slug
|
|
79
93
|
const matchedTool = tools.find((t) => t.slug === def.slug) ?? null;
|
|
80
94
|
|
|
81
|
-
// Find
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
// Find ALL credentials for this integration
|
|
96
|
+
// Credentials can be linked via id_tool OR id_platform_integration
|
|
97
|
+
// We match by tool slug through the tool record
|
|
98
|
+
const matchedCredentials = matchedTool
|
|
99
|
+
? credentials.filter((c) => c.id_tool === matchedTool.id)
|
|
100
|
+
: [];
|
|
101
|
+
|
|
102
|
+
// Also check credentials linked via platform_integration slug
|
|
103
|
+
// (platform_integrations use the same slug convention)
|
|
104
|
+
const piCredentials = credentials.filter(
|
|
105
|
+
(c) =>
|
|
106
|
+
c.id_platform_integration != null &&
|
|
107
|
+
!c.id_tool &&
|
|
108
|
+
// We can't directly match slug here since we don't have
|
|
109
|
+
// platform_integrations data, but credentials with
|
|
110
|
+
// id_platform_integration are for calendar integrations
|
|
111
|
+
// which match by the registry slug
|
|
112
|
+
matchedTool == null,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Combine — prefer tool-based credentials, fallback to PI-based
|
|
116
|
+
const allCredentials =
|
|
117
|
+
matchedCredentials.length > 0 ? matchedCredentials : piCredentials;
|
|
85
118
|
|
|
86
119
|
// Check if this agent has a linked agent_tool for this tool
|
|
87
120
|
const linkedToAgent = matchedTool
|
|
88
121
|
? agentTools.some((at) => at.id_tool === matchedTool.id)
|
|
89
122
|
: false;
|
|
90
123
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
// Create one card per existing credential
|
|
125
|
+
for (const cred of allCredentials) {
|
|
126
|
+
const state: IntegrationCardState =
|
|
127
|
+
cred.status === "expired" ? "expired" : "connected";
|
|
94
128
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
// Derive account label from external_reference or label
|
|
130
|
+
const accountLabel =
|
|
131
|
+
cred.external_reference || cred.label || undefined;
|
|
132
|
+
|
|
133
|
+
result.push({
|
|
134
|
+
definition: def,
|
|
135
|
+
state,
|
|
136
|
+
credential: cred,
|
|
137
|
+
tool: matchedTool,
|
|
138
|
+
sharedByAgentsCount: 1,
|
|
139
|
+
linkedToAgent,
|
|
140
|
+
credentialId: cred.id,
|
|
141
|
+
accountLabel,
|
|
142
|
+
});
|
|
100
143
|
}
|
|
101
144
|
|
|
102
|
-
|
|
145
|
+
// Always add an "add new account" card for this integration type
|
|
146
|
+
result.push({
|
|
103
147
|
definition: def,
|
|
104
|
-
state,
|
|
105
|
-
credential:
|
|
148
|
+
state: "available" as const,
|
|
149
|
+
credential: null,
|
|
106
150
|
tool: matchedTool,
|
|
107
|
-
sharedByAgentsCount,
|
|
108
|
-
linkedToAgent,
|
|
109
|
-
|
|
110
|
-
|
|
151
|
+
sharedByAgentsCount: 0,
|
|
152
|
+
linkedToAgent: false,
|
|
153
|
+
isAddNew: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result;
|
|
111
158
|
}, [credentialsData, toolsData, agentToolsData]);
|
|
112
159
|
|
|
113
160
|
return { cards, isLoading };
|
package/src/types/index.ts
CHANGED
|
@@ -100,6 +100,8 @@ export interface ToolCredential {
|
|
|
100
100
|
id: number;
|
|
101
101
|
id_account: number;
|
|
102
102
|
id_tool: number;
|
|
103
|
+
id_platform_integration: number | null;
|
|
104
|
+
external_reference: string | null;
|
|
103
105
|
label: string;
|
|
104
106
|
credentials_encrypted: string;
|
|
105
107
|
expires_at: string | null;
|