diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index fc102db24..6a41f9937 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4003,3 +4003,32 @@ export function SalesforceIcon(props: SVGProps) { ) } + +export function ApolloIcon(props: SVGProps) { + return ( + + + + + + + ) +} diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 37d6705e3..294e5214e 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -5,6 +5,7 @@ import type { ComponentType, SVGProps } from 'react' import { AirtableIcon, + ApolloIcon, ArxivIcon, AsanaIcon, BrainIcon, @@ -80,78 +81,79 @@ import { type IconComponent = ComponentType> export const blockTypeToIconMap: Record = { - postgresql: PostgresIcon, - twilio_voice: TwilioIcon, - translate: TranslateIcon, - tavily: TavilyIcon, - stagehand_agent: StagehandIcon, - youtube: YouTubeIcon, - supabase: SupabaseIcon, - vision: EyeIcon, - confluence: ConfluenceIcon, - arxiv: ArxivIcon, - webflow: WebflowIcon, - pinecone: PineconeIcon, - whatsapp: WhatsAppIcon, - typeform: TypeformIcon, - qdrant: QdrantIcon, - asana: AsanaIcon, - memory: BrainIcon, - serper: SerperIcon, - linear: LinearIcon, - exa: ExaAIIcon, - telegram: TelegramIcon, - salesforce: SalesforceIcon, - hubspot: HubspotIcon, - hunter: HunterIOIcon, - linkup: LinkupIcon, - mongodb: MongoDBIcon, - airtable: AirtableIcon, - discord: DiscordIcon, - jina: JinaAIIcon, - google_docs: GoogleDocsIcon, - perplexity: PerplexityIcon, - google_search: GoogleIcon, - x: xIcon, - google_calendar: GoogleCalendarIcon, zep: ZepIcon, - microsoft_planner: MicrosoftPlannerIcon, - thinking: BrainIcon, - pipedrive: PipedriveIcon, - stagehand: StagehandIcon, - google_forms: GoogleFormsIcon, - file: DocumentIcon, - mistral_parse: MistralIcon, - gmail: GmailIcon, - openai: OpenAIIcon, - outlook: OutlookIcon, - onedrive: MicrosoftOneDriveIcon, - resend: ResendIcon, - google_vault: GoogleVaultIcon, - sharepoint: MicrosoftSharepointIcon, - huggingface: HuggingFaceIcon, - clay: ClayIcon, - jira: JiraIcon, - wealthbox: WealthboxIcon, - notion: NotionIcon, - elevenlabs: ElevenLabsIcon, - microsoft_teams: MicrosoftTeamsIcon, - github: GithubIcon, - google_drive: GoogleDriveIcon, - reddit: RedditIcon, - parallel_ai: ParallelIcon, - stripe: StripeIcon, - s3: S3Icon, - trello: TrelloIcon, - mem0: Mem0Icon, - knowledge: PackageSearchIcon, - twilio_sms: TwilioIcon, - slack: SlackIcon, - microsoft_excel: MicrosoftExcelIcon, - image_generator: ImageIcon, - google_sheets: GoogleSheetsIcon, + youtube: YouTubeIcon, + x: xIcon, wikipedia: WikipediaIcon, - firecrawl: FirecrawlIcon, + whatsapp: WhatsAppIcon, + webflow: WebflowIcon, + wealthbox: WealthboxIcon, + vision: EyeIcon, + typeform: TypeformIcon, + twilio_voice: TwilioIcon, + twilio_sms: TwilioIcon, + trello: TrelloIcon, + translate: TranslateIcon, + thinking: BrainIcon, + telegram: TelegramIcon, + tavily: TavilyIcon, + supabase: SupabaseIcon, + stripe: StripeIcon, + stagehand_agent: StagehandIcon, + stagehand: StagehandIcon, + slack: SlackIcon, + sharepoint: MicrosoftSharepointIcon, + serper: SerperIcon, + salesforce: SalesforceIcon, + s3: S3Icon, + resend: ResendIcon, + reddit: RedditIcon, + qdrant: QdrantIcon, + postgresql: PostgresIcon, + pipedrive: PipedriveIcon, + pinecone: PineconeIcon, + perplexity: PerplexityIcon, + parallel_ai: ParallelIcon, + outlook: OutlookIcon, + openai: OpenAIIcon, + onedrive: MicrosoftOneDriveIcon, + notion: NotionIcon, mysql: MySQLIcon, + mongodb: MongoDBIcon, + mistral_parse: MistralIcon, + microsoft_teams: MicrosoftTeamsIcon, + microsoft_planner: MicrosoftPlannerIcon, + microsoft_excel: MicrosoftExcelIcon, + memory: BrainIcon, + mem0: Mem0Icon, + linkup: LinkupIcon, + linear: LinearIcon, + knowledge: PackageSearchIcon, + jira: JiraIcon, + jina: JinaAIIcon, + image_generator: ImageIcon, + hunter: HunterIOIcon, + huggingface: HuggingFaceIcon, + hubspot: HubspotIcon, + google_vault: GoogleVaultIcon, + google_sheets: GoogleSheetsIcon, + google_forms: GoogleFormsIcon, + google_drive: GoogleDriveIcon, + google_docs: GoogleDocsIcon, + google_calendar: GoogleCalendarIcon, + google_search: GoogleIcon, + gmail: GmailIcon, + github: GithubIcon, + firecrawl: FirecrawlIcon, + file: DocumentIcon, + exa: ExaAIIcon, + elevenlabs: ElevenLabsIcon, + discord: DiscordIcon, + confluence: ConfluenceIcon, + clay: ClayIcon, browser_use: BrowserUseIcon, + asana: AsanaIcon, + arxiv: ArxivIcon, + apollo: ApolloIcon, + airtable: AirtableIcon, } diff --git a/apps/docs/content/docs/en/tools/apollo.mdx b/apps/docs/content/docs/en/tools/apollo.mdx new file mode 100644 index 000000000..4b9e7b148 --- /dev/null +++ b/apps/docs/content/docs/en/tools/apollo.mdx @@ -0,0 +1,579 @@ +--- +title: Apollo +description: Search, enrich, and manage contacts with Apollo.io +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Apollo.io](https://apollo.io/) is a leading sales intelligence and engagement platform that empowers users to find, enrich, and engage contacts and companies at scale. Apollo.io combines an extensive contact database with robust enrichment and workflow automation tools, assisting sales, marketing, and recruiting teams to accelerate growth. + +With Apollo.io, you can: + +- **Search millions of contacts and companies**: Find precise leads using advanced filters +- **Enrich leads and accounts**: Fill in missing details with verified data and up-to-date information +- **Manage and organize CRM records**: Keep your people and company data accurate and actionable +- **Automate outreach**: Add contacts to sequences and create follow-up tasks directly from Apollo.io + +In Sim, the Apollo.io integration allows your agents to perform core Apollo operations programmatically: + +- **Search people and companies**: Use `apollo_people_search` to discover new leads using flexible filters. +- **Enrich people data**: Use `apollo_people_enrich` to augment contacts with verified information. +- **Enrich people in bulk**: Use `apollo_people_bulk_enrich` for large-scale enrichment of multiple contacts at once. +- **Search and enrich companies**: Use `apollo_company_search` and `apollo_company_enrich` to discover and update key company information. + +This enables your agents to build powerful workflows for prospecting, CRM enrichment, and automation without manual data entry or switching tabs. Integrate Apollo.io as a dynamic data source and CRM engine — empowering your agents to identify, qualify, and reach out to leads seamlessly as part of their daily operations. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrates Apollo.io into the workflow. Search for people and companies, enrich contact data, manage your CRM contacts and accounts, add contacts to sequences, and create tasks. + + + +## Tools + +### `apollo_people_search` + +Search Apollo + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `person_titles` | array | No | Job titles to search for \(e.g., \["CEO", "VP of Sales"\]\) | +| `person_locations` | array | No | Locations to search in \(e.g., \["San Francisco, CA", "New York, NY"\]\) | +| `person_seniorities` | array | No | Seniority levels \(e.g., \["senior", "executive", "manager"\]\) | +| `organization_names` | array | No | Company names to search within | +| `q_keywords` | string | No | Keywords to search for | +| `page` | number | No | Page number for pagination \(default: 1\) | +| `per_page` | number | No | Results per page \(default: 25, max: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `people` | json | Array of people matching the search criteria | +| `metadata` | json | Pagination information including page, per_page, and total_entries | + +### `apollo_people_enrich` + +Enrich data for a single person using Apollo + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `first_name` | string | No | First name of the person | +| `last_name` | string | No | Last name of the person | +| `email` | string | No | Email address of the person | +| `organization_name` | string | No | Company name where the person works | +| `domain` | string | No | Company domain \(e.g., apollo.io\) | +| `linkedin_url` | string | No | LinkedIn profile URL | +| `reveal_personal_emails` | boolean | No | Reveal personal email addresses \(uses credits\) | +| `reveal_phone_number` | boolean | No | Reveal phone numbers \(uses credits\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `person` | json | Enriched person data from Apollo | +| `metadata` | json | Enrichment metadata including enriched status | + +### `apollo_people_bulk_enrich` + +Enrich data for up to 10 people at once using Apollo + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `people` | array | Yes | Array of people to enrich \(max 10\) | +| `reveal_personal_emails` | boolean | No | Reveal personal email addresses \(uses credits\) | +| `reveal_phone_number` | boolean | No | Reveal phone numbers \(uses credits\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `people` | json | Array of enriched people data | +| `metadata` | json | Bulk enrichment metadata including total and enriched counts | + +### `apollo_organization_search` + +Search Apollo + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `organization_locations` | array | No | Company locations to search | +| `organization_num_employees_ranges` | array | No | Employee count ranges \(e.g., \["1-10", "11-50"\]\) | +| `q_organization_keyword_tags` | array | No | Industry or keyword tags | +| `q_organization_name` | string | No | Organization name to search for | +| `page` | number | No | Page number for pagination | +| `per_page` | number | No | Results per page \(max: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `organizations` | json | Array of organizations matching the search criteria | +| `metadata` | json | Pagination information including page, per_page, and total_entries | + +### `apollo_organization_enrich` + +Enrich data for a single organization using Apollo + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `organization_name` | string | No | Name of the organization \(at least one of organization_name or domain is required\) | +| `domain` | string | No | Company domain \(e.g., apollo.io\) \(at least one of domain or organization_name is required\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `organization` | json | Enriched organization data from Apollo | +| `metadata` | json | Enrichment metadata including enriched status | + +### `apollo_organization_bulk_enrich` + +Enrich data for up to 10 organizations at once using Apollo + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `organizations` | array | Yes | Array of organizations to enrich \(max 10\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `organizations` | json | Array of enriched organization data | +| `metadata` | json | Bulk enrichment metadata including total and enriched counts | + +### `apollo_contact_create` + +Create a new contact in your Apollo database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `first_name` | string | Yes | First name of the contact | +| `last_name` | string | Yes | Last name of the contact | +| `email` | string | No | Email address of the contact | +| `title` | string | No | Job title | +| `account_id` | string | No | Apollo account ID to associate with | +| `owner_id` | string | No | User ID of the contact owner | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `contact` | json | Created contact data from Apollo | +| `metadata` | json | Creation metadata including created status | + +### `apollo_contact_update` + +Update an existing contact in your Apollo database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `contact_id` | string | Yes | ID of the contact to update | +| `first_name` | string | No | First name of the contact | +| `last_name` | string | No | Last name of the contact | +| `email` | string | No | Email address | +| `title` | string | No | Job title | +| `account_id` | string | No | Apollo account ID | +| `owner_id` | string | No | User ID of the contact owner | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `contact` | json | Updated contact data from Apollo | +| `metadata` | json | Update metadata including updated status | + +### `apollo_contact_search` + +Search your team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `q_keywords` | string | No | Keywords to search for | +| `contact_stage_ids` | array | No | Filter by contact stage IDs | +| `page` | number | No | Page number for pagination | +| `per_page` | number | No | Results per page \(max: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `contacts` | json | Array of contacts matching the search criteria | +| `metadata` | json | Pagination information including page, per_page, and total_entries | + +### `apollo_contact_bulk_create` + +Create up to 100 contacts at once in your Apollo database. Supports deduplication to prevent creating duplicate contacts. Master key required. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `contacts` | array | Yes | Array of contacts to create \(max 100\). Each contact should include first_name, last_name, and optionally email, title, account_id, owner_id | +| `run_dedupe` | boolean | No | Enable deduplication to prevent creating duplicate contacts. When true, existing contacts are returned without modification | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `created_contacts` | json | Array of newly created contacts | +| `existing_contacts` | json | Array of existing contacts \(when deduplication is enabled\) | +| `metadata` | json | Bulk creation metadata including counts of created and existing contacts | + +### `apollo_contact_bulk_update` + +Update up to 100 existing contacts at once in your Apollo database. Each contact must include an id field. Master key required. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `contacts` | array | Yes | Array of contacts to update \(max 100\). Each contact must include id field, and optionally first_name, last_name, email, title, account_id, owner_id | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated_contacts` | json | Array of successfully updated contacts | +| `failed_contacts` | json | Array of contacts that failed to update | +| `metadata` | json | Bulk update metadata including counts of updated and failed contacts | + +### `apollo_account_create` + +Create a new account (company) in your Apollo database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `name` | string | Yes | Company name | +| `website_url` | string | No | Company website URL | +| `phone` | string | No | Company phone number | +| `owner_id` | string | No | User ID of the account owner | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `account` | json | Created account data from Apollo | +| `metadata` | json | Creation metadata including created status | + +### `apollo_account_update` + +Update an existing account in your Apollo database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `account_id` | string | Yes | ID of the account to update | +| `name` | string | No | Company name | +| `website_url` | string | No | Company website URL | +| `phone` | string | No | Company phone number | +| `owner_id` | string | No | User ID of the account owner | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `account` | json | Updated account data from Apollo | +| `metadata` | json | Update metadata including updated status | + +### `apollo_account_search` + +Search your team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `q_keywords` | string | No | Keywords to search for in account data | +| `owner_id` | string | No | Filter by account owner user ID | +| `account_stage_ids` | array | No | Filter by account stage IDs | +| `page` | number | No | Page number for pagination | +| `per_page` | number | No | Results per page \(max: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `accounts` | json | Array of accounts matching the search criteria | +| `metadata` | json | Pagination information including page, per_page, and total_entries | + +### `apollo_account_bulk_create` + +Create up to 100 accounts at once in your Apollo database. Note: Apollo does not apply deduplication - duplicate accounts may be created if entries share similar names or domains. Master key required. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `accounts` | array | Yes | Array of accounts to create \(max 100\). Each account should include name \(required\), and optionally website_url, phone, owner_id | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `created_accounts` | json | Array of newly created accounts | +| `failed_accounts` | json | Array of accounts that failed to create | +| `metadata` | json | Bulk creation metadata including counts of created and failed accounts | + +### `apollo_account_bulk_update` + +Update up to 1000 existing accounts at once in your Apollo database (higher limit than contacts!). Each account must include an id field. Master key required. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `accounts` | array | Yes | Array of accounts to update \(max 1000\). Each account must include id field, and optionally name, website_url, phone, owner_id | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated_accounts` | json | Array of successfully updated accounts | +| `failed_accounts` | json | Array of accounts that failed to update | +| `metadata` | json | Bulk update metadata including counts of updated and failed accounts | + +### `apollo_opportunity_create` + +Create a new deal for an account in your Apollo database (master key required) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `name` | string | Yes | Name of the opportunity/deal | +| `account_id` | string | Yes | ID of the account this opportunity belongs to | +| `amount` | number | No | Monetary value of the opportunity | +| `stage_id` | string | No | ID of the deal stage | +| `owner_id` | string | No | User ID of the opportunity owner | +| `close_date` | string | No | Expected close date \(ISO 8601 format\) | +| `description` | string | No | Description or notes about the opportunity | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `opportunity` | json | Created opportunity data from Apollo | +| `metadata` | json | Creation metadata including created status | + +### `apollo_opportunity_search` + +Search and list all deals/opportunities in your team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `q_keywords` | string | No | Keywords to search for in opportunity names | +| `account_ids` | array | No | Filter by specific account IDs | +| `stage_ids` | array | No | Filter by deal stage IDs | +| `owner_ids` | array | No | Filter by opportunity owner IDs | +| `page` | number | No | Page number for pagination | +| `per_page` | number | No | Results per page \(max: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `opportunities` | json | Array of opportunities matching the search criteria | +| `metadata` | json | Pagination information including page, per_page, and total_entries | + +### `apollo_opportunity_get` + +Retrieve complete details of a specific deal/opportunity by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `opportunity_id` | string | Yes | ID of the opportunity to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `opportunity` | json | Complete opportunity data from Apollo | +| `metadata` | json | Retrieval metadata including found status | + +### `apollo_opportunity_update` + +Update an existing deal/opportunity in your Apollo database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key | +| `opportunity_id` | string | Yes | ID of the opportunity to update | +| `name` | string | No | Name of the opportunity/deal | +| `amount` | number | No | Monetary value of the opportunity | +| `stage_id` | string | No | ID of the deal stage | +| `owner_id` | string | No | User ID of the opportunity owner | +| `close_date` | string | No | Expected close date \(ISO 8601 format\) | +| `description` | string | No | Description or notes about the opportunity | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `opportunity` | json | Updated opportunity data from Apollo | +| `metadata` | json | Update metadata including updated status | + +### `apollo_sequence_search` + +Search for sequences/campaigns in your team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `q_name` | string | No | Search sequences by name | +| `active` | boolean | No | Filter by active status \(true for active sequences, false for inactive\) | +| `page` | number | No | Page number for pagination | +| `per_page` | number | No | Results per page \(max: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sequences` | json | Array of sequences/campaigns matching the search criteria | +| `metadata` | json | Pagination information including page, per_page, and total_entries | + +### `apollo_sequence_add_contacts` + +Add contacts to an Apollo sequence + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `sequence_id` | string | Yes | ID of the sequence to add contacts to | +| `contact_ids` | array | Yes | Array of contact IDs to add to the sequence | +| `emailer_campaign_id` | string | No | Optional emailer campaign ID | +| `send_email_from_user_id` | string | No | User ID to send emails from | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `contacts_added` | json | Array of contact IDs added to the sequence | +| `metadata` | json | Sequence metadata including sequence_id and total_added count | + +### `apollo_task_create` + +Create a new task in Apollo + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `note` | string | Yes | Task note/description | +| `contact_id` | string | No | Contact ID to associate with | +| `account_id` | string | No | Account ID to associate with | +| `due_at` | string | No | Due date in ISO format | +| `priority` | string | No | Task priority | +| `type` | string | No | Task type | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `task` | json | Created task data from Apollo | +| `metadata` | json | Creation metadata including created status | + +### `apollo_task_search` + +Search for tasks in Apollo + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | +| `contact_id` | string | No | Filter by contact ID | +| `account_id` | string | No | Filter by account ID | +| `completed` | boolean | No | Filter by completion status | +| `page` | number | No | Page number for pagination | +| `per_page` | number | No | Results per page \(max: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tasks` | json | Array of tasks matching the search criteria | +| `metadata` | json | Pagination information including page, per_page, and total_entries | + +### `apollo_email_accounts` + +Get list of team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Apollo API key \(master key required\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email_accounts` | json | Array of team email accounts linked in Apollo | +| `metadata` | json | Metadata including total count of email accounts | + + + +## Notes + +- Category: `tools` +- Type: `apollo` diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index d6d5b93b9..2da0cafbe 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -2,6 +2,7 @@ "pages": [ "index", "airtable", + "apollo", "arxiv", "asana", "browser_use", diff --git a/apps/sim/app/api/organizations/route.ts b/apps/sim/app/api/organizations/route.ts index ddb2808c0..b9fdb374b 100644 --- a/apps/sim/app/api/organizations/route.ts +++ b/apps/sim/app/api/organizations/route.ts @@ -1,12 +1,49 @@ import { db } from '@sim/db' -import { member } from '@sim/db/schema' -import { eq } from 'drizzle-orm' +import { member, organization } from '@sim/db/schema' +import { and, eq, or } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createOrganizationForTeamPlan } from '@/lib/billing/organization' import { createLogger } from '@/lib/logs/console/logger' -const logger = createLogger('CreateTeamOrganization') +const logger = createLogger('OrganizationsAPI') + +export async function GET() { + try { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Get organizations where user is owner or admin + const userOrganizations = await db + .select({ + id: organization.id, + name: organization.name, + role: member.role, + }) + .from(member) + .innerJoin(organization, eq(member.organizationId, organization.id)) + .where( + and( + eq(member.userId, session.user.id), + or(eq(member.role, 'owner'), eq(member.role, 'admin')) + ) + ) + + return NextResponse.json({ + organizations: userOrganizations, + }) + } catch (error) { + logger.error('Failed to fetch organizations', { + error: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + }) + + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} export async function POST(request: Request) { try { diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index a25e48b08..186f6837e 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -10,6 +10,7 @@ import { OneDollarStats } from '@/components/analytics/onedollarstats' import { SessionProvider } from '@/lib/session/session-context' import { season } from '@/app/fonts/season/season' import { HydrationErrorHandler } from '@/app/hydration-error-handler' +import { QueryProvider } from '@/app/providers/query-client-provider' import { ThemeProvider } from '@/app/theme-provider' import { ZoomPrevention } from '@/app/zoom-prevention' @@ -173,12 +174,14 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - - - - {children} - - + + + + + {children} + + + diff --git a/apps/sim/app/providers/query-client-provider.tsx b/apps/sim/app/providers/query-client-provider.tsx new file mode 100644 index 000000000..84231ce39 --- /dev/null +++ b/apps/sim/app/providers/query-client-provider.tsx @@ -0,0 +1,26 @@ +'use client' + +import { type ReactNode, useState } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +export function QueryProvider({ children }: { children: ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + retry: 1, + retryOnMount: false, + }, + mutations: { + retry: 1, + }, + }, + }) + ) + + return {children} +} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx index 29254447c..eb74bdc68 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx @@ -3,6 +3,7 @@ import { TimerOff } from 'lucide-react' import { Button } from '@/components/emcn' import { isProd } from '@/lib/environment' +import { getSubscriptionStatus } from '@/lib/subscription/helpers' import { FilterSection, FolderFilter, @@ -11,14 +12,14 @@ import { Trigger, Workflow, } from '@/app/workspace/[workspaceId]/logs/components/filters/components' -import { useSubscriptionStore } from '@/stores/subscription/store' +import { useSubscriptionData } from '@/hooks/queries/subscription' /** * Filters component for logs page - includes timeline and other filter options */ export function Filters() { - const { getSubscriptionStatus, isLoading } = useSubscriptionStore() - const subscription = getSubscriptionStatus() + const { data: subscriptionData, isLoading } = useSubscriptionData() + const subscription = getSubscriptionStatus(subscriptionData?.data) const isPaid = subscription.isPaid const handleUpgradeClick = (e: React.MouseEvent) => { diff --git a/apps/sim/app/workspace/[workspaceId]/providers/settings-loader.tsx b/apps/sim/app/workspace/[workspaceId]/providers/settings-loader.tsx index 40588ba43..4187e216f 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/settings-loader.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/settings-loader.tsx @@ -2,26 +2,29 @@ import { useEffect, useRef } from 'react' import { useSession } from '@/lib/auth-client' -import { useGeneralStore } from '@/stores/settings/general/store' +import { useGeneralSettings } from '@/hooks/queries/general-settings' /** * Loads user settings from database once per workspace session. - * This ensures settings are synced from DB on initial load but uses - * localStorage cache for subsequent navigation within the app. + * React Query handles the fetching and automatically syncs to Zustand store. + * This ensures settings are available throughout the app. */ export function SettingsLoader() { const { data: session, isPending: isSessionPending } = useSession() - const loadSettings = useGeneralStore((state) => state.loadSettings) const hasLoadedRef = useRef(false) + // Use React Query hook which automatically syncs to Zustand + // This replaces the old Zustand loadSettings() call + const { refetch } = useGeneralSettings() + useEffect(() => { // Only load settings once per session for authenticated users if (!isSessionPending && session?.user && !hasLoadedRef.current) { hasLoadedRef.current = true - // Force load from DB on initial workspace entry - loadSettings(true) + // Force refetch from DB on initial workspace entry + refetch() } - }, [isSessionPending, session?.user, loadSettings]) + }, [isSessionPending, session?.user, refetch]) return null } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx index d49a11e8b..6c8376108 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx @@ -45,6 +45,7 @@ interface DeploymentInfoProps { getInputFormatExample?: (includeStreaming?: boolean) => string selectedStreamingOutputs: string[] onSelectedStreamingOutputsChange: (outputs: string[]) => void + onLoadDeploymentComplete: () => void } export function DeploymentInfo({ @@ -60,6 +61,7 @@ export function DeploymentInfo({ getInputFormatExample, selectedStreamingOutputs, onSelectedStreamingOutputsChange, + onLoadDeploymentComplete, }: DeploymentInfoProps) { const [isViewingDeployed, setIsViewingDeployed] = useState(false) @@ -174,6 +176,7 @@ export function DeploymentInfo({ needsRedeployment={deploymentInfo.needsRedeployment} activeDeployedState={deployedState} workflowId={workflowId} + onLoadDeploymentComplete={onLoadDeploymentComplete} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx index 7734a43a5..fdf1c3cac 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx @@ -784,6 +784,7 @@ export function DeployModal({ getInputFormatExample={getInputFormatExample} selectedStreamingOutputs={selectedStreamingOutputs} onSelectedStreamingOutputsChange={setSelectedStreamingOutputs} + onLoadDeploymentComplete={handleCloseModal} /> )} @@ -1062,6 +1063,7 @@ export function DeployModal({ } workflowId={workflowId} isSelectedVersionActive={versions.find((v) => v.version === previewVersion)?.isActive} + onLoadDeploymentComplete={handleCloseModal} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx index 01339cf8f..cdd09e881 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx @@ -35,6 +35,7 @@ interface DeployedWorkflowModalProps { selectedVersionLabel?: string workflowId: string isSelectedVersionActive?: boolean + onLoadDeploymentComplete?: () => void } export function DeployedWorkflowModal({ @@ -49,6 +50,7 @@ export function DeployedWorkflowModal({ selectedVersionLabel, workflowId, isSelectedVersionActive, + onLoadDeploymentComplete, }: DeployedWorkflowModalProps) { const [showRevertDialog, setShowRevertDialog] = useState(false) const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) @@ -82,6 +84,7 @@ export function DeployedWorkflowModal({ setShowRevertDialog(false) onClose() + onLoadDeploymentComplete?.() } catch (error) { logger.error('Failed to revert workflow:', error) } @@ -91,7 +94,7 @@ export function DeployedWorkflowModal({
@@ -136,7 +139,7 @@ export function DeployedWorkflowModal({ - + Load this Deployment? diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx index 044be38aa..1e7947e12 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx @@ -43,7 +43,6 @@ import { useDebounce } from '@/hooks/use-debounce' import { useFolderStore } from '@/stores/folders/store' import { useOperationQueueStore } from '@/stores/operation-queue/store' import { usePanelStore } from '@/stores/panel/store' -import { useSubscriptionStore } from '@/stores/subscription/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -81,11 +80,8 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { const { lastSaved, setNeedsRedeploymentFlag, blocks } = useWorkflowStore() const { workflows, - updateWorkflow, activeWorkflowId, - removeWorkflow, duplicateWorkflow, - setDeploymentStatus, isLoading: isRegistryLoading, } = useWorkflowRegistry() const { isExecuting, handleRunWorkflow, handleCancelExecution } = useWorkflowExecution() @@ -100,7 +96,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { useWorkflowExecution() // Local state - const [mounted, setMounted] = useState(false) + const [, setMounted] = useState(false) const [, forceUpdate] = useState({}) const [isExpanded, setIsExpanded] = useState(false) const [isWebhookSettingsOpen, setIsWebhookSettingsOpen] = useState(false) @@ -332,7 +328,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { /** * Check user usage limits and cache results */ - async function checkUserUsage(userId: string, forceRefresh = false): Promise { + async function checkUserUsage(_userId: string, forceRefresh = false): Promise { const now = Date.now() const cacheAge = now - usageDataCache.timestamp @@ -355,14 +351,8 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { return usage } - // Fallback: use store if API not available - const { getUsage, refresh } = useSubscriptionStore.getState() - if (forceRefresh) await refresh() - const usage = getUsage() - - // Update cache - usageDataCache = { data: usage, timestamp: now, expirationMs: usageDataCache.expirationMs } - return usage + // No fallback needed anymore - React Query handles this + return null } catch (error) { logger.error('Error checking usage limits:', { error }) return null @@ -1113,6 +1103,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { + {getTooltipContent()} ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/combobox/combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/combobox/combobox.tsx index b3096588a..54ddbb347 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/combobox/combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/combobox/combobox.tsx @@ -1,8 +1,6 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { useParams } from 'next/navigation' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useReactFlow } from 'reactflow' import { Combobox, type ComboboxOption } from '@/components/emcn/components' -import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text' import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/sub-block-input-controller' @@ -10,19 +8,14 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import type { SubBlockConfig } from '@/blocks/types' -const logger = createLogger('ComboBox') - /** * Constants for ComboBox component behavior */ -const CURSOR_POSITION_DELAY = 0 -const SCROLL_SYNC_DELAY = 0 const DEFAULT_MODEL = 'gpt-4o' const ZOOM_FACTOR_BASE = 0.96 const MIN_ZOOM = 0.1 const MAX_ZOOM = 1 const ZOOM_DURATION = 0 -const DROPDOWN_CLOSE_DELAY = 150 /** * Represents a selectable option in the combobox @@ -57,17 +50,6 @@ interface ComboBoxProps { config: SubBlockConfig } -/** - * ComboBox component that provides a searchable dropdown with support for: - * - Free text input or selection from predefined options - * - Environment variable and tag insertion via special triggers - * - Drag and drop connections from other blocks - * - Keyboard navigation (Arrow keys, Enter, Escape) - * - Preview mode for displaying read-only values - * - * @param props - Component props - * @returns Rendered ComboBox component - */ export function ComboBox({ options, defaultValue, @@ -81,20 +63,12 @@ export function ComboBox({ config, }: ComboBoxProps) { // Hooks and context - const params = useParams() const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) const reactFlowInstance = useReactFlow() // State management const [storeInitialized, setStoreInitialized] = useState(false) - const [open, setOpen] = useState(false) - const [highlightedIndex, setHighlightedIndex] = useState(-1) - - // Refs - const inputRef = useRef(null) - const overlayRef = useRef(null) - const dropdownRef = useRef(null) // Determine the active value based on mode (preview vs. controlled vs. store) const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue @@ -123,15 +97,6 @@ export function ComboBox({ return typeof option === 'string' ? option : option.id }, []) - /** - * Extracts the display label from an option - * @param option - The option to extract label from - * @returns The option's display label - */ - const getOptionLabel = useCallback((option: ComboBoxOption): string => { - return typeof option === 'string' ? option : option.label - }, []) - /** * Determines the default option value to use. * Priority: explicit defaultValue > gpt-4o for model field > first option @@ -157,33 +122,27 @@ export function ComboBox({ }, [defaultValue, evaluatedOptions, subBlockId, getOptionValue]) /** - * Filters options based on current input value - * Shows all options when dropdown is closed or when value matches an exact option - * Otherwise filters by search term + * Resolve the user-facing text for the current stored value. + * - For object options, map stored ID -> label + * - For everything else, display the raw value */ - const filteredOptions = useMemo(() => { - // Always show all options when dropdown is not open - if (!open) return evaluatedOptions + const displayValue = useMemo(() => { + const raw = value?.toString() ?? '' + if (!raw) return '' - // If no value or value matches an exact option, show all options - if (!value) return evaluatedOptions - - const currentValue = value.toString() - const exactMatch = evaluatedOptions.find( - (opt) => getOptionValue(opt) === currentValue || getOptionLabel(opt) === currentValue + const match = evaluatedOptions.find((option) => + typeof option === 'string' ? option === raw : option.id === raw ) - // If current value exactly matches an option, show all options (user just selected it) - if (exactMatch) return evaluatedOptions + if (!match) return raw + return typeof match === 'string' ? match : match.label + }, [value, evaluatedOptions]) - // Otherwise filter based on current input - return evaluatedOptions.filter((option) => { - const label = getOptionLabel(option).toLowerCase() - const optionValue = getOptionValue(option).toLowerCase() - const search = currentValue.toLowerCase() - return label.includes(search) || optionValue.includes(search) - }) - }, [evaluatedOptions, value, open, getOptionValue, getOptionLabel]) + const [inputValue, setInputValue] = useState(displayValue) + + useEffect(() => { + setInputValue(displayValue) + }, [displayValue]) // Mark store as initialized on first render useEffect(() => { @@ -201,128 +160,6 @@ export function ComboBox({ } }, [storeInitialized, value, defaultOptionValue, setStoreValue]) - /** - * Handles selection of an option from the dropdown - * @param selectedValue - The value of the selected option - */ - const handleSelect = useCallback( - (selectedValue: string) => { - if (!isPreview && !disabled) { - setStoreValue(selectedValue) - } - setOpen(false) - setHighlightedIndex(-1) - inputRef.current?.blur() - }, - [isPreview, disabled, setStoreValue] - ) - - /** - * Handles click on the dropdown chevron button - * @param e - Mouse event - */ - const handleDropdownClick = useCallback( - (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - if (!disabled) { - setOpen((prev) => { - const newOpen = !prev - if (newOpen) { - inputRef.current?.focus() - } - return newOpen - }) - } - }, - [disabled] - ) - - /** - * Handles focus event on the input - */ - const handleFocus = useCallback(() => { - setOpen(true) - setHighlightedIndex(-1) - }, []) - - /** - * Handles blur event on the input - * Delays closing to allow for dropdown interactions - */ - const handleBlur = useCallback(() => { - // Delay closing to allow dropdown selection - setTimeout(() => { - const activeElement = document.activeElement - if (!activeElement || !activeElement.closest('.absolute.top-full')) { - setOpen(false) - setHighlightedIndex(-1) - } - }, DROPDOWN_CLOSE_DELAY) - }, []) - - /** - * Handles keyboard navigation and selection - * @param e - Keyboard event - */ - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setOpen(false) - setHighlightedIndex(-1) - return - } - - if (e.key === 'ArrowDown') { - e.preventDefault() - if (!open) { - setOpen(true) - setHighlightedIndex(0) - } else { - setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0)) - } - } - - if (e.key === 'ArrowUp') { - e.preventDefault() - if (open) { - setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1)) - } - } - - if (e.key === 'Enter' && open && highlightedIndex >= 0) { - e.preventDefault() - const selectedOption = filteredOptions[highlightedIndex] - if (selectedOption) { - handleSelect(getOptionValue(selectedOption)) - } - } - }, - [open, filteredOptions, highlightedIndex, handleSelect, getOptionValue] - ) - - /** - * Synchronizes overlay scroll with input scroll - * @param e - UI event from input element - */ - const handleScroll = useCallback((e: React.UIEvent) => { - if (overlayRef.current) { - overlayRef.current.scrollLeft = e.currentTarget.scrollLeft - } - }, []) - - /** - * Synchronizes overlay scroll after paste operation - * @param e - Clipboard event - */ - const handlePaste = useCallback((e: React.ClipboardEvent) => { - setTimeout(() => { - if (inputRef.current && overlayRef.current) { - overlayRef.current.scrollLeft = inputRef.current.scrollLeft - } - }, SCROLL_SYNC_DELAY) - }, []) - /** * Handles wheel event for ReactFlow zoom control * Intercepts Ctrl/Cmd+Wheel to zoom the canvas @@ -362,121 +199,34 @@ export function ComboBox({ [reactFlowInstance] ) - // Synchronize overlay scroll position with input when value changes - useEffect(() => { - if (inputRef.current && overlayRef.current) { - overlayRef.current.scrollLeft = inputRef.current.scrollLeft - } - }, [value]) - - // Adjust highlighted index when filtered options change - useEffect(() => { - setHighlightedIndex((prev) => { - if (prev >= 0 && prev < filteredOptions.length) { - return prev - } - return -1 - }) - }, [filteredOptions]) - - // Scroll highlighted option into view for keyboard navigation - useEffect(() => { - if (highlightedIndex >= 0 && dropdownRef.current) { - const highlightedElement = dropdownRef.current.querySelector( - `[data-option-index="${highlightedIndex}"]` - ) - if (highlightedElement) { - highlightedElement.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) - } - } - }, [highlightedIndex]) - - // Handle clicks outside the dropdown to close it - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as Element - if ( - inputRef.current && - !inputRef.current.contains(target) && - !target.closest('[data-radix-popper-content-wrapper]') && - !target.closest('.absolute.top-full') - ) { - setOpen(false) - setHighlightedIndex(-1) - } - } - - if (open) { - document.addEventListener('mousedown', handleClickOutside) - return () => { - document.removeEventListener('mousedown', handleClickOutside) - } - } - }, [open]) - - const displayValue = useMemo(() => value?.toString() ?? '', [value]) - - /** - * Handles value change from Combobox - */ - const handleComboboxChange = useCallback( - (newValue: string) => { - if (!isPreview) { - setStoreValue(newValue) - } - }, - [isPreview, setStoreValue] - ) - /** * Gets the icon for the currently selected option */ - const selectedOptionIcon = useMemo(() => { - const selectedOpt = comboboxOptions.find((opt) => opt.value === displayValue) - return selectedOpt?.icon - }, [comboboxOptions, displayValue]) + const selectedOption = useMemo(() => { + if (!value) return undefined + return comboboxOptions.find((opt) => opt.value === value) + }, [comboboxOptions, value]) + + const selectedOptionIcon = selectedOption?.icon /** * Overlay content for the editable combobox */ const overlayContent = useMemo(() => { const SelectedIcon = selectedOptionIcon + const displayLabel = inputValue return (
{SelectedIcon && }
- {formatDisplayText(displayValue, { + {formatDisplayText(displayLabel, { accessiblePrefixes, highlightAll: !accessiblePrefixes, })}
) - }, [displayValue, accessiblePrefixes, selectedOptionIcon]) - - /** - * Handles mouse enter on dropdown option - * @param index - Index of the option - */ - const handleOptionMouseEnter = useCallback((index: number) => { - setHighlightedIndex(index) - }, []) - - /** - * Handles mouse down on dropdown option - * @param e - Mouse event - * @param optionValue - Value of the selected option - */ - const handleOptionMouseDown = useCallback( - (e: React.MouseEvent, optionValue: string) => { - e.preventDefault() - handleSelect(optionValue) - }, - [handleSelect] - ) + }, [inputValue, accessiblePrefixes, selectedOption, selectedOptionIcon]) return (
@@ -486,9 +236,23 @@ export function ComboBox({ config={config} value={propValue} onChange={(newValue) => { - if (!isPreview) { - setStoreValue(newValue) + if (isPreview) { + return } + + const matchedOption = evaluatedOptions.find((option) => { + if (typeof option === 'string') { + return option === newValue + } + return option.id === newValue + }) + + if (!matchedOption) { + return + } + + const nextValue = typeof matchedOption === 'string' ? matchedOption : matchedOption.id + setStoreValue(nextValue) }} isPreview={isPreview} disabled={disabled} @@ -497,9 +261,19 @@ export function ComboBox({ {({ ref, onChange: ctrlOnChange, onDrop, onDragOver }) => ( { - // Use controller's handler for consistency + const matchedComboboxOption = comboboxOptions.find( + (option) => option.value === newValue + ) + if (matchedComboboxOption) { + setInputValue(matchedComboboxOption.label) + } else { + setInputValue(newValue) + } + + // Use controller's handler so env vars, tags, and DnD still work const syntheticEvent = { target: { value: newValue, selectionStart: newValue.length }, } as React.ChangeEvent @@ -515,8 +289,6 @@ export function ComboBox({ inputProps={{ onDrop: onDrop as (e: React.DragEvent) => void, onDragOver: onDragOver as (e: React.DragEvent) => void, - onScroll: handleScroll, - onPaste: handlePaste, onWheel: handleWheel, autoComplete: 'off', }} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown.tsx index ab762b788..0fee1b967 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown.tsx @@ -1,4 +1,5 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' +import { Plus } from 'lucide-react' import { Popover, PopoverAnchor, @@ -8,7 +9,11 @@ import { PopoverSection, } from '@/components/emcn' import { cn } from '@/lib/utils' -import { useEnvironmentStore } from '@/stores/settings/environment/store' +import { + usePersonalEnvironment, + useWorkspaceEnvironment, + type WorkspaceEnvironmentData, +} from '@/hooks/queries/environment' /** * Props for the EnvVarDropdown component @@ -113,28 +118,27 @@ export const EnvVarDropdown: React.FC = ({ maxHeight = 'none', inputRef, }) => { - const loadWorkspaceEnvironment = useEnvironmentStore((state) => state.loadWorkspaceEnvironment) - const userEnvVars = useEnvironmentStore((state) => Object.keys(state.variables)) - const [workspaceEnvData, setWorkspaceEnvData] = useState<{ - workspace: Record - personal: Record - conflicts: string[] - }>({ workspace: {}, personal: {}, conflicts: [] }) - const [selectedIndex, setSelectedIndex] = useState(0) + // React Query hooks for environment variables + const { data: personalEnv = {} } = usePersonalEnvironment() + const { data: workspaceEnvData } = useWorkspaceEnvironment(workspaceId || '', { + select: useCallback( + (data: WorkspaceEnvironmentData): WorkspaceEnvironmentData => ({ + workspace: data.workspace || {}, + personal: data.personal || {}, + conflicts: data.conflicts || [], + }), + [] + ), + }) - useEffect(() => { - if (workspaceId && visible) { - loadWorkspaceEnvironment(workspaceId).then((data) => { - setWorkspaceEnvData(data) - }) - } - }, [workspaceId, visible, loadWorkspaceEnvironment]) + const userEnvVars = Object.keys(personalEnv) + const [selectedIndex, setSelectedIndex] = useState(0) const envVarGroups: EnvVarGroup[] = [] - if (workspaceId) { - const workspaceVars = Object.keys(workspaceEnvData.workspace) - const personalVars = Object.keys(workspaceEnvData.personal) + if (workspaceId && workspaceEnvData) { + const workspaceVars = Object.keys(workspaceEnvData?.workspace || {}) + const personalVars = Object.keys(workspaceEnvData?.personal || {}) envVarGroups.push({ label: 'Workspace', variables: workspaceVars }) envVarGroups.push({ label: 'Personal', variables: personalVars }) @@ -163,6 +167,11 @@ export const EnvVarDropdown: React.FC = ({ setSelectedIndex(0) }, [searchTerm]) + const openEnvironmentSettings = () => { + window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'environment' } })) + onClose?.() + } + const handleEnvVarSelect = (envVar: string) => { const textBeforeCursor = inputValue.slice(0, cursorPosition) const textAfterCursor = inputValue.slice(cursorPosition) @@ -284,9 +293,17 @@ export const EnvVarDropdown: React.FC = ({ onCloseAutoFocus={(e) => e.preventDefault()} > {filteredEnvVars.length === 0 ? ( -
- No matching environment variables -
+ + { + e.preventDefault() + openEnvironmentSettings() + }} + > + + Create environment variable + + ) : ( {filteredGroups.map((group) => ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/mcp-server-modal/mcp-server-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/mcp-server-modal/mcp-server-selector.tsx index 03435222d..67d955c14 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/mcp-server-modal/mcp-server-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/mcp-server-modal/mcp-server-selector.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { Check, ChevronDown, RefreshCw } from 'lucide-react' import { useParams } from 'next/navigation' import { Button } from '@/components/ui/button' @@ -15,7 +15,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' -import { useEnabledServers, useMcpServersStore } from '@/stores/mcp-servers/store' +import { useMcpServers } from '@/hooks/queries/mcp' interface McpServerSelectorProps { blockId: string @@ -36,8 +36,8 @@ export function McpServerSelector({ const workspaceId = params.workspaceId as string const [open, setOpen] = useState(false) - const { fetchServers, isLoading, error } = useMcpServersStore() - const enabledServers = useEnabledServers() + const { data: servers = [], isLoading, error } = useMcpServers(workspaceId) + const enabledServers = servers.filter((s) => s.enabled && !s.deletedAt) const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id) @@ -48,15 +48,9 @@ export function McpServerSelector({ const selectedServer = enabledServers.find((server) => server.id === selectedServerId) - useEffect(() => { - fetchServers(workspaceId) - }, [fetchServers, workspaceId]) - const handleOpenChange = (isOpen: boolean) => { setOpen(isOpen) - if (isOpen) { - fetchServers(workspaceId) - } + // React Query automatically keeps server list fresh } const handleSelect = (serverId: string) => { @@ -102,7 +96,9 @@ export function McpServerSelector({ ) : error ? (

Error loading servers

-

{error}

+

+ {error instanceof Error ? error.message : 'Unknown error'} +

) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx index 865bd74ae..7044f54b2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx @@ -36,7 +36,12 @@ import { import { CodeEditor } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor' import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar' import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand' -import { useCustomToolsStore } from '@/stores/custom-tools/store' +import { + useCreateCustomTool, + useCustomTools, + useDeleteCustomTool, + useUpdateCustomTool, +} from '@/hooks/queries/custom-tools' const logger = createLogger('CustomToolModal') @@ -261,9 +266,11 @@ try { // Schema params keyboard navigation const [schemaParamSelectedIndex, setSchemaParamSelectedIndex] = useState(0) - const createTool = useCustomToolsStore((state) => state.createTool) - const updateTool = useCustomToolsStore((state) => state.updateTool) - const deleteTool = useCustomToolsStore((state) => state.deleteTool) + // React Query mutations + const createToolMutation = useCreateCustomTool() + const updateToolMutation = useUpdateCustomTool() + const deleteToolMutation = useDeleteCustomTool() + const { data: customTools = [] } = useCustomTools(workspaceId) // Initialize form with initial values if provided useEffect(() => { @@ -448,10 +455,8 @@ try { if (isEditing && !toolIdToUpdate && initialValues?.schema) { const originalName = initialValues.schema.function?.name if (originalName) { - const customToolsStore = useCustomToolsStore.getState() - const existingTools = customToolsStore.getAllTools() - const originalTool = existingTools.find( - (tool) => tool.schema.function.name === originalName + const originalTool = customTools.find( + (tool) => tool.schema?.function?.name === originalName ) if (originalTool) { toolIdToUpdate = originalTool.id @@ -460,23 +465,27 @@ try { } // Save to the store (server validates duplicates) - let _finalToolId: string | undefined = toolIdToUpdate - if (isEditing && toolIdToUpdate) { // Update existing tool - await updateTool(workspaceId, toolIdToUpdate, { - title: name, - schema, - code: functionCode || '', + await updateToolMutation.mutateAsync({ + workspaceId, + toolId: toolIdToUpdate, + updates: { + title: name, + schema, + code: functionCode || '', + }, }) } else { // Create new tool - const createdTool = await createTool(workspaceId, { - title: name, - schema, - code: functionCode || '', + await createToolMutation.mutateAsync({ + workspaceId, + tool: { + title: name, + schema, + code: functionCode || '', + }, }) - _finalToolId = createdTool.id } // Create the custom tool object for the parent component @@ -782,8 +791,11 @@ try { try { setShowDeleteConfirm(false) - // Delete from store (which calls the API) - await deleteTool(workspaceId, toolId) + // Delete using React Query mutation + await deleteToolMutation.mutateAsync({ + workspaceId, + toolId, + }) logger.info(`Deleted tool: ${toolId}`) // Notify parent component if callback provided @@ -966,11 +978,6 @@ try { language='json' showWandButton={true} onWandClick={() => { - logger.debug('Schema AI button clicked') - logger.debug( - 'showPromptInline function exists:', - typeof schemaGeneration.showPromptInline === 'function' - ) schemaGeneration.isPromptVisible ? schemaGeneration.hidePromptInline() : schemaGeneration.showPromptInline() @@ -1045,11 +1052,6 @@ try { language='javascript' showWandButton={true} onWandClick={() => { - logger.debug('Code AI button clicked') - logger.debug( - 'showPromptInline function exists:', - typeof codeGeneration.showPromptInline === 'function' - ) codeGeneration.isPromptVisible ? codeGeneration.hidePromptInline() : codeGeneration.showPromptInline() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/mcp-server-modal/mcp-server-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/mcp-server-modal/mcp-server-modal.tsx index b750287b1..bb192d4d4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/mcp-server-modal/mcp-server-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/mcp-server-modal/mcp-server-modal.tsx @@ -28,8 +28,8 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/env-var-dropdown' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' +import { useCreateMcpServer } from '@/hooks/queries/mcp' import { useMcpServerTest } from '@/hooks/use-mcp-server-test' -import { useMcpServersStore } from '@/stores/mcp-servers/store' const logger = createLogger('McpServerModal') @@ -61,7 +61,7 @@ export function McpServerModal({ url: '', headers: { '': '' }, }) - const { createServer, isLoading, error: storeError, clearError } = useMcpServersStore() + const createServerMutation = useCreateMcpServer() const [localError, setLocalError] = useState(null) // MCP server testing @@ -79,7 +79,7 @@ export function McpServerModal({ const [urlScrollLeft, setUrlScrollLeft] = useState(0) const [headerScrollLeft, setHeaderScrollLeft] = useState>({}) - const error = localError || storeError + const error = localError || createServerMutation.error?.message const resetForm = () => { setFormData({ @@ -89,7 +89,7 @@ export function McpServerModal({ headers: { '': '' }, }) setLocalError(null) - clearError() + createServerMutation.reset() setShowEnvVars(false) setActiveInputField(null) setActiveHeaderIndex(null) @@ -210,7 +210,7 @@ export function McpServerModal({ } setLocalError(null) - clearError() + createServerMutation.reset() try { // If no test has been done, test first @@ -242,13 +242,16 @@ export function McpServerModal({ ) ) - await createServer(workspaceId, { - name: formData.name.trim(), - transport: formData.transport, - url: formData.url, - timeout: 30000, - headers: cleanHeaders, - enabled: true, + await createServerMutation.mutateAsync({ + workspaceId, + config: { + name: formData.name.trim(), + transport: formData.transport, + url: formData.url, + timeout: 30000, + headers: cleanHeaders, + enabled: true, + }, }) logger.info(`Added MCP server: ${formData.name}`) @@ -267,8 +270,7 @@ export function McpServerModal({ testConnection, onOpenChange, onServerCreated, - createServer, - clearError, + createServerMutation, workspaceId, ]) @@ -563,16 +565,18 @@ export function McpServerModal({ resetForm() onOpenChange(false) }} - disabled={isLoading} + disabled={createServerMutation.isPending} > Cancel
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 2b5f0f7d7..ffd21cfcb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -50,9 +50,9 @@ import { ToolCommand } from '@/app/workspace/[workspaceId]/w/[workflowId]/compon import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value' import { getAllBlocks } from '@/blocks' +import { useCustomTools } from '@/hooks/queries/custom-tools' import { useMcpTools } from '@/hooks/use-mcp-tools' import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils' -import { useCustomToolsStore } from '@/stores/custom-tools/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { formatParameterLabel, @@ -476,8 +476,7 @@ export function ToolInput({ const [searchQuery, setSearchQuery] = useState('') const [draggedIndex, setDraggedIndex] = useState(null) const [dragOverIndex, setDragOverIndex] = useState(null) - const customTools = useCustomToolsStore((state) => state.getAllTools()) - const fetchCustomTools = useCustomToolsStore((state) => state.fetchTools) + const { data: customTools = [] } = useCustomTools(workspaceId) const subBlockStore = useSubBlockStore() // MCP tools integration @@ -488,13 +487,6 @@ export function ToolInput({ refreshTools, } = useMcpTools(workspaceId) - // Fetch custom tools on mount - useEffect(() => { - if (workspaceId) { - fetchCustomTools(workspaceId) - } - }, [workspaceId, fetchCustomTools]) - // Get the current model from the 'model' subblock const modelValue = useSubBlockStore.getState().getValue(blockId, 'model') const model = typeof modelValue === 'string' ? modelValue : '' @@ -706,7 +698,7 @@ export function ToolInput({ (customTool: CustomTool) => { if (isPreview || disabled) return - const customToolId = `custom-${customTool.schema.function.name}` + const customToolId = `custom-${customTool.schema?.function?.name || 'unknown'}` const newTool: StoredTool = { type: 'custom-tool', @@ -782,7 +774,7 @@ export function ToolInput({ customTools.some( (customTool) => customTool.id === toolId && - customTool.schema.function.name === tool.schema.function.name + customTool.schema?.function?.name === tool.schema.function.name ) ) { return false @@ -823,7 +815,6 @@ export function ToolInput({ const handleOperationChange = useCallback( (toolIndex: number, operation: string) => { if (isPreview || disabled) { - logger.info('❌ Early return: preview or disabled') return } @@ -832,7 +823,6 @@ export function ToolInput({ const newToolId = getToolIdForOperation(tool.type, operation) if (!newToolId) { - logger.info('❌ Early return: no newToolId') return } @@ -840,7 +830,6 @@ export function ToolInput({ const toolParams = getToolParametersConfig(newToolId, tool.type) if (!toolParams) { - logger.info('❌ Early return: no toolParams') return } @@ -1400,7 +1389,7 @@ export function ToolInput({ const newTool: StoredTool = { type: 'custom-tool', title: customTool.title, - toolId: `custom-${customTool.schema.function.name}`, + toolId: `custom-${customTool.schema?.function?.name || 'unknown'}`, params: {}, isExpanded: true, schema: customTool.schema, @@ -1934,7 +1923,7 @@ export function ToolInput({ const newTool: StoredTool = { type: 'custom-tool', title: customTool.title, - toolId: `custom-${customTool.schema.function.name}`, + toolId: `custom-${customTool.schema?.function?.name || 'unknown'}`, params: {}, isExpanded: true, schema: customTool.schema, @@ -2025,8 +2014,8 @@ export function ToolInput({ ? { id: customTools.find( (tool) => - tool.schema.function.name === - selectedTools[editingToolIndex].schema.function.name + tool.schema?.function?.name === + selectedTools[editingToolIndex].schema?.function?.name )?.id, schema: selectedTools[editingToolIndex].schema, code: selectedTools[editingToolIndex].code || '', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx index 988f773b8..8328eb7be 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx @@ -67,12 +67,61 @@ interface SubBlockProps { } /** - * Returns whether the field is required for validation. Intentionally unused. + * Returns whether the field is required for validation. + * Evaluates conditional requirements based on current field values. * @param config - The sub-block configuration + * @param subBlockValues - Current values of all subblocks * @returns True if the field is required */ -const isFieldRequired = (config: SubBlockConfig): boolean => { - return config.required === true +const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record): boolean => { + if (!config.required) return false + if (typeof config.required === 'boolean') return config.required + + // Helper function to evaluate a condition + const evalCond = ( + cond: { + field: string + value: string | number | boolean | Array + not?: boolean + and?: { + field: string + value: string | number | boolean | Array | undefined + not?: boolean + } + }, + values: Record + ): boolean => { + const fieldValue = values[cond.field]?.value + const condValue = cond.value + + let match: boolean + if (Array.isArray(condValue)) { + match = condValue.includes(fieldValue) + } else { + match = fieldValue === condValue + } + + if (cond.not) match = !match + + if (cond.and) { + const andFieldValue = values[cond.and.field]?.value + const andCondValue = cond.and.value + let andMatch: boolean + if (Array.isArray(andCondValue)) { + andMatch = andCondValue.includes(andFieldValue) + } else { + andMatch = andFieldValue === andCondValue + } + if (cond.and.not) andMatch = !andMatch + match = match && andMatch + } + + return match + } + + // If required is a condition object or function, evaluate it + const condition = typeof config.required === 'function' ? config.required() : config.required + return evalCond(condition, subBlockValues || {}) } /** @@ -96,6 +145,7 @@ const getPreviewValue = ( * @param config - The sub-block configuration * @param isValidJson - Whether the JSON is valid * @param wandState - Wand interaction state + * @param subBlockValues - Current values of all subblocks for evaluating conditional requirements * @returns The label JSX element or null if no title or for switch types */ const renderLabel = ( @@ -113,7 +163,8 @@ const renderLabel = ( onSearchSubmit: () => void onSearchCancel: () => void searchInputRef: React.RefObject - } + }, + subBlockValues?: Record ): JSX.Element | null => { if (config.type === 'switch') return null if (!config.title) return null @@ -132,10 +183,13 @@ const renderLabel = ( searchInputRef, } = wandState + const required = isFieldRequired(config, subBlockValues) + return (