@dev-fastn-ai/react-core 1.0.20 → 2.0.1

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/README.md CHANGED
@@ -1,75 +1,141 @@
1
- # @fastn-ai/react-core
1
+ # [Fastn.ai](http://fastn.ai/) React Core Documentation
2
2
 
3
- A React library for integrating Fastn AI connectors into your application. This package provides robust hooks to manage connectors, configurations, and dynamic configuration forms, empowering you to build custom UIs on top of Fastn's data and logic.
3
+ A React library for integrating **Fastn AI connectors** into your application. It provides powerful hooks to manage:
4
4
 
5
- ---
6
-
7
- ## Features
5
+ - Connector listing, activation, and deactivation
6
+ - Configuration form rendering and submission
7
+ - Seamless integration with React Query for state management
8
8
 
9
- - List and manage connectors
10
- - Handle connector configurations
11
- - Render and submit dynamic configuration forms
12
- - Powered by React Query (supports custom or existing clients)
9
+ This enables you to **build fully custom UIs** on top of Fastn's powerful data and logic engine.
13
10
 
14
11
  ---
15
12
 
16
- ## Installation
13
+ ## 📦 Installation
14
+
15
+ Install the core library:
17
16
 
18
17
  ```bash
19
- npm install @fastn-ai/react-core
18
+ npm install @dev-fastn-ai/react-core
20
19
  ```
21
20
 
22
- ### Peer Dependencies
23
-
24
- Ensure you have React 18+ and React Query installed:
21
+ Also, make sure you install the required **peer dependencies**:
25
22
 
26
23
  ```bash
27
24
  npm install react react-dom @tanstack/react-query
28
25
  ```
29
26
 
27
+ > ✅ Requires React 18+
28
+
30
29
  ---
31
30
 
32
- ## Getting Started
31
+ ## 🏗️ Fastn Architecture Concepts
32
+
33
+ Before diving into the code, let's understand the key Fastn concepts and terminology:
34
+
35
+ ### **Space (Workspace)**
36
+
37
+ A **Space** (also called Workspace) is the top-level container in Fastn that groups all your connectors, configurations, and data flows. Think of it as your project or organization's workspace where all integrations live.
38
+
39
+ ### **Tenant**
40
+
41
+ A **Tenant** represents a user, team, or organization within your application. Each tenant has isolated data and configurations. For example:
42
+
43
+ - A single user account
44
+ - A team within your app
45
+ - An organization or company
46
+ - A client's workspace
47
+
48
+ ### **Connector**
49
+
50
+ A **Connector** represents an integration with an external service (like Slack, Google Drive, etc.). Connectors define what external services your app can connect to.
51
+
52
+ ### **Configuration**
53
+
54
+ A **Configuration** is a specific instance of a connector with saved settings and authentication. For example:
55
+
56
+ - A Slack workspace connection with specific channels selected
57
+ - A Google Drive connection with specific folders configured
58
+ - A database connection with connection parameters
33
59
 
34
- ### 1. Basic Setup
60
+ ### **Configuration ID**
35
61
 
36
- Wrap your app with `FastnProvider` and provide your configuration. By default, this creates its own React Query client.
62
+ A **Configuration ID** is a unique identifier that represents a specific configuration instance. This ID is used to:
63
+
64
+ - Load existing configurations
65
+ - Update configuration settings
66
+ - Manage the lifecycle of a specific integration
67
+
68
+ ---
69
+
70
+ ## ⚙️ Features
71
+
72
+ - **Connector Management**: List, activate, and deactivate connectors
73
+ - **Tenant Isolation**: Each tenant has its own isolated connector state and configurations
74
+ - **Configuration Persistence**: Save and retrieve configurations using unique `configurationId`s
75
+ - **Dynamic Forms**: Render configuration forms using Fastn's form schema
76
+ - **React Query Integration**: Built-in support for efficient caching and request handling
77
+ - **Authentication Flow**: Handle OAuth and custom authentication flows seamlessly
78
+
79
+ ---
80
+
81
+ ## 🚀 Getting Started
82
+
83
+ ### 1. **Wrap Your App with `FastnProvider`**
84
+
85
+ This sets up Fastn in your app and gives you access to its hooks and logic.
37
86
 
38
87
  ```tsx
39
- import { FastnProvider } from "@fastn-ai/react-core";
88
+ import { FastnProvider } from "@dev-fastn-ai/react-core";
40
89
 
41
90
  const fastnConfig = {
42
- environment: "LIVE", // or 'DRAFT' or custom string
43
- authToken: "your-auth-token",
44
- tenantId: "your-tenant-id",
45
- spaceId: "your-space-id",
91
+ environment: "LIVE", // "LIVE", "DRAFT", or a custom environment string for widgets
92
+ authToken: "your-auth-token", // Your app's access token, authenticated through Fastn Custom Auth
93
+ tenantId: "your-tenant-id", // A unique ID representing the user, team, or organization
94
+ spaceId: "your-space-id", // Fastn Space ID (also called Workspace ID) - groups all connectors and configurations
46
95
  };
47
96
 
48
97
  function App() {
49
98
  return (
50
99
  <FastnProvider config={fastnConfig}>
51
- {/* Your app components here */}
100
+ {/* Your app components */}
52
101
  </FastnProvider>
53
102
  );
54
103
  }
55
104
  ```
56
105
 
57
- ### 2. Using an Existing React Query Client
106
+ ### 🔍 Configuration Field Reference
107
+
108
+ | Field | Description |
109
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
110
+ | `environment` | The widget environment: use `"LIVE"` for production, `"DRAFT"` for preview/testing, or any custom string configured in Fastn |
111
+ | `authToken` | The token from your authentication flow. Fastn uses this to authenticate the user via the **fastnCustomAuth** flow |
112
+ | `tenantId` | A unique identifier for the current user or organization. This helps Fastn isolate data per tenant |
113
+ | `spaceId` | The Fastn **Space ID**, also called the Workspace ID. It groups all connectors, configurations, and flows |
114
+
115
+ ---
116
+
117
+ ### 2. **Use an Existing React Query Client (Optional)**
58
118
 
59
119
  If your app already uses React Query, you can pass your own client:
60
120
 
61
121
  ```tsx
62
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
63
- import { FastnProvider } from "@fastn-ai/react-core";
122
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
123
+ import { FastnProvider } from "@dev-fastn-ai/react-core";
64
124
 
65
125
  const queryClient = new QueryClient();
66
- const fastnConfig = { /* ... */ };
126
+
127
+ const fastnConfig = {
128
+ environment: "LIVE",
129
+ authToken: "your-auth-token",
130
+ tenantId: "your-tenant-id",
131
+ spaceId: "your-space-id",
132
+ };
67
133
 
68
134
  function App() {
69
135
  return (
70
136
  <QueryClientProvider client={queryClient}>
71
137
  <FastnProvider config={fastnConfig}>
72
- {/* Your app components here */}
138
+ {/* Your app components */}
73
139
  </FastnProvider>
74
140
  </QueryClientProvider>
75
141
  );
@@ -78,34 +144,11 @@ function App() {
78
144
 
79
145
  ---
80
146
 
81
- ## Usage
147
+ ## 🧩 Core Hooks & Types
82
148
 
83
- ### 1. Fetching Connectors
84
-
85
- Use the `useConnectors` hook to retrieve the list of available connectors.
149
+ ### **Connector Types**
86
150
 
87
151
  ```tsx
88
- import { useConnectors } from "@fastn-ai/react-core";
89
-
90
- function ConnectorsList() {
91
- const { data: connectors, isLoading, error } = useConnectors();
92
-
93
- if (isLoading) return <div>Loading...</div>;
94
- if (error) return <div>Error: {error.message}</div>;
95
-
96
- return (
97
- <ul>
98
- {connectors?.map((connector) => (
99
- <li key={connector.id}>{connector.name}</li>
100
- ))}
101
- </ul>
102
- );
103
- }
104
- ```
105
-
106
- #### Types
107
-
108
- ```ts
109
152
  interface Connector {
110
153
  id: string;
111
154
  name: string;
@@ -124,13 +167,10 @@ enum ConnectorStatus {
124
167
  interface ConnectorAction {
125
168
  name: string;
126
169
  actionType: ConnectorActionType | string;
127
- form?: ConnectorForm | null;
128
- onClick?: (data?: unknown) => Promise<ConnectorActionResult>;
129
- onSubmit?: (formData: Record<string, unknown>) => Promise<ConnectorActionResult>;
170
+ onClick?: () => Promise<ConnectorActionResult>;
130
171
  }
131
172
 
132
173
  interface ConnectorActionResult {
133
- data: unknown;
134
174
  status: "SUCCESS" | "ERROR" | "CANCELLED";
135
175
  }
136
176
 
@@ -141,123 +181,439 @@ enum ConnectorActionType {
141
181
  }
142
182
  ```
143
183
 
184
+ ### **Configuration Types**
185
+
186
+ ```tsx
187
+ interface Configuration {
188
+ id: string;
189
+ connectorId: string;
190
+ name: string;
191
+ description: string;
192
+ imageUri?: string;
193
+ status: ConfigurationStatus;
194
+ actions: ConfigurationAction[];
195
+ }
196
+
197
+ enum ConfigurationStatus {
198
+ ENABLED = "ENABLED",
199
+ DISABLED = "DISABLED",
200
+ PENDING = "PENDING",
201
+ }
202
+
203
+ interface ConfigurationAction {
204
+ name: string;
205
+ actionType: ConfigurationActionType | string;
206
+ onClick?: () => Promise<ConfigurationActionResult>;
207
+ }
208
+
209
+ interface ConfigurationActionResult {
210
+ status: "SUCCESS" | "ERROR" | "CANCELLED";
211
+ }
212
+
213
+ enum ConfigurationActionType {
214
+ ENABLE = "ENABLE",
215
+ DISABLE = "DISABLE",
216
+ DELETE = "DELETE",
217
+ UPDATE = "UPDATE",
218
+ }
219
+ ```
220
+
221
+ ### **Configuration Form Types**
222
+
223
+ ```tsx
224
+ interface ConfigurationForm {
225
+ name: string;
226
+ description: string;
227
+ imageUri: string;
228
+ fields: ConnectorField[];
229
+ submitHandler: (formData: FormData) => Promise<void>;
230
+ }
231
+
232
+ type FormData =
233
+ | Record<
234
+ string,
235
+ Record<string, Primitive> | Record<string, Primitive>[] | undefined | null
236
+ >
237
+ | Record<
238
+ string,
239
+ Record<string, Primitive> | Record<string, Primitive>[] | undefined | null
240
+ >[]
241
+ | undefined
242
+ | null;
243
+
244
+ interface ConnectorField {
245
+ readonly name: string;
246
+ readonly key: string;
247
+ readonly label: string;
248
+ readonly type: ConnectorFieldType | string;
249
+ readonly required: boolean;
250
+ readonly placeholder: string;
251
+ readonly description: string;
252
+ readonly hidden?: boolean;
253
+ readonly disabled?: boolean;
254
+ readonly initialValue?:
255
+ | Record<string, Primitive>
256
+ | Record<string, Primitive>[]
257
+ | Primitive
258
+ | Primitive[];
259
+ readonly optionsSource?: SelectOptionSource;
260
+ }
261
+ ```
262
+
144
263
  ---
145
264
 
146
- ### 2. Fetching Configurations
265
+ ## 🔄 Complete Integration Workflows
266
+
267
+ ### **Workflow 1: Setting Up Your First Slack Integration**
147
268
 
148
- Use the `useConfigurations` hook to get connector configurations for a given configuration ID.
269
+ Let's walk through a complete example of setting up a Slack integration from scratch.
270
+
271
+ #### Step 1: List Available Connectors
272
+
273
+ First, show users what connectors are available:
274
+
275
+ ```tsx
276
+ import { useConnectors } from "@dev-fastn-ai/react-core";
277
+
278
+ function ConnectorList() {
279
+ const { data: connectors, isLoading, error } = useConnectors();
280
+
281
+ if (isLoading) return <div>Loading available integrations...</div>;
282
+ if (error) return <div>Error loading connectors: {error.message}</div>;
283
+
284
+ return (
285
+ <div className="connector-grid">
286
+ <h2>Available Integrations</h2>
287
+ {connectors?.map((connector) => (
288
+ <div key={connector.id} className="connector-card">
289
+ <img src={connector.imageUri} alt={connector.name} />
290
+ <h3>{connector.name}</h3>
291
+ <p>{connector.description}</p>
292
+
293
+ {connector.status === "ACTIVE" && (
294
+ <span className="status-badge connected">Connected</span>
295
+ )}
296
+
297
+ {connector.actions?.map((action) => (
298
+ <button
299
+ key={action.name}
300
+ onClick={action.onClick}
301
+ className={`action-btn ${action.actionType.toLowerCase()}`}
302
+ >
303
+ {action.name}
304
+ </button>
305
+ ))}
306
+ </div>
307
+ ))}
308
+ </div>
309
+ );
310
+ }
311
+ ```
312
+
313
+ #### Step 2: List Configurations After Connector Activation
314
+
315
+ After a connector is activated, you can list its configurations:
149
316
 
150
317
  ```tsx
151
- import { useConfigurations } from "@fastn-ai/react-core";
318
+ import { useConfigurations } from "@dev-fastn-ai/react-core";
152
319
 
153
- function ConfigurationsList({ configurationId }: { configurationId: string }) {
154
- const { data: configurations, isLoading, error } = useConfigurations({
155
- configurationId,
156
- status: "ALL", // 'ENABLED' | 'DISABLED' | 'ALL'
157
- });
320
+ function ConfigurationList({ configurationId }) {
321
+ const {
322
+ data: configurations,
323
+ isLoading,
324
+ error,
325
+ } = useConfigurations({ configurationId });
326
+ const [selectedConfig, setSelectedConfig] = useState(null);
158
327
 
159
- if (isLoading) return <div>Loading...</div>;
328
+ if (isLoading) return <div>Loading configurations...</div>;
160
329
  if (error) return <div>Error: {error.message}</div>;
161
330
 
162
331
  return (
163
- <ul>
332
+ <div className="configuration-list">
333
+ <h2>Your Integrations</h2>
164
334
  {configurations?.map((config) => (
165
- <li key={config.id}>{config.name}</li>
335
+ <div key={config.id} className="config-card">
336
+ <div className="config-info">
337
+ <img src={config.imageUri} alt={config.name} />
338
+ <div>
339
+ <h3>{config.name}</h3>
340
+ <p>{config.description}</p>
341
+ </div>
342
+ </div>
343
+
344
+ <div className="config-actions">
345
+ {config.status === "ENABLED" && (
346
+ <span className="status-badge enabled">Active</span>
347
+ )}
348
+
349
+ {config.actions?.map((action) => (
350
+ <button
351
+ key={action.name}
352
+ onClick={async () => {
353
+ const result = await action.onClick();
354
+ if (
355
+ action.actionType === "ENABLE" &&
356
+ result?.status === "SUCCESS"
357
+ ) {
358
+ // Show configuration form for new setup
359
+ setSelectedConfig(config);
360
+ } else if (
361
+ action.actionType === "UPDATE" &&
362
+ result?.status === "SUCCESS"
363
+ ) {
364
+ // Show configuration form for editing
365
+ setSelectedConfig(config);
366
+ }
367
+ }}
368
+ className={`action-btn ${action.actionType.toLowerCase()}`}
369
+ >
370
+ {action.name}
371
+ </button>
372
+ ))}
373
+ </div>
374
+ </div>
166
375
  ))}
167
- </ul>
376
+
377
+ {selectedConfig && (
378
+ <ConfigurationForm
379
+ configurationId={selectedConfig.id}
380
+ onClose={() => setSelectedConfig(null)}
381
+ />
382
+ )}
383
+ </div>
168
384
  );
169
385
  }
170
386
  ```
171
387
 
172
- #### Types
388
+ #### Step 3: Load Configuration Form
173
389
 
174
- ```ts
175
- interface Configuration {
176
- id: string;
177
- connectorId: string;
178
- configurationId: string;
179
- name: string;
180
- flowId: string;
181
- description: string;
182
- imageUri: string;
183
- status: string;
184
- actions: ConfigurationAction[];
185
- metadata?: unknown;
186
- }
390
+ When a configuration is selected (either for new setup or editing), show the configuration form:
187
391
 
188
- interface ConfigurationAction {
189
- name: string;
190
- actionType: ConfigurationActionType;
191
- onClick?: () => Promise<ConnectorActionResult>;
192
- form?: ConnectorForm | null;
193
- onSubmit?: (formData: unknown) => Promise<ConnectorActionResult>;
194
- }
392
+ ```tsx
393
+ import { useConfigurationForm } from "@dev-fastn-ai/react-core";
195
394
 
196
- type ConfigurationActionType =
197
- | ConnectorActionType.ENABLE
198
- | ConnectorActionType.DISABLE
199
- | ConnectorActionType.DELETE;
395
+ function ConfigurationForm({ configurationId, onClose }) {
396
+ const {
397
+ data: configurationForm,
398
+ isLoading,
399
+ error,
400
+ handleSubmit,
401
+ } = useConfigurationForm({ configurationId });
402
+
403
+ const [formData, setFormData] = useState({});
404
+ const [isSubmitting, setIsSubmitting] = useState(false);
405
+
406
+ // Pre-populate form with existing values if editing
407
+ useEffect(() => {
408
+ if (configurationForm?.fields) {
409
+ const initialData = {};
410
+ configurationForm.fields.forEach((field) => {
411
+ if (field.initialValue !== undefined) {
412
+ initialData[field.key] = field.initialValue;
413
+ }
414
+ });
415
+ setFormData(initialData);
416
+ }
417
+ }, [configurationForm]);
418
+
419
+ if (isLoading) return <div>Loading configuration form...</div>;
420
+ if (error) return <div>Error: {error.message}</div>;
421
+
422
+ const onSubmit = async (e) => {
423
+ e.preventDefault();
424
+ setIsSubmitting(true);
425
+
426
+ try {
427
+ await handleSubmit({ formData });
428
+ console.log("Configuration saved successfully!");
429
+ onClose();
430
+ } catch (error) {
431
+ console.error("Failed to save configuration:", error);
432
+ } finally {
433
+ setIsSubmitting(false);
434
+ }
435
+ };
436
+
437
+ return (
438
+ <div className="modal">
439
+ <form onSubmit={onSubmit} className="configuration-form">
440
+ <h2>Configure {configurationForm.name}</h2>
441
+ <p>{configurationForm.description}</p>
442
+
443
+ {configurationForm.fields.map((field) => (
444
+ <FormField
445
+ key={field.key}
446
+ field={field}
447
+ value={formData[field.key]}
448
+ onChange={(value) =>
449
+ setFormData((prev) => ({ ...prev, [field.key]: value }))
450
+ }
451
+ />
452
+ ))}
453
+
454
+ <div className="form-actions">
455
+ <button type="button" onClick={onClose}>
456
+ Cancel
457
+ </button>
458
+ <button type="submit" disabled={isSubmitting}>
459
+ {isSubmitting ? "Saving..." : "Save Configuration"}
460
+ </button>
461
+ </div>
462
+ </form>
463
+ </div>
464
+ );
465
+ }
200
466
  ```
201
467
 
202
- ---
468
+ ### **Workflow 2: Managing Existing Configurations**
203
469
 
204
- ### 3. Dynamic Configuration Forms
470
+ Now let's show how to manage existing configurations - viewing, editing, and disabling them.
205
471
 
206
- Use the `useConfigurationForm` hook to fetch a configuration form for a connector or configuration. Render the form fields as needed.
472
+ #### Step 1: List Existing Configurations
207
473
 
208
474
  ```tsx
209
- import { useConfigurationForm } from "@fastn-ai/react-core";
475
+ import { useConfigurations } from "@dev-fastn-ai/react-core";
210
476
 
211
- function ConfigurationForm({ configurationId }: { configurationId: string }) {
212
- const { data: configurationForm, isLoading, error } = useConfigurationForm({ configurationId });
477
+ function ConfigurationManager({ configurationId }) {
478
+ const {
479
+ data: configurations,
480
+ isLoading,
481
+ error,
482
+ } = useConfigurations({ configurationId });
483
+ const [selectedConfig, setSelectedConfig] = useState(null);
213
484
 
214
- if (isLoading) return <div>Loading...</div>;
485
+ if (isLoading) return <div>Loading your integrations...</div>;
215
486
  if (error) return <div>Error: {error.message}</div>;
216
487
 
217
488
  return (
218
- <form onSubmit={/* your submit handler */}>
219
- {configurationForm.fields.map((field) => (
220
- // Render field based on type (see below for select and Google Drive picker fields)
489
+ <div className="configuration-manager">
490
+ <h2>Your Integrations</h2>
491
+
492
+ {configurations?.map((config) => (
493
+ <div key={config.id} className="config-card">
494
+ <div className="config-info">
495
+ <img src={config.imageUri} alt={config.name} />
496
+ <div>
497
+ <h3>{config.name}</h3>
498
+ <p>{config.description}</p>
499
+ </div>
500
+ </div>
501
+
502
+ <div className="config-actions">
503
+ {config.status === "ENABLED" && (
504
+ <span className="status-badge enabled">Active</span>
505
+ )}
506
+
507
+ {config.actions?.map((action) => (
508
+ <button
509
+ key={action.name}
510
+ onClick={async () => {
511
+ const result = await action.onClick();
512
+ if (
513
+ action.actionType === "UPDATE" &&
514
+ result?.status === "SUCCESS"
515
+ ) {
516
+ setSelectedConfig(config);
517
+ }
518
+ }}
519
+ className={`action-btn ${action.actionType.toLowerCase()}`}
520
+ >
521
+ {action.name}
522
+ </button>
523
+ ))}
524
+ </div>
525
+ </div>
221
526
  ))}
222
- <button type="submit">Submit</button>
223
- </form>
527
+
528
+ {selectedConfig && (
529
+ <ConfigurationForm
530
+ configurationId={selectedConfig.id}
531
+ onClose={() => setSelectedConfig(null)}
532
+ />
533
+ )}
534
+ </div>
224
535
  );
225
536
  }
226
537
  ```
227
538
 
228
- #### Types
539
+ #### Step 2: Disable Configuration
229
540
 
230
- ```ts
231
- interface ConfigurationForm {
232
- name: string;
233
- description: string;
234
- imageUri: string;
235
- fields: ConnectorField[];
236
- submitHandler: (formData: unknown) => Promise<void>;
237
- }
541
+ ```tsx
542
+ function ConfigActions({ config }) {
543
+ const handleDisable = async (action) => {
544
+ if (action.actionType === "DISABLE") {
545
+ const result = await action.onClick();
546
+ if (result?.status === "SUCCESS") {
547
+ console.log("Configuration disabled successfully");
548
+ // Refresh the configuration list
549
+ }
550
+ }
551
+ };
238
552
 
239
- interface ConnectorField {
240
- name: string;
241
- label: string;
242
- type: string; // e.g. 'text', 'select', etc.
243
- required?: boolean;
244
- options?: Array<{ label: string; value: string }>;
245
- // ...other field properties
553
+ return (
554
+ <div className="config-actions">
555
+ {config.actions?.map((action) => (
556
+ <button
557
+ key={action.name}
558
+ onClick={() => handleDisable(action)}
559
+ className={`action-btn ${action.actionType.toLowerCase()}`}
560
+ >
561
+ {action.name}
562
+ </button>
563
+ ))}
564
+ </div>
565
+ );
246
566
  }
247
567
  ```
248
568
 
249
- ---
569
+ #### Step 3: Form Field Value Handling
570
+
571
+ The form fields handle different value types based on the field type:
250
572
 
251
- ### 4. Advanced Field Handling: Select & Google Drive File Picker
573
+ - **Select fields**: Always return `{ label: string, value: string }` objects
574
+ - **Multi-select fields**: Always return `{ label: string, value: string }[]` arrays
575
+ - **Google Drive picker fields**: Always return `{ label: string, value: string }` objects or arrays
576
+ - **Other fields**: Return primitive values (string, number, boolean)
252
577
 
253
- #### Select Fields (Single & Multi-Select)
578
+ ```tsx
579
+ // Example form data structure
580
+ const formData = {
581
+ // Select field - single object
582
+ channel: { label: "General", value: "C123456" },
583
+
584
+ // Multi-select field - array of objects
585
+ channels: [
586
+ { label: "General", value: "C123456" },
587
+ { label: "Random", value: "C789012" },
588
+ ],
589
+
590
+ // Google Drive picker - single object
591
+ folder: { label: "My Documents", value: "folder_id_123" },
592
+
593
+ // Google Drive picker multi - array of objects
594
+ files: [
595
+ { label: "document1.pdf", value: "file_id_1" },
596
+ { label: "document2.pdf", value: "file_id_2" },
597
+ ],
598
+
599
+ // Text field - primitive
600
+ webhookUrl: "https://hooks.slack.com/...",
601
+
602
+ // Boolean field - primitive
603
+ enableNotifications: true,
604
+ };
605
+ ```
254
606
 
255
- For fields of type `select` or `multi-select`, use the `useFieldOptions` hook to fetch options, handle search, pagination, and errors. This abstracts away the complexity of loading options from remote sources.
607
+ ---
256
608
 
257
- **Example: Generic SelectField Component**
609
+ ## 🎨 Form Field Components
610
+
611
+ ### **Select and Multi-Select Fields**
612
+
613
+ For fields of type `select` or `multi-select`, use the `useFieldOptions` hook to handle dynamic options loading. These fields always work with `{ label, value }` objects:
258
614
 
259
615
  ```tsx
260
- import { useFieldOptions } from "@fastn-ai/react-core";
616
+ import { useFieldOptions } from "@dev-fastn-ai/react-core";
261
617
 
262
618
  function SelectField({ field, value, onChange, isMulti = false }) {
263
619
  const {
@@ -268,7 +624,6 @@ function SelectField({ field, value, onChange, isMulti = false }) {
268
624
  loadMore,
269
625
  error,
270
626
  search,
271
- refresh,
272
627
  totalLoadedOptions,
273
628
  } = useFieldOptions(field);
274
629
 
@@ -280,75 +635,105 @@ function SelectField({ field, value, onChange, isMulti = false }) {
280
635
  if (hasNext && !loadingMore) loadMore();
281
636
  }
282
637
 
283
- function handleRefresh() {
284
- refresh();
638
+ function handleSelectChange(selectedOptions) {
639
+ if (isMulti) {
640
+ // For multi-select, value should be an array of { label, value } objects
641
+ const selectedValues = selectedOptions.map((option) => ({
642
+ label: option.label,
643
+ value: option.value,
644
+ }));
645
+ onChange(selectedValues);
646
+ } else {
647
+ // For single select, value should be a single { label, value } object
648
+ const selectedValue = selectedOptions[0]
649
+ ? {
650
+ label: selectedOptions[0].label,
651
+ value: selectedOptions[0].value,
652
+ }
653
+ : null;
654
+ onChange(selectedValue);
655
+ }
285
656
  }
286
657
 
287
658
  return (
288
- <div>
289
- <label>{field.label}{field.required && ' *'}</label>
659
+ <div className="field-container">
660
+ <label className="field-label">
661
+ {field.label}
662
+ {field.required && <span className="required"> *</span>}
663
+ </label>
664
+
290
665
  {error && (
291
- <div>
292
- <span style={{ color: 'red' }}>Error loading options</span>
293
- <button type="button" onClick={handleRefresh}>Retry</button>
666
+ <div className="error-message">
667
+ Error loading options: {error.message}
294
668
  </div>
295
669
  )}
670
+
296
671
  <input
297
672
  type="text"
298
673
  placeholder={field.placeholder || `Search ${field.label}`}
299
674
  onChange={handleInputChange}
300
675
  disabled={loading}
676
+ className="search-input"
301
677
  />
678
+
302
679
  <select
303
680
  multiple={isMulti}
304
- value={value}
305
- onChange={e => {
681
+ value={isMulti ? (value || []).map((v) => v.value) : value?.value || ""}
682
+ onChange={(e) => {
306
683
  if (isMulti) {
307
- const selected = Array.from(e.target.selectedOptions, o => o.value);
308
- onChange(selected);
684
+ const selectedOptions = Array.from(e.target.selectedOptions).map(
685
+ (option) => {
686
+ const opt = options.find((o) => o.value === option.value);
687
+ return { label: opt.label, value: opt.value };
688
+ }
689
+ );
690
+ handleSelectChange(selectedOptions);
309
691
  } else {
310
- onChange(e.target.value);
692
+ const selectedOption = options.find(
693
+ (o) => o.value === e.target.value
694
+ );
695
+ handleSelectChange(selectedOption ? [selectedOption] : []);
311
696
  }
312
697
  }}
313
698
  disabled={loading}
699
+ className="select-field"
314
700
  >
315
- {options.map(opt => (
316
- <option key={opt.value} value={opt.value}>{opt.label}</option>
701
+ {options.map((opt) => (
702
+ <option key={opt.value} value={opt.value}>
703
+ {opt.label}
704
+ </option>
317
705
  ))}
318
706
  </select>
319
- {loading && <div>Loading options...</div>}
707
+
708
+ {loading && <div className="loading">Loading options...</div>}
709
+
320
710
  {hasNext && !loadingMore && (
321
- <button type="button" onClick={handleLoadMore}>Load More</button>
711
+ <button
712
+ type="button"
713
+ onClick={handleLoadMore}
714
+ className="load-more-btn"
715
+ >
716
+ Load More
717
+ </button>
718
+ )}
719
+
720
+ {loadingMore && <div className="loading">Loading more...</div>}
721
+
722
+ <div className="options-info">
723
+ Loaded {totalLoadedOptions} options{hasNext ? "" : " (all loaded)"}
724
+ </div>
725
+
726
+ {field.description && (
727
+ <div className="field-description">{field.description}</div>
322
728
  )}
323
- {loadingMore && <div>Loading more...</div>}
324
- <div>Loaded {totalLoadedOptions} options{hasNext ? '' : ' (all loaded)'}</div>
325
- {field.description && <div style={{ fontSize: 'smaller', color: '#666' }}>{field.description}</div>}
326
729
  </div>
327
730
  );
328
731
  }
329
732
  ```
330
733
 
331
- **useFieldOptions Return Type:**
332
-
333
- ```ts
334
- interface UseFieldOptionsReturn {
335
- options: Array<{ label: string; value: string }>;
336
- loading: boolean;
337
- loadingMore: boolean;
338
- hasNext: boolean;
339
- loadMore: () => Promise<void>;
340
- error: string | null;
341
- search: (query: string) => void;
342
- refresh: () => void;
343
- totalLoadedOptions: number;
344
- }
345
- ```
734
+ ### **Google Drive Picker Fields**
346
735
 
347
- #### Google Drive File Picker Field
348
-
349
- For fields requiring Google Drive file selection, the field type will be `google-files-picker-select` or similar. These fields provide an `optionsSource` with a method to open the Google Files Picker dialog.
350
-
351
- **Example: GoogleFilesPickerField Component**
736
+ For Google Drive file picker fields, handle the file selection flow. These fields also work with `{ label, value }` objects:
352
737
 
353
738
  ```tsx
354
739
  function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
@@ -357,27 +742,47 @@ function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
357
742
  await field.optionsSource.openGoogleFilesPicker({
358
743
  onComplete: async (files) => {
359
744
  if (isMulti) {
360
- onChange(files);
745
+ // For multi-select, ensure we have an array of { label, value } objects
746
+ const formattedFiles = files.map((file) => ({
747
+ label: file.label || file.name || file.value,
748
+ value: file.value || file.id,
749
+ }));
750
+ onChange(formattedFiles);
361
751
  } else {
362
- onChange(files[0]);
752
+ // For single select, ensure we have a single { label, value } object
753
+ const formattedFile = {
754
+ label: files[0]?.label || files[0]?.name || files[0]?.value,
755
+ value: files[0]?.value || files[0]?.id,
756
+ };
757
+ onChange(formattedFile);
363
758
  }
364
759
  },
365
760
  onError: async (pickerError) => {
366
- alert('Google Files Picker error: ' + pickerError);
761
+ console.error("Google Files Picker error:", pickerError);
762
+ alert("Failed to pick files: " + pickerError);
367
763
  },
368
764
  });
369
765
  }
370
766
  }
371
767
 
372
768
  return (
373
- <div>
374
- <label>{field.label}{field.required && ' *'}</label>
375
- <button type="button" onClick={handlePickFiles}>
769
+ <div className="field-container">
770
+ <label className="field-label">
771
+ {field.label}
772
+ {field.required && <span className="required"> *</span>}
773
+ </label>
774
+
775
+ <button
776
+ type="button"
777
+ onClick={handlePickFiles}
778
+ className="google-picker-btn"
779
+ >
376
780
  Pick from Google Drive
377
781
  </button>
782
+
378
783
  {value && (
379
- <div>
380
- <strong>Selected file{isMulti ? 's' : ''}:</strong>
784
+ <div className="selected-files">
785
+ <strong>Selected file{isMulti ? "s" : ""}:</strong>
381
786
  <ul>
382
787
  {(isMulti ? value : [value]).map((file, idx) => (
383
788
  <li key={file.value || idx}>{file.label || file.value}</li>
@@ -385,66 +790,264 @@ function GoogleFilesPickerField({ field, value, onChange, isMulti = false }) {
385
790
  </ul>
386
791
  </div>
387
792
  )}
388
- {field.description && <div style={{ fontSize: 'smaller', color: '#666' }}>{field.description}</div>}
793
+
794
+ {field.description && (
795
+ <div className="field-description">{field.description}</div>
796
+ )}
389
797
  </div>
390
798
  );
391
799
  }
392
800
  ```
393
801
 
394
- **GoogleFilesPicker Return Type:**
395
-
396
- ```ts
397
- interface UseGoogleFilesPickerReturn {
398
- options: Array<{ label: string; value: string }>;
399
- loading: boolean;
400
- loadingMore: boolean;
401
- hasNext: boolean;
402
- query: string;
403
- refresh: () => Promise<void>;
404
- search: (query: string) => void;
405
- loadMore: () => Promise<void>;
406
- totalLoadedOptions: number;
407
- error: string | null;
802
+ ### **Generic Form Field Component**
803
+
804
+ Create a reusable component that handles different field types with proper value handling:
805
+
806
+ ```tsx
807
+ function FormField({ field, value, onChange }) {
808
+ switch (field.type) {
809
+ case "text":
810
+ case "email":
811
+ case "password":
812
+ case "number":
813
+ return (
814
+ <div className="field-container">
815
+ <label className="field-label">
816
+ {field.label}
817
+ {field.required && <span className="required"> *</span>}
818
+ </label>
819
+ <input
820
+ type={field.type}
821
+ value={value || ""}
822
+ onChange={(e) => onChange(e.target.value)}
823
+ placeholder={field.placeholder}
824
+ disabled={field.disabled}
825
+ className="text-input"
826
+ />
827
+ {field.description && (
828
+ <div className="field-description">{field.description}</div>
829
+ )}
830
+ </div>
831
+ );
832
+
833
+ case "checkbox":
834
+ return (
835
+ <div className="field-container">
836
+ <label className="field-label">
837
+ <input
838
+ type="checkbox"
839
+ checked={value || false}
840
+ onChange={(e) => onChange(e.target.checked)}
841
+ disabled={field.disabled}
842
+ className="checkbox-input"
843
+ />
844
+ {field.label}
845
+ {field.required && <span className="required"> *</span>}
846
+ </label>
847
+ {field.description && (
848
+ <div className="field-description">{field.description}</div>
849
+ )}
850
+ </div>
851
+ );
852
+
853
+ case "select":
854
+ return (
855
+ <SelectField
856
+ field={field}
857
+ value={value}
858
+ onChange={onChange}
859
+ isMulti={false}
860
+ />
861
+ );
862
+
863
+ case "multi-select":
864
+ return (
865
+ <SelectField
866
+ field={field}
867
+ value={value}
868
+ onChange={onChange}
869
+ isMulti={true}
870
+ />
871
+ );
872
+
873
+ case "google-files-picker-select":
874
+ return (
875
+ <GoogleFilesPickerField
876
+ field={field}
877
+ value={value}
878
+ onChange={onChange}
879
+ isMulti={false}
880
+ />
881
+ );
882
+
883
+ case "google-files-picker-multi-select":
884
+ return (
885
+ <GoogleFilesPickerField
886
+ field={field}
887
+ value={value}
888
+ onChange={onChange}
889
+ isMulti={true}
890
+ />
891
+ );
892
+
893
+ default:
894
+ return (
895
+ <div className="field-container">
896
+ <label className="field-label">
897
+ {field.label} (Unsupported type: {field.type})
898
+ </label>
899
+ </div>
900
+ );
901
+ }
408
902
  }
409
903
  ```
410
904
 
411
- **Integration Tips:**
412
- - Use these fields as controlled components in your form library (Formik, React Hook Form, etc.).
413
- - Manage `value` and `onChange` via your form state.
414
- - Combine with other input types to build a complete dynamic configuration form.
415
-
416
905
  ---
417
906
 
418
- ## Error Handling & Troubleshooting
907
+ ### **Error Handling and Loading States**
908
+
909
+ ```tsx
910
+ function ConnectorManager() {
911
+ const { data: connectors, isLoading, error, refetch } = useConnectors();
912
+ const [retryCount, setRetryCount] = useState(0);
913
+
914
+ const handleRetry = () => {
915
+ setRetryCount((prev) => prev + 1);
916
+ refetch();
917
+ };
918
+
919
+ if (isLoading) {
920
+ return (
921
+ <div className="loading-container">
922
+ <div className="spinner"></div>
923
+ <p>Loading your integrations...</p>
924
+ </div>
925
+ );
926
+ }
927
+
928
+ if (error) {
929
+ return (
930
+ <div className="error-container">
931
+ <h3>Failed to load integrations</h3>
932
+ <p>{error.message}</p>
933
+ <button onClick={handleRetry} className="retry-btn">
934
+ Retry ({retryCount} attempts)
935
+ </button>
936
+ </div>
937
+ );
938
+ }
939
+
940
+ return (
941
+ <div className="connector-list">
942
+ {connectors?.map((connector) => (
943
+ <ConnectorCard key={connector.id} connector={connector} />
944
+ ))}
945
+ </div>
946
+ );
947
+ }
948
+ ```
949
+
950
+ ```tsx
951
+ function ConfigurationActions({ config }) {
952
+ const queryClient = useQueryClient();
953
+
954
+ const handleAction = async (action) => {
955
+ // Optimistically update the UI
956
+ queryClient.setQueryData(["configurations"], (oldData) => {
957
+ return oldData?.map((c) =>
958
+ c.id === config.id
959
+ ? {
960
+ ...c,
961
+ status: action.actionType === "ENABLE" ? "ENABLED" : "DISABLED",
962
+ }
963
+ : c
964
+ );
965
+ });
966
+
967
+ try {
968
+ const result = await action.onClick();
969
+ if (result?.status === "SUCCESS") {
970
+ // Invalidate and refetch to ensure consistency
971
+ queryClient.invalidateQueries(["configurations"]);
972
+ }
973
+ } catch (error) {
974
+ // Revert optimistic update on error
975
+ queryClient.invalidateQueries(["configurations"]);
976
+ console.error("Action failed:", error);
977
+ }
978
+ };
419
979
 
420
- - **Provider Not Found:** Ensure your components are wrapped in `FastnProvider`.
421
- - **Authentication Errors:** Check your `authToken`, `tenantId`, and `spaceId`.
422
- - **Network Errors:** Verify your network and API base URL.
423
- - **TypeScript Errors:** Import types from the package as needed.
980
+ return (
981
+ <div className="config-actions">
982
+ {config.actions?.map((action) => (
983
+ <button
984
+ key={action.name}
985
+ onClick={() => handleAction(action)}
986
+ className={`action-btn ${action.actionType.toLowerCase()}`}
987
+ >
988
+ {action.name}
989
+ </button>
990
+ ))}
991
+ </div>
992
+ );
993
+ }
994
+ ```
424
995
 
425
996
  ---
426
997
 
427
- ## TypeScript Support
998
+ ## 🚨 Troubleshooting
999
+
1000
+ ### **Common Issues**
428
1001
 
429
- All hooks and data structures are fully typed. You can import types directly:
1002
+ 1. **"Invalid tenant ID" error**
430
1003
 
431
- ```ts
432
- import type { Connector, Configuration, ConnectorAction, ConfigurationAction } from '@fastn-ai/react-core';
1004
+ - Ensure your `tenantId` is a valid string and matches your user/organization identifier
1005
+ - Check that the tenant has proper permissions in your Fastn space
1006
+
1007
+ 2. **"Space not found" error**
1008
+
1009
+ - Verify your `spaceId` is correct
1010
+ - Ensure your auth token has access to the specified space
1011
+
1012
+ 3. **Configuration form not loading**
1013
+
1014
+ - Check that the `configurationId` is valid and exists
1015
+ - Ensure the connector is properly activated before trying to configure it
1016
+
1017
+ 4. **Google Drive picker not working**
1018
+ - Verify Google Drive connector is properly configured in your Fastn space
1019
+ - Check that the user has granted necessary permissions
1020
+
1021
+ ### **Debug Mode**
1022
+
1023
+ Enable debug logging to troubleshoot issues:
1024
+
1025
+ ```tsx
1026
+ const fastnConfig = {
1027
+ environment: "LIVE",
1028
+ authToken: "your-auth-token",
1029
+ tenantId: "your-tenant-id",
1030
+ spaceId: "your-space-id",
1031
+ debug: true, // Enable debug logging
1032
+ };
433
1033
  ```
434
1034
 
435
1035
  ---
436
1036
 
437
- ## License
1037
+ ## 📚 Additional Resources
438
1038
 
439
- MIT License. See the [LICENSE](LICENSE) file for details.
1039
+ - [Fastn.ai Documentation](https://docs.fastn.ai/)
1040
+ - [React Query Documentation](https://tanstack.com/query/latest)
1041
+ - [Fastn Community](https://community.fastn.ai/)
440
1042
 
441
1043
  ---
442
1044
 
443
- ## Support
1045
+ ## 🤝 Contributing
444
1046
 
445
- - Email: support@fastn.ai
446
- - Documentation: [https://docs.fastn.ai](https://docs.fastn.ai)
1047
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
447
1048
 
448
1049
  ---
449
1050
 
450
- 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.
1051
+ ## 📄 License
1052
+
1053
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.