Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/docs/content/docs/en/tools/webflow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ List all items from a Webflow CMS collection

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Yes | ID of the Webflow site |
| `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\) |
Expand All @@ -61,6 +62,7 @@ Get a single item from a Webflow CMS collection

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Yes | ID of the Webflow site |
| `collectionId` | string | Yes | ID of the collection |
| `itemId` | string | Yes | ID of the item to retrieve |

Expand All @@ -79,6 +81,7 @@ Create a new item in a Webflow CMS collection

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Yes | ID of the Webflow site |
| `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. |

Expand All @@ -97,6 +100,7 @@ Update an existing item in a Webflow CMS collection

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Yes | ID of the Webflow site |
| `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. |
Expand All @@ -116,6 +120,7 @@ Delete an item from a Webflow CMS collection

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Yes | ID of the Webflow site |
| `collectionId` | string | Yes | ID of the collection |
| `itemId` | string | Yes | ID of the item to delete |

Expand Down
57 changes: 39 additions & 18 deletions apps/sim/app/api/tools/webflow/collections/route.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

const logger = createLogger('WebflowCollectionsAPI')

export const dynamic = 'force-dynamic'

export async function GET(request: NextRequest) {
export async function POST(request: Request) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId, siteId } = body

const { searchParams } = new URL(request.url)
const siteId = searchParams.get('siteId')
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}

if (!siteId) {
return NextResponse.json({ error: 'Missing siteId parameter' }, { status: 400 })
logger.error('Missing siteId in request')
return NextResponse.json({ error: 'Site ID is required' }, { status: 400 })
}

const accessToken = await getOAuthToken(session.user.id, 'webflow')
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'No Webflow access token found. Please connect your Webflow account.' },
{ status: 404 }
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}

Expand Down Expand Up @@ -58,11 +79,11 @@ export async function GET(request: NextRequest) {
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({ collections: formattedCollections })
} catch (error) {
logger.error('Error processing Webflow collections request:', error)
return NextResponse.json(
{ error: 'Internal server error', details: error.message },
{ error: 'Failed to retrieve Webflow collections', details: (error as Error).message },
{ status: 500 }
)
}
Expand Down
104 changes: 104 additions & 0 deletions apps/sim/app/api/tools/webflow/items/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

const logger = createLogger('WebflowItemsAPI')

export const dynamic = 'force-dynamic'

export async function POST(request: Request) {
try {
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId, collectionId, search } = body

if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}

if (!collectionId) {
logger.error('Missing collectionId in request')
return NextResponse.json({ error: 'Collection ID is required' }, { status: 400 })
}

const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}

const response = await fetch(
`https://api.webflow.com/v2/collections/${collectionId}/items?limit=100`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
accept: 'application/json',
},
}
)

if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Webflow items', {
status: response.status,
error: errorData,
collectionId,
})
return NextResponse.json(
{ error: 'Failed to fetch Webflow items', details: errorData },
{ status: response.status }
)
}

const data = await response.json()
const items = data.items || []

let formattedItems = items.map((item: any) => {
const fieldData = item.fieldData || {}
const name = fieldData.name || fieldData.title || fieldData.slug || item.id
return {
id: item.id,
name,
}
})

if (search) {
const searchLower = search.toLowerCase()
formattedItems = formattedItems.filter((item: { id: string; name: string }) =>
item.name.toLowerCase().includes(searchLower)
)
}

return NextResponse.json({ items: formattedItems })
} catch (error) {
logger.error('Error processing Webflow items request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Webflow items', details: (error as Error).message },
{ status: 500 }
)
}
}
51 changes: 37 additions & 14 deletions apps/sim/app/api/tools/webflow/sites/route.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

const logger = createLogger('WebflowSitesAPI')

export const dynamic = 'force-dynamic'

export async function GET(request: NextRequest) {
export async function POST(request: Request) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId } = body

if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}

const accessToken = await getOAuthToken(session.user.id, 'webflow')
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'No Webflow access token found. Please connect your Webflow account.' },
{ status: 404 }
{
error: 'Could not retrieve access token',
authRequired: true,
},
{ status: 401 }
)
}

Expand Down Expand Up @@ -50,11 +73,11 @@ export async function GET(request: NextRequest) {
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({ sites: formattedSites })
} catch (error) {
logger.error('Error processing Webflow sites request:', error)
return NextResponse.json(
{ error: 'Internal server error', details: error.message },
{ error: 'Failed to retrieve Webflow sites', details: (error as Error).message },
{ status: 500 }
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,16 @@ export function FileSelectorInput({
const [projectIdValueFromStore] = useSubBlockValue(blockId, 'projectId')
const [planIdValueFromStore] = useSubBlockValue(blockId, 'planId')
const [teamIdValueFromStore] = useSubBlockValue(blockId, 'teamId')
const [siteIdValueFromStore] = useSubBlockValue(blockId, 'siteId')
const [collectionIdValueFromStore] = useSubBlockValue(blockId, 'collectionId')

const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
const domainValue = previewContextValues?.domain ?? domainValueFromStore
const projectIdValue = previewContextValues?.projectId ?? projectIdValueFromStore
const planIdValue = previewContextValues?.planId ?? planIdValueFromStore
const teamIdValue = previewContextValues?.teamId ?? teamIdValueFromStore
const siteIdValue = previewContextValues?.siteId ?? siteIdValueFromStore
const collectionIdValue = previewContextValues?.collectionId ?? collectionIdValueFromStore

const normalizedCredentialId =
typeof connectedCredential === 'string'
Expand All @@ -75,6 +79,8 @@ export function FileSelectorInput({
projectId: (projectIdValue as string) || undefined,
planId: (planIdValue as string) || undefined,
teamId: (teamIdValue as string) || undefined,
siteId: (siteIdValue as string) || undefined,
collectionId: (collectionIdValue as string) || undefined,
})
}, [
subBlock,
Expand All @@ -84,6 +90,8 @@ export function FileSelectorInput({
projectIdValue,
planIdValue,
teamIdValue,
siteIdValue,
collectionIdValue,
])

const missingCredential = !normalizedCredentialId
Expand All @@ -97,6 +105,10 @@ export function FileSelectorInput({
!selectorResolution.context.projectId
const missingPlan =
selectorResolution?.key === 'microsoft.planner' && !selectorResolution.context.planId
const missingSite =
selectorResolution?.key === 'webflow.collections' && !selectorResolution.context.siteId
const missingCollection =
selectorResolution?.key === 'webflow.items' && !selectorResolution.context.collectionId

const disabledReason =
finalDisabled ||
Expand All @@ -105,6 +117,8 @@ export function FileSelectorInput({
missingDomain ||
missingProject ||
missingPlan ||
missingSite ||
missingCollection ||
!selectorResolution?.key

if (!selectorResolution?.key) {
Expand Down
Loading