From 33cf0de31e6676644913afd224b07d6ee1be5027 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 14 Mar 2026 19:28:56 -0700 Subject: [PATCH 1/2] improvement(grain): make trigger names in line with API since resource type not known --- .../components/dropdown/dropdown.tsx | 34 ++++++--- apps/sim/blocks/blocks/grain.ts | 10 ++- apps/sim/blocks/types.ts | 2 + .../lib/webhooks/provider-subscriptions.ts | 4 + apps/sim/triggers/grain/index.ts | 2 + apps/sim/triggers/grain/item_added.ts | 76 +++++++++++++++++++ apps/sim/triggers/grain/item_updated.ts | 76 +++++++++++++++++++ apps/sim/triggers/grain/utils.ts | 37 +++++++-- apps/sim/triggers/grain/webhook.ts | 11 +-- apps/sim/triggers/registry.ts | 4 + 10 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 apps/sim/triggers/grain/item_added.ts create mode 100644 apps/sim/triggers/grain/item_updated.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx index 35546381950..135a8cf02cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx @@ -14,11 +14,18 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' /** - * Dropdown option type - can be a simple string or an object with label, id, and optional icon + * Dropdown option type - can be a simple string or an object with label, id, and optional icon. + * Options with `hidden: true` are excluded from the picker but still resolve for label display, + * so existing workflows that reference them continue to work. */ type DropdownOption = | string - | { label: string; id: string; icon?: React.ComponentType<{ className?: string }> } + | { + label: string + id: string + icon?: React.ComponentType<{ className?: string }> + hidden?: boolean + } /** * Props for the Dropdown component @@ -185,13 +192,12 @@ export const Dropdown = memo(function Dropdown({ return fetchedOptions.map((opt) => ({ label: opt.label, id: opt.id })) }, [fetchedOptions]) - const availableOptions = useMemo(() => { + const allOptions = useMemo(() => { let opts: DropdownOption[] = fetchOptions && normalizedFetchedOptions.length > 0 ? normalizedFetchedOptions : evaluatedOptions - // Merge hydrated option if not already present if (hydratedOption) { const alreadyPresent = opts.some((o) => typeof o === 'string' ? o === hydratedOption.id : o.id === hydratedOption.id @@ -204,11 +210,12 @@ export const Dropdown = memo(function Dropdown({ return opts }, [fetchOptions, normalizedFetchedOptions, evaluatedOptions, hydratedOption]) - /** - * Convert dropdown options to Combobox format - */ + const selectableOptions = useMemo(() => { + return allOptions.filter((opt) => typeof opt === 'string' || !opt.hidden) + }, [allOptions]) + const comboboxOptions = useMemo((): ComboboxOption[] => { - return availableOptions.map((opt) => { + return selectableOptions.map((opt) => { if (typeof opt === 'string') { return { label: opt.toLowerCase(), value: opt } } @@ -218,11 +225,16 @@ export const Dropdown = memo(function Dropdown({ icon: 'icon' in opt ? opt.icon : undefined, } }) - }, [availableOptions]) + }, [selectableOptions]) const optionMap = useMemo(() => { - return new Map(comboboxOptions.map((opt) => [opt.value, opt.label])) - }, [comboboxOptions]) + return new Map( + allOptions.map((opt) => { + if (typeof opt === 'string') return [opt, opt.toLowerCase()] + return [opt.id, opt.label.toLowerCase()] + }) + ) + }, [allOptions]) const defaultOptionValue = useMemo(() => { if (multiSelect) return undefined diff --git a/apps/sim/blocks/blocks/grain.ts b/apps/sim/blocks/blocks/grain.ts index 4840272e088..aa0a6236824 100644 --- a/apps/sim/blocks/blocks/grain.ts +++ b/apps/sim/blocks/blocks/grain.ts @@ -268,15 +268,17 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`, type: 'dropdown', mode: 'trigger', options: grainTriggerOptions, - value: () => 'grain_webhook', + value: () => 'grain_item_added', required: true, }, + ...getTrigger('grain_item_added').subBlocks, + ...getTrigger('grain_item_updated').subBlocks, + ...getTrigger('grain_webhook').subBlocks, ...getTrigger('grain_recording_created').subBlocks, ...getTrigger('grain_recording_updated').subBlocks, ...getTrigger('grain_highlight_created').subBlocks, ...getTrigger('grain_highlight_updated').subBlocks, ...getTrigger('grain_story_created').subBlocks, - ...getTrigger('grain_webhook').subBlocks, ], tools: { access: [ @@ -447,12 +449,14 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`, triggers: { enabled: true, available: [ + 'grain_item_added', + 'grain_item_updated', + 'grain_webhook', 'grain_recording_created', 'grain_recording_updated', 'grain_highlight_created', 'grain_highlight_updated', 'grain_story_created', - 'grain_webhook', ], }, } diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 0a4065e83d9..1ff68892ee1 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -233,12 +233,14 @@ export interface SubBlockConfig { id: string icon?: React.ComponentType<{ className?: string }> group?: string + hidden?: boolean }[] | (() => { label: string id: string icon?: React.ComponentType<{ className?: string }> group?: string + hidden?: boolean }[]) min?: number max?: number diff --git a/apps/sim/lib/webhooks/provider-subscriptions.ts b/apps/sim/lib/webhooks/provider-subscriptions.ts index e34f4538742..c78538883b2 100644 --- a/apps/sim/lib/webhooks/provider-subscriptions.ts +++ b/apps/sim/lib/webhooks/provider-subscriptions.ts @@ -1258,6 +1258,8 @@ export async function createGrainWebhookSubscription( } const actionMap: Record> = { + grain_item_added: ['added'], + grain_item_updated: ['updated'], grain_recording_created: ['added'], grain_recording_updated: ['updated'], grain_highlight_created: ['added'], @@ -1267,6 +1269,8 @@ export async function createGrainWebhookSubscription( const eventTypeMap: Record = { grain_webhook: [], + grain_item_added: [], + grain_item_updated: [], grain_recording_created: ['recording_added'], grain_recording_updated: ['recording_updated'], grain_highlight_created: ['highlight_added'], diff --git a/apps/sim/triggers/grain/index.ts b/apps/sim/triggers/grain/index.ts index 2a4d165413d..4b219ca1132 100644 --- a/apps/sim/triggers/grain/index.ts +++ b/apps/sim/triggers/grain/index.ts @@ -1,5 +1,7 @@ export { grainHighlightCreatedTrigger } from './highlight_created' export { grainHighlightUpdatedTrigger } from './highlight_updated' +export { grainItemAddedTrigger } from './item_added' +export { grainItemUpdatedTrigger } from './item_updated' export { grainRecordingCreatedTrigger } from './recording_created' export { grainRecordingUpdatedTrigger } from './recording_updated' export { grainStoryCreatedTrigger } from './story_created' diff --git a/apps/sim/triggers/grain/item_added.ts b/apps/sim/triggers/grain/item_added.ts new file mode 100644 index 00000000000..1bca0d1b782 --- /dev/null +++ b/apps/sim/triggers/grain/item_added.ts @@ -0,0 +1,76 @@ +import { GrainIcon } from '@/components/icons' +import type { TriggerConfig } from '@/triggers/types' +import { buildGenericOutputs, grainV2SetupInstructions } from './utils' + +export const grainItemAddedTrigger: TriggerConfig = { + id: 'grain_item_added', + name: 'Grain Item Added', + provider: 'grain', + description: 'Trigger when a new item is added to a Grain view (recording, highlight, or story)', + version: '1.0.0', + icon: GrainIcon, + + subBlocks: [ + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Grain API key (Personal Access Token)', + description: 'Required to create the webhook in Grain.', + password: true, + required: true, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'grain_item_added', + }, + }, + { + id: 'viewId', + title: 'View ID', + type: 'short-input', + placeholder: 'Enter Grain view UUID', + description: + 'The view determines which content type fires events (recordings, highlights, or stories).', + required: true, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'grain_item_added', + }, + }, + { + id: 'triggerSave', + title: '', + type: 'trigger-save', + hideFromPreview: true, + mode: 'trigger', + triggerId: 'grain_item_added', + condition: { + field: 'selectedTriggerId', + value: 'grain_item_added', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: grainV2SetupInstructions('item added'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'grain_item_added', + }, + }, + ], + + outputs: buildGenericOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/grain/item_updated.ts b/apps/sim/triggers/grain/item_updated.ts new file mode 100644 index 00000000000..ca6b7b11b13 --- /dev/null +++ b/apps/sim/triggers/grain/item_updated.ts @@ -0,0 +1,76 @@ +import { GrainIcon } from '@/components/icons' +import type { TriggerConfig } from '@/triggers/types' +import { buildGenericOutputs, grainV2SetupInstructions } from './utils' + +export const grainItemUpdatedTrigger: TriggerConfig = { + id: 'grain_item_updated', + name: 'Grain Item Updated', + provider: 'grain', + description: 'Trigger when an item is updated in a Grain view (recording, highlight, or story)', + version: '1.0.0', + icon: GrainIcon, + + subBlocks: [ + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Grain API key (Personal Access Token)', + description: 'Required to create the webhook in Grain.', + password: true, + required: true, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'grain_item_updated', + }, + }, + { + id: 'viewId', + title: 'View ID', + type: 'short-input', + placeholder: 'Enter Grain view UUID', + description: + 'The view determines which content type fires events (recordings, highlights, or stories).', + required: true, + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'grain_item_updated', + }, + }, + { + id: 'triggerSave', + title: '', + type: 'trigger-save', + hideFromPreview: true, + mode: 'trigger', + triggerId: 'grain_item_updated', + condition: { + field: 'selectedTriggerId', + value: 'grain_item_updated', + }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: grainV2SetupInstructions('item updated'), + mode: 'trigger', + condition: { + field: 'selectedTriggerId', + value: 'grain_item_updated', + }, + }, + ], + + outputs: buildGenericOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/grain/utils.ts b/apps/sim/triggers/grain/utils.ts index ead2bed6cd4..3e2d60bd47b 100644 --- a/apps/sim/triggers/grain/utils.ts +++ b/apps/sim/triggers/grain/utils.ts @@ -1,15 +1,19 @@ import type { TriggerOutput } from '@/triggers/types' /** - * Shared trigger dropdown options for all Grain triggers + * Trigger dropdown options for Grain triggers. + * New options (Item Added / Item Updated / All Events) correctly scope by view_id only. + * Legacy options are hidden from the picker but still resolve for existing workflows. */ export const grainTriggerOptions = [ - { label: 'General Webhook (All Events)', id: 'grain_webhook' }, - { label: 'Recording Created', id: 'grain_recording_created' }, - { label: 'Recording Updated', id: 'grain_recording_updated' }, - { label: 'Highlight Created', id: 'grain_highlight_created' }, - { label: 'Highlight Updated', id: 'grain_highlight_updated' }, - { label: 'Story Created', id: 'grain_story_created' }, + { label: 'Item Added', id: 'grain_item_added' }, + { label: 'Item Updated', id: 'grain_item_updated' }, + { label: 'All Events', id: 'grain_webhook' }, + { label: 'Recording Created', id: 'grain_recording_created', hidden: true }, + { label: 'Recording Updated', id: 'grain_recording_updated', hidden: true }, + { label: 'Highlight Created', id: 'grain_highlight_created', hidden: true }, + { label: 'Highlight Updated', id: 'grain_highlight_updated', hidden: true }, + { label: 'Story Created', id: 'grain_story_created', hidden: true }, ] /** @@ -32,6 +36,25 @@ export function grainSetupInstructions(eventType: string): string { .join('') } +/** + * Setup instructions for the v2 triggers that correctly explain view-based scoping. + */ +export function grainV2SetupInstructions(action: string): string { + const instructions = [ + 'Enter your Grain API Key (Personal Access Token). You can find or create one in Grain at Workspace Settings > API under Integrations on grain.com.', + `Enter a Grain view ID. Each view has a type — recordings, highlights, or stories — and only items matching that type will fire the ${action} event.`, + 'To find your view IDs, use the List Views operation on this block or call GET /_/public-api/views directly.', + 'The webhook is created automatically when you save and will be deleted when you remove this trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + /** * Build output schema for recording events * Webhook payload structure: { type, user_id, data: { ...recording } } diff --git a/apps/sim/triggers/grain/webhook.ts b/apps/sim/triggers/grain/webhook.ts index 19e1362371e..5e858ca67cb 100644 --- a/apps/sim/triggers/grain/webhook.ts +++ b/apps/sim/triggers/grain/webhook.ts @@ -1,12 +1,12 @@ import { GrainIcon } from '@/components/icons' import type { TriggerConfig } from '@/triggers/types' -import { buildGenericOutputs, grainSetupInstructions } from './utils' +import { buildGenericOutputs, grainV2SetupInstructions } from './utils' export const grainWebhookTrigger: TriggerConfig = { id: 'grain_webhook', - name: 'Grain Webhook', + name: 'Grain All Events', provider: 'grain', - description: 'Generic webhook trigger for all actions in a selected Grain view', + description: 'Trigger on all actions (added, updated, removed) in a Grain view', version: '1.0.0', icon: GrainIcon, @@ -30,7 +30,8 @@ export const grainWebhookTrigger: TriggerConfig = { title: 'View ID', type: 'short-input', placeholder: 'Enter Grain view UUID', - description: 'Required by Grain to create the webhook subscription.', + description: + 'The view determines which content type fires events (recordings, highlights, or stories).', required: true, mode: 'trigger', condition: { @@ -55,7 +56,7 @@ export const grainWebhookTrigger: TriggerConfig = { title: 'Setup Instructions', hideFromPreview: true, type: 'text', - defaultValue: grainSetupInstructions('All events'), + defaultValue: grainV2SetupInstructions('all'), mode: 'trigger', condition: { field: 'selectedTriggerId', diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 90171736d99..b0c61fe8c1c 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -89,6 +89,8 @@ import { googleFormsWebhookTrigger } from '@/triggers/googleforms' import { grainHighlightCreatedTrigger, grainHighlightUpdatedTrigger, + grainItemAddedTrigger, + grainItemUpdatedTrigger, grainRecordingCreatedTrigger, grainRecordingUpdatedTrigger, grainStoryCreatedTrigger, @@ -245,6 +247,8 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { fathom_webhook: fathomWebhookTrigger, gmail_poller: gmailPollingTrigger, grain_webhook: grainWebhookTrigger, + grain_item_added: grainItemAddedTrigger, + grain_item_updated: grainItemUpdatedTrigger, grain_recording_created: grainRecordingCreatedTrigger, grain_recording_updated: grainRecordingUpdatedTrigger, grain_highlight_created: grainHighlightCreatedTrigger, From f39a30508b77f6c97cd9a7dc740fa4c131b67d59 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sun, 15 Mar 2026 00:33:49 -0700 Subject: [PATCH 2/2] address comments --- .../components/dropdown/dropdown.tsx | 18 +++++------------ .../emcn/components/combobox/combobox.tsx | 20 +++++++++++++------ apps/sim/triggers/grain/utils.ts | 9 +++++++-- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx index 135a8cf02cb..fb52e3086fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx @@ -210,12 +210,8 @@ export const Dropdown = memo(function Dropdown({ return opts }, [fetchOptions, normalizedFetchedOptions, evaluatedOptions, hydratedOption]) - const selectableOptions = useMemo(() => { - return allOptions.filter((opt) => typeof opt === 'string' || !opt.hidden) - }, [allOptions]) - const comboboxOptions = useMemo((): ComboboxOption[] => { - return selectableOptions.map((opt) => { + return allOptions.map((opt) => { if (typeof opt === 'string') { return { label: opt.toLowerCase(), value: opt } } @@ -223,18 +219,14 @@ export const Dropdown = memo(function Dropdown({ label: opt.label.toLowerCase(), value: opt.id, icon: 'icon' in opt ? opt.icon : undefined, + hidden: opt.hidden, } }) - }, [selectableOptions]) + }, [allOptions]) const optionMap = useMemo(() => { - return new Map( - allOptions.map((opt) => { - if (typeof opt === 'string') return [opt, opt.toLowerCase()] - return [opt.id, opt.label.toLowerCase()] - }) - ) - }, [allOptions]) + return new Map(comboboxOptions.map((opt) => [opt.value, opt.label])) + }, [comboboxOptions]) const defaultOptionValue = useMemo(() => { if (multiSelect) return undefined diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx index fff5b94f8e6..4b922ae8111 100644 --- a/apps/sim/components/emcn/components/combobox/combobox.tsx +++ b/apps/sim/components/emcn/components/combobox/combobox.tsx @@ -45,6 +45,8 @@ const comboboxVariants = cva( export type ComboboxOption = { label: string value: string + /** When true, hidden from the picker list but still resolves for display */ + hidden?: boolean /** Icon component to render */ icon?: React.ComponentType<{ className?: string }> /** Pre-rendered icon element (alternative to icon component) */ @@ -207,12 +209,11 @@ const Combobox = memo( * Filter options based on current value or search query */ const filteredOptions = useMemo(() => { - let result = allOptions + let result = allOptions.filter((opt) => !opt.hidden) - // Filter by editable input value if (filterOptions && value && open) { const currentValue = value.toString().toLowerCase() - const exactMatch = allOptions.find( + const exactMatch = result.find( (opt) => opt.value === value || opt.label.toLowerCase() === currentValue ) if (!exactMatch) { @@ -224,7 +225,6 @@ const Combobox = memo( } } - // Filter by search query (for searchable mode) if (searchable && searchQuery) { const query = searchQuery.toLowerCase() result = result.filter((option) => { @@ -242,10 +242,18 @@ const Combobox = memo( */ const filteredGroups = useMemo(() => { if (!groups) return null - if (!searchable || !searchQuery) return groups + + const baseGroups = groups + .map((group) => ({ + ...group, + items: group.items.filter((opt) => !opt.hidden), + })) + .filter((group) => group.items.length > 0) + + if (!searchable || !searchQuery) return baseGroups const query = searchQuery.toLowerCase() - return groups + return baseGroups .map((group) => ({ ...group, items: group.items.filter((option) => { diff --git a/apps/sim/triggers/grain/utils.ts b/apps/sim/triggers/grain/utils.ts index 3e2d60bd47b..3f3613d0a9b 100644 --- a/apps/sim/triggers/grain/utils.ts +++ b/apps/sim/triggers/grain/utils.ts @@ -39,10 +39,15 @@ export function grainSetupInstructions(eventType: string): string { /** * Setup instructions for the v2 triggers that correctly explain view-based scoping. */ -export function grainV2SetupInstructions(action: string): string { +export function grainV2SetupInstructions(action: 'item added' | 'item updated' | 'all'): string { + const viewSentence = + action === 'all' + ? 'Enter a Grain view ID. Each view has a type — recordings, highlights, or stories — and this trigger will fire on every event (added, updated, or removed) for items in that view.' + : `Enter a Grain view ID. Each view has a type — recordings, highlights, or stories — and only items matching that type will fire the ${action} event.` + const instructions = [ 'Enter your Grain API Key (Personal Access Token). You can find or create one in Grain at Workspace Settings > API under Integrations on grain.com.', - `Enter a Grain view ID. Each view has a type — recordings, highlights, or stories — and only items matching that type will fire the ${action} event.`, + viewSentence, 'To find your view IDs, use the List Views operation on this block or call GET /_/public-api/views directly.', 'The webhook is created automatically when you save and will be deleted when you remove this trigger.', ]