mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
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:
@@ -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])
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user