@dev-fastn-ai/react-core 1.0.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.
- package/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +1710 -0
- package/dist/core/src/core/activate-connector.d.ts +42 -0
- package/dist/core/src/core/config.d.ts +3 -0
- package/dist/core/src/core/configuration-form.d.ts +27 -0
- package/dist/core/src/core/configurations.d.ts +2 -0
- package/dist/core/src/core/connectors.d.ts +7 -0
- package/dist/core/src/core/execute-flow.d.ts +9 -0
- package/dist/core/src/core/register-refetch-functions.d.ts +4 -0
- package/dist/core/src/index.d.ts +11 -0
- package/dist/core/src/services/api-hooks.d.ts +35 -0
- package/dist/core/src/services/apollo.d.ts +4 -0
- package/dist/core/src/types/config.d.ts +9 -0
- package/dist/core/src/types/index.d.ts +269 -0
- package/dist/core/src/utils/constants.d.ts +12 -0
- package/dist/core/src/utils/errors.d.ts +22 -0
- package/dist/core/src/utils/google-files-picker.d.ts +13 -0
- package/dist/core/src/utils/misc.d.ts +35 -0
- package/dist/index.cjs.js +18271 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.esm.js +18259 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/react-core/src/core/provider.d.ts +9 -0
- package/dist/react-core/src/core/use-configuration-form.d.ts +2 -0
- package/dist/react-core/src/core/use-configurations.d.ts +2 -0
- package/dist/react-core/src/core/use-connectors.d.ts +1 -0
- package/dist/react-core/src/core/use-field-options.d.ts +36 -0
- package/dist/react-core/src/index.d.ts +7 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,1710 @@
|
|
|
1
|
+
<!-- # @fastn-ai/react-core
|
|
2
|
+
|
|
3
|
+
A powerful React library for seamlessly integrating the Fastn AI connector marketplace into your applications. This package provides a complete set of hooks and components to manage connectors, configurations, and dynamic forms with full TypeScript support.
|
|
4
|
+
|
|
5
|
+
## 🚀 Features
|
|
6
|
+
|
|
7
|
+
- **🔌 Connector Management**: Fetch, display, and manage marketplace connectors
|
|
8
|
+
- **⚙️ Configuration Handling**: Create, update, and manage connector configurations
|
|
9
|
+
- **📝 Dynamic Forms**: Render configuration forms with async field options
|
|
10
|
+
- **🔄 Real-time Updates**: Built-in caching and background refetching
|
|
11
|
+
- **🎨 Customizable**: Full styling control and component customization
|
|
12
|
+
- **📱 TypeScript First**: Complete type safety and IntelliSense support
|
|
13
|
+
- **⚡ Performance Optimized**: React Query powered with intelligent caching
|
|
14
|
+
|
|
15
|
+
## 📦 Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @fastn-ai/react-core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Peer Dependencies
|
|
22
|
+
|
|
23
|
+
This package requires React 18+ and React Query. Make sure you have these installed:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install react react-dom @tanstack/react-query
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🏗️ Quick Start
|
|
30
|
+
|
|
31
|
+
### 1. Setup the Provider
|
|
32
|
+
|
|
33
|
+
First, wrap your application with the `FastnProvider` and provide your configuration:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { FastnProvider } from '@fastn-ai/react-core';
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
const fastnConfig = {
|
|
40
|
+
baseUrl: 'https://api.fastn.ai', // Optional: defaults to production
|
|
41
|
+
environment: 'LIVE', // 'LIVE' | 'DRAFT' | custom string
|
|
42
|
+
authToken: 'your-auth-token',
|
|
43
|
+
tenantId: 'your-tenant-id',
|
|
44
|
+
spaceId: 'your-space-id',
|
|
45
|
+
customAuth: false, // Optional: for custom authentication
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<FastnProvider config={fastnConfig}>
|
|
50
|
+
<YourApp />
|
|
51
|
+
</FastnProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Use the Hooks
|
|
57
|
+
|
|
58
|
+
Now you can use any of the provided hooks in your components:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { useConnectors, useConfigurations } from '@fastn-ai/react-core';
|
|
62
|
+
|
|
63
|
+
function MyComponent() {
|
|
64
|
+
const { data: connectors, isLoading, error } = useConnectors();
|
|
65
|
+
|
|
66
|
+
if (isLoading) return <div>Loading connectors...</div>;
|
|
67
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div>
|
|
71
|
+
{connectors?.map(connector => (
|
|
72
|
+
<ConnectorCard key={connector.id} connector={connector} />
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 📚 Detailed API Reference
|
|
80
|
+
|
|
81
|
+
### FastnProvider
|
|
82
|
+
|
|
83
|
+
The main provider component that initializes the Fastn client and provides React Query context.
|
|
84
|
+
|
|
85
|
+
#### Props
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
interface FastnProviderProps {
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
config: Required<FastnConfig>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface FastnConfig {
|
|
94
|
+
baseUrl?: string;
|
|
95
|
+
environment?: FastnEnvironment;
|
|
96
|
+
authToken: string;
|
|
97
|
+
tenantId: string;
|
|
98
|
+
spaceId: string;
|
|
99
|
+
customAuth?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type FastnEnvironment = 'LIVE' | 'DRAFT' | string;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Example
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { FastnProvider } from '@fastn-ai/react-core';
|
|
109
|
+
|
|
110
|
+
function App() {
|
|
111
|
+
const config = {
|
|
112
|
+
baseUrl: 'https://api.fastn.ai',
|
|
113
|
+
environment: 'LIVE',
|
|
114
|
+
authToken: process.env.REACT_APP_FASTN_AUTH_TOKEN!,
|
|
115
|
+
tenantId: process.env.REACT_APP_FASTN_TENANT_ID!,
|
|
116
|
+
spaceId: process.env.REACT_APP_FASTN_SPACE_ID!,
|
|
117
|
+
customAuth: false,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<FastnProvider config={config}>
|
|
122
|
+
<Router>
|
|
123
|
+
<Routes>
|
|
124
|
+
<Route path="/connectors" element={<ConnectorsPage />} />
|
|
125
|
+
<Route path="/configurations" element={<ConfigurationsPage />} />
|
|
126
|
+
</Routes>
|
|
127
|
+
</Router>
|
|
128
|
+
</FastnProvider>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### useConnectors
|
|
134
|
+
|
|
135
|
+
Fetches and manages the list of available connectors from the marketplace.
|
|
136
|
+
|
|
137
|
+
#### Usage
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { useConnectors } from '@fastn-ai/react-core';
|
|
141
|
+
|
|
142
|
+
function ConnectorsList() {
|
|
143
|
+
const {
|
|
144
|
+
data: connectors,
|
|
145
|
+
isLoading,
|
|
146
|
+
error,
|
|
147
|
+
refetch
|
|
148
|
+
} = useConnectors();
|
|
149
|
+
|
|
150
|
+
if (isLoading) return <ConnectorsSkeleton />;
|
|
151
|
+
if (error) return <ErrorMessage error={error} />;
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
155
|
+
{connectors?.map(connector => (
|
|
156
|
+
<ConnectorCard
|
|
157
|
+
key={connector.id}
|
|
158
|
+
connector={connector}
|
|
159
|
+
onRefresh={refetch}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Return Value
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
interface UseConnectorsReturn {
|
|
171
|
+
data: Connector[] | undefined;
|
|
172
|
+
isLoading: boolean;
|
|
173
|
+
isError: boolean;
|
|
174
|
+
error: Error | null;
|
|
175
|
+
refetch: () => Promise<any>;
|
|
176
|
+
isFetching: boolean;
|
|
177
|
+
isSuccess: boolean;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Connector Type
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
interface Connector {
|
|
185
|
+
readonly id: string;
|
|
186
|
+
readonly name: string;
|
|
187
|
+
readonly description: string;
|
|
188
|
+
readonly imageUri?: string;
|
|
189
|
+
readonly status: ConnectorStatus;
|
|
190
|
+
readonly actions: readonly ConnectorAction[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
enum ConnectorStatus {
|
|
194
|
+
ACTIVE = "ACTIVE",
|
|
195
|
+
INACTIVE = "INACTIVE",
|
|
196
|
+
ALL = "ALL",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface ConnectorAction {
|
|
200
|
+
readonly name: string;
|
|
201
|
+
readonly actionType: ConnectorActionType | string;
|
|
202
|
+
readonly form?: ConnectorForm | null;
|
|
203
|
+
readonly onClick?: (data?: unknown) => Promise<unknown>;
|
|
204
|
+
readonly onSubmit?: (formData: Record<string, unknown>) => Promise<unknown>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
enum ConnectorActionType {
|
|
208
|
+
ACTIVATION = "ACTIVATION",
|
|
209
|
+
DEACTIVATION = "DEACTIVATION",
|
|
210
|
+
NONE = "NONE",
|
|
211
|
+
ENABLE = "ENABLE",
|
|
212
|
+
DISABLE = "DISABLE",
|
|
213
|
+
DELETE = "DELETE",
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### useConfigurations
|
|
218
|
+
|
|
219
|
+
Fetches and manages connector configurations for a specific configuration ID.
|
|
220
|
+
|
|
221
|
+
#### Usage
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { useConfigurations } from '@fastn-ai/react-core';
|
|
225
|
+
|
|
226
|
+
function ConfigurationsList({ configurationId }: { configurationId: string }) {
|
|
227
|
+
const {
|
|
228
|
+
data: configurations,
|
|
229
|
+
isLoading,
|
|
230
|
+
error
|
|
231
|
+
} = useConfigurations({
|
|
232
|
+
configurationId,
|
|
233
|
+
status: 'ALL' // 'ENABLED' | 'DISABLED' | 'ALL' | 'IDLE'
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (isLoading) return <ConfigurationsSkeleton />;
|
|
237
|
+
if (error) return <ErrorMessage error={error} />;
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="space-y-4">
|
|
241
|
+
{configurations?.map(configuration => (
|
|
242
|
+
<ConfigurationCard
|
|
243
|
+
key={configuration.id}
|
|
244
|
+
configuration={configuration}
|
|
245
|
+
/>
|
|
246
|
+
))}
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Parameters
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
interface GetConfigurationsInput {
|
|
256
|
+
readonly configurationId: string;
|
|
257
|
+
readonly status?: "ENABLED" | "DISABLED" | "ALL" | "IDLE";
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Return Value
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
interface UseConfigurationsReturn {
|
|
265
|
+
data: Configuration[] | undefined;
|
|
266
|
+
isLoading: boolean;
|
|
267
|
+
isError: boolean;
|
|
268
|
+
error: Error | null;
|
|
269
|
+
refetch: () => Promise<any>;
|
|
270
|
+
isFetching: boolean;
|
|
271
|
+
isSuccess: boolean;
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### Configuration Type
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
interface Configuration {
|
|
279
|
+
readonly id: string;
|
|
280
|
+
readonly connectorId: string;
|
|
281
|
+
readonly configurationId: string;
|
|
282
|
+
readonly name: string;
|
|
283
|
+
readonly flowId: string;
|
|
284
|
+
readonly description: string;
|
|
285
|
+
readonly imageUri: string;
|
|
286
|
+
readonly status: string;
|
|
287
|
+
readonly actions: readonly ConfigurationAction[];
|
|
288
|
+
readonly metadata?: unknown;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
interface ConfigurationAction {
|
|
292
|
+
readonly name: string;
|
|
293
|
+
readonly actionType: ConfigurationActionType;
|
|
294
|
+
readonly onClick?: () => Promise<void>;
|
|
295
|
+
readonly form?: ConnectorForm | null;
|
|
296
|
+
readonly onSubmit?: (formData: unknown) => Promise<void>;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
type ConfigurationActionType = ConnectorActionType.ENABLE | ConnectorActionType.DISABLE | ConnectorActionType.DELETE;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### useConfigurationForm
|
|
303
|
+
|
|
304
|
+
Fetches and manages configuration forms for setting up connectors.
|
|
305
|
+
|
|
306
|
+
#### Usage
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { useConfigurationForm } from '@fastn-ai/react-core';
|
|
310
|
+
|
|
311
|
+
function ConfigurationForm({
|
|
312
|
+
configurationId,
|
|
313
|
+
connectorId,
|
|
314
|
+
configuration
|
|
315
|
+
}: {
|
|
316
|
+
configurationId: string;
|
|
317
|
+
connectorId: string;
|
|
318
|
+
configuration: Configuration;
|
|
319
|
+
}) {
|
|
320
|
+
const {
|
|
321
|
+
data: form,
|
|
322
|
+
isLoading,
|
|
323
|
+
error
|
|
324
|
+
} = useConfigurationForm({
|
|
325
|
+
configurationId,
|
|
326
|
+
connectorId,
|
|
327
|
+
configuration
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (isLoading) return <FormSkeleton />;
|
|
331
|
+
if (error) return <ErrorMessage error={error} />;
|
|
332
|
+
if (!form) return <div>No form available</div>;
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<ConfigurationFormRenderer
|
|
336
|
+
form={form}
|
|
337
|
+
onSubmit={handleSubmit}
|
|
338
|
+
onCancel={handleCancel}
|
|
339
|
+
/>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### Parameters
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
interface GetConfigurationFormInput {
|
|
348
|
+
readonly configurationId: string;
|
|
349
|
+
readonly connectorId: string;
|
|
350
|
+
readonly configuration: Configuration;
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### Return Value
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
interface UseConfigurationFormReturn {
|
|
358
|
+
data: ConfigurationForm | undefined;
|
|
359
|
+
isLoading: boolean;
|
|
360
|
+
isError: boolean;
|
|
361
|
+
error: Error | null;
|
|
362
|
+
refetch: () => Promise<any>;
|
|
363
|
+
isFetching: boolean;
|
|
364
|
+
isSuccess: boolean;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### ConfigurationForm Type
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
interface ConfigurationForm {
|
|
372
|
+
readonly name: string;
|
|
373
|
+
readonly description: string;
|
|
374
|
+
readonly imageUri: string;
|
|
375
|
+
readonly fields: readonly ConnectorField[];
|
|
376
|
+
readonly submitButtonLabel?: string;
|
|
377
|
+
readonly loading?: boolean;
|
|
378
|
+
readonly error?: string;
|
|
379
|
+
readonly submitHandler?: (args: { formData: FormData }) => Promise<void>;
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### useFieldOptions
|
|
384
|
+
|
|
385
|
+
Manages async select field options with search, pagination, and caching.
|
|
386
|
+
|
|
387
|
+
#### Usage
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
import { useFieldOptions } from '@fastn-ai/react-core';
|
|
391
|
+
|
|
392
|
+
function AsyncSelectField({ field }: { field: ConnectorField }) {
|
|
393
|
+
const {
|
|
394
|
+
options,
|
|
395
|
+
loading,
|
|
396
|
+
loadingMore,
|
|
397
|
+
hasNext,
|
|
398
|
+
query,
|
|
399
|
+
refresh,
|
|
400
|
+
search,
|
|
401
|
+
loadMore,
|
|
402
|
+
error
|
|
403
|
+
} = useFieldOptions(field);
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<div>
|
|
407
|
+
<AsyncSelect
|
|
408
|
+
options={options}
|
|
409
|
+
isLoading={loading}
|
|
410
|
+
onInputChange={search}
|
|
411
|
+
onMenuScrollToBottom={loadMore}
|
|
412
|
+
loadMore={loadingMore}
|
|
413
|
+
hasMore={hasNext}
|
|
414
|
+
onRefresh={refresh}
|
|
415
|
+
placeholder={field.placeholder}
|
|
416
|
+
isClearable
|
|
417
|
+
isSearchable
|
|
418
|
+
/>
|
|
419
|
+
{error && <ErrorMessage error={error} />}
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
#### Return Value
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
interface UseFieldOptionsReturn {
|
|
429
|
+
options: SelectOption[];
|
|
430
|
+
loading: boolean;
|
|
431
|
+
loadingMore: boolean;
|
|
432
|
+
hasNext: boolean;
|
|
433
|
+
query: string;
|
|
434
|
+
refresh: () => Promise<void>;
|
|
435
|
+
search: (query: string) => void;
|
|
436
|
+
loadMore: () => Promise<void>;
|
|
437
|
+
totalLoadedOptions: number;
|
|
438
|
+
error: string | null;
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
#### SelectOption Type
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
interface SelectOption {
|
|
446
|
+
readonly label: string;
|
|
447
|
+
readonly value: string;
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## 🎨 Component Examples
|
|
452
|
+
|
|
453
|
+
### Connector Card Component
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
import { useConnectors } from '@fastn-ai/react-core';
|
|
457
|
+
import type { Connector } from '@fastn-ai/react-core';
|
|
458
|
+
|
|
459
|
+
interface ConnectorCardProps {
|
|
460
|
+
connector: Connector;
|
|
461
|
+
onRefresh?: () => void;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function ConnectorCard({ connector, onRefresh }: ConnectorCardProps) {
|
|
465
|
+
const handleAction = async (action: ConnectorAction) => {
|
|
466
|
+
try {
|
|
467
|
+
if (action.onClick) {
|
|
468
|
+
await action.onClick();
|
|
469
|
+
onRefresh?.();
|
|
470
|
+
}
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error('Action failed:', error);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<div className="border rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow">
|
|
478
|
+
<div className="flex items-center space-x-4">
|
|
479
|
+
{connector.imageUri && (
|
|
480
|
+
<img
|
|
481
|
+
src={connector.imageUri}
|
|
482
|
+
alt={connector.name}
|
|
483
|
+
className="w-12 h-12 rounded-lg object-cover"
|
|
484
|
+
/>
|
|
485
|
+
)}
|
|
486
|
+
<div className="flex-1">
|
|
487
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
488
|
+
{connector.name}
|
|
489
|
+
</h3>
|
|
490
|
+
<p className="text-sm text-gray-600">
|
|
491
|
+
{connector.description}
|
|
492
|
+
</p>
|
|
493
|
+
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
|
494
|
+
connector.status === 'ACTIVE'
|
|
495
|
+
? 'bg-green-100 text-green-800'
|
|
496
|
+
: 'bg-gray-100 text-gray-800'
|
|
497
|
+
}`}>
|
|
498
|
+
{connector.status}
|
|
499
|
+
</span>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<div className="mt-4 flex space-x-2">
|
|
504
|
+
{connector.actions.map((action, index) => (
|
|
505
|
+
<button
|
|
506
|
+
key={index}
|
|
507
|
+
onClick={() => handleAction(action)}
|
|
508
|
+
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
|
509
|
+
action.actionType === 'ACTIVATION' || action.actionType === 'ENABLE'
|
|
510
|
+
? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
511
|
+
: action.actionType === 'DEACTIVATION' || action.actionType === 'DISABLE'
|
|
512
|
+
? 'bg-red-600 text-white hover:bg-red-700'
|
|
513
|
+
: 'bg-gray-600 text-white hover:bg-gray-700'
|
|
514
|
+
}`}
|
|
515
|
+
>
|
|
516
|
+
{action.name}
|
|
517
|
+
</button>
|
|
518
|
+
))}
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Configuration Card Component
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
import { useConfigurations } from '@fastn-ai/react-core';
|
|
529
|
+
import type { Configuration } from '@fastn-ai/react-core';
|
|
530
|
+
|
|
531
|
+
interface ConfigurationCardProps {
|
|
532
|
+
configuration: Configuration;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function ConfigurationCard({ configuration }: ConfigurationCardProps) {
|
|
536
|
+
const handleAction = async (action: ConfigurationAction) => {
|
|
537
|
+
try {
|
|
538
|
+
if (action.onClick) {
|
|
539
|
+
await action.onClick();
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.error('Configuration action failed:', error);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
return (
|
|
547
|
+
<div className="border rounded-lg p-6 shadow-sm">
|
|
548
|
+
<div className="flex items-center space-x-4">
|
|
549
|
+
<img
|
|
550
|
+
src={configuration.imageUri}
|
|
551
|
+
alt={configuration.name}
|
|
552
|
+
className="w-12 h-12 rounded-lg object-cover"
|
|
553
|
+
/>
|
|
554
|
+
<div className="flex-1">
|
|
555
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
556
|
+
{configuration.name}
|
|
557
|
+
</h3>
|
|
558
|
+
<p className="text-sm text-gray-600">
|
|
559
|
+
{configuration.description}
|
|
560
|
+
</p>
|
|
561
|
+
<div className="flex items-center space-x-2 mt-2">
|
|
562
|
+
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
|
563
|
+
configuration.status === 'ENABLED'
|
|
564
|
+
? 'bg-green-100 text-green-800'
|
|
565
|
+
: configuration.status === 'DISABLED'
|
|
566
|
+
? 'bg-red-100 text-red-800'
|
|
567
|
+
: 'bg-yellow-100 text-yellow-800'
|
|
568
|
+
}`}>
|
|
569
|
+
{configuration.status}
|
|
570
|
+
</span>
|
|
571
|
+
<span className="text-xs text-gray-500">
|
|
572
|
+
ID: {configuration.id}
|
|
573
|
+
</span>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div className="mt-4 flex space-x-2">
|
|
579
|
+
{configuration.actions.map((action, index) => (
|
|
580
|
+
<button
|
|
581
|
+
key={index}
|
|
582
|
+
onClick={() => handleAction(action)}
|
|
583
|
+
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
|
584
|
+
action.actionType === 'ENABLE'
|
|
585
|
+
? 'bg-green-600 text-white hover:bg-green-700'
|
|
586
|
+
: action.actionType === 'DISABLE'
|
|
587
|
+
? 'bg-yellow-600 text-white hover:bg-yellow-700'
|
|
588
|
+
: action.actionType === 'DELETE'
|
|
589
|
+
? 'bg-red-600 text-white hover:bg-red-700'
|
|
590
|
+
: 'bg-gray-600 text-white hover:bg-gray-700'
|
|
591
|
+
}`}
|
|
592
|
+
>
|
|
593
|
+
{action.name}
|
|
594
|
+
</button>
|
|
595
|
+
))}
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Configuration Form Renderer
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
import { useConfigurationForm, useFieldOptions } from '@fastn-ai/react-core';
|
|
606
|
+
import type { ConfigurationForm, ConnectorField } from '@fastn-ai/react-core';
|
|
607
|
+
|
|
608
|
+
interface ConfigurationFormRendererProps {
|
|
609
|
+
form: ConfigurationForm;
|
|
610
|
+
onSubmit: (formData: Record<string, any>) => Promise<void>;
|
|
611
|
+
onCancel: () => void;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function ConfigurationFormRenderer({
|
|
615
|
+
form,
|
|
616
|
+
onSubmit,
|
|
617
|
+
onCancel
|
|
618
|
+
}: ConfigurationFormRendererProps) {
|
|
619
|
+
const [formData, setFormData] = useState<Record<string, any>>({});
|
|
620
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
621
|
+
|
|
622
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
623
|
+
e.preventDefault();
|
|
624
|
+
setIsSubmitting(true);
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
await onSubmit(formData);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
console.error('Form submission failed:', error);
|
|
630
|
+
} finally {
|
|
631
|
+
setIsSubmitting(false);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const renderField = (field: ConnectorField) => {
|
|
636
|
+
switch (field.type) {
|
|
637
|
+
case 'text':
|
|
638
|
+
case 'email':
|
|
639
|
+
case 'password':
|
|
640
|
+
return (
|
|
641
|
+
<input
|
|
642
|
+
type={field.type}
|
|
643
|
+
name={field.key}
|
|
644
|
+
placeholder={field.placeholder}
|
|
645
|
+
required={field.required}
|
|
646
|
+
disabled={field.disabled}
|
|
647
|
+
value={formData[field.key] || ''}
|
|
648
|
+
onChange={(e) => setFormData(prev => ({
|
|
649
|
+
...prev,
|
|
650
|
+
[field.key]: e.target.value
|
|
651
|
+
}))}
|
|
652
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
653
|
+
/>
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
case 'select':
|
|
657
|
+
return <AsyncSelectField field={field} />;
|
|
658
|
+
|
|
659
|
+
case 'multi-select':
|
|
660
|
+
return <AsyncMultiSelectField field={field} />;
|
|
661
|
+
|
|
662
|
+
case 'number':
|
|
663
|
+
return (
|
|
664
|
+
<input
|
|
665
|
+
type="number"
|
|
666
|
+
name={field.key}
|
|
667
|
+
placeholder={field.placeholder}
|
|
668
|
+
required={field.required}
|
|
669
|
+
disabled={field.disabled}
|
|
670
|
+
value={formData[field.key] || ''}
|
|
671
|
+
onChange={(e) => setFormData(prev => ({
|
|
672
|
+
...prev,
|
|
673
|
+
[field.key]: e.target.value
|
|
674
|
+
}))}
|
|
675
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
676
|
+
/>
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
case 'checkbox':
|
|
680
|
+
return (
|
|
681
|
+
<input
|
|
682
|
+
type="checkbox"
|
|
683
|
+
name={field.key}
|
|
684
|
+
required={field.required}
|
|
685
|
+
disabled={field.disabled}
|
|
686
|
+
checked={formData[field.key] || false}
|
|
687
|
+
onChange={(e) => setFormData(prev => ({
|
|
688
|
+
...prev,
|
|
689
|
+
[field.key]: e.target.checked
|
|
690
|
+
}))}
|
|
691
|
+
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
692
|
+
/>
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
default:
|
|
696
|
+
return (
|
|
697
|
+
<input
|
|
698
|
+
type="text"
|
|
699
|
+
name={field.key}
|
|
700
|
+
placeholder={field.placeholder}
|
|
701
|
+
required={field.required}
|
|
702
|
+
disabled={field.disabled}
|
|
703
|
+
value={formData[field.key] || ''}
|
|
704
|
+
onChange={(e) => setFormData(prev => ({
|
|
705
|
+
...prev,
|
|
706
|
+
[field.key]: e.target.value
|
|
707
|
+
}))}
|
|
708
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
709
|
+
/>
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
return (
|
|
715
|
+
<div className="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-lg">
|
|
716
|
+
<div className="mb-6">
|
|
717
|
+
<h2 className="text-2xl font-bold text-gray-900">{form.name}</h2>
|
|
718
|
+
<p className="text-gray-600 mt-2">{form.description}</p>
|
|
719
|
+
</div>
|
|
720
|
+
|
|
721
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
722
|
+
{form.fields.map((field) => (
|
|
723
|
+
<div key={field.key} className="space-y-2">
|
|
724
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
725
|
+
{field.label}
|
|
726
|
+
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
727
|
+
</label>
|
|
728
|
+
{renderField(field)}
|
|
729
|
+
{field.description && (
|
|
730
|
+
<p className="text-sm text-gray-500">{field.description}</p>
|
|
731
|
+
)}
|
|
732
|
+
</div>
|
|
733
|
+
))}
|
|
734
|
+
|
|
735
|
+
<div className="flex space-x-4 pt-6">
|
|
736
|
+
<button
|
|
737
|
+
type="submit"
|
|
738
|
+
disabled={isSubmitting}
|
|
739
|
+
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
740
|
+
>
|
|
741
|
+
{isSubmitting ? 'Saving...' : (form.submitButtonLabel || 'Save Configuration')}
|
|
742
|
+
</button>
|
|
743
|
+
<button
|
|
744
|
+
type="button"
|
|
745
|
+
onClick={onCancel}
|
|
746
|
+
className="flex-1 bg-gray-300 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-400"
|
|
747
|
+
>
|
|
748
|
+
Cancel
|
|
749
|
+
</button>
|
|
750
|
+
</div>
|
|
751
|
+
</form>
|
|
752
|
+
</div>
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Async Select Field Component
|
|
758
|
+
|
|
759
|
+
```tsx
|
|
760
|
+
import { useFieldOptions } from '@fastn-ai/react-core';
|
|
761
|
+
import type { ConnectorField, SelectOption } from '@fastn-ai/react-core';
|
|
762
|
+
|
|
763
|
+
interface AsyncSelectFieldProps {
|
|
764
|
+
field: ConnectorField;
|
|
765
|
+
value?: SelectOption | null;
|
|
766
|
+
onChange?: (option: SelectOption | null) => void;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function AsyncSelectField({ field, value, onChange }: AsyncSelectFieldProps) {
|
|
770
|
+
const {
|
|
771
|
+
options,
|
|
772
|
+
loading,
|
|
773
|
+
loadingMore,
|
|
774
|
+
hasNext,
|
|
775
|
+
query,
|
|
776
|
+
refresh,
|
|
777
|
+
search,
|
|
778
|
+
loadMore,
|
|
779
|
+
error
|
|
780
|
+
} = useFieldOptions(field);
|
|
781
|
+
|
|
782
|
+
const handleInputChange = (inputValue: string) => {
|
|
783
|
+
search(inputValue);
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const handleMenuScrollToBottom = () => {
|
|
787
|
+
if (hasNext && !loadingMore) {
|
|
788
|
+
loadMore();
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
return (
|
|
793
|
+
<div className="relative">
|
|
794
|
+
<select
|
|
795
|
+
value={value?.value || ''}
|
|
796
|
+
onChange={(e) => {
|
|
797
|
+
const option = options.find(opt => opt.value === e.target.value);
|
|
798
|
+
onChange?.(option || null);
|
|
799
|
+
}}
|
|
800
|
+
disabled={loading}
|
|
801
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
802
|
+
>
|
|
803
|
+
<option value="">{field.placeholder || 'Select an option...'}</option>
|
|
804
|
+
{options.map((option) => (
|
|
805
|
+
<option key={option.value} value={option.value}>
|
|
806
|
+
{option.label}
|
|
807
|
+
</option>
|
|
808
|
+
))}
|
|
809
|
+
</select>
|
|
810
|
+
|
|
811
|
+
{loading && (
|
|
812
|
+
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
813
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
|
814
|
+
</div>
|
|
815
|
+
)}
|
|
816
|
+
|
|
817
|
+
{hasNext && (
|
|
818
|
+
<button
|
|
819
|
+
type="button"
|
|
820
|
+
onClick={loadMore}
|
|
821
|
+
disabled={loadingMore}
|
|
822
|
+
className="mt-2 text-sm text-blue-600 hover:text-blue-800 disabled:opacity-50"
|
|
823
|
+
>
|
|
824
|
+
{loadingMore ? 'Loading more...' : 'Load more options'}
|
|
825
|
+
</button>
|
|
826
|
+
)}
|
|
827
|
+
|
|
828
|
+
{error && (
|
|
829
|
+
<p className="mt-1 text-sm text-red-600">{error}</p>
|
|
830
|
+
)}
|
|
831
|
+
</div>
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
## 🔧 Advanced Usage
|
|
837
|
+
|
|
838
|
+
### Custom Error Handling
|
|
839
|
+
|
|
840
|
+
```tsx
|
|
841
|
+
import { useConnectors } from '@fastn-ai/react-core';
|
|
842
|
+
|
|
843
|
+
function ConnectorsWithErrorHandling() {
|
|
844
|
+
const { data: connectors, isLoading, error, refetch } = useConnectors();
|
|
845
|
+
|
|
846
|
+
if (error) {
|
|
847
|
+
return (
|
|
848
|
+
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
849
|
+
<div className="flex">
|
|
850
|
+
<div className="flex-shrink-0">
|
|
851
|
+
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
852
|
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
853
|
+
</svg>
|
|
854
|
+
</div>
|
|
855
|
+
<div className="ml-3">
|
|
856
|
+
<h3 className="text-sm font-medium text-red-800">
|
|
857
|
+
Failed to load connectors
|
|
858
|
+
</h3>
|
|
859
|
+
<div className="mt-2 text-sm text-red-700">
|
|
860
|
+
<p>{error.message}</p>
|
|
861
|
+
</div>
|
|
862
|
+
<div className="mt-4">
|
|
863
|
+
<button
|
|
864
|
+
onClick={() => refetch()}
|
|
865
|
+
className="bg-red-100 text-red-800 px-3 py-2 rounded-md text-sm font-medium hover:bg-red-200"
|
|
866
|
+
>
|
|
867
|
+
Try again
|
|
868
|
+
</button>
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ... rest of component
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Optimistic Updates
|
|
881
|
+
|
|
882
|
+
```tsx
|
|
883
|
+
import { useConnectors } from '@fastn-ai/react-core';
|
|
884
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
885
|
+
|
|
886
|
+
function ConnectorWithOptimisticUpdate({ connector }: { connector: Connector }) {
|
|
887
|
+
const queryClient = useQueryClient();
|
|
888
|
+
|
|
889
|
+
const handleAction = async (action: ConnectorAction) => {
|
|
890
|
+
if (!action.onClick) return;
|
|
891
|
+
|
|
892
|
+
// Optimistically update the UI
|
|
893
|
+
queryClient.setQueryData(['connectors'], (oldData: Connector[] | undefined) => {
|
|
894
|
+
if (!oldData) return oldData;
|
|
895
|
+
|
|
896
|
+
return oldData.map(conn => {
|
|
897
|
+
if (conn.id === connector.id) {
|
|
898
|
+
return {
|
|
899
|
+
...conn,
|
|
900
|
+
status: action.actionType === 'ACTIVATION' ? 'ACTIVE' : 'INACTIVE'
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
return conn;
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
await action.onClick();
|
|
909
|
+
// Refetch to ensure data consistency
|
|
910
|
+
queryClient.invalidateQueries(['connectors']);
|
|
911
|
+
} catch (error) {
|
|
912
|
+
// Revert optimistic update on error
|
|
913
|
+
queryClient.invalidateQueries(['connectors']);
|
|
914
|
+
console.error('Action failed:', error);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
// ... rest of component
|
|
919
|
+
}
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### Custom Styling with CSS-in-JS
|
|
923
|
+
|
|
924
|
+
```tsx
|
|
925
|
+
import { useConnectors } from '@fastn-ai/react-core';
|
|
926
|
+
import styled from 'styled-components';
|
|
927
|
+
|
|
928
|
+
const ConnectorGrid = styled.div`
|
|
929
|
+
display: grid;
|
|
930
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
931
|
+
gap: 1.5rem;
|
|
932
|
+
padding: 1rem;
|
|
933
|
+
`;
|
|
934
|
+
|
|
935
|
+
const ConnectorCard = styled.div`
|
|
936
|
+
background: white;
|
|
937
|
+
border: 1px solid #e5e7eb;
|
|
938
|
+
border-radius: 0.5rem;
|
|
939
|
+
padding: 1.5rem;
|
|
940
|
+
transition: all 0.2s ease;
|
|
941
|
+
|
|
942
|
+
&:hover {
|
|
943
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
944
|
+
transform: translateY(-2px);
|
|
945
|
+
}
|
|
946
|
+
`;
|
|
947
|
+
|
|
948
|
+
const ActionButton = styled.button<{ variant: 'primary' | 'secondary' | 'danger' }>`
|
|
949
|
+
padding: 0.5rem 1rem;
|
|
950
|
+
border-radius: 0.375rem;
|
|
951
|
+
font-weight: 500;
|
|
952
|
+
transition: all 0.2s ease;
|
|
953
|
+
|
|
954
|
+
${({ variant }) => {
|
|
955
|
+
switch (variant) {
|
|
956
|
+
case 'primary':
|
|
957
|
+
return `
|
|
958
|
+
background: #3b82f6;
|
|
959
|
+
color: white;
|
|
960
|
+
&:hover { background: #2563eb; }
|
|
961
|
+
`;
|
|
962
|
+
case 'secondary':
|
|
963
|
+
return `
|
|
964
|
+
background: #6b7280;
|
|
965
|
+
color: white;
|
|
966
|
+
&:hover { background: #4b5563; }
|
|
967
|
+
`;
|
|
968
|
+
case 'danger':
|
|
969
|
+
return `
|
|
970
|
+
background: #dc2626;
|
|
971
|
+
color: white;
|
|
972
|
+
&:hover { background: #b91c1c; }
|
|
973
|
+
`;
|
|
974
|
+
}
|
|
975
|
+
}}
|
|
976
|
+
`;
|
|
977
|
+
|
|
978
|
+
function StyledConnectorsList() {
|
|
979
|
+
const { data: connectors, isLoading, error } = useConnectors();
|
|
980
|
+
|
|
981
|
+
if (isLoading) return <div>Loading...</div>;
|
|
982
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
983
|
+
|
|
984
|
+
return (
|
|
985
|
+
<ConnectorGrid>
|
|
986
|
+
{connectors?.map(connector => (
|
|
987
|
+
<ConnectorCard key={connector.id}>
|
|
988
|
+
<h3>{connector.name}</h3>
|
|
989
|
+
<p>{connector.description}</p>
|
|
990
|
+
<div>
|
|
991
|
+
{connector.actions.map((action, index) => (
|
|
992
|
+
<ActionButton
|
|
993
|
+
key={index}
|
|
994
|
+
variant={
|
|
995
|
+
action.actionType === 'ACTIVATION' ? 'primary' :
|
|
996
|
+
action.actionType === 'DEACTIVATION' ? 'danger' : 'secondary'
|
|
997
|
+
}
|
|
998
|
+
onClick={() => action.onClick?.()}
|
|
999
|
+
>
|
|
1000
|
+
{action.name}
|
|
1001
|
+
</ActionButton>
|
|
1002
|
+
))}
|
|
1003
|
+
</div>
|
|
1004
|
+
</ConnectorCard>
|
|
1005
|
+
))}
|
|
1006
|
+
</ConnectorGrid>
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
## 🎯 Best Practices
|
|
1012
|
+
|
|
1013
|
+
### 1. Error Boundaries
|
|
1014
|
+
|
|
1015
|
+
Wrap your Fastn components with error boundaries to handle unexpected errors gracefully:
|
|
1016
|
+
|
|
1017
|
+
```tsx
|
|
1018
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
1019
|
+
|
|
1020
|
+
function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
|
|
1021
|
+
return (
|
|
1022
|
+
<div className="text-center p-6">
|
|
1023
|
+
<h2 className="text-lg font-semibold text-gray-900">Something went wrong</h2>
|
|
1024
|
+
<p className="text-gray-600 mt-2">{error.message}</p>
|
|
1025
|
+
<button
|
|
1026
|
+
onClick={resetErrorBoundary}
|
|
1027
|
+
className="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
|
|
1028
|
+
>
|
|
1029
|
+
Try again
|
|
1030
|
+
</button>
|
|
1031
|
+
</div>
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function App() {
|
|
1036
|
+
return (
|
|
1037
|
+
<FastnProvider config={config}>
|
|
1038
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
1039
|
+
<YourApp />
|
|
1040
|
+
</ErrorBoundary>
|
|
1041
|
+
</FastnProvider>
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### 2. Loading States
|
|
1047
|
+
|
|
1048
|
+
Always provide meaningful loading states for better user experience:
|
|
1049
|
+
|
|
1050
|
+
```tsx
|
|
1051
|
+
function LoadingSkeleton() {
|
|
1052
|
+
return (
|
|
1053
|
+
<div className="animate-pulse">
|
|
1054
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
1055
|
+
{[...Array(6)].map((_, i) => (
|
|
1056
|
+
<div key={i} className="border rounded-lg p-6">
|
|
1057
|
+
<div className="flex items-center space-x-4">
|
|
1058
|
+
<div className="w-12 h-12 bg-gray-200 rounded-lg"></div>
|
|
1059
|
+
<div className="flex-1">
|
|
1060
|
+
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
|
1061
|
+
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
|
1062
|
+
</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
<div className="mt-4 flex space-x-2">
|
|
1065
|
+
<div className="h-8 bg-gray-200 rounded w-20"></div>
|
|
1066
|
+
<div className="h-8 bg-gray-200 rounded w-20"></div>
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
))}
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
### 3. Type Safety
|
|
1077
|
+
|
|
1078
|
+
Always use TypeScript interfaces for better type safety:
|
|
1079
|
+
|
|
1080
|
+
```tsx
|
|
1081
|
+
import type {
|
|
1082
|
+
Connector,
|
|
1083
|
+
Configuration,
|
|
1084
|
+
ConfigurationForm,
|
|
1085
|
+
ConnectorField
|
|
1086
|
+
} from '@fastn-ai/react-core';
|
|
1087
|
+
|
|
1088
|
+
interface ConnectorCardProps {
|
|
1089
|
+
connector: Connector;
|
|
1090
|
+
onAction?: (action: ConnectorAction) => Promise<void>;
|
|
1091
|
+
className?: string;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
interface ConfigurationFormProps {
|
|
1095
|
+
form: ConfigurationForm;
|
|
1096
|
+
onSubmit: (data: Record<string, any>) => Promise<void>;
|
|
1097
|
+
onCancel: () => void;
|
|
1098
|
+
loading?: boolean;
|
|
1099
|
+
}
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
### 4. Performance Optimization
|
|
1103
|
+
|
|
1104
|
+
Use React.memo and useMemo for expensive operations:
|
|
1105
|
+
|
|
1106
|
+
```tsx
|
|
1107
|
+
import { memo, useMemo } from 'react';
|
|
1108
|
+
|
|
1109
|
+
const ConnectorCard = memo(({ connector, onAction }: ConnectorCardProps) => {
|
|
1110
|
+
const statusColor = useMemo(() => {
|
|
1111
|
+
return connector.status === 'ACTIVE' ? 'green' : 'gray';
|
|
1112
|
+
}, [connector.status]);
|
|
1113
|
+
|
|
1114
|
+
const actionButtons = useMemo(() => {
|
|
1115
|
+
return connector.actions.map((action, index) => (
|
|
1116
|
+
<button
|
|
1117
|
+
key={index}
|
|
1118
|
+
onClick={() => onAction?.(action)}
|
|
1119
|
+
className={`px-4 py-2 rounded-md ${
|
|
1120
|
+
action.actionType === 'ACTIVATION'
|
|
1121
|
+
? 'bg-blue-600 text-white'
|
|
1122
|
+
: 'bg-gray-600 text-white'
|
|
1123
|
+
}`}
|
|
1124
|
+
>
|
|
1125
|
+
{action.name}
|
|
1126
|
+
</button>
|
|
1127
|
+
));
|
|
1128
|
+
}, [connector.actions, onAction]);
|
|
1129
|
+
|
|
1130
|
+
return (
|
|
1131
|
+
<div className="border rounded-lg p-6">
|
|
1132
|
+
<h3>{connector.name}</h3>
|
|
1133
|
+
<p>{connector.description}</p>
|
|
1134
|
+
<div className="flex space-x-2 mt-4">
|
|
1135
|
+
{actionButtons}
|
|
1136
|
+
</div>
|
|
1137
|
+
</div>
|
|
1138
|
+
);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
ConnectorCard.displayName = 'ConnectorCard';
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
## 🐛 Troubleshooting
|
|
1145
|
+
|
|
1146
|
+
### Common Issues
|
|
1147
|
+
|
|
1148
|
+
#### 1. Provider Not Found Error
|
|
1149
|
+
|
|
1150
|
+
**Error**: `Initialize Fastn with FastnProvider first`
|
|
1151
|
+
|
|
1152
|
+
**Solution**: Make sure your component is wrapped with `FastnProvider`:
|
|
1153
|
+
|
|
1154
|
+
```tsx
|
|
1155
|
+
// ❌ Wrong - Component outside provider
|
|
1156
|
+
function MyComponent() {
|
|
1157
|
+
const { data } = useConnectors(); // This will throw an error
|
|
1158
|
+
return <div>...</div>;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// ✅ Correct - Component inside provider
|
|
1162
|
+
function App() {
|
|
1163
|
+
return (
|
|
1164
|
+
<FastnProvider config={config}>
|
|
1165
|
+
<MyComponent />
|
|
1166
|
+
</FastnProvider>
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
#### 2. Authentication Errors
|
|
1172
|
+
|
|
1173
|
+
**Error**: `401 Unauthorized` or `403 Forbidden`
|
|
1174
|
+
|
|
1175
|
+
**Solution**: Check your configuration:
|
|
1176
|
+
|
|
1177
|
+
```tsx
|
|
1178
|
+
const config = {
|
|
1179
|
+
authToken: 'your-valid-auth-token', // Make sure this is valid
|
|
1180
|
+
tenantId: 'your-tenant-id',
|
|
1181
|
+
spaceId: 'your-space-id',
|
|
1182
|
+
// ... other config
|
|
1183
|
+
};
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
#### 3. Network Errors
|
|
1187
|
+
|
|
1188
|
+
**Error**: `Network Error` or `Failed to fetch`
|
|
1189
|
+
|
|
1190
|
+
**Solution**: Check your base URL and network connectivity:
|
|
1191
|
+
|
|
1192
|
+
```tsx
|
|
1193
|
+
const config = {
|
|
1194
|
+
baseUrl: 'https://api.fastn.ai', // Make sure this is correct
|
|
1195
|
+
// ... other config
|
|
1196
|
+
};
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
#### 4. TypeScript Errors
|
|
1200
|
+
|
|
1201
|
+
**Error**: Type mismatches or missing types
|
|
1202
|
+
|
|
1203
|
+
**Solution**: Import types explicitly:
|
|
1204
|
+
|
|
1205
|
+
```tsx
|
|
1206
|
+
import type {
|
|
1207
|
+
Connector,
|
|
1208
|
+
Configuration,
|
|
1209
|
+
ConnectorAction,
|
|
1210
|
+
ConfigurationAction
|
|
1211
|
+
} from '@fastn-ai/react-core';
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
### Debug Mode
|
|
1215
|
+
|
|
1216
|
+
Enable debug logging to troubleshoot issues:
|
|
1217
|
+
|
|
1218
|
+
```tsx
|
|
1219
|
+
// Add this to your app to see detailed logs
|
|
1220
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1221
|
+
console.log('Fastn Config:', config);
|
|
1222
|
+
}
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
## 📚 Additional Resources
|
|
1226
|
+
|
|
1227
|
+
- [Fastn AI Documentation](https://docs.fastn.ai)
|
|
1228
|
+
- [React Query Documentation](https://tanstack.com/query/latest)
|
|
1229
|
+
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
|
|
1230
|
+
|
|
1231
|
+
## 🤝 Contributing
|
|
1232
|
+
|
|
1233
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
1234
|
+
|
|
1235
|
+
## 📄 License
|
|
1236
|
+
|
|
1237
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
1238
|
+
|
|
1239
|
+
## 🆘 Support
|
|
1240
|
+
|
|
1241
|
+
If you need help or have questions:
|
|
1242
|
+
|
|
1243
|
+
- 📧 Email: [support@fastn.ai](mailto:support@fastn.ai)
|
|
1244
|
+
- 📖 Documentation: [docs.fastn.ai](https://docs.fastn.ai)
|
|
1245
|
+
- 🐛 Issues: [GitHub Issues](https://github.com/fastn-ai/react-core/issues)
|
|
1246
|
+
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
Made with ❤️ by the Fastn team -->
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
# @fastn-ai/react-core
|
|
1253
|
+
|
|
1254
|
+
A React library for integrating Fastn AI connectors into your application. This package provides hooks and types to manage connectors, configurations, and dynamic configuration forms, allowing you to build your own UI on top of Fastn's data and logic.
|
|
1255
|
+
|
|
1256
|
+
## Features
|
|
1257
|
+
|
|
1258
|
+
- Manage and list connectors
|
|
1259
|
+
- Handle connector configurations
|
|
1260
|
+
- Render and submit dynamic configuration forms
|
|
1261
|
+
- Powered by React Query (supports custom or existing clients)
|
|
1262
|
+
|
|
1263
|
+
## Installation
|
|
1264
|
+
|
|
1265
|
+
```bash
|
|
1266
|
+
npm install @fastn-ai/react-core
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
### Peer Dependencies
|
|
1270
|
+
|
|
1271
|
+
You need React 18+ and React Query. Install if not already present:
|
|
1272
|
+
|
|
1273
|
+
```bash
|
|
1274
|
+
npm install react react-dom @tanstack/react-query
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
## Initialization
|
|
1278
|
+
|
|
1279
|
+
### 1. Basic Setup
|
|
1280
|
+
|
|
1281
|
+
Wrap your app with `FastnProvider` and provide your configuration. This will create its own React Query client by default.
|
|
1282
|
+
|
|
1283
|
+
```tsx
|
|
1284
|
+
import { FastnProvider } from "@fastn-ai/react-core";
|
|
1285
|
+
|
|
1286
|
+
const fastnConfig = {
|
|
1287
|
+
environment: "LIVE", // or 'DRAFT' or custom string
|
|
1288
|
+
authToken: "your-auth-token",
|
|
1289
|
+
tenantId: "your-tenant-id",
|
|
1290
|
+
spaceId: "your-space-id",
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
function App() {
|
|
1294
|
+
return (
|
|
1295
|
+
<FastnProvider config={fastnConfig}>
|
|
1296
|
+
{/* Your app components here */}
|
|
1297
|
+
</FastnProvider>
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
### 2. Using an Existing React Query Client
|
|
1303
|
+
|
|
1304
|
+
If your app already uses React Query, you can pass your own client:
|
|
1305
|
+
|
|
1306
|
+
```tsx
|
|
1307
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
1308
|
+
import { FastnProvider } from "@fastn-ai/react-core";
|
|
1309
|
+
|
|
1310
|
+
const queryClient = new QueryClient();
|
|
1311
|
+
const fastnConfig = { /* ... */ };
|
|
1312
|
+
|
|
1313
|
+
function App() {
|
|
1314
|
+
return (
|
|
1315
|
+
<QueryClientProvider client={queryClient}>
|
|
1316
|
+
<FastnProvider config={fastnConfig}>
|
|
1317
|
+
{/* Your app components here */}
|
|
1318
|
+
</FastnProvider>
|
|
1319
|
+
</QueryClientProvider>
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
## Usage
|
|
1325
|
+
|
|
1326
|
+
### 1. Fetching Connectors
|
|
1327
|
+
|
|
1328
|
+
Use the `useConnectors` hook to get the list of available connectors. You can render them however you like.
|
|
1329
|
+
|
|
1330
|
+
```tsx
|
|
1331
|
+
import { useConnectors } from "@fastn-ai/react-core";
|
|
1332
|
+
|
|
1333
|
+
function ConnectorsList() {
|
|
1334
|
+
const { data: connectors, isLoading, error } = useConnectors();
|
|
1335
|
+
|
|
1336
|
+
if (isLoading) return <div>Loading...</div>;
|
|
1337
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1338
|
+
|
|
1339
|
+
return (
|
|
1340
|
+
<ul>
|
|
1341
|
+
{connectors?.map((connector) => (
|
|
1342
|
+
<li key={connector.id}>{connector.name}</li>
|
|
1343
|
+
))}
|
|
1344
|
+
</ul>
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
#### Types
|
|
1350
|
+
|
|
1351
|
+
```ts
|
|
1352
|
+
interface Connector {
|
|
1353
|
+
id: string;
|
|
1354
|
+
name: string;
|
|
1355
|
+
description: string;
|
|
1356
|
+
imageUri?: string;
|
|
1357
|
+
status: ConnectorStatus;
|
|
1358
|
+
actions: ConnectorAction[];
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
enum ConnectorStatus {
|
|
1362
|
+
ACTIVE = "ACTIVE",
|
|
1363
|
+
INACTIVE = "INACTIVE",
|
|
1364
|
+
ALL = "ALL",
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
interface ConnectorAction {
|
|
1368
|
+
name: string;
|
|
1369
|
+
actionType: ConnectorActionType | string;
|
|
1370
|
+
form?: ConnectorForm | null;
|
|
1371
|
+
onClick?: (data?: unknown) => Promise<ConnectorActionResult>;
|
|
1372
|
+
onSubmit?: (formData: Record<string, unknown>) => Promise<ConnectorActionResult>;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
interface ConnectorActionResult {
|
|
1376
|
+
data: unknown;
|
|
1377
|
+
status: "SUCCESS" | "ERROR" | "CANCELLED;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
enum ConnectorActionType {
|
|
1381
|
+
ACTIVATION = "ACTIVATION",
|
|
1382
|
+
DEACTIVATION = "DEACTIVATION",
|
|
1383
|
+
NONE = "NONE",
|
|
1384
|
+
}
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
### 2. Fetching Configurations
|
|
1388
|
+
|
|
1389
|
+
Use the `useConfigurations` hook to get connector configurations for a given configuration ID.
|
|
1390
|
+
|
|
1391
|
+
```tsx
|
|
1392
|
+
import { useConfigurations } from "@fastn-ai/react-core";
|
|
1393
|
+
|
|
1394
|
+
function ConfigurationsList({ configurationId }: { configurationId: string }) {
|
|
1395
|
+
const { data: configurations, isLoading, error } = useConfigurations({
|
|
1396
|
+
configurationId,
|
|
1397
|
+
status: "ALL", // 'ENABLED' | 'DISABLED' | 'ALL'
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
if (isLoading) return <div>Loading...</div>;
|
|
1401
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1402
|
+
|
|
1403
|
+
return (
|
|
1404
|
+
<ul>
|
|
1405
|
+
{configurations?.map((config) => (
|
|
1406
|
+
<li key={config.id}>{config.name}</li>
|
|
1407
|
+
))}
|
|
1408
|
+
</ul>
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
#### Types
|
|
1414
|
+
|
|
1415
|
+
```ts
|
|
1416
|
+
interface Configuration {
|
|
1417
|
+
id: string;
|
|
1418
|
+
connectorId: string;
|
|
1419
|
+
configurationId: string;
|
|
1420
|
+
name: string;
|
|
1421
|
+
flowId: string;
|
|
1422
|
+
description: string;
|
|
1423
|
+
imageUri: string;
|
|
1424
|
+
status: string;
|
|
1425
|
+
actions: ConfigurationAction[];
|
|
1426
|
+
metadata?: unknown;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
interface ConfigurationAction {
|
|
1430
|
+
name: string;
|
|
1431
|
+
actionType: ConfigurationActionType;
|
|
1432
|
+
onClick?: () => Promise<ConnectorActionResult>;
|
|
1433
|
+
form?: ConnectorForm | null;
|
|
1434
|
+
onSubmit?: (formData: unknown) => Promise<ConnectorActionResult>;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
type ConfigurationActionType =
|
|
1438
|
+
| ConnectorActionType.ENABLE
|
|
1439
|
+
| ConnectorActionType.DISABLE
|
|
1440
|
+
| ConnectorActionType.DELETE;
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1443
|
+
### 3. Dynamic Configuration Forms
|
|
1444
|
+
|
|
1445
|
+
Use the `useConfigurationForm` hook to fetch a configuration form for a connector or configuration. You can render the form fields as you wish.
|
|
1446
|
+
|
|
1447
|
+
```tsx
|
|
1448
|
+
import { useConfigurationForm } from "@fastn-ai/react-core";
|
|
1449
|
+
|
|
1450
|
+
function ConfigurationForm({ configurationId }: { configurationId: string }) {
|
|
1451
|
+
const { data: configurationForm, isLoading, error } = useConfigurationForm({ configurationId });
|
|
1452
|
+
|
|
1453
|
+
if (isLoading) return <div>Loading...</div>;
|
|
1454
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1455
|
+
|
|
1456
|
+
return (
|
|
1457
|
+
<form onSubmit={/* your submit handler */}>
|
|
1458
|
+
{configurationForm.fields.map((field) => (
|
|
1459
|
+
// Render your input based on field.type and field.options
|
|
1460
|
+
<div key={field.name}>{field.label}</div>
|
|
1461
|
+
))}
|
|
1462
|
+
<button type="submit">Submit</button>
|
|
1463
|
+
</form>
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
#### Types
|
|
1469
|
+
|
|
1470
|
+
```ts
|
|
1471
|
+
interface ConfigurationForm {
|
|
1472
|
+
name: string;
|
|
1473
|
+
description: string;
|
|
1474
|
+
imageUri: string;
|
|
1475
|
+
fields: ConnectorField[];
|
|
1476
|
+
submitHandler: (formData: unknown) => Promise<void>;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
interface ConnectorField {
|
|
1480
|
+
name: string;
|
|
1481
|
+
label: string;
|
|
1482
|
+
type: string; // e.g. 'text', 'select', etc.
|
|
1483
|
+
required?: boolean;
|
|
1484
|
+
options?: Array<{ label: string; value: string }>;
|
|
1485
|
+
// ...other field properties
|
|
1486
|
+
}
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
### 4. Select Fields and Google Drive File Picker (Advanced Field Handling)
|
|
1490
|
+
|
|
1491
|
+
Handling dynamic select fields and Google Drive file pickers is the most advanced part of integrating Fastn AI connectors. This section provides a comprehensive guide to implementing these fields in your own UI.
|
|
1492
|
+
|
|
1493
|
+
#### Select Fields (Single & Multi-Select)
|
|
1494
|
+
|
|
1495
|
+
For fields of type `select` or `multi-select`, use the `useFieldOptions` hook to fetch options, handle search, pagination, and errors. This hook abstracts away the complexity of loading options from remote sources, searching, and paginating.
|
|
1496
|
+
|
|
1497
|
+
**Example: Generic SelectField Component**
|
|
1498
|
+
|
|
1499
|
+
```tsx
|
|
1500
|
+
import { useFieldOptions } from "@fastn-ai/react-core";
|
|
1501
|
+
|
|
1502
|
+
function SelectField({ field, value, onChange, isMulti = false }) {
|
|
1503
|
+
// useFieldOptions provides all logic for options, loading, errors, search, pagination, etc.
|
|
1504
|
+
const {
|
|
1505
|
+
options,
|
|
1506
|
+
loading,
|
|
1507
|
+
loadingMore,
|
|
1508
|
+
hasNext,
|
|
1509
|
+
loadMore,
|
|
1510
|
+
error,
|
|
1511
|
+
search,
|
|
1512
|
+
refresh,
|
|
1513
|
+
totalLoadedOptions,
|
|
1514
|
+
} = useFieldOptions(field);
|
|
1515
|
+
|
|
1516
|
+
// Handle search input
|
|
1517
|
+
function handleInputChange(e) {
|
|
1518
|
+
search(e.target.value);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Handle loading more options (pagination)
|
|
1522
|
+
function handleLoadMore() {
|
|
1523
|
+
if (hasNext && !loadingMore) loadMore();
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Handle refresh on error
|
|
1527
|
+
function handleRefresh() {
|
|
1528
|
+
refresh();
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
return (
|
|
1532
|
+
<div>
|
|
1533
|
+
<label>{field.label}{field.required && ' *'}</label>
|
|
1534
|
+
{error && (
|
|
1535
|
+
<div>
|
|
1536
|
+
<span style={{ color: 'red' }}>Error loading options</span>
|
|
1537
|
+
<button type="button" onClick={handleRefresh}>Retry</button>
|
|
1538
|
+
</div>
|
|
1539
|
+
)}
|
|
1540
|
+
<input
|
|
1541
|
+
type="text"
|
|
1542
|
+
placeholder={field.placeholder || `Search ${field.label}`}
|
|
1543
|
+
onChange={handleInputChange}
|
|
1544
|
+
disabled={loading}
|
|
1545
|
+
/>
|
|
1546
|
+
<select
|
|
1547
|
+
multiple={isMulti}
|
|
1548
|
+
value={value}
|
|
1549
|
+
onChange={e => {
|
|
1550
|
+
if (isMulti) {
|
|
1551
|
+
const selected = Array.from(e.target.selectedOptions, o => o.value);
|
|
1552
|
+
onChange(selected);
|
|
1553
|
+
} else {
|
|
1554
|
+
onChange(e.target.value);
|
|
1555
|
+
}
|
|
1556
|
+
}}
|
|
1557
|
+
disabled={loading}
|
|
1558
|
+
>
|
|
1559
|
+
{options.map(opt => (
|
|
1560
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
1561
|
+
))}
|
|
1562
|
+
</select>
|
|
1563
|
+
{loading && <div>Loading options...</div>}
|
|
1564
|
+
{hasNext && !loadingMore && (
|
|
1565
|
+
<button type="button" onClick={handleLoadMore}>Load More</button>
|
|
1566
|
+
)}
|
|
1567
|
+
{loadingMore && <div>Loading more...</div>}
|
|
1568
|
+
<div>Loaded {totalLoadedOptions} options{hasNext ? '' : ' (all loaded)'}</div>
|
|
1569
|
+
{field.description && <div style={{ fontSize: 'smaller', color: '#666' }}>{field.description}</div>}
|
|
1570
|
+
</div>
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
**Key Points:**
|
|
1576
|
+
- `useFieldOptions(field)` returns all logic for options, loading, errors, search, pagination, and refresh.
|
|
1577
|
+
- Use `search` for filtering options, `loadMore` for pagination, and `refresh` to retry on error.
|
|
1578
|
+
- The component above is generic and can be styled or replaced with any UI library (e.g., react-select).
|
|
1579
|
+
|
|
1580
|
+
**useFieldOptions Return Type:**
|
|
1581
|
+
```ts
|
|
1582
|
+
interface UseFieldOptionsReturn {
|
|
1583
|
+
options: Array<{ label: string; value: string }>;
|
|
1584
|
+
loading: boolean;
|
|
1585
|
+
loadingMore: boolean;
|
|
1586
|
+
hasNext: boolean;
|
|
1587
|
+
loadMore: () => Promise<void>;
|
|
1588
|
+
error: string | null;
|
|
1589
|
+
search: (query: string) => void;
|
|
1590
|
+
refresh: () => void;
|
|
1591
|
+
totalLoadedOptions: number;
|
|
1592
|
+
}
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
#### Google Drive File Picker Field
|
|
1596
|
+
|
|
1597
|
+
For fields that require Google Drive file selection, the field type will be something like `select_with_google_files_picker` or similar. These fields provide an `optionsSource` with a method to open the Google Files Picker dialog.
|
|
1598
|
+
|
|
1599
|
+
**Example: GoogleFilesPickerField Component**
|
|
1600
|
+
|
|
1601
|
+
```tsx
|
|
1602
|
+
function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
|
|
1603
|
+
// This field may also use useFieldOptions for listing previously picked files
|
|
1604
|
+
const { options, loading, error, refresh } = useFieldOptions(field);
|
|
1605
|
+
|
|
1606
|
+
// Handler to open Google Drive picker
|
|
1607
|
+
async function handlePickFiles() {
|
|
1608
|
+
if (field.optionsSource?.openGoogleFilesPicker) {
|
|
1609
|
+
await field.optionsSource.openGoogleFilesPicker({
|
|
1610
|
+
onComplete: async (files) => {
|
|
1611
|
+
// files is an array of selected file objects
|
|
1612
|
+
if (isMulti) {
|
|
1613
|
+
onChange(files);
|
|
1614
|
+
} else {
|
|
1615
|
+
onChange(files[0]);
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
onError: async (pickerError) => {
|
|
1619
|
+
// Handle picker error (optional)
|
|
1620
|
+
alert('Google Files Picker error: ' + pickerError);
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
return (
|
|
1627
|
+
<div>
|
|
1628
|
+
<label>{field.label}{field.required && ' *'}</label>
|
|
1629
|
+
{error && (
|
|
1630
|
+
<div>
|
|
1631
|
+
<span style={{ color: 'red' }}>Error loading files</span>
|
|
1632
|
+
<button type="button" onClick={refresh}>Retry</button>
|
|
1633
|
+
</div>
|
|
1634
|
+
)}
|
|
1635
|
+
<button type="button" onClick={handlePickFiles} disabled={loading}>
|
|
1636
|
+
Pick from Google Drive
|
|
1637
|
+
</button>
|
|
1638
|
+
{value && (
|
|
1639
|
+
<div>
|
|
1640
|
+
<strong>Selected file{isMulti ? 's' : ''}:</strong>
|
|
1641
|
+
<ul>
|
|
1642
|
+
{(isMulti ? value : [value]).map((file, idx) => (
|
|
1643
|
+
<li key={file.id || idx}>{file.name || file.id}</li>
|
|
1644
|
+
))}
|
|
1645
|
+
</ul>
|
|
1646
|
+
</div>
|
|
1647
|
+
)}
|
|
1648
|
+
{field.description && <div style={{ fontSize: 'smaller', color: '#666' }}>{field.description}</div>}
|
|
1649
|
+
</div>
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
```
|
|
1653
|
+
|
|
1654
|
+
**Key Points:**
|
|
1655
|
+
- The `openGoogleFilesPicker` method is provided on the field's `optionsSource` property.
|
|
1656
|
+
- The `onComplete` callback receives the selected files (array of file objects).
|
|
1657
|
+
- You can display the selected files as you wish.
|
|
1658
|
+
- This field can be used in both single and multi-select modes.
|
|
1659
|
+
|
|
1660
|
+
**GoogleFilesPicker Return Type:**
|
|
1661
|
+
```ts
|
|
1662
|
+
interface UseGoogleFilesPickerReturn {
|
|
1663
|
+
options: Array<{ label: string; value: string }>;
|
|
1664
|
+
loading: boolean;
|
|
1665
|
+
loadingMore: boolean;
|
|
1666
|
+
hasNext: boolean;
|
|
1667
|
+
query: string;
|
|
1668
|
+
refresh: () => Promise<void>;
|
|
1669
|
+
search: (query: string) => void;
|
|
1670
|
+
loadMore: () => Promise<void>;
|
|
1671
|
+
totalLoadedOptions: number;
|
|
1672
|
+
error: string | null;
|
|
1673
|
+
}
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
**Integrating with Forms**
|
|
1677
|
+
- For both SelectField and GoogleFilesPickerField, you can use them as controlled components in your form library (Formik, React Hook Form, etc.).
|
|
1678
|
+
- The value and onChange props should be managed by your form state.
|
|
1679
|
+
- You can combine these fields with other input types to build a complete dynamic configuration form.
|
|
1680
|
+
|
|
1681
|
+
---
|
|
1682
|
+
|
|
1683
|
+
## Error Handling & Troubleshooting
|
|
1684
|
+
|
|
1685
|
+
- **Provider Not Found**: Ensure your components are wrapped in `FastnProvider`.
|
|
1686
|
+
- **Authentication Errors**: Check your `authToken`, `tenantId`, and `spaceId`.
|
|
1687
|
+
- **Network Errors**: Verify your network and API base URL.
|
|
1688
|
+
- **TypeScript Errors**: Import types from the package as needed.
|
|
1689
|
+
|
|
1690
|
+
## TypeScript Support
|
|
1691
|
+
|
|
1692
|
+
All hooks and data structures are fully typed. You can import types directly:
|
|
1693
|
+
|
|
1694
|
+
```ts
|
|
1695
|
+
import type { Connector, Configuration, ConnectorAction, ConfigurationAction } from '@fastn-ai/react-core';
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
## License
|
|
1699
|
+
|
|
1700
|
+
MIT License. See the [LICENSE](LICENSE) file for details.
|
|
1701
|
+
|
|
1702
|
+
## Support
|
|
1703
|
+
|
|
1704
|
+
- Email: support@fastn.ai
|
|
1705
|
+
- Documentation: https://docs.fastn.ai
|
|
1706
|
+
- Issues: https://github.com/fastn-ai/react-core/issues
|
|
1707
|
+
|
|
1708
|
+
---
|
|
1709
|
+
|
|
1710
|
+
This package provides the data and logic for Fastn AI connectors. You are free to build your own UI and integrate these hooks and types as needed for your application.
|