mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(tools): added webflow OAuth + tools (#1720)
* feat(tools): added webflow OAuth + tools * remove itemId from delete item * remove siteId * added webhook triggers + oauth scopes + site/collection selector * update sample payload for webflow triggers * cleanup * fix discord color
This commit is contained in:
@@ -66,6 +66,7 @@
|
||||
"typeform",
|
||||
"vision",
|
||||
"wealthbox",
|
||||
"webflow",
|
||||
"webhook",
|
||||
"whatsapp",
|
||||
"wikipedia",
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Webflow
|
||||
description: Manage Webflow CMS collections
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="webflow"
|
||||
color="#E0E0E0"
|
||||
icon={true}
|
||||
iconSvg={`<svg className="block-icon"
|
||||
|
||||
|
||||
|
||||
viewBox='0 0 1080 674'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
|
||||
fill='#146EF5'
|
||||
/>
|
||||
</svg>`}
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Webflow](https://webflow.com/) is a powerful visual web design platform that enables you to build responsive websites without writing code. It combines a visual design interface with a robust CMS (Content Management System) that allows you to create, manage, and publish dynamic content for your websites.
|
||||
|
||||
With Webflow, you can:
|
||||
|
||||
- **Design visually**: Create custom websites with a visual editor that generates clean, semantic HTML/CSS code
|
||||
- **Manage content dynamically**: Use the CMS to create collections of structured content like blog posts, products, team members, or any custom data
|
||||
- **Publish instantly**: Deploy your sites to Webflow's hosting or export the code for custom hosting
|
||||
- **Create responsive designs**: Build sites that work seamlessly across desktop, tablet, and mobile devices
|
||||
- **Customize collections**: Define custom fields and data structures for your content types
|
||||
- **Automate content updates**: Programmatically manage your CMS content through APIs
|
||||
|
||||
In Sim, the Webflow integration enables your agents to seamlessly interact with your Webflow CMS collections through API authentication. This allows for powerful automation scenarios such as automatically creating blog posts from AI-generated content, updating product information, managing team member profiles, and retrieving CMS items for dynamic content generation. Your agents can list existing items to browse your content, retrieve specific items by ID, create new entries to add fresh content, update existing items to keep information current, and delete outdated content. This integration bridges the gap between your AI workflows and your Webflow CMS, enabling automated content management, dynamic website updates, and streamlined content workflows that keep your sites fresh and up-to-date without manual intervention.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrates Webflow CMS into the workflow. Can create, get, list, update, or delete items in Webflow CMS collections. Manage your Webflow content programmatically. Can be used in trigger mode to trigger workflows when collection items change or forms are submitted.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `webflow_list_items`
|
||||
|
||||
List all items from a Webflow CMS collection
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `collectionId` | string | Yes | ID of the collection |
|
||||
| `offset` | number | No | Offset for pagination \(optional\) |
|
||||
| `limit` | number | No | Maximum number of items to return \(optional, default: 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | json | Array of collection items |
|
||||
| `metadata` | json | Metadata about the query |
|
||||
|
||||
### `webflow_get_item`
|
||||
|
||||
Get a single item from a Webflow CMS collection
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `collectionId` | string | Yes | ID of the collection |
|
||||
| `itemId` | string | Yes | ID of the item to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `item` | json | The retrieved item object |
|
||||
| `metadata` | json | Metadata about the retrieved item |
|
||||
|
||||
### `webflow_create_item`
|
||||
|
||||
Create a new item in a Webflow CMS collection
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `collectionId` | string | Yes | ID of the collection |
|
||||
| `fieldData` | json | Yes | Field data for the new item as a JSON object. Keys should match collection field names. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `item` | json | The created item object |
|
||||
| `metadata` | json | Metadata about the created item |
|
||||
|
||||
### `webflow_update_item`
|
||||
|
||||
Update an existing item in a Webflow CMS collection
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `collectionId` | string | Yes | ID of the collection |
|
||||
| `itemId` | string | Yes | ID of the item to update |
|
||||
| `fieldData` | json | Yes | Field data to update as a JSON object. Only include fields you want to change. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `item` | json | The updated item object |
|
||||
| `metadata` | json | Metadata about the updated item |
|
||||
|
||||
### `webflow_delete_item`
|
||||
|
||||
Delete an item from a Webflow CMS collection
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `collectionId` | string | Yes | ID of the collection |
|
||||
| `itemId` | string | Yes | ID of the item to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the deletion was successful |
|
||||
| `metadata` | json | Metadata about the deletion |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `webflow`
|
||||
@@ -0,0 +1,69 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('WebflowCollectionsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const siteId = searchParams.get('siteId')
|
||||
|
||||
if (!siteId) {
|
||||
return NextResponse.json({ error: 'Missing siteId parameter' }, { status: 400 })
|
||||
}
|
||||
|
||||
const accessToken = await getOAuthToken(session.user.id, 'webflow')
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No Webflow access token found. Please connect your Webflow account.' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.webflow.com/v2/sites/${siteId}/collections`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Webflow collections', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
siteId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Webflow collections', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const collections = data.collections || []
|
||||
|
||||
const formattedCollections = collections.map((collection: any) => ({
|
||||
id: collection.id,
|
||||
name: collection.displayName || collection.slug || collection.id,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ collections: formattedCollections }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching Webflow collections', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error', details: error.message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('WebflowSitesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const accessToken = await getOAuthToken(session.user.id, 'webflow')
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No Webflow access token found. Please connect your Webflow account.' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.webflow.com/v2/sites', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Webflow sites', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Webflow sites', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const sites = data.sites || []
|
||||
|
||||
const formattedSites = sites.map((site: any) => ({
|
||||
id: site.id,
|
||||
name: site.displayName || site.shortName || site.id,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ sites: formattedSites }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching Webflow sites', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error', details: error.message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -428,6 +428,26 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
// --- End Outlook specific logic ---
|
||||
|
||||
// --- Webflow webhook setup ---
|
||||
if (savedWebhook && provider === 'webflow') {
|
||||
logger.info(
|
||||
`[${requestId}] Webflow provider detected. Attempting to create webhook in Webflow.`
|
||||
)
|
||||
try {
|
||||
await createWebflowWebhookSubscription(request, userId, savedWebhook, requestId)
|
||||
} catch (err) {
|
||||
logger.error(`[${requestId}] Error creating Webflow webhook`, err)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to create webhook in Webflow',
|
||||
details: err instanceof Error ? err.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
// --- End Webflow specific logic ---
|
||||
|
||||
const status = targetWebhookId ? 200 : 201
|
||||
return NextResponse.json({ webhook: savedWebhook }, { status })
|
||||
} catch (error: any) {
|
||||
@@ -548,3 +568,136 @@ async function createAirtableWebhookSubscription(
|
||||
)
|
||||
}
|
||||
}
|
||||
// Helper function to create the webhook subscription in Webflow
|
||||
async function createWebflowWebhookSubscription(
|
||||
request: NextRequest,
|
||||
userId: string,
|
||||
webhookData: any,
|
||||
requestId: string
|
||||
) {
|
||||
try {
|
||||
const { path, providerConfig } = webhookData
|
||||
const { siteId, triggerId, collectionId, formId } = providerConfig || {}
|
||||
|
||||
if (!siteId) {
|
||||
logger.warn(`[${requestId}] Missing siteId for Webflow webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error('Site ID is required to create Webflow webhook')
|
||||
}
|
||||
|
||||
if (!triggerId) {
|
||||
logger.warn(`[${requestId}] Missing triggerId for Webflow webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error('Trigger type is required to create Webflow webhook')
|
||||
}
|
||||
|
||||
const accessToken = await getOAuthToken(userId, 'webflow')
|
||||
if (!accessToken) {
|
||||
logger.warn(
|
||||
`[${requestId}] Could not retrieve Webflow access token for user ${userId}. Cannot create webhook in Webflow.`
|
||||
)
|
||||
throw new Error(
|
||||
'Webflow account connection required. Please connect your Webflow account in the trigger configuration and try again.'
|
||||
)
|
||||
}
|
||||
|
||||
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
|
||||
|
||||
// Map trigger IDs to Webflow trigger types
|
||||
const triggerTypeMap: Record<string, string> = {
|
||||
webflow_collection_item_created: 'collection_item_created',
|
||||
webflow_collection_item_changed: 'collection_item_changed',
|
||||
webflow_collection_item_deleted: 'collection_item_deleted',
|
||||
webflow_form_submission: 'form_submission',
|
||||
}
|
||||
|
||||
const webflowTriggerType = triggerTypeMap[triggerId]
|
||||
if (!webflowTriggerType) {
|
||||
logger.warn(`[${requestId}] Invalid triggerId for Webflow: ${triggerId}`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(`Invalid Webflow trigger type: ${triggerId}`)
|
||||
}
|
||||
|
||||
const webflowApiUrl = `https://api.webflow.com/v2/sites/${siteId}/webhooks`
|
||||
|
||||
const requestBody: any = {
|
||||
triggerType: webflowTriggerType,
|
||||
url: notificationUrl,
|
||||
}
|
||||
|
||||
// Add filter for collection-based triggers
|
||||
if (collectionId && webflowTriggerType.startsWith('collection_item_')) {
|
||||
requestBody.filter = {
|
||||
resource_type: 'collection',
|
||||
resource_id: collectionId,
|
||||
}
|
||||
}
|
||||
|
||||
// Add filter for form submissions
|
||||
if (formId && webflowTriggerType === 'form_submission') {
|
||||
requestBody.filter = {
|
||||
resource_type: 'form',
|
||||
resource_id: formId,
|
||||
}
|
||||
}
|
||||
|
||||
const webflowResponse = await fetch(webflowApiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
const responseBody = await webflowResponse.json()
|
||||
|
||||
if (!webflowResponse.ok || responseBody.error) {
|
||||
const errorMessage = responseBody.message || responseBody.error || 'Unknown Webflow API error'
|
||||
logger.error(
|
||||
`[${requestId}] Failed to create webhook in Webflow for webhook ${webhookData.id}. Status: ${webflowResponse.status}`,
|
||||
{ message: errorMessage, response: responseBody }
|
||||
)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully created webhook in Webflow for webhook ${webhookData.id}.`,
|
||||
{
|
||||
webflowWebhookId: responseBody.id || responseBody._id,
|
||||
}
|
||||
)
|
||||
|
||||
// Store the Webflow webhook ID in the providerConfig
|
||||
try {
|
||||
const currentConfig = (webhookData.providerConfig as Record<string, any>) || {}
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
externalId: responseBody.id || responseBody._id,
|
||||
}
|
||||
await db
|
||||
.update(webhook)
|
||||
.set({ providerConfig: updatedConfig, updatedAt: new Date() })
|
||||
.where(eq(webhook.id, webhookData.id))
|
||||
} catch (dbError: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Failed to store externalId in providerConfig for webhook ${webhookData.id}.`,
|
||||
dbError
|
||||
)
|
||||
// Even if saving fails, the webhook exists in Webflow. Log and continue.
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Exception during Webflow webhook creation for webhook ${webhookData.id}.`,
|
||||
{
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -115,6 +115,10 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'users:read': 'View workspace users',
|
||||
'files:write': 'Upload files',
|
||||
'canvases:write': 'Create canvas documents',
|
||||
'sites:read': 'View your Webflow sites',
|
||||
'sites:write': 'Manage webhooks and site settings',
|
||||
'cms:read': 'View your CMS content',
|
||||
'cms:write': 'Manage your CMS content',
|
||||
}
|
||||
|
||||
// Convert OAuth scope to user-friendly description
|
||||
|
||||
+34
-11
@@ -35,6 +35,7 @@ interface TriggerConfigSectionProps {
|
||||
onChange: (fieldId: string, value: any) => void
|
||||
webhookUrl: string
|
||||
dynamicOptions?: Record<string, Array<{ id: string; name: string }> | string[]>
|
||||
loadingFields?: Record<string, boolean>
|
||||
}
|
||||
|
||||
export function TriggerConfigSection({
|
||||
@@ -44,6 +45,7 @@ export function TriggerConfigSection({
|
||||
onChange,
|
||||
webhookUrl,
|
||||
dynamicOptions = {},
|
||||
loadingFields = {},
|
||||
}: TriggerConfigSectionProps) {
|
||||
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({})
|
||||
const [copied, setCopied] = useState<string | null>(null)
|
||||
@@ -81,27 +83,43 @@ export function TriggerConfigSection({
|
||||
)
|
||||
|
||||
case 'select': {
|
||||
const rawOptions = dynamicOptions?.[fieldId] || fieldDef.options || []
|
||||
const isLoading = loadingFields[fieldId] || false
|
||||
|
||||
const availableOptions = Array.isArray(rawOptions)
|
||||
? rawOptions.map((option: any) => {
|
||||
if (typeof option === 'string') {
|
||||
return { id: option, name: option }
|
||||
}
|
||||
return option
|
||||
})
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor={fieldId}>
|
||||
<Label htmlFor={fieldId} className='font-medium text-sm'>
|
||||
{fieldDef.label}
|
||||
{fieldDef.required && <span className='ml-1 text-red-500'>*</span>}
|
||||
</Label>
|
||||
<Select value={value} onValueChange={(value) => onChange(fieldId, value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={fieldDef.placeholder} />
|
||||
{fieldDef.description && (
|
||||
<p className='text-muted-foreground text-sm'>{fieldDef.description}</p>
|
||||
)}
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(value) => onChange(fieldId, value)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger id={fieldId} className='h-10'>
|
||||
<SelectValue placeholder={isLoading ? 'Loading...' : fieldDef.placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fieldDef.options?.map((option: string) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
{availableOptions.map((option: any) => (
|
||||
<SelectItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{fieldDef.description && (
|
||||
<p className='text-muted-foreground text-sm'>{fieldDef.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -352,9 +370,14 @@ export function TriggerConfigSection({
|
||||
}
|
||||
}
|
||||
|
||||
// Show webhook URL only for manual webhooks (have webhook config but no OAuth auto-registration)
|
||||
// Auto-registered webhooks (like Webflow, Airtable) have requiresCredentials and register via API
|
||||
// Polling triggers (like Gmail) don't have webhook property at all
|
||||
const shouldShowWebhookUrl = webhookUrl && triggerDef.webhook && !triggerDef.requiresCredentials
|
||||
|
||||
return (
|
||||
<div className='space-y-4 rounded-md border border-border bg-card p-4 shadow-sm'>
|
||||
{webhookUrl && (
|
||||
{shouldShowWebhookUrl && (
|
||||
<div className='mb-4 space-y-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Label className='font-medium text-sm'>Webhook URL</Label>
|
||||
|
||||
+63
-11
@@ -96,6 +96,7 @@ export function TriggerModal({
|
||||
const [dynamicOptions, setDynamicOptions] = useState<
|
||||
Record<string, Array<{ id: string; name: string }>>
|
||||
>({})
|
||||
const [loadingFields, setLoadingFields] = useState<Record<string, boolean>>({})
|
||||
const lastCredentialIdRef = useRef<string | null>(null)
|
||||
const [testUrl, setTestUrl] = useState<string | null>(null)
|
||||
const [testUrlExpiresAt, setTestUrlExpiresAt] = useState<string | null>(null)
|
||||
@@ -113,6 +114,9 @@ export function TriggerModal({
|
||||
} else if (triggerDef.provider === 'airtable') {
|
||||
if (typeof next.baseId === 'string') next.baseId = ''
|
||||
if (typeof next.tableId === 'string') next.tableId = ''
|
||||
} else if (triggerDef.provider === 'webflow') {
|
||||
if (typeof next.siteId === 'string') next.siteId = ''
|
||||
if (typeof next.collectionId === 'string') next.collectionId = ''
|
||||
}
|
||||
return next
|
||||
})
|
||||
@@ -177,6 +181,8 @@ export function TriggerModal({
|
||||
void loadGmailLabels(currentCredentialId)
|
||||
} else if (triggerDef.provider === 'outlook') {
|
||||
void loadOutlookFolders(currentCredentialId)
|
||||
} else if (triggerDef.provider === 'webflow') {
|
||||
void loadWebflowSites()
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -197,6 +203,8 @@ export function TriggerModal({
|
||||
void loadGmailLabels(currentCredentialId)
|
||||
} else if (triggerDef.provider === 'outlook') {
|
||||
void loadOutlookFolders(currentCredentialId)
|
||||
} else if (triggerDef.provider === 'webflow') {
|
||||
void loadWebflowSites()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,9 +270,57 @@ export function TriggerModal({
|
||||
}
|
||||
}
|
||||
|
||||
// Generate webhook path and URL
|
||||
const loadWebflowSites = async () => {
|
||||
setLoadingFields((prev) => ({ ...prev, siteId: true }))
|
||||
try {
|
||||
const response = await fetch('/api/tools/webflow/sites')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.sites && Array.isArray(data.sites)) {
|
||||
setDynamicOptions((prev) => ({
|
||||
...prev,
|
||||
siteId: data.sites,
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
logger.error('Failed to load Webflow sites:', response.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error loading Webflow sites:', error)
|
||||
} finally {
|
||||
setLoadingFields((prev) => ({ ...prev, siteId: false }))
|
||||
}
|
||||
}
|
||||
|
||||
const loadWebflowCollections = async (siteId: string) => {
|
||||
setLoadingFields((prev) => ({ ...prev, collectionId: true }))
|
||||
try {
|
||||
const response = await fetch(`/api/tools/webflow/collections?siteId=${siteId}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.collections && Array.isArray(data.collections)) {
|
||||
setDynamicOptions((prev) => ({
|
||||
...prev,
|
||||
collectionId: data.collections,
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
logger.error('Failed to load Webflow collections:', response.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error loading Webflow collections:', error)
|
||||
} finally {
|
||||
setLoadingFields((prev) => ({ ...prev, collectionId: false }))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerDef.provider === 'webflow' && config.siteId) {
|
||||
void loadWebflowCollections(config.siteId)
|
||||
}
|
||||
}, [config.siteId, triggerDef.provider])
|
||||
|
||||
useEffect(() => {
|
||||
// For triggers that don't use webhooks (like Gmail polling), skip URL generation
|
||||
if (triggerDef.requiresCredentials && !triggerDef.webhook) {
|
||||
setWebhookUrl('')
|
||||
setGeneratedPath('')
|
||||
@@ -273,14 +329,11 @@ export function TriggerModal({
|
||||
|
||||
let finalPath = triggerPath
|
||||
|
||||
// If no path exists and we haven't generated one yet, generate one
|
||||
if (!finalPath && !generatedPath) {
|
||||
// Use UUID format consistent with other webhooks
|
||||
const newPath = crypto.randomUUID()
|
||||
setGeneratedPath(newPath)
|
||||
finalPath = newPath
|
||||
} else if (generatedPath && !triggerPath) {
|
||||
// Use the already generated path
|
||||
finalPath = generatedPath
|
||||
}
|
||||
|
||||
@@ -538,6 +591,7 @@ export function TriggerModal({
|
||||
onChange={handleConfigChange}
|
||||
webhookUrl={webhookUrl}
|
||||
dynamicOptions={dynamicOptions}
|
||||
loadingFields={loadingFields}
|
||||
/>
|
||||
|
||||
{triggerDef.webhook && (
|
||||
@@ -684,17 +738,15 @@ export function TriggerModal({
|
||||
(!(hasConfigChanged || hasCredentialChanged) && !!triggerId)
|
||||
}
|
||||
className={cn(
|
||||
'h-9 rounded-[8px]',
|
||||
'w-[140px] rounded-[8px]',
|
||||
isConfigValid() && (hasConfigChanged || hasCredentialChanged || !triggerId)
|
||||
? 'bg-primary hover:bg-primary/90'
|
||||
: '',
|
||||
isSaving &&
|
||||
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20'
|
||||
: ''
|
||||
)}
|
||||
size='default'
|
||||
size='sm'
|
||||
>
|
||||
{isSaving && (
|
||||
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
)}
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
|
||||
+1
-8
@@ -145,21 +145,14 @@ export function TriggerConfig({
|
||||
throw new Error('Trigger definition not found')
|
||||
}
|
||||
|
||||
// Set the trigger path and config in the block state
|
||||
if (path && path !== triggerPath) {
|
||||
setTriggerPath(path)
|
||||
}
|
||||
setTriggerConfig(config)
|
||||
setStoredTriggerId(effectiveTriggerId)
|
||||
|
||||
// Map trigger ID to webhook provider name
|
||||
const webhookProvider = effectiveTriggerId
|
||||
.replace(/_chat_subscription$/, '')
|
||||
.replace(/_webhook$/, '')
|
||||
.replace(/_poller$/, '')
|
||||
.replace(/_subscription$/, '') // e.g., 'slack_webhook' -> 'slack', 'gmail_poller' -> 'gmail', 'microsoftteams_chat_subscription' -> 'microsoftteams'
|
||||
const webhookProvider = triggerDef.provider
|
||||
|
||||
// Include selected credential from the modal (if any)
|
||||
const selectedCredentialId =
|
||||
(useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as string | null) ||
|
||||
null
|
||||
|
||||
+1
@@ -7,4 +7,5 @@ export { OutlookConfig } from './outlook'
|
||||
export { SlackConfig } from './slack'
|
||||
export { StripeConfig } from './stripe'
|
||||
export { TelegramConfig } from './telegram'
|
||||
export { WebflowConfig } from './webflow'
|
||||
export { WhatsAppConfig } from './whatsapp'
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { Input, Skeleton } from '@/components/ui'
|
||||
import {
|
||||
ConfigField,
|
||||
ConfigSection,
|
||||
InstructionsSection,
|
||||
TestResultDisplay as WebhookTestResult,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components'
|
||||
|
||||
interface WebflowConfigProps {
|
||||
siteId: string
|
||||
setSiteId: (value: string) => void
|
||||
collectionId?: string
|
||||
setCollectionId?: (value: string) => void
|
||||
formId?: string
|
||||
setFormId?: (value: string) => void
|
||||
isLoadingToken: boolean
|
||||
testResult: any
|
||||
copied: string | null
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
testWebhook?: () => void
|
||||
webhookId?: string
|
||||
triggerType?: string // The selected trigger type to show relevant fields
|
||||
}
|
||||
|
||||
export function WebflowConfig({
|
||||
siteId,
|
||||
setSiteId,
|
||||
collectionId,
|
||||
setCollectionId,
|
||||
formId,
|
||||
setFormId,
|
||||
isLoadingToken,
|
||||
testResult,
|
||||
copied,
|
||||
copyToClipboard,
|
||||
testWebhook,
|
||||
webhookId,
|
||||
triggerType,
|
||||
}: WebflowConfigProps) {
|
||||
const isCollectionTrigger = triggerType?.includes('collection_item') || !triggerType
|
||||
const isFormTrigger = triggerType?.includes('form_submission')
|
||||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
<ConfigSection title='Webflow Configuration'>
|
||||
<ConfigField
|
||||
id='webflow-site-id'
|
||||
label='Site ID *'
|
||||
description='The ID of the Webflow site to monitor (found in site settings or URL)'
|
||||
>
|
||||
{isLoadingToken ? (
|
||||
<Skeleton className='h-10 w-full' />
|
||||
) : (
|
||||
<Input
|
||||
id='webflow-site-id'
|
||||
value={siteId}
|
||||
onChange={(e) => setSiteId(e.target.value)}
|
||||
placeholder='6c3592'
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</ConfigField>
|
||||
|
||||
{isCollectionTrigger && setCollectionId && (
|
||||
<ConfigField
|
||||
id='webflow-collection-id'
|
||||
label='Collection ID'
|
||||
description='The ID of the collection to monitor (optional - leave empty to monitor all collections)'
|
||||
>
|
||||
{isLoadingToken ? (
|
||||
<Skeleton className='h-10 w-full' />
|
||||
) : (
|
||||
<Input
|
||||
id='webflow-collection-id'
|
||||
value={collectionId || ''}
|
||||
onChange={(e) => setCollectionId(e.target.value)}
|
||||
placeholder='68f9666257aa8abaa9b0b6d6'
|
||||
/>
|
||||
)}
|
||||
</ConfigField>
|
||||
)}
|
||||
|
||||
{isFormTrigger && setFormId && (
|
||||
<ConfigField
|
||||
id='webflow-form-id'
|
||||
label='Form ID'
|
||||
description='The ID of the specific form to monitor (optional - leave empty to monitor all forms)'
|
||||
>
|
||||
{isLoadingToken ? (
|
||||
<Skeleton className='h-10 w-full' />
|
||||
) : (
|
||||
<Input
|
||||
id='webflow-form-id'
|
||||
value={formId || ''}
|
||||
onChange={(e) => setFormId(e.target.value)}
|
||||
placeholder='form-contact'
|
||||
/>
|
||||
)}
|
||||
</ConfigField>
|
||||
)}
|
||||
</ConfigSection>
|
||||
|
||||
{testResult && (
|
||||
<WebhookTestResult
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
)}
|
||||
|
||||
<InstructionsSection tip='Webflow webhooks monitor changes in your CMS and trigger your workflow automatically.'>
|
||||
<ol className='list-inside list-decimal space-y-1'>
|
||||
<li>Connect your Webflow account using the credential selector above.</li>
|
||||
<li>Enter your Webflow Site ID (found in the site URL or site settings).</li>
|
||||
{isCollectionTrigger && (
|
||||
<>
|
||||
<li>
|
||||
Optionally enter a Collection ID to monitor only specific collections (leave empty
|
||||
to monitor all).
|
||||
</li>
|
||||
<li>
|
||||
The webhook will trigger when items are created, changed, or deleted in the
|
||||
specified collection(s).
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{isFormTrigger && (
|
||||
<>
|
||||
<li>
|
||||
Optionally enter a Form ID to monitor only a specific form (leave empty to monitor
|
||||
all forms).
|
||||
</li>
|
||||
<li>The webhook will trigger whenever a form is submitted on your site.</li>
|
||||
</>
|
||||
)}
|
||||
<li>
|
||||
Sim will automatically register the webhook with Webflow when you save this
|
||||
configuration.
|
||||
</li>
|
||||
<li>Make sure your Webflow account has appropriate permissions for the site.</li>
|
||||
</ol>
|
||||
</InstructionsSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
longDescription:
|
||||
'Integrate Discord into the workflow. Can send and get messages, get server information, and get a user’s information.',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
bgColor: '#5865F2',
|
||||
icon: DiscordIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { WebflowIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { WebflowResponse } from '@/tools/webflow/types'
|
||||
|
||||
export const WebflowBlock: BlockConfig<WebflowResponse> = {
|
||||
type: 'webflow',
|
||||
name: 'Webflow',
|
||||
description: 'Manage Webflow CMS collections',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrates Webflow CMS into the workflow. Can create, get, list, update, or delete items in Webflow CMS collections. Manage your Webflow content programmatically. Can be used in trigger mode to trigger workflows when collection items change or forms are submitted.',
|
||||
docsLink: 'https://docs.sim.ai/tools/webflow',
|
||||
category: 'tools',
|
||||
triggerAllowed: true,
|
||||
bgColor: '#E0E0E0',
|
||||
icon: WebflowIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
{ label: 'List Items', id: 'list' },
|
||||
{ label: 'Get Item', id: 'get' },
|
||||
{ label: 'Create Item', id: 'create' },
|
||||
{ label: 'Update Item', id: 'update' },
|
||||
{ label: 'Delete Item', id: 'delete' },
|
||||
],
|
||||
value: () => 'list',
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Webflow Account',
|
||||
type: 'oauth-input',
|
||||
layout: 'full',
|
||||
provider: 'webflow',
|
||||
serviceId: 'webflow',
|
||||
requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
|
||||
placeholder: 'Select Webflow account',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'collectionId',
|
||||
title: 'Collection ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter collection ID',
|
||||
dependsOn: ['credential'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'itemId',
|
||||
title: 'Item ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'ID of the item',
|
||||
condition: { field: 'operation', value: ['get', 'update', 'delete'] },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'offset',
|
||||
title: 'Offset',
|
||||
type: 'short-input',
|
||||
layout: 'half',
|
||||
placeholder: 'Pagination offset (optional)',
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
},
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
layout: 'half',
|
||||
placeholder: 'Max items to return (optional)',
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
},
|
||||
{
|
||||
id: 'fieldData',
|
||||
title: 'Field Data',
|
||||
type: 'code',
|
||||
layout: 'full',
|
||||
language: 'json',
|
||||
placeholder: 'Field data as JSON: `{ "name": "Item Name", "slug": "item-slug" }`',
|
||||
condition: { field: 'operation', value: ['create', 'update'] },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'triggerConfig',
|
||||
title: 'Trigger Configuration',
|
||||
type: 'trigger-config',
|
||||
layout: 'full',
|
||||
triggerProvider: 'webflow',
|
||||
availableTriggers: [
|
||||
'webflow_collection_item_created',
|
||||
'webflow_collection_item_changed',
|
||||
'webflow_collection_item_deleted',
|
||||
'webflow_form_submission',
|
||||
],
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'webflow_list_items',
|
||||
'webflow_get_item',
|
||||
'webflow_create_item',
|
||||
'webflow_update_item',
|
||||
'webflow_delete_item',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'list':
|
||||
return 'webflow_list_items'
|
||||
case 'get':
|
||||
return 'webflow_get_item'
|
||||
case 'create':
|
||||
return 'webflow_create_item'
|
||||
case 'update':
|
||||
return 'webflow_update_item'
|
||||
case 'delete':
|
||||
return 'webflow_delete_item'
|
||||
default:
|
||||
throw new Error(`Invalid Webflow operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, fieldData, ...rest } = params
|
||||
let parsedFieldData: any | undefined
|
||||
|
||||
try {
|
||||
if (fieldData && (params.operation === 'create' || params.operation === 'update')) {
|
||||
parsedFieldData = JSON.parse(fieldData)
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(`Invalid JSON input for ${params.operation} operation: ${error.message}`)
|
||||
}
|
||||
|
||||
const baseParams = {
|
||||
credential,
|
||||
...rest,
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'create':
|
||||
case 'update':
|
||||
return { ...baseParams, fieldData: parsedFieldData }
|
||||
default:
|
||||
return baseParams
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Webflow OAuth access token' },
|
||||
collectionId: { type: 'string', description: 'Webflow collection identifier' },
|
||||
// Conditional inputs
|
||||
itemId: { type: 'string', description: 'Item identifier' }, // Required for get/update/delete
|
||||
offset: { type: 'number', description: 'Pagination offset' }, // Optional for list
|
||||
limit: { type: 'number', description: 'Maximum items to return' }, // Optional for list
|
||||
fieldData: { type: 'json', description: 'Item field data' }, // Required for create/update
|
||||
},
|
||||
outputs: {
|
||||
items: { type: 'json', description: 'Array of items (list operation)' },
|
||||
item: { type: 'json', description: 'Single item data (get/create/update operations)' },
|
||||
success: { type: 'boolean', description: 'Operation success status (delete operation)' },
|
||||
metadata: { type: 'json', description: 'Operation metadata' },
|
||||
// Trigger outputs
|
||||
siteId: { type: 'string', description: 'Site ID where event occurred' },
|
||||
workspaceId: { type: 'string', description: 'Workspace ID where event occurred' },
|
||||
collectionId: { type: 'string', description: 'Collection ID (for collection events)' },
|
||||
payload: { type: 'json', description: 'Event payload data (item data for collection events)' },
|
||||
name: { type: 'string', description: 'Form name (for form submissions)' },
|
||||
id: { type: 'string', description: 'Submission ID (for form submissions)' },
|
||||
submittedAt: { type: 'string', description: 'Submission timestamp (for form submissions)' },
|
||||
data: { type: 'json', description: 'Form field data (for form submissions)' },
|
||||
schema: { type: 'json', description: 'Form schema (for form submissions)' },
|
||||
formElementId: { type: 'string', description: 'Form element ID (for form submissions)' },
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: [
|
||||
'webflow_collection_item_created',
|
||||
'webflow_collection_item_changed',
|
||||
'webflow_collection_item_deleted',
|
||||
'webflow_form_submission',
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* Blocks Registry
|
||||
*
|
||||
*/
|
||||
|
||||
import { AgentBlock } from '@/blocks/blocks/agent'
|
||||
import { AirtableBlock } from '@/blocks/blocks/airtable'
|
||||
import { ApiBlock } from '@/blocks/blocks/api'
|
||||
@@ -80,6 +75,7 @@ import { TwilioSMSBlock } from '@/blocks/blocks/twilio'
|
||||
import { TypeformBlock } from '@/blocks/blocks/typeform'
|
||||
import { VisionBlock } from '@/blocks/blocks/vision'
|
||||
import { WealthboxBlock } from '@/blocks/blocks/wealthbox'
|
||||
import { WebflowBlock } from '@/blocks/blocks/webflow'
|
||||
import { WebhookBlock } from '@/blocks/blocks/webhook'
|
||||
import { WhatsAppBlock } from '@/blocks/blocks/whatsapp'
|
||||
import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
|
||||
@@ -171,6 +167,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
typeform: TypeformBlock,
|
||||
vision: VisionBlock,
|
||||
wealthbox: WealthboxBlock,
|
||||
webflow: WebflowBlock,
|
||||
webhook: WebhookBlock,
|
||||
whatsapp: WhatsAppBlock,
|
||||
wikipedia: WikipediaBlock,
|
||||
|
||||
@@ -3775,3 +3775,23 @@ export function ZepIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function WebflowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='1080'
|
||||
height='674'
|
||||
viewBox='0 0 1080 674'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
|
||||
fill='#146EF5'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ export const auth = betterAuth({
|
||||
'microsoft',
|
||||
'slack',
|
||||
'reddit',
|
||||
'webflow',
|
||||
|
||||
// Common SSO provider patterns
|
||||
...SSO_TRUSTED_PROVIDERS,
|
||||
@@ -1214,6 +1215,56 @@ export const auth = betterAuth({
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Webflow provider
|
||||
{
|
||||
providerId: 'webflow',
|
||||
clientId: env.WEBFLOW_CLIENT_ID as string,
|
||||
clientSecret: env.WEBFLOW_CLIENT_SECRET as string,
|
||||
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'],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/webflow`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
logger.info('Fetching Webflow user info')
|
||||
|
||||
const response = await fetch('https://api.webflow.com/v2/token/introspect', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Error fetching Webflow user info:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const now = new Date()
|
||||
|
||||
const userId = data.user_id || `webflow-${Date.now()}`
|
||||
const uniqueId = `webflow-${userId}`
|
||||
|
||||
return {
|
||||
id: uniqueId,
|
||||
name: data.user_name || 'Webflow User',
|
||||
email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@webflow.user`,
|
||||
emailVerified: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in Webflow getUserInfo:', { error })
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Include SSO plugin when enabled
|
||||
|
||||
@@ -201,6 +201,8 @@ export const env = createEnv({
|
||||
SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret
|
||||
REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID
|
||||
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
|
||||
WEBFLOW_CLIENT_ID: z.string().optional(), // Webflow OAuth client ID
|
||||
WEBFLOW_CLIENT_SECRET: z.string().optional(), // Webflow OAuth client secret
|
||||
|
||||
// E2B Remote Code Execution
|
||||
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
SlackIcon,
|
||||
SupabaseIcon,
|
||||
WealthboxIcon,
|
||||
WebflowIcon,
|
||||
xIcon,
|
||||
} from '@/components/icons'
|
||||
import { env } from '@/lib/env'
|
||||
@@ -47,6 +48,7 @@ export type OAuthProvider =
|
||||
| 'slack'
|
||||
| 'reddit'
|
||||
| 'wealthbox'
|
||||
| 'webflow'
|
||||
| string
|
||||
|
||||
export type OAuthService =
|
||||
@@ -76,6 +78,7 @@ export type OAuthService =
|
||||
| 'reddit'
|
||||
| 'wealthbox'
|
||||
| 'onedrive'
|
||||
| 'webflow'
|
||||
export interface OAuthProviderConfig {
|
||||
id: OAuthProvider
|
||||
name: string
|
||||
@@ -500,6 +503,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
},
|
||||
defaultService: 'wealthbox',
|
||||
},
|
||||
webflow: {
|
||||
id: 'webflow',
|
||||
name: 'Webflow',
|
||||
icon: (props) => WebflowIcon(props),
|
||||
services: {
|
||||
webflow: {
|
||||
id: 'webflow',
|
||||
name: 'Webflow',
|
||||
description: 'Manage Webflow CMS collections, sites, and content.',
|
||||
providerId: 'webflow',
|
||||
icon: (props) => WebflowIcon(props),
|
||||
baseProviderIcon: (props) => WebflowIcon(props),
|
||||
scopes: ['cms:read', 'cms:write', 'sites:read', 'sites:write'],
|
||||
},
|
||||
},
|
||||
defaultService: 'webflow',
|
||||
},
|
||||
}
|
||||
|
||||
// Helper function to get a service by provider and service ID
|
||||
@@ -584,6 +604,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]
|
||||
return 'reddit'
|
||||
} else if (provider === 'wealthbox') {
|
||||
return 'wealthbox'
|
||||
} else if (provider === 'webflow') {
|
||||
return 'webflow'
|
||||
}
|
||||
|
||||
return providerConfig.defaultService
|
||||
@@ -886,6 +908,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
|
||||
supportsRefreshTokenRotation: true,
|
||||
}
|
||||
}
|
||||
case 'webflow': {
|
||||
const { clientId, clientSecret } = getCredentials(
|
||||
env.WEBFLOW_CLIENT_ID,
|
||||
env.WEBFLOW_CLIENT_SECRET
|
||||
)
|
||||
return {
|
||||
tokenEndpoint: 'https://api.webflow.com/oauth/access_token',
|
||||
clientId,
|
||||
clientSecret,
|
||||
useBasicAuth: false,
|
||||
supportsRefreshTokenRotation: false,
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${provider}`)
|
||||
}
|
||||
|
||||
@@ -900,6 +900,57 @@ export async function formatWebhookInput(
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'webflow') {
|
||||
const triggerType = body?.triggerType || 'unknown'
|
||||
const siteId = body?.siteId || ''
|
||||
const workspaceId = body?.workspaceId || ''
|
||||
const collectionId = body?.collectionId || ''
|
||||
const payload = body?.payload || {}
|
||||
const formId = body?.formId || ''
|
||||
const formName = body?.name || ''
|
||||
const formSubmissionId = body?.id || ''
|
||||
const submittedAt = body?.submittedAt || ''
|
||||
const formData = body?.data || {}
|
||||
const schema = body?.schema || {}
|
||||
|
||||
return {
|
||||
siteId,
|
||||
workspaceId,
|
||||
collectionId,
|
||||
payload,
|
||||
triggerType,
|
||||
|
||||
formId,
|
||||
name: formName,
|
||||
id: formSubmissionId,
|
||||
submittedAt,
|
||||
data: formData,
|
||||
schema,
|
||||
formElementId: body?.formElementId || '',
|
||||
|
||||
webflow: {
|
||||
siteId,
|
||||
workspaceId,
|
||||
collectionId,
|
||||
payload,
|
||||
triggerType,
|
||||
raw: body,
|
||||
},
|
||||
|
||||
webhook: {
|
||||
data: {
|
||||
provider: 'webflow',
|
||||
path: foundWebhook.path,
|
||||
providerConfig: foundWebhook.providerConfig,
|
||||
payload: body,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
method: request.method,
|
||||
},
|
||||
},
|
||||
workflowId: foundWorkflow.id,
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'generic') {
|
||||
return body
|
||||
}
|
||||
|
||||
@@ -205,6 +205,13 @@ import {
|
||||
wealthboxWriteNoteTool,
|
||||
wealthboxWriteTaskTool,
|
||||
} from '@/tools/wealthbox'
|
||||
import {
|
||||
webflowCreateItemTool,
|
||||
webflowDeleteItemTool,
|
||||
webflowGetItemTool,
|
||||
webflowListItemsTool,
|
||||
webflowUpdateItemTool,
|
||||
} from '@/tools/webflow'
|
||||
import { whatsappSendMessageTool } from '@/tools/whatsapp'
|
||||
import {
|
||||
wikipediaPageContentTool,
|
||||
@@ -420,6 +427,11 @@ export const tools: Record<string, ToolConfig> = {
|
||||
wealthbox_write_task: wealthboxWriteTaskTool,
|
||||
wealthbox_read_note: wealthboxReadNoteTool,
|
||||
wealthbox_write_note: wealthboxWriteNoteTool,
|
||||
webflow_list_items: webflowListItemsTool,
|
||||
webflow_get_item: webflowGetItemTool,
|
||||
webflow_create_item: webflowCreateItemTool,
|
||||
webflow_update_item: webflowUpdateItemTool,
|
||||
webflow_delete_item: webflowDeleteItemTool,
|
||||
wikipedia_summary: wikipediaPageSummaryTool,
|
||||
wikipedia_search: wikipediaSearchTool,
|
||||
wikipedia_content: wikipediaPageContentTool,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WebflowCreateItemParams, WebflowCreateItemResponse } from '@/tools/webflow/types'
|
||||
|
||||
export const webflowCreateItemTool: ToolConfig<WebflowCreateItemParams, WebflowCreateItemResponse> =
|
||||
{
|
||||
id: 'webflow_create_item',
|
||||
name: 'Webflow Create Item',
|
||||
description: 'Create a new item in a Webflow CMS collection',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'webflow',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'ID of the collection',
|
||||
},
|
||||
fieldData: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Field data for the new item as a JSON object. Keys should match collection field names.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.webflow.com/v2/collections/${params.collectionId}/items`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
fieldData: params.fieldData,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
item: data,
|
||||
metadata: {
|
||||
itemId: data.id || 'unknown',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
item: {
|
||||
type: 'json',
|
||||
description: 'The created item object',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Metadata about the created item',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WebflowDeleteItemParams, WebflowDeleteItemResponse } from '@/tools/webflow/types'
|
||||
|
||||
export const webflowDeleteItemTool: ToolConfig<WebflowDeleteItemParams, WebflowDeleteItemResponse> =
|
||||
{
|
||||
id: 'webflow_delete_item',
|
||||
name: 'Webflow Delete Item',
|
||||
description: 'Delete an item from a Webflow CMS collection',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'webflow',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'ID of the collection',
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the item to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.webflow.com/v2/collections/${params.collectionId}/items/${params.itemId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const isSuccess = response.status === 204 || response.ok
|
||||
|
||||
return {
|
||||
success: isSuccess,
|
||||
output: {
|
||||
success: isSuccess,
|
||||
metadata: {
|
||||
deleted: isSuccess,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the deletion was successful',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Metadata about the deletion',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WebflowGetItemParams, WebflowGetItemResponse } from '@/tools/webflow/types'
|
||||
|
||||
export const webflowGetItemTool: ToolConfig<WebflowGetItemParams, WebflowGetItemResponse> = {
|
||||
id: 'webflow_get_item',
|
||||
name: 'Webflow Get Item',
|
||||
description: 'Get a single item from a Webflow CMS collection',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'webflow',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'ID of the collection',
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the item to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.webflow.com/v2/collections/${params.collectionId}/items/${params.itemId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
item: data,
|
||||
metadata: {
|
||||
itemId: data.id || 'unknown',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
item: {
|
||||
type: 'json',
|
||||
description: 'The retrieved item object',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Metadata about the retrieved item',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { webflowCreateItemTool } from '@/tools/webflow/create_item'
|
||||
import { webflowDeleteItemTool } from '@/tools/webflow/delete_item'
|
||||
import { webflowGetItemTool } from '@/tools/webflow/get_item'
|
||||
import { webflowListItemsTool } from '@/tools/webflow/list_items'
|
||||
import { webflowUpdateItemTool } from '@/tools/webflow/update_item'
|
||||
|
||||
export {
|
||||
webflowCreateItemTool,
|
||||
webflowDeleteItemTool,
|
||||
webflowGetItemTool,
|
||||
webflowListItemsTool,
|
||||
webflowUpdateItemTool,
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WebflowListItemsParams, WebflowListItemsResponse } from '@/tools/webflow/types'
|
||||
|
||||
export const webflowListItemsTool: ToolConfig<WebflowListItemsParams, WebflowListItemsResponse> = {
|
||||
id: 'webflow_list_items',
|
||||
name: 'Webflow List Items',
|
||||
description: 'List all items from a Webflow CMS collection',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'webflow',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'ID of the collection',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Offset for pagination (optional)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of items to return (optional, default: 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = `https://api.webflow.com/v2/collections/${params.collectionId}/items`
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.offset !== undefined) {
|
||||
queryParams.append('offset', params.offset.toString())
|
||||
}
|
||||
if (params.limit !== undefined) {
|
||||
queryParams.append('limit', params.limit.toString())
|
||||
}
|
||||
|
||||
const query = queryParams.toString()
|
||||
return query ? `${baseUrl}?${query}` : baseUrl
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items: data.items || [],
|
||||
metadata: {
|
||||
itemCount: (data.items || []).length,
|
||||
offset: data.offset,
|
||||
limit: data.limit,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'json',
|
||||
description: 'Array of collection items',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Metadata about the query',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
export interface WebflowBaseParams {
|
||||
accessToken: string
|
||||
collectionId: string
|
||||
}
|
||||
|
||||
export interface WebflowListItemsParams extends WebflowBaseParams {
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface WebflowListItemsOutput {
|
||||
items: any[]
|
||||
metadata: {
|
||||
itemCount: number
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface WebflowListItemsResponse {
|
||||
success: boolean
|
||||
output: WebflowListItemsOutput
|
||||
}
|
||||
|
||||
export interface WebflowGetItemParams extends WebflowBaseParams {
|
||||
itemId: string
|
||||
}
|
||||
|
||||
export interface WebflowGetItemOutput {
|
||||
item: any
|
||||
metadata: {
|
||||
itemId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface WebflowGetItemResponse {
|
||||
success: boolean
|
||||
output: WebflowGetItemOutput
|
||||
}
|
||||
|
||||
export interface WebflowCreateItemParams extends WebflowBaseParams {
|
||||
fieldData: Record<string, any>
|
||||
}
|
||||
|
||||
export interface WebflowCreateItemOutput {
|
||||
item: any
|
||||
metadata: {
|
||||
itemId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface WebflowCreateItemResponse {
|
||||
success: boolean
|
||||
output: WebflowCreateItemOutput
|
||||
}
|
||||
|
||||
export interface WebflowUpdateItemParams extends WebflowBaseParams {
|
||||
itemId: string
|
||||
fieldData: Record<string, any>
|
||||
}
|
||||
|
||||
export interface WebflowUpdateItemOutput {
|
||||
item: any
|
||||
metadata: {
|
||||
itemId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface WebflowUpdateItemResponse {
|
||||
success: boolean
|
||||
output: WebflowUpdateItemOutput
|
||||
}
|
||||
|
||||
export interface WebflowDeleteItemParams extends WebflowBaseParams {
|
||||
itemId: string
|
||||
}
|
||||
|
||||
export interface WebflowDeleteItemOutput {
|
||||
success: boolean
|
||||
metadata: {
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface WebflowDeleteItemResponse {
|
||||
success: boolean
|
||||
output: WebflowDeleteItemOutput
|
||||
}
|
||||
|
||||
export type WebflowResponse =
|
||||
| WebflowListItemsResponse
|
||||
| WebflowGetItemResponse
|
||||
| WebflowCreateItemResponse
|
||||
| WebflowUpdateItemResponse
|
||||
| WebflowDeleteItemResponse
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WebflowUpdateItemParams, WebflowUpdateItemResponse } from '@/tools/webflow/types'
|
||||
|
||||
export const webflowUpdateItemTool: ToolConfig<WebflowUpdateItemParams, WebflowUpdateItemResponse> =
|
||||
{
|
||||
id: 'webflow_update_item',
|
||||
name: 'Webflow Update Item',
|
||||
description: 'Update an existing item in a Webflow CMS collection',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'webflow',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'ID of the collection',
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the item to update',
|
||||
},
|
||||
fieldData: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Field data to update as a JSON object. Only include fields you want to change.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.webflow.com/v2/collections/${params.collectionId}/items/${params.itemId}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
fieldData: params.fieldData,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
item: data,
|
||||
metadata: {
|
||||
itemId: data.id || 'unknown',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
item: {
|
||||
type: 'json',
|
||||
description: 'The updated item object',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Metadata about the updated item',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -14,6 +14,12 @@ import { slackWebhookTrigger } from './slack'
|
||||
import { stripeWebhookTrigger } from './stripe/webhook'
|
||||
import { telegramWebhookTrigger } from './telegram'
|
||||
import type { TriggerConfig, TriggerRegistry } from './types'
|
||||
import {
|
||||
webflowCollectionItemChangedTrigger,
|
||||
webflowCollectionItemCreatedTrigger,
|
||||
webflowCollectionItemDeletedTrigger,
|
||||
webflowFormSubmissionTrigger,
|
||||
} from './webflow'
|
||||
import { whatsappWebhookTrigger } from './whatsapp'
|
||||
|
||||
// Central registry of all available triggers
|
||||
@@ -30,6 +36,10 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
telegram_webhook: telegramWebhookTrigger,
|
||||
whatsapp_webhook: whatsappWebhookTrigger,
|
||||
google_forms_webhook: googleFormsWebhookTrigger,
|
||||
webflow_collection_item_created: webflowCollectionItemCreatedTrigger,
|
||||
webflow_collection_item_changed: webflowCollectionItemChangedTrigger,
|
||||
webflow_collection_item_deleted: webflowCollectionItemDeletedTrigger,
|
||||
webflow_form_submission: webflowFormSubmissionTrigger,
|
||||
}
|
||||
|
||||
// Utility functions for working with triggers
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { WebflowIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '../types'
|
||||
|
||||
export const webflowCollectionItemChangedTrigger: TriggerConfig = {
|
||||
id: 'webflow_collection_item_changed',
|
||||
name: 'Collection Item Changed',
|
||||
provider: 'webflow',
|
||||
description:
|
||||
'Trigger workflow when an item is updated in a Webflow CMS collection (requires Webflow credentials)',
|
||||
version: '1.0.0',
|
||||
icon: WebflowIcon,
|
||||
|
||||
requiresCredentials: true,
|
||||
credentialProvider: 'webflow',
|
||||
|
||||
configFields: {
|
||||
siteId: {
|
||||
type: 'select',
|
||||
label: 'Site',
|
||||
placeholder: 'Select a site',
|
||||
description: 'The Webflow site to monitor',
|
||||
required: true,
|
||||
options: [],
|
||||
},
|
||||
collectionId: {
|
||||
type: 'select',
|
||||
label: 'Collection',
|
||||
placeholder: 'Select a collection (optional)',
|
||||
description: 'Optionally filter to monitor only a specific collection',
|
||||
required: false,
|
||||
options: [],
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID where the event occurred',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID where the event occurred',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The collection ID where the item was changed',
|
||||
},
|
||||
payload: {
|
||||
id: { type: 'string', description: 'The ID of the changed item' },
|
||||
cmsLocaleId: { type: 'string', description: 'CMS locale ID' },
|
||||
lastPublished: { type: 'string', description: 'Last published timestamp' },
|
||||
lastUpdated: { type: 'string', description: 'Last updated timestamp' },
|
||||
createdOn: { type: 'string', description: 'Timestamp when the item was created' },
|
||||
isArchived: { type: 'boolean', description: 'Whether the item is archived' },
|
||||
isDraft: { type: 'boolean', description: 'Whether the item is a draft' },
|
||||
fieldData: { type: 'object', description: 'The updated field data of the item' },
|
||||
},
|
||||
},
|
||||
|
||||
instructions: [
|
||||
'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.',
|
||||
'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.',
|
||||
],
|
||||
|
||||
samplePayload: {
|
||||
siteId: '68f9666057aa8abaa9b0b668',
|
||||
workspaceId: '68f96081e7018465432953b5',
|
||||
collectionId: '68f9666257aa8abaa9b0b6d6',
|
||||
payload: {
|
||||
id: '68fa8445de250e147cd95cfd',
|
||||
cmsLocaleId: '68f9666257aa8abaa9b0b6c9',
|
||||
lastPublished: '2024-01-15T14:45:00.000Z',
|
||||
lastUpdated: '2024-01-15T14:45:00.000Z',
|
||||
createdOn: '2024-01-15T10:30:00.000Z',
|
||||
isArchived: false,
|
||||
isDraft: false,
|
||||
fieldData: {
|
||||
name: 'Updated Blog Post',
|
||||
slug: 'updated-blog-post',
|
||||
'post-summary': 'This blog post has been updated',
|
||||
featured: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { WebflowIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '../types'
|
||||
|
||||
export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
|
||||
id: 'webflow_collection_item_created',
|
||||
name: 'Collection Item Created',
|
||||
provider: 'webflow',
|
||||
description:
|
||||
'Trigger workflow when a new item is created in a Webflow CMS collection (requires Webflow credentials)',
|
||||
version: '1.0.0',
|
||||
icon: WebflowIcon,
|
||||
|
||||
// Webflow requires OAuth credentials to create webhooks
|
||||
requiresCredentials: true,
|
||||
credentialProvider: 'webflow',
|
||||
|
||||
configFields: {
|
||||
siteId: {
|
||||
type: 'select',
|
||||
label: 'Site',
|
||||
placeholder: 'Select a site',
|
||||
description: 'The Webflow site to monitor',
|
||||
required: true,
|
||||
options: [], // Will be populated dynamically from API
|
||||
},
|
||||
collectionId: {
|
||||
type: 'select',
|
||||
label: 'Collection',
|
||||
placeholder: 'Select a collection (optional)',
|
||||
description: 'Optionally filter to monitor only a specific collection',
|
||||
required: false,
|
||||
options: [], // Will be populated dynamically based on selected site
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID where the event occurred',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID where the event occurred',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The collection ID where the item was created',
|
||||
},
|
||||
payload: {
|
||||
id: { type: 'string', description: 'The ID of the created item' },
|
||||
cmsLocaleId: { type: 'string', description: 'CMS locale ID' },
|
||||
lastPublished: { type: 'string', description: 'Last published timestamp' },
|
||||
lastUpdated: { type: 'string', description: 'Last updated timestamp' },
|
||||
createdOn: { type: 'string', description: 'Timestamp when the item was created' },
|
||||
isArchived: { type: 'boolean', description: 'Whether the item is archived' },
|
||||
isDraft: { type: 'boolean', description: 'Whether the item is a draft' },
|
||||
fieldData: { type: 'object', description: 'The field data of the item' },
|
||||
},
|
||||
},
|
||||
|
||||
instructions: [
|
||||
'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.',
|
||||
'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.',
|
||||
],
|
||||
|
||||
samplePayload: {
|
||||
siteId: '68f9666057aa8abaa9b0b668',
|
||||
workspaceId: '68f96081e7018465432953b5',
|
||||
collectionId: '68f9666257aa8abaa9b0b6d6',
|
||||
payload: {
|
||||
id: '68fa8445de250e147cd95cfd',
|
||||
cmsLocaleId: '68f9666257aa8abaa9b0b6c9',
|
||||
lastPublished: '2024-01-15T10:30:00.000Z',
|
||||
lastUpdated: '2024-01-15T10:30:00.000Z',
|
||||
createdOn: '2024-01-15T10:30:00.000Z',
|
||||
isArchived: false,
|
||||
isDraft: false,
|
||||
fieldData: {
|
||||
name: 'Sample Blog Post',
|
||||
slug: 'sample-blog-post',
|
||||
'post-summary': 'This is a sample blog post created in the collection',
|
||||
featured: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { WebflowIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '../types'
|
||||
|
||||
export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
|
||||
id: 'webflow_collection_item_deleted',
|
||||
name: 'Collection Item Deleted',
|
||||
provider: 'webflow',
|
||||
description:
|
||||
'Trigger workflow when an item is deleted from a Webflow CMS collection (requires Webflow credentials)',
|
||||
version: '1.0.0',
|
||||
icon: WebflowIcon,
|
||||
|
||||
requiresCredentials: true,
|
||||
credentialProvider: 'webflow',
|
||||
|
||||
configFields: {
|
||||
siteId: {
|
||||
type: 'select',
|
||||
label: 'Site',
|
||||
placeholder: 'Select a site',
|
||||
description: 'The Webflow site to monitor',
|
||||
required: true,
|
||||
options: [],
|
||||
},
|
||||
collectionId: {
|
||||
type: 'select',
|
||||
label: 'Collection',
|
||||
placeholder: 'Select a collection (optional)',
|
||||
description: 'Optionally filter to monitor only a specific collection',
|
||||
required: false,
|
||||
options: [],
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID where the event occurred',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID where the event occurred',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The collection ID where the item was deleted',
|
||||
},
|
||||
payload: {
|
||||
id: { type: 'string', description: 'The ID of the deleted item' },
|
||||
deletedOn: { type: 'string', description: 'Timestamp when the item was deleted' },
|
||||
},
|
||||
},
|
||||
|
||||
instructions: [
|
||||
'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.',
|
||||
'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.',
|
||||
],
|
||||
|
||||
samplePayload: {
|
||||
siteId: '68f9666057aa8abaa9b0b668',
|
||||
workspaceId: '68f96081e7018465432953b5',
|
||||
collectionId: '68f9666257aa8abaa9b0b6d6',
|
||||
payload: {
|
||||
id: '68fa8445de250e147cd95cfd',
|
||||
deletedOn: '2024-01-15T16:20:00.000Z',
|
||||
},
|
||||
},
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { WebflowIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '../types'
|
||||
|
||||
export const webflowFormSubmissionTrigger: TriggerConfig = {
|
||||
id: 'webflow_form_submission',
|
||||
name: 'Form Submission',
|
||||
provider: 'webflow',
|
||||
description:
|
||||
'Trigger workflow when a form is submitted on a Webflow site (requires Webflow credentials)',
|
||||
version: '1.0.0',
|
||||
icon: WebflowIcon,
|
||||
|
||||
requiresCredentials: true,
|
||||
credentialProvider: 'webflow',
|
||||
|
||||
configFields: {
|
||||
siteId: {
|
||||
type: 'select',
|
||||
label: 'Site',
|
||||
placeholder: 'Select a site',
|
||||
description: 'The Webflow site to monitor',
|
||||
required: true,
|
||||
options: [],
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
label: 'Form ID',
|
||||
placeholder: 'form-123abc (optional)',
|
||||
description: 'The ID of the specific form to monitor (optional - leave empty for all forms)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID where the form was submitted',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID where the event occurred',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The name of the form',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The unique ID of the form submission',
|
||||
},
|
||||
submittedAt: {
|
||||
type: 'string',
|
||||
description: 'Timestamp when the form was submitted',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'The form submission field data (keys are field names)',
|
||||
},
|
||||
schema: {
|
||||
type: 'object',
|
||||
description: 'Form schema information',
|
||||
},
|
||||
formElementId: {
|
||||
type: 'string',
|
||||
description: 'The form element ID',
|
||||
},
|
||||
},
|
||||
|
||||
instructions: [
|
||||
'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.',
|
||||
'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.',
|
||||
],
|
||||
|
||||
samplePayload: {
|
||||
siteId: '68f9666057aa8abaa9b0b668',
|
||||
workspaceId: '68f96081e7018465432953b5',
|
||||
name: 'Contact Form',
|
||||
id: '68fa8445de250e147cd95cfd',
|
||||
submittedAt: '2024-01-15T12:00:00.000Z',
|
||||
data: {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
message: 'I would like more information about your services.',
|
||||
'consent-checkbox': 'true',
|
||||
},
|
||||
schema: {
|
||||
fields: [
|
||||
{ name: 'name', type: 'text' },
|
||||
{ name: 'email', type: 'email' },
|
||||
{ name: 'message', type: 'textarea' },
|
||||
],
|
||||
},
|
||||
formElementId: '68f9666257aa8abaa9b0b6e2',
|
||||
},
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { webflowCollectionItemChangedTrigger } from './collection_item_changed'
|
||||
export { webflowCollectionItemCreatedTrigger } from './collection_item_created'
|
||||
export { webflowCollectionItemDeletedTrigger } from './collection_item_deleted'
|
||||
export { webflowFormSubmissionTrigger } from './form_submission'
|
||||
Reference in New Issue
Block a user