Skip to content

Commit 2180127

Browse files
committed
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.)
1 parent f89a70a commit 2180127

11 files changed

Lines changed: 126 additions & 128 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ export function Editor() {
145145
if (!triggerMode) return subBlocks
146146
return subBlocks.filter(
147147
(subBlock) =>
148-
subBlock.mode === 'trigger' || subBlock.type === ('trigger-config' as SubBlockType)
148+
subBlock.mode === 'trigger' ||
149+
subBlock.mode === 'trigger-advanced' ||
150+
subBlock.type === ('trigger-config' as SubBlockType)
149151
)
150152
}, [blockConfig?.subBlocks, triggerMode])
151153

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ export function useEditorSubblockLayout(
102102
const subBlocksForCanonical = displayTriggerMode
103103
? (config.subBlocks || []).filter(
104104
(subBlock) =>
105-
subBlock.mode === 'trigger' || subBlock.type === ('trigger-config' as SubBlockType)
105+
subBlock.mode === 'trigger' ||
106+
subBlock.mode === 'trigger-advanced' ||
107+
subBlock.type === ('trigger-config' as SubBlockType)
106108
)
107109
: config.subBlocks || []
108110
const canonicalIndex = buildCanonicalIndex(subBlocksForCanonical)
@@ -137,12 +139,12 @@ export function useEditorSubblockLayout(
137139
}
138140

139141
// Filter by mode if specified
140-
if (block.mode === 'trigger') {
142+
if (block.mode === 'trigger' || block.mode === 'trigger-advanced') {
141143
if (!displayTriggerMode) return false
142144
}
143145

144-
// When in trigger mode, hide blocks that don't have mode: 'trigger'
145-
if (displayTriggerMode && block.mode !== 'trigger') {
146+
// When in trigger mode, hide blocks that don't have mode: 'trigger' or 'trigger-advanced'
147+
if (displayTriggerMode && block.mode !== 'trigger' && block.mode !== 'trigger-advanced') {
146148
return false
147149
}
148150

apps/sim/blocks/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ export interface SubBlockConfig {
275275
id: string
276276
title?: string
277277
type: SubBlockType
278-
mode?: 'basic' | 'advanced' | 'both' | 'trigger' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode
278+
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
279279
canonicalParamId?: string
280280
/** Controls parameter visibility in agent/tool-input context */
281281
paramVisibility?: 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden'

apps/sim/hooks/use-trigger-config-aggregation.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ export function useTriggerConfigAggregation(
5555
let hasAnyValue = false
5656

5757
triggerDef.subBlocks
58-
.filter((sb) => sb.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(sb.id))
58+
.filter(
59+
(sb) =>
60+
(sb.mode === 'trigger' || sb.mode === 'trigger-advanced') &&
61+
!SYSTEM_SUBBLOCK_IDS.includes(sb.id)
62+
)
5963
.forEach((subBlock) => {
6064
const fieldValue = subBlockStore.getValue(blockId, subBlock.id)
6165

@@ -117,7 +121,11 @@ export function populateTriggerFieldsFromConfig(
117121
const subBlockStore = useSubBlockStore.getState()
118122

119123
triggerDef.subBlocks
120-
.filter((sb) => sb.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(sb.id))
124+
.filter(
125+
(sb) =>
126+
(sb.mode === 'trigger' || sb.mode === 'trigger-advanced') &&
127+
!SYSTEM_SUBBLOCK_IDS.includes(sb.id)
128+
)
121129
.forEach((subBlock) => {
122130
let configValue: any
123131

apps/sim/lib/webhooks/polling/google-calendar.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const MAX_PAGES = 10
1515
type CalendarEventTypeFilter = '' | 'created' | 'updated' | 'cancelled'
1616

1717
interface GoogleCalendarWebhookConfig {
18-
calendarId: string
18+
calendarId?: string
19+
manualCalendarId?: string
1920
eventTypeFilter?: CalendarEventTypeFilter
2021
searchTerm?: string
2122
lastCheckedTimestamp?: string
@@ -99,7 +100,7 @@ export const googleCalendarPollingHandler: PollingProviderHandler = {
99100
)
100101

101102
const config = webhookData.providerConfig as unknown as GoogleCalendarWebhookConfig
102-
const calendarId = config.calendarId || 'primary'
103+
const calendarId = config.calendarId || config.manualCalendarId || 'primary'
103104
const now = new Date()
104105

105106
// First poll: seed timestamp, emit nothing

apps/sim/lib/webhooks/polling/google-drive.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type DriveEventTypeFilter = '' | 'created' | 'modified' | 'deleted' | 'created_o
1717

1818
interface GoogleDriveWebhookConfig {
1919
folderId?: string
20+
manualFolderId?: string
2021
mimeTypeFilter?: string
2122
includeSharedDrives?: boolean
2223
eventTypeFilter?: DriveEventTypeFilter
@@ -292,8 +293,9 @@ function filterChanges(
292293
if (file.trashed) return false
293294

294295
// Folder filter: check if file is in the specified folder
295-
if (config.folderId) {
296-
if (!file.parents || !file.parents.includes(config.folderId)) {
296+
const folderId = config.folderId || config.manualFolderId
297+
if (folderId) {
298+
if (!file.parents || !file.parents.includes(folderId)) {
297299
return false
298300
}
299301
}

apps/sim/lib/webhooks/polling/google-sheets.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ type ValueRenderOption = 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA'
1414
type DateTimeRenderOption = 'SERIAL_NUMBER' | 'FORMATTED_STRING'
1515

1616
interface GoogleSheetsWebhookConfig {
17-
spreadsheetId: string
18-
sheetName: string
17+
spreadsheetId?: string
18+
manualSpreadsheetId?: string
19+
sheetName?: string
20+
manualSheetName?: string
1921
includeHeaders: boolean
2022
valueRenderOption?: ValueRenderOption
2123
dateTimeRenderOption?: DateTimeRenderOption
@@ -52,9 +54,11 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
5254
)
5355

5456
const config = webhookData.providerConfig as unknown as GoogleSheetsWebhookConfig
57+
const spreadsheetId = config.spreadsheetId || config.manualSpreadsheetId
58+
const sheetName = config.sheetName || config.manualSheetName
5559
const now = new Date()
5660

57-
if (!config?.spreadsheetId || !config?.sheetName) {
61+
if (!spreadsheetId || !sheetName) {
5862
logger.error(`[${requestId}] Missing spreadsheetId or sheetName for webhook ${webhookId}`)
5963
await markWebhookFailed(webhookId, logger)
6064
return 'failure'
@@ -63,7 +67,7 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
6367
// Pre-check: use Drive API to see if the file was modified since last poll
6468
const { unchanged: skipPoll, currentModifiedTime } = await isDriveFileUnchanged(
6569
accessToken,
66-
config.spreadsheetId,
70+
spreadsheetId,
6771
config.lastModifiedTime,
6872
requestId,
6973
logger
@@ -83,8 +87,8 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
8387
// Fetch current row count via column A
8488
const currentRowCount = await getDataRowCount(
8589
accessToken,
86-
config.spreadsheetId,
87-
config.sheetName,
90+
spreadsheetId,
91+
sheetName,
8892
requestId,
8993
logger
9094
)
@@ -148,8 +152,8 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
148152
if (config.includeHeaders !== false) {
149153
headers = await fetchHeaderRow(
150154
accessToken,
151-
config.spreadsheetId,
152-
config.sheetName,
155+
spreadsheetId,
156+
sheetName,
153157
valueRender,
154158
dateTimeRender,
155159
requestId,
@@ -161,8 +165,8 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
161165
// because lastKnownRowCount includes the header row
162166
const newRows = await fetchRowRange(
163167
accessToken,
164-
config.spreadsheetId,
165-
config.sheetName,
168+
spreadsheetId,
169+
sheetName,
166170
startRow,
167171
endRow,
168172
valueRender,
@@ -175,6 +179,8 @@ export const googleSheetsPollingHandler: PollingProviderHandler = {
175179
newRows,
176180
headers,
177181
startRow,
182+
spreadsheetId,
183+
sheetName,
178184
config,
179185
webhookData,
180186
workflowData,
@@ -373,6 +379,8 @@ async function processRows(
373379
rows: string[][],
374380
headers: string[],
375381
startRowIndex: number,
382+
spreadsheetId: string,
383+
sheetName: string,
376384
config: GoogleSheetsWebhookConfig,
377385
webhookData: PollWebhookContext['webhookData'],
378386
workflowData: PollWebhookContext['workflowData'],
@@ -389,7 +397,7 @@ async function processRows(
389397
try {
390398
await pollingIdempotency.executeWithIdempotency(
391399
'google-sheets',
392-
`${webhookData.id}:${config.spreadsheetId}:${config.sheetName}:row${rowNumber}:${row.join('|')}`,
400+
`${webhookData.id}:${spreadsheetId}:${sheetName}:row${rowNumber}:${row.join('|')}`,
393401
async () => {
394402
// Map row values to headers
395403
let mappedRow: Record<string, string> | null = null
@@ -410,8 +418,8 @@ async function processRows(
410418
rawRow: row,
411419
headers,
412420
rowNumber,
413-
spreadsheetId: config.spreadsheetId,
414-
sheetName: config.sheetName,
421+
spreadsheetId,
422+
sheetName,
415423
timestamp: new Date().toISOString(),
416424
}
417425

apps/sim/lib/workflows/subblocks/visibility.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,17 @@ export function buildCanonicalIndex(subBlocks: SubBlockConfig[]): CanonicalIndex
5858
groupsById[canonicalId] = { canonicalId, advancedIds: [] }
5959
}
6060
const group = groupsById[canonicalId]
61-
if (subBlock.mode === 'advanced') {
62-
group.advancedIds.push(subBlock.id)
61+
if (subBlock.mode === 'advanced' || subBlock.mode === 'trigger-advanced') {
62+
// Deduplicate: trigger spreads may repeat the same advanced ID as the regular block
63+
if (!group.advancedIds.includes(subBlock.id)) {
64+
group.advancedIds.push(subBlock.id)
65+
}
6366
} else {
64-
group.basicId = subBlock.id
67+
// A trigger-mode subblock must not overwrite a basicId already claimed by a non-trigger subblock.
68+
// Blocks spread their trigger's subBlocks after their own, so the regular subblock always wins.
69+
if (!group.basicId || subBlock.mode !== 'trigger') {
70+
group.basicId = subBlock.id
71+
}
6572
}
6673
canonicalIdBySubBlockId[subBlock.id] = canonicalId
6774
})

apps/sim/triggers/google-calendar/poller.ts

Lines changed: 17 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
import { createLogger } from '@sim/logger'
21
import { GoogleCalendarIcon } from '@/components/icons'
3-
import { isCredentialSetValue } from '@/executor/constants'
4-
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
52
import type { TriggerConfig } from '@/triggers/types'
63

7-
const logger = createLogger('GoogleCalendarPollingTrigger')
8-
9-
const DEFAULT_CALENDARS = [{ id: 'primary', label: 'Primary Calendar' }]
10-
114
export const googleCalendarPollingTrigger: TriggerConfig = {
125
id: 'google_calendar_poller',
136
name: 'Google Calendar Event Trigger',
@@ -28,54 +21,30 @@ export const googleCalendarPollingTrigger: TriggerConfig = {
2821
required: true,
2922
mode: 'trigger',
3023
supportsCredentialSets: true,
24+
canonicalParamId: 'oauthCredential',
3125
},
3226
{
3327
id: 'calendarId',
3428
title: 'Calendar',
35-
type: 'dropdown',
36-
placeholder: 'Select a calendar',
29+
type: 'file-selector',
3730
description: 'The calendar to monitor for event changes.',
3831
required: false,
39-
defaultValue: 'primary',
40-
options: [],
41-
fetchOptions: async (blockId: string) => {
42-
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
43-
| string
44-
| null
45-
46-
if (!credentialId) {
47-
throw new Error('No Google Calendar credential selected')
48-
}
49-
50-
// Credential sets can't fetch user-specific calendars
51-
if (isCredentialSetValue(credentialId)) {
52-
return DEFAULT_CALENDARS
53-
}
54-
55-
try {
56-
const response = await fetch(
57-
`/api/tools/google_calendar/calendars?credentialId=${credentialId}`
58-
)
59-
if (!response.ok) {
60-
throw new Error('Failed to fetch calendars')
61-
}
62-
const data = await response.json()
63-
if (data.calendars && Array.isArray(data.calendars)) {
64-
return data.calendars.map(
65-
(calendar: { id: string; summary: string; primary: boolean }) => ({
66-
id: calendar.id,
67-
label: calendar.primary ? `${calendar.summary} (Primary)` : calendar.summary,
68-
})
69-
)
70-
}
71-
return DEFAULT_CALENDARS
72-
} catch (error) {
73-
logger.error('Error fetching calendars:', error)
74-
throw error
75-
}
76-
},
77-
dependsOn: ['triggerCredentials'],
7832
mode: 'trigger',
33+
canonicalParamId: 'calendarId',
34+
serviceId: 'google-calendar',
35+
selectorKey: 'google.calendar',
36+
selectorAllowSearch: false,
37+
dependsOn: ['triggerCredentials'],
38+
},
39+
{
40+
id: 'manualCalendarId',
41+
title: 'Calendar ID',
42+
type: 'short-input',
43+
placeholder: 'Enter calendar ID (e.g., primary or calendar@gmail.com)',
44+
description: 'The calendar to monitor for event changes.',
45+
required: false,
46+
mode: 'trigger-advanced',
47+
canonicalParamId: 'calendarId',
7948
},
8049
{
8150
id: 'eventTypeFilter',

apps/sim/triggers/google-drive/poller.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,31 @@ export const googleDrivePollingTrigger: TriggerConfig = {
3131
required: true,
3232
mode: 'trigger',
3333
supportsCredentialSets: true,
34+
canonicalParamId: 'oauthCredential',
3435
},
3536
{
3637
id: 'folderId',
38+
title: 'Folder',
39+
type: 'file-selector',
40+
description: 'Optional: The folder to monitor. Leave empty to monitor all files in Drive.',
41+
required: false,
42+
mode: 'trigger',
43+
canonicalParamId: 'folderId',
44+
serviceId: 'google-drive',
45+
selectorKey: 'google.drive',
46+
mimeType: 'application/vnd.google-apps.folder',
47+
dependsOn: ['triggerCredentials'],
48+
},
49+
{
50+
id: 'manualFolderId',
3751
title: 'Folder ID',
3852
type: 'short-input',
3953
placeholder: 'Leave empty to monitor entire Drive',
4054
description:
4155
'Optional: The folder ID from the Google Drive URL to monitor. Leave empty to monitor all files.',
4256
required: false,
43-
mode: 'trigger',
57+
mode: 'trigger-advanced',
58+
canonicalParamId: 'folderId',
4459
},
4560
{
4661
id: 'mimeTypeFilter',

0 commit comments

Comments
 (0)