diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index e681382233..2423215f82 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -145,7 +145,9 @@ export function Editor() { if (!triggerMode) return subBlocks return subBlocks.filter( (subBlock) => - subBlock.mode === 'trigger' || subBlock.type === ('trigger-config' as SubBlockType) + subBlock.mode === 'trigger' || + subBlock.mode === 'trigger-advanced' || + subBlock.type === ('trigger-config' as SubBlockType) ) }, [blockConfig?.subBlocks, triggerMode]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts index 6f9d22a784..ac2554bd57 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts @@ -102,7 +102,9 @@ export function useEditorSubblockLayout( const subBlocksForCanonical = displayTriggerMode ? (config.subBlocks || []).filter( (subBlock) => - subBlock.mode === 'trigger' || subBlock.type === ('trigger-config' as SubBlockType) + subBlock.mode === 'trigger' || + subBlock.mode === 'trigger-advanced' || + subBlock.type === ('trigger-config' as SubBlockType) ) : config.subBlocks || [] const canonicalIndex = buildCanonicalIndex(subBlocksForCanonical) @@ -137,12 +139,12 @@ export function useEditorSubblockLayout( } // Filter by mode if specified - if (block.mode === 'trigger') { + if (block.mode === 'trigger' || block.mode === 'trigger-advanced') { if (!displayTriggerMode) return false } - // When in trigger mode, hide blocks that don't have mode: 'trigger' - if (displayTriggerMode && block.mode !== 'trigger') { + // When in trigger mode, hide blocks that don't have mode: 'trigger' or 'trigger-advanced' + if (displayTriggerMode && block.mode !== 'trigger' && block.mode !== 'trigger-advanced') { return false } diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index d6969cc9c2..7ce603a883 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -275,7 +275,7 @@ export interface SubBlockConfig { id: string title?: string type: SubBlockType - mode?: 'basic' | 'advanced' | 'both' | 'trigger' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode + mode?: 'basic' | 'advanced' | 'both' | 'trigger' | 'trigger-advanced' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode. 'trigger-advanced' is for advanced canonical pair members shown in trigger mode canonicalParamId?: string /** Controls parameter visibility in agent/tool-input context */ paramVisibility?: 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden' diff --git a/apps/sim/hooks/use-trigger-config-aggregation.ts b/apps/sim/hooks/use-trigger-config-aggregation.ts index 5e15edf8e9..b7dae7ecfd 100644 --- a/apps/sim/hooks/use-trigger-config-aggregation.ts +++ b/apps/sim/hooks/use-trigger-config-aggregation.ts @@ -55,7 +55,11 @@ export function useTriggerConfigAggregation( let hasAnyValue = false triggerDef.subBlocks - .filter((sb) => sb.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(sb.id)) + .filter( + (sb) => + (sb.mode === 'trigger' || sb.mode === 'trigger-advanced') && + !SYSTEM_SUBBLOCK_IDS.includes(sb.id) + ) .forEach((subBlock) => { const fieldValue = subBlockStore.getValue(blockId, subBlock.id) @@ -117,7 +121,11 @@ export function populateTriggerFieldsFromConfig( const subBlockStore = useSubBlockStore.getState() triggerDef.subBlocks - .filter((sb) => sb.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(sb.id)) + .filter( + (sb) => + (sb.mode === 'trigger' || sb.mode === 'trigger-advanced') && + !SYSTEM_SUBBLOCK_IDS.includes(sb.id) + ) .forEach((subBlock) => { let configValue: any diff --git a/apps/sim/lib/webhooks/polling/google-calendar.ts b/apps/sim/lib/webhooks/polling/google-calendar.ts index 0399939d48..16d5f23006 100644 --- a/apps/sim/lib/webhooks/polling/google-calendar.ts +++ b/apps/sim/lib/webhooks/polling/google-calendar.ts @@ -15,7 +15,8 @@ const MAX_PAGES = 10 type CalendarEventTypeFilter = '' | 'created' | 'updated' | 'cancelled' interface GoogleCalendarWebhookConfig { - calendarId: string + calendarId?: string + manualCalendarId?: string eventTypeFilter?: CalendarEventTypeFilter searchTerm?: string lastCheckedTimestamp?: string @@ -99,7 +100,7 @@ export const googleCalendarPollingHandler: PollingProviderHandler = { ) const config = webhookData.providerConfig as unknown as GoogleCalendarWebhookConfig - const calendarId = config.calendarId || 'primary' + const calendarId = config.calendarId || config.manualCalendarId || 'primary' const now = new Date() // First poll: seed timestamp, emit nothing diff --git a/apps/sim/lib/webhooks/polling/google-drive.ts b/apps/sim/lib/webhooks/polling/google-drive.ts index aa4f00a96a..af47d406ce 100644 --- a/apps/sim/lib/webhooks/polling/google-drive.ts +++ b/apps/sim/lib/webhooks/polling/google-drive.ts @@ -17,6 +17,7 @@ type DriveEventTypeFilter = '' | 'created' | 'modified' | 'deleted' | 'created_o interface GoogleDriveWebhookConfig { folderId?: string + manualFolderId?: string mimeTypeFilter?: string includeSharedDrives?: boolean eventTypeFilter?: DriveEventTypeFilter @@ -292,8 +293,9 @@ function filterChanges( if (file.trashed) return false // Folder filter: check if file is in the specified folder - if (config.folderId) { - if (!file.parents || !file.parents.includes(config.folderId)) { + const folderId = config.folderId || config.manualFolderId + if (folderId) { + if (!file.parents || !file.parents.includes(folderId)) { return false } } diff --git a/apps/sim/lib/webhooks/polling/google-sheets.ts b/apps/sim/lib/webhooks/polling/google-sheets.ts index 8a51615e86..c900befabb 100644 --- a/apps/sim/lib/webhooks/polling/google-sheets.ts +++ b/apps/sim/lib/webhooks/polling/google-sheets.ts @@ -14,8 +14,10 @@ type ValueRenderOption = 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA' type DateTimeRenderOption = 'SERIAL_NUMBER' | 'FORMATTED_STRING' interface GoogleSheetsWebhookConfig { - spreadsheetId: string - sheetName: string + spreadsheetId?: string + manualSpreadsheetId?: string + sheetName?: string + manualSheetName?: string includeHeaders: boolean valueRenderOption?: ValueRenderOption dateTimeRenderOption?: DateTimeRenderOption @@ -52,9 +54,11 @@ export const googleSheetsPollingHandler: PollingProviderHandler = { ) const config = webhookData.providerConfig as unknown as GoogleSheetsWebhookConfig + const spreadsheetId = config.spreadsheetId || config.manualSpreadsheetId + const sheetName = config.sheetName || config.manualSheetName const now = new Date() - if (!config?.spreadsheetId || !config?.sheetName) { + if (!spreadsheetId || !sheetName) { logger.error(`[${requestId}] Missing spreadsheetId or sheetName for webhook ${webhookId}`) await markWebhookFailed(webhookId, logger) return 'failure' @@ -63,7 +67,7 @@ export const googleSheetsPollingHandler: PollingProviderHandler = { // Pre-check: use Drive API to see if the file was modified since last poll const { unchanged: skipPoll, currentModifiedTime } = await isDriveFileUnchanged( accessToken, - config.spreadsheetId, + spreadsheetId, config.lastModifiedTime, requestId, logger @@ -83,8 +87,8 @@ export const googleSheetsPollingHandler: PollingProviderHandler = { // Fetch current row count via column A const currentRowCount = await getDataRowCount( accessToken, - config.spreadsheetId, - config.sheetName, + spreadsheetId, + sheetName, requestId, logger ) @@ -148,8 +152,8 @@ export const googleSheetsPollingHandler: PollingProviderHandler = { if (config.includeHeaders !== false) { headers = await fetchHeaderRow( accessToken, - config.spreadsheetId, - config.sheetName, + spreadsheetId, + sheetName, valueRender, dateTimeRender, requestId, @@ -161,8 +165,8 @@ export const googleSheetsPollingHandler: PollingProviderHandler = { // because lastKnownRowCount includes the header row const newRows = await fetchRowRange( accessToken, - config.spreadsheetId, - config.sheetName, + spreadsheetId, + sheetName, startRow, endRow, valueRender, @@ -175,6 +179,8 @@ export const googleSheetsPollingHandler: PollingProviderHandler = { newRows, headers, startRow, + spreadsheetId, + sheetName, config, webhookData, workflowData, @@ -373,6 +379,8 @@ async function processRows( rows: string[][], headers: string[], startRowIndex: number, + spreadsheetId: string, + sheetName: string, config: GoogleSheetsWebhookConfig, webhookData: PollWebhookContext['webhookData'], workflowData: PollWebhookContext['workflowData'], @@ -389,7 +397,7 @@ async function processRows( try { await pollingIdempotency.executeWithIdempotency( 'google-sheets', - `${webhookData.id}:${config.spreadsheetId}:${config.sheetName}:row${rowNumber}:${row.join('|')}`, + `${webhookData.id}:${spreadsheetId}:${sheetName}:row${rowNumber}:${row.join('|')}`, async () => { // Map row values to headers let mappedRow: Record | null = null @@ -410,8 +418,8 @@ async function processRows( rawRow: row, headers, rowNumber, - spreadsheetId: config.spreadsheetId, - sheetName: config.sheetName, + spreadsheetId, + sheetName, timestamp: new Date().toISOString(), } diff --git a/apps/sim/lib/workflows/subblocks/visibility.ts b/apps/sim/lib/workflows/subblocks/visibility.ts index 356ab0507b..55c4de1c69 100644 --- a/apps/sim/lib/workflows/subblocks/visibility.ts +++ b/apps/sim/lib/workflows/subblocks/visibility.ts @@ -58,10 +58,17 @@ export function buildCanonicalIndex(subBlocks: SubBlockConfig[]): CanonicalIndex groupsById[canonicalId] = { canonicalId, advancedIds: [] } } const group = groupsById[canonicalId] - if (subBlock.mode === 'advanced') { - group.advancedIds.push(subBlock.id) + if (subBlock.mode === 'advanced' || subBlock.mode === 'trigger-advanced') { + // Deduplicate: trigger spreads may repeat the same advanced ID as the regular block + if (!group.advancedIds.includes(subBlock.id)) { + group.advancedIds.push(subBlock.id) + } } else { - group.basicId = subBlock.id + // A trigger-mode subblock must not overwrite a basicId already claimed by a non-trigger subblock. + // Blocks spread their trigger's subBlocks after their own, so the regular subblock always wins. + if (!group.basicId || subBlock.mode !== 'trigger') { + group.basicId = subBlock.id + } } canonicalIdBySubBlockId[subBlock.id] = canonicalId }) diff --git a/apps/sim/triggers/google-calendar/poller.ts b/apps/sim/triggers/google-calendar/poller.ts index 15dcf4bc89..2b39cf1ab8 100644 --- a/apps/sim/triggers/google-calendar/poller.ts +++ b/apps/sim/triggers/google-calendar/poller.ts @@ -1,13 +1,6 @@ -import { createLogger } from '@sim/logger' import { GoogleCalendarIcon } from '@/components/icons' -import { isCredentialSetValue } from '@/executor/constants' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' import type { TriggerConfig } from '@/triggers/types' -const logger = createLogger('GoogleCalendarPollingTrigger') - -const DEFAULT_CALENDARS = [{ id: 'primary', label: 'Primary Calendar' }] - export const googleCalendarPollingTrigger: TriggerConfig = { id: 'google_calendar_poller', name: 'Google Calendar Event Trigger', @@ -28,54 +21,30 @@ export const googleCalendarPollingTrigger: TriggerConfig = { required: true, mode: 'trigger', supportsCredentialSets: true, + canonicalParamId: 'oauthCredential', }, { id: 'calendarId', title: 'Calendar', - type: 'dropdown', - placeholder: 'Select a calendar', + type: 'file-selector', description: 'The calendar to monitor for event changes.', required: false, - defaultValue: 'primary', - options: [], - fetchOptions: async (blockId: string) => { - const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as - | string - | null - - if (!credentialId) { - throw new Error('No Google Calendar credential selected') - } - - // Credential sets can't fetch user-specific calendars - if (isCredentialSetValue(credentialId)) { - return DEFAULT_CALENDARS - } - - try { - const response = await fetch( - `/api/tools/google_calendar/calendars?credentialId=${credentialId}` - ) - if (!response.ok) { - throw new Error('Failed to fetch calendars') - } - const data = await response.json() - if (data.calendars && Array.isArray(data.calendars)) { - return data.calendars.map( - (calendar: { id: string; summary: string; primary: boolean }) => ({ - id: calendar.id, - label: calendar.primary ? `${calendar.summary} (Primary)` : calendar.summary, - }) - ) - } - return DEFAULT_CALENDARS - } catch (error) { - logger.error('Error fetching calendars:', error) - throw error - } - }, - dependsOn: ['triggerCredentials'], mode: 'trigger', + canonicalParamId: 'calendarId', + serviceId: 'google-calendar', + selectorKey: 'google.calendar', + selectorAllowSearch: false, + dependsOn: ['triggerCredentials'], + }, + { + id: 'manualCalendarId', + title: 'Calendar ID', + type: 'short-input', + placeholder: 'Enter calendar ID (e.g., primary or calendar@gmail.com)', + description: 'The calendar to monitor for event changes.', + required: false, + mode: 'trigger-advanced', + canonicalParamId: 'calendarId', }, { id: 'eventTypeFilter', diff --git a/apps/sim/triggers/google-drive/poller.ts b/apps/sim/triggers/google-drive/poller.ts index 51289233a7..6911643a6b 100644 --- a/apps/sim/triggers/google-drive/poller.ts +++ b/apps/sim/triggers/google-drive/poller.ts @@ -31,16 +31,31 @@ export const googleDrivePollingTrigger: TriggerConfig = { required: true, mode: 'trigger', supportsCredentialSets: true, + canonicalParamId: 'oauthCredential', }, { id: 'folderId', + title: 'Folder', + type: 'file-selector', + description: 'Optional: The folder to monitor. Leave empty to monitor all files in Drive.', + required: false, + mode: 'trigger', + canonicalParamId: 'folderId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + mimeType: 'application/vnd.google-apps.folder', + dependsOn: ['triggerCredentials'], + }, + { + id: 'manualFolderId', title: 'Folder ID', type: 'short-input', placeholder: 'Leave empty to monitor entire Drive', description: 'Optional: The folder ID from the Google Drive URL to monitor. Leave empty to monitor all files.', required: false, - mode: 'trigger', + mode: 'trigger-advanced', + canonicalParamId: 'folderId', }, { id: 'mimeTypeFilter', diff --git a/apps/sim/triggers/google-sheets/poller.ts b/apps/sim/triggers/google-sheets/poller.ts index 0655bc5ca0..8d2f6a97f5 100644 --- a/apps/sim/triggers/google-sheets/poller.ts +++ b/apps/sim/triggers/google-sheets/poller.ts @@ -1,11 +1,6 @@ -import { createLogger } from '@sim/logger' import { GoogleSheetsIcon } from '@/components/icons' -import { isCredentialSetValue } from '@/executor/constants' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' import type { TriggerConfig } from '@/triggers/types' -const logger = createLogger('GoogleSheetsPollingTrigger') - export const googleSheetsPollingTrigger: TriggerConfig = { id: 'google_sheets_poller', name: 'Google Sheets New Row Trigger', @@ -26,64 +21,53 @@ export const googleSheetsPollingTrigger: TriggerConfig = { required: true, mode: 'trigger', supportsCredentialSets: true, + canonicalParamId: 'oauthCredential', }, { id: 'spreadsheetId', - title: 'Spreadsheet ID', - type: 'short-input', - placeholder: 'e.g., 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms', - description: - 'The spreadsheet ID from the URL: docs.google.com/spreadsheets/d/{spreadsheetId}/edit', + title: 'Spreadsheet', + type: 'file-selector', + description: 'The spreadsheet to monitor for new rows.', required: true, mode: 'trigger', + canonicalParamId: 'spreadsheetId', + serviceId: 'google-sheets', + selectorKey: 'google.drive', + mimeType: 'application/vnd.google-apps.spreadsheet', + dependsOn: ['triggerCredentials'], + }, + { + id: 'manualSpreadsheetId', + title: 'Spreadsheet ID', + type: 'short-input', + placeholder: 'ID from URL: docs.google.com/spreadsheets/d/{ID}/edit', + description: 'The spreadsheet to monitor for new rows.', + required: true, + mode: 'trigger-advanced', + canonicalParamId: 'spreadsheetId', }, { id: 'sheetName', title: 'Sheet Tab', - type: 'dropdown', - placeholder: 'Select a sheet tab', + type: 'sheet-selector', description: 'The sheet tab to monitor for new rows.', required: true, - options: [], - fetchOptions: async (blockId: string) => { - const subBlockStore = useSubBlockStore.getState() - const credentialId = subBlockStore.getValue(blockId, 'triggerCredentials') as string | null - const spreadsheetId = subBlockStore.getValue(blockId, 'spreadsheetId') as string | null - - if (!credentialId) { - throw new Error('No Google Sheets credential selected') - } - if (!spreadsheetId) { - throw new Error('No spreadsheet ID provided') - } - - // Credential sets can't fetch user-specific data; return empty to allow manual entry - if (isCredentialSetValue(credentialId)) { - return [] - } - - try { - const response = await fetch( - `/api/tools/google_sheets/sheets?credentialId=${credentialId}&spreadsheetId=${spreadsheetId}` - ) - if (!response.ok) { - throw new Error('Failed to fetch sheet tabs') - } - const data = await response.json() - if (data.sheets && Array.isArray(data.sheets)) { - return data.sheets.map((sheet: { id: string; name: string }) => ({ - id: sheet.id, - label: sheet.name, - })) - } - return [] - } catch (error) { - logger.error('Error fetching sheet tabs:', error) - throw error - } - }, - dependsOn: ['triggerCredentials', 'spreadsheetId'], mode: 'trigger', + canonicalParamId: 'sheetName', + serviceId: 'google-sheets', + selectorKey: 'google.sheets', + selectorAllowSearch: false, + dependsOn: { all: ['triggerCredentials'], any: ['spreadsheetId', 'manualSpreadsheetId'] }, + }, + { + id: 'manualSheetName', + title: 'Sheet Tab Name', + type: 'short-input', + placeholder: 'Enter sheet tab name (e.g., Sheet1)', + description: 'The sheet tab to monitor for new rows.', + required: true, + mode: 'trigger-advanced', + canonicalParamId: 'sheetName', }, { id: 'includeHeaders', @@ -139,7 +123,7 @@ export const googleSheetsPollingTrigger: TriggerConfig = { type: 'text', defaultValue: [ 'Connect your Google account using OAuth credentials', - 'Enter the Spreadsheet ID from your Google Sheets URL', + 'Select the spreadsheet to monitor', 'Select the sheet tab to monitor', 'The system will automatically detect new rows appended to the sheet', ]