@greatapps/greatagents-ui 0.3.9 → 0.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/greatagents-ui",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Shared agents UI components for Great platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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(state: IntegrationCardState): string {
62
- switch (state) {
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 + description */}
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
- <p className="text-xs text-muted-foreground leading-relaxed">
124
- {definition.description}
125
- </p>
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
- const response = await fetch(
229
- `${gagentsApiUrl}/v1/${language}/${idWl}/accounts/${accountId}/oauth/authorize/${integration.slug}`,
230
- { headers: { Authorization: `Bearer ${token}` } },
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="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
52
- {cards.map((card) => (
53
- <IntegrationCard
54
- key={card.definition.slug}
55
- card={card}
56
- onConnect={onConnect}
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
- return INTEGRATIONS_REGISTRY.map((def) => {
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
- return {
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 credential for that tool
82
- const matchedCredential = matchedTool
83
- ? credentials.find((c) => c.id_tool === matchedTool.id) ?? null
84
- : null;
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
- // Sharing indicator: credential exists at account level (available to all agents)
92
- // When a credential is account-scoped, any agent can use it — show as "shared"
93
- const sharedByAgentsCount = matchedCredential ? 1 : 0;
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
- // Determine state
96
- let state: IntegrationCardState = "available";
97
- if (matchedCredential) {
98
- state =
99
- matchedCredential.status === "expired" ? "expired" : "connected";
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
- return {
145
+ // Always add an "add new account" card for this integration type
146
+ result.push({
103
147
  definition: def,
104
- state,
105
- credential: matchedCredential,
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 };
@@ -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;