@nordsym/apiclaw 1.2.2 → 1.2.3
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/AGENTS.md +50 -33
- package/README.md +22 -12
- package/SOUL.md +60 -19
- package/STATUS.md +91 -169
- package/convex/_generated/api.d.ts +6 -0
- package/convex/directCall.ts +598 -0
- package/convex/providers.ts +341 -26
- package/convex/schema.ts +87 -0
- package/convex/usage.ts +260 -0
- package/convex/waitlist.ts +55 -0
- package/data/combined-02-26.json +22102 -0
- package/data/night-expansion-02-26-06-batch2.json +1898 -0
- package/data/night-expansion-02-26-06-batch3.json +1410 -0
- package/data/night-expansion-02-26-06.json +3146 -0
- package/data/night-expansion-02-26-full.json +9726 -0
- package/data/night-expansion-02-26-v2.json +330 -0
- package/data/night-expansion-02-26.json +171 -0
- package/dist/crypto.d.ts +7 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +67 -0
- package/dist/crypto.js.map +1 -0
- package/dist/execute-dynamic.d.ts +116 -0
- package/dist/execute-dynamic.d.ts.map +1 -0
- package/dist/execute-dynamic.js +456 -0
- package/dist/execute-dynamic.js.map +1 -0
- package/dist/execute.d.ts +2 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +35 -5
- package/dist/execute.js.map +1 -1
- package/dist/index.js +33 -4
- package/dist/index.js.map +1 -1
- package/dist/registry/apis.json +2081 -3
- package/docs/PRD-customer-key-passthrough.md +184 -0
- package/landing/public/badges/available-on-apiclaw.svg +14 -0
- package/landing/scripts/generate-stats.js +75 -4
- package/landing/src/app/admin/page.tsx +1 -1
- package/landing/src/app/api/auth/magic-link/route.ts +1 -1
- package/landing/src/app/api/auth/session/route.ts +1 -1
- package/landing/src/app/api/auth/verify/route.ts +1 -1
- package/landing/src/app/api/og/route.tsx +5 -3
- package/landing/src/app/docs/page.tsx +5 -4
- package/landing/src/app/earn/page.tsx +14 -11
- package/landing/src/app/globals.css +16 -15
- package/landing/src/app/layout.tsx +2 -2
- package/landing/src/app/page.tsx +425 -254
- package/landing/src/app/providers/dashboard/[apiId]/actions/[actionId]/edit/page.tsx +600 -0
- package/landing/src/app/providers/dashboard/[apiId]/actions/new/page.tsx +583 -0
- package/landing/src/app/providers/dashboard/[apiId]/actions/page.tsx +301 -0
- package/landing/src/app/providers/dashboard/[apiId]/direct-call/page.tsx +659 -0
- package/landing/src/app/providers/dashboard/[apiId]/page.tsx +381 -0
- package/landing/src/app/providers/dashboard/[apiId]/test/page.tsx +418 -0
- package/landing/src/app/providers/dashboard/layout.tsx +292 -0
- package/landing/src/app/providers/dashboard/page.tsx +353 -290
- package/landing/src/app/providers/register/page.tsx +87 -10
- package/landing/src/components/AiClientDropdown.tsx +85 -0
- package/landing/src/components/ConfigHelperModal.tsx +113 -0
- package/landing/src/components/HeroTabs.tsx +187 -0
- package/landing/src/components/ShareIntegrationModal.tsx +198 -0
- package/landing/src/hooks/useDashboardData.ts +53 -1
- package/landing/src/lib/apis.json +46554 -174
- package/landing/src/lib/convex-client.ts +22 -3
- package/landing/src/lib/stats.json +4 -4
- package/landing/tsconfig.tsbuildinfo +1 -1
- package/night-expansion-02-26-06-batch2.py +368 -0
- package/night-expansion-02-26-06-batch3.py +299 -0
- package/night-expansion-02-26-06.py +756 -0
- package/package.json +1 -1
- package/scripts/bulk-add-public-apis-v2.py +418 -0
- package/scripts/night-expansion-02-26-v2.py +296 -0
- package/scripts/night-expansion-02-26.py +890 -0
- package/scripts/seed-complete-api.js +181 -0
- package/scripts/seed-demo-api.sh +44 -0
- package/src/crypto.ts +75 -0
- package/src/execute-dynamic.ts +589 -0
- package/src/execute.ts +41 -5
- package/src/index.ts +38 -4
- package/src/registry/apis.json +2081 -3
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { useParams, useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import {
|
|
7
|
+
ArrowLeft,
|
|
8
|
+
Save,
|
|
9
|
+
Loader2,
|
|
10
|
+
AlertCircle,
|
|
11
|
+
Plus,
|
|
12
|
+
Trash2,
|
|
13
|
+
Check,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
|
|
16
|
+
interface ActionParam {
|
|
17
|
+
name: string;
|
|
18
|
+
type: "string" | "number" | "boolean" | "object";
|
|
19
|
+
required: boolean;
|
|
20
|
+
description: string;
|
|
21
|
+
in: "body" | "query" | "path";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ResponseMapping {
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ActionFormData {
|
|
30
|
+
_id?: string;
|
|
31
|
+
name: string;
|
|
32
|
+
displayName: string;
|
|
33
|
+
description: string;
|
|
34
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
35
|
+
path: string;
|
|
36
|
+
params: ActionParam[];
|
|
37
|
+
responseMapping: ResponseMapping[];
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || 'https://adventurous-avocet-799.convex.cloud';
|
|
42
|
+
|
|
43
|
+
const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
|
|
44
|
+
const PARAM_TYPES = ["string", "number", "boolean", "object"] as const;
|
|
45
|
+
const PARAM_LOCATIONS = ["body", "query", "path"] as const;
|
|
46
|
+
|
|
47
|
+
export default function EditActionPage() {
|
|
48
|
+
const params = useParams();
|
|
49
|
+
const router = useRouter();
|
|
50
|
+
const apiId = params.apiId as string;
|
|
51
|
+
const actionId = params.actionId as string;
|
|
52
|
+
|
|
53
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
54
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
55
|
+
const [error, setError] = useState<string | null>(null);
|
|
56
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
57
|
+
const [directCallId, setDirectCallId] = useState<string | null>(null);
|
|
58
|
+
|
|
59
|
+
const [formData, setFormData] = useState<ActionFormData>({
|
|
60
|
+
name: "",
|
|
61
|
+
displayName: "",
|
|
62
|
+
description: "",
|
|
63
|
+
method: "GET",
|
|
64
|
+
path: "",
|
|
65
|
+
params: [],
|
|
66
|
+
responseMapping: [],
|
|
67
|
+
enabled: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const loadData = async () => {
|
|
72
|
+
const token = localStorage.getItem("apiclaw_session");
|
|
73
|
+
if (!token) {
|
|
74
|
+
router.push("/providers/dashboard/login");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// First get the direct call config
|
|
80
|
+
const configRes = await fetch(`${CONVEX_URL}/api/query`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
path: 'directCall:getDirectCallConfigByApiId',
|
|
85
|
+
args: { apiId }
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
const configData = await configRes.json();
|
|
89
|
+
|
|
90
|
+
if (!configData || !configData._id) {
|
|
91
|
+
setError("Direct Call not configured.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setDirectCallId(configData._id);
|
|
96
|
+
|
|
97
|
+
// Then load the action
|
|
98
|
+
const actionRes = await fetch(`${CONVEX_URL}/api/query`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
path: 'directCall:getActionById',
|
|
103
|
+
args: { actionId }
|
|
104
|
+
})
|
|
105
|
+
});
|
|
106
|
+
const actionData = await actionRes.json();
|
|
107
|
+
|
|
108
|
+
if (!actionData) {
|
|
109
|
+
setError("Action not found");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setFormData({
|
|
114
|
+
_id: actionData._id,
|
|
115
|
+
name: actionData.name || "",
|
|
116
|
+
displayName: actionData.displayName || "",
|
|
117
|
+
description: actionData.description || "",
|
|
118
|
+
method: actionData.method || "GET",
|
|
119
|
+
path: actionData.path || "",
|
|
120
|
+
params: actionData.params || [],
|
|
121
|
+
responseMapping: actionData.responseMapping || [],
|
|
122
|
+
enabled: actionData.enabled ?? true,
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error("Failed to load data:", err);
|
|
126
|
+
setError(err instanceof Error ? err.message : "Failed to load action");
|
|
127
|
+
} finally {
|
|
128
|
+
setIsLoading(false);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
loadData();
|
|
133
|
+
}, [apiId, actionId, router]);
|
|
134
|
+
|
|
135
|
+
const updateField = <K extends keyof ActionFormData>(
|
|
136
|
+
field: K,
|
|
137
|
+
value: ActionFormData[K]
|
|
138
|
+
) => {
|
|
139
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
140
|
+
setError(null);
|
|
141
|
+
setSuccess(null);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Param management
|
|
145
|
+
const addParam = () => {
|
|
146
|
+
setFormData((prev) => ({
|
|
147
|
+
...prev,
|
|
148
|
+
params: [
|
|
149
|
+
...prev.params,
|
|
150
|
+
{ name: "", type: "string", required: false, description: "", in: "body" }
|
|
151
|
+
]
|
|
152
|
+
}));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const updateParam = (index: number, field: keyof ActionParam, value: string | boolean) => {
|
|
156
|
+
setFormData((prev) => ({
|
|
157
|
+
...prev,
|
|
158
|
+
params: prev.params.map((p, i) =>
|
|
159
|
+
i === index ? { ...p, [field]: value } : p
|
|
160
|
+
)
|
|
161
|
+
}));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const removeParam = (index: number) => {
|
|
165
|
+
setFormData((prev) => ({
|
|
166
|
+
...prev,
|
|
167
|
+
params: prev.params.filter((_, i) => i !== index)
|
|
168
|
+
}));
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Response mapping management
|
|
172
|
+
const addResponseMapping = () => {
|
|
173
|
+
setFormData((prev) => ({
|
|
174
|
+
...prev,
|
|
175
|
+
responseMapping: [
|
|
176
|
+
...prev.responseMapping,
|
|
177
|
+
{ name: "", path: "" }
|
|
178
|
+
]
|
|
179
|
+
}));
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const updateResponseMapping = (index: number, field: keyof ResponseMapping, value: string) => {
|
|
183
|
+
setFormData((prev) => ({
|
|
184
|
+
...prev,
|
|
185
|
+
responseMapping: prev.responseMapping.map((r, i) =>
|
|
186
|
+
i === index ? { ...r, [field]: value } : r
|
|
187
|
+
)
|
|
188
|
+
}));
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const removeResponseMapping = (index: number) => {
|
|
192
|
+
setFormData((prev) => ({
|
|
193
|
+
...prev,
|
|
194
|
+
responseMapping: prev.responseMapping.filter((_, i) => i !== index)
|
|
195
|
+
}));
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleSave = async () => {
|
|
199
|
+
// Validation
|
|
200
|
+
if (!formData.name.trim()) {
|
|
201
|
+
setError("Action name is required");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!formData.displayName.trim()) {
|
|
205
|
+
setError("Display name is required");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!formData.path.trim()) {
|
|
209
|
+
setError("Path is required");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!directCallId) {
|
|
213
|
+
setError("Direct Call configuration not found");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Validate params have names
|
|
218
|
+
const invalidParams = formData.params.filter(p => !p.name.trim());
|
|
219
|
+
if (invalidParams.length > 0) {
|
|
220
|
+
setError("All parameters must have a name");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setIsSaving(true);
|
|
225
|
+
setError(null);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: { 'Content-Type': 'application/json' },
|
|
231
|
+
body: JSON.stringify({
|
|
232
|
+
path: 'directCall:saveAction',
|
|
233
|
+
args: {
|
|
234
|
+
directCallId,
|
|
235
|
+
action: {
|
|
236
|
+
_id: formData._id,
|
|
237
|
+
name: formData.name,
|
|
238
|
+
displayName: formData.displayName,
|
|
239
|
+
description: formData.description,
|
|
240
|
+
method: formData.method,
|
|
241
|
+
path: formData.path,
|
|
242
|
+
params: formData.params,
|
|
243
|
+
responseMapping: formData.responseMapping,
|
|
244
|
+
enabled: formData.enabled,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
setSuccess("Action updated successfully!");
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
router.push(`/providers/dashboard/${apiId}/actions`);
|
|
253
|
+
}, 1000);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error("Save failed:", err);
|
|
256
|
+
setError(err instanceof Error ? err.message : "Failed to save action");
|
|
257
|
+
} finally {
|
|
258
|
+
setIsSaving(false);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const getMethodColor = (method: string) => {
|
|
263
|
+
switch (method) {
|
|
264
|
+
case 'GET': return 'border-green-500 bg-green-500/20 text-green-500';
|
|
265
|
+
case 'POST': return 'border-blue-500 bg-blue-500/20 text-blue-500';
|
|
266
|
+
case 'PUT': return 'border-yellow-500 bg-yellow-500/20 text-yellow-600';
|
|
267
|
+
case 'PATCH': return 'border-orange-500 bg-orange-500/20 text-orange-500';
|
|
268
|
+
case 'DELETE': return 'border-red-500 bg-red-500/20 text-red-500';
|
|
269
|
+
default: return 'border-gray-500 bg-gray-500/20 text-gray-500';
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (isLoading) {
|
|
274
|
+
return (
|
|
275
|
+
<div className="flex items-center justify-center py-24">
|
|
276
|
+
<Loader2 className="w-8 h-8 text-accent animate-spin" />
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (error && !formData._id) {
|
|
282
|
+
return (
|
|
283
|
+
<div className="text-center py-24">
|
|
284
|
+
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
|
285
|
+
<h1 className="text-2xl font-bold mb-2">Error</h1>
|
|
286
|
+
<p className="text-text-muted mb-6">{error}</p>
|
|
287
|
+
<Link href={`/providers/dashboard/${apiId}/actions`} className="btn-primary">
|
|
288
|
+
Back to Actions
|
|
289
|
+
</Link>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div className="max-w-3xl space-y-8">
|
|
296
|
+
{/* Header */}
|
|
297
|
+
<div className="flex items-center gap-4">
|
|
298
|
+
<Link
|
|
299
|
+
href={`/providers/dashboard/${apiId}/actions`}
|
|
300
|
+
className="p-2 rounded-lg hover:bg-surface transition"
|
|
301
|
+
>
|
|
302
|
+
<ArrowLeft className="w-5 h-5" />
|
|
303
|
+
</Link>
|
|
304
|
+
<div>
|
|
305
|
+
<h1 className="text-2xl font-bold">Edit Action</h1>
|
|
306
|
+
<p className="text-text-muted">{formData.displayName || formData.name}</p>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{/* Basic Info */}
|
|
311
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6 space-y-4">
|
|
312
|
+
<h2 className="font-semibold">Basic Information</h2>
|
|
313
|
+
|
|
314
|
+
<div>
|
|
315
|
+
<label className="block text-sm font-medium mb-2">Display Name *</label>
|
|
316
|
+
<input
|
|
317
|
+
type="text"
|
|
318
|
+
value={formData.displayName}
|
|
319
|
+
onChange={(e) => updateField("displayName", e.target.value)}
|
|
320
|
+
placeholder="Create Invoice"
|
|
321
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
322
|
+
/>
|
|
323
|
+
<p className="mt-1 text-xs text-text-muted">Human-readable name shown to users</p>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div>
|
|
327
|
+
<label className="block text-sm font-medium mb-2">Action Name (slug) *</label>
|
|
328
|
+
<input
|
|
329
|
+
type="text"
|
|
330
|
+
value={formData.name}
|
|
331
|
+
onChange={(e) => updateField("name", e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-'))}
|
|
332
|
+
placeholder="create-invoice"
|
|
333
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition font-mono"
|
|
334
|
+
/>
|
|
335
|
+
<p className="mt-1 text-xs text-text-muted">Unique identifier used in API calls</p>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<div>
|
|
339
|
+
<label className="block text-sm font-medium mb-2">Description</label>
|
|
340
|
+
<textarea
|
|
341
|
+
value={formData.description}
|
|
342
|
+
onChange={(e) => updateField("description", e.target.value)}
|
|
343
|
+
placeholder="Creates a new invoice in the system..."
|
|
344
|
+
rows={3}
|
|
345
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition resize-none"
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<div className="flex items-center gap-3">
|
|
350
|
+
<input
|
|
351
|
+
type="checkbox"
|
|
352
|
+
id="enabled"
|
|
353
|
+
checked={formData.enabled}
|
|
354
|
+
onChange={(e) => updateField("enabled", e.target.checked)}
|
|
355
|
+
className="w-5 h-5 rounded border-border text-accent focus:ring-accent"
|
|
356
|
+
/>
|
|
357
|
+
<label htmlFor="enabled" className="text-sm font-medium">
|
|
358
|
+
Enabled (agents can call this action)
|
|
359
|
+
</label>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
{/* Endpoint */}
|
|
364
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6 space-y-4">
|
|
365
|
+
<h2 className="font-semibold">Endpoint</h2>
|
|
366
|
+
|
|
367
|
+
<div>
|
|
368
|
+
<label className="block text-sm font-medium mb-2">HTTP Method *</label>
|
|
369
|
+
<div className="flex flex-wrap gap-2">
|
|
370
|
+
{METHODS.map((method) => (
|
|
371
|
+
<button
|
|
372
|
+
key={method}
|
|
373
|
+
onClick={() => updateField("method", method)}
|
|
374
|
+
className={`px-4 py-2 rounded-lg border transition font-bold text-sm ${
|
|
375
|
+
formData.method === method
|
|
376
|
+
? getMethodColor(method)
|
|
377
|
+
: 'border-border hover:border-accent/50'
|
|
378
|
+
}`}
|
|
379
|
+
>
|
|
380
|
+
{method}
|
|
381
|
+
</button>
|
|
382
|
+
))}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div>
|
|
387
|
+
<label className="block text-sm font-medium mb-2">Path *</label>
|
|
388
|
+
<input
|
|
389
|
+
type="text"
|
|
390
|
+
value={formData.path}
|
|
391
|
+
onChange={(e) => updateField("path", e.target.value)}
|
|
392
|
+
placeholder="/invoices or /invoices/{id}"
|
|
393
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition font-mono"
|
|
394
|
+
/>
|
|
395
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
396
|
+
Use {"{param}"} for path parameters, e.g. /users/{"{id}"}/orders
|
|
397
|
+
</p>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
{/* Parameters */}
|
|
402
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6 space-y-4">
|
|
403
|
+
<div className="flex items-center justify-between">
|
|
404
|
+
<h2 className="font-semibold">Parameters</h2>
|
|
405
|
+
<button
|
|
406
|
+
onClick={addParam}
|
|
407
|
+
className="btn-secondary !py-2 !px-3 text-sm"
|
|
408
|
+
>
|
|
409
|
+
<Plus className="w-4 h-4" />
|
|
410
|
+
Add Parameter
|
|
411
|
+
</button>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
{formData.params.length === 0 ? (
|
|
415
|
+
<p className="text-sm text-text-muted py-4 text-center">
|
|
416
|
+
No parameters defined. Click "Add Parameter" to add one.
|
|
417
|
+
</p>
|
|
418
|
+
) : (
|
|
419
|
+
<div className="space-y-4">
|
|
420
|
+
{formData.params.map((param, index) => (
|
|
421
|
+
<div
|
|
422
|
+
key={index}
|
|
423
|
+
className="p-4 rounded-xl bg-surface border border-border space-y-3"
|
|
424
|
+
>
|
|
425
|
+
<div className="flex items-start justify-between gap-4">
|
|
426
|
+
<div className="flex-1 grid grid-cols-2 gap-3">
|
|
427
|
+
<div>
|
|
428
|
+
<label className="block text-xs font-medium mb-1">Name *</label>
|
|
429
|
+
<input
|
|
430
|
+
type="text"
|
|
431
|
+
value={param.name}
|
|
432
|
+
onChange={(e) => updateParam(index, "name", e.target.value)}
|
|
433
|
+
placeholder="customer_id"
|
|
434
|
+
className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm font-mono"
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
<div>
|
|
438
|
+
<label className="block text-xs font-medium mb-1">Type</label>
|
|
439
|
+
<select
|
|
440
|
+
value={param.type}
|
|
441
|
+
onChange={(e) => updateParam(index, "type", e.target.value)}
|
|
442
|
+
className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm"
|
|
443
|
+
>
|
|
444
|
+
{PARAM_TYPES.map((type) => (
|
|
445
|
+
<option key={type} value={type}>{type}</option>
|
|
446
|
+
))}
|
|
447
|
+
</select>
|
|
448
|
+
</div>
|
|
449
|
+
<div>
|
|
450
|
+
<label className="block text-xs font-medium mb-1">Location</label>
|
|
451
|
+
<select
|
|
452
|
+
value={param.in}
|
|
453
|
+
onChange={(e) => updateParam(index, "in", e.target.value)}
|
|
454
|
+
className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm"
|
|
455
|
+
>
|
|
456
|
+
{PARAM_LOCATIONS.map((loc) => (
|
|
457
|
+
<option key={loc} value={loc}>{loc}</option>
|
|
458
|
+
))}
|
|
459
|
+
</select>
|
|
460
|
+
</div>
|
|
461
|
+
<div className="flex items-end pb-1">
|
|
462
|
+
<label className="flex items-center gap-2 text-sm">
|
|
463
|
+
<input
|
|
464
|
+
type="checkbox"
|
|
465
|
+
checked={param.required}
|
|
466
|
+
onChange={(e) => updateParam(index, "required", e.target.checked)}
|
|
467
|
+
className="w-4 h-4 rounded border-border text-accent focus:ring-accent"
|
|
468
|
+
/>
|
|
469
|
+
Required
|
|
470
|
+
</label>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
<button
|
|
474
|
+
onClick={() => removeParam(index)}
|
|
475
|
+
className="p-2 rounded-lg hover:bg-red-500/20 text-text-muted hover:text-red-500 transition mt-5"
|
|
476
|
+
>
|
|
477
|
+
<Trash2 className="w-4 h-4" />
|
|
478
|
+
</button>
|
|
479
|
+
</div>
|
|
480
|
+
<div>
|
|
481
|
+
<label className="block text-xs font-medium mb-1">Description</label>
|
|
482
|
+
<input
|
|
483
|
+
type="text"
|
|
484
|
+
value={param.description}
|
|
485
|
+
onChange={(e) => updateParam(index, "description", e.target.value)}
|
|
486
|
+
placeholder="The unique identifier of the customer"
|
|
487
|
+
className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm"
|
|
488
|
+
/>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
))}
|
|
492
|
+
</div>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
{/* Response Mapping */}
|
|
497
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6 space-y-4">
|
|
498
|
+
<div className="flex items-center justify-between">
|
|
499
|
+
<div>
|
|
500
|
+
<h2 className="font-semibold">Response Mapping</h2>
|
|
501
|
+
<p className="text-xs text-text-muted mt-1">Extract specific fields from the API response</p>
|
|
502
|
+
</div>
|
|
503
|
+
<button
|
|
504
|
+
onClick={addResponseMapping}
|
|
505
|
+
className="btn-secondary !py-2 !px-3 text-sm"
|
|
506
|
+
>
|
|
507
|
+
<Plus className="w-4 h-4" />
|
|
508
|
+
Add Mapping
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
{formData.responseMapping.length === 0 ? (
|
|
513
|
+
<p className="text-sm text-text-muted py-4 text-center">
|
|
514
|
+
No response mappings defined. The full response will be returned.
|
|
515
|
+
</p>
|
|
516
|
+
) : (
|
|
517
|
+
<div className="space-y-3">
|
|
518
|
+
{formData.responseMapping.map((mapping, index) => (
|
|
519
|
+
<div
|
|
520
|
+
key={index}
|
|
521
|
+
className="flex items-center gap-3 p-3 rounded-xl bg-surface border border-border"
|
|
522
|
+
>
|
|
523
|
+
<div className="flex-1 grid grid-cols-2 gap-3">
|
|
524
|
+
<div>
|
|
525
|
+
<label className="block text-xs font-medium mb-1">Field Name</label>
|
|
526
|
+
<input
|
|
527
|
+
type="text"
|
|
528
|
+
value={mapping.name}
|
|
529
|
+
onChange={(e) => updateResponseMapping(index, "name", e.target.value)}
|
|
530
|
+
placeholder="invoiceId"
|
|
531
|
+
className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm font-mono"
|
|
532
|
+
/>
|
|
533
|
+
</div>
|
|
534
|
+
<div>
|
|
535
|
+
<label className="block text-xs font-medium mb-1">JSONPath</label>
|
|
536
|
+
<input
|
|
537
|
+
type="text"
|
|
538
|
+
value={mapping.path}
|
|
539
|
+
onChange={(e) => updateResponseMapping(index, "path", e.target.value)}
|
|
540
|
+
placeholder="$.data.id"
|
|
541
|
+
className="w-full px-3 py-2 rounded-lg bg-surface-elevated border border-border focus:border-accent focus:outline-none transition text-sm font-mono"
|
|
542
|
+
/>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
<button
|
|
546
|
+
onClick={() => removeResponseMapping(index)}
|
|
547
|
+
className="p-2 rounded-lg hover:bg-red-500/20 text-text-muted hover:text-red-500 transition"
|
|
548
|
+
>
|
|
549
|
+
<Trash2 className="w-4 h-4" />
|
|
550
|
+
</button>
|
|
551
|
+
</div>
|
|
552
|
+
))}
|
|
553
|
+
</div>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
{/* Error/Success Messages */}
|
|
558
|
+
{error && (
|
|
559
|
+
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/30 flex items-center gap-3 text-red-500">
|
|
560
|
+
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
|
561
|
+
<span className="text-sm">{error}</span>
|
|
562
|
+
</div>
|
|
563
|
+
)}
|
|
564
|
+
|
|
565
|
+
{success && (
|
|
566
|
+
<div className="p-4 rounded-xl bg-green-500/10 border border-green-500/30 flex items-center gap-3 text-green-500">
|
|
567
|
+
<Check className="w-5 h-5 flex-shrink-0" />
|
|
568
|
+
<span className="text-sm">{success}</span>
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
571
|
+
|
|
572
|
+
{/* Save Button */}
|
|
573
|
+
<div className="flex items-center justify-between pt-4 border-t border-border">
|
|
574
|
+
<Link
|
|
575
|
+
href={`/providers/dashboard/${apiId}/actions`}
|
|
576
|
+
className="btn-secondary !py-2.5"
|
|
577
|
+
>
|
|
578
|
+
Cancel
|
|
579
|
+
</Link>
|
|
580
|
+
<button
|
|
581
|
+
onClick={handleSave}
|
|
582
|
+
disabled={isSaving}
|
|
583
|
+
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
|
584
|
+
>
|
|
585
|
+
{isSaving ? (
|
|
586
|
+
<>
|
|
587
|
+
<Loader2 className="w-5 h-5 animate-spin" />
|
|
588
|
+
Saving...
|
|
589
|
+
</>
|
|
590
|
+
) : (
|
|
591
|
+
<>
|
|
592
|
+
<Save className="w-5 h-5" />
|
|
593
|
+
Save Changes
|
|
594
|
+
</>
|
|
595
|
+
)}
|
|
596
|
+
</button>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
);
|
|
600
|
+
}
|