import { generateMockPayloadFromOutputsDefinition } from '@/lib/workflows/triggers/trigger-utils' import type { SubBlockConfig } from '@/blocks/types' import { TRIGGER_REGISTRY } from '@/triggers/registry' import type { TriggerConfig } from '@/triggers/types' /** * IDs that should NOT be namespaced because they are shared across triggers * or are the control mechanism for trigger selection */ const SHARED_SUBBLOCK_IDS = new Set(['selectedTriggerId']) /** * Checks if a subBlock is display-only (not user-editable). * Display-only subBlocks should be namespaced to avoid conflicts when * multiple triggers show different content for the same conceptual field. */ function isDisplayOnlySubBlock(subBlock: SubBlockConfig): boolean { // Text type is always display-only if (subBlock.type === 'text') { return true } // ReadOnly inputs are display-only if (subBlock.readOnly === true) { return true } return false } /** * Namespaces a subBlock ID with the trigger ID to avoid conflicts when * multiple triggers are merged into a single block. * * Only namespaces display-only subBlocks (readOnly or text type) that have * a condition on selectedTriggerId. User-input fields are NOT namespaced * so their values persist when switching between triggers. */ function namespaceSubBlockId(subBlock: SubBlockConfig, triggerId: string): SubBlockConfig { // Don't namespace shared IDs if (SHARED_SUBBLOCK_IDS.has(subBlock.id)) { return subBlock } // Only namespace display-only subBlocks to avoid content conflicts // User-input fields should remain shared so values persist across trigger switches if (!isDisplayOnlySubBlock(subBlock)) { return subBlock } // Only namespace if the subBlock has a condition on selectedTriggerId // These are the ones that are trigger-specific and will conflict when merged const condition = typeof subBlock.condition === 'function' ? subBlock.condition() : subBlock.condition if (condition?.field === 'selectedTriggerId') { return { ...subBlock, id: `${subBlock.id}_${triggerId}`, } } return subBlock } /** * Gets a trigger config and injects samplePayload subblock with condition. * Also namespaces subBlock IDs to avoid conflicts when multiple triggers * are merged into a single block (e.g., ...getTrigger('a').subBlocks, ...getTrigger('b').subBlocks). */ export function getTrigger(triggerId: string): TriggerConfig { const trigger = TRIGGER_REGISTRY[triggerId] if (!trigger) { throw new Error(`Trigger not found: ${triggerId}`) } // Filter out deprecated trigger-save subblocks from legacy stored data const subBlocks = trigger.subBlocks .filter( (subBlock) => subBlock.id !== 'triggerSave' && (subBlock.type as string) !== 'trigger-save' ) .map((subBlock) => namespaceSubBlockId(subBlock, triggerId)) const clonedTrigger = { ...trigger, subBlocks } // Inject samplePayload for webhooks/pollers with condition if (trigger.webhook || trigger.id.includes('webhook') || trigger.id.includes('poller')) { const samplePayloadExists = clonedTrigger.subBlocks.some( (sb) => sb.id === 'samplePayload' || sb.id === `samplePayload_${triggerId}` ) if (!samplePayloadExists && trigger.outputs) { const mockPayload = generateMockPayloadFromOutputsDefinition(trigger.outputs) const generatedPayload = JSON.stringify(mockPayload, null, 2) const samplePayloadSubBlock: SubBlockConfig = { id: `samplePayload_${triggerId}`, title: 'Event Payload Example', type: 'code', language: 'json', defaultValue: generatedPayload, readOnly: true, collapsible: true, defaultCollapsed: true, hideFromPreview: true, mode: 'trigger', condition: { field: 'selectedTriggerId', value: trigger.id, }, } clonedTrigger.subBlocks.push(samplePayloadSubBlock) } } return clonedTrigger } export function getTriggersByProvider(provider: string): TriggerConfig[] { return Object.values(TRIGGER_REGISTRY) .filter((trigger) => trigger.provider === provider) .map((trigger) => getTrigger(trigger.id)) } export function getAllTriggers(): TriggerConfig[] { return Object.keys(TRIGGER_REGISTRY).map((triggerId) => getTrigger(triggerId)) } export function getTriggerIds(): string[] { return Object.keys(TRIGGER_REGISTRY) } export function isTriggerValid(triggerId: string): boolean { return triggerId in TRIGGER_REGISTRY } export type { TriggerConfig } from '@/triggers/types' /** * Options for building trigger subBlocks */ export interface BuildTriggerSubBlocksOptions { /** The trigger ID (e.g., 'lemlist_email_replied') */ triggerId: string /** Dropdown options for selecting trigger type */ triggerOptions: Array<{ label: string; id: string }> /** Whether to include the trigger type dropdown (only for primary trigger) */ includeDropdown?: boolean /** HTML setup instructions to display */ setupInstructions: string /** Additional fields to insert before the save button (e.g., campaign filters) */ extraFields?: SubBlockConfig[] /** Webhook URL placeholder text */ webhookPlaceholder?: string } /** * Generic builder for trigger subBlocks. * Creates a consistent structure: [dropdown?] -> webhookUrl -> extraFields -> instructions * * Usage: * - Primary trigger: `buildTriggerSubBlocks({ ...options, includeDropdown: true })` * - Secondary triggers: `buildTriggerSubBlocks({ ...options })` (no dropdown) * * @example * ```typescript * // Primary trigger (with dropdown) * subBlocks: buildTriggerSubBlocks({ * triggerId: 'service_event_a', * triggerOptions: serviceTriggerOptions, * includeDropdown: true, * setupInstructions: serviceSetupInstructions('eventA'), * }) * * // Secondary trigger (no dropdown) * subBlocks: buildTriggerSubBlocks({ * triggerId: 'service_event_b', * triggerOptions: serviceTriggerOptions, * setupInstructions: serviceSetupInstructions('eventB'), * }) * ``` */ export function buildTriggerSubBlocks(options: BuildTriggerSubBlocksOptions): SubBlockConfig[] { const { triggerId, triggerOptions, includeDropdown = false, setupInstructions, extraFields = [], webhookPlaceholder = 'Webhook URL will be generated', } = options const blocks: SubBlockConfig[] = [] // Only the primary trigger includes the dropdown if (includeDropdown) { blocks.push({ id: 'selectedTriggerId', title: 'Trigger Type', type: 'dropdown', mode: 'trigger', options: triggerOptions, value: () => triggerId, required: true, }) } // Webhook URL display (common to all triggers) // ID will be namespaced by getTrigger() when merged into blocks blocks.push({ id: 'webhookUrlDisplay', title: 'Webhook URL', type: 'short-input', readOnly: true, showCopyButton: true, useWebhookUrl: true, placeholder: webhookPlaceholder, mode: 'trigger', condition: { field: 'selectedTriggerId', value: triggerId }, }) // Insert any extra fields (campaign filters, event types, etc.) if (extraFields.length > 0) { blocks.push(...extraFields) } // Setup instructions // ID will be namespaced by getTrigger() when merged into blocks blocks.push({ id: 'triggerInstructions', title: 'Setup Instructions', hideFromPreview: true, type: 'text', defaultValue: setupInstructions, mode: 'trigger', condition: { field: 'selectedTriggerId', value: triggerId }, }) return blocks }