diff --git a/apps/docs/content/docs/en/tools/webflow.mdx b/apps/docs/content/docs/en/tools/webflow.mdx index f23fd172cf..168f3eaf43 100644 --- a/apps/docs/content/docs/en/tools/webflow.mdx +++ b/apps/docs/content/docs/en/tools/webflow.mdx @@ -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\) | @@ -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 | @@ -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. | @@ -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. | @@ -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 | diff --git a/apps/sim/app/api/tools/webflow/collections/route.ts b/apps/sim/app/api/tools/webflow/collections/route.ts index 79c1d4605e..2aa17de0da 100644 --- a/apps/sim/app/api/tools/webflow/collections/route.ts +++ b/apps/sim/app/api/tools/webflow/collections/route.ts @@ -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 } ) } @@ -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 } ) } diff --git a/apps/sim/app/api/tools/webflow/items/route.ts b/apps/sim/app/api/tools/webflow/items/route.ts new file mode 100644 index 0000000000..8639c63e35 --- /dev/null +++ b/apps/sim/app/api/tools/webflow/items/route.ts @@ -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 } + ) + } +} diff --git a/apps/sim/app/api/tools/webflow/sites/route.ts b/apps/sim/app/api/tools/webflow/sites/route.ts index f94c3e3406..2cfc4698a3 100644 --- a/apps/sim/app/api/tools/webflow/sites/route.ts +++ b/apps/sim/app/api/tools/webflow/sites/route.ts @@ -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 } ) } @@ -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 } ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx index b2232202d2..27415b31a4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx @@ -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' @@ -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, @@ -84,6 +90,8 @@ export function FileSelectorInput({ projectIdValue, planIdValue, teamIdValue, + siteIdValue, + collectionIdValue, ]) const missingCredential = !normalizedCredentialId @@ -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 || @@ -105,6 +117,8 @@ export function FileSelectorInput({ missingDomain || missingProject || missingPlan || + missingSite || + missingCollection || !selectorResolution?.key if (!selectorResolution?.key) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/project-selector/project-selector-input.tsx index 8dccf4a449..d189aa92f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/project-selector/project-selector-input.tsx @@ -43,14 +43,12 @@ export function ProjectSelectorInput({ // Use previewContextValues if provided (for tools inside agent blocks), otherwise use store values const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore - const linearCredential = previewContextValues?.credential ?? connectedCredentialFromStore const linearTeamId = previewContextValues?.teamId ?? linearTeamIdFromStore const jiraDomain = previewContextValues?.domain ?? jiraDomainFromStore // Derive provider from serviceId using OAuth config const serviceId = subBlock.serviceId || '' const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) - const isLinear = serviceId === 'linear' const { isForeignCredential } = useForeignCredential( effectiveProviderId, @@ -65,7 +63,6 @@ export function ProjectSelectorInput({ }) // Jira/Discord upstream fields - use values from previewContextValues or store - const jiraCredential = connectedCredential const domain = (jiraDomain as string) || '' // Verify Jira credential belongs to current user; if not, treat as absent @@ -84,19 +81,11 @@ export function ProjectSelectorInput({ const selectorResolution = useMemo(() => { return resolveSelectorForSubBlock(subBlock, { workflowId: workflowIdFromUrl || undefined, - credentialId: (isLinear ? linearCredential : jiraCredential) as string | undefined, + credentialId: (connectedCredential as string) || undefined, domain, teamId: (linearTeamId as string) || undefined, }) - }, [ - subBlock, - workflowIdFromUrl, - isLinear, - linearCredential, - jiraCredential, - domain, - linearTeamId, - ]) + }, [subBlock, workflowIdFromUrl, connectedCredential, domain, linearTeamId]) const missingCredential = !selectorResolution?.context.credentialId diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx index 6dfc8044d3..3f4dab8519 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx @@ -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' @@ -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, @@ -84,6 +90,8 @@ export function FileSelectorInput({ projectIdValue, planIdValue, teamIdValue, + siteIdValue, + collectionIdValue, ]) const missingCredential = !normalizedCredentialId @@ -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 || @@ -105,6 +117,8 @@ export function FileSelectorInput({ missingDomain || missingProject || missingPlan || + missingSite || + missingCollection || !selectorResolution?.key if (!selectorResolution?.key) { diff --git a/apps/sim/blocks/blocks/webflow.ts b/apps/sim/blocks/blocks/webflow.ts index 21b3c6cd2d..cdb55df299 100644 --- a/apps/sim/blocks/blocks/webflow.ts +++ b/apps/sim/blocks/blocks/webflow.ts @@ -39,19 +39,65 @@ export const WebflowBlock: BlockConfig = { placeholder: 'Select Webflow account', required: true, }, + { + id: 'siteId', + title: 'Site', + type: 'project-selector', + canonicalParamId: 'siteId', + serviceId: 'webflow', + placeholder: 'Select Webflow site', + dependsOn: ['credential'], + mode: 'basic', + required: true, + }, + { + id: 'manualSiteId', + title: 'Site ID', + type: 'short-input', + canonicalParamId: 'siteId', + placeholder: 'Enter site ID', + mode: 'advanced', + required: true, + }, { id: 'collectionId', + title: 'Collection', + type: 'file-selector', + canonicalParamId: 'collectionId', + serviceId: 'webflow', + placeholder: 'Select collection', + dependsOn: ['credential', 'siteId'], + mode: 'basic', + required: true, + }, + { + id: 'manualCollectionId', title: 'Collection ID', type: 'short-input', + canonicalParamId: 'collectionId', placeholder: 'Enter collection ID', - dependsOn: ['credential'], + mode: 'advanced', required: true, }, { id: 'itemId', + title: 'Item', + type: 'file-selector', + canonicalParamId: 'itemId', + serviceId: 'webflow', + placeholder: 'Select item', + dependsOn: ['credential', 'collectionId'], + mode: 'basic', + condition: { field: 'operation', value: ['get', 'update', 'delete'] }, + required: true, + }, + { + id: 'manualItemId', title: 'Item ID', type: 'short-input', - placeholder: 'ID of the item', + canonicalParamId: 'itemId', + placeholder: 'Enter item ID', + mode: 'advanced', condition: { field: 'operation', value: ['get', 'update', 'delete'] }, required: true, }, @@ -108,7 +154,17 @@ export const WebflowBlock: BlockConfig = { } }, params: (params) => { - const { credential, fieldData, ...rest } = params + const { + credential, + fieldData, + siteId, + manualSiteId, + collectionId, + manualCollectionId, + itemId, + manualItemId, + ...rest + } = params let parsedFieldData: any | undefined try { @@ -119,15 +175,46 @@ export const WebflowBlock: BlockConfig = { throw new Error(`Invalid JSON input for ${params.operation} operation: ${error.message}`) } + const effectiveSiteId = ((siteId as string) || (manualSiteId as string) || '').trim() + const effectiveCollectionId = ( + (collectionId as string) || + (manualCollectionId as string) || + '' + ).trim() + const effectiveItemId = ((itemId as string) || (manualItemId as string) || '').trim() + + if (!effectiveSiteId) { + throw new Error('Site ID is required') + } + + if (!effectiveCollectionId) { + throw new Error('Collection ID is required') + } + const baseParams = { credential, + siteId: effectiveSiteId, + collectionId: effectiveCollectionId, ...rest, } switch (params.operation) { case 'create': case 'update': - return { ...baseParams, fieldData: parsedFieldData } + if (params.operation === 'update' && !effectiveItemId) { + throw new Error('Item ID is required for update operation') + } + return { + ...baseParams, + itemId: effectiveItemId || undefined, + fieldData: parsedFieldData, + } + case 'get': + case 'delete': + if (!effectiveItemId) { + throw new Error(`Item ID is required for ${params.operation} operation`) + } + return { ...baseParams, itemId: effectiveItemId } default: return baseParams } @@ -137,12 +224,15 @@ export const WebflowBlock: BlockConfig = { inputs: { operation: { type: 'string', description: 'Operation to perform' }, credential: { type: 'string', description: 'Webflow OAuth access token' }, + siteId: { type: 'string', description: 'Webflow site identifier' }, + manualSiteId: { type: 'string', description: 'Manual site identifier' }, 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 + manualCollectionId: { type: 'string', description: 'Manual collection identifier' }, + itemId: { type: 'string', description: 'Item identifier' }, + manualItemId: { type: 'string', description: 'Manual item identifier' }, + offset: { type: 'number', description: 'Pagination offset' }, + limit: { type: 'number', description: 'Maximum items to return' }, + fieldData: { type: 'json', description: 'Item field data' }, }, outputs: { items: { type: 'json', description: 'Array of items (list operation)' }, diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 87662747a6..4137e3065f 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -673,6 +673,99 @@ const registry: Record = { return { id: doc.id, label: doc.filename } }, }, + 'webflow.sites': { + key: 'webflow.sites', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'webflow.sites', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'webflow.sites') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ sites: { id: string; name: string }[] }>( + '/api/tools/webflow/sites', + { + method: 'POST', + body, + } + ) + return (data.sites || []).map((site) => ({ + id: site.id, + label: site.name, + })) + }, + }, + 'webflow.collections': { + key: 'webflow.collections', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'webflow.collections', + context.credentialId ?? 'none', + context.siteId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId && context.siteId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'webflow.collections') + if (!context.siteId) { + throw new Error('Missing site ID for webflow.collections selector') + } + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + siteId: context.siteId, + }) + const data = await fetchJson<{ collections: { id: string; name: string }[] }>( + '/api/tools/webflow/collections', + { + method: 'POST', + body, + } + ) + return (data.collections || []).map((collection) => ({ + id: collection.id, + label: collection.name, + })) + }, + }, + 'webflow.items': { + key: 'webflow.items', + staleTime: 15 * 1000, + getQueryKey: ({ context, search }: SelectorQueryArgs) => [ + 'selectors', + 'webflow.items', + context.credentialId ?? 'none', + context.collectionId ?? 'none', + search ?? '', + ], + enabled: ({ context }) => Boolean(context.credentialId && context.collectionId), + fetchList: async ({ context, search }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'webflow.items') + if (!context.collectionId) { + throw new Error('Missing collection ID for webflow.items selector') + } + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + collectionId: context.collectionId, + search, + }) + const data = await fetchJson<{ items: { id: string; name: string }[] }>( + '/api/tools/webflow/items', + { + method: 'POST', + body, + } + ) + return (data.items || []).map((item) => ({ + id: item.id, + label: item.name, + })) + }, + }, } export function getSelectorDefinition(key: SelectorKey): SelectorDefinition { diff --git a/apps/sim/hooks/selectors/resolution.ts b/apps/sim/hooks/selectors/resolution.ts index fcb6747d2c..76e3f2117b 100644 --- a/apps/sim/hooks/selectors/resolution.ts +++ b/apps/sim/hooks/selectors/resolution.ts @@ -15,6 +15,8 @@ export interface SelectorResolutionArgs { planId?: string teamId?: string knowledgeBaseId?: string + siteId?: string + collectionId?: string } const defaultContext: SelectorContext = {} @@ -52,6 +54,8 @@ function buildBaseContext( planId: args.planId, teamId: args.teamId, knowledgeBaseId: args.knowledgeBaseId, + siteId: args.siteId, + collectionId: args.collectionId, ...extra, } } @@ -106,6 +110,14 @@ function resolveFileSelector( } case 'sharepoint': return { key: 'sharepoint.sites', context, allowSearch: true } + case 'webflow': + if (subBlock.id === 'collectionId') { + return { key: 'webflow.collections', context, allowSearch: false } + } + if (subBlock.id === 'itemId') { + return { key: 'webflow.items', context, allowSearch: true } + } + return { key: null, context, allowSearch: true } default: return { key: null, context, allowSearch: true } } @@ -159,6 +171,8 @@ function resolveProjectSelector( } case 'jira': return { key: 'jira.projects', context, allowSearch: true } + case 'webflow': + return { key: 'webflow.sites', context, allowSearch: false } default: return { key: null, context, allowSearch: true } } diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index d83a7816a7..d186c4d50e 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -23,6 +23,9 @@ export type SelectorKey = | 'microsoft.planner' | 'google.drive' | 'knowledge.documents' + | 'webflow.sites' + | 'webflow.collections' + | 'webflow.items' export interface SelectorOption { id: string @@ -43,6 +46,8 @@ export interface SelectorContext { planId?: string mimeType?: string fileId?: string + siteId?: string + collectionId?: string } export interface SelectorQueryArgs { diff --git a/apps/sim/tools/webflow/create_item.ts b/apps/sim/tools/webflow/create_item.ts index 7dd736b23b..516bab931f 100644 --- a/apps/sim/tools/webflow/create_item.ts +++ b/apps/sim/tools/webflow/create_item.ts @@ -20,6 +20,12 @@ export const webflowCreateItemTool: ToolConfig