@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/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.