@agentprojectcontext/apx 1.60.1 → 1.61.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,275 +0,0 @@
1
- import { useState } from "react";
2
- import useSWR from "swr";
3
- import {
4
- AlertCircle, CheckCircle2, ChevronDown, ExternalLink, Eye, EyeOff, Loader2, WifiOff, X,
5
- } from "lucide-react";
6
- import { cn } from "../../lib/cn";
7
- import { Integrations, type IntegrationScope, type IntegrationStatus } from "../../lib/api";
8
- import { PluginCard } from "./PluginCard";
9
- import { PluginToolsSection, type PluginTool } from "./PluginToolsSection";
10
-
11
- function AsanaLogo({ className }: { className?: string }) {
12
- return (
13
- <svg className={className} viewBox="0 0 24 24" fill="currentColor">
14
- <path d="M18.833 9.637a4.167 4.167 0 1 1 0 8.333 4.167 4.167 0 0 1 0-8.333zm-13.666 0a4.167 4.167 0 1 1 0 8.333 4.167 4.167 0 0 1 0-8.333zM12 2a4.167 4.167 0 1 1 0 8.333A4.167 4.167 0 0 1 12 2z" />
15
- </svg>
16
- );
17
- }
18
-
19
- const ASANA_TOOLS: PluginTool[] = [
20
- { slug: "asana_list_projects", desc: "Listar proyectos del workspace" },
21
- { slug: "asana_list_tasks", desc: "Listar tareas de un proyecto" },
22
- { slug: "asana_create_task", desc: "Crear una tarea" },
23
- { slug: "asana_update_task", desc: "Actualizar estado o campos de una tarea" },
24
- ];
25
-
26
- const HELP_STEPS = [
27
- "Abrí app.asana.com/0/my-apps en el navegador.",
28
- 'Bajá hasta la sección "Personal access tokens" (no tus apps OAuth).',
29
- 'Hacé clic en "+ New access token".',
30
- "Dale un nombre y confirmá.",
31
- 'Copiá el token completo — empieza con "1/..." y tiene un ":" en el medio.',
32
- "Pegalo en el campo de abajo.",
33
- ];
34
-
35
- type Step = "idle" | "saving" | "validating" | "done";
36
-
37
- export function AsanaPlugin({ pid, scope }: { pid: string; scope: IntegrationScope }) {
38
- const [expanded, setExpanded] = useState(false);
39
- const [pat, setPat] = useState("");
40
- const [showPat, setShowPat] = useState(false);
41
- const [showHelp, setShowHelp] = useState(false);
42
- const [step, setStep] = useState<Step>("idle");
43
- const [error, setError] = useState<string | null>(null);
44
- const [workspaces, setWorkspaces] = useState<{ gid: string; name: string }[]>([]);
45
- const [selectedWorkspace, setSelectedWorkspace] = useState("");
46
-
47
- const key = `asana-status-${pid}-${scope}`;
48
- const { data: status, mutate, isLoading } = useSWR<IntegrationStatus>(
49
- key,
50
- () => Integrations.status(pid, "asana", scope),
51
- { shouldRetryOnError: false },
52
- );
53
-
54
- const isActive = status?.status === "active" && status.is_enabled;
55
- const busy = step === "saving" || step === "validating";
56
-
57
- async function handleConnect() {
58
- if (!pat.trim()) return;
59
- setStep("saving");
60
- setError(null);
61
- try {
62
- await Integrations.asanaConfigure(pid, scope, { personalAccessToken: pat.trim() });
63
- setStep("validating");
64
- const result = await Integrations.asanaValidate(pid, scope);
65
- await mutate();
66
- if (!result.workspace_gid) {
67
- const ws = await Integrations.asanaWorkspaces(pid, scope);
68
- if (ws.workspaces.length > 1) setWorkspaces(ws.workspaces);
69
- }
70
- setStep("done");
71
- setPat("");
72
- } catch (err) {
73
- setError(err instanceof Error ? err.message : "Error al conectar con Asana");
74
- setStep("idle");
75
- }
76
- }
77
-
78
- async function handleSelectWorkspace() {
79
- if (!selectedWorkspace) return;
80
- setStep("saving");
81
- setError(null);
82
- try {
83
- await Integrations.asanaConfigure(pid, scope, { workspaceGid: selectedWorkspace });
84
- await Integrations.asanaValidate(pid, scope);
85
- await mutate();
86
- setWorkspaces([]);
87
- setStep("done");
88
- } catch (err) {
89
- setError(err instanceof Error ? err.message : "Error al seleccionar workspace");
90
- setStep("idle");
91
- }
92
- }
93
-
94
- async function handleDeactivate() {
95
- setError(null);
96
- try {
97
- await Integrations.deactivate(pid, "asana", scope);
98
- await mutate();
99
- } catch (err) {
100
- setError(err instanceof Error ? err.message : "Error al desactivar");
101
- }
102
- }
103
-
104
- return (
105
- <PluginCard
106
- icon={
107
- <div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-2xl border border-rose-500/30 bg-gradient-to-br from-rose-500/20 to-pink-500/20">
108
- <AsanaLogo className="h-6 w-6 text-rose-400" />
109
- </div>
110
- }
111
- title="Asana"
112
- description="Conectá tu workspace de Asana para que los agentes creen, actualicen y consulten tareas"
113
- hasTools
114
- badges={
115
- <span
116
- className={cn(
117
- "flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px]",
118
- isActive
119
- ? "border-emerald-700 bg-emerald-900/20 text-emerald-400"
120
- : "border-border bg-muted text-muted-foreground",
121
- )}
122
- >
123
- <span className={cn("h-1.5 w-1.5 rounded-full", isActive ? "bg-emerald-400" : "bg-muted-foreground")} />
124
- {isLoading ? "..." : isActive ? "Activo" : status?.status === "error" ? "Error" : "No configurado"}
125
- </span>
126
- }
127
- rightContent={
128
- isActive && status?.workspace_name ? (
129
- <span className="max-w-[120px] truncate font-mono text-[10px] text-muted-foreground">
130
- {status.workspace_name}
131
- </span>
132
- ) : null
133
- }
134
- expanded={expanded}
135
- onToggle={() => setExpanded((v) => !v)}
136
- >
137
- <div className="space-y-4 p-4">
138
- {error && (
139
- <div className="flex items-center gap-2 rounded-lg border border-red-700/30 bg-red-900/20 px-3 py-2.5 text-xs text-red-300">
140
- <AlertCircle className="h-3.5 w-3.5 flex-shrink-0" />
141
- <span className="flex-1">{error}</span>
142
- <button onClick={() => setError(null)}><X className="h-3.5 w-3.5" /></button>
143
- </div>
144
- )}
145
-
146
- {isActive && (
147
- <div className="space-y-1 rounded-xl border border-emerald-700/30 bg-emerald-900/10 p-3">
148
- <div className="flex items-center gap-2">
149
- <CheckCircle2 className="h-3.5 w-3.5 text-emerald-400" />
150
- <span className="text-xs font-medium text-emerald-300">Conectado como {status?.user_name}</span>
151
- </div>
152
- {status?.user_email && <p className="pl-5 text-[10px] text-muted-foreground">{status.user_email}</p>}
153
- {status?.workspace_name && (
154
- <p className="pl-5 text-[10px] text-muted-foreground">Workspace: {status.workspace_name}</p>
155
- )}
156
- </div>
157
- )}
158
-
159
- {workspaces.length > 1 && (
160
- <div className="space-y-2">
161
- <p className="text-[11px] text-muted-foreground">Seleccioná el workspace a usar:</p>
162
- <div className="flex gap-2">
163
- <select
164
- value={selectedWorkspace}
165
- onChange={(e) => setSelectedWorkspace(e.target.value)}
166
- className="flex-1 rounded-lg border border-border bg-background px-2 py-1.5 text-xs outline-none focus:border-rose-500/50"
167
- >
168
- <option value="">Seleccionar workspace...</option>
169
- {workspaces.map((ws) => (
170
- <option key={ws.gid} value={ws.gid}>{ws.name}</option>
171
- ))}
172
- </select>
173
- <button
174
- onClick={handleSelectWorkspace}
175
- disabled={!selectedWorkspace || busy}
176
- className="rounded-lg border border-rose-700/50 px-3 py-1.5 text-xs text-rose-400 transition-all hover:bg-rose-900/20 disabled:cursor-not-allowed disabled:opacity-50"
177
- >
178
- {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Confirmar"}
179
- </button>
180
- </div>
181
- </div>
182
- )}
183
-
184
- {(!isActive || step === "idle") && workspaces.length === 0 && (
185
- <div className="space-y-3">
186
- <p className="mb-1 text-xs font-semibold text-foreground">Credenciales Asana</p>
187
-
188
- <div className="overflow-hidden rounded-lg border border-border">
189
- <button
190
- type="button"
191
- onClick={() => setShowHelp((v) => !v)}
192
- className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/40"
193
- >
194
- <span className="text-[11px] text-muted-foreground">
195
- ¿Cómo obtener el token? ·{" "}
196
- <a
197
- href="https://app.asana.com/0/my-apps"
198
- target="_blank"
199
- rel="noreferrer"
200
- onClick={(e) => e.stopPropagation()}
201
- className="inline-flex items-center gap-0.5 text-rose-400 hover:underline"
202
- >
203
- app.asana.com/0/my-apps <ExternalLink className="h-2.5 w-2.5" />
204
- </a>
205
- </span>
206
- <ChevronDown className={cn("h-3.5 w-3.5 flex-shrink-0 text-muted-foreground transition-transform", showHelp && "rotate-180")} />
207
- </button>
208
- {showHelp && (
209
- <div className="space-y-1.5 border-t border-border px-3 pb-3 pt-2.5">
210
- {HELP_STEPS.map((s, i) => (
211
- <div key={i} className="flex items-start gap-2">
212
- <span className="mt-0.5 flex-shrink-0 font-mono text-[10px] text-rose-400/70">{i + 1}.</span>
213
- <p className="text-[11px] text-muted-foreground">{s}</p>
214
- </div>
215
- ))}
216
- </div>
217
- )}
218
- </div>
219
-
220
- <div>
221
- <label className="mb-1 block text-[10px] text-muted-foreground">Personal Access Token</label>
222
- <div className="relative">
223
- <input
224
- type={showPat ? "text" : "password"}
225
- placeholder="1/1234567890abcdef:..."
226
- value={pat}
227
- onChange={(e) => setPat(e.target.value)}
228
- onKeyDown={(e) => e.key === "Enter" && handleConnect()}
229
- className="w-full rounded-lg border border-border bg-background px-3 py-2 pr-14 font-mono text-xs outline-none placeholder:text-muted-foreground/60 focus:border-rose-500/50"
230
- />
231
- <button
232
- type="button"
233
- onClick={() => setShowPat((v) => !v)}
234
- className="absolute right-2.5 top-1/2 flex -translate-y-1/2 items-center gap-0.5 text-[10px] text-muted-foreground transition-colors hover:text-foreground"
235
- >
236
- {showPat ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
237
- {showPat ? "Ocultar" : "Ver"}
238
- </button>
239
- </div>
240
- </div>
241
-
242
- {step === "validating" && (
243
- <div className="flex items-center gap-2 text-xs text-muted-foreground">
244
- <Loader2 className="h-3.5 w-3.5 animate-spin" /> Verificando token con Asana...
245
- </div>
246
- )}
247
-
248
- <button
249
- onClick={handleConnect}
250
- disabled={!pat.trim() || busy}
251
- className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-rose-700/50 px-3 py-2 text-xs text-rose-400 transition-all hover:bg-rose-900/20 disabled:cursor-not-allowed disabled:opacity-50"
252
- >
253
- {busy ? (
254
- <><Loader2 className="h-3.5 w-3.5 animate-spin" />{step === "saving" ? "Guardando..." : "Validando..."}</>
255
- ) : isActive ? "Reconectar" : "Conectar"}
256
- </button>
257
- </div>
258
- )}
259
-
260
- <PluginToolsSection pid={pid} tools={ASANA_TOOLS} isActive={!!isActive} />
261
-
262
- {isActive && (
263
- <div className="flex justify-end border-t border-border pt-2">
264
- <button
265
- onClick={handleDeactivate}
266
- className="flex items-center gap-1.5 rounded-lg border border-red-700/50 px-3 py-1.5 text-xs text-red-400 transition-all hover:bg-red-900/20"
267
- >
268
- <WifiOff className="h-3.5 w-3.5" /> Desactivar
269
- </button>
270
- </div>
271
- )}
272
- </div>
273
- </PluginCard>
274
- );
275
- }