fix(webflow): fix collection & site dropdown in webflow triggers (#2849)

* fix(webflow): fix collection & site dropdown in webflow triggers

* added form submission trigger to webflow

* fix(webflow): added form submission trigger and scope

* fixed function signatures
This commit is contained in:
Waleed
2026-01-16 08:22:09 -08:00
committed by GitHub
parent 6ff68b39ce
commit 583f5c4cbb
9 changed files with 476 additions and 36 deletions

View File

@@ -127,6 +127,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
...getTrigger('webflow_collection_item_created').subBlocks,
...getTrigger('webflow_collection_item_changed').subBlocks,
...getTrigger('webflow_collection_item_deleted').subBlocks,
...getTrigger('webflow_form_submission').subBlocks,
],
tools: {
access: [

View File

@@ -1858,7 +1858,7 @@ export const auth = betterAuth({
authorizationUrl: 'https://webflow.com/oauth/authorize',
tokenUrl: 'https://api.webflow.com/oauth/access_token',
userInfoUrl: 'https://api.webflow.com/v2/token/introspect',
scopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
scopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write', 'forms:read'],
responseType: 'code',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/webflow`,
getUserInfo: async (tokens) => {

View File

@@ -251,6 +251,20 @@ export function shouldSkipWebhookEvent(webhook: any, body: any, requestId: strin
}
}
// Webflow collection filtering - filter by collectionId if configured
if (webhook.provider === 'webflow') {
const configuredCollectionId = providerConfig.collectionId
if (configuredCollectionId) {
const payloadCollectionId = body?.payload?.collectionId || body?.collectionId
if (payloadCollectionId && payloadCollectionId !== configuredCollectionId) {
logger.info(
`[${requestId}] Webflow collection '${payloadCollectionId}' doesn't match configured collection '${configuredCollectionId}' for webhook ${webhook.id}, skipping`
)
return true
}
}
}
return false
}

View File

@@ -1400,7 +1400,7 @@ export async function createWebflowWebhookSubscription(
): Promise<string | undefined> {
try {
const { path, providerConfig } = webhookData
const { siteId, triggerId, collectionId, formId } = providerConfig || {}
const { siteId, triggerId, collectionId, formName } = providerConfig || {}
if (!siteId) {
webflowLogger.warn(`[${requestId}] Missing siteId for Webflow webhook creation.`, {
@@ -1455,17 +1455,10 @@ export async function createWebflowWebhookSubscription(
url: notificationUrl,
}
if (collectionId && webflowTriggerType.startsWith('collection_item_')) {
// Note: Webflow API only supports 'filter' for form_submission triggers.
if (formName && webflowTriggerType === 'form_submission') {
requestBody.filter = {
resource_type: 'collection',
resource_id: collectionId,
}
}
if (formId && webflowTriggerType === 'form_submission') {
requestBody.filter = {
resource_type: 'form',
resource_id: formId,
name: formName,
}
}

View File

@@ -800,15 +800,39 @@ export async function formatWebhookInput(
}
if (foundWebhook.provider === 'webflow') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined
// Form submission trigger
if (triggerId === 'webflow_form_submission') {
return {
siteId: body?.siteId || '',
formId: body?.formId || '',
name: body?.name || '',
id: body?.id || '',
submittedAt: body?.submittedAt || '',
data: body?.data || {},
schema: body?.schema || {},
formElementId: body?.formElementId || '',
}
}
// Collection item triggers (created, changed, deleted)
// Webflow uses _cid for collection ID and _id for item ID
const { _cid, _id, ...itemFields } = body || {}
return {
siteId: body?.siteId || '',
formId: body?.formId || '',
name: body?.name || '',
id: body?.id || '',
submittedAt: body?.submittedAt || '',
data: body?.data || {},
schema: body?.schema || {},
formElementId: body?.formElementId || '',
collectionId: _cid || body?.collectionId || '',
payload: {
id: _id || '',
cmsLocaleId: itemFields?.cmsLocaleId || '',
lastPublished: itemFields?.lastPublished || itemFields?.['last-published'] || '',
lastUpdated: itemFields?.lastUpdated || itemFields?.['last-updated'] || '',
createdOn: itemFields?.createdOn || itemFields?.['created-on'] || '',
isArchived: itemFields?.isArchived || itemFields?._archived || false,
isDraft: itemFields?.isDraft || itemFields?._draft || false,
fieldData: itemFields,
},
}
}

View File

@@ -1,6 +1,10 @@
import { createLogger } from '@sim/logger'
import { WebflowIcon } from '@/components/icons'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import type { TriggerConfig } from '../types'
const logger = createLogger('webflow-collection-item-changed-trigger')
export const webflowCollectionItemChangedTrigger: TriggerConfig = {
id: 'webflow_collection_item_changed',
name: 'Collection Item Changed',
@@ -38,6 +42,58 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
field: 'selectedTriggerId',
value: 'webflow_collection_item_changed',
},
fetchOptions: async (blockId: string, _subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) {
throw new Error('No Webflow credential selected')
}
try {
const response = await fetch('/api/tools/webflow/sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId }),
})
if (!response.ok) {
throw new Error('Failed to fetch Webflow sites')
}
const data = await response.json()
if (data.sites && Array.isArray(data.sites)) {
return data.sites.map((site: { id: string; name: string }) => ({
id: site.id,
label: site.name,
}))
}
return []
} catch (error) {
logger.error('Error fetching Webflow sites:', error)
throw error
}
},
fetchOptionById: async (blockId: string, _subBlockId: string, optionId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) return null
try {
const response = await fetch('/api/tools/webflow/sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId: optionId }),
})
if (!response.ok) return null
const data = await response.json()
const site = data.sites?.find((s: { id: string }) => s.id === optionId)
if (site) {
return { id: site.id, label: site.name }
}
return null
} catch {
return null
}
},
dependsOn: ['triggerCredentials'],
},
{
id: 'collectionId',
@@ -52,6 +108,60 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
field: 'selectedTriggerId',
value: 'webflow_collection_item_changed',
},
fetchOptions: async (blockId: string, _subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
if (!credentialId || !siteId) {
return []
}
try {
const response = await fetch('/api/tools/webflow/collections', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId }),
})
if (!response.ok) {
throw new Error('Failed to fetch Webflow collections')
}
const data = await response.json()
if (data.collections && Array.isArray(data.collections)) {
return data.collections.map((collection: { id: string; name: string }) => ({
id: collection.id,
label: collection.name,
}))
}
return []
} catch (error) {
logger.error('Error fetching Webflow collections:', error)
throw error
}
},
fetchOptionById: async (blockId: string, _subBlockId: string, optionId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
if (!credentialId || !siteId) return null
try {
const response = await fetch('/api/tools/webflow/collections', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId }),
})
if (!response.ok) return null
const data = await response.json()
const collection = data.collections?.find((c: { id: string }) => c.id === optionId)
if (collection) {
return { id: collection.id, label: collection.name }
}
return null
} catch {
return null
}
},
dependsOn: ['triggerCredentials', 'siteId'],
},
{
id: 'triggerSave',
@@ -72,9 +182,9 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
type: 'text',
defaultValue: [
'Connect your Webflow account using the "Select Webflow credential" button above.',
'Enter your Webflow Site ID (found in the site URL or site settings).',
'Optionally enter a Collection ID to monitor only specific collections.',
'If no Collection ID is provided, the trigger will fire for items changed in any collection on the site.',
'Select your Webflow site from the dropdown.',
'Optionally select a collection to monitor only specific collections.',
'If no collection is selected, the trigger will fire for items changed in any collection on the site.',
'The webhook will trigger whenever an existing item is updated in the specified collection(s).',
'Make sure your Webflow account has appropriate permissions for the specified site.',
]

View File

@@ -1,6 +1,10 @@
import { createLogger } from '@sim/logger'
import { WebflowIcon } from '@/components/icons'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import type { TriggerConfig } from '../types'
const logger = createLogger('webflow-collection-item-created-trigger')
export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
id: 'webflow_collection_item_created',
name: 'Collection Item Created',
@@ -20,6 +24,7 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
{ label: 'Collection Item Created', id: 'webflow_collection_item_created' },
{ label: 'Collection Item Changed', id: 'webflow_collection_item_changed' },
{ label: 'Collection Item Deleted', id: 'webflow_collection_item_deleted' },
{ label: 'Form Submission', id: 'webflow_form_submission' },
],
value: () => 'webflow_collection_item_created',
required: true,
@@ -51,6 +56,58 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
field: 'selectedTriggerId',
value: 'webflow_collection_item_created',
},
fetchOptions: async (blockId: string, _subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) {
throw new Error('No Webflow credential selected')
}
try {
const response = await fetch('/api/tools/webflow/sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId }),
})
if (!response.ok) {
throw new Error('Failed to fetch Webflow sites')
}
const data = await response.json()
if (data.sites && Array.isArray(data.sites)) {
return data.sites.map((site: { id: string; name: string }) => ({
id: site.id,
label: site.name,
}))
}
return []
} catch (error) {
logger.error('Error fetching Webflow sites:', error)
throw error
}
},
fetchOptionById: async (blockId: string, _subBlockId: string, optionId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) return null
try {
const response = await fetch('/api/tools/webflow/sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId: optionId }),
})
if (!response.ok) return null
const data = await response.json()
const site = data.sites?.find((s: { id: string }) => s.id === optionId)
if (site) {
return { id: site.id, label: site.name }
}
return null
} catch {
return null
}
},
dependsOn: ['triggerCredentials'],
},
{
id: 'collectionId',
@@ -65,6 +122,60 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
field: 'selectedTriggerId',
value: 'webflow_collection_item_created',
},
fetchOptions: async (blockId: string, _subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
if (!credentialId || !siteId) {
return []
}
try {
const response = await fetch('/api/tools/webflow/collections', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId }),
})
if (!response.ok) {
throw new Error('Failed to fetch Webflow collections')
}
const data = await response.json()
if (data.collections && Array.isArray(data.collections)) {
return data.collections.map((collection: { id: string; name: string }) => ({
id: collection.id,
label: collection.name,
}))
}
return []
} catch (error) {
logger.error('Error fetching Webflow collections:', error)
throw error
}
},
fetchOptionById: async (blockId: string, _subBlockId: string, optionId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
if (!credentialId || !siteId) return null
try {
const response = await fetch('/api/tools/webflow/collections', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId }),
})
if (!response.ok) return null
const data = await response.json()
const collection = data.collections?.find((c: { id: string }) => c.id === optionId)
if (collection) {
return { id: collection.id, label: collection.name }
}
return null
} catch {
return null
}
},
dependsOn: ['triggerCredentials', 'siteId'],
},
{
id: 'triggerSave',
@@ -85,9 +196,9 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
type: 'text',
defaultValue: [
'Connect your Webflow account using the "Select Webflow credential" button above.',
'Enter your Webflow Site ID (found in the site URL or site settings).',
'Optionally enter a Collection ID to monitor only specific collections.',
'If no Collection ID is provided, the trigger will fire for items created in any collection on the site.',
'Select your Webflow site from the dropdown.',
'Optionally select a collection to monitor only specific collections.',
'If no collection is selected, the trigger will fire for items created in any collection on the site.',
'The webhook will trigger whenever a new item is created in the specified collection(s).',
'Make sure your Webflow account has appropriate permissions for the specified site.',
]

View File

@@ -1,6 +1,10 @@
import { createLogger } from '@sim/logger'
import { WebflowIcon } from '@/components/icons'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import type { TriggerConfig } from '../types'
const logger = createLogger('webflow-collection-item-deleted-trigger')
export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
id: 'webflow_collection_item_deleted',
name: 'Collection Item Deleted',
@@ -38,6 +42,58 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
field: 'selectedTriggerId',
value: 'webflow_collection_item_deleted',
},
fetchOptions: async (blockId: string, _subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) {
throw new Error('No Webflow credential selected')
}
try {
const response = await fetch('/api/tools/webflow/sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId }),
})
if (!response.ok) {
throw new Error('Failed to fetch Webflow sites')
}
const data = await response.json()
if (data.sites && Array.isArray(data.sites)) {
return data.sites.map((site: { id: string; name: string }) => ({
id: site.id,
label: site.name,
}))
}
return []
} catch (error) {
logger.error('Error fetching Webflow sites:', error)
throw error
}
},
fetchOptionById: async (blockId: string, _subBlockId: string, optionId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) return null
try {
const response = await fetch('/api/tools/webflow/sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId: optionId }),
})
if (!response.ok) return null
const data = await response.json()
const site = data.sites?.find((s: { id: string }) => s.id === optionId)
if (site) {
return { id: site.id, label: site.name }
}
return null
} catch {
return null
}
},
dependsOn: ['triggerCredentials'],
},
{
id: 'collectionId',
@@ -52,6 +108,60 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
field: 'selectedTriggerId',
value: 'webflow_collection_item_deleted',
},
fetchOptions: async (blockId: string, _subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
if (!credentialId || !siteId) {
return []
}
try {
const response = await fetch('/api/tools/webflow/collections', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId }),
})
if (!response.ok) {
throw new Error('Failed to fetch Webflow collections')
}
const data = await response.json()
if (data.collections && Array.isArray(data.collections)) {
return data.collections.map((collection: { id: string; name: string }) => ({
id: collection.id,
label: collection.name,
}))
}
return []
} catch (error) {
logger.error('Error fetching Webflow collections:', error)
throw error
}
},
fetchOptionById: async (blockId: string, _subBlockId: string, optionId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
if (!credentialId || !siteId) return null
try {
const response = await fetch('/api/tools/webflow/collections', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId }),
})
if (!response.ok) return null
const data = await response.json()
const collection = data.collections?.find((c: { id: string }) => c.id === optionId)
if (collection) {
return { id: collection.id, label: collection.name }
}
return null
} catch {
return null
}
},
dependsOn: ['triggerCredentials', 'siteId'],
},
{
id: 'triggerSave',
@@ -72,9 +182,9 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
type: 'text',
defaultValue: [
'Connect your Webflow account using the "Select Webflow credential" button above.',
'Enter your Webflow Site ID (found in the site URL or site settings).',
'Optionally enter a Collection ID to monitor only specific collections.',
'If no Collection ID is provided, the trigger will fire for items deleted in any collection on the site.',
'Select your Webflow site from the dropdown.',
'Optionally select a collection to monitor only specific collections.',
'If no collection is selected, the trigger will fire for items deleted in any collection on the site.',
'The webhook will trigger whenever an item is deleted from the specified collection(s).',
'Note: Once an item is deleted, only minimal information (ID, collection, site) is available.',
'Make sure your Webflow account has appropriate permissions for the specified site.',

View File

@@ -1,6 +1,10 @@
import { createLogger } from '@sim/logger'
import { WebflowIcon } from '@/components/icons'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import type { TriggerConfig } from '../types'
const logger = createLogger('webflow-form-submission-trigger')
export const webflowFormSubmissionTrigger: TriggerConfig = {
id: 'webflow_form_submission',
name: 'Form Submission',
@@ -17,9 +21,13 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
type: 'oauth-input',
description: 'This trigger requires webflow credentials to access your account.',
serviceId: 'webflow',
requiredScopes: [],
requiredScopes: ['forms:read'],
required: true,
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'webflow_form_submission',
},
},
{
id: 'siteId',
@@ -30,15 +38,76 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
required: true,
options: [],
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'webflow_form_submission',
},
fetchOptions: async (blockId: string, _subBlockId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) {
throw new Error('No Webflow credential selected')
}
try {
const response = await fetch('/api/tools/webflow/sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId }),
})
if (!response.ok) {
throw new Error('Failed to fetch Webflow sites')
}
const data = await response.json()
if (data.sites && Array.isArray(data.sites)) {
return data.sites.map((site: { id: string; name: string }) => ({
id: site.id,
label: site.name,
}))
}
return []
} catch (error) {
logger.error('Error fetching Webflow sites:', error)
throw error
}
},
fetchOptionById: async (blockId: string, _subBlockId: string, optionId: string) => {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
if (!credentialId) return null
try {
const response = await fetch('/api/tools/webflow/sites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: credentialId, siteId: optionId }),
})
if (!response.ok) return null
const data = await response.json()
const site = data.sites?.find((s: { id: string }) => s.id === optionId)
if (site) {
return { id: site.id, label: site.name }
}
return null
} catch {
return null
}
},
dependsOn: ['triggerCredentials'],
},
{
id: 'formId',
title: 'Form ID',
id: 'formName',
title: 'Form Name',
type: 'short-input',
placeholder: 'form-123abc (optional)',
description: 'The ID of the specific form to monitor (optional - leave empty for all forms)',
placeholder: 'Contact Form (optional)',
description:
'The name of the specific form to monitor (optional - leave empty for all forms)',
required: false,
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'webflow_form_submission',
},
},
{
id: 'triggerSave',
@@ -47,6 +116,10 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
hideFromPreview: true,
mode: 'trigger',
triggerId: 'webflow_form_submission',
condition: {
field: 'selectedTriggerId',
value: 'webflow_form_submission',
},
},
{
id: 'triggerInstructions',
@@ -55,9 +128,9 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
type: 'text',
defaultValue: [
'Connect your Webflow account using the "Select Webflow credential" button above.',
'Enter your Webflow Site ID (found in the site URL or site settings).',
'Optionally enter a Form ID to monitor only a specific form.',
'If no Form ID is provided, the trigger will fire for any form submission on the site.',
'Select your Webflow site from the dropdown.',
'Optionally enter the Form Name to monitor only a specific form.',
'If no Form Name is provided, the trigger will fire for any form submission on the site.',
'The webhook will trigger whenever a form is submitted on the specified site.',
'Form data will be included in the payload with all submitted field values.',
'Make sure your Webflow account has appropriate permissions for the specified site.',
@@ -68,6 +141,10 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
)
.join(''),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'webflow_form_submission',
},
},
],