# [Fastn.ai](http://fastn.ai/) React Core Documentation

A React library for integrating **Fastn AI connectors** into your application. It provides powerful hooks to manage:

- Connector listing, activation, and deactivation
- Configuration form rendering and submission
- Seamless integration with React Query for state management

This enables you to **build fully custom UIs** on top of Fastn's powerful data and logic engine.

---

## 📦 Installation

Install the core library:

```bash
npm install @fastn-ai/react-core
```

Also, make sure you install the required **peer dependencies**:

```bash
npm install react react-dom @tanstack/react-query
```

> ✅ Requires React 18+

---

## 🏗️ Fastn Architecture Concepts

Before diving into the code, let's understand the key Fastn concepts and terminology:

### **Space (Workspace)**

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.

### **Tenant**

A **Tenant** represents a user, team, or organization within your application. Each tenant has isolated data and configurations. For example:

- A single user account
- A team within your app
- An organization or company
- A client's workspace

### **Connector**

A **Connector** represents an integration with an external service (like Slack, Google Drive, etc.). Connectors define what external services your app can connect to.

### **Configuration**

A **Configuration** is a specific instance of a connector with saved settings and authentication. For example:

- A Slack workspace connection with specific channels selected
- A Google Drive connection with specific folders configured
- A database connection with connection parameters

### **Configuration ID**

A **Configuration ID** is a unique identifier that represents a specific configuration instance. This ID is used to:

- Load existing configurations
- Update configuration settings
- Manage the lifecycle of a specific integration

---

## ⚙️ Features

- **Connector Management**: List, activate, and deactivate connectors
- **Tenant Isolation**: Each tenant has its own isolated connector state and configurations
- **Configuration Persistence**: Save and retrieve configurations using unique `configurationId`s
- **Dynamic Forms**: Render configuration forms using Fastn's form schema
- **React Query Integration**: Built-in support for efficient caching and request handling
- **Authentication Flow**: Handle OAuth and custom authentication flows seamlessly

---

## 🚀 Getting Started

### 1. **Wrap Your App with `FastnProvider`**

This sets up Fastn in your app and gives you access to its hooks and logic.

```tsx
import { FastnProvider } from "@fastn-ai/react-core";

const fastnConfig = {
  environment: "LIVE", // "LIVE", "DRAFT", or a custom environment string for widgets
  authToken: "your-auth-token", // Your app's access token, authenticated through Fastn Custom Auth
  tenantId: "your-tenant-id", // A unique ID representing the user, team, or organization
  spaceId: "your-space-id", // Fastn Space ID (also called Workspace ID) - groups all connectors and configurations
};

function App() {
  return (
    <FastnProvider config={fastnConfig}>
      {/* Your app components */}
    </FastnProvider>
  );
}
```

### 🔍 Configuration Field Reference

| Field         | Description                                                                                                                  |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `environment` | The widget environment: use `"LIVE"` for production, `"DRAFT"` for preview/testing, or any custom string configured in Fastn |
| `authToken`   | The token from your authentication flow. Fastn uses this to authenticate the user via the **fastnCustomAuth** flow           |
| `tenantId`    | A unique identifier for the current user or organization. This helps Fastn isolate data per tenant                           |
| `spaceId`     | The Fastn **Space ID**, also called the Workspace ID. It groups all connectors, configurations, and flows                    |

---

### 2. **Use an Existing React Query Client (Optional)**

If your app already uses React Query, you can pass your own client:

```tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { FastnProvider } from "@fastn-ai/react-core";

const queryClient = new QueryClient();

const fastnConfig = {
  environment: "LIVE",
  authToken: "your-auth-token",
  tenantId: "your-tenant-id",
  spaceId: "your-space-id",
};

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <FastnProvider config={fastnConfig}>
        {/* Your app components */}
      </FastnProvider>
    </QueryClientProvider>
  );
}
```

---

## 🧩 Core Hooks & Types

### **Connector Types**

```tsx
interface Connector {
  id: string;
  name: string;
  description: string;
  imageUri?: string;
  status: ConnectorStatus;
  actions: ConnectorAction[];
}

enum ConnectorStatus {
  ACTIVE = "ACTIVE",
  INACTIVE = "INACTIVE",
  ALL = "ALL",
}

interface ConnectorAction {
  name: string;
  actionType: ConnectorActionType | string;
  onClick?: () => Promise<ConnectorActionResult>;
}

interface ConnectorActionResult {
  status: "SUCCESS" | "ERROR" | "CANCELLED";
}

enum ConnectorActionType {
  ACTIVATION = "ACTIVATION",
  DEACTIVATION = "DEACTIVATION",
  NONE = "NONE",
}
```

### **Configuration Types**

```tsx
interface Configuration {
  id: string;
  connectorId: string;
  name: string;
  description: string;
  imageUri?: string;
  status: ConfigurationStatus;
  actions: ConfigurationAction[];
}

enum ConfigurationStatus {
  ENABLED = "ENABLED",
  DISABLED = "DISABLED",
  PENDING = "PENDING",
}

interface ConfigurationAction {
  name: string;
  actionType: ConfigurationActionType | string;
  onClick?: () => Promise<ConfigurationActionResult>;
}

interface ConfigurationActionResult {
  status: "SUCCESS" | "ERROR" | "CANCELLED";
}

enum ConfigurationActionType {
  ENABLE = "ENABLE",
  DISABLE = "DISABLE",
  DELETE = "DELETE",
  UPDATE = "UPDATE",
}
```

### **Configuration Form Types**

```tsx
type Primitive = string | number | boolean | null | undefined;
interface ConfigurationForm {
  name: string;
  description: string;
  imageUri: string;
  fields: ConnectorField[];
  submitHandler: (formData: FormData) => Promise<void>;
}

type FormData =
  | Record<
      string,
      Record<string, Primitive> | Record<string, Primitive>[] | undefined | null
    >
  | Record<
      string,
      Record<string, Primitive> | Record<string, Primitive>[] | undefined | null
    >[]
  | undefined
  | null;

interface ConnectorField {
  readonly name: string;
  readonly key: string;
  readonly label: string;
  readonly type: ConnectorFieldType | string;
  readonly required: boolean;
  readonly placeholder: string;
  readonly description: string;
  readonly hidden?: boolean;
  readonly disabled?: boolean;
  readonly initialValue?:
    | Record<string, Primitive>
    | Record<string, Primitive>[]
    | Primitive
    | Primitive[];
  readonly optionsSource?: SelectOptionSource;
  readonly configs?: ConnectorFieldConfig;
}

interface ConnectorFieldConfig {
  readonly selection?: SelectionConfig;
  readonly disable?: boolean;
  readonly label?: string;
  readonly hideOption?: HideOptionConfig;
}

interface SelectionConfig {
  readonly enable: boolean;
  readonly flowId: string;
  readonly sameProject: boolean;
  readonly list: readonly any[];
  readonly type: "OFFSET" | "CURSOR";
  readonly offset: number;
  readonly isIndependent: boolean;
  readonly fileTypes: readonly string[];
  readonly source: FlowConfig;
  readonly destination: FlowConfig;
  readonly isEditKeys: boolean;
  readonly isAddFields: boolean;
}

interface FlowConfig {
  readonly flowId: string;
  readonly isSameProject: boolean;
}

interface HideOptionConfig {
  readonly enable: boolean;
  readonly hideBasedOnValue: boolean;
  readonly key: string;
  readonly operation: "!=" | "==" | ">" | "<" | ">=" | "<=" | "contains" | "not_contains";
  readonly value: string;
}
```

### Configuration Usage Example

Here's an example of how the configuration types work with connector fields in React:

```tsx
// Example configuration structure
const fieldConfig: ConnectorFieldConfig = {
  selection: {
    enable: true,
    flowId: "asanaGetWorkspaces",
    sameProject: false,
    list: [],
    type: "OFFSET",
    offset: 10,
    isIndependent: false,
    fileTypes: [],
    source: {
      flowId: "",
      isSameProject: false
    },
    destination: {
      flowId: "",
      isSameProject: false
    },
    isEditKeys: false,
    isAddFields: false
  },
  disable: false,
  label: "Select Workspace",
  hideOption: {
    enable: false,
    hideBasedOnValue: false,
    key: "",
    operation: "!=",
    value: ""
  }
};

// Example connector field with configuration
const connectorField: ConnectorField = {
  name: "workspace",
  key: "workspace",
  label: "Workspace",
  type: "select",
  required: true,
  placeholder: "Select a workspace",
  description: "Choose the workspace to connect to",
  configs: fieldConfig
};

// Using the field in a React component
function WorkspaceField({ field }: { field: ConnectorField }) {
  const { configs } = field;
  
  // Access configuration properties
  const isSelectionEnabled = configs?.selection?.enable;
  const flowId = configs?.selection?.flowId;
  const isDisabled = configs?.disable;
  
  return (
    <div className="field-container">
      <label>{field.label}</label>
      {isSelectionEnabled && (
        <p>Selection enabled for flow: {flowId}</p>
      )}
      <select disabled={isDisabled}>
        {/* Field options */}
      </select>
    </div>
  );
}
```

---

## 🔄 Complete Integration Workflows

### **Workflow 1: Setting Up Your First Slack Integration**

Let's walk through a complete example of setting up a Slack integration from scratch.

#### Step 1: List Available Connectors

First, show users what connectors are available:

```tsx
import { useConnectors } from "@fastn-ai/react-core";

function ConnectorList() {
  const { data: connectors, isLoading, error } = useConnectors();

  if (isLoading) return <div>Loading available integrations...</div>;
  if (error) return <div>Error loading connectors: {error.message}</div>;

  return (
    <div className="connector-grid">
      <h2>Available Integrations</h2>
      {connectors?.map((connector) => (
        <div key={connector.id} className="connector-card">
          <img src={connector.imageUri} alt={connector.name} />
          <h3>{connector.name}</h3>
          <p>{connector.description}</p>
          
          {connector.status === "ACTIVE" && (
            <span className="status-badge connected">Connected</span>
          )}
          
          {connector.actions?.map((action) => (
            <button 
              key={action.name}
              onClick={action.onClick}
              className={`action-btn ${action.actionType.toLowerCase()}`}
            >
              {action.name}
            </button>
          ))}
        </div>
      ))}
    </div>
  );
}
```

#### Step 2: List Configurations After Connector Activation

After a connector is activated, you can list its configurations:

```tsx
import { useConfigurations } from "@fastn-ai/react-core";

function ConfigurationList({ configurationId }) {
  const { data: configurations, isLoading, error } = useConfigurations({ configurationId });
  const [selectedConfig, setSelectedConfig] = useState(null);

  if (isLoading) return <div>Loading configurations...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div className="configuration-list">
      <h2>Your Integrations</h2>
      {configurations?.map((config) => (
        <div key={config.id} className="config-card">
          <div className="config-info">
            <img src={config.imageUri} alt={config.name} />
            <div>
              <h3>{config.name}</h3>
              <p>{config.description}</p>
            </div>
          </div>
          
          <div className="config-actions">
            {config.status === "ENABLED" && (
              <span className="status-badge enabled">Active</span>
            )}
            
            {config.actions?.map((action) => (
              <button
                key={action.name}
                onClick={async () => {
                  const result = await action.onClick();
                  if (action.actionType === "ENABLE" && result?.status === "SUCCESS") {
                    // Show configuration form for new setup
                    setSelectedConfig(config);
                  } else if (action.actionType === "UPDATE" && result?.status === "SUCCESS") {
                    // Show configuration form for editing
                    setSelectedConfig(config);
                  }
                }}
                className={`action-btn ${action.actionType.toLowerCase()}`}
              >
                {action.name}
              </button>
            ))}
          </div>
        </div>
      ))}
      
      {selectedConfig && (
        <ConfigurationForm 
          configurationId={selectedConfig.id}
          onClose={() => setSelectedConfig(null)}
        />
      )}
    </div>
  );
}
```

#### Step 3: Load Configuration Form

When a configuration is selected (either for new setup or editing), show the configuration form:

```tsx
import { useConfigurationForm } from "@fastn-ai/react-core";

function ConfigurationForm({ configurationId, onClose }) {
  const {
    data: configurationForm,
    isLoading,
    error,
    handleSubmit,
  } = useConfigurationForm({ configurationId });

  const [formData, setFormData] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Pre-populate form with existing values if editing
  useEffect(() => {
    if (configurationForm?.fields) {
      const initialData = {};
      configurationForm.fields.forEach((field) => {
        if (field.initialValue !== undefined) {
          initialData[field.key] = field.initialValue;
        }
      });
      setFormData(initialData);
    }
  }, [configurationForm]);

  if (isLoading) return <div>Loading configuration form...</div>;
  if (error) return <div>Error: {error.message}</div>;

  const onSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      await handleSubmit({ formData });
      console.log("Configuration saved successfully!");
      onClose();
    } catch (error) {
      console.error("Failed to save configuration:", error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="modal">
      <form onSubmit={onSubmit} className="configuration-form">
        <h2>Configure {configurationForm.name}</h2>
        <p>{configurationForm.description}</p>
        
        {configurationForm.fields.map((field) => (
          <FormField 
            key={field.key}
            field={field}
            value={formData[field.key]}
            onChange={(value) =>
              setFormData((prev) => ({ ...prev, [field.key]: value }))
            }
          />
        ))}
        
        <div className="form-actions">
          <button type="button" onClick={onClose}>Cancel</button>
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? "Saving..." : "Save Configuration"}
          </button>
        </div>
      </form>
    </div>
  );
}
```

### **Workflow 2: Managing Existing Configurations**

Now let's show how to manage existing configurations - viewing, editing, and disabling them.

#### Step 1: List Existing Configurations

```tsx
import { useConfigurations } from "@fastn-ai/react-core";

function ConfigurationManager({ configurationId }) {
  const {
    data: configurations,
    isLoading,
    error,
  } = useConfigurations({ configurationId });
  const [selectedConfig, setSelectedConfig] = useState(null);

  if (isLoading) return <div>Loading your integrations...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div className="configuration-manager">
      <h2>Your Integrations</h2>

      {configurations?.map((config) => (
        <div key={config.id} className="config-card">
          <div className="config-info">
            <img src={config.imageUri} alt={config.name} />
            <div>
              <h3>{config.name}</h3>
              <p>{config.description}</p>
            </div>
          </div>

          <div className="config-actions">
            {config.status === "ENABLED" && (
              <span className="status-badge enabled">Active</span>
            )}

            {config.actions?.map((action) => (
              <button
                key={action.name}
                onClick={async () => {
                  const result = await action.onClick();
                  if (
                    action.actionType === "UPDATE" &&
                    result?.status === "SUCCESS"
                  ) {
                    setSelectedConfig(config);
                  }
                }}
                className={`action-btn ${action.actionType.toLowerCase()}`}
              >
                {action.name}
              </button>
            ))}
          </div>
        </div>
      ))}

      {selectedConfig && (
        <ConfigurationForm
          configurationId={selectedConfig.id}
          onClose={() => setSelectedConfig(null)}
        />
      )}
    </div>
  );
}
```

#### Step 2: Disable Configuration

```tsx
function ConfigActions({ config }) {
  const handleDisable = async (action) => {
    if (action.actionType === "DISABLE") {
      const result = await action.onClick();
      if (result?.status === "SUCCESS") {
        console.log("Configuration disabled successfully");
        // Refresh the configuration list
      }
    }
  };

  return (
    <div className="config-actions">
      {config.actions?.map((action) => (
        <button
          key={action.name}
          onClick={() => handleDisable(action)}
          className={`action-btn ${action.actionType.toLowerCase()}`}
        >
          {action.name}
        </button>
      ))}
    </div>
  );
}
```

#### Step 3: Form Field Value Handling

The form fields handle different value types based on the field type:

- **Select fields**: Always return `{ label: string, value: string }` objects
- **Multi-select fields**: Always return `{ label: string, value: string }[]` arrays
- **Google Drive picker fields**: Always return `{ label: string, value: string }` objects or arrays
- **Other fields**: Return primitive values (string, number, boolean)

```tsx
// Example form data structure
const formData = {
  // Select field - single object
  channel: { label: "General", value: "C123456" },
  
  // Multi-select field - array of objects
  channels: [
    { label: "General", value: "C123456" },
    { label: "Random", value: "C789012" }
  ],
  
  // Google Drive picker - single object
  folder: { label: "My Documents", value: "folder_id_123" },
  
  // Google Drive picker multi - array of objects
  files: [
    { label: "document1.pdf", value: "file_id_1" },
    { label: "document2.pdf", value: "file_id_2" }
  ],
  
  // Text field - primitive
  webhookUrl: "https://hooks.slack.com/...",
  
  // Boolean field - primitive
  enableNotifications: true
};
```

---

## 🎨 Form Field Components

### **Select and Multi-Select Fields**

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:

```tsx
import { useFieldOptions } from "@fastn-ai/react-core";

function SelectField({ field, value, onChange, isMulti = false, context = {} }) {
  // context contains all form values and is used to fetch dependent options
  const {
    options,
    loading,
    loadingMore,
    hasNext,
    loadMore,
    error,
    search,
    totalLoadedOptions,
  } = useFieldOptions(field, context);

  function handleInputChange(e) {
    search(e.target.value);
  }

  function handleLoadMore() {
    if (hasNext && !loadingMore) loadMore();
  }

  function handleSelectChange(selectedOptions) {
    if (isMulti) {
      // For multi-select, value should be an array of { label, value } objects
      const selectedValues = selectedOptions.map(option => ({
        label: option.label,
        value: option.value
      }));
      onChange(selectedValues);
    } else {
      // For single select, value should be a single { label, value } object
      const selectedValue = selectedOptions[0] ? {
        label: selectedOptions[0].label,
        value: selectedOptions[0].value
      } : null;
      onChange(selectedValue);
    }
  }

  return (
    <div className="field-container">
      <label className="field-label">
        {field.label}
        {field.required && <span className="required"> *</span>}
      </label>
      
      {error && (
        <div className="error-message">
          Error loading options: {error.message}
        </div>
      )}
      
      <input
        type="text"
        placeholder={field.placeholder || `Search ${field.label}`}
        onChange={handleInputChange}
        disabled={loading}
        className="search-input"
      />
      
      <select
        multiple={isMulti}
        value={isMulti ? (value || []).map(v => v.value) : (value?.value || '')}
        onChange={(e) => {
          if (isMulti) {
            const selectedOptions = Array.from(e.target.selectedOptions).map(option => {
              const opt = options.find(o => o.value === option.value);
              return { label: opt.label, value: opt.value };
            });
            handleSelectChange(selectedOptions);
          } else {
            const selectedOption = options.find(o => o.value === e.target.value);
            handleSelectChange(selectedOption ? [selectedOption] : []);
          }
        }}
        disabled={loading}
        className="select-field"
      >
        {options.map((opt) => (
          <option key={opt.value} value={opt.value}>
            {opt.label}
          </option>
        ))}
      </select>
      
      {loading && <div className="loading">Loading options...</div>}
      
      {hasNext && !loadingMore && (
        <button
          type="button"
          onClick={handleLoadMore}
          className="load-more-btn"
        >
          Load More
        </button>
      )}
      
      {loadingMore && <div className="loading">Loading more...</div>}
      
      <div className="options-info">
        Loaded {totalLoadedOptions} options{hasNext ? "" : " (all loaded)"}
      </div>
      
      {field.description && (
        <div className="field-description">{field.description}</div>
      )}
    </div>
  );
}
```

### **Google Drive Picker Fields**

For Google Drive file picker fields, handle the file selection flow. These fields also work with `{ label, value }` objects and support file type filtering:

```tsx
function GoogleFilesPickerField({ field, value, onChange, isMulti = false, context = {} }) {
  async function handlePickFiles() {
    if (field.optionsSource?.openGoogleFilesPicker) {
      await field.optionsSource.openGoogleFilesPicker({
        onComplete: async (files) => {
          if (isMulti) {
            // For multi-select, ensure we have an array of { label, value } objects
            const formattedFiles = files.map(file => ({
              label: file.label || file.name || file.value,
              value: file.value || file.id
            }));
            onChange(formattedFiles);
          } else {
            // For single select, ensure we have a single { label, value } object
            const formattedFile = {
              label: files[0]?.label || files[0]?.name || files[0]?.value,
              value: files[0]?.value || files[0]?.id
            };
            onChange(formattedFile);
          }
        },
        onError: async (pickerError) => {
          console.error("Google Files Picker error:", pickerError);
          alert("Failed to pick files: " + pickerError);
        },
        fileTypes: field?.optionsSource?.fileTypes,
      });
    }
  }

  return (
    <div className="field-container">
      <label className="field-label">
        {field.label}
        {field.required && <span className="required"> *</span>}
      </label>
      
      <button 
        type="button" 
        onClick={handlePickFiles}
        className="google-picker-btn"
      >
        Pick from Google Drive
      </button>
      
      {value && (
        <div className="selected-files">
          <strong>Selected file{isMulti ? "s" : ""}:</strong>
          <ul>
            {(isMulti ? value : [value]).map((file, idx) => (
              <li key={file.value || idx}>{file.label || file.value}</li>
            ))}
          </ul>
        </div>
      )}
      
      {field.description && (
        <div className="field-description">{field.description}</div>
      )}
    </div>
  );
}
```

### **Generic Form Field Component**

Create a reusable component that handles different field types with proper value handling:

```tsx
function FormField({ field, value, onChange, context = {} }) {
  switch (field.type) {
    case "text":
    case "email":
    case "password":
    case "number":
      return (
        <div className="field-container">
          <label className="field-label">
            {field.label}
            {field.required && <span className="required"> *</span>}
          </label>
          <input
            type={field.type}
            value={value || ""}
            onChange={(e) => onChange(e.target.value)}
            placeholder={field.placeholder}
            disabled={field.disabled}
            className="text-input"
          />
          {field.description && (
            <div className="field-description">{field.description}</div>
          )}
        </div>
      );
      
    case "checkbox":
      return (
        <div className="field-container">
          <label className="field-label">
            <input
              type="checkbox"
              checked={value || false}
              onChange={(e) => onChange(e.target.checked)}
              disabled={field.disabled}
              className="checkbox-input"
            />
            {field.label}
            {field.required && <span className="required"> *</span>}
          </label>
          {field.description && (
            <div className="field-description">{field.description}</div>
          )}
        </div>
      );
      
    case "select":
      return (
        <SelectField 
          field={field} 
          value={value} 
          onChange={onChange} 
          isMulti={false}
          context={context}
        />
      );
      
    case "multi-select":
      return (
        <SelectField 
          field={field} 
          value={value} 
          onChange={onChange} 
          isMulti={true}
          context={context}
        />
      );
      
    case "google-files-picker-select":
      return (
        <GoogleFilesPickerField 
          field={field} 
          value={value} 
          onChange={onChange} 
          isMulti={false}
          context={context}
        />
      );
      
    case "google-files-picker-multi-select":
      return (
        <GoogleFilesPickerField 
          field={field} 
          value={value} 
          onChange={onChange} 
          isMulti={true}
          context={context}
        />
      );
      
    default:
      return (
        <div className="field-container">
          <label className="field-label">
            {field.label} (Unsupported type: {field.type})
          </label>
        </div>
      );
  }
}
```

---

## 🔗 Field Context Passing

The React Core library supports **dynamic field dependencies** where form fields can access and respond to values from other fields in real-time. This enables complex form workflows where the options or behavior of one field depends on the selection made in another field.

### **How Context Passing Works**

When a user selects a value in one field, that value becomes available to all other fields through the `context` parameter. This allows dependent fields to:

- Load different options based on the parent field's selection
- Show/hide fields based on other field values
- Update their behavior dynamically

### **Implementation Example**

Here's how to implement context passing in your form components:

```tsx
import { useConfigurationForm } from "@fastn-ai/react-core";

function ConfigurationForm({ configurationId, connectorId, configuration }) {
  const { data: configurationForm, isLoading, error } = useConfigurationForm({
    configurationId,
    connectorId,
    configuration,
  });

  return (
    <Formik
      initialValues={initialValues}
      onSubmit={handleSubmit}
    >
      {({ values, setFieldValue, setFieldTouched, errors, touched }) => (
        <Form>
          {configurationForm.fields
            .filter((field) => !field.hidden)
            .map((field) => (
              <FormField
                key={field.name}
                field={field}
                form={{ values, errors, touched }}
                setFieldValue={setFieldValue}
                setFieldTouched={setFieldTouched}
                context={values} // Pass all form values as context
              />
            ))}
        </Form>
      )}
    </Formik>
  );
}

// FormField component that passes context to individual fields
function FormField({ field, form, setFieldValue, setFieldTouched, context }) {
  const handleChange = useCallback(
    (value) => {
      setFieldValue(field.name, value);
      setFieldTouched(field.name, true);
    },
    [field.name, setFieldValue, setFieldTouched]
  );

  switch (field.type) {
    case "select":
    case "multi-select":
      return (
        <SelectField
          field={field}
          onChange={handleChange}
          value={form.values[field.name]}
          isMulti={field.type === "multi-select"}
          context={context} // Pass context to SelectField
        />
      );
    // ... other field types
  }
}

// SelectField component that uses context
function SelectField({ field, onChange, value, isMulti, context }) {
  // The useFieldOptions hook receives the context and can use it
  // to fetch options based on other field values
  const {
    options,
    loading,
    loadingMore,
    hasNext,
    loadMore,
    error,
    search,
    refresh,
    totalLoadedOptions,
  } = useFieldOptions(field, context);

  const handleChange = useCallback(
    (selected) => {
      onChange(selected);
      // When this field changes, the context is automatically updated
      // and passed to all other fields
    },
    [onChange]
  );

  // ... rest of component implementation
}
```

### **Context Structure**

The `context` parameter contains all current form values in the following structure:

```tsx
// Example context object
const context = {
  workspace: { label: "My Workspace", value: "ws_123" },
  channel: { label: "General", value: "ch_456" },
  webhookUrl: "https://hooks.slack.com/...",
  enableNotifications: true,
  // ... other field values
};
```

### **Use Cases**

1. **Workspace → Channel Selection**: When a user selects a workspace, the channel field loads only channels from that workspace.

2. **Database → Table Selection**: When a database is selected, the table field shows only tables from that database.

3. **Conditional Field Display**: Show/hide fields based on other field values.

4. **Dynamic Option Loading**: Load different options based on parent field selections.

### **Best Practices**

1. **Always pass context**: Make sure to pass the `context` parameter to all field components that need access to other field values.

2. **Handle loading states**: When context changes, dependent fields may need to reload their options, so handle loading states appropriately.

3. **Optimize re-renders**: Use `useCallback` and `useMemo` to prevent unnecessary re-renders when context changes.

4. **Error handling**: Handle cases where context-dependent operations fail gracefully.

### **Advanced Example: Multi-level Dependencies**

```tsx
// Example: Workspace → Channel → User selection
function MultiLevelSelectForm() {
  const [formValues, setFormValues] = useState({});

  return (
    <div>
      {/* Workspace selection */}
      <SelectField
        field={workspaceField}
        value={formValues.workspace}
        onChange={(value) => setFormValues(prev => ({ ...prev, workspace: value }))}
        context={formValues}
      />

      {/* Channel selection - depends on workspace */}
      <SelectField
        field={channelField}
        value={formValues.channel}
        onChange={(value) => setFormValues(prev => ({ ...prev, channel: value }))}
        context={formValues} // Has access to workspace value
      />

      {/* User selection - depends on both workspace and channel */}
      <SelectField
        field={userField}
        value={formValues.user}
        onChange={(value) => setFormValues(prev => ({ ...prev, user: value }))}
        context={formValues} // Has access to both workspace and channel values
      />
    </div>
  );
}
```

---

### **Error Handling and Loading States**

```tsx
function ConnectorManager() {
  const { data: connectors, isLoading, error, refetch } = useConnectors();
  const [retryCount, setRetryCount] = useState(0);

  const handleRetry = () => {
    setRetryCount((prev) => prev + 1);
    refetch();
  };

  if (isLoading) {
    return (
      <div className="loading-container">
        <div className="spinner"></div>
        <p>Loading your integrations...</p>
      </div>
    );
  }

  if (error) {
    return (
      <div className="error-container">
        <h3>Failed to load integrations</h3>
        <p>{error.message}</p>
        <button onClick={handleRetry} className="retry-btn">
          Retry ({retryCount} attempts)
        </button>
      </div>
    );
  }

  return (
    <div className="connector-list">
      {connectors?.map((connector) => (
        <ConnectorCard key={connector.id} connector={connector} />
      ))}
    </div>
  );
}
```


```tsx
function ConfigurationActions({ config }) {
  const queryClient = useQueryClient();

  const handleAction = async (action) => {
    // Optimistically update the UI
    queryClient.setQueryData(["configurations"], (oldData) => {
      return oldData?.map((c) =>
        c.id === config.id
          ? {
              ...c,
              status: action.actionType === "ENABLE" ? "ENABLED" : "DISABLED",
            }
          : c
      );
    });

    try {
      const result = await action.onClick();
      if (result?.status === "SUCCESS") {
        // Invalidate and refetch to ensure consistency
        queryClient.invalidateQueries(["configurations"]);
      }
    } catch (error) {
      // Revert optimistic update on error
      queryClient.invalidateQueries(["configurations"]);
      console.error("Action failed:", error);
    }
  };

  return (
    <div className="config-actions">
      {config.actions?.map((action) => (
        <button
          key={action.name}
          onClick={() => handleAction(action)}
          className={`action-btn ${action.actionType.toLowerCase()}`}
        >
          {action.name}
        </button>
      ))}
    </div>
  );
}
```

---
## 🚨 Troubleshooting

### **Common Issues**

1. **"Invalid tenant ID" error**

   - Ensure your `tenantId` is a valid string and matches your user/organization identifier
   - Check that the tenant has proper permissions in your Fastn space

2. **"Space not found" error**

   - Verify your `spaceId` is correct
   - Ensure your auth token has access to the specified space

3. **Configuration form not loading**

   - Check that the `configurationId` is valid and exists
   - Ensure the connector is properly activated before trying to configure it

4. **Google Drive picker not working**
   - Verify Google Drive connector is properly configured in your Fastn space
   - Check that the user has granted necessary permissions

### **Debug Mode**

Enable debug logging to troubleshoot issues:

```tsx
const fastnConfig = {
  environment: "LIVE",
  authToken: "your-auth-token",
  tenantId: "your-tenant-id",
  spaceId: "your-space-id",
  debug: true, // Enable debug logging
};
```

---

## 📚 Additional Resources

- [Fastn.ai Documentation](https://docs.fastn.ai/)
- [React Query Documentation](https://tanstack.com/query/latest)
- [Fastn Community](https://community.fastn.ai/)

---

## 🤝 Contributing

We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.

---

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
