From 665e22c68a2511ea0960e61307bd90c9ceb43725 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 17:26:20 -0800 Subject: [PATCH 01/32] feat(selectors): add dropdown selectors for 14 integrations --- .../sim/app/api/tools/airtable/bases/route.ts | 79 ++ .../app/api/tools/airtable/tables/route.ts | 95 +++ .../app/api/tools/asana/workspaces/route.ts | 79 ++ apps/sim/app/api/tools/attio/lists/route.ts | 79 ++ apps/sim/app/api/tools/attio/objects/route.ts | 79 ++ .../app/api/tools/calcom/event-types/route.ts | 83 ++ .../app/api/tools/calcom/schedules/route.ts | 80 ++ .../tools/google_bigquery/datasets/route.ts | 100 +++ .../api/tools/google_bigquery/tables/route.ts | 94 +++ .../tools/google_tasks/task-lists/route.ts | 79 ++ .../tools/jsm/selector-requesttypes/route.ts | 82 ++ .../tools/jsm/selector-servicedesks/route.ts | 72 ++ .../tools/microsoft_planner/plans/route.ts | 102 +++ .../app/api/tools/notion/databases/route.ts | 86 ++ apps/sim/app/api/tools/notion/pages/route.ts | 86 ++ .../api/tools/pipedrive/pipelines/route.ts | 79 ++ .../app/api/tools/sharepoint/lists/route.ts | 122 +++ apps/sim/app/api/tools/trello/boards/route.ts | 89 ++ apps/sim/app/api/tools/zoom/meetings/route.ts | 82 ++ .../sub-block/hooks/use-selector-setup.ts | 3 + apps/sim/blocks/blocks.test.ts | 13 +- apps/sim/blocks/blocks/airtable.ts | 34 +- apps/sim/blocks/blocks/asana.ts | 37 + apps/sim/blocks/blocks/attio.ts | 74 ++ apps/sim/blocks/blocks/calcom.ts | 80 ++ apps/sim/blocks/blocks/confluence.ts | 15 + apps/sim/blocks/blocks/google_bigquery.ts | 32 + apps/sim/blocks/blocks/google_tasks.ts | 17 +- .../blocks/blocks/jira_service_management.ts | 57 ++ apps/sim/blocks/blocks/microsoft_planner.ts | 28 +- apps/sim/blocks/blocks/notion.ts | 108 ++- apps/sim/blocks/blocks/pipedrive.ts | 42 +- apps/sim/blocks/blocks/sharepoint.ts | 16 +- apps/sim/blocks/blocks/trello.ts | 63 +- apps/sim/blocks/blocks/zoom.ts | 29 +- apps/sim/hooks/selectors/registry.ts | 763 ++++++++++++++++++ apps/sim/hooks/selectors/types.ts | 23 + 37 files changed, 2986 insertions(+), 95 deletions(-) create mode 100644 apps/sim/app/api/tools/airtable/bases/route.ts create mode 100644 apps/sim/app/api/tools/airtable/tables/route.ts create mode 100644 apps/sim/app/api/tools/asana/workspaces/route.ts create mode 100644 apps/sim/app/api/tools/attio/lists/route.ts create mode 100644 apps/sim/app/api/tools/attio/objects/route.ts create mode 100644 apps/sim/app/api/tools/calcom/event-types/route.ts create mode 100644 apps/sim/app/api/tools/calcom/schedules/route.ts create mode 100644 apps/sim/app/api/tools/google_bigquery/datasets/route.ts create mode 100644 apps/sim/app/api/tools/google_bigquery/tables/route.ts create mode 100644 apps/sim/app/api/tools/google_tasks/task-lists/route.ts create mode 100644 apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts create mode 100644 apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts create mode 100644 apps/sim/app/api/tools/microsoft_planner/plans/route.ts create mode 100644 apps/sim/app/api/tools/notion/databases/route.ts create mode 100644 apps/sim/app/api/tools/notion/pages/route.ts create mode 100644 apps/sim/app/api/tools/pipedrive/pipelines/route.ts create mode 100644 apps/sim/app/api/tools/sharepoint/lists/route.ts create mode 100644 apps/sim/app/api/tools/trello/boards/route.ts create mode 100644 apps/sim/app/api/tools/zoom/meetings/route.ts diff --git a/apps/sim/app/api/tools/airtable/bases/route.ts b/apps/sim/app/api/tools/airtable/bases/route.ts new file mode 100644 index 00000000000..7c9799c4ece --- /dev/null +++ b/apps/sim/app/api/tools/airtable/bases/route.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('AirtableBasesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.airtable.com/v0/meta/bases', { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Airtable bases', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Airtable bases', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const bases = (data.bases || []).map((base: { id: string; name: string }) => ({ + id: base.id, + name: base.name, + })) + + return NextResponse.json({ bases }) + } catch (error) { + logger.error('Error processing Airtable bases request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Airtable bases', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/airtable/tables/route.ts b/apps/sim/app/api/tools/airtable/tables/route.ts new file mode 100644 index 00000000000..32a2fdd90bb --- /dev/null +++ b/apps/sim/app/api/tools/airtable/tables/route.ts @@ -0,0 +1,95 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateAirtableId } from '@/lib/core/security/input-validation' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('AirtableTablesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { credential, workflowId, baseId } = body + + if (!credential) { + logger.error('Missing credential in request') + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } + + if (!baseId) { + logger.error('Missing baseId in request') + return NextResponse.json({ error: 'Base ID is required' }, { status: 400 }) + } + + const baseIdValidation = validateAirtableId(baseId, 'app', 'baseId') + if (!baseIdValidation.isValid) { + logger.error('Invalid baseId', { error: baseIdValidation.error }) + return NextResponse.json({ error: baseIdValidation.error }, { 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.airtable.com/v0/meta/bases/${baseIdValidation.sanitized}/tables`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Airtable tables', { + status: response.status, + error: errorData, + baseId, + }) + return NextResponse.json( + { error: 'Failed to fetch Airtable tables', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const tables = (data.tables || []).map((table: { id: string; name: string }) => ({ + id: table.id, + name: table.name, + })) + + return NextResponse.json({ tables }) + } catch (error) { + logger.error('Error processing Airtable tables request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Airtable tables', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/asana/workspaces/route.ts b/apps/sim/app/api/tools/asana/workspaces/route.ts new file mode 100644 index 00000000000..3eeefbedeaf --- /dev/null +++ b/apps/sim/app/api/tools/asana/workspaces/route.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('AsanaWorkspacesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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://app.asana.com/api/1.0/workspaces', { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Asana workspaces', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Asana workspaces', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const workspaces = (data.data || []).map((workspace: { gid: string; name: string }) => ({ + gid: workspace.gid, + name: workspace.name, + })) + + return NextResponse.json({ workspaces }) + } catch (error) { + logger.error('Error processing Asana workspaces request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Asana workspaces', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/attio/lists/route.ts b/apps/sim/app/api/tools/attio/lists/route.ts new file mode 100644 index 00000000000..75e0909901f --- /dev/null +++ b/apps/sim/app/api/tools/attio/lists/route.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('AttioListsAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.attio.com/v2/lists', { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Attio lists', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Attio lists', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const lists = (data.data || []).map((list: { api_slug: string; name: string }) => ({ + id: list.api_slug, + name: list.name, + })) + + return NextResponse.json({ lists }) + } catch (error) { + logger.error('Error processing Attio lists request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Attio lists', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/attio/objects/route.ts b/apps/sim/app/api/tools/attio/objects/route.ts new file mode 100644 index 00000000000..559253b6a17 --- /dev/null +++ b/apps/sim/app/api/tools/attio/objects/route.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('AttioObjectsAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.attio.com/v2/objects', { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Attio objects', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Attio objects', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const objects = (data.data || []).map((obj: { api_slug: string; singular_noun: string }) => ({ + id: obj.api_slug, + name: obj.singular_noun, + })) + + return NextResponse.json({ objects }) + } catch (error) { + logger.error('Error processing Attio objects request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Attio objects', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/calcom/event-types/route.ts b/apps/sim/app/api/tools/calcom/event-types/route.ts new file mode 100644 index 00000000000..cfcfd707bf4 --- /dev/null +++ b/apps/sim/app/api/tools/calcom/event-types/route.ts @@ -0,0 +1,83 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('CalcomEventTypesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.cal.com/v2/event-types', { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'cal-api-version': '2024-06-14', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Cal.com event types', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Cal.com event types', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const eventTypes = (data.data || []).map( + (eventType: { id: number; title: string; slug: string }) => ({ + id: eventType.id, + title: eventType.title, + slug: eventType.slug, + }) + ) + + return NextResponse.json({ eventTypes }) + } catch (error) { + logger.error('Error processing Cal.com event types request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Cal.com event types', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/calcom/schedules/route.ts b/apps/sim/app/api/tools/calcom/schedules/route.ts new file mode 100644 index 00000000000..8ca3cb49a94 --- /dev/null +++ b/apps/sim/app/api/tools/calcom/schedules/route.ts @@ -0,0 +1,80 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('CalcomSchedulesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.cal.com/v2/schedules', { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'cal-api-version': '2024-06-11', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Cal.com schedules', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Cal.com schedules', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const schedules = (data.data || []).map((schedule: { id: number; name: string }) => ({ + id: schedule.id, + name: schedule.name, + })) + + return NextResponse.json({ schedules }) + } catch (error) { + logger.error('Error processing Cal.com schedules request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Cal.com schedules', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/google_bigquery/datasets/route.ts b/apps/sim/app/api/tools/google_bigquery/datasets/route.ts new file mode 100644 index 00000000000..5e9a604759e --- /dev/null +++ b/apps/sim/app/api/tools/google_bigquery/datasets/route.ts @@ -0,0 +1,100 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('GoogleBigQueryDatasetsAPI') + +export const dynamic = 'force-dynamic' + +/** + * POST /api/tools/google_bigquery/datasets + * + * Fetches the list of BigQuery datasets for a given project using the caller's OAuth credential. + * + * @param request - Incoming request containing `credential`, `workflowId`, and `projectId` in the JSON body + * @returns JSON response with a `datasets` array, each entry containing `datasetReference` and optional `friendlyName` + */ +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { credential, workflowId, projectId } = body + + if (!credential) { + logger.error('Missing credential in request') + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } + + if (!projectId) { + logger.error('Missing project ID in request') + return NextResponse.json({ error: 'Project 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://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(projectId)}/datasets?maxResults=200`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch BigQuery datasets', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch BigQuery datasets', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const datasets = (data.datasets || []).map( + (ds: { + datasetReference: { datasetId: string; projectId: string } + friendlyName?: string + }) => ({ + datasetReference: ds.datasetReference, + friendlyName: ds.friendlyName, + }) + ) + + return NextResponse.json({ datasets }) + } catch (error) { + logger.error('Error processing BigQuery datasets request:', error) + return NextResponse.json( + { error: 'Failed to retrieve BigQuery datasets', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/google_bigquery/tables/route.ts b/apps/sim/app/api/tools/google_bigquery/tables/route.ts new file mode 100644 index 00000000000..5ac2a0d3705 --- /dev/null +++ b/apps/sim/app/api/tools/google_bigquery/tables/route.ts @@ -0,0 +1,94 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('GoogleBigQueryTablesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { credential, workflowId, projectId, datasetId } = body + + if (!credential) { + logger.error('Missing credential in request') + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } + + if (!projectId) { + logger.error('Missing project ID in request') + return NextResponse.json({ error: 'Project ID is required' }, { status: 400 }) + } + + if (!datasetId) { + logger.error('Missing dataset ID in request') + return NextResponse.json({ error: 'Dataset 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://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}/tables?maxResults=200`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch BigQuery tables', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch BigQuery tables', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const tables = (data.tables || []).map( + (t: { tableReference: { tableId: string }; friendlyName?: string }) => ({ + tableReference: t.tableReference, + friendlyName: t.friendlyName, + }) + ) + + return NextResponse.json({ tables }) + } catch (error) { + logger.error('Error processing BigQuery tables request:', error) + return NextResponse.json( + { error: 'Failed to retrieve BigQuery tables', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/google_tasks/task-lists/route.ts b/apps/sim/app/api/tools/google_tasks/task-lists/route.ts new file mode 100644 index 00000000000..4b37e25129b --- /dev/null +++ b/apps/sim/app/api/tools/google_tasks/task-lists/route.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('GoogleTasksTaskListsAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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://tasks.googleapis.com/tasks/v1/users/@me/lists', { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Google Tasks task lists', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Google Tasks task lists', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const taskLists = (data.items || []).map((list: { id: string; title: string }) => ({ + id: list.id, + title: list.title, + })) + + return NextResponse.json({ taskLists }) + } catch (error) { + logger.error('Error processing Google Tasks task lists request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Google Tasks task lists', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts new file mode 100644 index 00000000000..297eacbc7f9 --- /dev/null +++ b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts @@ -0,0 +1,82 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' + +const logger = createLogger('JsmSelectorRequestTypesAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const serviceDeskId = searchParams.get('serviceDeskId') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!serviceDeskId) { + return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 }) + } + + const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId') + if (!serviceDeskIdValidation.isValid) { + return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 }) + } + + const cloudId = await getJiraCloudId(domain, accessToken) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const baseUrl = getJsmApiBaseUrl(cloudId) + const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype?limit=100` + + const response = await fetch(url, { + method: 'GET', + headers: getJsmHeaders(accessToken), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('JSM API error:', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + return NextResponse.json( + { error: `JSM API error: ${response.status} ${response.statusText}` }, + { status: response.status } + ) + } + + const data = await response.json() + const requestTypes = (data.values || []).map((rt: { id: string; name: string }) => ({ + id: rt.id, + name: rt.name, + })) + + return NextResponse.json({ requestTypes }) + } catch (error) { + logger.error('Error listing JSM request types:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts new file mode 100644 index 00000000000..ce162a4d51f --- /dev/null +++ b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts @@ -0,0 +1,72 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' + +const logger = createLogger('JsmSelectorServiceDesksAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + const cloudId = await getJiraCloudId(domain, accessToken) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const baseUrl = getJsmApiBaseUrl(cloudId) + const url = `${baseUrl}/servicedesk?limit=100` + + const response = await fetch(url, { + method: 'GET', + headers: getJsmHeaders(accessToken), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('JSM API error:', { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + return NextResponse.json( + { error: `JSM API error: ${response.status} ${response.statusText}` }, + { status: response.status } + ) + } + + const data = await response.json() + const serviceDesks = (data.values || []).map((sd: { id: string; projectName: string }) => ({ + id: sd.id, + name: sd.projectName, + })) + + return NextResponse.json({ serviceDesks }) + } catch (error) { + logger.error('Error listing JSM service desks:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/microsoft_planner/plans/route.ts b/apps/sim/app/api/tools/microsoft_planner/plans/route.ts new file mode 100644 index 00000000000..632483c55da --- /dev/null +++ b/apps/sim/app/api/tools/microsoft_planner/plans/route.ts @@ -0,0 +1,102 @@ +import { randomUUID } from 'crypto' +import { db } from '@sim/db' +import { account } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('MicrosoftPlannerPlansAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const session = await getSession() + + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthenticated request rejected`) + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const credentialId = searchParams.get('credentialId') + + if (!credentialId) { + logger.error(`[${requestId}] Missing credentialId parameter`) + return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) + } + + const resolved = await resolveOAuthAccountId(credentialId) + if (!resolved) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + if (resolved.workspaceId) { + const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils') + const perm = await getUserEntityPermissions( + session.user.id, + 'workspace', + resolved.workspaceId + ) + if (perm === null) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + } + + const credentials = await db + .select() + .from(account) + .where(eq(account.id, resolved.accountId)) + .limit(1) + + if (!credentials.length) { + logger.warn(`[${requestId}] Credential not found`, { credentialId }) + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + const accountRow = credentials[0] + + const accessToken = await refreshAccessTokenIfNeeded( + resolved.accountId, + accountRow.userId, + requestId + ) + + if (!accessToken) { + logger.error(`[${requestId}] Failed to obtain valid access token`) + return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + } + + const response = await fetch('https://graph.microsoft.com/v1.0/me/planner/plans', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error(`[${requestId}] Microsoft Graph API error:`, errorText) + return NextResponse.json( + { error: 'Failed to fetch plans from Microsoft Graph' }, + { status: response.status } + ) + } + + const data = await response.json() + const plans = data.value || [] + + const filteredPlans = plans.map((plan: { id: string; title: string }) => ({ + id: plan.id, + title: plan.title, + })) + + return NextResponse.json({ plans: filteredPlans }) + } catch (error) { + logger.error(`[${requestId}] Error fetching Microsoft Planner plans:`, error) + return NextResponse.json({ error: 'Failed to fetch plans' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/notion/databases/route.ts b/apps/sim/app/api/tools/notion/databases/route.ts new file mode 100644 index 00000000000..8c9104478cb --- /dev/null +++ b/apps/sim/app/api/tools/notion/databases/route.ts @@ -0,0 +1,86 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { extractTitleFromItem } from '@/tools/notion/utils' + +const logger = createLogger('NotionDatabasesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.notion.com/v1/search', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + }, + body: JSON.stringify({ + filter: { value: 'database', property: 'object' }, + page_size: 100, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Notion databases', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Notion databases', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const databases = (data.results || []).map((db: Record) => ({ + id: db.id as string, + name: extractTitleFromItem(db), + })) + + return NextResponse.json({ databases }) + } catch (error) { + logger.error('Error processing Notion databases request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Notion databases', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/notion/pages/route.ts b/apps/sim/app/api/tools/notion/pages/route.ts new file mode 100644 index 00000000000..2a9cf9e176f --- /dev/null +++ b/apps/sim/app/api/tools/notion/pages/route.ts @@ -0,0 +1,86 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { extractTitleFromItem } from '@/tools/notion/utils' + +const logger = createLogger('NotionPagesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.notion.com/v1/search', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'Notion-Version': '2022-06-28', + }, + body: JSON.stringify({ + filter: { value: 'page', property: 'object' }, + page_size: 100, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Notion pages', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Notion pages', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const pages = (data.results || []).map((page: Record) => ({ + id: page.id as string, + name: extractTitleFromItem(page), + })) + + return NextResponse.json({ pages }) + } catch (error) { + logger.error('Error processing Notion pages request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Notion pages', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/pipedrive/pipelines/route.ts b/apps/sim/app/api/tools/pipedrive/pipelines/route.ts new file mode 100644 index 00000000000..2386b06e03b --- /dev/null +++ b/apps/sim/app/api/tools/pipedrive/pipelines/route.ts @@ -0,0 +1,79 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('PipedrivePipelinesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.pipedrive.com/v1/pipelines', { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Pipedrive pipelines', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Pipedrive pipelines', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const pipelines = (data.data || []).map((pipeline: { id: number; name: string }) => ({ + id: pipeline.id, + name: pipeline.name, + })) + + return NextResponse.json({ pipelines }) + } catch (error) { + logger.error('Error processing Pipedrive pipelines request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Pipedrive pipelines', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/sharepoint/lists/route.ts b/apps/sim/app/api/tools/sharepoint/lists/route.ts new file mode 100644 index 00000000000..68399c72cd2 --- /dev/null +++ b/apps/sim/app/api/tools/sharepoint/lists/route.ts @@ -0,0 +1,122 @@ +import { randomUUID } from 'crypto' +import { db } from '@sim/db' +import { account } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SharePointListsAPI') + +interface SharePointList { + id: string + displayName: string + description?: string + webUrl?: string + list?: { + hidden?: boolean + } +} + +/** + * Get SharePoint lists for a given site from Microsoft Graph API + */ +export async function GET(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const credentialId = searchParams.get('credentialId') + const siteId = searchParams.get('siteId') + + if (!credentialId) { + return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) + } + + if (!siteId) { + return NextResponse.json({ error: 'Site ID is required' }, { status: 400 }) + } + + const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255) + if (!credentialIdValidation.isValid) { + logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error }) + return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 }) + } + + const resolved = await resolveOAuthAccountId(credentialId) + if (!resolved) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + if (resolved.workspaceId) { + const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils') + const perm = await getUserEntityPermissions( + session.user.id, + 'workspace', + resolved.workspaceId + ) + if (perm === null) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + } + + const credentials = await db + .select() + .from(account) + .where(eq(account.id, resolved.accountId)) + .limit(1) + if (!credentials.length) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + + const accountRow = credentials[0] + + const accessToken = await refreshAccessTokenIfNeeded( + resolved.accountId, + accountRow.userId, + requestId + ) + if (!accessToken) { + return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + } + + const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100` + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + return NextResponse.json( + { error: errorData.error?.message || 'Failed to fetch lists from SharePoint' }, + { status: response.status } + ) + } + + const data = await response.json() + const lists = (data.value || []) + .filter((list: SharePointList) => list.list?.hidden !== true) + .map((list: SharePointList) => ({ + id: list.id, + displayName: list.displayName, + })) + + logger.info(`[${requestId}] Successfully fetched ${lists.length} SharePoint lists`) + return NextResponse.json({ lists }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error fetching lists from SharePoint`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/trello/boards/route.ts b/apps/sim/app/api/tools/trello/boards/route.ts new file mode 100644 index 00000000000..fa251ac9e05 --- /dev/null +++ b/apps/sim/app/api/tools/trello/boards/route.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('TrelloBoardsAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + const apiKey = process.env.TRELLO_API_KEY + if (!apiKey) { + logger.error('Trello API key not configured') + return NextResponse.json({ error: 'Trello API key not configured' }, { status: 500 }) + } + + 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 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.trello.com/1/members/me/boards?key=${apiKey}&token=${accessToken}&fields=id,name,closed`, + { + headers: { + Accept: 'application/json', + }, + } + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Trello boards', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Trello boards', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const boards = (data || []) + .filter((board: { id: string; name: string; closed: boolean }) => !board.closed) + .map((board: { id: string; name: string }) => ({ + id: board.id, + name: board.name, + })) + + return NextResponse.json({ boards }) + } catch (error) { + logger.error('Error processing Trello boards request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Trello boards', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/zoom/meetings/route.ts b/apps/sim/app/api/tools/zoom/meetings/route.ts new file mode 100644 index 00000000000..3a947acd088 --- /dev/null +++ b/apps/sim/app/api/tools/zoom/meetings/route.ts @@ -0,0 +1,82 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('ZoomMeetingsAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + 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 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.zoom.us/v2/users/me/meetings?page_size=300&type=scheduled', + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Zoom meetings', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Zoom meetings', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const meetings = (data.meetings || []).map((meeting: { id: number; topic: string }) => ({ + id: meeting.id, + name: meeting.topic, + })) + + return NextResponse.json({ meetings }) + } catch (error) { + logger.error('Error processing Zoom meetings request:', error) + return NextResponse.json( + { error: 'Failed to retrieve Zoom meetings', details: (error as Error).message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts index ff6d5e43b6d..6401273d142 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts @@ -78,4 +78,7 @@ const CONTEXT_FIELD_SET: Record = { collectionId: true, spreadsheetId: true, fileId: true, + baseId: true, + datasetId: true, + serviceDeskId: true, } diff --git a/apps/sim/blocks/blocks.test.ts b/apps/sim/blocks/blocks.test.ts index dfc80540915..9f668aab81e 100644 --- a/apps/sim/blocks/blocks.test.ts +++ b/apps/sim/blocks/blocks.test.ts @@ -667,15 +667,18 @@ describe.concurrent('Blocks Module', () => { const errors: string[] = [] for (const block of blocks) { - const allSubBlockIds = new Set(block.subBlocks.map((sb) => sb.id)) + // Exclude trigger-mode subBlocks — they operate in a separate rendering context + // and their IDs don't participate in canonical param resolution + const nonTriggerSubBlocks = block.subBlocks.filter((sb) => sb.mode !== 'trigger') + const allSubBlockIds = new Set(nonTriggerSubBlocks.map((sb) => sb.id)) const canonicalParamIds = new Set( - block.subBlocks.filter((sb) => sb.canonicalParamId).map((sb) => sb.canonicalParamId) + nonTriggerSubBlocks.filter((sb) => sb.canonicalParamId).map((sb) => sb.canonicalParamId) ) for (const canonicalId of canonicalParamIds) { if (allSubBlockIds.has(canonicalId!)) { // Check if the matching subBlock also has a canonicalParamId pointing to itself - const matchingSubBlock = block.subBlocks.find( + const matchingSubBlock = nonTriggerSubBlocks.find( (sb) => sb.id === canonicalId && !sb.canonicalParamId ) if (matchingSubBlock) { @@ -857,6 +860,10 @@ describe.concurrent('Blocks Module', () => { if (typeof subBlock.condition === 'function') { continue } + // Skip trigger-mode subBlocks — they operate in a separate rendering context + if (subBlock.mode === 'trigger') { + continue + } const conditionKey = serializeCondition(subBlock.condition) if (!canonicalByCondition.has(subBlock.canonicalParamId)) { canonicalByCondition.set(subBlock.canonicalParamId, new Set()) diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index 63fc14b33dd..f2b38efb309 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -57,21 +57,51 @@ export const AirtableBlock: BlockConfig = { placeholder: 'Enter credential ID', required: true, }, + { + id: 'baseSelector', + title: 'Base', + type: 'project-selector', + canonicalParamId: 'baseId', + serviceId: 'airtable', + selectorKey: 'airtable.bases', + selectorAllowSearch: false, + placeholder: 'Select Airtable base', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'listBases', not: true }, + required: { field: 'operation', value: 'listBases', not: true }, + }, { id: 'baseId', title: 'Base ID', type: 'short-input', + canonicalParamId: 'baseId', placeholder: 'Enter your base ID (e.g., appXXXXXXXXXXXXXX)', - dependsOn: ['credential'], + mode: 'advanced', condition: { field: 'operation', value: 'listBases', not: true }, required: { field: 'operation', value: 'listBases', not: true }, }, + { + id: 'tableSelector', + title: 'Table', + type: 'file-selector', + canonicalParamId: 'tableId', + serviceId: 'airtable', + selectorKey: 'airtable.tables', + selectorAllowSearch: false, + placeholder: 'Select Airtable table', + dependsOn: ['credential', 'baseSelector'], + mode: 'basic', + condition: { field: 'operation', value: ['listBases', 'listTables'], not: true }, + required: { field: 'operation', value: ['listBases', 'listTables'], not: true }, + }, { id: 'tableId', title: 'Table ID', type: 'short-input', + canonicalParamId: 'tableId', placeholder: 'Enter table ID (e.g., tblXXXXXXXXXXXXXX)', - dependsOn: ['credential', 'baseId'], + mode: 'advanced', condition: { field: 'operation', value: ['listBases', 'listTables'], not: true }, required: { field: 'operation', value: ['listBases', 'listTables'], not: true }, }, diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index 1276e8d572b..25418864593 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -48,12 +48,31 @@ export const AsanaBlock: BlockConfig = { placeholder: 'Enter credential ID', required: true, }, + { + id: 'workspaceSelector', + title: 'Workspace', + type: 'project-selector', + canonicalParamId: 'workspace', + serviceId: 'asana', + selectorKey: 'asana.workspaces', + selectorAllowSearch: false, + placeholder: 'Select Asana workspace', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['create_task', 'get_projects', 'search_tasks'], + }, + required: true, + }, { id: 'workspace', title: 'Workspace GID', type: 'short-input', + canonicalParamId: 'workspace', required: true, placeholder: 'Enter Asana workspace GID', + mode: 'advanced', condition: { field: 'operation', value: ['create_task', 'get_projects', 'search_tasks'], @@ -81,11 +100,29 @@ export const AsanaBlock: BlockConfig = { value: ['update_task', 'add_comment'], }, }, + { + id: 'getTasksWorkspaceSelector', + title: 'Workspace', + type: 'project-selector', + canonicalParamId: 'getTasks_workspace', + serviceId: 'asana', + selectorKey: 'asana.workspaces', + selectorAllowSearch: false, + placeholder: 'Select Asana workspace', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['get_task'], + }, + }, { id: 'getTasks_workspace', title: 'Workspace GID', type: 'short-input', + canonicalParamId: 'getTasks_workspace', placeholder: 'Enter workspace GID', + mode: 'advanced', condition: { field: 'operation', value: ['get_task'], diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 8e51dbbe713..aebea95d363 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -86,11 +86,47 @@ export const AttioBlock: BlockConfig = { }, // Record fields + { + id: 'objectTypeSelector', + title: 'Object Type', + type: 'project-selector', + canonicalParamId: 'objectType', + serviceId: 'attio', + selectorKey: 'attio.objects', + selectorAllowSearch: false, + placeholder: 'Select object type', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: [ + 'list_records', + 'get_record', + 'create_record', + 'update_record', + 'delete_record', + 'assert_record', + ], + }, + required: { + field: 'operation', + value: [ + 'list_records', + 'get_record', + 'create_record', + 'update_record', + 'delete_record', + 'assert_record', + ], + }, + }, { id: 'objectType', title: 'Object Type', type: 'short-input', + canonicalParamId: 'objectType', placeholder: 'e.g. people, companies', + mode: 'advanced', condition: { field: 'operation', value: [ @@ -524,11 +560,49 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. }, // List fields + { + id: 'listSelector', + title: 'List', + type: 'project-selector', + canonicalParamId: 'listIdOrSlug', + serviceId: 'attio', + selectorKey: 'attio.lists', + selectorAllowSearch: false, + placeholder: 'Select Attio list', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: [ + 'get_list', + 'update_list', + 'query_list_entries', + 'get_list_entry', + 'create_list_entry', + 'update_list_entry', + 'delete_list_entry', + ], + }, + required: { + field: 'operation', + value: [ + 'get_list', + 'update_list', + 'query_list_entries', + 'get_list_entry', + 'create_list_entry', + 'update_list_entry', + 'delete_list_entry', + ], + }, + }, { id: 'listIdOrSlug', title: 'List ID or Slug', type: 'short-input', + canonicalParamId: 'listIdOrSlug', placeholder: 'Enter the list ID or slug', + mode: 'advanced', condition: { field: 'operation', value: [ diff --git a/apps/sim/blocks/blocks/calcom.ts b/apps/sim/blocks/blocks/calcom.ts index a294c40b634..b26280d73e4 100644 --- a/apps/sim/blocks/blocks/calcom.ts +++ b/apps/sim/blocks/blocks/calcom.ts @@ -65,11 +65,30 @@ export const CalComBlock: BlockConfig = { }, // === Create Booking fields === + { + id: 'eventTypeSelector', + title: 'Event Type', + type: 'project-selector', + canonicalParamId: 'eventTypeId', + serviceId: 'calcom', + selectorKey: 'calcom.eventTypes', + selectorAllowSearch: false, + placeholder: 'Select event type', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['calcom_create_booking', 'calcom_get_slots'], + }, + required: { field: 'operation', value: 'calcom_create_booking' }, + }, { id: 'eventTypeId', title: 'Event Type ID', type: 'short-input', + canonicalParamId: 'eventTypeId', placeholder: 'Enter event type ID (number)', + mode: 'advanced', condition: { field: 'operation', value: ['calcom_create_booking', 'calcom_get_slots'], @@ -261,11 +280,33 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, // === Event Type fields === + { + id: 'eventTypeParamSelector', + title: 'Event Type', + type: 'project-selector', + canonicalParamId: 'eventTypeIdParam', + serviceId: 'calcom', + selectorKey: 'calcom.eventTypes', + selectorAllowSearch: false, + placeholder: 'Select event type', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'], + }, + required: { + field: 'operation', + value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'], + }, + }, { id: 'eventTypeIdParam', title: 'Event Type ID', type: 'short-input', + canonicalParamId: 'eventTypeIdParam', placeholder: 'Enter event type ID', + mode: 'advanced', condition: { field: 'operation', value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'], @@ -364,10 +405,27 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, mode: 'advanced', }, + { + id: 'eventTypeScheduleSelector', + title: 'Schedule', + type: 'project-selector', + canonicalParamId: 'eventTypeScheduleId', + serviceId: 'calcom', + selectorKey: 'calcom.schedules', + selectorAllowSearch: false, + placeholder: 'Select schedule', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['calcom_create_event_type', 'calcom_update_event_type'], + }, + }, { id: 'eventTypeScheduleId', title: 'Schedule ID', type: 'short-input', + canonicalParamId: 'eventTypeScheduleId', placeholder: 'Assign to a specific schedule', condition: { field: 'operation', @@ -388,11 +446,33 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, // === Schedule fields === + { + id: 'scheduleSelector', + title: 'Schedule', + type: 'project-selector', + canonicalParamId: 'scheduleId', + serviceId: 'calcom', + selectorKey: 'calcom.schedules', + selectorAllowSearch: false, + placeholder: 'Select schedule', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'], + }, + required: { + field: 'operation', + value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'], + }, + }, { id: 'scheduleId', title: 'Schedule ID', type: 'short-input', + canonicalParamId: 'scheduleId', placeholder: 'Enter schedule ID', + mode: 'advanced', condition: { field: 'operation', value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'], diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 180477fd044..84574fdf017 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -143,11 +143,26 @@ export const ConfluenceBlock: BlockConfig = { ], }, }, + { + id: 'spaceSelector', + title: 'Space', + type: 'project-selector', + canonicalParamId: 'spaceId', + serviceId: 'confluence', + selectorKey: 'confluence.spaces', + selectorAllowSearch: false, + placeholder: 'Select Confluence space', + dependsOn: ['credential', 'domain'], + mode: 'basic', + required: { field: 'operation', value: ['create', 'get_space'] }, + }, { id: 'spaceId', title: 'Space ID', type: 'short-input', + canonicalParamId: 'spaceId', placeholder: 'Enter Confluence space ID', + mode: 'advanced', required: { field: 'operation', value: ['create', 'get_space'] }, }, { diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 0ba15dfe565..1fdece82317 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -109,20 +109,52 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, condition: { field: 'operation', value: 'query' }, }, + { + id: 'datasetSelector', + title: 'Dataset', + type: 'project-selector', + canonicalParamId: 'datasetId', + serviceId: 'google-bigquery', + selectorKey: 'bigquery.datasets', + selectorAllowSearch: false, + placeholder: 'Select BigQuery dataset', + dependsOn: ['credential', 'projectId'], + mode: 'basic', + condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, + required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, + }, { id: 'datasetId', title: 'Dataset ID', type: 'short-input', + canonicalParamId: 'datasetId', placeholder: 'Enter BigQuery dataset ID', + mode: 'advanced', condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, }, + { + id: 'tableSelector', + title: 'Table', + type: 'file-selector', + canonicalParamId: 'tableId', + serviceId: 'google-bigquery', + selectorKey: 'bigquery.tables', + selectorAllowSearch: false, + placeholder: 'Select BigQuery table', + dependsOn: ['credential', 'projectId', 'datasetSelector'], + mode: 'basic', + condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, + required: { field: 'operation', value: ['get_table', 'insert_rows'] }, + }, { id: 'tableId', title: 'Table ID', type: 'short-input', + canonicalParamId: 'tableId', placeholder: 'Enter BigQuery table ID', + mode: 'advanced', condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, required: { field: 'operation', value: ['get_table', 'insert_rows'] }, }, diff --git a/apps/sim/blocks/blocks/google_tasks.ts b/apps/sim/blocks/blocks/google_tasks.ts index 850f824d509..06b4b4ea4f6 100644 --- a/apps/sim/blocks/blocks/google_tasks.ts +++ b/apps/sim/blocks/blocks/google_tasks.ts @@ -51,12 +51,27 @@ export const GoogleTasksBlock: BlockConfig = { required: true, }, - // Task List ID - shown for all task operations (not list_task_lists) + // Task List - shown for all task operations (not list_task_lists) + { + id: 'taskListSelector', + title: 'Task List', + type: 'project-selector', + canonicalParamId: 'taskListId', + serviceId: 'google-tasks', + selectorKey: 'google.tasks.lists', + selectorAllowSearch: false, + placeholder: 'Select task list', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'list_task_lists', not: true }, + }, { id: 'taskListId', title: 'Task List ID', type: 'short-input', + canonicalParamId: 'taskListId', placeholder: 'Task list ID (leave empty for default list)', + mode: 'advanced', condition: { field: 'operation', value: 'list_task_lists', not: true }, }, diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index e1a1ae2da2d..916f0b2bd1e 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -106,11 +106,52 @@ export const JiraServiceManagementBlock: BlockConfig = { placeholder: 'Enter credential ID', required: true, }, + { + id: 'serviceDeskSelector', + title: 'Service Desk', + type: 'project-selector', + canonicalParamId: 'serviceDeskId', + serviceId: 'jira', + selectorKey: 'jsm.serviceDesks', + selectorAllowSearch: false, + placeholder: 'Select service desk', + dependsOn: ['credential', 'domain'], + mode: 'basic', + required: { + field: 'operation', + value: [ + 'get_request_types', + 'create_request', + 'get_customers', + 'add_customer', + 'get_organizations', + 'add_organization', + 'get_queues', + 'get_request_type_fields', + ], + }, + condition: { + field: 'operation', + value: [ + 'get_request_types', + 'create_request', + 'get_customers', + 'add_customer', + 'get_organizations', + 'add_organization', + 'get_queues', + 'get_requests', + 'get_request_type_fields', + ], + }, + }, { id: 'serviceDeskId', title: 'Service Desk ID', type: 'short-input', + canonicalParamId: 'serviceDeskId', placeholder: 'Enter service desk ID', + mode: 'advanced', required: { field: 'operation', value: [ @@ -139,12 +180,28 @@ export const JiraServiceManagementBlock: BlockConfig = { ], }, }, + { + id: 'requestTypeSelector', + title: 'Request Type', + type: 'file-selector', + canonicalParamId: 'requestTypeId', + serviceId: 'jira', + selectorKey: 'jsm.requestTypes', + selectorAllowSearch: false, + placeholder: 'Select request type', + dependsOn: ['credential', 'domain', 'serviceDeskSelector'], + mode: 'basic', + required: true, + condition: { field: 'operation', value: ['create_request', 'get_request_type_fields'] }, + }, { id: 'requestTypeId', title: 'Request Type ID', type: 'short-input', + canonicalParamId: 'requestTypeId', required: true, placeholder: 'Enter request type ID', + mode: 'advanced', condition: { field: 'operation', value: ['create_request', 'get_request_type_fields'] }, }, { diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index a73d01b48d3..ab90c179236 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -84,12 +84,36 @@ export const MicrosoftPlannerBlock: BlockConfig = { placeholder: 'Enter credential ID', }, - // Plan ID - for various operations + // Plan selector - basic mode + { + id: 'planSelector', + title: 'Plan', + type: 'project-selector', + canonicalParamId: 'planId', + serviceId: 'microsoft-planner', + selectorKey: 'microsoft.planner.plans', + selectorAllowSearch: false, + placeholder: 'Select a plan', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'], + }, + required: { + field: 'operation', + value: ['read_plan', 'list_buckets', 'create_bucket', 'create_task'], + }, + }, + + // Plan ID - advanced mode { id: 'planId', title: 'Plan ID', type: 'short-input', + canonicalParamId: 'planId', placeholder: 'Enter the plan ID', + mode: 'advanced', condition: { field: 'operation', value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'], @@ -110,7 +134,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { serviceId: 'microsoft-planner', selectorKey: 'microsoft.planner', condition: { field: 'operation', value: ['read_task'] }, - dependsOn: ['credential', 'planId'], + dependsOn: ['credential', 'planSelector'], mode: 'basic', canonicalParamId: 'readTaskId', }, diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 92727444d94..2b7c563c655 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -53,48 +53,104 @@ export const NotionBlock: BlockConfig = { placeholder: 'Enter credential ID', required: true, }, - // Read/Write operation - Page ID + // Page selector for Read Page and Append Content + { + id: 'pageSelector', + title: 'Page', + type: 'file-selector', + canonicalParamId: 'pageId', + serviceId: 'notion', + selectorKey: 'notion.pages', + selectorAllowSearch: true, + placeholder: 'Select Notion page', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: ['notion_read', 'notion_write'] }, + required: { field: 'operation', value: ['notion_read', 'notion_write'] }, + }, { id: 'pageId', title: 'Page ID', type: 'short-input', + canonicalParamId: 'pageId', placeholder: 'Enter Notion page ID', + mode: 'advanced', + condition: { field: 'operation', value: ['notion_read', 'notion_write'] }, + required: { field: 'operation', value: ['notion_read', 'notion_write'] }, + }, + // Database selector (consolidated across read_database, query_database, add_database_row) + { + id: 'databaseSelector', + title: 'Database', + type: 'project-selector', + canonicalParamId: 'databaseId', + serviceId: 'notion', + selectorKey: 'notion.databases', + selectorAllowSearch: true, + placeholder: 'Select Notion database', + dependsOn: ['credential'], + mode: 'basic', condition: { field: 'operation', - value: 'notion_read', + value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], + }, + required: { + field: 'operation', + value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], }, - required: true, }, { id: 'databaseId', title: 'Database ID', type: 'short-input', + canonicalParamId: 'databaseId', placeholder: 'Enter Notion database ID', + mode: 'advanced', condition: { field: 'operation', - value: 'notion_read_database', + value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], + }, + required: { + field: 'operation', + value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], }, - required: true, }, + // Parent page selector (consolidated across create_page, create_database) { - id: 'pageId', - title: 'Page ID', - type: 'short-input', - placeholder: 'Enter Notion page ID', + id: 'parentSelector', + title: 'Parent Page', + type: 'file-selector', + canonicalParamId: 'parentId', + serviceId: 'notion', + selectorKey: 'notion.pages', + selectorAllowSearch: true, + placeholder: 'Select parent page', + dependsOn: ['credential'], + mode: 'basic', condition: { field: 'operation', - value: 'notion_write', + value: ['notion_create_page', 'notion_create_database'], + }, + required: { + field: 'operation', + value: ['notion_create_page', 'notion_create_database'], }, - required: true, }, - // Create operation fields { id: 'parentId', title: 'Parent Page ID', type: 'short-input', + canonicalParamId: 'parentId', placeholder: 'ID of parent page', - condition: { field: 'operation', value: 'notion_create_page' }, - required: true, + mode: 'advanced', + condition: { + field: 'operation', + value: ['notion_create_page', 'notion_create_database'], + }, + required: { + field: 'operation', + value: ['notion_create_page', 'notion_create_database'], + }, }, { id: 'title', @@ -148,14 +204,6 @@ export const NotionBlock: BlockConfig = { }, }, // Query Database Fields - { - id: 'databaseId', - title: 'Database ID', - type: 'short-input', - placeholder: 'Enter Notion database ID', - condition: { field: 'operation', value: 'notion_query_database' }, - required: true, - }, { id: 'filter', title: 'Filter', @@ -218,14 +266,6 @@ export const NotionBlock: BlockConfig = { condition: { field: 'operation', value: 'notion_search' }, }, // Create Database Fields - { - id: 'parentId', - title: 'Parent Page ID', - type: 'short-input', - placeholder: 'ID of parent page where database will be created', - condition: { field: 'operation', value: 'notion_create_database' }, - required: true, - }, { id: 'title', title: 'Database Title', @@ -256,14 +296,6 @@ export const NotionBlock: BlockConfig = { }, }, // Add Database Row Fields - { - id: 'databaseId', - title: 'Database ID', - type: 'short-input', - placeholder: 'Enter Notion database ID', - condition: { field: 'operation', value: 'notion_add_database_row' }, - required: true, - }, { id: 'properties', title: 'Row Properties', diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index 543a6d0de34..55e5c331ff4 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -96,12 +96,35 @@ export const PipedriveBlock: BlockConfig = { placeholder: 'Filter by organization ID', condition: { field: 'operation', value: ['get_all_deals'] }, }, + { + id: 'pipelineSelector', + title: 'Pipeline', + type: 'project-selector', + canonicalParamId: 'pipeline_id', + serviceId: 'pipedrive', + selectorKey: 'pipedrive.pipelines', + selectorAllowSearch: false, + placeholder: 'Select pipeline', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: ['get_all_deals', 'create_deal', 'get_pipeline_deals'], + }, + required: { field: 'operation', value: 'get_pipeline_deals' }, + }, { id: 'pipeline_id', title: 'Pipeline ID', type: 'short-input', - placeholder: 'Filter by pipeline ID ', - condition: { field: 'operation', value: ['get_all_deals'] }, + canonicalParamId: 'pipeline_id', + placeholder: 'Enter pipeline ID', + mode: 'advanced', + condition: { + field: 'operation', + value: ['get_all_deals', 'create_deal', 'get_pipeline_deals'], + }, + required: { field: 'operation', value: 'get_pipeline_deals' }, }, { id: 'updated_since', @@ -174,13 +197,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, placeholder: 'Associated organization ID ', condition: { field: 'operation', value: ['create_deal'] }, }, - { - id: 'pipeline_id', - title: 'Pipeline ID', - type: 'short-input', - placeholder: 'Pipeline ID ', - condition: { field: 'operation', value: ['create_deal'] }, - }, { id: 'stage_id', title: 'Stage ID', @@ -329,14 +345,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n ], }, }, - { - id: 'pipeline_id', - title: 'Pipeline ID', - type: 'short-input', - placeholder: 'Enter pipeline ID', - required: true, - condition: { field: 'operation', value: ['get_pipeline_deals'] }, - }, { id: 'stage_id', title: 'Stage ID', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index af54dc2f75a..dcc02839d36 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -112,12 +112,26 @@ export const SharepointBlock: BlockConfig = { mode: 'advanced', }, + { + id: 'listSelector', + title: 'List', + type: 'file-selector', + canonicalParamId: 'listId', + serviceId: 'sharepoint', + selectorKey: 'sharepoint.lists', + selectorAllowSearch: false, + placeholder: 'Select a list', + dependsOn: ['credential', 'siteSelector'], + mode: 'basic', + condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] }, + }, { id: 'listId', title: 'List ID', type: 'short-input', - placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.', canonicalParamId: 'listId', + placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.', + mode: 'advanced', condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] }, }, diff --git a/apps/sim/blocks/blocks/trello.ts b/apps/sim/blocks/blocks/trello.ts index 777e060fe9b..bff115ebdcf 100644 --- a/apps/sim/blocks/blocks/trello.ts +++ b/apps/sim/blocks/blocks/trello.ts @@ -59,26 +59,50 @@ export const TrelloBlock: BlockConfig = { }, { - id: 'boardId', + id: 'boardSelector', title: 'Board', - type: 'short-input', - placeholder: 'Enter board ID', + type: 'project-selector', + canonicalParamId: 'boardId', + serviceId: 'trello', + selectorKey: 'trello.boards', + selectorAllowSearch: false, + placeholder: 'Select Trello board', + dependsOn: ['credential'], + mode: 'basic', condition: { field: 'operation', - value: 'trello_list_lists', + value: [ + 'trello_list_lists', + 'trello_list_cards', + 'trello_create_card', + 'trello_get_actions', + ], + }, + required: { + field: 'operation', + value: ['trello_list_lists', 'trello_list_cards', 'trello_create_card'], }, - required: true, }, { id: 'boardId', - title: 'Board', + title: 'Board ID', type: 'short-input', - placeholder: 'Enter board ID or search for a board', + canonicalParamId: 'boardId', + placeholder: 'Enter board ID', + mode: 'advanced', condition: { field: 'operation', - value: 'trello_list_cards', + value: [ + 'trello_list_lists', + 'trello_list_cards', + 'trello_create_card', + 'trello_get_actions', + ], + }, + required: { + field: 'operation', + value: ['trello_list_lists', 'trello_list_cards', 'trello_create_card'], }, - required: true, }, { id: 'listId', @@ -90,17 +114,6 @@ export const TrelloBlock: BlockConfig = { value: 'trello_list_cards', }, }, - { - id: 'boardId', - title: 'Board', - type: 'short-input', - placeholder: 'Enter board ID or search for a board', - condition: { - field: 'operation', - value: 'trello_create_card', - }, - required: true, - }, { id: 'listId', title: 'List', @@ -278,16 +291,6 @@ Return ONLY the date/timestamp string - no explanations, no quotes, no extra tex }, }, - { - id: 'boardId', - title: 'Board ID', - type: 'short-input', - placeholder: 'Enter board ID to get board actions', - condition: { - field: 'operation', - value: 'trello_get_actions', - }, - }, { id: 'cardId', title: 'Card ID', diff --git a/apps/sim/blocks/blocks/zoom.ts b/apps/sim/blocks/blocks/zoom.ts index 711ab3f7681..23b7dc70a84 100644 --- a/apps/sim/blocks/blocks/zoom.ts +++ b/apps/sim/blocks/blocks/zoom.ts @@ -77,12 +77,39 @@ export const ZoomBlock: BlockConfig = { value: ['zoom_create_meeting', 'zoom_list_meetings', 'zoom_list_recordings'], }, }, - // Meeting ID for get/update/delete/invitation/recordings/participants operations + // Meeting selector for get/update/delete/invitation/recordings/participants operations + { + id: 'meetingSelector', + title: 'Meeting', + type: 'project-selector', + canonicalParamId: 'meetingId', + serviceId: 'zoom', + selectorKey: 'zoom.meetings', + selectorAllowSearch: true, + placeholder: 'Select Zoom meeting', + dependsOn: ['credential'], + mode: 'basic', + required: true, + condition: { + field: 'operation', + value: [ + 'zoom_get_meeting', + 'zoom_update_meeting', + 'zoom_delete_meeting', + 'zoom_get_meeting_invitation', + 'zoom_get_meeting_recordings', + 'zoom_delete_recording', + 'zoom_list_past_participants', + ], + }, + }, { id: 'meetingId', title: 'Meeting ID', type: 'short-input', + canonicalParamId: 'meetingId', placeholder: 'Enter meeting ID', + mode: 'advanced', required: true, condition: { field: 'operation', diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 6b0674b7597..51a334f5b1d 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -10,6 +10,29 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const SELECTOR_STALE = 60 * 1000 +type AirtableBase = { id: string; name: string } +type AirtableTable = { id: string; name: string } +type AsanaWorkspace = { gid: string; name: string } +type AttioObject = { id: string; name: string } +type AttioList = { id: string; name: string } +type BigQueryDataset = { + datasetReference: { datasetId: string; projectId: string } + friendlyName?: string +} +type BigQueryTable = { tableReference: { tableId: string }; friendlyName?: string } +type CalcomEventType = { id: number; title: string; slug: string } +type ConfluenceSpace = { id: string; name: string; key: string } +type JsmServiceDesk = { id: string; name: string } +type JsmRequestType = { id: string; name: string } +type NotionDatabase = { id: string; name: string } +type NotionPage = { id: string; name: string } +type PipedrivePipeline = { id: number; name: string } +type ZoomMeeting = { id: number; name: string } +type CalcomSchedule = { id: number; name: string } +type GoogleTaskList = { id: string; title: string } +type PlannerPlan = { id: string; title: string } +type SharepointList = { id: string; displayName: string } +type TrelloBoard = { id: string; name: string } type SlackChannel = { id: string; name: string } type SlackUser = { id: string; name: string; real_name: string } type FolderResponse = { id: string; name: string } @@ -37,6 +60,746 @@ const ensureKnowledgeBase = (context: SelectorContext): string => { } const registry: Record = { + 'airtable.bases': { + key: 'airtable.bases', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'airtable.bases', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'airtable.bases') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ bases: AirtableBase[] }>('/api/tools/airtable/bases', { + method: 'POST', + body, + }) + return (data.bases || []).map((base) => ({ + id: base.id, + label: base.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'airtable.bases') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + baseId: detailId, + }) + const data = await fetchJson<{ bases: AirtableBase[] }>('/api/tools/airtable/bases', { + method: 'POST', + body, + }) + const base = (data.bases || []).find((b) => b.id === detailId) ?? null + if (!base) return null + return { id: base.id, label: base.name } + }, + }, + 'airtable.tables': { + key: 'airtable.tables', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'airtable.tables', + context.credentialId ?? 'none', + context.baseId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId && context.baseId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'airtable.tables') + if (!context.baseId) { + throw new Error('Missing base ID for airtable.tables selector') + } + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + baseId: context.baseId, + }) + const data = await fetchJson<{ tables: AirtableTable[] }>('/api/tools/airtable/tables', { + method: 'POST', + body, + }) + return (data.tables || []).map((table) => ({ + id: table.id, + label: table.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'airtable.tables') + if (!context.baseId) return null + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + baseId: context.baseId, + }) + const data = await fetchJson<{ tables: AirtableTable[] }>('/api/tools/airtable/tables', { + method: 'POST', + body, + }) + const table = (data.tables || []).find((t) => t.id === detailId) ?? null + if (!table) return null + return { id: table.id, label: table.name } + }, + }, + 'asana.workspaces': { + key: 'asana.workspaces', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'asana.workspaces', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'asana.workspaces') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ workspaces: AsanaWorkspace[] }>( + '/api/tools/asana/workspaces', + { method: 'POST', body } + ) + return (data.workspaces || []).map((ws) => ({ id: ws.gid, label: ws.name })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'asana.workspaces') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ workspaces: AsanaWorkspace[] }>( + '/api/tools/asana/workspaces', + { method: 'POST', body } + ) + const ws = (data.workspaces || []).find((w) => w.gid === detailId) ?? null + if (!ws) return null + return { id: ws.gid, label: ws.name } + }, + }, + 'attio.objects': { + key: 'attio.objects', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'attio.objects', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'attio.objects') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ objects: AttioObject[] }>('/api/tools/attio/objects', { + method: 'POST', + body, + }) + return (data.objects || []).map((obj) => ({ + id: obj.id, + label: obj.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'attio.objects') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ objects: AttioObject[] }>('/api/tools/attio/objects', { + method: 'POST', + body, + }) + const obj = (data.objects || []).find((o) => o.id === detailId) ?? null + if (!obj) return null + return { id: obj.id, label: obj.name } + }, + }, + 'attio.lists': { + key: 'attio.lists', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'attio.lists', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'attio.lists') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ lists: AttioList[] }>('/api/tools/attio/lists', { + method: 'POST', + body, + }) + return (data.lists || []).map((list) => ({ + id: list.id, + label: list.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'attio.lists') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ lists: AttioList[] }>('/api/tools/attio/lists', { + method: 'POST', + body, + }) + const list = (data.lists || []).find((l) => l.id === detailId) ?? null + if (!list) return null + return { id: list.id, label: list.name } + }, + }, + 'bigquery.datasets': { + key: 'bigquery.datasets', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'bigquery.datasets', + context.credentialId ?? 'none', + context.projectId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId && context.projectId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'bigquery.datasets') + if (!context.projectId) throw new Error('Missing project ID for bigquery.datasets selector') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + projectId: context.projectId, + }) + const data = await fetchJson<{ datasets: BigQueryDataset[] }>( + '/api/tools/google_bigquery/datasets', + { method: 'POST', body } + ) + return (data.datasets || []).map((ds) => ({ + id: ds.datasetReference.datasetId, + label: ds.friendlyName || ds.datasetReference.datasetId, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId || !context.projectId) return null + const credentialId = ensureCredential(context, 'bigquery.datasets') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + projectId: context.projectId, + }) + const data = await fetchJson<{ datasets: BigQueryDataset[] }>( + '/api/tools/google_bigquery/datasets', + { method: 'POST', body } + ) + const ds = + (data.datasets || []).find((d) => d.datasetReference.datasetId === detailId) ?? null + if (!ds) return null + return { + id: ds.datasetReference.datasetId, + label: ds.friendlyName || ds.datasetReference.datasetId, + } + }, + }, + 'bigquery.tables': { + key: 'bigquery.tables', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'bigquery.tables', + context.credentialId ?? 'none', + context.projectId ?? 'none', + context.datasetId ?? 'none', + ], + enabled: ({ context }) => + Boolean(context.credentialId && context.projectId && context.datasetId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'bigquery.tables') + if (!context.projectId) throw new Error('Missing project ID for bigquery.tables selector') + if (!context.datasetId) throw new Error('Missing dataset ID for bigquery.tables selector') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + projectId: context.projectId, + datasetId: context.datasetId, + }) + const data = await fetchJson<{ tables: BigQueryTable[] }>( + '/api/tools/google_bigquery/tables', + { method: 'POST', body } + ) + return (data.tables || []).map((t) => ({ + id: t.tableReference.tableId, + label: t.friendlyName || t.tableReference.tableId, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId || !context.projectId || !context.datasetId) return null + const credentialId = ensureCredential(context, 'bigquery.tables') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + projectId: context.projectId, + datasetId: context.datasetId, + }) + const data = await fetchJson<{ tables: BigQueryTable[] }>( + '/api/tools/google_bigquery/tables', + { method: 'POST', body } + ) + const t = (data.tables || []).find((tbl) => tbl.tableReference.tableId === detailId) ?? null + if (!t) return null + return { id: t.tableReference.tableId, label: t.friendlyName || t.tableReference.tableId } + }, + }, + 'calcom.eventTypes': { + key: 'calcom.eventTypes', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'calcom.eventTypes', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'calcom.eventTypes') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ eventTypes: CalcomEventType[] }>( + '/api/tools/calcom/event-types', + { method: 'POST', body } + ) + return (data.eventTypes || []).map((et) => ({ + id: String(et.id), + label: et.title || et.slug, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'calcom.eventTypes') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ eventTypes: CalcomEventType[] }>( + '/api/tools/calcom/event-types', + { method: 'POST', body } + ) + const et = (data.eventTypes || []).find((e) => String(e.id) === detailId) ?? null + if (!et) return null + return { id: String(et.id), label: et.title || et.slug } + }, + }, + 'calcom.schedules': { + key: 'calcom.schedules', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'calcom.schedules', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'calcom.schedules') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ schedules: CalcomSchedule[] }>('/api/tools/calcom/schedules', { + method: 'POST', + body, + }) + return (data.schedules || []).map((s) => ({ + id: String(s.id), + label: s.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'calcom.schedules') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ schedules: CalcomSchedule[] }>('/api/tools/calcom/schedules', { + method: 'POST', + body, + }) + const s = (data.schedules || []).find((sc) => String(sc.id) === detailId) ?? null + if (!s) return null + return { id: String(s.id), label: s.name } + }, + }, + 'confluence.spaces': { + key: 'confluence.spaces', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'confluence.spaces', + context.credentialId ?? 'none', + context.domain ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId && context.domain), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'confluence.spaces') + const domain = ensureDomain(context, 'confluence.spaces') + const accessToken = await fetchOAuthToken(credentialId, context.workflowId) + if (!accessToken) throw new Error('Missing Confluence access token') + const data = await fetchJson<{ spaces: ConfluenceSpace[] }>('/api/tools/confluence/spaces', { + searchParams: { domain, accessToken, limit: '250' }, + }) + return (data.spaces || []).map((space) => ({ + id: space.id, + label: `${space.name} (${space.key})`, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'confluence.spaces') + const domain = ensureDomain(context, 'confluence.spaces') + const accessToken = await fetchOAuthToken(credentialId, context.workflowId) + if (!accessToken) throw new Error('Missing Confluence access token') + const data = await fetchJson<{ spaces: ConfluenceSpace[] }>('/api/tools/confluence/spaces', { + searchParams: { domain, accessToken, limit: '250' }, + }) + const space = (data.spaces || []).find((s) => s.id === detailId) ?? null + if (!space) return null + return { id: space.id, label: `${space.name} (${space.key})` } + }, + }, + 'jsm.serviceDesks': { + key: 'jsm.serviceDesks', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'jsm.serviceDesks', + context.credentialId ?? 'none', + context.domain ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId && context.domain), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'jsm.serviceDesks') + const domain = ensureDomain(context, 'jsm.serviceDesks') + const accessToken = await fetchOAuthToken(credentialId, context.workflowId) + if (!accessToken) throw new Error('Missing JSM access token') + const data = await fetchJson<{ serviceDesks: JsmServiceDesk[] }>( + '/api/tools/jsm/selector-servicedesks', + { + searchParams: { domain, accessToken }, + } + ) + return (data.serviceDesks || []).map((sd) => ({ + id: sd.id, + label: sd.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'jsm.serviceDesks') + const domain = ensureDomain(context, 'jsm.serviceDesks') + const accessToken = await fetchOAuthToken(credentialId, context.workflowId) + if (!accessToken) throw new Error('Missing JSM access token') + const data = await fetchJson<{ serviceDesks: JsmServiceDesk[] }>( + '/api/tools/jsm/selector-servicedesks', + { + searchParams: { domain, accessToken }, + } + ) + const sd = (data.serviceDesks || []).find((s) => s.id === detailId) ?? null + if (!sd) return null + return { id: sd.id, label: sd.name } + }, + }, + 'jsm.requestTypes': { + key: 'jsm.requestTypes', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'jsm.requestTypes', + context.credentialId ?? 'none', + context.domain ?? 'none', + context.serviceDeskId ?? 'none', + ], + enabled: ({ context }) => + Boolean(context.credentialId && context.domain && context.serviceDeskId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'jsm.requestTypes') + const domain = ensureDomain(context, 'jsm.requestTypes') + if (!context.serviceDeskId) throw new Error('Missing serviceDeskId for jsm.requestTypes') + const accessToken = await fetchOAuthToken(credentialId, context.workflowId) + if (!accessToken) throw new Error('Missing JSM access token') + const data = await fetchJson<{ requestTypes: JsmRequestType[] }>( + '/api/tools/jsm/selector-requesttypes', + { + searchParams: { + domain, + accessToken, + serviceDeskId: context.serviceDeskId, + }, + } + ) + return (data.requestTypes || []).map((rt) => ({ + id: rt.id, + label: rt.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'jsm.requestTypes') + const domain = ensureDomain(context, 'jsm.requestTypes') + if (!context.serviceDeskId) return null + const accessToken = await fetchOAuthToken(credentialId, context.workflowId) + if (!accessToken) throw new Error('Missing JSM access token') + const data = await fetchJson<{ requestTypes: JsmRequestType[] }>( + '/api/tools/jsm/selector-requesttypes', + { + searchParams: { + domain, + accessToken, + serviceDeskId: context.serviceDeskId, + }, + } + ) + const rt = (data.requestTypes || []).find((r) => r.id === detailId) ?? null + if (!rt) return null + return { id: rt.id, label: rt.name } + }, + }, + 'google.tasks.lists': { + key: 'google.tasks.lists', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'google.tasks.lists', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'google.tasks.lists') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ taskLists: GoogleTaskList[] }>( + '/api/tools/google_tasks/task-lists', + { method: 'POST', body } + ) + return (data.taskLists || []).map((tl) => ({ id: tl.id, label: tl.title })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'google.tasks.lists') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ taskLists: GoogleTaskList[] }>( + '/api/tools/google_tasks/task-lists', + { method: 'POST', body } + ) + const tl = (data.taskLists || []).find((t) => t.id === detailId) ?? null + if (!tl) return null + return { id: tl.id, label: tl.title } + }, + }, + 'microsoft.planner.plans': { + key: 'microsoft.planner.plans', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'microsoft.planner.plans', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const data = await fetchJson<{ plans: PlannerPlan[] }>('/api/tools/microsoft_planner/plans', { + searchParams: { credentialId: context.credentialId }, + }) + return (data.plans || []).map((plan) => ({ id: plan.id, label: plan.title })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const data = await fetchJson<{ plans: PlannerPlan[] }>('/api/tools/microsoft_planner/plans', { + searchParams: { credentialId: context.credentialId }, + }) + const plan = (data.plans || []).find((p) => p.id === detailId) ?? null + if (!plan) return null + return { id: plan.id, label: plan.title } + }, + }, + 'notion.databases': { + key: 'notion.databases', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'notion.databases', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'notion.databases') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ databases: NotionDatabase[] }>('/api/tools/notion/databases', { + method: 'POST', + body, + }) + return (data.databases || []).map((db) => ({ + id: db.id, + label: db.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'notion.databases') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ databases: NotionDatabase[] }>('/api/tools/notion/databases', { + method: 'POST', + body, + }) + const db = (data.databases || []).find((d) => d.id === detailId) ?? null + if (!db) return null + return { id: db.id, label: db.name } + }, + }, + 'notion.pages': { + key: 'notion.pages', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'notion.pages', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'notion.pages') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ pages: NotionPage[] }>('/api/tools/notion/pages', { + method: 'POST', + body, + }) + return (data.pages || []).map((page) => ({ + id: page.id, + label: page.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'notion.pages') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ pages: NotionPage[] }>('/api/tools/notion/pages', { + method: 'POST', + body, + }) + const page = (data.pages || []).find((p) => p.id === detailId) ?? null + if (!page) return null + return { id: page.id, label: page.name } + }, + }, + 'pipedrive.pipelines': { + key: 'pipedrive.pipelines', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'pipedrive.pipelines', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'pipedrive.pipelines') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ pipelines: PipedrivePipeline[] }>( + '/api/tools/pipedrive/pipelines', + { method: 'POST', body } + ) + return (data.pipelines || []).map((p) => ({ + id: String(p.id), + label: p.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'pipedrive.pipelines') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ pipelines: PipedrivePipeline[] }>( + '/api/tools/pipedrive/pipelines', + { method: 'POST', body } + ) + const p = (data.pipelines || []).find((pl) => String(pl.id) === detailId) ?? null + if (!p) return null + return { id: String(p.id), label: p.name } + }, + }, + 'sharepoint.lists': { + key: 'sharepoint.lists', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'sharepoint.lists', + context.credentialId ?? 'none', + context.siteId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId && context.siteId), + fetchList: async ({ context }: SelectorQueryArgs) => { + if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector') + const data = await fetchJson<{ lists: SharepointList[] }>('/api/tools/sharepoint/lists', { + searchParams: { + credentialId: context.credentialId, + siteId: context.siteId, + }, + }) + return (data.lists || []).map((list) => ({ id: list.id, label: list.displayName })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId || !context.siteId) return null + const data = await fetchJson<{ lists: SharepointList[] }>('/api/tools/sharepoint/lists', { + searchParams: { + credentialId: context.credentialId, + siteId: context.siteId, + }, + }) + const list = (data.lists || []).find((l) => l.id === detailId) ?? null + if (!list) return null + return { id: list.id, label: list.displayName } + }, + }, + 'trello.boards': { + key: 'trello.boards', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'trello.boards', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'trello.boards') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ boards: TrelloBoard[] }>('/api/tools/trello/boards', { + method: 'POST', + body, + }) + return (data.boards || []).map((board) => ({ id: board.id, label: board.name })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'trello.boards') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ boards: TrelloBoard[] }>('/api/tools/trello/boards', { + method: 'POST', + body, + }) + const board = (data.boards || []).find((b) => b.id === detailId) ?? null + if (!board) return null + return { id: board.id, label: board.name } + }, + }, + 'zoom.meetings': { + key: 'zoom.meetings', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'zoom.meetings', + context.credentialId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.credentialId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'zoom.meetings') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ meetings: ZoomMeeting[] }>('/api/tools/zoom/meetings', { + method: 'POST', + body, + }) + return (data.meetings || []).map((m) => ({ + id: String(m.id), + label: m.name || `Meeting ${m.id}`, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'zoom.meetings') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) + const data = await fetchJson<{ meetings: ZoomMeeting[] }>('/api/tools/zoom/meetings', { + method: 'POST', + body, + }) + const meeting = (data.meetings || []).find((m) => String(m.id) === detailId) ?? null + if (!meeting) return null + return { id: String(meeting.id), label: meeting.name || `Meeting ${meeting.id}` } + }, + }, 'slack.channels': { key: 'slack.channels', staleTime: SELECTOR_STALE, diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index b884a471911..8f8beee32e3 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -2,6 +2,26 @@ import type React from 'react' import type { QueryKey } from '@tanstack/react-query' export type SelectorKey = + | 'airtable.bases' + | 'airtable.tables' + | 'asana.workspaces' + | 'attio.lists' + | 'attio.objects' + | 'bigquery.datasets' + | 'bigquery.tables' + | 'calcom.eventTypes' + | 'calcom.schedules' + | 'confluence.spaces' + | 'google.tasks.lists' + | 'jsm.requestTypes' + | 'jsm.serviceDesks' + | 'microsoft.planner.plans' + | 'notion.databases' + | 'notion.pages' + | 'pipedrive.pipelines' + | 'sharepoint.lists' + | 'trello.boards' + | 'zoom.meetings' | 'slack.channels' | 'slack.users' | 'gmail.labels' @@ -54,6 +74,9 @@ export interface SelectorContext { collectionId?: string spreadsheetId?: string excludeWorkflowId?: string + baseId?: string + datasetId?: string + serviceDeskId?: string } export interface SelectorQueryArgs { From c644a23d8a04ae16c0047cc2d767bcda619039f7 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 17:41:31 -0800 Subject: [PATCH 02/32] fix(selectors): secure OAuth tokens in JSM and Confluence selector routes Convert JSM selector-servicedesks, selector-requesttypes, and Confluence selector-spaces routes from GET (with access token in URL query params) to POST with authorizeCredentialUse + refreshAccessTokenIfNeeded pattern. Also adds missing ensureCredential guard to microsoft.planner.plans registry entry. --- .../tools/confluence/selector-spaces/route.ts | 98 +++++++++++++++++++ .../tools/jsm/selector-requesttypes/route.ts | 51 +++++++--- .../tools/jsm/selector-servicedesks/route.ts | 44 ++++++--- apps/sim/hooks/selectors/registry.ts | 84 +++++++++------- 4 files changed, 213 insertions(+), 64 deletions(-) create mode 100644 apps/sim/app/api/tools/confluence/selector-spaces/route.ts diff --git a/apps/sim/app/api/tools/confluence/selector-spaces/route.ts b/apps/sim/app/api/tools/confluence/selector-spaces/route.ts new file mode 100644 index 00000000000..aa3bca1559f --- /dev/null +++ b/apps/sim/app/api/tools/confluence/selector-spaces/route.ts @@ -0,0 +1,98 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateJiraCloudId } from '@/lib/core/security/input-validation' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSelectorSpacesAPI') + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + const requestId = generateRequestId() + const body = await request.json() + const { credential, workflowId, domain } = body + + if (!credential) { + logger.error('Missing credential in request') + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } + + if (!domain) { + return NextResponse.json({ error: 'Domain 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 cloudId = await getConfluenceCloudId(domain, accessToken) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?limit=250` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error:', { + status: response.status, + statusText: response.statusText, + error: errorData, + }) + const errorMessage = + errorData?.message || `Failed to list Confluence spaces (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + const spaces = (data.results || []).map( + (space: { id: string; name: string; key: string }) => ({ + id: space.id, + name: space.name, + key: space.key, + }) + ) + + return NextResponse.json({ spaces }) + } catch (error) { + logger.error('Error listing Confluence spaces:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts index 297eacbc7f9..ed34d5d7bdf 100644 --- a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts @@ -1,33 +1,30 @@ import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' const logger = createLogger('JsmSelectorRequestTypesAPI') export const dynamic = 'force-dynamic' -export async function GET(request: NextRequest) { +export async function POST(request: Request) { try { - const auth = await checkSessionOrInternalAuth(request) - if (!auth.success || !auth.userId) { - return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) - } + const requestId = generateRequestId() + const body = await request.json() + const { credential, workflowId, domain, serviceDeskId } = body - const { searchParams } = new URL(request.url) - const domain = searchParams.get('domain') - const accessToken = searchParams.get('accessToken') - const serviceDeskId = searchParams.get('serviceDeskId') + if (!credential) { + logger.error('Missing credential in request') + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) } - if (!accessToken) { - return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) - } - if (!serviceDeskId) { return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 }) } @@ -37,6 +34,30 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: serviceDeskIdValidation.error }, { 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 cloudId = await getJiraCloudId(domain, accessToken) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') diff --git a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts index ce162a4d51f..ab3d02a087c 100644 --- a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts @@ -1,30 +1,52 @@ import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { validateJiraCloudId } from '@/lib/core/security/input-validation' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' const logger = createLogger('JsmSelectorServiceDesksAPI') export const dynamic = 'force-dynamic' -export async function GET(request: NextRequest) { +export async function POST(request: Request) { try { - const auth = await checkSessionOrInternalAuth(request) - if (!auth.success || !auth.userId) { - return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) - } + const requestId = generateRequestId() + const body = await request.json() + const { credential, workflowId, domain } = body - const { searchParams } = new URL(request.url) - const domain = searchParams.get('domain') - const accessToken = searchParams.get('accessToken') + if (!credential) { + logger.error('Missing credential in request') + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } if (!domain) { return NextResponse.json({ error: 'Domain 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) { - return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + 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 cloudId = await getJiraCloudId(domain, accessToken) diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 51a334f5b1d..a15d1674d8c 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -422,11 +422,15 @@ const registry: Record = { fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'confluence.spaces') const domain = ensureDomain(context, 'confluence.spaces') - const accessToken = await fetchOAuthToken(credentialId, context.workflowId) - if (!accessToken) throw new Error('Missing Confluence access token') - const data = await fetchJson<{ spaces: ConfluenceSpace[] }>('/api/tools/confluence/spaces', { - searchParams: { domain, accessToken, limit: '250' }, + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + domain, }) + const data = await fetchJson<{ spaces: ConfluenceSpace[] }>( + '/api/tools/confluence/selector-spaces', + { method: 'POST', body } + ) return (data.spaces || []).map((space) => ({ id: space.id, label: `${space.name} (${space.key})`, @@ -436,11 +440,15 @@ const registry: Record = { if (!detailId) return null const credentialId = ensureCredential(context, 'confluence.spaces') const domain = ensureDomain(context, 'confluence.spaces') - const accessToken = await fetchOAuthToken(credentialId, context.workflowId) - if (!accessToken) throw new Error('Missing Confluence access token') - const data = await fetchJson<{ spaces: ConfluenceSpace[] }>('/api/tools/confluence/spaces', { - searchParams: { domain, accessToken, limit: '250' }, + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + domain, }) + const data = await fetchJson<{ spaces: ConfluenceSpace[] }>( + '/api/tools/confluence/selector-spaces', + { method: 'POST', body } + ) const space = (data.spaces || []).find((s) => s.id === detailId) ?? null if (!space) return null return { id: space.id, label: `${space.name} (${space.key})` } @@ -459,13 +467,14 @@ const registry: Record = { fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'jsm.serviceDesks') const domain = ensureDomain(context, 'jsm.serviceDesks') - const accessToken = await fetchOAuthToken(credentialId, context.workflowId) - if (!accessToken) throw new Error('Missing JSM access token') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + domain, + }) const data = await fetchJson<{ serviceDesks: JsmServiceDesk[] }>( '/api/tools/jsm/selector-servicedesks', - { - searchParams: { domain, accessToken }, - } + { method: 'POST', body } ) return (data.serviceDesks || []).map((sd) => ({ id: sd.id, @@ -476,13 +485,14 @@ const registry: Record = { if (!detailId) return null const credentialId = ensureCredential(context, 'jsm.serviceDesks') const domain = ensureDomain(context, 'jsm.serviceDesks') - const accessToken = await fetchOAuthToken(credentialId, context.workflowId) - if (!accessToken) throw new Error('Missing JSM access token') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + domain, + }) const data = await fetchJson<{ serviceDesks: JsmServiceDesk[] }>( '/api/tools/jsm/selector-servicedesks', - { - searchParams: { domain, accessToken }, - } + { method: 'POST', body } ) const sd = (data.serviceDesks || []).find((s) => s.id === detailId) ?? null if (!sd) return null @@ -505,17 +515,15 @@ const registry: Record = { const credentialId = ensureCredential(context, 'jsm.requestTypes') const domain = ensureDomain(context, 'jsm.requestTypes') if (!context.serviceDeskId) throw new Error('Missing serviceDeskId for jsm.requestTypes') - const accessToken = await fetchOAuthToken(credentialId, context.workflowId) - if (!accessToken) throw new Error('Missing JSM access token') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + domain, + serviceDeskId: context.serviceDeskId, + }) const data = await fetchJson<{ requestTypes: JsmRequestType[] }>( '/api/tools/jsm/selector-requesttypes', - { - searchParams: { - domain, - accessToken, - serviceDeskId: context.serviceDeskId, - }, - } + { method: 'POST', body } ) return (data.requestTypes || []).map((rt) => ({ id: rt.id, @@ -527,17 +535,15 @@ const registry: Record = { const credentialId = ensureCredential(context, 'jsm.requestTypes') const domain = ensureDomain(context, 'jsm.requestTypes') if (!context.serviceDeskId) return null - const accessToken = await fetchOAuthToken(credentialId, context.workflowId) - if (!accessToken) throw new Error('Missing JSM access token') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + domain, + serviceDeskId: context.serviceDeskId, + }) const data = await fetchJson<{ requestTypes: JsmRequestType[] }>( '/api/tools/jsm/selector-requesttypes', - { - searchParams: { - domain, - accessToken, - serviceDeskId: context.serviceDeskId, - }, - } + { method: 'POST', body } ) const rt = (data.requestTypes || []).find((r) => r.id === detailId) ?? null if (!rt) return null @@ -585,15 +591,17 @@ const registry: Record = { ], enabled: ({ context }) => Boolean(context.credentialId), fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'microsoft.planner.plans') const data = await fetchJson<{ plans: PlannerPlan[] }>('/api/tools/microsoft_planner/plans', { - searchParams: { credentialId: context.credentialId }, + searchParams: { credentialId }, }) return (data.plans || []).map((plan) => ({ id: plan.id, label: plan.title })) }, fetchById: async ({ context, detailId }: SelectorQueryArgs) => { if (!detailId) return null + const credentialId = ensureCredential(context, 'microsoft.planner.plans') const data = await fetchJson<{ plans: PlannerPlan[] }>('/api/tools/microsoft_planner/plans', { - searchParams: { credentialId: context.credentialId }, + searchParams: { credentialId }, }) const plan = (data.plans || []).find((p) => p.id === detailId) ?? null if (!plan) return null From 8cf988c8a10966f08ebde2a71ebfde829e0c2987 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 17:43:02 -0800 Subject: [PATCH 03/32] fix(selectors): use sanitized serviceDeskId and encode SharePoint siteId Use serviceDeskIdValidation.sanitized instead of raw serviceDeskId in JSM request types URL. Add encodeURIComponent to SharePoint siteId to prevent URL path injection. --- apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts | 2 +- apps/sim/app/api/tools/sharepoint/lists/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts index ed34d5d7bdf..6838e61c131 100644 --- a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts @@ -66,7 +66,7 @@ export async function POST(request: Request) { } const baseUrl = getJsmApiBaseUrl(cloudId) - const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype?limit=100` + const url = `${baseUrl}/servicedesk/${serviceDeskIdValidation.sanitized}/requesttype?limit=100` const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/sharepoint/lists/route.ts b/apps/sim/app/api/tools/sharepoint/lists/route.ts index 68399c72cd2..df5602b43a8 100644 --- a/apps/sim/app/api/tools/sharepoint/lists/route.ts +++ b/apps/sim/app/api/tools/sharepoint/lists/route.ts @@ -89,7 +89,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) } - const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100` + const url = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100` const response = await fetch(url, { headers: { From d55216a5e19b118c6b469d8fa447c3bed861abd8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 17:43:34 -0800 Subject: [PATCH 04/32] lint --- .../api/tools/confluence/selector-spaces/route.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/api/tools/confluence/selector-spaces/route.ts b/apps/sim/app/api/tools/confluence/selector-spaces/route.ts index aa3bca1559f..521d8baad88 100644 --- a/apps/sim/app/api/tools/confluence/selector-spaces/route.ts +++ b/apps/sim/app/api/tools/confluence/selector-spaces/route.ts @@ -79,13 +79,11 @@ export async function POST(request: Request) { } const data = await response.json() - const spaces = (data.results || []).map( - (space: { id: string; name: string; key: string }) => ({ - id: space.id, - name: space.name, - key: space.key, - }) - ) + const spaces = (data.results || []).map((space: { id: string; name: string; key: string }) => ({ + id: space.id, + name: space.name, + key: space.key, + })) return NextResponse.json({ spaces }) } catch (error) { From b532a3716b38cfc1e2d71ff43b61f123e070243a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 17:44:31 -0800 Subject: [PATCH 05/32] fix(selectors): revert encodeURIComponent on SharePoint siteId SharePoint site IDs use the format "hostname,guid,guid" with commas that must remain unencoded for the Microsoft Graph API. The encodeURIComponent call would convert commas to %2C and break the API call. --- apps/sim/app/api/tools/sharepoint/lists/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/sharepoint/lists/route.ts b/apps/sim/app/api/tools/sharepoint/lists/route.ts index df5602b43a8..68399c72cd2 100644 --- a/apps/sim/app/api/tools/sharepoint/lists/route.ts +++ b/apps/sim/app/api/tools/sharepoint/lists/route.ts @@ -89,7 +89,7 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) } - const url = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100` + const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100` const response = await fetch(url, { headers: { From a6e695af4e3356e2d2a5a336d6b4db28b7499338 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 17:53:15 -0800 Subject: [PATCH 06/32] fix(selectors): use sanitized cloudId in Confluence and JSM route URLs Use cloudIdValidation.sanitized instead of raw cloudId in URL construction for consistency with the validation pattern, even though the current validator returns the input unchanged. --- apps/sim/app/api/tools/confluence/selector-spaces/route.ts | 2 +- apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts | 2 +- apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/api/tools/confluence/selector-spaces/route.ts b/apps/sim/app/api/tools/confluence/selector-spaces/route.ts index 521d8baad88..7c3669ea1bf 100644 --- a/apps/sim/app/api/tools/confluence/selector-spaces/route.ts +++ b/apps/sim/app/api/tools/confluence/selector-spaces/route.ts @@ -56,7 +56,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?limit=250` + const url = `https://api.atlassian.com/ex/confluence/${cloudIdValidation.sanitized}/wiki/api/v2/spaces?limit=250` const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts index 6838e61c131..368c2707f37 100644 --- a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts @@ -65,7 +65,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const baseUrl = getJsmApiBaseUrl(cloudId) + const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!) const url = `${baseUrl}/servicedesk/${serviceDeskIdValidation.sanitized}/requesttype?limit=100` const response = await fetch(url, { diff --git a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts index ab3d02a087c..17bfbbbaa26 100644 --- a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts @@ -56,7 +56,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const baseUrl = getJsmApiBaseUrl(cloudId) + const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!) const url = `${baseUrl}/servicedesk?limit=100` const response = await fetch(url, { From c2041afe9cddc70865900a005d0599e49c4a7bef Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:18:07 -0800 Subject: [PATCH 07/32] fix(selectors): add missing context fields to resolution, ensureCredential to sharepoint.lists, and siteId validation - Add baseId, datasetId, serviceDeskId to SelectorResolutionArgs, ExtendedSelectorContext, extractExtendedContext, useSelectorDisplayName, and resolveSelectorForSubBlock so cascading selectors resolve correctly through the resolution path. - Add ensureCredential guard to sharepoint.lists registry entry. - Add regex validation for SharePoint siteId format (hostname,GUID,GUID). --- apps/sim/app/api/tools/sharepoint/lists/route.ts | 5 +++++ apps/sim/hooks/selectors/registry.ts | 6 ++++-- apps/sim/hooks/selectors/resolution.ts | 6 ++++++ apps/sim/hooks/use-selector-display-name.ts | 12 ++++++++++++ apps/sim/lib/workflows/comparison/resolve-values.ts | 12 ++++++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/tools/sharepoint/lists/route.ts b/apps/sim/app/api/tools/sharepoint/lists/route.ts index 68399c72cd2..32ca9eb4897 100644 --- a/apps/sim/app/api/tools/sharepoint/lists/route.ts +++ b/apps/sim/app/api/tools/sharepoint/lists/route.ts @@ -46,6 +46,11 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Site ID is required' }, { status: 400 }) } + const SITE_ID_RE = /^[\w.\-,]+$/ + if (siteId.length > 512 || !SITE_ID_RE.test(siteId)) { + return NextResponse.json({ error: 'Invalid site ID format' }, { status: 400 }) + } + const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255) if (!credentialIdValidation.isValid) { logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error }) diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index a15d1674d8c..9b997aa9781 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -721,10 +721,11 @@ const registry: Record = { ], enabled: ({ context }) => Boolean(context.credentialId && context.siteId), fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'sharepoint.lists') if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector') const data = await fetchJson<{ lists: SharepointList[] }>('/api/tools/sharepoint/lists', { searchParams: { - credentialId: context.credentialId, + credentialId, siteId: context.siteId, }, }) @@ -732,9 +733,10 @@ const registry: Record = { }, fetchById: async ({ context, detailId }: SelectorQueryArgs) => { if (!detailId || !context.siteId) return null + const credentialId = ensureCredential(context, 'sharepoint.lists') const data = await fetchJson<{ lists: SharepointList[] }>('/api/tools/sharepoint/lists', { searchParams: { - credentialId: context.credentialId, + credentialId, siteId: context.siteId, }, }) diff --git a/apps/sim/hooks/selectors/resolution.ts b/apps/sim/hooks/selectors/resolution.ts index 9f299d99d8f..b37b1761320 100644 --- a/apps/sim/hooks/selectors/resolution.ts +++ b/apps/sim/hooks/selectors/resolution.ts @@ -18,6 +18,9 @@ export interface SelectorResolutionArgs { siteId?: string collectionId?: string spreadsheetId?: string + baseId?: string + datasetId?: string + serviceDeskId?: string } export function resolveSelectorForSubBlock( @@ -38,6 +41,9 @@ export function resolveSelectorForSubBlock( siteId: args.siteId, collectionId: args.collectionId, spreadsheetId: args.spreadsheetId, + baseId: args.baseId, + datasetId: args.datasetId, + serviceDeskId: args.serviceDeskId, mimeType: subBlock.mimeType, }, allowSearch: subBlock.selectorAllowSearch ?? true, diff --git a/apps/sim/hooks/use-selector-display-name.ts b/apps/sim/hooks/use-selector-display-name.ts index 91d6f7f8172..7ead2462669 100644 --- a/apps/sim/hooks/use-selector-display-name.ts +++ b/apps/sim/hooks/use-selector-display-name.ts @@ -18,6 +18,9 @@ interface SelectorDisplayNameArgs { planId?: string teamId?: string knowledgeBaseId?: string + baseId?: string + datasetId?: string + serviceDeskId?: string } export function useSelectorDisplayName({ @@ -30,6 +33,9 @@ export function useSelectorDisplayName({ planId, teamId, knowledgeBaseId, + baseId, + datasetId, + serviceDeskId, }: SelectorDisplayNameArgs) { const detailId = typeof value === 'string' && value.length > 0 ? value : undefined @@ -43,6 +49,9 @@ export function useSelectorDisplayName({ planId, teamId, knowledgeBaseId, + baseId, + datasetId, + serviceDeskId, }) }, [ subBlock, @@ -54,6 +63,9 @@ export function useSelectorDisplayName({ planId, teamId, knowledgeBaseId, + baseId, + datasetId, + serviceDeskId, ]) const key = resolution?.key diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index 4912654023d..9a041e7cc9c 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -52,6 +52,9 @@ interface ExtendedSelectorContext { siteId?: string collectionId?: string spreadsheetId?: string + baseId?: string + datasetId?: string + serviceDeskId?: string } function getSemanticFallback(subBlockId: string, subBlockConfig?: SubBlockConfig): string { @@ -163,6 +166,9 @@ async function resolveSelectorValue( siteId: extendedContext.siteId, collectionId: extendedContext.collectionId, spreadsheetId: extendedContext.spreadsheetId, + baseId: extendedContext.baseId, + datasetId: extendedContext.datasetId, + serviceDeskId: extendedContext.serviceDeskId, } if (definition.fetchById) { @@ -240,6 +246,9 @@ function extractExtendedContext( siteId: getStringValue('siteId'), collectionId: getStringValue('collectionId'), spreadsheetId: getStringValue('spreadsheetId') || getStringValue('fileId'), + baseId: getStringValue('baseId') || getStringValue('baseSelector'), + datasetId: getStringValue('datasetId') || getStringValue('datasetSelector'), + serviceDeskId: getStringValue('serviceDeskId') || getStringValue('serviceDeskSelector'), } } @@ -313,6 +322,9 @@ export async function resolveValueForDisplay( siteId: extendedContext.siteId, collectionId: extendedContext.collectionId, spreadsheetId: extendedContext.spreadsheetId, + baseId: extendedContext.baseId, + datasetId: extendedContext.datasetId, + serviceDeskId: extendedContext.serviceDeskId, }) if (resolution?.key) { From 4e30161c68e640f1c2e4ca79d16b69f12d631ab2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:26:28 -0800 Subject: [PATCH 08/32] fix(selectors): rename advanced subBlock IDs to avoid canonicalParamId clashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename all advanced-mode subBlock IDs that matched their canonicalParamId to use a `manual*` prefix, following the established convention (e.g., manualSiteId, manualCredential). This prevents ambiguity between subBlock IDs and canonical parameter names in the serialization layer. 25 renames across 14 blocks: baseId→manualBaseId, tableId→manualTableId, workspace→manualWorkspace, objectType→manualObjectType, etc. --- apps/sim/blocks/blocks/airtable.ts | 4 ++-- apps/sim/blocks/blocks/asana.ts | 4 ++-- apps/sim/blocks/blocks/attio.ts | 4 ++-- apps/sim/blocks/blocks/calcom.ts | 8 ++++---- apps/sim/blocks/blocks/confluence.ts | 2 +- apps/sim/blocks/blocks/google_bigquery.ts | 4 ++-- apps/sim/blocks/blocks/google_tasks.ts | 2 +- apps/sim/blocks/blocks/jira_service_management.ts | 4 ++-- apps/sim/blocks/blocks/microsoft_planner.ts | 4 ++-- apps/sim/blocks/blocks/notion.ts | 6 +++--- apps/sim/blocks/blocks/pipedrive.ts | 2 +- apps/sim/blocks/blocks/sharepoint.ts | 2 +- apps/sim/blocks/blocks/trello.ts | 2 +- apps/sim/blocks/blocks/zoom.ts | 2 +- apps/sim/lib/workflows/comparison/resolve-values.ts | 6 +++--- 15 files changed, 28 insertions(+), 28 deletions(-) diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index f2b38efb309..c533b9084da 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -72,7 +72,7 @@ export const AirtableBlock: BlockConfig = { required: { field: 'operation', value: 'listBases', not: true }, }, { - id: 'baseId', + id: 'manualBaseId', title: 'Base ID', type: 'short-input', canonicalParamId: 'baseId', @@ -96,7 +96,7 @@ export const AirtableBlock: BlockConfig = { required: { field: 'operation', value: ['listBases', 'listTables'], not: true }, }, { - id: 'tableId', + id: 'manualTableId', title: 'Table ID', type: 'short-input', canonicalParamId: 'tableId', diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index 25418864593..c5b0fcc7926 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -66,7 +66,7 @@ export const AsanaBlock: BlockConfig = { required: true, }, { - id: 'workspace', + id: 'manualWorkspace', title: 'Workspace GID', type: 'short-input', canonicalParamId: 'workspace', @@ -117,7 +117,7 @@ export const AsanaBlock: BlockConfig = { }, }, { - id: 'getTasks_workspace', + id: 'manualGetTasksWorkspace', title: 'Workspace GID', type: 'short-input', canonicalParamId: 'getTasks_workspace', diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index aebea95d363..20b8d277b2c 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -121,7 +121,7 @@ export const AttioBlock: BlockConfig = { }, }, { - id: 'objectType', + id: 'manualObjectType', title: 'Object Type', type: 'short-input', canonicalParamId: 'objectType', @@ -597,7 +597,7 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. }, }, { - id: 'listIdOrSlug', + id: 'manualListIdOrSlug', title: 'List ID or Slug', type: 'short-input', canonicalParamId: 'listIdOrSlug', diff --git a/apps/sim/blocks/blocks/calcom.ts b/apps/sim/blocks/blocks/calcom.ts index b26280d73e4..c55cd459f13 100644 --- a/apps/sim/blocks/blocks/calcom.ts +++ b/apps/sim/blocks/blocks/calcom.ts @@ -83,7 +83,7 @@ export const CalComBlock: BlockConfig = { required: { field: 'operation', value: 'calcom_create_booking' }, }, { - id: 'eventTypeId', + id: 'manualEventTypeId', title: 'Event Type ID', type: 'short-input', canonicalParamId: 'eventTypeId', @@ -301,7 +301,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, }, { - id: 'eventTypeIdParam', + id: 'manualEventTypeIdParam', title: 'Event Type ID', type: 'short-input', canonicalParamId: 'eventTypeIdParam', @@ -422,7 +422,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, }, { - id: 'eventTypeScheduleId', + id: 'manualEventTypeScheduleId', title: 'Schedule ID', type: 'short-input', canonicalParamId: 'eventTypeScheduleId', @@ -467,7 +467,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, }, { - id: 'scheduleId', + id: 'manualScheduleId', title: 'Schedule ID', type: 'short-input', canonicalParamId: 'scheduleId', diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 84574fdf017..c7d7f9d3f6d 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -157,7 +157,7 @@ export const ConfluenceBlock: BlockConfig = { required: { field: 'operation', value: ['create', 'get_space'] }, }, { - id: 'spaceId', + id: 'manualSpaceId', title: 'Space ID', type: 'short-input', canonicalParamId: 'spaceId', diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 1fdece82317..3c86dfaae89 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -124,7 +124,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, }, { - id: 'datasetId', + id: 'manualDatasetId', title: 'Dataset ID', type: 'short-input', canonicalParamId: 'datasetId', @@ -149,7 +149,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, required: { field: 'operation', value: ['get_table', 'insert_rows'] }, }, { - id: 'tableId', + id: 'manualTableId', title: 'Table ID', type: 'short-input', canonicalParamId: 'tableId', diff --git a/apps/sim/blocks/blocks/google_tasks.ts b/apps/sim/blocks/blocks/google_tasks.ts index 06b4b4ea4f6..63f658ae1f9 100644 --- a/apps/sim/blocks/blocks/google_tasks.ts +++ b/apps/sim/blocks/blocks/google_tasks.ts @@ -66,7 +66,7 @@ export const GoogleTasksBlock: BlockConfig = { condition: { field: 'operation', value: 'list_task_lists', not: true }, }, { - id: 'taskListId', + id: 'manualTaskListId', title: 'Task List ID', type: 'short-input', canonicalParamId: 'taskListId', diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 916f0b2bd1e..5ffa1739d0e 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -146,7 +146,7 @@ export const JiraServiceManagementBlock: BlockConfig = { }, }, { - id: 'serviceDeskId', + id: 'manualServiceDeskId', title: 'Service Desk ID', type: 'short-input', canonicalParamId: 'serviceDeskId', @@ -195,7 +195,7 @@ export const JiraServiceManagementBlock: BlockConfig = { condition: { field: 'operation', value: ['create_request', 'get_request_type_fields'] }, }, { - id: 'requestTypeId', + id: 'manualRequestTypeId', title: 'Request Type ID', type: 'short-input', canonicalParamId: 'requestTypeId', diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index ab90c179236..5e1e0e67160 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -108,7 +108,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { // Plan ID - advanced mode { - id: 'planId', + id: 'manualPlanId', title: 'Plan ID', type: 'short-input', canonicalParamId: 'planId', @@ -146,7 +146,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { type: 'short-input', placeholder: 'Enter the task ID', condition: { field: 'operation', value: ['read_task'] }, - dependsOn: ['credential', 'planId'], + dependsOn: ['credential', 'manualPlanId'], mode: 'advanced', canonicalParamId: 'readTaskId', }, diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 2b7c563c655..769dcf4893d 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -69,7 +69,7 @@ export const NotionBlock: BlockConfig = { required: { field: 'operation', value: ['notion_read', 'notion_write'] }, }, { - id: 'pageId', + id: 'manualPageId', title: 'Page ID', type: 'short-input', canonicalParamId: 'pageId', @@ -100,7 +100,7 @@ export const NotionBlock: BlockConfig = { }, }, { - id: 'databaseId', + id: 'manualDatabaseId', title: 'Database ID', type: 'short-input', canonicalParamId: 'databaseId', @@ -137,7 +137,7 @@ export const NotionBlock: BlockConfig = { }, }, { - id: 'parentId', + id: 'manualParentId', title: 'Parent Page ID', type: 'short-input', canonicalParamId: 'parentId', diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index 55e5c331ff4..ca12d425a28 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -114,7 +114,7 @@ export const PipedriveBlock: BlockConfig = { required: { field: 'operation', value: 'get_pipeline_deals' }, }, { - id: 'pipeline_id', + id: 'manualPipelineId', title: 'Pipeline ID', type: 'short-input', canonicalParamId: 'pipeline_id', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index dcc02839d36..539b8571e95 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -126,7 +126,7 @@ export const SharepointBlock: BlockConfig = { condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] }, }, { - id: 'listId', + id: 'manualListId', title: 'List ID', type: 'short-input', canonicalParamId: 'listId', diff --git a/apps/sim/blocks/blocks/trello.ts b/apps/sim/blocks/blocks/trello.ts index bff115ebdcf..3e4c94bf091 100644 --- a/apps/sim/blocks/blocks/trello.ts +++ b/apps/sim/blocks/blocks/trello.ts @@ -84,7 +84,7 @@ export const TrelloBlock: BlockConfig = { }, }, { - id: 'boardId', + id: 'manualBoardId', title: 'Board ID', type: 'short-input', canonicalParamId: 'boardId', diff --git a/apps/sim/blocks/blocks/zoom.ts b/apps/sim/blocks/blocks/zoom.ts index 23b7dc70a84..01171057e71 100644 --- a/apps/sim/blocks/blocks/zoom.ts +++ b/apps/sim/blocks/blocks/zoom.ts @@ -104,7 +104,7 @@ export const ZoomBlock: BlockConfig = { }, }, { - id: 'meetingId', + id: 'manualMeetingId', title: 'Meeting ID', type: 'short-input', canonicalParamId: 'meetingId', diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index 9a041e7cc9c..c7b56f4a4fe 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -246,9 +246,9 @@ function extractExtendedContext( siteId: getStringValue('siteId'), collectionId: getStringValue('collectionId'), spreadsheetId: getStringValue('spreadsheetId') || getStringValue('fileId'), - baseId: getStringValue('baseId') || getStringValue('baseSelector'), - datasetId: getStringValue('datasetId') || getStringValue('datasetSelector'), - serviceDeskId: getStringValue('serviceDeskId') || getStringValue('serviceDeskSelector'), + baseId: getStringValue('manualBaseId') || getStringValue('baseSelector'), + datasetId: getStringValue('manualDatasetId') || getStringValue('datasetSelector'), + serviceDeskId: getStringValue('manualServiceDeskId') || getStringValue('serviceDeskSelector'), } } From 517d400fe07a1b5df1b301cebeb3e679c350479e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:28:02 -0800 Subject: [PATCH 09/32] Revert "fix(selectors): rename advanced subBlock IDs to avoid canonicalParamId clashes" This reverts commit 4e30161c68e640f1c2e4ca79d16b69f12d631ab2. --- apps/sim/blocks/blocks/airtable.ts | 4 ++-- apps/sim/blocks/blocks/asana.ts | 4 ++-- apps/sim/blocks/blocks/attio.ts | 4 ++-- apps/sim/blocks/blocks/calcom.ts | 8 ++++---- apps/sim/blocks/blocks/confluence.ts | 2 +- apps/sim/blocks/blocks/google_bigquery.ts | 4 ++-- apps/sim/blocks/blocks/google_tasks.ts | 2 +- apps/sim/blocks/blocks/jira_service_management.ts | 4 ++-- apps/sim/blocks/blocks/microsoft_planner.ts | 4 ++-- apps/sim/blocks/blocks/notion.ts | 6 +++--- apps/sim/blocks/blocks/pipedrive.ts | 2 +- apps/sim/blocks/blocks/sharepoint.ts | 2 +- apps/sim/blocks/blocks/trello.ts | 2 +- apps/sim/blocks/blocks/zoom.ts | 2 +- apps/sim/lib/workflows/comparison/resolve-values.ts | 6 +++--- 15 files changed, 28 insertions(+), 28 deletions(-) diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index c533b9084da..f2b38efb309 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -72,7 +72,7 @@ export const AirtableBlock: BlockConfig = { required: { field: 'operation', value: 'listBases', not: true }, }, { - id: 'manualBaseId', + id: 'baseId', title: 'Base ID', type: 'short-input', canonicalParamId: 'baseId', @@ -96,7 +96,7 @@ export const AirtableBlock: BlockConfig = { required: { field: 'operation', value: ['listBases', 'listTables'], not: true }, }, { - id: 'manualTableId', + id: 'tableId', title: 'Table ID', type: 'short-input', canonicalParamId: 'tableId', diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index c5b0fcc7926..25418864593 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -66,7 +66,7 @@ export const AsanaBlock: BlockConfig = { required: true, }, { - id: 'manualWorkspace', + id: 'workspace', title: 'Workspace GID', type: 'short-input', canonicalParamId: 'workspace', @@ -117,7 +117,7 @@ export const AsanaBlock: BlockConfig = { }, }, { - id: 'manualGetTasksWorkspace', + id: 'getTasks_workspace', title: 'Workspace GID', type: 'short-input', canonicalParamId: 'getTasks_workspace', diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 20b8d277b2c..aebea95d363 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -121,7 +121,7 @@ export const AttioBlock: BlockConfig = { }, }, { - id: 'manualObjectType', + id: 'objectType', title: 'Object Type', type: 'short-input', canonicalParamId: 'objectType', @@ -597,7 +597,7 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. }, }, { - id: 'manualListIdOrSlug', + id: 'listIdOrSlug', title: 'List ID or Slug', type: 'short-input', canonicalParamId: 'listIdOrSlug', diff --git a/apps/sim/blocks/blocks/calcom.ts b/apps/sim/blocks/blocks/calcom.ts index c55cd459f13..b26280d73e4 100644 --- a/apps/sim/blocks/blocks/calcom.ts +++ b/apps/sim/blocks/blocks/calcom.ts @@ -83,7 +83,7 @@ export const CalComBlock: BlockConfig = { required: { field: 'operation', value: 'calcom_create_booking' }, }, { - id: 'manualEventTypeId', + id: 'eventTypeId', title: 'Event Type ID', type: 'short-input', canonicalParamId: 'eventTypeId', @@ -301,7 +301,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, }, { - id: 'manualEventTypeIdParam', + id: 'eventTypeIdParam', title: 'Event Type ID', type: 'short-input', canonicalParamId: 'eventTypeIdParam', @@ -422,7 +422,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, }, { - id: 'manualEventTypeScheduleId', + id: 'eventTypeScheduleId', title: 'Schedule ID', type: 'short-input', canonicalParamId: 'eventTypeScheduleId', @@ -467,7 +467,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, }, }, { - id: 'manualScheduleId', + id: 'scheduleId', title: 'Schedule ID', type: 'short-input', canonicalParamId: 'scheduleId', diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index c7d7f9d3f6d..84574fdf017 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -157,7 +157,7 @@ export const ConfluenceBlock: BlockConfig = { required: { field: 'operation', value: ['create', 'get_space'] }, }, { - id: 'manualSpaceId', + id: 'spaceId', title: 'Space ID', type: 'short-input', canonicalParamId: 'spaceId', diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 3c86dfaae89..1fdece82317 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -124,7 +124,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, }, { - id: 'manualDatasetId', + id: 'datasetId', title: 'Dataset ID', type: 'short-input', canonicalParamId: 'datasetId', @@ -149,7 +149,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, required: { field: 'operation', value: ['get_table', 'insert_rows'] }, }, { - id: 'manualTableId', + id: 'tableId', title: 'Table ID', type: 'short-input', canonicalParamId: 'tableId', diff --git a/apps/sim/blocks/blocks/google_tasks.ts b/apps/sim/blocks/blocks/google_tasks.ts index 63f658ae1f9..06b4b4ea4f6 100644 --- a/apps/sim/blocks/blocks/google_tasks.ts +++ b/apps/sim/blocks/blocks/google_tasks.ts @@ -66,7 +66,7 @@ export const GoogleTasksBlock: BlockConfig = { condition: { field: 'operation', value: 'list_task_lists', not: true }, }, { - id: 'manualTaskListId', + id: 'taskListId', title: 'Task List ID', type: 'short-input', canonicalParamId: 'taskListId', diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 5ffa1739d0e..916f0b2bd1e 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -146,7 +146,7 @@ export const JiraServiceManagementBlock: BlockConfig = { }, }, { - id: 'manualServiceDeskId', + id: 'serviceDeskId', title: 'Service Desk ID', type: 'short-input', canonicalParamId: 'serviceDeskId', @@ -195,7 +195,7 @@ export const JiraServiceManagementBlock: BlockConfig = { condition: { field: 'operation', value: ['create_request', 'get_request_type_fields'] }, }, { - id: 'manualRequestTypeId', + id: 'requestTypeId', title: 'Request Type ID', type: 'short-input', canonicalParamId: 'requestTypeId', diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index 5e1e0e67160..ab90c179236 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -108,7 +108,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { // Plan ID - advanced mode { - id: 'manualPlanId', + id: 'planId', title: 'Plan ID', type: 'short-input', canonicalParamId: 'planId', @@ -146,7 +146,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { type: 'short-input', placeholder: 'Enter the task ID', condition: { field: 'operation', value: ['read_task'] }, - dependsOn: ['credential', 'manualPlanId'], + dependsOn: ['credential', 'planId'], mode: 'advanced', canonicalParamId: 'readTaskId', }, diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 769dcf4893d..2b7c563c655 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -69,7 +69,7 @@ export const NotionBlock: BlockConfig = { required: { field: 'operation', value: ['notion_read', 'notion_write'] }, }, { - id: 'manualPageId', + id: 'pageId', title: 'Page ID', type: 'short-input', canonicalParamId: 'pageId', @@ -100,7 +100,7 @@ export const NotionBlock: BlockConfig = { }, }, { - id: 'manualDatabaseId', + id: 'databaseId', title: 'Database ID', type: 'short-input', canonicalParamId: 'databaseId', @@ -137,7 +137,7 @@ export const NotionBlock: BlockConfig = { }, }, { - id: 'manualParentId', + id: 'parentId', title: 'Parent Page ID', type: 'short-input', canonicalParamId: 'parentId', diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index ca12d425a28..55e5c331ff4 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -114,7 +114,7 @@ export const PipedriveBlock: BlockConfig = { required: { field: 'operation', value: 'get_pipeline_deals' }, }, { - id: 'manualPipelineId', + id: 'pipeline_id', title: 'Pipeline ID', type: 'short-input', canonicalParamId: 'pipeline_id', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 539b8571e95..dcc02839d36 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -126,7 +126,7 @@ export const SharepointBlock: BlockConfig = { condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] }, }, { - id: 'manualListId', + id: 'listId', title: 'List ID', type: 'short-input', canonicalParamId: 'listId', diff --git a/apps/sim/blocks/blocks/trello.ts b/apps/sim/blocks/blocks/trello.ts index 3e4c94bf091..bff115ebdcf 100644 --- a/apps/sim/blocks/blocks/trello.ts +++ b/apps/sim/blocks/blocks/trello.ts @@ -84,7 +84,7 @@ export const TrelloBlock: BlockConfig = { }, }, { - id: 'manualBoardId', + id: 'boardId', title: 'Board ID', type: 'short-input', canonicalParamId: 'boardId', diff --git a/apps/sim/blocks/blocks/zoom.ts b/apps/sim/blocks/blocks/zoom.ts index 01171057e71..23b7dc70a84 100644 --- a/apps/sim/blocks/blocks/zoom.ts +++ b/apps/sim/blocks/blocks/zoom.ts @@ -104,7 +104,7 @@ export const ZoomBlock: BlockConfig = { }, }, { - id: 'manualMeetingId', + id: 'meetingId', title: 'Meeting ID', type: 'short-input', canonicalParamId: 'meetingId', diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index c7b56f4a4fe..9a041e7cc9c 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -246,9 +246,9 @@ function extractExtendedContext( siteId: getStringValue('siteId'), collectionId: getStringValue('collectionId'), spreadsheetId: getStringValue('spreadsheetId') || getStringValue('fileId'), - baseId: getStringValue('manualBaseId') || getStringValue('baseSelector'), - datasetId: getStringValue('manualDatasetId') || getStringValue('datasetSelector'), - serviceDeskId: getStringValue('manualServiceDeskId') || getStringValue('serviceDeskSelector'), + baseId: getStringValue('baseId') || getStringValue('baseSelector'), + datasetId: getStringValue('datasetId') || getStringValue('datasetSelector'), + serviceDeskId: getStringValue('serviceDeskId') || getStringValue('serviceDeskSelector'), } } From 8512dbdbf5cd54b3ffd4d0beb759a852cea22cf8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:40:34 -0800 Subject: [PATCH 10/32] fix(selectors): rename canonicalParamIds to avoid subBlock ID clashes Prefix all clashing canonicalParamId values with `selected_` so they don't match any subBlock ID. Update each block's `inputs` section and `tools.config.params` function to destructure the new canonical names and remap them to the original tool param names. SubBlock IDs and tool definitions remain unchanged for backwards compatibility. Affected: 25 canonical params across 14 blocks (airtable, asana, attio, calcom, confluence, google_bigquery, google_tasks, jsm, microsoft_planner, notion, pipedrive, sharepoint, trello, zoom). --- apps/sim/blocks/blocks/airtable.ts | 17 +++--- apps/sim/blocks/blocks/asana.ts | 20 +++---- apps/sim/blocks/blocks/attio.ts | 16 +++--- apps/sim/blocks/blocks/calcom.ts | 44 +++++++-------- apps/sim/blocks/blocks/confluence.ts | 29 +++++----- apps/sim/blocks/blocks/google_bigquery.ts | 17 +++--- apps/sim/blocks/blocks/google_tasks.ts | 20 +++++-- .../blocks/blocks/jira_service_management.ts | 54 +++++++++---------- apps/sim/blocks/blocks/microsoft_planner.ts | 20 +++---- apps/sim/blocks/blocks/notion.ts | 33 ++++++++---- apps/sim/blocks/blocks/pipedrive.ts | 12 +++-- apps/sim/blocks/blocks/sharepoint.ts | 10 ++-- apps/sim/blocks/blocks/trello.ts | 10 ++-- apps/sim/blocks/blocks/zoom.ts | 34 ++++++------ 14 files changed, 187 insertions(+), 149 deletions(-) diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index f2b38efb309..b5afa6446bf 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -61,7 +61,7 @@ export const AirtableBlock: BlockConfig = { id: 'baseSelector', title: 'Base', type: 'project-selector', - canonicalParamId: 'baseId', + canonicalParamId: 'selected_baseId', serviceId: 'airtable', selectorKey: 'airtable.bases', selectorAllowSearch: false, @@ -75,7 +75,7 @@ export const AirtableBlock: BlockConfig = { id: 'baseId', title: 'Base ID', type: 'short-input', - canonicalParamId: 'baseId', + canonicalParamId: 'selected_baseId', placeholder: 'Enter your base ID (e.g., appXXXXXXXXXXXXXX)', mode: 'advanced', condition: { field: 'operation', value: 'listBases', not: true }, @@ -85,7 +85,7 @@ export const AirtableBlock: BlockConfig = { id: 'tableSelector', title: 'Table', type: 'file-selector', - canonicalParamId: 'tableId', + canonicalParamId: 'selected_tableId', serviceId: 'airtable', selectorKey: 'airtable.tables', selectorAllowSearch: false, @@ -99,7 +99,7 @@ export const AirtableBlock: BlockConfig = { id: 'tableId', title: 'Table ID', type: 'short-input', - canonicalParamId: 'tableId', + canonicalParamId: 'selected_tableId', placeholder: 'Enter table ID (e.g., tblXXXXXXXXXXXXXX)', mode: 'advanced', condition: { field: 'operation', value: ['listBases', 'listTables'], not: true }, @@ -274,7 +274,8 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, } }, params: (params) => { - const { oauthCredential, records, fields, ...rest } = params + const { oauthCredential, records, fields, selected_baseId, selected_tableId, ...rest } = + params let parsedRecords: any | undefined let parsedFields: any | undefined @@ -293,6 +294,8 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, // Construct parameters based on operation const baseParams = { credential: oauthCredential, + baseId: selected_baseId, + tableId: selected_tableId, ...rest, } @@ -311,8 +314,8 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Airtable access token' }, - baseId: { type: 'string', description: 'Airtable base identifier' }, - tableId: { type: 'string', description: 'Airtable table identifier' }, + selected_baseId: { type: 'string', description: 'Airtable base identifier' }, + selected_tableId: { type: 'string', description: 'Airtable table identifier' }, // Conditional inputs recordId: { type: 'string', description: 'Record identifier' }, // Required for get/update maxRecords: { type: 'number', description: 'Maximum records to return' }, // Optional for list diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index 25418864593..96ee21cbd41 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -52,7 +52,7 @@ export const AsanaBlock: BlockConfig = { id: 'workspaceSelector', title: 'Workspace', type: 'project-selector', - canonicalParamId: 'workspace', + canonicalParamId: 'selected_workspace', serviceId: 'asana', selectorKey: 'asana.workspaces', selectorAllowSearch: false, @@ -69,7 +69,7 @@ export const AsanaBlock: BlockConfig = { id: 'workspace', title: 'Workspace GID', type: 'short-input', - canonicalParamId: 'workspace', + canonicalParamId: 'selected_workspace', required: true, placeholder: 'Enter Asana workspace GID', mode: 'advanced', @@ -104,7 +104,7 @@ export const AsanaBlock: BlockConfig = { id: 'getTasksWorkspaceSelector', title: 'Workspace', type: 'project-selector', - canonicalParamId: 'getTasks_workspace', + canonicalParamId: 'selected_getTasks_workspace', serviceId: 'asana', selectorKey: 'asana.workspaces', selectorAllowSearch: false, @@ -120,7 +120,7 @@ export const AsanaBlock: BlockConfig = { id: 'getTasks_workspace', title: 'Workspace GID', type: 'short-input', - canonicalParamId: 'getTasks_workspace', + canonicalParamId: 'selected_getTasks_workspace', placeholder: 'Enter workspace GID', mode: 'advanced', condition: { @@ -280,14 +280,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, taskGid: params.taskGid, - workspace: params.getTasks_workspace, + workspace: params.selected_getTasks_workspace, project: params.getTasks_project, limit: params.getTasks_limit ? Number(params.getTasks_limit) : undefined, } case 'create_task': return { ...baseParams, - workspace: params.workspace, + workspace: params.selected_workspace, name: params.name, notes: params.notes, assignee: params.assignee, @@ -306,12 +306,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n case 'get_projects': return { ...baseParams, - workspace: params.workspace, + workspace: params.selected_workspace, } case 'search_tasks': return { ...baseParams, - workspace: params.workspace, + workspace: params.selected_workspace, text: params.searchText, assignee: params.assignee, projects: projectsArray, @@ -332,9 +332,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Asana OAuth credential' }, - workspace: { type: 'string', description: 'Workspace GID' }, + selected_workspace: { type: 'string', description: 'Workspace GID' }, taskGid: { type: 'string', description: 'Task GID' }, - getTasks_workspace: { type: 'string', description: 'Workspace GID for getting tasks' }, + selected_getTasks_workspace: { type: 'string', description: 'Workspace GID for getting tasks' }, getTasks_project: { type: 'string', description: 'Project GID filter for getting tasks' }, getTasks_limit: { type: 'string', description: 'Limit for getting tasks' }, name: { type: 'string', description: 'Task name' }, diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index aebea95d363..49719a5e5ac 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -90,7 +90,7 @@ export const AttioBlock: BlockConfig = { id: 'objectTypeSelector', title: 'Object Type', type: 'project-selector', - canonicalParamId: 'objectType', + canonicalParamId: 'selected_objectType', serviceId: 'attio', selectorKey: 'attio.objects', selectorAllowSearch: false, @@ -124,7 +124,7 @@ export const AttioBlock: BlockConfig = { id: 'objectType', title: 'Object Type', type: 'short-input', - canonicalParamId: 'objectType', + canonicalParamId: 'selected_objectType', placeholder: 'e.g. people, companies', mode: 'advanced', condition: { @@ -564,7 +564,7 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. id: 'listSelector', title: 'List', type: 'project-selector', - canonicalParamId: 'listIdOrSlug', + canonicalParamId: 'selected_listIdOrSlug', serviceId: 'attio', selectorKey: 'attio.lists', selectorAllowSearch: false, @@ -600,7 +600,7 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. id: 'listIdOrSlug', title: 'List ID or Slug', type: 'short-input', - canonicalParamId: 'listIdOrSlug', + canonicalParamId: 'selected_listIdOrSlug', placeholder: 'Enter the list ID or slug', mode: 'advanced', condition: { @@ -1116,7 +1116,7 @@ workspace-member.created } // Record params - if (params.objectType) cleanParams.objectType = params.objectType + if (params.selected_objectType) cleanParams.objectType = params.selected_objectType if (params.recordId) cleanParams.recordId = params.recordId if (params.matchingAttribute) cleanParams.matchingAttribute = params.matchingAttribute if (params.values) cleanParams.values = params.values @@ -1157,7 +1157,7 @@ workspace-member.created if (params.objectPluralNoun) cleanParams.pluralNoun = params.objectPluralNoun // List params - if (params.listIdOrSlug) cleanParams.list = params.listIdOrSlug + if (params.selected_listIdOrSlug) cleanParams.list = params.selected_listIdOrSlug if (params.listName) cleanParams.name = params.listName if (params.listParentObject) cleanParams.parentObject = params.listParentObject if (params.listApiSlug) cleanParams.apiSlug = params.listApiSlug @@ -1209,7 +1209,7 @@ workspace-member.created inputs: { operation: { type: 'string', description: 'The operation to perform' }, oauthCredential: { type: 'string', description: 'Attio OAuth credential' }, - objectType: { type: 'string', description: 'Object type slug' }, + selected_objectType: { type: 'string', description: 'Object type slug' }, recordId: { type: 'string', description: 'Record ID' }, matchingAttribute: { type: 'string', description: 'Matching attribute for upsert' }, values: { type: 'json', description: 'Record attribute values' }, @@ -1235,7 +1235,7 @@ workspace-member.created objectApiSlug: { type: 'string', description: 'Object API slug' }, objectSingularNoun: { type: 'string', description: 'Object singular name' }, objectPluralNoun: { type: 'string', description: 'Object plural name' }, - listIdOrSlug: { type: 'string', description: 'List ID or slug' }, + selected_listIdOrSlug: { type: 'string', description: 'List ID or slug' }, listName: { type: 'string', description: 'List name' }, listParentObject: { type: 'string', description: 'List parent object' }, listApiSlug: { type: 'string', description: 'List API slug' }, diff --git a/apps/sim/blocks/blocks/calcom.ts b/apps/sim/blocks/blocks/calcom.ts index b26280d73e4..45789181ae4 100644 --- a/apps/sim/blocks/blocks/calcom.ts +++ b/apps/sim/blocks/blocks/calcom.ts @@ -69,7 +69,7 @@ export const CalComBlock: BlockConfig = { id: 'eventTypeSelector', title: 'Event Type', type: 'project-selector', - canonicalParamId: 'eventTypeId', + canonicalParamId: 'selected_eventTypeId', serviceId: 'calcom', selectorKey: 'calcom.eventTypes', selectorAllowSearch: false, @@ -86,7 +86,7 @@ export const CalComBlock: BlockConfig = { id: 'eventTypeId', title: 'Event Type ID', type: 'short-input', - canonicalParamId: 'eventTypeId', + canonicalParamId: 'selected_eventTypeId', placeholder: 'Enter event type ID (number)', mode: 'advanced', condition: { @@ -284,7 +284,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'eventTypeParamSelector', title: 'Event Type', type: 'project-selector', - canonicalParamId: 'eventTypeIdParam', + canonicalParamId: 'selected_eventTypeIdParam', serviceId: 'calcom', selectorKey: 'calcom.eventTypes', selectorAllowSearch: false, @@ -304,7 +304,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'eventTypeIdParam', title: 'Event Type ID', type: 'short-input', - canonicalParamId: 'eventTypeIdParam', + canonicalParamId: 'selected_eventTypeIdParam', placeholder: 'Enter event type ID', mode: 'advanced', condition: { @@ -409,7 +409,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'eventTypeScheduleSelector', title: 'Schedule', type: 'project-selector', - canonicalParamId: 'eventTypeScheduleId', + canonicalParamId: 'selected_eventTypeScheduleId', serviceId: 'calcom', selectorKey: 'calcom.schedules', selectorAllowSearch: false, @@ -425,7 +425,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'eventTypeScheduleId', title: 'Schedule ID', type: 'short-input', - canonicalParamId: 'eventTypeScheduleId', + canonicalParamId: 'selected_eventTypeScheduleId', placeholder: 'Assign to a specific schedule', condition: { field: 'operation', @@ -450,7 +450,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'scheduleSelector', title: 'Schedule', type: 'project-selector', - canonicalParamId: 'scheduleId', + canonicalParamId: 'selected_scheduleId', serviceId: 'calcom', selectorKey: 'calcom.schedules', selectorAllowSearch: false, @@ -470,7 +470,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'scheduleId', title: 'Schedule ID', type: 'short-input', - canonicalParamId: 'scheduleId', + canonicalParamId: 'selected_scheduleId', placeholder: 'Enter schedule ID', mode: 'advanced', condition: { @@ -659,17 +659,17 @@ Return ONLY valid JSON - no explanations.`, reschedulingReason, bookingStatus, lengthInMinutes, - eventTypeIdParam, - eventTypeId, + selected_eventTypeIdParam, + selected_eventTypeId, eventLength, - eventTypeScheduleId, + selected_eventTypeScheduleId, slotInterval, minimumBookingNotice, beforeEventBuffer, afterEventBuffer, disableGuests, name, - scheduleId, + selected_scheduleId, isDefault, eventTypeSlug, username, @@ -698,7 +698,7 @@ Return ONLY valid JSON - no explanations.`, ...(attendeePhone && { phoneNumber: attendeePhone }), } { - const eventTypeIdNum = toNumber(eventTypeId) + const eventTypeIdNum = toNumber(selected_eventTypeId) if (eventTypeIdNum !== undefined) result.eventTypeId = eventTypeIdNum } if (start) result.start = start @@ -736,12 +736,12 @@ Return ONLY valid JSON - no explanations.`, case 'calcom_update_event_type': { if (operation === 'calcom_update_event_type') { - const eventTypeIdNum = toNumber(eventTypeIdParam) + const eventTypeIdNum = toNumber(selected_eventTypeIdParam) if (eventTypeIdNum !== undefined) result.eventTypeId = eventTypeIdNum } const lengthNum = toNumber(eventLength) if (lengthNum !== undefined) result.lengthInMinutes = lengthNum - const scheduleIdNum = toNumber(eventTypeScheduleId) + const scheduleIdNum = toNumber(selected_eventTypeScheduleId) if (scheduleIdNum !== undefined) result.scheduleId = scheduleIdNum const slotIntervalNum = toNumber(slotInterval) if (slotIntervalNum !== undefined) result.slotInterval = slotIntervalNum @@ -760,7 +760,7 @@ Return ONLY valid JSON - no explanations.`, case 'calcom_get_event_type': case 'calcom_delete_event_type': { - const eventTypeIdNum = toNumber(eventTypeIdParam) + const eventTypeIdNum = toNumber(selected_eventTypeIdParam) if (eventTypeIdNum !== undefined) result.eventTypeId = eventTypeIdNum } break @@ -782,7 +782,7 @@ Return ONLY valid JSON - no explanations.`, case 'calcom_update_schedule': case 'calcom_delete_schedule': { - const scheduleIdNum = toNumber(scheduleId) + const scheduleIdNum = toNumber(selected_scheduleId) if (scheduleIdNum !== undefined) result.scheduleId = scheduleIdNum } if (operation === 'calcom_update_schedule') { @@ -801,7 +801,7 @@ Return ONLY valid JSON - no explanations.`, case 'calcom_get_slots': { - const eventTypeIdNum = toNumber(eventTypeId) + const eventTypeIdNum = toNumber(selected_eventTypeId) const hasEventTypeId = eventTypeIdNum !== undefined const hasSlugAndUsername = Boolean(eventTypeSlug) && Boolean(username) @@ -837,7 +837,7 @@ Return ONLY valid JSON - no explanations.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Cal.com OAuth credential' }, - eventTypeId: { type: 'number', description: 'Event type ID' }, + selected_eventTypeId: { type: 'number', description: 'Event type ID' }, start: { type: 'string', description: 'Start time (ISO 8601)' }, end: { type: 'string', description: 'End time (ISO 8601)' }, attendeeName: { type: 'string', description: 'Attendee name' }, @@ -851,7 +851,7 @@ Return ONLY valid JSON - no explanations.`, cancellationReason: { type: 'string', description: 'Reason for cancellation' }, reschedulingReason: { type: 'string', description: 'Reason for rescheduling' }, bookingStatus: { type: 'string', description: 'Filter by booking status' }, - eventTypeIdParam: { type: 'number', description: 'Event type ID for get/update/delete' }, + selected_eventTypeIdParam: { type: 'number', description: 'Event type ID for get/update/delete' }, title: { type: 'string', description: 'Event type title' }, slug: { type: 'string', description: 'URL-friendly slug' }, eventLength: { type: 'number', description: 'Event duration in minutes' }, @@ -860,10 +860,10 @@ Return ONLY valid JSON - no explanations.`, minimumBookingNotice: { type: 'number', description: 'Minimum advance notice' }, beforeEventBuffer: { type: 'number', description: 'Buffer before event' }, afterEventBuffer: { type: 'number', description: 'Buffer after event' }, - eventTypeScheduleId: { type: 'number', description: 'Schedule ID for event type' }, + selected_eventTypeScheduleId: { type: 'number', description: 'Schedule ID for event type' }, disableGuests: { type: 'boolean', description: 'Disable guest additions' }, sortCreatedAt: { type: 'string', description: 'Sort order for event types' }, - scheduleId: { type: 'number', description: 'Schedule ID' }, + selected_scheduleId: { type: 'number', description: 'Schedule ID' }, name: { type: 'string', description: 'Schedule name' }, timeZone: { type: 'string', description: 'Time zone' }, isDefault: { type: 'boolean', description: 'Set as default schedule' }, diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 84574fdf017..1fc4d4cfdf6 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -102,7 +102,7 @@ export const ConfluenceBlock: BlockConfig = { id: 'pageId', title: 'Select Page', type: 'file-selector', - canonicalParamId: 'pageId', + canonicalParamId: 'selected_pageId', serviceId: 'confluence', selectorKey: 'confluence.pages', placeholder: 'Select Confluence page', @@ -126,7 +126,7 @@ export const ConfluenceBlock: BlockConfig = { id: 'manualPageId', title: 'Page ID', type: 'short-input', - canonicalParamId: 'pageId', + canonicalParamId: 'selected_pageId', placeholder: 'Enter Confluence page ID', mode: 'advanced', required: { @@ -147,7 +147,7 @@ export const ConfluenceBlock: BlockConfig = { id: 'spaceSelector', title: 'Space', type: 'project-selector', - canonicalParamId: 'spaceId', + canonicalParamId: 'selected_spaceId', serviceId: 'confluence', selectorKey: 'confluence.spaces', selectorAllowSearch: false, @@ -160,7 +160,7 @@ export const ConfluenceBlock: BlockConfig = { id: 'spaceId', title: 'Space ID', type: 'short-input', - canonicalParamId: 'spaceId', + canonicalParamId: 'selected_spaceId', placeholder: 'Enter Confluence space ID', mode: 'advanced', required: { field: 'operation', value: ['create', 'get_space'] }, @@ -317,7 +317,8 @@ export const ConfluenceBlock: BlockConfig = { params: (params) => { const { oauthCredential, - pageId, + selected_pageId, + selected_spaceId, operation, attachmentFile, attachmentFileName, @@ -325,7 +326,7 @@ export const ConfluenceBlock: BlockConfig = { ...rest } = params - const effectivePageId = pageId ? String(pageId).trim() : '' + const effectivePageId = selected_pageId ? String(selected_pageId).trim() : '' if (operation === 'upload_attachment') { return { @@ -335,6 +336,7 @@ export const ConfluenceBlock: BlockConfig = { file: attachmentFile, fileName: attachmentFileName, comment: attachmentComment, + ...(selected_spaceId ? { spaceId: selected_spaceId } : {}), ...rest, } } @@ -343,6 +345,7 @@ export const ConfluenceBlock: BlockConfig = { credential: oauthCredential, pageId: effectivePageId || undefined, operation, + ...(selected_spaceId ? { spaceId: selected_spaceId } : {}), ...rest, } }, @@ -352,8 +355,8 @@ export const ConfluenceBlock: BlockConfig = { operation: { type: 'string', description: 'Operation to perform' }, domain: { type: 'string', description: 'Confluence domain' }, oauthCredential: { type: 'string', description: 'Confluence access token' }, - pageId: { type: 'string', description: 'Page identifier (canonical param)' }, - spaceId: { type: 'string', description: 'Space identifier' }, + selected_pageId: { type: 'string', description: 'Page identifier (canonical param)' }, + selected_spaceId: { type: 'string', description: 'Space identifier' }, title: { type: 'string', description: 'Page title' }, content: { type: 'string', description: 'Page content' }, parentId: { type: 'string', description: 'Parent page identifier' }, @@ -541,7 +544,7 @@ export const ConfluenceV2Block: BlockConfig = { id: 'pageId', title: 'Select Page', type: 'file-selector', - canonicalParamId: 'pageId', + canonicalParamId: 'selected_pageId', serviceId: 'confluence', selectorKey: 'confluence.pages', placeholder: 'Select Confluence page', @@ -604,7 +607,7 @@ export const ConfluenceV2Block: BlockConfig = { id: 'manualPageId', title: 'Page ID', type: 'short-input', - canonicalParamId: 'pageId', + canonicalParamId: 'selected_pageId', placeholder: 'Enter Confluence page ID', mode: 'advanced', condition: { @@ -1236,7 +1239,7 @@ export const ConfluenceV2Block: BlockConfig = { params: (params) => { const { oauthCredential, - pageId, + selected_pageId, operation, attachmentFile, attachmentFileName, @@ -1266,7 +1269,7 @@ export const ConfluenceV2Block: BlockConfig = { } = params // Use canonical param (serializer already handles basic/advanced mode) - const effectivePageId = pageId ? String(pageId).trim() : '' + const effectivePageId = selected_pageId ? String(selected_pageId).trim() : '' if (operation === 'add_label') { return { @@ -1526,7 +1529,7 @@ export const ConfluenceV2Block: BlockConfig = { operation: { type: 'string', description: 'Operation to perform' }, domain: { type: 'string', description: 'Confluence domain' }, oauthCredential: { type: 'string', description: 'Confluence access token' }, - pageId: { type: 'string', description: 'Page identifier (canonical param)' }, + selected_pageId: { type: 'string', description: 'Page identifier (canonical param)' }, spaceId: { type: 'string', description: 'Space identifier' }, blogPostId: { type: 'string', description: 'Blog post identifier' }, versionNumber: { type: 'number', description: 'Page version number' }, diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 1fdece82317..670d9ca5da1 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -113,7 +113,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, id: 'datasetSelector', title: 'Dataset', type: 'project-selector', - canonicalParamId: 'datasetId', + canonicalParamId: 'selected_datasetId', serviceId: 'google-bigquery', selectorKey: 'bigquery.datasets', selectorAllowSearch: false, @@ -127,7 +127,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, id: 'datasetId', title: 'Dataset ID', type: 'short-input', - canonicalParamId: 'datasetId', + canonicalParamId: 'selected_datasetId', placeholder: 'Enter BigQuery dataset ID', mode: 'advanced', condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, @@ -138,7 +138,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, id: 'tableSelector', title: 'Table', type: 'file-selector', - canonicalParamId: 'tableId', + canonicalParamId: 'selected_tableId', serviceId: 'google-bigquery', selectorKey: 'bigquery.tables', selectorAllowSearch: false, @@ -152,7 +152,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, id: 'tableId', title: 'Table ID', type: 'short-input', - canonicalParamId: 'tableId', + canonicalParamId: 'selected_tableId', placeholder: 'Enter BigQuery table ID', mode: 'advanced', condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, @@ -227,10 +227,13 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, } }, params: (params) => { - const { oauthCredential, rows, maxResults, ...rest } = params + const { oauthCredential, rows, maxResults, selected_datasetId, selected_tableId, ...rest } = + params return { ...rest, oauthCredential, + datasetId: selected_datasetId, + tableId: selected_tableId, ...(rows && { rows: typeof rows === 'string' ? rows : JSON.stringify(rows) }), ...(maxResults !== undefined && maxResults !== '' && { maxResults: Number(maxResults) }), } @@ -249,8 +252,8 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, description: 'Default dataset for unqualified table names', }, location: { type: 'string', description: 'Processing location' }, - datasetId: { type: 'string', description: 'BigQuery dataset ID' }, - tableId: { type: 'string', description: 'BigQuery table ID' }, + selected_datasetId: { type: 'string', description: 'BigQuery dataset ID' }, + selected_tableId: { type: 'string', description: 'BigQuery table ID' }, rows: { type: 'string', description: 'JSON array of row objects to insert' }, skipInvalidRows: { type: 'boolean', description: 'Whether to skip invalid rows during insert' }, ignoreUnknownValues: { diff --git a/apps/sim/blocks/blocks/google_tasks.ts b/apps/sim/blocks/blocks/google_tasks.ts index 06b4b4ea4f6..7ae34666d40 100644 --- a/apps/sim/blocks/blocks/google_tasks.ts +++ b/apps/sim/blocks/blocks/google_tasks.ts @@ -56,7 +56,7 @@ export const GoogleTasksBlock: BlockConfig = { id: 'taskListSelector', title: 'Task List', type: 'project-selector', - canonicalParamId: 'taskListId', + canonicalParamId: 'selected_taskListId', serviceId: 'google-tasks', selectorKey: 'google.tasks.lists', selectorAllowSearch: false, @@ -69,7 +69,7 @@ export const GoogleTasksBlock: BlockConfig = { id: 'taskListId', title: 'Task List ID', type: 'short-input', - canonicalParamId: 'taskListId', + canonicalParamId: 'selected_taskListId', placeholder: 'Task list ID (leave empty for default list)', mode: 'advanced', condition: { field: 'operation', value: 'list_task_lists', not: true }, @@ -223,9 +223,19 @@ Return ONLY the timestamp - no explanations, no extra text.`, } }, params: (params) => { - const { oauthCredential, operation, showCompleted, maxResults, ...rest } = params + const { + oauthCredential, + operation, + showCompleted, + maxResults, + selected_taskListId, + ...rest + } = params - const processedParams: Record = { ...rest } + const processedParams: Record = { + ...rest, + taskListId: selected_taskListId, + } if (maxResults && typeof maxResults === 'string') { processedParams.maxResults = Number.parseInt(maxResults, 10) @@ -246,7 +256,7 @@ Return ONLY the timestamp - no explanations, no extra text.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Google Tasks access token' }, - taskListId: { type: 'string', description: 'Task list identifier' }, + selected_taskListId: { type: 'string', description: 'Task list identifier' }, title: { type: 'string', description: 'Task title' }, notes: { type: 'string', description: 'Task notes' }, due: { type: 'string', description: 'Task due date' }, diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 916f0b2bd1e..1b02d989b6d 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -110,7 +110,7 @@ export const JiraServiceManagementBlock: BlockConfig = { id: 'serviceDeskSelector', title: 'Service Desk', type: 'project-selector', - canonicalParamId: 'serviceDeskId', + canonicalParamId: 'selected_serviceDeskId', serviceId: 'jira', selectorKey: 'jsm.serviceDesks', selectorAllowSearch: false, @@ -149,7 +149,7 @@ export const JiraServiceManagementBlock: BlockConfig = { id: 'serviceDeskId', title: 'Service Desk ID', type: 'short-input', - canonicalParamId: 'serviceDeskId', + canonicalParamId: 'selected_serviceDeskId', placeholder: 'Enter service desk ID', mode: 'advanced', required: { @@ -184,7 +184,7 @@ export const JiraServiceManagementBlock: BlockConfig = { id: 'requestTypeSelector', title: 'Request Type', type: 'file-selector', - canonicalParamId: 'requestTypeId', + canonicalParamId: 'selected_requestTypeId', serviceId: 'jira', selectorKey: 'jsm.requestTypes', selectorAllowSearch: false, @@ -198,7 +198,7 @@ export const JiraServiceManagementBlock: BlockConfig = { id: 'requestTypeId', title: 'Request Type ID', type: 'short-input', - canonicalParamId: 'requestTypeId', + canonicalParamId: 'selected_requestTypeId', required: true, placeholder: 'Enter request type ID', mode: 'advanced', @@ -586,21 +586,21 @@ Return ONLY the comment text - no explanations.`, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'get_request_types': - if (!params.serviceDeskId) { + if (!params.selected_serviceDeskId) { throw new Error('Service Desk ID is required') } return { ...baseParams, - serviceDeskId: params.serviceDeskId, + serviceDeskId: params.selected_serviceDeskId, searchQuery: params.searchQuery, groupId: params.groupId, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'create_request': - if (!params.serviceDeskId) { + if (!params.selected_serviceDeskId) { throw new Error('Service Desk ID is required') } - if (!params.requestTypeId) { + if (!params.selected_requestTypeId) { throw new Error('Request Type ID is required') } if (!params.summary) { @@ -608,8 +608,8 @@ Return ONLY the comment text - no explanations.`, } return { ...baseParams, - serviceDeskId: params.serviceDeskId, - requestTypeId: params.requestTypeId, + serviceDeskId: params.selected_serviceDeskId, + requestTypeId: params.selected_requestTypeId, summary: params.summary, description: params.description, raiseOnBehalfOf: params.raiseOnBehalfOf, @@ -631,7 +631,7 @@ Return ONLY the comment text - no explanations.`, case 'get_requests': return { ...baseParams, - serviceDeskId: params.serviceDeskId, + serviceDeskId: params.selected_serviceDeskId, requestOwnership: params.requestOwnership, requestStatus: params.requestStatus, searchTerm: params.searchTerm, @@ -662,17 +662,17 @@ Return ONLY the comment text - no explanations.`, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'get_customers': - if (!params.serviceDeskId) { + if (!params.selected_serviceDeskId) { throw new Error('Service Desk ID is required') } return { ...baseParams, - serviceDeskId: params.serviceDeskId, + serviceDeskId: params.selected_serviceDeskId, query: params.customerQuery, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'add_customer': { - if (!params.serviceDeskId) { + if (!params.selected_serviceDeskId) { throw new Error('Service Desk ID is required') } if (!params.accountIds && !params.emails) { @@ -680,27 +680,27 @@ Return ONLY the comment text - no explanations.`, } return { ...baseParams, - serviceDeskId: params.serviceDeskId, + serviceDeskId: params.selected_serviceDeskId, accountIds: params.accountIds, emails: params.emails, } } case 'get_organizations': - if (!params.serviceDeskId) { + if (!params.selected_serviceDeskId) { throw new Error('Service Desk ID is required') } return { ...baseParams, - serviceDeskId: params.serviceDeskId, + serviceDeskId: params.selected_serviceDeskId, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'get_queues': - if (!params.serviceDeskId) { + if (!params.selected_serviceDeskId) { throw new Error('Service Desk ID is required') } return { ...baseParams, - serviceDeskId: params.serviceDeskId, + serviceDeskId: params.selected_serviceDeskId, includeCount: params.includeCount === 'true', limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } @@ -744,7 +744,7 @@ Return ONLY the comment text - no explanations.`, name: params.organizationName, } case 'add_organization': - if (!params.serviceDeskId) { + if (!params.selected_serviceDeskId) { throw new Error('Service Desk ID is required') } if (!params.organizationId) { @@ -752,7 +752,7 @@ Return ONLY the comment text - no explanations.`, } return { ...baseParams, - serviceDeskId: params.serviceDeskId, + serviceDeskId: params.selected_serviceDeskId, organizationId: params.organizationId, } case 'get_participants': @@ -802,16 +802,16 @@ Return ONLY the comment text - no explanations.`, decision: params.approvalDecision, } case 'get_request_type_fields': - if (!params.serviceDeskId) { + if (!params.selected_serviceDeskId) { throw new Error('Service Desk ID is required') } - if (!params.requestTypeId) { + if (!params.selected_requestTypeId) { throw new Error('Request Type ID is required') } return { ...baseParams, - serviceDeskId: params.serviceDeskId, - requestTypeId: params.requestTypeId, + serviceDeskId: params.selected_serviceDeskId, + requestTypeId: params.selected_requestTypeId, } default: return baseParams @@ -823,8 +823,8 @@ Return ONLY the comment text - no explanations.`, operation: { type: 'string', description: 'Operation to perform' }, domain: { type: 'string', description: 'Jira domain' }, oauthCredential: { type: 'string', description: 'Jira Service Management access token' }, - serviceDeskId: { type: 'string', description: 'Service desk ID' }, - requestTypeId: { type: 'string', description: 'Request type ID' }, + selected_serviceDeskId: { type: 'string', description: 'Service desk ID' }, + selected_requestTypeId: { type: 'string', description: 'Request type ID' }, issueIdOrKey: { type: 'string', description: 'Issue ID or key' }, summary: { type: 'string', description: 'Request summary' }, description: { type: 'string', description: 'Request description' }, diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index ab90c179236..a7d14cdcf1d 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -89,7 +89,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { id: 'planSelector', title: 'Plan', type: 'project-selector', - canonicalParamId: 'planId', + canonicalParamId: 'selected_planId', serviceId: 'microsoft-planner', selectorKey: 'microsoft.planner.plans', selectorAllowSearch: false, @@ -111,7 +111,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { id: 'planId', title: 'Plan ID', type: 'short-input', - canonicalParamId: 'planId', + canonicalParamId: 'selected_planId', placeholder: 'Enter the plan ID', mode: 'advanced', condition: { @@ -388,7 +388,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, oauthCredential, operation, groupId, - planId, + selected_planId, readTaskId, // Canonical param from taskSelector (basic) or manualReadTaskId (advanced) for read_task updateTaskId, // Task ID for update/delete operations bucketId, @@ -427,7 +427,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (operation === 'read_plan') { return { ...baseParams, - planId: planId?.trim(), + planId: selected_planId?.trim(), } } @@ -435,7 +435,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (operation === 'list_buckets') { return { ...baseParams, - planId: planId?.trim(), + planId: selected_planId?.trim(), } } @@ -451,7 +451,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (operation === 'create_bucket') { return { ...baseParams, - planId: planId?.trim(), + planId: selected_planId?.trim(), name: name?.trim(), } } @@ -484,8 +484,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (effectiveReadTaskId) { readParams.taskId = effectiveReadTaskId - } else if (planId?.trim()) { - readParams.planId = planId.trim() + } else if (selected_planId?.trim()) { + readParams.planId = selected_planId.trim() } return readParams @@ -495,7 +495,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (operation === 'create_task') { const createParams: MicrosoftPlannerBlockParams = { ...baseParams, - planId: planId?.trim(), + planId: selected_planId?.trim(), title: title?.trim(), } @@ -597,7 +597,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Microsoft account credential' }, groupId: { type: 'string', description: 'Microsoft 365 group ID' }, - planId: { type: 'string', description: 'Plan ID' }, + selected_planId: { type: 'string', description: 'Plan ID' }, readTaskId: { type: 'string', description: 'Task ID for read operation' }, updateTaskId: { type: 'string', description: 'Task ID for update/delete operations' }, bucketId: { type: 'string', description: 'Bucket ID' }, diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 2b7c563c655..0e12e605413 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -58,7 +58,7 @@ export const NotionBlock: BlockConfig = { id: 'pageSelector', title: 'Page', type: 'file-selector', - canonicalParamId: 'pageId', + canonicalParamId: 'selected_pageId', serviceId: 'notion', selectorKey: 'notion.pages', selectorAllowSearch: true, @@ -72,7 +72,7 @@ export const NotionBlock: BlockConfig = { id: 'pageId', title: 'Page ID', type: 'short-input', - canonicalParamId: 'pageId', + canonicalParamId: 'selected_pageId', placeholder: 'Enter Notion page ID', mode: 'advanced', condition: { field: 'operation', value: ['notion_read', 'notion_write'] }, @@ -83,7 +83,7 @@ export const NotionBlock: BlockConfig = { id: 'databaseSelector', title: 'Database', type: 'project-selector', - canonicalParamId: 'databaseId', + canonicalParamId: 'selected_databaseId', serviceId: 'notion', selectorKey: 'notion.databases', selectorAllowSearch: true, @@ -103,7 +103,7 @@ export const NotionBlock: BlockConfig = { id: 'databaseId', title: 'Database ID', type: 'short-input', - canonicalParamId: 'databaseId', + canonicalParamId: 'selected_databaseId', placeholder: 'Enter Notion database ID', mode: 'advanced', condition: { @@ -120,7 +120,7 @@ export const NotionBlock: BlockConfig = { id: 'parentSelector', title: 'Parent Page', type: 'file-selector', - canonicalParamId: 'parentId', + canonicalParamId: 'selected_parentId', serviceId: 'notion', selectorKey: 'notion.pages', selectorAllowSearch: true, @@ -140,7 +140,7 @@ export const NotionBlock: BlockConfig = { id: 'parentId', title: 'Parent Page ID', type: 'short-input', - canonicalParamId: 'parentId', + canonicalParamId: 'selected_parentId', placeholder: 'ID of parent page', mode: 'advanced', condition: { @@ -345,7 +345,17 @@ export const NotionBlock: BlockConfig = { } }, params: (params) => { - const { oauthCredential, operation, properties, filter, sorts, ...rest } = params + const { + oauthCredential, + operation, + properties, + filter, + sorts, + selected_pageId, + selected_databaseId, + selected_parentId, + ...rest + } = params // Parse properties from JSON string for create/add operations let parsedProperties @@ -395,6 +405,9 @@ export const NotionBlock: BlockConfig = { return { ...rest, oauthCredential, + ...(selected_pageId ? { pageId: selected_pageId } : {}), + ...(selected_databaseId ? { databaseId: selected_databaseId } : {}), + ...(selected_parentId ? { parentId: selected_parentId } : {}), ...(parsedProperties ? { properties: parsedProperties } : {}), ...(parsedFilter ? { filter: JSON.stringify(parsedFilter) } : {}), ...(parsedSorts ? { sorts: JSON.stringify(parsedSorts) } : {}), @@ -405,13 +418,13 @@ export const NotionBlock: BlockConfig = { inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Notion access token' }, - pageId: { type: 'string', description: 'Page identifier' }, + selected_pageId: { type: 'string', description: 'Page identifier' }, content: { type: 'string', description: 'Page content' }, // Create page inputs - parentId: { type: 'string', description: 'Parent page identifier' }, + selected_parentId: { type: 'string', description: 'Parent page identifier' }, title: { type: 'string', description: 'Page title' }, // Query database inputs - databaseId: { type: 'string', description: 'Database identifier' }, + selected_databaseId: { type: 'string', description: 'Database identifier' }, filter: { type: 'string', description: 'Filter criteria' }, sorts: { type: 'string', description: 'Sort criteria' }, pageSize: { type: 'number', description: 'Page size limit' }, diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index 55e5c331ff4..b1b70d5e4ea 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -100,7 +100,7 @@ export const PipedriveBlock: BlockConfig = { id: 'pipelineSelector', title: 'Pipeline', type: 'project-selector', - canonicalParamId: 'pipeline_id', + canonicalParamId: 'selected_pipeline_id', serviceId: 'pipedrive', selectorKey: 'pipedrive.pipelines', selectorAllowSearch: false, @@ -117,7 +117,7 @@ export const PipedriveBlock: BlockConfig = { id: 'pipeline_id', title: 'Pipeline ID', type: 'short-input', - canonicalParamId: 'pipeline_id', + canonicalParamId: 'selected_pipeline_id', placeholder: 'Enter pipeline ID', mode: 'advanced', condition: { @@ -769,12 +769,16 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n } }, params: (params) => { - const { oauthCredential, operation, ...rest } = params + const { oauthCredential, operation, selected_pipeline_id, ...rest } = params const cleanParams: Record = { oauthCredential, } + if (selected_pipeline_id !== undefined && selected_pipeline_id !== null && selected_pipeline_id !== '') { + cleanParams.pipeline_id = selected_pipeline_id + } + Object.entries(rest).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { cleanParams[key] = value @@ -794,7 +798,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n currency: { type: 'string', description: 'Currency code' }, person_id: { type: 'string', description: 'Person ID' }, org_id: { type: 'string', description: 'Organization ID' }, - pipeline_id: { type: 'string', description: 'Pipeline ID' }, + selected_pipeline_id: { type: 'string', description: 'Pipeline ID' }, stage_id: { type: 'string', description: 'Stage ID' }, status: { type: 'string', description: 'Status' }, expected_close_date: { type: 'string', description: 'Expected close date' }, diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index dcc02839d36..9f2de4ab174 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -116,7 +116,7 @@ export const SharepointBlock: BlockConfig = { id: 'listSelector', title: 'List', type: 'file-selector', - canonicalParamId: 'listId', + canonicalParamId: 'selected_listId', serviceId: 'sharepoint', selectorKey: 'sharepoint.lists', selectorAllowSearch: false, @@ -129,7 +129,7 @@ export const SharepointBlock: BlockConfig = { id: 'listId', title: 'List ID', type: 'short-input', - canonicalParamId: 'listId', + canonicalParamId: 'selected_listId', placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.', mode: 'advanced', condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] }, @@ -440,6 +440,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, includeItems, files, // canonical param from uploadFiles (basic) or files (advanced) columnDefinitions, + selected_listId, ...others } = rest as any @@ -471,7 +472,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, try { logger.info('SharepointBlock list item param check', { siteId: effectiveSiteId || undefined, - listId: (others as any)?.listId, + listId: selected_listId, listTitle: (others as any)?.listTitle, itemId: sanitizedItemId, hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object', @@ -491,6 +492,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined, mimeType: mimeType, ...others, + ...(selected_listId ? { listId: selected_listId } : {}), itemId: sanitizedItemId, listItemFields: parsedItemFields, includeColumns: coerceBoolean(includeColumns), @@ -525,7 +527,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, listDisplayName: { type: 'string', description: 'List display name' }, listDescription: { type: 'string', description: 'List description' }, listTemplate: { type: 'string', description: 'List template' }, - listId: { type: 'string', description: 'List ID' }, + selected_listId: { type: 'string', description: 'List ID' }, listTitle: { type: 'string', description: 'List title' }, includeColumns: { type: 'boolean', description: 'Include columns in response' }, includeItems: { type: 'boolean', description: 'Include items in response' }, diff --git a/apps/sim/blocks/blocks/trello.ts b/apps/sim/blocks/blocks/trello.ts index bff115ebdcf..ac64a60dd64 100644 --- a/apps/sim/blocks/blocks/trello.ts +++ b/apps/sim/blocks/blocks/trello.ts @@ -62,7 +62,7 @@ export const TrelloBlock: BlockConfig = { id: 'boardSelector', title: 'Board', type: 'project-selector', - canonicalParamId: 'boardId', + canonicalParamId: 'selected_boardId', serviceId: 'trello', selectorKey: 'trello.boards', selectorAllowSearch: false, @@ -87,7 +87,7 @@ export const TrelloBlock: BlockConfig = { id: 'boardId', title: 'Board ID', type: 'short-input', - canonicalParamId: 'boardId', + canonicalParamId: 'selected_boardId', placeholder: 'Enter board ID', mode: 'advanced', condition: { @@ -374,9 +374,9 @@ Return ONLY the date/timestamp string - no explanations, no quotes, no extra tex } }, params: (params) => { - const { operation, limit, closed, dueComplete, ...rest } = params + const { operation, limit, closed, dueComplete, selected_boardId, ...rest } = params - const result: Record = { ...rest } + const result: Record = { ...rest, boardId: selected_boardId } if (limit && operation === 'trello_get_actions') { result.limit = Number.parseInt(limit, 10) @@ -409,7 +409,7 @@ Return ONLY the date/timestamp string - no explanations, no quotes, no extra tex inputs: { operation: { type: 'string', description: 'Trello operation to perform' }, oauthCredential: { type: 'string', description: 'Trello OAuth credential' }, - boardId: { type: 'string', description: 'Board ID' }, + selected_boardId: { type: 'string', description: 'Board ID' }, listId: { type: 'string', description: 'List ID' }, cardId: { type: 'string', description: 'Card ID' }, name: { type: 'string', description: 'Card name/title' }, diff --git a/apps/sim/blocks/blocks/zoom.ts b/apps/sim/blocks/blocks/zoom.ts index 23b7dc70a84..5cd09656450 100644 --- a/apps/sim/blocks/blocks/zoom.ts +++ b/apps/sim/blocks/blocks/zoom.ts @@ -82,7 +82,7 @@ export const ZoomBlock: BlockConfig = { id: 'meetingSelector', title: 'Meeting', type: 'project-selector', - canonicalParamId: 'meetingId', + canonicalParamId: 'selected_meetingId', serviceId: 'zoom', selectorKey: 'zoom.meetings', selectorAllowSearch: true, @@ -107,7 +107,7 @@ export const ZoomBlock: BlockConfig = { id: 'meetingId', title: 'Meeting ID', type: 'short-input', - canonicalParamId: 'meetingId', + canonicalParamId: 'selected_meetingId', placeholder: 'Enter meeting ID', mode: 'advanced', required: true, @@ -512,22 +512,22 @@ Return ONLY the date string - no explanations, no quotes, no extra text.`, } case 'zoom_get_meeting': - if (!params.meetingId?.trim()) { + if (!params.selected_meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.meetingId.trim(), + meetingId: params.selected_meetingId.trim(), occurrenceId: params.occurrenceId, } case 'zoom_update_meeting': - if (!params.meetingId?.trim()) { + if (!params.selected_meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.meetingId.trim(), + meetingId: params.selected_meetingId.trim(), topic: params.topicUpdate, type: params.type ? Number(params.type) : undefined, startTime: params.startTime, @@ -544,23 +544,23 @@ Return ONLY the date string - no explanations, no quotes, no extra text.`, } case 'zoom_delete_meeting': - if (!params.meetingId?.trim()) { + if (!params.selected_meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.meetingId.trim(), + meetingId: params.selected_meetingId.trim(), occurrenceId: params.occurrenceId, cancelMeetingReminder: params.cancelMeetingReminder, } case 'zoom_get_meeting_invitation': - if (!params.meetingId?.trim()) { + if (!params.selected_meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.meetingId.trim(), + meetingId: params.selected_meetingId.trim(), } case 'zoom_list_recordings': @@ -577,32 +577,32 @@ Return ONLY the date string - no explanations, no quotes, no extra text.`, } case 'zoom_get_meeting_recordings': - if (!params.meetingId?.trim()) { + if (!params.selected_meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.meetingId.trim(), + meetingId: params.selected_meetingId.trim(), } case 'zoom_delete_recording': - if (!params.meetingId?.trim()) { + if (!params.selected_meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.meetingId.trim(), + meetingId: params.selected_meetingId.trim(), recordingId: params.recordingId, action: params.deleteAction, } case 'zoom_list_past_participants': - if (!params.meetingId?.trim()) { + if (!params.selected_meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.meetingId.trim(), + meetingId: params.selected_meetingId.trim(), pageSize: params.pageSize ? Number(params.pageSize) : undefined, nextPageToken: params.nextPageToken, } @@ -617,7 +617,7 @@ Return ONLY the date string - no explanations, no quotes, no extra text.`, operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Zoom access token' }, userId: { type: 'string', description: 'User ID or email (use "me" for authenticated user)' }, - meetingId: { type: 'string', description: 'Meeting ID' }, + selected_meetingId: { type: 'string', description: 'Meeting ID' }, topic: { type: 'string', description: 'Meeting topic' }, topicUpdate: { type: 'string', description: 'Meeting topic for update' }, type: { type: 'string', description: 'Meeting type' }, From 4c6c9e8e49609f4268404383c5404efdf06f0af5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:42:28 -0800 Subject: [PATCH 11/32] fix(selectors): rename pre-existing driveId and files canonicalParamIds in SharePoint Apply the same selected_ prefix convention to the pre-existing SharePoint driveId and files canonical params that clashed with their subBlock IDs. --- apps/sim/blocks/blocks/sharepoint.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 9f2de4ab174..d3af93cc2c7 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -348,7 +348,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, title: 'Document Library ID', type: 'short-input', placeholder: 'Enter document library (drive) ID', - canonicalParamId: 'driveId', + canonicalParamId: 'selected_driveId', condition: { field: 'operation', value: 'upload_file' }, mode: 'advanced', }, @@ -374,7 +374,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, id: 'uploadFiles', title: 'Files', type: 'file-upload', - canonicalParamId: 'files', + canonicalParamId: 'selected_files', placeholder: 'Upload files to SharePoint', condition: { field: 'operation', value: 'upload_file' }, mode: 'basic', @@ -386,7 +386,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, id: 'files', title: 'Files', type: 'short-input', - canonicalParamId: 'files', + canonicalParamId: 'selected_files', placeholder: 'Reference files from previous blocks', condition: { field: 'operation', value: 'upload_file' }, mode: 'advanced', @@ -438,7 +438,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, listItemFields, // canonical param includeColumns, includeItems, - files, // canonical param from uploadFiles (basic) or files (advanced) + selected_files, // canonical param from uploadFiles (basic) or files (advanced) + selected_driveId, // canonical param from driveId columnDefinitions, selected_listId, ...others @@ -485,7 +486,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, } // Handle file upload files parameter using canonical param - const normalizedFiles = normalizeFileInput(files) + const normalizedFiles = normalizeFileInput(selected_files) const baseParams: Record = { oauthCredential, siteId: effectiveSiteId || undefined, @@ -493,6 +494,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, mimeType: mimeType, ...others, ...(selected_listId ? { listId: selected_listId } : {}), + ...(selected_driveId ? { driveId: selected_driveId } : {}), itemId: sanitizedItemId, listItemFields: parsedItemFields, includeColumns: coerceBoolean(includeColumns), @@ -533,10 +535,10 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, includeItems: { type: 'boolean', description: 'Include items in response' }, itemId: { type: 'string', description: 'List item ID (canonical param)' }, listItemFields: { type: 'string', description: 'List item fields (canonical param)' }, - driveId: { type: 'string', description: 'Document library (drive) ID (canonical param)' }, + selected_driveId: { type: 'string', description: 'Document library (drive) ID (canonical param)' }, folderPath: { type: 'string', description: 'Folder path for file upload' }, fileName: { type: 'string', description: 'File name override' }, - files: { type: 'array', description: 'Files to upload (canonical param)' }, + selected_files: { type: 'array', description: 'Files to upload (canonical param)' }, }, outputs: { sites: { From a56451a6ea207dee91bb43ffffee30ab6d7b593e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 19:52:14 -0800 Subject: [PATCH 12/32] style: format long lines in calcom, pipedrive, and sharepoint blocks Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/calcom.ts | 5 ++++- apps/sim/blocks/blocks/pipedrive.ts | 6 +++++- apps/sim/blocks/blocks/sharepoint.ts | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/sim/blocks/blocks/calcom.ts b/apps/sim/blocks/blocks/calcom.ts index 45789181ae4..ed6cf2fc506 100644 --- a/apps/sim/blocks/blocks/calcom.ts +++ b/apps/sim/blocks/blocks/calcom.ts @@ -851,7 +851,10 @@ Return ONLY valid JSON - no explanations.`, cancellationReason: { type: 'string', description: 'Reason for cancellation' }, reschedulingReason: { type: 'string', description: 'Reason for rescheduling' }, bookingStatus: { type: 'string', description: 'Filter by booking status' }, - selected_eventTypeIdParam: { type: 'number', description: 'Event type ID for get/update/delete' }, + selected_eventTypeIdParam: { + type: 'number', + description: 'Event type ID for get/update/delete', + }, title: { type: 'string', description: 'Event type title' }, slug: { type: 'string', description: 'URL-friendly slug' }, eventLength: { type: 'number', description: 'Event duration in minutes' }, diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index b1b70d5e4ea..ef1cd56fcd6 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -775,7 +775,11 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n oauthCredential, } - if (selected_pipeline_id !== undefined && selected_pipeline_id !== null && selected_pipeline_id !== '') { + if ( + selected_pipeline_id !== undefined && + selected_pipeline_id !== null && + selected_pipeline_id !== '' + ) { cleanParams.pipeline_id = selected_pipeline_id } diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index d3af93cc2c7..be781fcfd39 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -535,7 +535,10 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, includeItems: { type: 'boolean', description: 'Include items in response' }, itemId: { type: 'string', description: 'List item ID (canonical param)' }, listItemFields: { type: 'string', description: 'List item fields (canonical param)' }, - selected_driveId: { type: 'string', description: 'Document library (drive) ID (canonical param)' }, + selected_driveId: { + type: 'string', + description: 'Document library (drive) ID (canonical param)', + }, folderPath: { type: 'string', description: 'Folder path for file upload' }, fileName: { type: 'string', description: 'File name override' }, selected_files: { type: 'array', description: 'Files to upload (canonical param)' }, From fe812d3c781009b3a104f918f6a862ba6b0a59ee Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 20:09:10 -0800 Subject: [PATCH 13/32] fix(selectors): resolve cascading context for selected_ canonical params and normalize Asana response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip `selected_` prefix from canonical param IDs when mapping to SelectorContext fields so cascading selectors (Airtable base→table, BigQuery dataset→table, JSM serviceDesk→requestType) correctly propagate parent values. Normalize Asana workspaces route to return `{ id, name }` instead of `{ gid, name }` for consistency with all other selector routes. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/asana/workspaces/route.ts | 2 +- .../components/sub-block/hooks/use-selector-setup.ts | 9 +++++++-- apps/sim/hooks/selectors/registry.ts | 8 ++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/api/tools/asana/workspaces/route.ts b/apps/sim/app/api/tools/asana/workspaces/route.ts index 3eeefbedeaf..e00e60e263c 100644 --- a/apps/sim/app/api/tools/asana/workspaces/route.ts +++ b/apps/sim/app/api/tools/asana/workspaces/route.ts @@ -64,7 +64,7 @@ export async function POST(request: Request) { const data = await response.json() const workspaces = (data.data || []).map((workspace: { gid: string; name: string }) => ({ - gid: workspace.gid, + id: workspace.gid, name: workspace.name, })) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts index 6401273d142..f7688cfce38 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts @@ -50,8 +50,13 @@ export function useSelectorSetup( if (canonicalParamId === 'oauthCredential') { context.credentialId = strValue - } else if (canonicalParamId in CONTEXT_FIELD_SET) { - ;(context as Record)[canonicalParamId] = strValue + } else { + const contextField = canonicalParamId.startsWith('selected_') + ? canonicalParamId.slice('selected_'.length) + : canonicalParamId + if (contextField in CONTEXT_FIELD_SET) { + ;(context as Record)[contextField] = strValue + } } } diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 9b997aa9781..391b38f1d4e 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -12,7 +12,7 @@ const SELECTOR_STALE = 60 * 1000 type AirtableBase = { id: string; name: string } type AirtableTable = { id: string; name: string } -type AsanaWorkspace = { gid: string; name: string } +type AsanaWorkspace = { id: string; name: string } type AttioObject = { id: string; name: string } type AttioList = { id: string; name: string } type BigQueryDataset = { @@ -161,7 +161,7 @@ const registry: Record = { '/api/tools/asana/workspaces', { method: 'POST', body } ) - return (data.workspaces || []).map((ws) => ({ id: ws.gid, label: ws.name })) + return (data.workspaces || []).map((ws) => ({ id: ws.id, label: ws.name })) }, fetchById: async ({ context, detailId }: SelectorQueryArgs) => { if (!detailId) return null @@ -171,9 +171,9 @@ const registry: Record = { '/api/tools/asana/workspaces', { method: 'POST', body } ) - const ws = (data.workspaces || []).find((w) => w.gid === detailId) ?? null + const ws = (data.workspaces || []).find((w) => w.id === detailId) ?? null if (!ws) return null - return { id: ws.gid, label: ws.name } + return { id: ws.id, label: ws.name } }, }, 'attio.objects': { From 20b621d503e9dfc1e272217025a81c27e32f570b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 21:47:59 -0800 Subject: [PATCH 14/32] fix(selectors): replace hacky prefix stripping with explicit CANONICAL_TO_CONTEXT mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace CONTEXT_FIELD_SET (Record) with CANONICAL_TO_CONTEXT (Record) that explicitly maps canonical param IDs to their SelectorContext field names. This properly handles the selected_ prefix aliases (e.g. selected_baseId → baseId) without string manipulation, and removes the unsafe Record cast. Co-Authored-By: Claude Opus 4.6 --- .../sub-block/hooks/use-selector-setup.ts | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts index f7688cfce38..c62e396fe74 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts @@ -51,11 +51,9 @@ export function useSelectorSetup( if (canonicalParamId === 'oauthCredential') { context.credentialId = strValue } else { - const contextField = canonicalParamId.startsWith('selected_') - ? canonicalParamId.slice('selected_'.length) - : canonicalParamId - if (contextField in CONTEXT_FIELD_SET) { - ;(context as Record)[contextField] = strValue + const contextField = CANONICAL_TO_CONTEXT[canonicalParamId] + if (contextField) { + context[contextField] = strValue } } } @@ -72,18 +70,29 @@ export function useSelectorSetup( } } -const CONTEXT_FIELD_SET: Record = { - credentialId: true, - domain: true, - teamId: true, - projectId: true, - knowledgeBaseId: true, - planId: true, - siteId: true, - collectionId: true, - spreadsheetId: true, - fileId: true, - baseId: true, - datasetId: true, - serviceDeskId: true, +/** + * Maps canonical param IDs to SelectorContext field names. + * + * Most canonical param IDs match their SelectorContext field directly (e.g. `siteId` → `siteId`). + * Aliased entries handle cases where `canonicalParamId` was prefixed with `selected_` to avoid + * clashing with a subBlock `id` of the same name. + */ +const CANONICAL_TO_CONTEXT: Record = { + credentialId: 'credentialId', + domain: 'domain', + teamId: 'teamId', + projectId: 'projectId', + knowledgeBaseId: 'knowledgeBaseId', + planId: 'planId', + siteId: 'siteId', + collectionId: 'collectionId', + spreadsheetId: 'spreadsheetId', + fileId: 'fileId', + baseId: 'baseId', + datasetId: 'datasetId', + serviceDeskId: 'serviceDeskId', + selected_baseId: 'baseId', + selected_datasetId: 'datasetId', + selected_serviceDeskId: 'serviceDeskId', + selected_planId: 'planId', } From 8d231ce44b11036d580e6bdfbf3dd37c0c50b7eb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 22:07:11 -0800 Subject: [PATCH 15/32] refactor(selectors): remove unnecessary selected_ prefix from canonicalParamIds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The selected_ prefix was added to avoid a perceived clash between canonicalParamId and subBlock id values, but this clash does not actually cause any issues — pre-existing blocks on main (Google Sheets, Webflow, SharePoint) already use matching values successfully. Remove the prefix from all 14 blocks, revert use-selector-setup.ts to the simple CONTEXT_FIELD_SET pattern, and simplify tools.config.params functions that were only remapping the prefix back. Co-Authored-By: Claude Opus 4.6 --- .../sub-block/hooks/use-selector-setup.ts | 46 ++++++---------- apps/sim/blocks/blocks/airtable.ts | 17 +++--- apps/sim/blocks/blocks/asana.ts | 20 +++---- apps/sim/blocks/blocks/attio.ts | 16 +++--- apps/sim/blocks/blocks/calcom.ts | 44 +++++++-------- apps/sim/blocks/blocks/confluence.ts | 33 ++++++------ apps/sim/blocks/blocks/google_bigquery.ts | 17 +++--- apps/sim/blocks/blocks/google_tasks.ts | 16 ++---- .../blocks/blocks/jira_service_management.ts | 54 +++++++++---------- apps/sim/blocks/blocks/microsoft_planner.ts | 20 +++---- apps/sim/blocks/blocks/notion.ts | 33 ++++-------- apps/sim/blocks/blocks/pipedrive.ts | 16 ++---- apps/sim/blocks/blocks/sharepoint.ts | 32 +++++------ apps/sim/blocks/blocks/trello.ts | 10 ++-- apps/sim/blocks/blocks/zoom.ts | 34 ++++++------ 15 files changed, 179 insertions(+), 229 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts index c62e396fe74..6401273d142 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts @@ -50,11 +50,8 @@ export function useSelectorSetup( if (canonicalParamId === 'oauthCredential') { context.credentialId = strValue - } else { - const contextField = CANONICAL_TO_CONTEXT[canonicalParamId] - if (contextField) { - context[contextField] = strValue - } + } else if (canonicalParamId in CONTEXT_FIELD_SET) { + ;(context as Record)[canonicalParamId] = strValue } } @@ -70,29 +67,18 @@ export function useSelectorSetup( } } -/** - * Maps canonical param IDs to SelectorContext field names. - * - * Most canonical param IDs match their SelectorContext field directly (e.g. `siteId` → `siteId`). - * Aliased entries handle cases where `canonicalParamId` was prefixed with `selected_` to avoid - * clashing with a subBlock `id` of the same name. - */ -const CANONICAL_TO_CONTEXT: Record = { - credentialId: 'credentialId', - domain: 'domain', - teamId: 'teamId', - projectId: 'projectId', - knowledgeBaseId: 'knowledgeBaseId', - planId: 'planId', - siteId: 'siteId', - collectionId: 'collectionId', - spreadsheetId: 'spreadsheetId', - fileId: 'fileId', - baseId: 'baseId', - datasetId: 'datasetId', - serviceDeskId: 'serviceDeskId', - selected_baseId: 'baseId', - selected_datasetId: 'datasetId', - selected_serviceDeskId: 'serviceDeskId', - selected_planId: 'planId', +const CONTEXT_FIELD_SET: Record = { + credentialId: true, + domain: true, + teamId: true, + projectId: true, + knowledgeBaseId: true, + planId: true, + siteId: true, + collectionId: true, + spreadsheetId: true, + fileId: true, + baseId: true, + datasetId: true, + serviceDeskId: true, } diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index b5afa6446bf..f2b38efb309 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -61,7 +61,7 @@ export const AirtableBlock: BlockConfig = { id: 'baseSelector', title: 'Base', type: 'project-selector', - canonicalParamId: 'selected_baseId', + canonicalParamId: 'baseId', serviceId: 'airtable', selectorKey: 'airtable.bases', selectorAllowSearch: false, @@ -75,7 +75,7 @@ export const AirtableBlock: BlockConfig = { id: 'baseId', title: 'Base ID', type: 'short-input', - canonicalParamId: 'selected_baseId', + canonicalParamId: 'baseId', placeholder: 'Enter your base ID (e.g., appXXXXXXXXXXXXXX)', mode: 'advanced', condition: { field: 'operation', value: 'listBases', not: true }, @@ -85,7 +85,7 @@ export const AirtableBlock: BlockConfig = { id: 'tableSelector', title: 'Table', type: 'file-selector', - canonicalParamId: 'selected_tableId', + canonicalParamId: 'tableId', serviceId: 'airtable', selectorKey: 'airtable.tables', selectorAllowSearch: false, @@ -99,7 +99,7 @@ export const AirtableBlock: BlockConfig = { id: 'tableId', title: 'Table ID', type: 'short-input', - canonicalParamId: 'selected_tableId', + canonicalParamId: 'tableId', placeholder: 'Enter table ID (e.g., tblXXXXXXXXXXXXXX)', mode: 'advanced', condition: { field: 'operation', value: ['listBases', 'listTables'], not: true }, @@ -274,8 +274,7 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, } }, params: (params) => { - const { oauthCredential, records, fields, selected_baseId, selected_tableId, ...rest } = - params + const { oauthCredential, records, fields, ...rest } = params let parsedRecords: any | undefined let parsedFields: any | undefined @@ -294,8 +293,6 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, // Construct parameters based on operation const baseParams = { credential: oauthCredential, - baseId: selected_baseId, - tableId: selected_tableId, ...rest, } @@ -314,8 +311,8 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Airtable access token' }, - selected_baseId: { type: 'string', description: 'Airtable base identifier' }, - selected_tableId: { type: 'string', description: 'Airtable table identifier' }, + baseId: { type: 'string', description: 'Airtable base identifier' }, + tableId: { type: 'string', description: 'Airtable table identifier' }, // Conditional inputs recordId: { type: 'string', description: 'Record identifier' }, // Required for get/update maxRecords: { type: 'number', description: 'Maximum records to return' }, // Optional for list diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index 96ee21cbd41..25418864593 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -52,7 +52,7 @@ export const AsanaBlock: BlockConfig = { id: 'workspaceSelector', title: 'Workspace', type: 'project-selector', - canonicalParamId: 'selected_workspace', + canonicalParamId: 'workspace', serviceId: 'asana', selectorKey: 'asana.workspaces', selectorAllowSearch: false, @@ -69,7 +69,7 @@ export const AsanaBlock: BlockConfig = { id: 'workspace', title: 'Workspace GID', type: 'short-input', - canonicalParamId: 'selected_workspace', + canonicalParamId: 'workspace', required: true, placeholder: 'Enter Asana workspace GID', mode: 'advanced', @@ -104,7 +104,7 @@ export const AsanaBlock: BlockConfig = { id: 'getTasksWorkspaceSelector', title: 'Workspace', type: 'project-selector', - canonicalParamId: 'selected_getTasks_workspace', + canonicalParamId: 'getTasks_workspace', serviceId: 'asana', selectorKey: 'asana.workspaces', selectorAllowSearch: false, @@ -120,7 +120,7 @@ export const AsanaBlock: BlockConfig = { id: 'getTasks_workspace', title: 'Workspace GID', type: 'short-input', - canonicalParamId: 'selected_getTasks_workspace', + canonicalParamId: 'getTasks_workspace', placeholder: 'Enter workspace GID', mode: 'advanced', condition: { @@ -280,14 +280,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n return { ...baseParams, taskGid: params.taskGid, - workspace: params.selected_getTasks_workspace, + workspace: params.getTasks_workspace, project: params.getTasks_project, limit: params.getTasks_limit ? Number(params.getTasks_limit) : undefined, } case 'create_task': return { ...baseParams, - workspace: params.selected_workspace, + workspace: params.workspace, name: params.name, notes: params.notes, assignee: params.assignee, @@ -306,12 +306,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n case 'get_projects': return { ...baseParams, - workspace: params.selected_workspace, + workspace: params.workspace, } case 'search_tasks': return { ...baseParams, - workspace: params.selected_workspace, + workspace: params.workspace, text: params.searchText, assignee: params.assignee, projects: projectsArray, @@ -332,9 +332,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Asana OAuth credential' }, - selected_workspace: { type: 'string', description: 'Workspace GID' }, + workspace: { type: 'string', description: 'Workspace GID' }, taskGid: { type: 'string', description: 'Task GID' }, - selected_getTasks_workspace: { type: 'string', description: 'Workspace GID for getting tasks' }, + getTasks_workspace: { type: 'string', description: 'Workspace GID for getting tasks' }, getTasks_project: { type: 'string', description: 'Project GID filter for getting tasks' }, getTasks_limit: { type: 'string', description: 'Limit for getting tasks' }, name: { type: 'string', description: 'Task name' }, diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 49719a5e5ac..aebea95d363 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -90,7 +90,7 @@ export const AttioBlock: BlockConfig = { id: 'objectTypeSelector', title: 'Object Type', type: 'project-selector', - canonicalParamId: 'selected_objectType', + canonicalParamId: 'objectType', serviceId: 'attio', selectorKey: 'attio.objects', selectorAllowSearch: false, @@ -124,7 +124,7 @@ export const AttioBlock: BlockConfig = { id: 'objectType', title: 'Object Type', type: 'short-input', - canonicalParamId: 'selected_objectType', + canonicalParamId: 'objectType', placeholder: 'e.g. people, companies', mode: 'advanced', condition: { @@ -564,7 +564,7 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. id: 'listSelector', title: 'List', type: 'project-selector', - canonicalParamId: 'selected_listIdOrSlug', + canonicalParamId: 'listIdOrSlug', serviceId: 'attio', selectorKey: 'attio.lists', selectorAllowSearch: false, @@ -600,7 +600,7 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. id: 'listIdOrSlug', title: 'List ID or Slug', type: 'short-input', - canonicalParamId: 'selected_listIdOrSlug', + canonicalParamId: 'listIdOrSlug', placeholder: 'Enter the list ID or slug', mode: 'advanced', condition: { @@ -1116,7 +1116,7 @@ workspace-member.created } // Record params - if (params.selected_objectType) cleanParams.objectType = params.selected_objectType + if (params.objectType) cleanParams.objectType = params.objectType if (params.recordId) cleanParams.recordId = params.recordId if (params.matchingAttribute) cleanParams.matchingAttribute = params.matchingAttribute if (params.values) cleanParams.values = params.values @@ -1157,7 +1157,7 @@ workspace-member.created if (params.objectPluralNoun) cleanParams.pluralNoun = params.objectPluralNoun // List params - if (params.selected_listIdOrSlug) cleanParams.list = params.selected_listIdOrSlug + if (params.listIdOrSlug) cleanParams.list = params.listIdOrSlug if (params.listName) cleanParams.name = params.listName if (params.listParentObject) cleanParams.parentObject = params.listParentObject if (params.listApiSlug) cleanParams.apiSlug = params.listApiSlug @@ -1209,7 +1209,7 @@ workspace-member.created inputs: { operation: { type: 'string', description: 'The operation to perform' }, oauthCredential: { type: 'string', description: 'Attio OAuth credential' }, - selected_objectType: { type: 'string', description: 'Object type slug' }, + objectType: { type: 'string', description: 'Object type slug' }, recordId: { type: 'string', description: 'Record ID' }, matchingAttribute: { type: 'string', description: 'Matching attribute for upsert' }, values: { type: 'json', description: 'Record attribute values' }, @@ -1235,7 +1235,7 @@ workspace-member.created objectApiSlug: { type: 'string', description: 'Object API slug' }, objectSingularNoun: { type: 'string', description: 'Object singular name' }, objectPluralNoun: { type: 'string', description: 'Object plural name' }, - selected_listIdOrSlug: { type: 'string', description: 'List ID or slug' }, + listIdOrSlug: { type: 'string', description: 'List ID or slug' }, listName: { type: 'string', description: 'List name' }, listParentObject: { type: 'string', description: 'List parent object' }, listApiSlug: { type: 'string', description: 'List API slug' }, diff --git a/apps/sim/blocks/blocks/calcom.ts b/apps/sim/blocks/blocks/calcom.ts index ed6cf2fc506..0a32aa854dc 100644 --- a/apps/sim/blocks/blocks/calcom.ts +++ b/apps/sim/blocks/blocks/calcom.ts @@ -69,7 +69,7 @@ export const CalComBlock: BlockConfig = { id: 'eventTypeSelector', title: 'Event Type', type: 'project-selector', - canonicalParamId: 'selected_eventTypeId', + canonicalParamId: 'eventTypeId', serviceId: 'calcom', selectorKey: 'calcom.eventTypes', selectorAllowSearch: false, @@ -86,7 +86,7 @@ export const CalComBlock: BlockConfig = { id: 'eventTypeId', title: 'Event Type ID', type: 'short-input', - canonicalParamId: 'selected_eventTypeId', + canonicalParamId: 'eventTypeId', placeholder: 'Enter event type ID (number)', mode: 'advanced', condition: { @@ -284,7 +284,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'eventTypeParamSelector', title: 'Event Type', type: 'project-selector', - canonicalParamId: 'selected_eventTypeIdParam', + canonicalParamId: 'eventTypeIdParam', serviceId: 'calcom', selectorKey: 'calcom.eventTypes', selectorAllowSearch: false, @@ -304,7 +304,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'eventTypeIdParam', title: 'Event Type ID', type: 'short-input', - canonicalParamId: 'selected_eventTypeIdParam', + canonicalParamId: 'eventTypeIdParam', placeholder: 'Enter event type ID', mode: 'advanced', condition: { @@ -409,7 +409,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'eventTypeScheduleSelector', title: 'Schedule', type: 'project-selector', - canonicalParamId: 'selected_eventTypeScheduleId', + canonicalParamId: 'eventTypeScheduleId', serviceId: 'calcom', selectorKey: 'calcom.schedules', selectorAllowSearch: false, @@ -425,7 +425,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'eventTypeScheduleId', title: 'Schedule ID', type: 'short-input', - canonicalParamId: 'selected_eventTypeScheduleId', + canonicalParamId: 'eventTypeScheduleId', placeholder: 'Assign to a specific schedule', condition: { field: 'operation', @@ -450,7 +450,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'scheduleSelector', title: 'Schedule', type: 'project-selector', - canonicalParamId: 'selected_scheduleId', + canonicalParamId: 'scheduleId', serviceId: 'calcom', selectorKey: 'calcom.schedules', selectorAllowSearch: false, @@ -470,7 +470,7 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, id: 'scheduleId', title: 'Schedule ID', type: 'short-input', - canonicalParamId: 'selected_scheduleId', + canonicalParamId: 'scheduleId', placeholder: 'Enter schedule ID', mode: 'advanced', condition: { @@ -659,17 +659,17 @@ Return ONLY valid JSON - no explanations.`, reschedulingReason, bookingStatus, lengthInMinutes, - selected_eventTypeIdParam, - selected_eventTypeId, + eventTypeIdParam, + eventTypeId, eventLength, - selected_eventTypeScheduleId, + eventTypeScheduleId, slotInterval, minimumBookingNotice, beforeEventBuffer, afterEventBuffer, disableGuests, name, - selected_scheduleId, + scheduleId, isDefault, eventTypeSlug, username, @@ -698,7 +698,7 @@ Return ONLY valid JSON - no explanations.`, ...(attendeePhone && { phoneNumber: attendeePhone }), } { - const eventTypeIdNum = toNumber(selected_eventTypeId) + const eventTypeIdNum = toNumber(eventTypeId) if (eventTypeIdNum !== undefined) result.eventTypeId = eventTypeIdNum } if (start) result.start = start @@ -736,12 +736,12 @@ Return ONLY valid JSON - no explanations.`, case 'calcom_update_event_type': { if (operation === 'calcom_update_event_type') { - const eventTypeIdNum = toNumber(selected_eventTypeIdParam) + const eventTypeIdNum = toNumber(eventTypeIdParam) if (eventTypeIdNum !== undefined) result.eventTypeId = eventTypeIdNum } const lengthNum = toNumber(eventLength) if (lengthNum !== undefined) result.lengthInMinutes = lengthNum - const scheduleIdNum = toNumber(selected_eventTypeScheduleId) + const scheduleIdNum = toNumber(eventTypeScheduleId) if (scheduleIdNum !== undefined) result.scheduleId = scheduleIdNum const slotIntervalNum = toNumber(slotInterval) if (slotIntervalNum !== undefined) result.slotInterval = slotIntervalNum @@ -760,7 +760,7 @@ Return ONLY valid JSON - no explanations.`, case 'calcom_get_event_type': case 'calcom_delete_event_type': { - const eventTypeIdNum = toNumber(selected_eventTypeIdParam) + const eventTypeIdNum = toNumber(eventTypeIdParam) if (eventTypeIdNum !== undefined) result.eventTypeId = eventTypeIdNum } break @@ -782,7 +782,7 @@ Return ONLY valid JSON - no explanations.`, case 'calcom_update_schedule': case 'calcom_delete_schedule': { - const scheduleIdNum = toNumber(selected_scheduleId) + const scheduleIdNum = toNumber(scheduleId) if (scheduleIdNum !== undefined) result.scheduleId = scheduleIdNum } if (operation === 'calcom_update_schedule') { @@ -801,7 +801,7 @@ Return ONLY valid JSON - no explanations.`, case 'calcom_get_slots': { - const eventTypeIdNum = toNumber(selected_eventTypeId) + const eventTypeIdNum = toNumber(eventTypeId) const hasEventTypeId = eventTypeIdNum !== undefined const hasSlugAndUsername = Boolean(eventTypeSlug) && Boolean(username) @@ -837,7 +837,7 @@ Return ONLY valid JSON - no explanations.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Cal.com OAuth credential' }, - selected_eventTypeId: { type: 'number', description: 'Event type ID' }, + eventTypeId: { type: 'number', description: 'Event type ID' }, start: { type: 'string', description: 'Start time (ISO 8601)' }, end: { type: 'string', description: 'End time (ISO 8601)' }, attendeeName: { type: 'string', description: 'Attendee name' }, @@ -851,7 +851,7 @@ Return ONLY valid JSON - no explanations.`, cancellationReason: { type: 'string', description: 'Reason for cancellation' }, reschedulingReason: { type: 'string', description: 'Reason for rescheduling' }, bookingStatus: { type: 'string', description: 'Filter by booking status' }, - selected_eventTypeIdParam: { + eventTypeIdParam: { type: 'number', description: 'Event type ID for get/update/delete', }, @@ -863,10 +863,10 @@ Return ONLY valid JSON - no explanations.`, minimumBookingNotice: { type: 'number', description: 'Minimum advance notice' }, beforeEventBuffer: { type: 'number', description: 'Buffer before event' }, afterEventBuffer: { type: 'number', description: 'Buffer after event' }, - selected_eventTypeScheduleId: { type: 'number', description: 'Schedule ID for event type' }, + eventTypeScheduleId: { type: 'number', description: 'Schedule ID for event type' }, disableGuests: { type: 'boolean', description: 'Disable guest additions' }, sortCreatedAt: { type: 'string', description: 'Sort order for event types' }, - selected_scheduleId: { type: 'number', description: 'Schedule ID' }, + scheduleId: { type: 'number', description: 'Schedule ID' }, name: { type: 'string', description: 'Schedule name' }, timeZone: { type: 'string', description: 'Time zone' }, isDefault: { type: 'boolean', description: 'Set as default schedule' }, diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 1fc4d4cfdf6..2439bda9c62 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -102,7 +102,7 @@ export const ConfluenceBlock: BlockConfig = { id: 'pageId', title: 'Select Page', type: 'file-selector', - canonicalParamId: 'selected_pageId', + canonicalParamId: 'pageId', serviceId: 'confluence', selectorKey: 'confluence.pages', placeholder: 'Select Confluence page', @@ -126,7 +126,7 @@ export const ConfluenceBlock: BlockConfig = { id: 'manualPageId', title: 'Page ID', type: 'short-input', - canonicalParamId: 'selected_pageId', + canonicalParamId: 'pageId', placeholder: 'Enter Confluence page ID', mode: 'advanced', required: { @@ -147,7 +147,7 @@ export const ConfluenceBlock: BlockConfig = { id: 'spaceSelector', title: 'Space', type: 'project-selector', - canonicalParamId: 'selected_spaceId', + canonicalParamId: 'spaceId', serviceId: 'confluence', selectorKey: 'confluence.spaces', selectorAllowSearch: false, @@ -160,7 +160,7 @@ export const ConfluenceBlock: BlockConfig = { id: 'spaceId', title: 'Space ID', type: 'short-input', - canonicalParamId: 'selected_spaceId', + canonicalParamId: 'spaceId', placeholder: 'Enter Confluence space ID', mode: 'advanced', required: { field: 'operation', value: ['create', 'get_space'] }, @@ -317,8 +317,8 @@ export const ConfluenceBlock: BlockConfig = { params: (params) => { const { oauthCredential, - selected_pageId, - selected_spaceId, + pageId, + spaceId, operation, attachmentFile, attachmentFileName, @@ -326,7 +326,7 @@ export const ConfluenceBlock: BlockConfig = { ...rest } = params - const effectivePageId = selected_pageId ? String(selected_pageId).trim() : '' + const effectivePageId = pageId ? String(pageId).trim() : '' if (operation === 'upload_attachment') { return { @@ -336,7 +336,7 @@ export const ConfluenceBlock: BlockConfig = { file: attachmentFile, fileName: attachmentFileName, comment: attachmentComment, - ...(selected_spaceId ? { spaceId: selected_spaceId } : {}), + ...(spaceId ? { spaceId } : {}), ...rest, } } @@ -345,7 +345,7 @@ export const ConfluenceBlock: BlockConfig = { credential: oauthCredential, pageId: effectivePageId || undefined, operation, - ...(selected_spaceId ? { spaceId: selected_spaceId } : {}), + ...(spaceId ? { spaceId } : {}), ...rest, } }, @@ -355,8 +355,8 @@ export const ConfluenceBlock: BlockConfig = { operation: { type: 'string', description: 'Operation to perform' }, domain: { type: 'string', description: 'Confluence domain' }, oauthCredential: { type: 'string', description: 'Confluence access token' }, - selected_pageId: { type: 'string', description: 'Page identifier (canonical param)' }, - selected_spaceId: { type: 'string', description: 'Space identifier' }, + pageId: { type: 'string', description: 'Page identifier' }, + spaceId: { type: 'string', description: 'Space identifier' }, title: { type: 'string', description: 'Page title' }, content: { type: 'string', description: 'Page content' }, parentId: { type: 'string', description: 'Parent page identifier' }, @@ -544,7 +544,7 @@ export const ConfluenceV2Block: BlockConfig = { id: 'pageId', title: 'Select Page', type: 'file-selector', - canonicalParamId: 'selected_pageId', + canonicalParamId: 'pageId', serviceId: 'confluence', selectorKey: 'confluence.pages', placeholder: 'Select Confluence page', @@ -607,7 +607,7 @@ export const ConfluenceV2Block: BlockConfig = { id: 'manualPageId', title: 'Page ID', type: 'short-input', - canonicalParamId: 'selected_pageId', + canonicalParamId: 'pageId', placeholder: 'Enter Confluence page ID', mode: 'advanced', condition: { @@ -1239,7 +1239,7 @@ export const ConfluenceV2Block: BlockConfig = { params: (params) => { const { oauthCredential, - selected_pageId, + pageId, operation, attachmentFile, attachmentFileName, @@ -1268,8 +1268,7 @@ export const ConfluenceV2Block: BlockConfig = { ...rest } = params - // Use canonical param (serializer already handles basic/advanced mode) - const effectivePageId = selected_pageId ? String(selected_pageId).trim() : '' + const effectivePageId = pageId ? String(pageId).trim() : '' if (operation === 'add_label') { return { @@ -1529,7 +1528,7 @@ export const ConfluenceV2Block: BlockConfig = { operation: { type: 'string', description: 'Operation to perform' }, domain: { type: 'string', description: 'Confluence domain' }, oauthCredential: { type: 'string', description: 'Confluence access token' }, - selected_pageId: { type: 'string', description: 'Page identifier (canonical param)' }, + pageId: { type: 'string', description: 'Page identifier' }, spaceId: { type: 'string', description: 'Space identifier' }, blogPostId: { type: 'string', description: 'Blog post identifier' }, versionNumber: { type: 'number', description: 'Page version number' }, diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 670d9ca5da1..1fdece82317 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -113,7 +113,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, id: 'datasetSelector', title: 'Dataset', type: 'project-selector', - canonicalParamId: 'selected_datasetId', + canonicalParamId: 'datasetId', serviceId: 'google-bigquery', selectorKey: 'bigquery.datasets', selectorAllowSearch: false, @@ -127,7 +127,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, id: 'datasetId', title: 'Dataset ID', type: 'short-input', - canonicalParamId: 'selected_datasetId', + canonicalParamId: 'datasetId', placeholder: 'Enter BigQuery dataset ID', mode: 'advanced', condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, @@ -138,7 +138,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, id: 'tableSelector', title: 'Table', type: 'file-selector', - canonicalParamId: 'selected_tableId', + canonicalParamId: 'tableId', serviceId: 'google-bigquery', selectorKey: 'bigquery.tables', selectorAllowSearch: false, @@ -152,7 +152,7 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, id: 'tableId', title: 'Table ID', type: 'short-input', - canonicalParamId: 'selected_tableId', + canonicalParamId: 'tableId', placeholder: 'Enter BigQuery table ID', mode: 'advanced', condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, @@ -227,13 +227,10 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, } }, params: (params) => { - const { oauthCredential, rows, maxResults, selected_datasetId, selected_tableId, ...rest } = - params + const { oauthCredential, rows, maxResults, ...rest } = params return { ...rest, oauthCredential, - datasetId: selected_datasetId, - tableId: selected_tableId, ...(rows && { rows: typeof rows === 'string' ? rows : JSON.stringify(rows) }), ...(maxResults !== undefined && maxResults !== '' && { maxResults: Number(maxResults) }), } @@ -252,8 +249,8 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, description: 'Default dataset for unqualified table names', }, location: { type: 'string', description: 'Processing location' }, - selected_datasetId: { type: 'string', description: 'BigQuery dataset ID' }, - selected_tableId: { type: 'string', description: 'BigQuery table ID' }, + datasetId: { type: 'string', description: 'BigQuery dataset ID' }, + tableId: { type: 'string', description: 'BigQuery table ID' }, rows: { type: 'string', description: 'JSON array of row objects to insert' }, skipInvalidRows: { type: 'boolean', description: 'Whether to skip invalid rows during insert' }, ignoreUnknownValues: { diff --git a/apps/sim/blocks/blocks/google_tasks.ts b/apps/sim/blocks/blocks/google_tasks.ts index 7ae34666d40..ad63e6e1a72 100644 --- a/apps/sim/blocks/blocks/google_tasks.ts +++ b/apps/sim/blocks/blocks/google_tasks.ts @@ -56,7 +56,7 @@ export const GoogleTasksBlock: BlockConfig = { id: 'taskListSelector', title: 'Task List', type: 'project-selector', - canonicalParamId: 'selected_taskListId', + canonicalParamId: 'taskListId', serviceId: 'google-tasks', selectorKey: 'google.tasks.lists', selectorAllowSearch: false, @@ -69,7 +69,7 @@ export const GoogleTasksBlock: BlockConfig = { id: 'taskListId', title: 'Task List ID', type: 'short-input', - canonicalParamId: 'selected_taskListId', + canonicalParamId: 'taskListId', placeholder: 'Task list ID (leave empty for default list)', mode: 'advanced', condition: { field: 'operation', value: 'list_task_lists', not: true }, @@ -223,18 +223,10 @@ Return ONLY the timestamp - no explanations, no extra text.`, } }, params: (params) => { - const { - oauthCredential, - operation, - showCompleted, - maxResults, - selected_taskListId, - ...rest - } = params + const { oauthCredential, operation, showCompleted, maxResults, ...rest } = params const processedParams: Record = { ...rest, - taskListId: selected_taskListId, } if (maxResults && typeof maxResults === 'string') { @@ -256,7 +248,7 @@ Return ONLY the timestamp - no explanations, no extra text.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Google Tasks access token' }, - selected_taskListId: { type: 'string', description: 'Task list identifier' }, + taskListId: { type: 'string', description: 'Task list identifier' }, title: { type: 'string', description: 'Task title' }, notes: { type: 'string', description: 'Task notes' }, due: { type: 'string', description: 'Task due date' }, diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 1b02d989b6d..916f0b2bd1e 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -110,7 +110,7 @@ export const JiraServiceManagementBlock: BlockConfig = { id: 'serviceDeskSelector', title: 'Service Desk', type: 'project-selector', - canonicalParamId: 'selected_serviceDeskId', + canonicalParamId: 'serviceDeskId', serviceId: 'jira', selectorKey: 'jsm.serviceDesks', selectorAllowSearch: false, @@ -149,7 +149,7 @@ export const JiraServiceManagementBlock: BlockConfig = { id: 'serviceDeskId', title: 'Service Desk ID', type: 'short-input', - canonicalParamId: 'selected_serviceDeskId', + canonicalParamId: 'serviceDeskId', placeholder: 'Enter service desk ID', mode: 'advanced', required: { @@ -184,7 +184,7 @@ export const JiraServiceManagementBlock: BlockConfig = { id: 'requestTypeSelector', title: 'Request Type', type: 'file-selector', - canonicalParamId: 'selected_requestTypeId', + canonicalParamId: 'requestTypeId', serviceId: 'jira', selectorKey: 'jsm.requestTypes', selectorAllowSearch: false, @@ -198,7 +198,7 @@ export const JiraServiceManagementBlock: BlockConfig = { id: 'requestTypeId', title: 'Request Type ID', type: 'short-input', - canonicalParamId: 'selected_requestTypeId', + canonicalParamId: 'requestTypeId', required: true, placeholder: 'Enter request type ID', mode: 'advanced', @@ -586,21 +586,21 @@ Return ONLY the comment text - no explanations.`, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'get_request_types': - if (!params.selected_serviceDeskId) { + if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, + serviceDeskId: params.serviceDeskId, searchQuery: params.searchQuery, groupId: params.groupId, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'create_request': - if (!params.selected_serviceDeskId) { + if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } - if (!params.selected_requestTypeId) { + if (!params.requestTypeId) { throw new Error('Request Type ID is required') } if (!params.summary) { @@ -608,8 +608,8 @@ Return ONLY the comment text - no explanations.`, } return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, - requestTypeId: params.selected_requestTypeId, + serviceDeskId: params.serviceDeskId, + requestTypeId: params.requestTypeId, summary: params.summary, description: params.description, raiseOnBehalfOf: params.raiseOnBehalfOf, @@ -631,7 +631,7 @@ Return ONLY the comment text - no explanations.`, case 'get_requests': return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, + serviceDeskId: params.serviceDeskId, requestOwnership: params.requestOwnership, requestStatus: params.requestStatus, searchTerm: params.searchTerm, @@ -662,17 +662,17 @@ Return ONLY the comment text - no explanations.`, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'get_customers': - if (!params.selected_serviceDeskId) { + if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, + serviceDeskId: params.serviceDeskId, query: params.customerQuery, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'add_customer': { - if (!params.selected_serviceDeskId) { + if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } if (!params.accountIds && !params.emails) { @@ -680,27 +680,27 @@ Return ONLY the comment text - no explanations.`, } return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, + serviceDeskId: params.serviceDeskId, accountIds: params.accountIds, emails: params.emails, } } case 'get_organizations': - if (!params.selected_serviceDeskId) { + if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, + serviceDeskId: params.serviceDeskId, limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } case 'get_queues': - if (!params.selected_serviceDeskId) { + if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, + serviceDeskId: params.serviceDeskId, includeCount: params.includeCount === 'true', limit: params.maxResults ? Number.parseInt(params.maxResults) : undefined, } @@ -744,7 +744,7 @@ Return ONLY the comment text - no explanations.`, name: params.organizationName, } case 'add_organization': - if (!params.selected_serviceDeskId) { + if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } if (!params.organizationId) { @@ -752,7 +752,7 @@ Return ONLY the comment text - no explanations.`, } return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, + serviceDeskId: params.serviceDeskId, organizationId: params.organizationId, } case 'get_participants': @@ -802,16 +802,16 @@ Return ONLY the comment text - no explanations.`, decision: params.approvalDecision, } case 'get_request_type_fields': - if (!params.selected_serviceDeskId) { + if (!params.serviceDeskId) { throw new Error('Service Desk ID is required') } - if (!params.selected_requestTypeId) { + if (!params.requestTypeId) { throw new Error('Request Type ID is required') } return { ...baseParams, - serviceDeskId: params.selected_serviceDeskId, - requestTypeId: params.selected_requestTypeId, + serviceDeskId: params.serviceDeskId, + requestTypeId: params.requestTypeId, } default: return baseParams @@ -823,8 +823,8 @@ Return ONLY the comment text - no explanations.`, operation: { type: 'string', description: 'Operation to perform' }, domain: { type: 'string', description: 'Jira domain' }, oauthCredential: { type: 'string', description: 'Jira Service Management access token' }, - selected_serviceDeskId: { type: 'string', description: 'Service desk ID' }, - selected_requestTypeId: { type: 'string', description: 'Request type ID' }, + serviceDeskId: { type: 'string', description: 'Service desk ID' }, + requestTypeId: { type: 'string', description: 'Request type ID' }, issueIdOrKey: { type: 'string', description: 'Issue ID or key' }, summary: { type: 'string', description: 'Request summary' }, description: { type: 'string', description: 'Request description' }, diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index a7d14cdcf1d..ab90c179236 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -89,7 +89,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { id: 'planSelector', title: 'Plan', type: 'project-selector', - canonicalParamId: 'selected_planId', + canonicalParamId: 'planId', serviceId: 'microsoft-planner', selectorKey: 'microsoft.planner.plans', selectorAllowSearch: false, @@ -111,7 +111,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { id: 'planId', title: 'Plan ID', type: 'short-input', - canonicalParamId: 'selected_planId', + canonicalParamId: 'planId', placeholder: 'Enter the plan ID', mode: 'advanced', condition: { @@ -388,7 +388,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, oauthCredential, operation, groupId, - selected_planId, + planId, readTaskId, // Canonical param from taskSelector (basic) or manualReadTaskId (advanced) for read_task updateTaskId, // Task ID for update/delete operations bucketId, @@ -427,7 +427,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (operation === 'read_plan') { return { ...baseParams, - planId: selected_planId?.trim(), + planId: planId?.trim(), } } @@ -435,7 +435,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (operation === 'list_buckets') { return { ...baseParams, - planId: selected_planId?.trim(), + planId: planId?.trim(), } } @@ -451,7 +451,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (operation === 'create_bucket') { return { ...baseParams, - planId: selected_planId?.trim(), + planId: planId?.trim(), name: name?.trim(), } } @@ -484,8 +484,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (effectiveReadTaskId) { readParams.taskId = effectiveReadTaskId - } else if (selected_planId?.trim()) { - readParams.planId = selected_planId.trim() + } else if (planId?.trim()) { + readParams.planId = planId.trim() } return readParams @@ -495,7 +495,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (operation === 'create_task') { const createParams: MicrosoftPlannerBlockParams = { ...baseParams, - planId: selected_planId?.trim(), + planId: planId?.trim(), title: title?.trim(), } @@ -597,7 +597,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Microsoft account credential' }, groupId: { type: 'string', description: 'Microsoft 365 group ID' }, - selected_planId: { type: 'string', description: 'Plan ID' }, + planId: { type: 'string', description: 'Plan ID' }, readTaskId: { type: 'string', description: 'Task ID for read operation' }, updateTaskId: { type: 'string', description: 'Task ID for update/delete operations' }, bucketId: { type: 'string', description: 'Bucket ID' }, diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 0e12e605413..2b7c563c655 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -58,7 +58,7 @@ export const NotionBlock: BlockConfig = { id: 'pageSelector', title: 'Page', type: 'file-selector', - canonicalParamId: 'selected_pageId', + canonicalParamId: 'pageId', serviceId: 'notion', selectorKey: 'notion.pages', selectorAllowSearch: true, @@ -72,7 +72,7 @@ export const NotionBlock: BlockConfig = { id: 'pageId', title: 'Page ID', type: 'short-input', - canonicalParamId: 'selected_pageId', + canonicalParamId: 'pageId', placeholder: 'Enter Notion page ID', mode: 'advanced', condition: { field: 'operation', value: ['notion_read', 'notion_write'] }, @@ -83,7 +83,7 @@ export const NotionBlock: BlockConfig = { id: 'databaseSelector', title: 'Database', type: 'project-selector', - canonicalParamId: 'selected_databaseId', + canonicalParamId: 'databaseId', serviceId: 'notion', selectorKey: 'notion.databases', selectorAllowSearch: true, @@ -103,7 +103,7 @@ export const NotionBlock: BlockConfig = { id: 'databaseId', title: 'Database ID', type: 'short-input', - canonicalParamId: 'selected_databaseId', + canonicalParamId: 'databaseId', placeholder: 'Enter Notion database ID', mode: 'advanced', condition: { @@ -120,7 +120,7 @@ export const NotionBlock: BlockConfig = { id: 'parentSelector', title: 'Parent Page', type: 'file-selector', - canonicalParamId: 'selected_parentId', + canonicalParamId: 'parentId', serviceId: 'notion', selectorKey: 'notion.pages', selectorAllowSearch: true, @@ -140,7 +140,7 @@ export const NotionBlock: BlockConfig = { id: 'parentId', title: 'Parent Page ID', type: 'short-input', - canonicalParamId: 'selected_parentId', + canonicalParamId: 'parentId', placeholder: 'ID of parent page', mode: 'advanced', condition: { @@ -345,17 +345,7 @@ export const NotionBlock: BlockConfig = { } }, params: (params) => { - const { - oauthCredential, - operation, - properties, - filter, - sorts, - selected_pageId, - selected_databaseId, - selected_parentId, - ...rest - } = params + const { oauthCredential, operation, properties, filter, sorts, ...rest } = params // Parse properties from JSON string for create/add operations let parsedProperties @@ -405,9 +395,6 @@ export const NotionBlock: BlockConfig = { return { ...rest, oauthCredential, - ...(selected_pageId ? { pageId: selected_pageId } : {}), - ...(selected_databaseId ? { databaseId: selected_databaseId } : {}), - ...(selected_parentId ? { parentId: selected_parentId } : {}), ...(parsedProperties ? { properties: parsedProperties } : {}), ...(parsedFilter ? { filter: JSON.stringify(parsedFilter) } : {}), ...(parsedSorts ? { sorts: JSON.stringify(parsedSorts) } : {}), @@ -418,13 +405,13 @@ export const NotionBlock: BlockConfig = { inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Notion access token' }, - selected_pageId: { type: 'string', description: 'Page identifier' }, + pageId: { type: 'string', description: 'Page identifier' }, content: { type: 'string', description: 'Page content' }, // Create page inputs - selected_parentId: { type: 'string', description: 'Parent page identifier' }, + parentId: { type: 'string', description: 'Parent page identifier' }, title: { type: 'string', description: 'Page title' }, // Query database inputs - selected_databaseId: { type: 'string', description: 'Database identifier' }, + databaseId: { type: 'string', description: 'Database identifier' }, filter: { type: 'string', description: 'Filter criteria' }, sorts: { type: 'string', description: 'Sort criteria' }, pageSize: { type: 'number', description: 'Page size limit' }, diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index ef1cd56fcd6..55e5c331ff4 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -100,7 +100,7 @@ export const PipedriveBlock: BlockConfig = { id: 'pipelineSelector', title: 'Pipeline', type: 'project-selector', - canonicalParamId: 'selected_pipeline_id', + canonicalParamId: 'pipeline_id', serviceId: 'pipedrive', selectorKey: 'pipedrive.pipelines', selectorAllowSearch: false, @@ -117,7 +117,7 @@ export const PipedriveBlock: BlockConfig = { id: 'pipeline_id', title: 'Pipeline ID', type: 'short-input', - canonicalParamId: 'selected_pipeline_id', + canonicalParamId: 'pipeline_id', placeholder: 'Enter pipeline ID', mode: 'advanced', condition: { @@ -769,20 +769,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n } }, params: (params) => { - const { oauthCredential, operation, selected_pipeline_id, ...rest } = params + const { oauthCredential, operation, ...rest } = params const cleanParams: Record = { oauthCredential, } - if ( - selected_pipeline_id !== undefined && - selected_pipeline_id !== null && - selected_pipeline_id !== '' - ) { - cleanParams.pipeline_id = selected_pipeline_id - } - Object.entries(rest).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { cleanParams[key] = value @@ -802,7 +794,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n currency: { type: 'string', description: 'Currency code' }, person_id: { type: 'string', description: 'Person ID' }, org_id: { type: 'string', description: 'Organization ID' }, - selected_pipeline_id: { type: 'string', description: 'Pipeline ID' }, + pipeline_id: { type: 'string', description: 'Pipeline ID' }, stage_id: { type: 'string', description: 'Stage ID' }, status: { type: 'string', description: 'Status' }, expected_close_date: { type: 'string', description: 'Expected close date' }, diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index be781fcfd39..2bfc08bcd8b 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -116,7 +116,7 @@ export const SharepointBlock: BlockConfig = { id: 'listSelector', title: 'List', type: 'file-selector', - canonicalParamId: 'selected_listId', + canonicalParamId: 'listId', serviceId: 'sharepoint', selectorKey: 'sharepoint.lists', selectorAllowSearch: false, @@ -129,7 +129,7 @@ export const SharepointBlock: BlockConfig = { id: 'listId', title: 'List ID', type: 'short-input', - canonicalParamId: 'selected_listId', + canonicalParamId: 'listId', placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.', mode: 'advanced', condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] }, @@ -348,7 +348,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, title: 'Document Library ID', type: 'short-input', placeholder: 'Enter document library (drive) ID', - canonicalParamId: 'selected_driveId', + canonicalParamId: 'driveId', condition: { field: 'operation', value: 'upload_file' }, mode: 'advanced', }, @@ -374,7 +374,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, id: 'uploadFiles', title: 'Files', type: 'file-upload', - canonicalParamId: 'selected_files', + canonicalParamId: 'files', placeholder: 'Upload files to SharePoint', condition: { field: 'operation', value: 'upload_file' }, mode: 'basic', @@ -386,7 +386,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, id: 'files', title: 'Files', type: 'short-input', - canonicalParamId: 'selected_files', + canonicalParamId: 'files', placeholder: 'Reference files from previous blocks', condition: { field: 'operation', value: 'upload_file' }, mode: 'advanced', @@ -438,10 +438,10 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, listItemFields, // canonical param includeColumns, includeItems, - selected_files, // canonical param from uploadFiles (basic) or files (advanced) - selected_driveId, // canonical param from driveId + files, // canonical param from uploadFiles (basic) or files (advanced) + driveId, // canonical param from driveId columnDefinitions, - selected_listId, + listId, ...others } = rest as any @@ -473,7 +473,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, try { logger.info('SharepointBlock list item param check', { siteId: effectiveSiteId || undefined, - listId: selected_listId, + listId: listId, listTitle: (others as any)?.listTitle, itemId: sanitizedItemId, hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object', @@ -486,15 +486,15 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, } // Handle file upload files parameter using canonical param - const normalizedFiles = normalizeFileInput(selected_files) + const normalizedFiles = normalizeFileInput(files) const baseParams: Record = { oauthCredential, siteId: effectiveSiteId || undefined, pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined, mimeType: mimeType, ...others, - ...(selected_listId ? { listId: selected_listId } : {}), - ...(selected_driveId ? { driveId: selected_driveId } : {}), + ...(listId ? { listId } : {}), + ...(driveId ? { driveId } : {}), itemId: sanitizedItemId, listItemFields: parsedItemFields, includeColumns: coerceBoolean(includeColumns), @@ -529,19 +529,19 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, listDisplayName: { type: 'string', description: 'List display name' }, listDescription: { type: 'string', description: 'List description' }, listTemplate: { type: 'string', description: 'List template' }, - selected_listId: { type: 'string', description: 'List ID' }, + listId: { type: 'string', description: 'List ID' }, listTitle: { type: 'string', description: 'List title' }, includeColumns: { type: 'boolean', description: 'Include columns in response' }, includeItems: { type: 'boolean', description: 'Include items in response' }, itemId: { type: 'string', description: 'List item ID (canonical param)' }, listItemFields: { type: 'string', description: 'List item fields (canonical param)' }, - selected_driveId: { + driveId: { type: 'string', - description: 'Document library (drive) ID (canonical param)', + description: 'Document library (drive) ID', }, folderPath: { type: 'string', description: 'Folder path for file upload' }, fileName: { type: 'string', description: 'File name override' }, - selected_files: { type: 'array', description: 'Files to upload (canonical param)' }, + files: { type: 'array', description: 'Files to upload' }, }, outputs: { sites: { diff --git a/apps/sim/blocks/blocks/trello.ts b/apps/sim/blocks/blocks/trello.ts index ac64a60dd64..bff115ebdcf 100644 --- a/apps/sim/blocks/blocks/trello.ts +++ b/apps/sim/blocks/blocks/trello.ts @@ -62,7 +62,7 @@ export const TrelloBlock: BlockConfig = { id: 'boardSelector', title: 'Board', type: 'project-selector', - canonicalParamId: 'selected_boardId', + canonicalParamId: 'boardId', serviceId: 'trello', selectorKey: 'trello.boards', selectorAllowSearch: false, @@ -87,7 +87,7 @@ export const TrelloBlock: BlockConfig = { id: 'boardId', title: 'Board ID', type: 'short-input', - canonicalParamId: 'selected_boardId', + canonicalParamId: 'boardId', placeholder: 'Enter board ID', mode: 'advanced', condition: { @@ -374,9 +374,9 @@ Return ONLY the date/timestamp string - no explanations, no quotes, no extra tex } }, params: (params) => { - const { operation, limit, closed, dueComplete, selected_boardId, ...rest } = params + const { operation, limit, closed, dueComplete, ...rest } = params - const result: Record = { ...rest, boardId: selected_boardId } + const result: Record = { ...rest } if (limit && operation === 'trello_get_actions') { result.limit = Number.parseInt(limit, 10) @@ -409,7 +409,7 @@ Return ONLY the date/timestamp string - no explanations, no quotes, no extra tex inputs: { operation: { type: 'string', description: 'Trello operation to perform' }, oauthCredential: { type: 'string', description: 'Trello OAuth credential' }, - selected_boardId: { type: 'string', description: 'Board ID' }, + boardId: { type: 'string', description: 'Board ID' }, listId: { type: 'string', description: 'List ID' }, cardId: { type: 'string', description: 'Card ID' }, name: { type: 'string', description: 'Card name/title' }, diff --git a/apps/sim/blocks/blocks/zoom.ts b/apps/sim/blocks/blocks/zoom.ts index 5cd09656450..23b7dc70a84 100644 --- a/apps/sim/blocks/blocks/zoom.ts +++ b/apps/sim/blocks/blocks/zoom.ts @@ -82,7 +82,7 @@ export const ZoomBlock: BlockConfig = { id: 'meetingSelector', title: 'Meeting', type: 'project-selector', - canonicalParamId: 'selected_meetingId', + canonicalParamId: 'meetingId', serviceId: 'zoom', selectorKey: 'zoom.meetings', selectorAllowSearch: true, @@ -107,7 +107,7 @@ export const ZoomBlock: BlockConfig = { id: 'meetingId', title: 'Meeting ID', type: 'short-input', - canonicalParamId: 'selected_meetingId', + canonicalParamId: 'meetingId', placeholder: 'Enter meeting ID', mode: 'advanced', required: true, @@ -512,22 +512,22 @@ Return ONLY the date string - no explanations, no quotes, no extra text.`, } case 'zoom_get_meeting': - if (!params.selected_meetingId?.trim()) { + if (!params.meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.selected_meetingId.trim(), + meetingId: params.meetingId.trim(), occurrenceId: params.occurrenceId, } case 'zoom_update_meeting': - if (!params.selected_meetingId?.trim()) { + if (!params.meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.selected_meetingId.trim(), + meetingId: params.meetingId.trim(), topic: params.topicUpdate, type: params.type ? Number(params.type) : undefined, startTime: params.startTime, @@ -544,23 +544,23 @@ Return ONLY the date string - no explanations, no quotes, no extra text.`, } case 'zoom_delete_meeting': - if (!params.selected_meetingId?.trim()) { + if (!params.meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.selected_meetingId.trim(), + meetingId: params.meetingId.trim(), occurrenceId: params.occurrenceId, cancelMeetingReminder: params.cancelMeetingReminder, } case 'zoom_get_meeting_invitation': - if (!params.selected_meetingId?.trim()) { + if (!params.meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.selected_meetingId.trim(), + meetingId: params.meetingId.trim(), } case 'zoom_list_recordings': @@ -577,32 +577,32 @@ Return ONLY the date string - no explanations, no quotes, no extra text.`, } case 'zoom_get_meeting_recordings': - if (!params.selected_meetingId?.trim()) { + if (!params.meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.selected_meetingId.trim(), + meetingId: params.meetingId.trim(), } case 'zoom_delete_recording': - if (!params.selected_meetingId?.trim()) { + if (!params.meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.selected_meetingId.trim(), + meetingId: params.meetingId.trim(), recordingId: params.recordingId, action: params.deleteAction, } case 'zoom_list_past_participants': - if (!params.selected_meetingId?.trim()) { + if (!params.meetingId?.trim()) { throw new Error('Meeting ID is required.') } return { ...baseParams, - meetingId: params.selected_meetingId.trim(), + meetingId: params.meetingId.trim(), pageSize: params.pageSize ? Number(params.pageSize) : undefined, nextPageToken: params.nextPageToken, } @@ -617,7 +617,7 @@ Return ONLY the date string - no explanations, no quotes, no extra text.`, operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Zoom access token' }, userId: { type: 'string', description: 'User ID or email (use "me" for authenticated user)' }, - selected_meetingId: { type: 'string', description: 'Meeting ID' }, + meetingId: { type: 'string', description: 'Meeting ID' }, topic: { type: 'string', description: 'Meeting topic' }, topicUpdate: { type: 'string', description: 'Meeting topic for update' }, type: { type: 'string', description: 'Meeting type' }, From 06cc970700abd1bc61f6e6937b94fd47cbd17388 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 22:15:02 -0800 Subject: [PATCH 16/32] fix(selectors): add spaceId selector pair to Confluence V2 block The V2 block was missing the spaceSelector basic-mode selector that the V1 (Legacy) block already had. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/confluence.ts | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 2439bda9c62..f91a3b72296 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -663,11 +663,44 @@ export const ConfluenceV2Block: BlockConfig = { ], }, }, + { + id: 'spaceSelector', + title: 'Space', + type: 'project-selector', + canonicalParamId: 'spaceId', + serviceId: 'confluence', + selectorKey: 'confluence.spaces', + selectorAllowSearch: false, + placeholder: 'Select Confluence space', + dependsOn: ['credential', 'domain'], + mode: 'basic', + required: true, + condition: { + field: 'operation', + value: [ + 'create', + 'get_space', + 'update_space', + 'delete_space', + 'list_pages_in_space', + 'search_in_space', + 'create_blogpost', + 'list_blogposts_in_space', + 'list_space_labels', + 'list_space_permissions', + 'list_space_properties', + 'create_space_property', + 'delete_space_property', + ], + }, + }, { id: 'spaceId', title: 'Space ID', type: 'short-input', + canonicalParamId: 'spaceId', placeholder: 'Enter Confluence space ID', + mode: 'advanced', required: true, condition: { field: 'operation', From 613d3e1841d03d93d0c03e1c4d714758b1f6831d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 22:20:50 -0800 Subject: [PATCH 17/32] refactor(selectors): revert V1 block changes, add selectors to Notion V1 for V2 inheritance Confluence V1: reverted to main state (V2 has its own subBlocks). Notion V1: added selector pairs per-operation since V2 inherits subBlocks, inputs, and params from V1. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/confluence.ts | 23 +---- apps/sim/blocks/blocks/notion.ts | 127 +++++++++++++++++++-------- 2 files changed, 93 insertions(+), 57 deletions(-) diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index f91a3b72296..fe7eca52f64 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -84,7 +84,6 @@ export const ConfluenceBlock: BlockConfig = { 'write:content.property:confluence', 'read:hierarchical-content:confluence', 'read:content.metadata:confluence', - 'read:user:confluence', ], placeholder: 'Select Confluence account', required: true, @@ -104,7 +103,6 @@ export const ConfluenceBlock: BlockConfig = { type: 'file-selector', canonicalParamId: 'pageId', serviceId: 'confluence', - selectorKey: 'confluence.pages', placeholder: 'Select Confluence page', dependsOn: ['credential', 'domain'], mode: 'basic', @@ -143,26 +141,11 @@ export const ConfluenceBlock: BlockConfig = { ], }, }, - { - id: 'spaceSelector', - title: 'Space', - type: 'project-selector', - canonicalParamId: 'spaceId', - serviceId: 'confluence', - selectorKey: 'confluence.spaces', - selectorAllowSearch: false, - placeholder: 'Select Confluence space', - dependsOn: ['credential', 'domain'], - mode: 'basic', - required: { field: 'operation', value: ['create', 'get_space'] }, - }, { id: 'spaceId', title: 'Space ID', type: 'short-input', - canonicalParamId: 'spaceId', placeholder: 'Enter Confluence space ID', - mode: 'advanced', required: { field: 'operation', value: ['create', 'get_space'] }, }, { @@ -318,7 +301,6 @@ export const ConfluenceBlock: BlockConfig = { const { oauthCredential, pageId, - spaceId, operation, attachmentFile, attachmentFileName, @@ -336,7 +318,6 @@ export const ConfluenceBlock: BlockConfig = { file: attachmentFile, fileName: attachmentFileName, comment: attachmentComment, - ...(spaceId ? { spaceId } : {}), ...rest, } } @@ -345,7 +326,6 @@ export const ConfluenceBlock: BlockConfig = { credential: oauthCredential, pageId: effectivePageId || undefined, operation, - ...(spaceId ? { spaceId } : {}), ...rest, } }, @@ -355,7 +335,7 @@ export const ConfluenceBlock: BlockConfig = { operation: { type: 'string', description: 'Operation to perform' }, domain: { type: 'string', description: 'Confluence domain' }, oauthCredential: { type: 'string', description: 'Confluence access token' }, - pageId: { type: 'string', description: 'Page identifier' }, + pageId: { type: 'string', description: 'Page identifier (canonical param)' }, spaceId: { type: 'string', description: 'Space identifier' }, title: { type: 'string', description: 'Page title' }, content: { type: 'string', description: 'Page content' }, @@ -401,6 +381,7 @@ export const ConfluenceBlock: BlockConfig = { }, } + export const ConfluenceV2Block: BlockConfig = { ...ConfluenceBlock, type: 'confluence_v2', diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 2b7c563c655..2151d1ae468 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -53,7 +53,6 @@ export const NotionBlock: BlockConfig = { placeholder: 'Enter credential ID', required: true, }, - // Page selector for Read Page and Append Content { id: 'pageSelector', title: 'Page', @@ -61,12 +60,14 @@ export const NotionBlock: BlockConfig = { canonicalParamId: 'pageId', serviceId: 'notion', selectorKey: 'notion.pages', - selectorAllowSearch: true, placeholder: 'Select Notion page', dependsOn: ['credential'], mode: 'basic', - condition: { field: 'operation', value: ['notion_read', 'notion_write'] }, - required: { field: 'operation', value: ['notion_read', 'notion_write'] }, + condition: { + field: 'operation', + value: ['notion_read', 'notion_write'], + }, + required: true, }, { id: 'pageId', @@ -75,10 +76,12 @@ export const NotionBlock: BlockConfig = { canonicalParamId: 'pageId', placeholder: 'Enter Notion page ID', mode: 'advanced', - condition: { field: 'operation', value: ['notion_read', 'notion_write'] }, - required: { field: 'operation', value: ['notion_read', 'notion_write'] }, + condition: { + field: 'operation', + value: ['notion_read', 'notion_write'], + }, + required: true, }, - // Database selector (consolidated across read_database, query_database, add_database_row) { id: 'databaseSelector', title: 'Database', @@ -86,18 +89,15 @@ export const NotionBlock: BlockConfig = { canonicalParamId: 'databaseId', serviceId: 'notion', selectorKey: 'notion.databases', - selectorAllowSearch: true, + selectorAllowSearch: false, placeholder: 'Select Notion database', dependsOn: ['credential'], mode: 'basic', condition: { field: 'operation', - value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], - }, - required: { - field: 'operation', - value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], + value: 'notion_read_database', }, + required: true, }, { id: 'databaseId', @@ -108,14 +108,10 @@ export const NotionBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], - }, - required: { - field: 'operation', - value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], + value: 'notion_read_database', }, + required: true, }, - // Parent page selector (consolidated across create_page, create_database) { id: 'parentSelector', title: 'Parent Page', @@ -123,18 +119,11 @@ export const NotionBlock: BlockConfig = { canonicalParamId: 'parentId', serviceId: 'notion', selectorKey: 'notion.pages', - selectorAllowSearch: true, placeholder: 'Select parent page', dependsOn: ['credential'], mode: 'basic', - condition: { - field: 'operation', - value: ['notion_create_page', 'notion_create_database'], - }, - required: { - field: 'operation', - value: ['notion_create_page', 'notion_create_database'], - }, + condition: { field: 'operation', value: 'notion_create_page' }, + required: true, }, { id: 'parentId', @@ -143,14 +132,8 @@ export const NotionBlock: BlockConfig = { canonicalParamId: 'parentId', placeholder: 'ID of parent page', mode: 'advanced', - condition: { - field: 'operation', - value: ['notion_create_page', 'notion_create_database'], - }, - required: { - field: 'operation', - value: ['notion_create_page', 'notion_create_database'], - }, + condition: { field: 'operation', value: 'notion_create_page' }, + required: true, }, { id: 'title', @@ -204,6 +187,30 @@ export const NotionBlock: BlockConfig = { }, }, // Query Database Fields + { + id: 'databaseSelector', + title: 'Database', + type: 'project-selector', + canonicalParamId: 'databaseId', + serviceId: 'notion', + selectorKey: 'notion.databases', + selectorAllowSearch: false, + placeholder: 'Select Notion database', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'notion_query_database' }, + required: true, + }, + { + id: 'databaseId', + title: 'Database ID', + type: 'short-input', + canonicalParamId: 'databaseId', + placeholder: 'Enter Notion database ID', + mode: 'advanced', + condition: { field: 'operation', value: 'notion_query_database' }, + required: true, + }, { id: 'filter', title: 'Filter', @@ -266,6 +273,29 @@ export const NotionBlock: BlockConfig = { condition: { field: 'operation', value: 'notion_search' }, }, // Create Database Fields + { + id: 'parentSelector', + title: 'Parent Page', + type: 'file-selector', + canonicalParamId: 'parentId', + serviceId: 'notion', + selectorKey: 'notion.pages', + placeholder: 'Select parent page', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'notion_create_database' }, + required: true, + }, + { + id: 'parentId', + title: 'Parent Page ID', + type: 'short-input', + canonicalParamId: 'parentId', + placeholder: 'ID of parent page where database will be created', + mode: 'advanced', + condition: { field: 'operation', value: 'notion_create_database' }, + required: true, + }, { id: 'title', title: 'Database Title', @@ -296,6 +326,30 @@ export const NotionBlock: BlockConfig = { }, }, // Add Database Row Fields + { + id: 'databaseSelector', + title: 'Database', + type: 'project-selector', + canonicalParamId: 'databaseId', + serviceId: 'notion', + selectorKey: 'notion.databases', + selectorAllowSearch: false, + placeholder: 'Select Notion database', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'notion_add_database_row' }, + required: true, + }, + { + id: 'databaseId', + title: 'Database ID', + type: 'short-input', + canonicalParamId: 'databaseId', + placeholder: 'Enter Notion database ID', + mode: 'advanced', + condition: { field: 'operation', value: 'notion_add_database_row' }, + required: true, + }, { id: 'properties', title: 'Row Properties', @@ -436,6 +490,7 @@ export const NotionBlock: BlockConfig = { } // V2 Block with API-aligned outputs + export const NotionV2Block: BlockConfig = { type: 'notion_v2', name: 'Notion', From 1d24842ce4bc48b1011d4d6c10d2eae3aec472f1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 22:34:14 -0800 Subject: [PATCH 18/32] fix(selectors): audit fixes for auth patterns, registry gaps, and display name resolution - Convert Microsoft Planner plans/tasks routes from GET+getSession to POST+authorizeCredentialUse - Add fetchById to microsoft.planner (tasks) and sharepoint.sites registry entries - Add ensureCredential to sharepoint.sites and microsoft.planner registry fetchList - Update microsoft.planner.plans registry to use POST method - Add siteId, collectionId, spreadsheetId, fileId to SelectorDisplayNameArgs and caller - Add fileId to SelectorResolutionArgs and resolution context - Fix Zoom topicUpdate visibility in basic mode (remove mode:'advanced') - Change Zoom meetings selector to fetch upcoming_meetings instead of only scheduled Co-Authored-By: Claude Opus 4.6 --- .../tools/microsoft_planner/plans/route.ts | 76 ++++++----------- .../tools/microsoft_planner/tasks/route.ts | 81 ++++++------------- apps/sim/app/api/tools/zoom/meetings/route.ts | 2 +- .../workflow-block/workflow-block.tsx | 14 ++++ apps/sim/blocks/blocks/zoom.ts | 1 - apps/sim/hooks/selectors/registry.ts | 52 ++++++++++-- apps/sim/hooks/selectors/resolution.ts | 2 + apps/sim/hooks/use-selector-display-name.ts | 16 ++++ 8 files changed, 127 insertions(+), 117 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_planner/plans/route.ts b/apps/sim/app/api/tools/microsoft_planner/plans/route.ts index 632483c55da..e43650d3d7a 100644 --- a/apps/sim/app/api/tools/microsoft_planner/plans/route.ts +++ b/apps/sim/app/api/tools/microsoft_planner/plans/route.ts @@ -1,74 +1,44 @@ -import { randomUUID } from 'crypto' -import { db } from '@sim/db' -import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { getSession } from '@/lib/auth' -import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' const logger = createLogger('MicrosoftPlannerPlansAPI') export const dynamic = 'force-dynamic' -export async function GET(request: NextRequest) { - const requestId = randomUUID().slice(0, 8) +export async function POST(request: Request) { + const requestId = generateRequestId() try { - const session = await getSession() + const body = await request.json() + const { credential, workflowId } = body - if (!session?.user?.id) { - logger.warn(`[${requestId}] Unauthenticated request rejected`) - return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + if (!credential) { + logger.error(`[${requestId}] Missing credential in request`) + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) } - const { searchParams } = new URL(request.url) - const credentialId = searchParams.get('credentialId') - - if (!credentialId) { - logger.error(`[${requestId}] Missing credentialId parameter`) - return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) - } - - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) - } - - if (resolved.workspaceId) { - const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils') - const perm = await getUserEntityPermissions( - session.user.id, - 'workspace', - resolved.workspaceId - ) - if (perm === null) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - } - - const credentials = await db - .select() - .from(account) - .where(eq(account.id, resolved.accountId)) - .limit(1) - - if (!credentials.length) { - logger.warn(`[${requestId}] Credential not found`, { credentialId }) - return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + 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 accountRow = credentials[0] - const accessToken = await refreshAccessTokenIfNeeded( - resolved.accountId, - accountRow.userId, + credential, + authz.credentialOwnerUserId, requestId ) - if (!accessToken) { logger.error(`[${requestId}] Failed to obtain valid access token`) - return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + return NextResponse.json( + { error: 'Failed to obtain valid access token', authRequired: true }, + { status: 401 } + ) } const response = await fetch('https://graph.microsoft.com/v1.0/me/planner/plans', { diff --git a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts index eecfdb48c75..761254fe8be 100644 --- a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts +++ b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts @@ -1,38 +1,29 @@ -import { randomUUID } from 'crypto' -import { db } from '@sim/db' -import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' -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 { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' -import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import type { PlannerTask } from '@/tools/microsoft_planner/types' const logger = createLogger('MicrosoftPlannerTasksAPI') -export async function GET(request: NextRequest) { - const requestId = randomUUID().slice(0, 8) +export const dynamic = 'force-dynamic' - try { - const session = await getSession() - - if (!session?.user?.id) { - logger.warn(`[${requestId}] Unauthenticated request rejected`) - return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) - } +export async function POST(request: Request) { + const requestId = generateRequestId() - const { searchParams } = new URL(request.url) - const credentialId = searchParams.get('credentialId') - const planId = searchParams.get('planId') + try { + const body = await request.json() + const { credential, workflowId, planId } = body - if (!credentialId) { - logger.error(`[${requestId}] Missing credentialId parameter`) - return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) + if (!credential) { + logger.error(`[${requestId}] Missing credential in request`) + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) } if (!planId) { - logger.error(`[${requestId}] Missing planId parameter`) + logger.error(`[${requestId}] Missing planId in request`) return NextResponse.json({ error: 'Plan ID is required' }, { status: 400 }) } @@ -42,45 +33,25 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: planIdValidation.error }, { status: 400 }) } - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) - } - - if (resolved.workspaceId) { - const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils') - const perm = await getUserEntityPermissions( - session.user.id, - 'workspace', - resolved.workspaceId - ) - if (perm === null) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - } - - const credentials = await db - .select() - .from(account) - .where(eq(account.id, resolved.accountId)) - .limit(1) - - if (!credentials.length) { - logger.warn(`[${requestId}] Credential not found`, { credentialId }) - return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + 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 accountRow = credentials[0] - const accessToken = await refreshAccessTokenIfNeeded( - resolved.accountId, - accountRow.userId, + credential, + authz.credentialOwnerUserId, requestId ) - if (!accessToken) { logger.error(`[${requestId}] Failed to obtain valid access token`) - return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + return NextResponse.json( + { error: 'Failed to obtain valid access token', authRequired: true }, + { status: 401 } + ) } const response = await fetch(`https://graph.microsoft.com/v1.0/planner/plans/${planId}/tasks`, { diff --git a/apps/sim/app/api/tools/zoom/meetings/route.ts b/apps/sim/app/api/tools/zoom/meetings/route.ts index 3a947acd088..9869ff0b7e5 100644 --- a/apps/sim/app/api/tools/zoom/meetings/route.ts +++ b/apps/sim/app/api/tools/zoom/meetings/route.ts @@ -44,7 +44,7 @@ export async function POST(request: Request) { } const response = await fetch( - 'https://api.zoom.us/v2/users/me/meetings?page_size=300&type=scheduled', + 'https://api.zoom.us/v2/users/me/meetings?page_size=300&type=upcoming_meetings', { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index ae10a76a029..bb82f6254ca 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -553,6 +553,13 @@ const SubBlockRow = memo(function SubBlockRow({ const teamIdValue = getStringValue('teamId') const projectIdValue = getStringValue('projectId') const planIdValue = getStringValue('planId') + const baseIdValue = getStringValue('baseId') + const datasetIdValue = getStringValue('datasetId') + const serviceDeskIdValue = getStringValue('serviceDeskId') + const siteIdValue = getStringValue('siteId') + const collectionIdValue = getStringValue('collectionId') + const spreadsheetIdValue = getStringValue('spreadsheetId') + const fileIdValue = getStringValue('fileId') const { displayName: selectorDisplayName } = useSelectorDisplayName({ subBlock, @@ -564,6 +571,13 @@ const SubBlockRow = memo(function SubBlockRow({ teamId: teamIdValue, projectId: projectIdValue, planId: planIdValue, + baseId: baseIdValue, + datasetId: datasetIdValue, + serviceDeskId: serviceDeskIdValue, + siteId: siteIdValue, + collectionId: collectionIdValue, + spreadsheetId: spreadsheetIdValue, + fileId: fileIdValue, }) const { knowledgeBase: kbForDisplayName } = useKnowledgeBase( diff --git a/apps/sim/blocks/blocks/zoom.ts b/apps/sim/blocks/blocks/zoom.ts index 23b7dc70a84..9b74422ae2c 100644 --- a/apps/sim/blocks/blocks/zoom.ts +++ b/apps/sim/blocks/blocks/zoom.ts @@ -141,7 +141,6 @@ export const ZoomBlock: BlockConfig = { title: 'Topic', type: 'short-input', placeholder: 'Meeting topic (optional)', - mode: 'advanced', condition: { field: 'operation', value: ['zoom_update_meeting'], diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 391b38f1d4e..d231780b72f 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -592,16 +592,20 @@ const registry: Record = { enabled: ({ context }) => Boolean(context.credentialId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'microsoft.planner.plans') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) const data = await fetchJson<{ plans: PlannerPlan[] }>('/api/tools/microsoft_planner/plans', { - searchParams: { credentialId }, + method: 'POST', + body, }) return (data.plans || []).map((plan) => ({ id: plan.id, label: plan.title })) }, fetchById: async ({ context, detailId }: SelectorQueryArgs) => { if (!detailId) return null const credentialId = ensureCredential(context, 'microsoft.planner.plans') + const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) const data = await fetchJson<{ plans: PlannerPlan[] }>('/api/tools/microsoft_planner/plans', { - searchParams: { credentialId }, + method: 'POST', + body, }) const plan = (data.plans || []).find((p) => p.id === detailId) ?? null if (!plan) return null @@ -1015,10 +1019,11 @@ const registry: Record = { ], enabled: ({ context }) => Boolean(context.credentialId), fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'sharepoint.sites') const data = await fetchJson<{ files: { id: string; name: string }[] }>( '/api/tools/sharepoint/sites', { - searchParams: { credentialId: context.credentialId }, + searchParams: { credentialId }, } ) return (data.files || []).map((file) => ({ @@ -1026,6 +1031,19 @@ const registry: Record = { label: file.name, })) }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'sharepoint.sites') + const data = await fetchJson<{ files: { id: string; name: string }[] }>( + '/api/tools/sharepoint/sites', + { + searchParams: { credentialId }, + } + ) + const site = (data.files || []).find((f) => f.id === detailId) ?? null + if (!site) return null + return { id: site.id, label: site.name } + }, }, 'microsoft.planner': { key: 'microsoft.planner', @@ -1038,17 +1056,37 @@ const registry: Record = { ], enabled: ({ context }) => Boolean(context.credentialId && context.planId), fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'microsoft.planner') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + planId: context.planId, + }) const data = await fetchJson<{ tasks: PlannerTask[] }>('/api/tools/microsoft_planner/tasks', { - searchParams: { - credentialId: context.credentialId, - planId: context.planId, - }, + method: 'POST', + body, }) return (data.tasks || []).map((task) => ({ id: task.id, label: task.title, })) }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId) return null + const credentialId = ensureCredential(context, 'microsoft.planner') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + planId: context.planId, + }) + const data = await fetchJson<{ tasks: PlannerTask[] }>('/api/tools/microsoft_planner/tasks', { + method: 'POST', + body, + }) + const task = (data.tasks || []).find((t) => t.id === detailId) ?? null + if (!task) return null + return { id: task.id, label: task.title } + }, }, 'jira.projects': { key: 'jira.projects', diff --git a/apps/sim/hooks/selectors/resolution.ts b/apps/sim/hooks/selectors/resolution.ts index b37b1761320..81986860adb 100644 --- a/apps/sim/hooks/selectors/resolution.ts +++ b/apps/sim/hooks/selectors/resolution.ts @@ -18,6 +18,7 @@ export interface SelectorResolutionArgs { siteId?: string collectionId?: string spreadsheetId?: string + fileId?: string baseId?: string datasetId?: string serviceDeskId?: string @@ -41,6 +42,7 @@ export function resolveSelectorForSubBlock( siteId: args.siteId, collectionId: args.collectionId, spreadsheetId: args.spreadsheetId, + fileId: args.fileId, baseId: args.baseId, datasetId: args.datasetId, serviceDeskId: args.serviceDeskId, diff --git a/apps/sim/hooks/use-selector-display-name.ts b/apps/sim/hooks/use-selector-display-name.ts index 7ead2462669..24d2fe51ebe 100644 --- a/apps/sim/hooks/use-selector-display-name.ts +++ b/apps/sim/hooks/use-selector-display-name.ts @@ -21,6 +21,10 @@ interface SelectorDisplayNameArgs { baseId?: string datasetId?: string serviceDeskId?: string + siteId?: string + collectionId?: string + spreadsheetId?: string + fileId?: string } export function useSelectorDisplayName({ @@ -36,6 +40,10 @@ export function useSelectorDisplayName({ baseId, datasetId, serviceDeskId, + siteId, + collectionId, + spreadsheetId, + fileId, }: SelectorDisplayNameArgs) { const detailId = typeof value === 'string' && value.length > 0 ? value : undefined @@ -52,6 +60,10 @@ export function useSelectorDisplayName({ baseId, datasetId, serviceDeskId, + siteId, + collectionId, + spreadsheetId, + fileId, }) }, [ subBlock, @@ -66,6 +78,10 @@ export function useSelectorDisplayName({ baseId, datasetId, serviceDeskId, + siteId, + collectionId, + spreadsheetId, + fileId, ]) const key = resolution?.key From 92e96e812bcdd312bfc50f3f9a12534a00fb7f32 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 22:39:29 -0800 Subject: [PATCH 19/32] style: lint formatting fixes Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/microsoft_planner/tasks/route.ts | 2 +- apps/sim/blocks/blocks/confluence.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts index 761254fe8be..5e07b0722d3 100644 --- a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts +++ b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts @@ -1,8 +1,8 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { generateRequestId } from '@/lib/core/utils/request' import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' +import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import type { PlannerTask } from '@/tools/microsoft_planner/types' diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index fe7eca52f64..61090fd4983 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -381,7 +381,6 @@ export const ConfluenceBlock: BlockConfig = { }, } - export const ConfluenceV2Block: BlockConfig = { ...ConfluenceBlock, type: 'confluence_v2', From fe344622d52f852e2a10003842d42d3dc24df193 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 22:45:42 -0800 Subject: [PATCH 20/32] fix(selectors): consolidate Notion canonical param pairs into array conditions Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/notion.ts | 79 ++------------------------------ 1 file changed, 4 insertions(+), 75 deletions(-) diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 2151d1ae468..f222565fdf4 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -95,7 +95,7 @@ export const NotionBlock: BlockConfig = { mode: 'basic', condition: { field: 'operation', - value: 'notion_read_database', + value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], }, required: true, }, @@ -108,7 +108,7 @@ export const NotionBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: 'notion_read_database', + value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'], }, required: true, }, @@ -122,7 +122,7 @@ export const NotionBlock: BlockConfig = { placeholder: 'Select parent page', dependsOn: ['credential'], mode: 'basic', - condition: { field: 'operation', value: 'notion_create_page' }, + condition: { field: 'operation', value: ['notion_create_page', 'notion_create_database'] }, required: true, }, { @@ -132,7 +132,7 @@ export const NotionBlock: BlockConfig = { canonicalParamId: 'parentId', placeholder: 'ID of parent page', mode: 'advanced', - condition: { field: 'operation', value: 'notion_create_page' }, + condition: { field: 'operation', value: ['notion_create_page', 'notion_create_database'] }, required: true, }, { @@ -187,30 +187,6 @@ export const NotionBlock: BlockConfig = { }, }, // Query Database Fields - { - id: 'databaseSelector', - title: 'Database', - type: 'project-selector', - canonicalParamId: 'databaseId', - serviceId: 'notion', - selectorKey: 'notion.databases', - selectorAllowSearch: false, - placeholder: 'Select Notion database', - dependsOn: ['credential'], - mode: 'basic', - condition: { field: 'operation', value: 'notion_query_database' }, - required: true, - }, - { - id: 'databaseId', - title: 'Database ID', - type: 'short-input', - canonicalParamId: 'databaseId', - placeholder: 'Enter Notion database ID', - mode: 'advanced', - condition: { field: 'operation', value: 'notion_query_database' }, - required: true, - }, { id: 'filter', title: 'Filter', @@ -273,29 +249,6 @@ export const NotionBlock: BlockConfig = { condition: { field: 'operation', value: 'notion_search' }, }, // Create Database Fields - { - id: 'parentSelector', - title: 'Parent Page', - type: 'file-selector', - canonicalParamId: 'parentId', - serviceId: 'notion', - selectorKey: 'notion.pages', - placeholder: 'Select parent page', - dependsOn: ['credential'], - mode: 'basic', - condition: { field: 'operation', value: 'notion_create_database' }, - required: true, - }, - { - id: 'parentId', - title: 'Parent Page ID', - type: 'short-input', - canonicalParamId: 'parentId', - placeholder: 'ID of parent page where database will be created', - mode: 'advanced', - condition: { field: 'operation', value: 'notion_create_database' }, - required: true, - }, { id: 'title', title: 'Database Title', @@ -326,30 +279,6 @@ export const NotionBlock: BlockConfig = { }, }, // Add Database Row Fields - { - id: 'databaseSelector', - title: 'Database', - type: 'project-selector', - canonicalParamId: 'databaseId', - serviceId: 'notion', - selectorKey: 'notion.databases', - selectorAllowSearch: false, - placeholder: 'Select Notion database', - dependsOn: ['credential'], - mode: 'basic', - condition: { field: 'operation', value: 'notion_add_database_row' }, - required: true, - }, - { - id: 'databaseId', - title: 'Database ID', - type: 'short-input', - canonicalParamId: 'databaseId', - placeholder: 'Enter Notion database ID', - mode: 'advanced', - condition: { field: 'operation', value: 'notion_add_database_row' }, - required: true, - }, { id: 'properties', title: 'Row Properties', From 789b8d7a4ce422f6566bdea91dae804e6b773af2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 22:52:40 -0800 Subject: [PATCH 21/32] fix(selectors): add missing selectorKey to Confluence V1 page selector Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/confluence.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 61090fd4983..b71bc653e0f 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -103,6 +103,7 @@ export const ConfluenceBlock: BlockConfig = { type: 'file-selector', canonicalParamId: 'pageId', serviceId: 'confluence', + selectorKey: 'confluence.pages', placeholder: 'Select Confluence page', dependsOn: ['credential', 'domain'], mode: 'basic', From 8ad66605d03d29574b9c6845586fc1b69201e309 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 23:00:10 -0800 Subject: [PATCH 22/32] fix(selectors): use sanitized IDs in URLs, convert SharePoint routes to POST+authorizeCredentialUse - Use planIdValidation.sanitized in MS Planner tasks fetch URL - Convert sharepoint/lists and sharepoint/sites from GET+getSession to POST+authorizeCredentialUse - Update registry entries to match POST pattern Co-Authored-By: Claude Opus 4.6 --- .../tools/microsoft_planner/tasks/route.ts | 13 +-- .../app/api/tools/sharepoint/lists/route.ts | 83 ++++++------------- .../app/api/tools/sharepoint/sites/route.ts | 82 ++++++------------ apps/sim/hooks/selectors/registry.ts | 36 +++++--- 4 files changed, 83 insertions(+), 131 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts index 5e07b0722d3..db0ccd88ae6 100644 --- a/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts +++ b/apps/sim/app/api/tools/microsoft_planner/tasks/route.ts @@ -54,11 +54,14 @@ export async function POST(request: Request) { ) } - const response = await fetch(`https://graph.microsoft.com/v1.0/planner/plans/${planId}/tasks`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }) + const response = await fetch( + `https://graph.microsoft.com/v1.0/planner/plans/${planIdValidation.sanitized}/tasks`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) if (!response.ok) { const errorText = await response.text() diff --git a/apps/sim/app/api/tools/sharepoint/lists/route.ts b/apps/sim/app/api/tools/sharepoint/lists/route.ts index 32ca9eb4897..d5da2a712eb 100644 --- a/apps/sim/app/api/tools/sharepoint/lists/route.ts +++ b/apps/sim/app/api/tools/sharepoint/lists/route.ts @@ -1,12 +1,8 @@ -import { randomUUID } from 'crypto' -import { db } from '@sim/db' -import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { getSession } from '@/lib/auth' -import { validateAlphanumericId } from '@/lib/core/security/input-validation' -import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' export const dynamic = 'force-dynamic' @@ -22,27 +18,20 @@ interface SharePointList { } } -/** - * Get SharePoint lists for a given site from Microsoft Graph API - */ -export async function GET(request: NextRequest) { - const requestId = randomUUID().slice(0, 8) +export async function POST(request: Request) { + const requestId = generateRequestId() try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) - } - - const { searchParams } = new URL(request.url) - const credentialId = searchParams.get('credentialId') - const siteId = searchParams.get('siteId') + const body = await request.json() + const { credential, workflowId, siteId } = body - if (!credentialId) { - return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) + if (!credential) { + logger.error(`[${requestId}] Missing credential in request`) + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) } if (!siteId) { + logger.error(`[${requestId}] Missing siteId in request`) return NextResponse.json({ error: 'Site ID is required' }, { status: 400 }) } @@ -51,47 +40,25 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Invalid site ID format' }, { status: 400 }) } - const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255) - if (!credentialIdValidation.isValid) { - logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error }) - return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 }) - } - - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) - } - - if (resolved.workspaceId) { - const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils') - const perm = await getUserEntityPermissions( - session.user.id, - 'workspace', - resolved.workspaceId - ) - if (perm === null) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - } - - const credentials = await db - .select() - .from(account) - .where(eq(account.id, resolved.accountId)) - .limit(1) - if (!credentials.length) { - return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + 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 accountRow = credentials[0] - const accessToken = await refreshAccessTokenIfNeeded( - resolved.accountId, - accountRow.userId, + credential, + authz.credentialOwnerUserId, requestId ) if (!accessToken) { - return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + logger.error(`[${requestId}] Failed to obtain valid access token`) + return NextResponse.json( + { error: 'Failed to obtain valid access token', authRequired: true }, + { status: 401 } + ) } const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100` diff --git a/apps/sim/app/api/tools/sharepoint/sites/route.ts b/apps/sim/app/api/tools/sharepoint/sites/route.ts index de161b97309..2119fe975c6 100644 --- a/apps/sim/app/api/tools/sharepoint/sites/route.ts +++ b/apps/sim/app/api/tools/sharepoint/sites/route.ts @@ -1,79 +1,45 @@ -import { randomUUID } from 'crypto' -import { db } from '@sim/db' -import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { getSession } from '@/lib/auth' -import { validateAlphanumericId } from '@/lib/core/security/input-validation' -import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import type { SharepointSite } from '@/tools/sharepoint/types' export const dynamic = 'force-dynamic' const logger = createLogger('SharePointSitesAPI') -/** - * Get SharePoint sites from Microsoft Graph API - */ -export async function GET(request: NextRequest) { - const requestId = randomUUID().slice(0, 8) +export async function POST(request: Request) { + const requestId = generateRequestId() try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) - } - - const { searchParams } = new URL(request.url) - const credentialId = searchParams.get('credentialId') - const query = searchParams.get('query') || '' - - if (!credentialId) { - return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) - } + const body = await request.json() + const { credential, workflowId, query } = body - const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255) - if (!credentialIdValidation.isValid) { - logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error }) - return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 }) + if (!credential) { + logger.error(`[${requestId}] Missing credential in request`) + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) } - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) - } - - if (resolved.workspaceId) { - const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils') - const perm = await getUserEntityPermissions( - session.user.id, - 'workspace', - resolved.workspaceId - ) - if (perm === null) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - } - - const credentials = await db - .select() - .from(account) - .where(eq(account.id, resolved.accountId)) - .limit(1) - if (!credentials.length) { - return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + 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 accountRow = credentials[0] - const accessToken = await refreshAccessTokenIfNeeded( - resolved.accountId, - accountRow.userId, + credential, + authz.credentialOwnerUserId, requestId ) if (!accessToken) { - return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 }) + logger.error(`[${requestId}] Failed to obtain valid access token`) + return NextResponse.json( + { error: 'Failed to obtain valid access token', authRequired: true }, + { status: 401 } + ) } const searchQuery = query || '*' diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index d231780b72f..52467cc9ef2 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -727,22 +727,28 @@ const registry: Record = { fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'sharepoint.lists') if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + siteId: context.siteId, + }) const data = await fetchJson<{ lists: SharepointList[] }>('/api/tools/sharepoint/lists', { - searchParams: { - credentialId, - siteId: context.siteId, - }, + method: 'POST', + body, }) return (data.lists || []).map((list) => ({ id: list.id, label: list.displayName })) }, fetchById: async ({ context, detailId }: SelectorQueryArgs) => { if (!detailId || !context.siteId) return null const credentialId = ensureCredential(context, 'sharepoint.lists') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + siteId: context.siteId, + }) const data = await fetchJson<{ lists: SharepointList[] }>('/api/tools/sharepoint/lists', { - searchParams: { - credentialId, - siteId: context.siteId, - }, + method: 'POST', + body, }) const list = (data.lists || []).find((l) => l.id === detailId) ?? null if (!list) return null @@ -1020,10 +1026,15 @@ const registry: Record = { enabled: ({ context }) => Boolean(context.credentialId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'sharepoint.sites') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + }) const data = await fetchJson<{ files: { id: string; name: string }[] }>( '/api/tools/sharepoint/sites', { - searchParams: { credentialId }, + method: 'POST', + body, } ) return (data.files || []).map((file) => ({ @@ -1034,10 +1045,15 @@ const registry: Record = { fetchById: async ({ context, detailId }: SelectorQueryArgs) => { if (!detailId) return null const credentialId = ensureCredential(context, 'sharepoint.sites') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + }) const data = await fetchJson<{ files: { id: string; name: string }[] }>( '/api/tools/sharepoint/sites', { - searchParams: { credentialId }, + method: 'POST', + body, } ) const site = (data.files || []).find((f) => f.id === detailId) ?? null From b527d23fcedbce73b244debbac8f96938e337716 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 23:12:35 -0800 Subject: [PATCH 23/32] fix(selectors): revert Zoom meetings type to scheduled for broader compatibility Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/zoom/meetings/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/zoom/meetings/route.ts b/apps/sim/app/api/tools/zoom/meetings/route.ts index 9869ff0b7e5..3a947acd088 100644 --- a/apps/sim/app/api/tools/zoom/meetings/route.ts +++ b/apps/sim/app/api/tools/zoom/meetings/route.ts @@ -44,7 +44,7 @@ export async function POST(request: Request) { } const response = await fetch( - 'https://api.zoom.us/v2/users/me/meetings?page_size=300&type=upcoming_meetings', + 'https://api.zoom.us/v2/users/me/meetings?page_size=300&type=scheduled', { headers: { Authorization: `Bearer ${accessToken}`, From ed6feb317134f456285e4e94d96fa8c78e168f98 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 23:26:22 -0800 Subject: [PATCH 24/32] fix(selectors): add SharePoint site ID validator, fix cascading selector display name fallbacks - Add validateSharePointSiteId to input-validation.ts - Use validation util in SharePoint lists route instead of inline regex - Add || fallback to selector IDs in workflow-block.tsx so cascading display names resolve in basic mode (baseSelector, planSelector, etc.) Co-Authored-By: Claude Opus 4.6 --- .../app/api/tools/sharepoint/lists/route.ts | 15 +++---- .../workflow-block/workflow-block.tsx | 13 +++--- .../sim/lib/core/security/input-validation.ts | 45 +++++++++++++++++++ 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/api/tools/sharepoint/lists/route.ts b/apps/sim/app/api/tools/sharepoint/lists/route.ts index d5da2a712eb..fbbbaab6817 100644 --- a/apps/sim/app/api/tools/sharepoint/lists/route.ts +++ b/apps/sim/app/api/tools/sharepoint/lists/route.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateSharePointSiteId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -30,14 +31,10 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) } - if (!siteId) { - logger.error(`[${requestId}] Missing siteId in request`) - return NextResponse.json({ error: 'Site ID is required' }, { status: 400 }) - } - - const SITE_ID_RE = /^[\w.\-,]+$/ - if (siteId.length > 512 || !SITE_ID_RE.test(siteId)) { - return NextResponse.json({ error: 'Invalid site ID format' }, { status: 400 }) + const siteIdValidation = validateSharePointSiteId(siteId) + if (!siteIdValidation.isValid) { + logger.error(`[${requestId}] Invalid siteId: ${siteIdValidation.error}`) + return NextResponse.json({ error: siteIdValidation.error }, { status: 400 }) } const authz = await authorizeCredentialUse(request as any, { @@ -61,7 +58,7 @@ export async function POST(request: Request) { ) } - const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100` + const url = `https://graph.microsoft.com/v1.0/sites/${siteIdValidation.sanitized}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100` const response = await fetch(url, { headers: { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index bb82f6254ca..a1878ab9503 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -552,12 +552,13 @@ const SubBlockRow = memo(function SubBlockRow({ const domainValue = getStringValue('domain') const teamIdValue = getStringValue('teamId') const projectIdValue = getStringValue('projectId') - const planIdValue = getStringValue('planId') - const baseIdValue = getStringValue('baseId') - const datasetIdValue = getStringValue('datasetId') - const serviceDeskIdValue = getStringValue('serviceDeskId') - const siteIdValue = getStringValue('siteId') - const collectionIdValue = getStringValue('collectionId') + const planIdValue = getStringValue('planId') || getStringValue('planSelector') + const baseIdValue = getStringValue('baseId') || getStringValue('baseSelector') + const datasetIdValue = getStringValue('datasetId') || getStringValue('datasetSelector') + const serviceDeskIdValue = + getStringValue('serviceDeskId') || getStringValue('serviceDeskSelector') + const siteIdValue = getStringValue('siteId') || getStringValue('siteSelector') + const collectionIdValue = getStringValue('collectionId') || getStringValue('collectionSelector') const spreadsheetIdValue = getStringValue('spreadsheetId') const fileIdValue = getStringValue('fileId') diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index cd005277ba1..94d20023749 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -553,6 +553,51 @@ export function validateMicrosoftGraphId( return { isValid: true, sanitized: value } } +/** + * Validates SharePoint site IDs used in Microsoft Graph API. + * + * Site IDs are compound identifiers: `hostname,spsite-guid,spweb-guid` + * (e.g. `contoso.sharepoint.com,2C712604-1370-44E7-A1F5-426573FDA80A,2D2244C3-251A-49EA-93A8-39E1C3A060FE`). + * The API also accepts partial forms like a single GUID or just a hostname. + * + * Allowed characters: alphanumeric, periods, hyphens, and commas. + * + * @param value - The SharePoint site ID to validate + * @param paramName - Name of the parameter for error messages + * @returns ValidationResult + */ +export function validateSharePointSiteId( + value: string | null | undefined, + paramName = 'siteId' +): ValidationResult { + if (value === null || value === undefined || value === '') { + return { + isValid: false, + error: `${paramName} is required`, + } + } + + if (value.length > 512) { + return { + isValid: false, + error: `${paramName} exceeds maximum length`, + } + } + + if (!/^[\w.\-,]+$/.test(value)) { + logger.warn('Invalid characters in SharePoint site ID', { + paramName, + value: value.substring(0, 100), + }) + return { + isValid: false, + error: `${paramName} contains invalid characters`, + } + } + + return { isValid: true, sanitized: value } +} + /** * Validates Jira Cloud IDs (typically UUID format) * From 13c25ecf7b466815aea5f7047cedb5338e075f13 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Mar 2026 23:40:28 -0800 Subject: [PATCH 25/32] fix(selectors): hoist requestId before try block in all selector routes Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/airtable/bases/route.ts | 2 +- apps/sim/app/api/tools/airtable/tables/route.ts | 2 +- apps/sim/app/api/tools/asana/workspaces/route.ts | 2 +- apps/sim/app/api/tools/attio/lists/route.ts | 2 +- apps/sim/app/api/tools/attio/objects/route.ts | 2 +- apps/sim/app/api/tools/calcom/event-types/route.ts | 2 +- apps/sim/app/api/tools/calcom/schedules/route.ts | 2 +- apps/sim/app/api/tools/confluence/selector-spaces/route.ts | 2 +- apps/sim/app/api/tools/google_bigquery/datasets/route.ts | 2 +- apps/sim/app/api/tools/google_bigquery/tables/route.ts | 2 +- apps/sim/app/api/tools/google_tasks/task-lists/route.ts | 2 +- apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts | 2 +- apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts | 2 +- apps/sim/app/api/tools/notion/databases/route.ts | 2 +- apps/sim/app/api/tools/notion/pages/route.ts | 2 +- apps/sim/app/api/tools/pipedrive/pipelines/route.ts | 2 +- apps/sim/app/api/tools/zoom/meetings/route.ts | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/api/tools/airtable/bases/route.ts b/apps/sim/app/api/tools/airtable/bases/route.ts index 7c9799c4ece..839c1359dd3 100644 --- a/apps/sim/app/api/tools/airtable/bases/route.ts +++ b/apps/sim/app/api/tools/airtable/bases/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('AirtableBasesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/airtable/tables/route.ts b/apps/sim/app/api/tools/airtable/tables/route.ts index 32a2fdd90bb..41cd68dc12f 100644 --- a/apps/sim/app/api/tools/airtable/tables/route.ts +++ b/apps/sim/app/api/tools/airtable/tables/route.ts @@ -10,8 +10,8 @@ const logger = createLogger('AirtableTablesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId, baseId } = body diff --git a/apps/sim/app/api/tools/asana/workspaces/route.ts b/apps/sim/app/api/tools/asana/workspaces/route.ts index e00e60e263c..2393ade11c9 100644 --- a/apps/sim/app/api/tools/asana/workspaces/route.ts +++ b/apps/sim/app/api/tools/asana/workspaces/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('AsanaWorkspacesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/attio/lists/route.ts b/apps/sim/app/api/tools/attio/lists/route.ts index 75e0909901f..1575f7eb3a0 100644 --- a/apps/sim/app/api/tools/attio/lists/route.ts +++ b/apps/sim/app/api/tools/attio/lists/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('AttioListsAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/attio/objects/route.ts b/apps/sim/app/api/tools/attio/objects/route.ts index 559253b6a17..ae3ba5152dd 100644 --- a/apps/sim/app/api/tools/attio/objects/route.ts +++ b/apps/sim/app/api/tools/attio/objects/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('AttioObjectsAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/calcom/event-types/route.ts b/apps/sim/app/api/tools/calcom/event-types/route.ts index cfcfd707bf4..92a30a49abe 100644 --- a/apps/sim/app/api/tools/calcom/event-types/route.ts +++ b/apps/sim/app/api/tools/calcom/event-types/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('CalcomEventTypesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/calcom/schedules/route.ts b/apps/sim/app/api/tools/calcom/schedules/route.ts index 8ca3cb49a94..23bcfd0fe24 100644 --- a/apps/sim/app/api/tools/calcom/schedules/route.ts +++ b/apps/sim/app/api/tools/calcom/schedules/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('CalcomSchedulesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/confluence/selector-spaces/route.ts b/apps/sim/app/api/tools/confluence/selector-spaces/route.ts index 7c3669ea1bf..7ae61d3e983 100644 --- a/apps/sim/app/api/tools/confluence/selector-spaces/route.ts +++ b/apps/sim/app/api/tools/confluence/selector-spaces/route.ts @@ -11,8 +11,8 @@ const logger = createLogger('ConfluenceSelectorSpacesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId, domain } = body diff --git a/apps/sim/app/api/tools/google_bigquery/datasets/route.ts b/apps/sim/app/api/tools/google_bigquery/datasets/route.ts index 5e9a604759e..ffc4ef7235d 100644 --- a/apps/sim/app/api/tools/google_bigquery/datasets/route.ts +++ b/apps/sim/app/api/tools/google_bigquery/datasets/route.ts @@ -17,8 +17,8 @@ export const dynamic = 'force-dynamic' * @returns JSON response with a `datasets` array, each entry containing `datasetReference` and optional `friendlyName` */ export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId, projectId } = body diff --git a/apps/sim/app/api/tools/google_bigquery/tables/route.ts b/apps/sim/app/api/tools/google_bigquery/tables/route.ts index 5ac2a0d3705..f2f7c6c43c4 100644 --- a/apps/sim/app/api/tools/google_bigquery/tables/route.ts +++ b/apps/sim/app/api/tools/google_bigquery/tables/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('GoogleBigQueryTablesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId, projectId, datasetId } = body diff --git a/apps/sim/app/api/tools/google_tasks/task-lists/route.ts b/apps/sim/app/api/tools/google_tasks/task-lists/route.ts index 4b37e25129b..6448f216505 100644 --- a/apps/sim/app/api/tools/google_tasks/task-lists/route.ts +++ b/apps/sim/app/api/tools/google_tasks/task-lists/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('GoogleTasksTaskListsAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts index 368c2707f37..a9ef02bec86 100644 --- a/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-requesttypes/route.ts @@ -11,8 +11,8 @@ const logger = createLogger('JsmSelectorRequestTypesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId, domain, serviceDeskId } = body diff --git a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts index 17bfbbbaa26..b4bc93032fb 100644 --- a/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts +++ b/apps/sim/app/api/tools/jsm/selector-servicedesks/route.ts @@ -11,8 +11,8 @@ const logger = createLogger('JsmSelectorServiceDesksAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId, domain } = body diff --git a/apps/sim/app/api/tools/notion/databases/route.ts b/apps/sim/app/api/tools/notion/databases/route.ts index 8c9104478cb..1dee214a2d9 100644 --- a/apps/sim/app/api/tools/notion/databases/route.ts +++ b/apps/sim/app/api/tools/notion/databases/route.ts @@ -10,8 +10,8 @@ const logger = createLogger('NotionDatabasesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/notion/pages/route.ts b/apps/sim/app/api/tools/notion/pages/route.ts index 2a9cf9e176f..0a0bd4f4703 100644 --- a/apps/sim/app/api/tools/notion/pages/route.ts +++ b/apps/sim/app/api/tools/notion/pages/route.ts @@ -10,8 +10,8 @@ const logger = createLogger('NotionPagesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/pipedrive/pipelines/route.ts b/apps/sim/app/api/tools/pipedrive/pipelines/route.ts index 2386b06e03b..57df65fd8d7 100644 --- a/apps/sim/app/api/tools/pipedrive/pipelines/route.ts +++ b/apps/sim/app/api/tools/pipedrive/pipelines/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('PipedrivePipelinesAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body diff --git a/apps/sim/app/api/tools/zoom/meetings/route.ts b/apps/sim/app/api/tools/zoom/meetings/route.ts index 3a947acd088..b1df1ab4ddb 100644 --- a/apps/sim/app/api/tools/zoom/meetings/route.ts +++ b/apps/sim/app/api/tools/zoom/meetings/route.ts @@ -9,8 +9,8 @@ const logger = createLogger('ZoomMeetingsAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body From 706751af1ef02a486de146bf886d233dc2898440 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Mar 2026 05:40:18 -0800 Subject: [PATCH 26/32] fix(selectors): hoist requestId before try block in Trello boards route Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/trello/boards/route.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/sim/app/api/tools/trello/boards/route.ts b/apps/sim/app/api/tools/trello/boards/route.ts index fa251ac9e05..7c5f188a11a 100644 --- a/apps/sim/app/api/tools/trello/boards/route.ts +++ b/apps/sim/app/api/tools/trello/boards/route.ts @@ -9,14 +9,13 @@ const logger = createLogger('TrelloBoardsAPI') export const dynamic = 'force-dynamic' export async function POST(request: Request) { + const requestId = generateRequestId() try { const apiKey = process.env.TRELLO_API_KEY if (!apiKey) { logger.error('Trello API key not configured') return NextResponse.json({ error: 'Trello API key not configured' }, { status: 500 }) } - - const requestId = generateRequestId() const body = await request.json() const { credential, workflowId } = body From 61d4cbf39fefc6c88c7c320b1fe12e231e15d317 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Mar 2026 05:59:35 -0800 Subject: [PATCH 27/32] fix(selectors): guard selector queries against unresolved variable references Skip fetchById and context population when values are design-time placeholders ( or {{ENV_VAR}}) rather than real IDs. Co-Authored-By: Claude Opus 4.6 --- .../editor/components/sub-block/hooks/use-selector-setup.ts | 2 ++ apps/sim/hooks/selectors/use-selector-query.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts index 6401273d142..e9acaca7dcb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react' import { useParams } from 'next/navigation' import type { SubBlockConfig } from '@/blocks/types' +import { isEnvVarReference, isReference } from '@/executor/constants' import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useDependsOnGate } from './use-depends-on-gate' @@ -45,6 +46,7 @@ export function useSelectorSetup( if (value === null || value === undefined) continue const strValue = String(value) if (!strValue) continue + if (isReference(strValue) || isEnvVarReference(strValue)) continue const canonicalParamId = canonicalIndex.canonicalIdBySubBlockId[depKey] ?? depKey diff --git a/apps/sim/hooks/selectors/use-selector-query.ts b/apps/sim/hooks/selectors/use-selector-query.ts index 85a2aab98fd..4ed0770cdc7 100644 --- a/apps/sim/hooks/selectors/use-selector-query.ts +++ b/apps/sim/hooks/selectors/use-selector-query.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' +import { isEnvVarReference, isReference } from '@/executor/constants' import { getSelectorDefinition, mergeOption } from '@/hooks/selectors/registry' import type { SelectorKey, SelectorOption, SelectorQueryArgs } from '@/hooks/selectors/types' @@ -35,8 +36,10 @@ export function useSelectorOptionDetail( context: args.context, detailId: args.detailId, } + const hasRealDetailId = + Boolean(args.detailId) && !isReference(args.detailId!) && !isEnvVarReference(args.detailId!) const baseEnabled = - Boolean(args.detailId) && definition.fetchById !== undefined + hasRealDetailId && definition.fetchById !== undefined ? definition.enabled ? definition.enabled(queryArgs) : true From b2cb783acdc411ba68bd316f8c2bc6df355984eb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Mar 2026 06:23:23 -0800 Subject: [PATCH 28/32] refactor(selectors): replace hardcoded display name fallbacks with canonical-aware resolution Use resolveDependencyValue to resolve context values for useSelectorDisplayName, eliminating manual || getStringValue('*Selector') fallbacks that required updating for each new selector pair. Co-Authored-By: Claude Opus 4.6 --- .../workflow-block/workflow-block.tsx | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index a1878ab9503..de5694b7b9b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -549,18 +549,30 @@ const SubBlockRow = memo(function SubBlockRow({ return typeof option === 'string' ? option : option.label }, [subBlock, rawValue]) - const domainValue = getStringValue('domain') - const teamIdValue = getStringValue('teamId') - const projectIdValue = getStringValue('projectId') - const planIdValue = getStringValue('planId') || getStringValue('planSelector') - const baseIdValue = getStringValue('baseId') || getStringValue('baseSelector') - const datasetIdValue = getStringValue('datasetId') || getStringValue('datasetSelector') - const serviceDeskIdValue = - getStringValue('serviceDeskId') || getStringValue('serviceDeskSelector') - const siteIdValue = getStringValue('siteId') || getStringValue('siteSelector') - const collectionIdValue = getStringValue('collectionId') || getStringValue('collectionSelector') - const spreadsheetIdValue = getStringValue('spreadsheetId') - const fileIdValue = getStringValue('fileId') + const resolveContextValue = useCallback( + (key: string): string | undefined => { + const resolved = resolveDependencyValue( + key, + rawValues, + canonicalIndex || buildCanonicalIndex([]), + canonicalModeOverrides + ) + return typeof resolved === 'string' && resolved.length > 0 ? resolved : undefined + }, + [rawValues, canonicalIndex, canonicalModeOverrides] + ) + + const domainValue = resolveContextValue('domain') + const teamIdValue = resolveContextValue('teamId') + const projectIdValue = resolveContextValue('projectId') + const planIdValue = resolveContextValue('planId') + const baseIdValue = resolveContextValue('baseId') + const datasetIdValue = resolveContextValue('datasetId') + const serviceDeskIdValue = resolveContextValue('serviceDeskId') + const siteIdValue = resolveContextValue('siteId') + const collectionIdValue = resolveContextValue('collectionId') + const spreadsheetIdValue = resolveContextValue('spreadsheetId') + const fileIdValue = resolveContextValue('fileId') const { displayName: selectorDisplayName } = useSelectorDisplayName({ subBlock, From 3c76b473a1f11ecfe5faa8fa5ba75121fbda0ef9 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Mar 2026 06:29:57 -0800 Subject: [PATCH 29/32] fix(selectors): tighten SharePoint site ID validation to exclude underscores SharePoint composite site IDs use hostname,guid,guid format where only alphanumerics, periods, hyphens, and commas are valid characters. Co-Authored-By: Claude Opus 4.6 --- apps/sim/lib/core/security/input-validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 94d20023749..a62bd657218 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -584,7 +584,7 @@ export function validateSharePointSiteId( } } - if (!/^[\w.\-,]+$/.test(value)) { + if (!/^[a-zA-Z0-9.\-,]+$/.test(value)) { logger.warn('Invalid characters in SharePoint site ID', { paramName, value: value.substring(0, 100), From 6b301b700f5d1631308982d1bfce61cc48a00d02 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Mar 2026 10:08:26 -0800 Subject: [PATCH 30/32] fix(selectors): ensure string IDs in Pipedrive/Cal.com routes, fix Trello closed board filter Pipedrive pipelines and Cal.com event-types/schedules routes now consistently return string IDs via String() conversion. Trello boards route no longer filters out closed boards, preserving them for fetchById lookups. The closed filter is applied only in the registry's fetchList so archived boards don't appear in dropdowns but can still be resolved by ID for display names. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/calcom/event-types/route.ts | 2 +- apps/sim/app/api/tools/calcom/schedules/route.ts | 2 +- apps/sim/app/api/tools/pipedrive/pipelines/route.ts | 2 +- apps/sim/app/api/tools/trello/boards/route.ts | 11 +++++------ apps/sim/hooks/selectors/registry.ts | 6 ++++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/api/tools/calcom/event-types/route.ts b/apps/sim/app/api/tools/calcom/event-types/route.ts index 92a30a49abe..b8596f614f8 100644 --- a/apps/sim/app/api/tools/calcom/event-types/route.ts +++ b/apps/sim/app/api/tools/calcom/event-types/route.ts @@ -66,7 +66,7 @@ export async function POST(request: Request) { const data = await response.json() const eventTypes = (data.data || []).map( (eventType: { id: number; title: string; slug: string }) => ({ - id: eventType.id, + id: String(eventType.id), title: eventType.title, slug: eventType.slug, }) diff --git a/apps/sim/app/api/tools/calcom/schedules/route.ts b/apps/sim/app/api/tools/calcom/schedules/route.ts index 23bcfd0fe24..8f69328cc65 100644 --- a/apps/sim/app/api/tools/calcom/schedules/route.ts +++ b/apps/sim/app/api/tools/calcom/schedules/route.ts @@ -65,7 +65,7 @@ export async function POST(request: Request) { const data = await response.json() const schedules = (data.data || []).map((schedule: { id: number; name: string }) => ({ - id: schedule.id, + id: String(schedule.id), name: schedule.name, })) diff --git a/apps/sim/app/api/tools/pipedrive/pipelines/route.ts b/apps/sim/app/api/tools/pipedrive/pipelines/route.ts index 57df65fd8d7..ba188e6c386 100644 --- a/apps/sim/app/api/tools/pipedrive/pipelines/route.ts +++ b/apps/sim/app/api/tools/pipedrive/pipelines/route.ts @@ -64,7 +64,7 @@ export async function POST(request: Request) { const data = await response.json() const pipelines = (data.data || []).map((pipeline: { id: number; name: string }) => ({ - id: pipeline.id, + id: String(pipeline.id), name: pipeline.name, })) diff --git a/apps/sim/app/api/tools/trello/boards/route.ts b/apps/sim/app/api/tools/trello/boards/route.ts index 7c5f188a11a..fb4ca52738a 100644 --- a/apps/sim/app/api/tools/trello/boards/route.ts +++ b/apps/sim/app/api/tools/trello/boards/route.ts @@ -70,12 +70,11 @@ export async function POST(request: Request) { } const data = await response.json() - const boards = (data || []) - .filter((board: { id: string; name: string; closed: boolean }) => !board.closed) - .map((board: { id: string; name: string }) => ({ - id: board.id, - name: board.name, - })) + const boards = (data || []).map((board: { id: string; name: string; closed: boolean }) => ({ + id: board.id, + name: board.name, + closed: board.closed, + })) return NextResponse.json({ boards }) } catch (error) { diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 52467cc9ef2..ad95c65a126 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -32,7 +32,7 @@ type CalcomSchedule = { id: number; name: string } type GoogleTaskList = { id: string; title: string } type PlannerPlan = { id: string; title: string } type SharepointList = { id: string; displayName: string } -type TrelloBoard = { id: string; name: string } +type TrelloBoard = { id: string; name: string; closed?: boolean } type SlackChannel = { id: string; name: string } type SlackUser = { id: string; name: string; real_name: string } type FolderResponse = { id: string; name: string } @@ -771,7 +771,9 @@ const registry: Record = { method: 'POST', body, }) - return (data.boards || []).map((board) => ({ id: board.id, label: board.name })) + return (data.boards || []) + .filter((board) => !board.closed) + .map((board) => ({ id: board.id, label: board.name })) }, fetchById: async ({ context, detailId }: SelectorQueryArgs) => { if (!detailId) return null From 1addf327e29a0f12467a1f57cbc9cb72ab11d4b0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Mar 2026 10:25:13 -0800 Subject: [PATCH 31/32] fix(selectors): convert Zoom meeting IDs to strings for consistency Zoom API returns numeric meeting IDs. Convert with String() to match the string ID convention used by all other selector routes. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/zoom/meetings/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/zoom/meetings/route.ts b/apps/sim/app/api/tools/zoom/meetings/route.ts index b1df1ab4ddb..01360af7610 100644 --- a/apps/sim/app/api/tools/zoom/meetings/route.ts +++ b/apps/sim/app/api/tools/zoom/meetings/route.ts @@ -67,7 +67,7 @@ export async function POST(request: Request) { const data = await response.json() const meetings = (data.meetings || []).map((meeting: { id: number; topic: string }) => ({ - id: meeting.id, + id: String(meeting.id), name: meeting.topic, })) From db9a9fa50c16994f1f750aff22670486cd3de92e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Mar 2026 10:32:11 -0800 Subject: [PATCH 32/32] fix(selectors): align registry types with route string ID returns Routes already convert numeric IDs to strings via String(), so update the registry types (CalcomEventType, CalcomSchedule, PipedrivePipeline, ZoomMeeting) from id: number to id: string and remove the now-redundant String() coercions in fetchList/fetchById. Co-Authored-By: Claude Opus 4.6 --- apps/sim/hooks/selectors/registry.ts | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index ad95c65a126..db0d6b28f04 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -20,15 +20,15 @@ type BigQueryDataset = { friendlyName?: string } type BigQueryTable = { tableReference: { tableId: string }; friendlyName?: string } -type CalcomEventType = { id: number; title: string; slug: string } +type CalcomEventType = { id: string; title: string; slug: string } type ConfluenceSpace = { id: string; name: string; key: string } type JsmServiceDesk = { id: string; name: string } type JsmRequestType = { id: string; name: string } type NotionDatabase = { id: string; name: string } type NotionPage = { id: string; name: string } -type PipedrivePipeline = { id: number; name: string } -type ZoomMeeting = { id: number; name: string } -type CalcomSchedule = { id: number; name: string } +type PipedrivePipeline = { id: string; name: string } +type ZoomMeeting = { id: string; name: string } +type CalcomSchedule = { id: string; name: string } type GoogleTaskList = { id: string; title: string } type PlannerPlan = { id: string; title: string } type SharepointList = { id: string; displayName: string } @@ -358,7 +358,7 @@ const registry: Record = { { method: 'POST', body } ) return (data.eventTypes || []).map((et) => ({ - id: String(et.id), + id: et.id, label: et.title || et.slug, })) }, @@ -370,9 +370,9 @@ const registry: Record = { '/api/tools/calcom/event-types', { method: 'POST', body } ) - const et = (data.eventTypes || []).find((e) => String(e.id) === detailId) ?? null + const et = (data.eventTypes || []).find((e) => e.id === detailId) ?? null if (!et) return null - return { id: String(et.id), label: et.title || et.slug } + return { id: et.id, label: et.title || et.slug } }, }, 'calcom.schedules': { @@ -392,7 +392,7 @@ const registry: Record = { body, }) return (data.schedules || []).map((s) => ({ - id: String(s.id), + id: s.id, label: s.name, })) }, @@ -404,9 +404,9 @@ const registry: Record = { method: 'POST', body, }) - const s = (data.schedules || []).find((sc) => String(sc.id) === detailId) ?? null + const s = (data.schedules || []).find((sc) => sc.id === detailId) ?? null if (!s) return null - return { id: String(s.id), label: s.name } + return { id: s.id, label: s.name } }, }, 'confluence.spaces': { @@ -697,7 +697,7 @@ const registry: Record = { { method: 'POST', body } ) return (data.pipelines || []).map((p) => ({ - id: String(p.id), + id: p.id, label: p.name, })) }, @@ -709,9 +709,9 @@ const registry: Record = { '/api/tools/pipedrive/pipelines', { method: 'POST', body } ) - const p = (data.pipelines || []).find((pl) => String(pl.id) === detailId) ?? null + const p = (data.pipelines || []).find((pl) => pl.id === detailId) ?? null if (!p) return null - return { id: String(p.id), label: p.name } + return { id: p.id, label: p.name } }, }, 'sharepoint.lists': { @@ -805,7 +805,7 @@ const registry: Record = { body, }) return (data.meetings || []).map((m) => ({ - id: String(m.id), + id: m.id, label: m.name || `Meeting ${m.id}`, })) }, @@ -817,9 +817,9 @@ const registry: Record = { method: 'POST', body, }) - const meeting = (data.meetings || []).find((m) => String(m.id) === detailId) ?? null + const meeting = (data.meetings || []).find((m) => m.id === detailId) ?? null if (!meeting) return null - return { id: String(meeting.id), label: meeting.name || `Meeting ${meeting.id}` } + return { id: meeting.id, label: meeting.name || `Meeting ${meeting.id}` } }, }, 'slack.channels': {