feat(triggers): add canonical selector toggle to google polling triggers

- Add 'trigger-advanced' mode to SubBlockConfig so canonical pairs work in trigger mode
- Fix buildCanonicalIndex: trigger-mode subblocks don't overwrite non-trigger basicId, deduplicate advancedIds from block spreads
- Update editor, subblock layout, and trigger config aggregation to include trigger-advanced subblocks
- Replace dropdown+fetchOptions in Calendar/Sheets/Drive pollers with file-selector (basic) + short-input (advanced) canonical pairs
- Add canonicalParamId: 'oauthCredential' to triggerCredentials for selector context resolution
- Update polling handlers to read canonical fallbacks (calendarId||manualCalendarId, etc.)
This commit is contained in:
waleed
2026-04-09 18:23:33 -07:00
parent f89a70a88a
commit 2180127513
11 changed files with 127 additions and 129 deletions

View File

@@ -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])

View File

@@ -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
}

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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<string, string> | 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(),
}

View File

@@ -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
})

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',
]