Skip to content

Commit c962e3b

Browse files
authored
feat(webflow): added collection, item, & site selectors for webflow (#2368)
* feat(webflow): added collection, item, & site selectors for webflow * ack PR comments * ack PR comments
1 parent d5b95cb commit c962e3b

File tree

17 files changed

+460
-57
lines changed

17 files changed

+460
-57
lines changed

apps/docs/content/docs/en/tools/webflow.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ List all items from a Webflow CMS collection
4242

4343
| Parameter | Type | Required | Description |
4444
| --------- | ---- | -------- | ----------- |
45+
| `siteId` | string | Yes | ID of the Webflow site |
4546
| `collectionId` | string | Yes | ID of the collection |
4647
| `offset` | number | No | Offset for pagination \(optional\) |
4748
| `limit` | number | No | Maximum number of items to return \(optional, default: 100\) |
@@ -61,6 +62,7 @@ Get a single item from a Webflow CMS collection
6162

6263
| Parameter | Type | Required | Description |
6364
| --------- | ---- | -------- | ----------- |
65+
| `siteId` | string | Yes | ID of the Webflow site |
6466
| `collectionId` | string | Yes | ID of the collection |
6567
| `itemId` | string | Yes | ID of the item to retrieve |
6668

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

8082
| Parameter | Type | Required | Description |
8183
| --------- | ---- | -------- | ----------- |
84+
| `siteId` | string | Yes | ID of the Webflow site |
8285
| `collectionId` | string | Yes | ID of the collection |
8386
| `fieldData` | json | Yes | Field data for the new item as a JSON object. Keys should match collection field names. |
8487

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

98101
| Parameter | Type | Required | Description |
99102
| --------- | ---- | -------- | ----------- |
103+
| `siteId` | string | Yes | ID of the Webflow site |
100104
| `collectionId` | string | Yes | ID of the collection |
101105
| `itemId` | string | Yes | ID of the item to update |
102106
| `fieldData` | json | Yes | Field data to update as a JSON object. Only include fields you want to change. |
@@ -116,6 +120,7 @@ Delete an item from a Webflow CMS collection
116120

117121
| Parameter | Type | Required | Description |
118122
| --------- | ---- | -------- | ----------- |
123+
| `siteId` | string | Yes | ID of the Webflow site |
119124
| `collectionId` | string | Yes | ID of the collection |
120125
| `itemId` | string | Yes | ID of the item to delete |
121126

apps/sim/app/api/tools/webflow/collections/route.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,53 @@
1-
import { type NextRequest, NextResponse } from 'next/server'
2-
import { getSession } from '@/lib/auth'
1+
import { NextResponse } from 'next/server'
2+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
3+
import { generateRequestId } from '@/lib/core/utils/request'
34
import { createLogger } from '@/lib/logs/console/logger'
4-
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
5+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
56

67
const logger = createLogger('WebflowCollectionsAPI')
78

89
export const dynamic = 'force-dynamic'
910

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

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

2022
if (!siteId) {
21-
return NextResponse.json({ error: 'Missing siteId parameter' }, { status: 400 })
23+
logger.error('Missing siteId in request')
24+
return NextResponse.json({ error: 'Site ID is required' }, { status: 400 })
2225
}
2326

24-
const accessToken = await getOAuthToken(session.user.id, 'webflow')
27+
const authz = await authorizeCredentialUse(request as any, {
28+
credentialId: credential,
29+
workflowId,
30+
})
31+
if (!authz.ok || !authz.credentialOwnerUserId) {
32+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
33+
}
2534

35+
const accessToken = await refreshAccessTokenIfNeeded(
36+
credential,
37+
authz.credentialOwnerUserId,
38+
requestId
39+
)
2640
if (!accessToken) {
41+
logger.error('Failed to get access token', {
42+
credentialId: credential,
43+
userId: authz.credentialOwnerUserId,
44+
})
2745
return NextResponse.json(
28-
{ error: 'No Webflow access token found. Please connect your Webflow account.' },
29-
{ status: 404 }
46+
{
47+
error: 'Could not retrieve access token',
48+
authRequired: true,
49+
},
50+
{ status: 401 }
3051
)
3152
}
3253

@@ -58,11 +79,11 @@ export async function GET(request: NextRequest) {
5879
name: collection.displayName || collection.slug || collection.id,
5980
}))
6081

61-
return NextResponse.json({ collections: formattedCollections }, { status: 200 })
62-
} catch (error: any) {
63-
logger.error('Error fetching Webflow collections', error)
82+
return NextResponse.json({ collections: formattedCollections })
83+
} catch (error) {
84+
logger.error('Error processing Webflow collections request:', error)
6485
return NextResponse.json(
65-
{ error: 'Internal server error', details: error.message },
86+
{ error: 'Failed to retrieve Webflow collections', details: (error as Error).message },
6687
{ status: 500 }
6788
)
6889
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { NextResponse } from 'next/server'
2+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
3+
import { generateRequestId } from '@/lib/core/utils/request'
4+
import { createLogger } from '@/lib/logs/console/logger'
5+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
6+
7+
const logger = createLogger('WebflowItemsAPI')
8+
9+
export const dynamic = 'force-dynamic'
10+
11+
export async function POST(request: Request) {
12+
try {
13+
const requestId = generateRequestId()
14+
const body = await request.json()
15+
const { credential, workflowId, collectionId, search } = body
16+
17+
if (!credential) {
18+
logger.error('Missing credential in request')
19+
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
20+
}
21+
22+
if (!collectionId) {
23+
logger.error('Missing collectionId in request')
24+
return NextResponse.json({ error: 'Collection ID is required' }, { status: 400 })
25+
}
26+
27+
const authz = await authorizeCredentialUse(request as any, {
28+
credentialId: credential,
29+
workflowId,
30+
})
31+
if (!authz.ok || !authz.credentialOwnerUserId) {
32+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
33+
}
34+
35+
const accessToken = await refreshAccessTokenIfNeeded(
36+
credential,
37+
authz.credentialOwnerUserId,
38+
requestId
39+
)
40+
if (!accessToken) {
41+
logger.error('Failed to get access token', {
42+
credentialId: credential,
43+
userId: authz.credentialOwnerUserId,
44+
})
45+
return NextResponse.json(
46+
{
47+
error: 'Could not retrieve access token',
48+
authRequired: true,
49+
},
50+
{ status: 401 }
51+
)
52+
}
53+
54+
const response = await fetch(
55+
`https://api.webflow.com/v2/collections/${collectionId}/items?limit=100`,
56+
{
57+
headers: {
58+
Authorization: `Bearer ${accessToken}`,
59+
accept: 'application/json',
60+
},
61+
}
62+
)
63+
64+
if (!response.ok) {
65+
const errorData = await response.json().catch(() => ({}))
66+
logger.error('Failed to fetch Webflow items', {
67+
status: response.status,
68+
error: errorData,
69+
collectionId,
70+
})
71+
return NextResponse.json(
72+
{ error: 'Failed to fetch Webflow items', details: errorData },
73+
{ status: response.status }
74+
)
75+
}
76+
77+
const data = await response.json()
78+
const items = data.items || []
79+
80+
let formattedItems = items.map((item: any) => {
81+
const fieldData = item.fieldData || {}
82+
const name = fieldData.name || fieldData.title || fieldData.slug || item.id
83+
return {
84+
id: item.id,
85+
name,
86+
}
87+
})
88+
89+
if (search) {
90+
const searchLower = search.toLowerCase()
91+
formattedItems = formattedItems.filter((item: { id: string; name: string }) =>
92+
item.name.toLowerCase().includes(searchLower)
93+
)
94+
}
95+
96+
return NextResponse.json({ items: formattedItems })
97+
} catch (error) {
98+
logger.error('Error processing Webflow items request:', error)
99+
return NextResponse.json(
100+
{ error: 'Failed to retrieve Webflow items', details: (error as Error).message },
101+
{ status: 500 }
102+
)
103+
}
104+
}

apps/sim/app/api/tools/webflow/sites/route.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
1-
import { type NextRequest, NextResponse } from 'next/server'
2-
import { getSession } from '@/lib/auth'
1+
import { NextResponse } from 'next/server'
2+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
3+
import { generateRequestId } from '@/lib/core/utils/request'
34
import { createLogger } from '@/lib/logs/console/logger'
4-
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
5+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
56

67
const logger = createLogger('WebflowSitesAPI')
78

89
export const dynamic = 'force-dynamic'
910

10-
export async function GET(request: NextRequest) {
11+
export async function POST(request: Request) {
1112
try {
12-
const session = await getSession()
13-
if (!session?.user?.id) {
14-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
13+
const requestId = generateRequestId()
14+
const body = await request.json()
15+
const { credential, workflowId } = body
16+
17+
if (!credential) {
18+
logger.error('Missing credential in request')
19+
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
1520
}
1621

17-
const accessToken = await getOAuthToken(session.user.id, 'webflow')
22+
const authz = await authorizeCredentialUse(request as any, {
23+
credentialId: credential,
24+
workflowId,
25+
})
26+
if (!authz.ok || !authz.credentialOwnerUserId) {
27+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
28+
}
1829

30+
const accessToken = await refreshAccessTokenIfNeeded(
31+
credential,
32+
authz.credentialOwnerUserId,
33+
requestId
34+
)
1935
if (!accessToken) {
36+
logger.error('Failed to get access token', {
37+
credentialId: credential,
38+
userId: authz.credentialOwnerUserId,
39+
})
2040
return NextResponse.json(
21-
{ error: 'No Webflow access token found. Please connect your Webflow account.' },
22-
{ status: 404 }
41+
{
42+
error: 'Could not retrieve access token',
43+
authRequired: true,
44+
},
45+
{ status: 401 }
2346
)
2447
}
2548

@@ -50,11 +73,11 @@ export async function GET(request: NextRequest) {
5073
name: site.displayName || site.shortName || site.id,
5174
}))
5275

53-
return NextResponse.json({ sites: formattedSites }, { status: 200 })
54-
} catch (error: any) {
55-
logger.error('Error fetching Webflow sites', error)
76+
return NextResponse.json({ sites: formattedSites })
77+
} catch (error) {
78+
logger.error('Error processing Webflow sites request:', error)
5679
return NextResponse.json(
57-
{ error: 'Internal server error', details: error.message },
80+
{ error: 'Failed to retrieve Webflow sites', details: (error as Error).message },
5881
{ status: 500 }
5982
)
6083
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,16 @@ export function FileSelectorInput({
4747
const [projectIdValueFromStore] = useSubBlockValue(blockId, 'projectId')
4848
const [planIdValueFromStore] = useSubBlockValue(blockId, 'planId')
4949
const [teamIdValueFromStore] = useSubBlockValue(blockId, 'teamId')
50+
const [siteIdValueFromStore] = useSubBlockValue(blockId, 'siteId')
51+
const [collectionIdValueFromStore] = useSubBlockValue(blockId, 'collectionId')
5052

5153
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
5254
const domainValue = previewContextValues?.domain ?? domainValueFromStore
5355
const projectIdValue = previewContextValues?.projectId ?? projectIdValueFromStore
5456
const planIdValue = previewContextValues?.planId ?? planIdValueFromStore
5557
const teamIdValue = previewContextValues?.teamId ?? teamIdValueFromStore
58+
const siteIdValue = previewContextValues?.siteId ?? siteIdValueFromStore
59+
const collectionIdValue = previewContextValues?.collectionId ?? collectionIdValueFromStore
5660

5761
const normalizedCredentialId =
5862
typeof connectedCredential === 'string'
@@ -75,6 +79,8 @@ export function FileSelectorInput({
7579
projectId: (projectIdValue as string) || undefined,
7680
planId: (planIdValue as string) || undefined,
7781
teamId: (teamIdValue as string) || undefined,
82+
siteId: (siteIdValue as string) || undefined,
83+
collectionId: (collectionIdValue as string) || undefined,
7884
})
7985
}, [
8086
subBlock,
@@ -84,6 +90,8 @@ export function FileSelectorInput({
8490
projectIdValue,
8591
planIdValue,
8692
teamIdValue,
93+
siteIdValue,
94+
collectionIdValue,
8795
])
8896

8997
const missingCredential = !normalizedCredentialId
@@ -97,6 +105,10 @@ export function FileSelectorInput({
97105
!selectorResolution.context.projectId
98106
const missingPlan =
99107
selectorResolution?.key === 'microsoft.planner' && !selectorResolution.context.planId
108+
const missingSite =
109+
selectorResolution?.key === 'webflow.collections' && !selectorResolution.context.siteId
110+
const missingCollection =
111+
selectorResolution?.key === 'webflow.items' && !selectorResolution.context.collectionId
100112

101113
const disabledReason =
102114
finalDisabled ||
@@ -105,6 +117,8 @@ export function FileSelectorInput({
105117
missingDomain ||
106118
missingProject ||
107119
missingPlan ||
120+
missingSite ||
121+
missingCollection ||
108122
!selectorResolution?.key
109123

110124
if (!selectorResolution?.key) {

0 commit comments

Comments
 (0)