From c1c6ed66d1a2d0ac379269916b6519e09f92cef9 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 6 Mar 2026 18:03:40 -0800 Subject: [PATCH 1/5] improvement(selectors): simplify selectorContext + add tests --- .../sub-block/hooks/use-selector-setup.ts | 27 +-- .../editor/components/sub-block/sub-block.tsx | 4 +- .../workflow-block/workflow-block.tsx | 2 +- apps/sim/hooks/selectors/registry.ts | 206 +++++++++--------- apps/sim/hooks/selectors/resolution.ts | 36 +-- apps/sim/hooks/selectors/types.ts | 2 +- apps/sim/hooks/use-selector-display-name.ts | 8 +- .../workflows/comparison/resolve-values.ts | 99 +-------- .../lib/workflows/subblocks/context.test.ts | 125 +++++++++++ apps/sim/lib/workflows/subblocks/context.ts | 60 +++++ 10 files changed, 315 insertions(+), 254 deletions(-) create mode 100644 apps/sim/lib/workflows/subblocks/context.test.ts create mode 100644 apps/sim/lib/workflows/subblocks/context.ts 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 cdbf1e17b62..98dc8c45606 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 @@ -2,6 +2,7 @@ import { useMemo } from 'react' import { useParams } from 'next/navigation' +import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context' import type { SubBlockConfig } from '@/blocks/types' import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants' import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' @@ -14,8 +15,7 @@ import { useDependsOnGate } from './use-depends-on-gate' * * Builds a `SelectorContext` by mapping each `dependsOn` entry through the * canonical index to its `canonicalParamId`, which maps directly to - * `SelectorContext` field names (e.g. `siteId`, `teamId`, `collectionId`). - * The one special case is `oauthCredential` which maps to `credentialId`. + * `SelectorContext` field names (e.g. `siteId`, `teamId`, `oauthCredential`). * * @param blockId - The block containing the selector sub-block * @param subBlock - The sub-block config (must have `selectorKey` set) @@ -70,11 +70,8 @@ export function useSelectorSetup( if (isReference(strValue)) continue const canonicalParamId = canonicalIndex.canonicalIdBySubBlockId[depKey] ?? depKey - - if (canonicalParamId === 'oauthCredential') { - context.credentialId = strValue - } else if (canonicalParamId in CONTEXT_FIELD_SET) { - ;(context as Record)[canonicalParamId] = strValue + if (SELECTOR_CONTEXT_FIELDS.has(canonicalParamId as keyof SelectorContext)) { + context[canonicalParamId as keyof SelectorContext] = strValue } } @@ -89,19 +86,3 @@ export function useSelectorSetup( dependencyValues: resolvedDependencyValues, } } - -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/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 3887b46e55f..047fbc2d836 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -57,9 +57,9 @@ import { useWebhookManagement } from '@/hooks/use-webhook-management' const SLACK_OVERRIDES: SelectorOverrides = { transformContext: (context, deps) => { const authMethod = deps.authMethod as string - const credentialId = + const oauthCredential = authMethod === 'bot_token' ? String(deps.botToken ?? '') : String(deps.credential ?? '') - return { ...context, credentialId } + return { ...context, oauthCredential } }, } 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 de5694b7b9b..5a559801917 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 @@ -578,7 +578,7 @@ const SubBlockRow = memo(function SubBlockRow({ subBlock, value: rawValue, workflowId, - credentialId: typeof credentialId === 'string' ? credentialId : undefined, + oauthCredential: typeof credentialId === 'string' ? credentialId : undefined, knowledgeBaseId: typeof knowledgeBaseId === 'string' ? knowledgeBaseId : undefined, domain: domainValue, teamId: teamIdValue, diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index db0d6b28f04..fd050f97a6a 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -39,10 +39,10 @@ type FolderResponse = { id: string; name: string } type PlannerTask = { id: string; title: string } const ensureCredential = (context: SelectorContext, key: SelectorKey): string => { - if (!context.credentialId) { + if (!context.oauthCredential) { throw new Error(`Missing credential for selector ${key}`) } - return context.credentialId + return context.oauthCredential } const ensureDomain = (context: SelectorContext, key: SelectorKey): string => { @@ -66,9 +66,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'airtable.bases', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'airtable.bases') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -104,10 +104,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'airtable.tables', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.baseId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.baseId), + enabled: ({ context }) => Boolean(context.oauthCredential && context.baseId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'airtable.tables') if (!context.baseId) { @@ -151,9 +151,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'asana.workspaces', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'asana.workspaces') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -182,9 +182,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'attio.objects', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'attio.objects') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -216,9 +216,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'attio.lists', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'attio.lists') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -250,10 +250,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'bigquery.datasets', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.projectId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.projectId), + enabled: ({ context }) => Boolean(context.oauthCredential && 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') @@ -298,12 +298,12 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'bigquery.tables', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.projectId ?? 'none', context.datasetId ?? 'none', ], enabled: ({ context }) => - Boolean(context.credentialId && context.projectId && context.datasetId), + Boolean(context.oauthCredential && 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') @@ -347,9 +347,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'calcom.eventTypes', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'calcom.eventTypes') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -381,9 +381,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'calcom.schedules', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'calcom.schedules') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -415,10 +415,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'confluence.spaces', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.domain ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.domain), + enabled: ({ context }) => Boolean(context.oauthCredential && context.domain), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'confluence.spaces') const domain = ensureDomain(context, 'confluence.spaces') @@ -460,10 +460,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'jsm.serviceDesks', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.domain ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.domain), + enabled: ({ context }) => Boolean(context.oauthCredential && context.domain), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'jsm.serviceDesks') const domain = ensureDomain(context, 'jsm.serviceDesks') @@ -505,12 +505,12 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'jsm.requestTypes', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.domain ?? 'none', context.serviceDeskId ?? 'none', ], enabled: ({ context }) => - Boolean(context.credentialId && context.domain && context.serviceDeskId), + Boolean(context.oauthCredential && context.domain && context.serviceDeskId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'jsm.requestTypes') const domain = ensureDomain(context, 'jsm.requestTypes') @@ -556,9 +556,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'google.tasks.lists', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'google.tasks.lists') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -587,9 +587,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'microsoft.planner.plans', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'microsoft.planner.plans') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -618,9 +618,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'notion.databases', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'notion.databases') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -652,9 +652,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'notion.pages', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'notion.pages') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -686,9 +686,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'pipedrive.pipelines', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'pipedrive.pipelines') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -720,10 +720,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'sharepoint.lists', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.siteId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.siteId), + enabled: ({ context }) => Boolean(context.oauthCredential && 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') @@ -761,9 +761,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'trello.boards', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'trello.boards') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -794,9 +794,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'zoom.meetings', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'zoom.meetings') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -828,12 +828,12 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'slack.channels', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const body = JSON.stringify({ - credential: context.credentialId, + credential: context.oauthCredential, workflowId: context.workflowId, }) const data = await fetchJson<{ channels: SlackChannel[] }>('/api/tools/slack/channels', { @@ -852,12 +852,12 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'slack.users', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const body = JSON.stringify({ - credential: context.credentialId, + credential: context.oauthCredential, workflowId: context.workflowId, }) const data = await fetchJson<{ users: SlackUser[] }>('/api/tools/slack/users', { @@ -876,12 +876,12 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'gmail.labels', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const data = await fetchJson<{ labels: FolderResponse[] }>('/api/tools/gmail/labels', { - searchParams: { credentialId: context.credentialId }, + searchParams: { credentialId: context.oauthCredential }, }) return (data.labels || []).map((label) => ({ id: label.id, @@ -895,12 +895,12 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'outlook.folders', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const data = await fetchJson<{ folders: FolderResponse[] }>('/api/tools/outlook/folders', { - searchParams: { credentialId: context.credentialId }, + searchParams: { credentialId: context.oauthCredential }, }) return (data.folders || []).map((folder) => ({ id: folder.id, @@ -914,13 +914,13 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'google.calendar', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const data = await fetchJson<{ calendars: { id: string; summary: string }[] }>( '/api/tools/google_calendar/calendars', - { searchParams: { credentialId: context.credentialId } } + { searchParams: { credentialId: context.oauthCredential } } ) return (data.calendars || []).map((calendar) => ({ id: calendar.id, @@ -934,11 +934,11 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'microsoft.teams', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { - const body = JSON.stringify({ credential: context.credentialId }) + const body = JSON.stringify({ credential: context.oauthCredential }) const data = await fetchJson<{ teams: { id: string; displayName: string }[] }>( '/api/tools/microsoft-teams/teams', { method: 'POST', body } @@ -955,11 +955,11 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'microsoft.chats', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { - const body = JSON.stringify({ credential: context.credentialId }) + const body = JSON.stringify({ credential: context.oauthCredential }) const data = await fetchJson<{ chats: { id: string; displayName: string }[] }>( '/api/tools/microsoft-teams/chats', { method: 'POST', body } @@ -976,13 +976,13 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'microsoft.channels', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.teamId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.teamId), + enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId), fetchList: async ({ context }: SelectorQueryArgs) => { const body = JSON.stringify({ - credential: context.credentialId, + credential: context.oauthCredential, teamId: context.teamId, }) const data = await fetchJson<{ channels: { id: string; displayName: string }[] }>( @@ -1001,14 +1001,14 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'wealthbox.contacts', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const data = await fetchJson<{ items: { id: string; name: string }[] }>( '/api/tools/wealthbox/items', { - searchParams: { credentialId: context.credentialId, type: 'contact' }, + searchParams: { credentialId: context.oauthCredential, type: 'contact' }, } ) return (data.items || []).map((item) => ({ @@ -1023,9 +1023,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'sharepoint.sites', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'sharepoint.sites') const body = JSON.stringify({ @@ -1069,10 +1069,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'microsoft.planner', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.planId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.planId), + enabled: ({ context }) => Boolean(context.oauthCredential && context.planId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'microsoft.planner') const body = JSON.stringify({ @@ -1112,11 +1112,11 @@ const registry: Record = { getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'jira.projects', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.domain ?? 'none', search ?? '', ], - enabled: ({ context }) => Boolean(context.credentialId && context.domain), + enabled: ({ context }) => Boolean(context.oauthCredential && context.domain), fetchList: async ({ context, search }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'jira.projects') const domain = ensureDomain(context, 'jira.projects') @@ -1171,12 +1171,12 @@ const registry: Record = { getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'jira.issues', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.domain ?? 'none', context.projectId ?? 'none', search ?? '', ], - enabled: ({ context }) => Boolean(context.credentialId && context.domain), + enabled: ({ context }) => Boolean(context.oauthCredential && context.domain), fetchList: async ({ context, search }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'jira.issues') const domain = ensureDomain(context, 'jira.issues') @@ -1235,9 +1235,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'linear.teams', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'linear.teams') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -1260,10 +1260,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'linear.projects', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.teamId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.teamId), + enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'linear.projects') const body = JSON.stringify({ @@ -1290,11 +1290,11 @@ const registry: Record = { getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'confluence.pages', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.domain ?? 'none', search ?? '', ], - enabled: ({ context }) => Boolean(context.credentialId && context.domain), + enabled: ({ context }) => Boolean(context.oauthCredential && context.domain), fetchList: async ({ context, search }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'confluence.pages') const domain = ensureDomain(context, 'confluence.pages') @@ -1343,9 +1343,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'onedrive.files', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'onedrive.files') const data = await fetchJson<{ files: { id: string; name: string }[] }>( @@ -1366,9 +1366,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'onedrive.folders', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'onedrive.folders') const data = await fetchJson<{ files: { id: string; name: string }[] }>( @@ -1389,12 +1389,12 @@ const registry: Record = { getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'google.drive', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.mimeType ?? 'any', context.fileId ?? 'root', search ?? '', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context, search }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'google.drive') const data = await fetchJson<{ files: { id: string; name: string }[] }>( @@ -1438,10 +1438,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'google.sheets', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.spreadsheetId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.spreadsheetId), + enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'google.sheets') if (!context.spreadsheetId) { @@ -1469,10 +1469,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'microsoft.excel.sheets', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.spreadsheetId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.spreadsheetId), + enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'microsoft.excel.sheets') if (!context.spreadsheetId) { @@ -1500,10 +1500,10 @@ const registry: Record = { getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'microsoft.excel', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', search ?? '', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context, search }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'microsoft.excel') const data = await fetchJson<{ files: { id: string; name: string }[] }>( @@ -1528,10 +1528,10 @@ const registry: Record = { getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'microsoft.word', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', search ?? '', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context, search }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'microsoft.word') const data = await fetchJson<{ files: { id: string; name: string }[] }>( @@ -1596,9 +1596,9 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'webflow.sites', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId), + enabled: ({ context }) => Boolean(context.oauthCredential), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'webflow.sites') const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId }) @@ -1621,10 +1621,10 @@ const registry: Record = { getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'webflow.collections', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.siteId ?? 'none', ], - enabled: ({ context }) => Boolean(context.credentialId && context.siteId), + enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId), fetchList: async ({ context }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'webflow.collections') if (!context.siteId) { @@ -1654,11 +1654,11 @@ const registry: Record = { getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'webflow.items', - context.credentialId ?? 'none', + context.oauthCredential ?? 'none', context.collectionId ?? 'none', search ?? '', ], - enabled: ({ context }) => Boolean(context.credentialId && context.collectionId), + enabled: ({ context }) => Boolean(context.oauthCredential && context.collectionId), fetchList: async ({ context, search }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'webflow.items') if (!context.collectionId) { diff --git a/apps/sim/hooks/selectors/resolution.ts b/apps/sim/hooks/selectors/resolution.ts index 81986860adb..38886d70de4 100644 --- a/apps/sim/hooks/selectors/resolution.ts +++ b/apps/sim/hooks/selectors/resolution.ts @@ -7,46 +7,16 @@ export interface SelectorResolution { allowSearch: boolean } -export interface SelectorResolutionArgs { - workflowId?: string - credentialId?: string - domain?: string - projectId?: string - planId?: string - teamId?: string - knowledgeBaseId?: string - siteId?: string - collectionId?: string - spreadsheetId?: string - fileId?: string - baseId?: string - datasetId?: string - serviceDeskId?: string -} - export function resolveSelectorForSubBlock( subBlock: SubBlockConfig, - args: SelectorResolutionArgs + context: SelectorContext ): SelectorResolution | null { if (!subBlock.selectorKey) return null return { key: subBlock.selectorKey, context: { - workflowId: args.workflowId, - credentialId: args.credentialId, - domain: args.domain, - projectId: args.projectId, - planId: args.planId, - teamId: args.teamId, - knowledgeBaseId: args.knowledgeBaseId, - siteId: args.siteId, - collectionId: args.collectionId, - spreadsheetId: args.spreadsheetId, - fileId: args.fileId, - baseId: args.baseId, - datasetId: args.datasetId, - serviceDeskId: args.serviceDeskId, - mimeType: subBlock.mimeType, + ...context, + mimeType: subBlock.mimeType ?? context.mimeType, }, allowSearch: subBlock.selectorAllowSearch ?? true, } diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index 8f8beee32e3..87e1572ef57 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -61,7 +61,7 @@ export interface SelectorOption { export interface SelectorContext { workspaceId?: string workflowId?: string - credentialId?: string + oauthCredential?: string serviceId?: string domain?: string teamId?: string diff --git a/apps/sim/hooks/use-selector-display-name.ts b/apps/sim/hooks/use-selector-display-name.ts index 24d2fe51ebe..8275ca5ea77 100644 --- a/apps/sim/hooks/use-selector-display-name.ts +++ b/apps/sim/hooks/use-selector-display-name.ts @@ -12,7 +12,7 @@ interface SelectorDisplayNameArgs { subBlock?: SubBlockConfig value: unknown workflowId?: string - credentialId?: string + oauthCredential?: string domain?: string projectId?: string planId?: string @@ -31,7 +31,7 @@ export function useSelectorDisplayName({ subBlock, value, workflowId, - credentialId, + oauthCredential, domain, projectId, planId, @@ -51,7 +51,7 @@ export function useSelectorDisplayName({ if (!subBlock || !detailId) return null return resolveSelectorForSubBlock(subBlock, { workflowId, - credentialId, + oauthCredential, domain, projectId, planId, @@ -69,7 +69,7 @@ export function useSelectorDisplayName({ subBlock, detailId, workflowId, - credentialId, + oauthCredential, domain, projectId, planId, diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index 9a041e7cc9c..0df8ce91e12 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { buildSelectorContextFromBlock } from '@/lib/workflows/subblocks/context' import { getBlock } from '@/blocks/registry' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' import { CREDENTIAL_SET, isUuid } from '@/executor/constants' @@ -6,7 +7,7 @@ import { fetchCredentialSetById } from '@/hooks/queries/credential-sets' import { fetchOAuthCredentialDetail } from '@/hooks/queries/oauth-credentials' import { getSelectorDefinition } from '@/hooks/selectors/registry' import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution' -import type { SelectorKey } from '@/hooks/selectors/types' +import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' import type { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('ResolveValues') @@ -39,24 +40,6 @@ interface ResolutionContext { blockId?: string } -/** - * Extended context extracted from block subBlocks for selector resolution - */ -interface ExtendedSelectorContext { - credentialId?: string - domain?: string - projectId?: string - planId?: string - teamId?: string - knowledgeBaseId?: string - siteId?: string - collectionId?: string - spreadsheetId?: string - baseId?: string - datasetId?: string - serviceDeskId?: string -} - function getSemanticFallback(subBlockId: string, subBlockConfig?: SubBlockConfig): string { if (subBlockConfig?.title) { return subBlockConfig.title.toLowerCase() @@ -150,26 +133,10 @@ async function resolveWorkflow(workflowId: string): Promise { async function resolveSelectorValue( value: string, selectorKey: SelectorKey, - extendedContext: ExtendedSelectorContext, - workflowId: string + selectorContext: SelectorContext ): Promise { try { const definition = getSelectorDefinition(selectorKey) - const selectorContext = { - workflowId, - credentialId: extendedContext.credentialId, - domain: extendedContext.domain, - projectId: extendedContext.projectId, - planId: extendedContext.planId, - teamId: extendedContext.teamId, - knowledgeBaseId: extendedContext.knowledgeBaseId, - siteId: extendedContext.siteId, - collectionId: extendedContext.collectionId, - spreadsheetId: extendedContext.spreadsheetId, - baseId: extendedContext.baseId, - datasetId: extendedContext.datasetId, - serviceDeskId: extendedContext.serviceDeskId, - } if (definition.fetchById) { const result = await definition.fetchById({ @@ -219,37 +186,14 @@ export function formatValueForDisplay(value: unknown): string { return String(value) } -/** - * Extracts extended context from a block's subBlocks for selector resolution. - * This mirrors the context extraction done in the UI components. - */ -function extractExtendedContext( +function extractSelectorContext( blockId: string, - currentState: WorkflowState -): ExtendedSelectorContext { + currentState: WorkflowState, + workflowId: string +): SelectorContext { const block = currentState.blocks?.[blockId] if (!block?.subBlocks) return {} - - const getStringValue = (id: string): string | undefined => { - const subBlock = block.subBlocks[id] as { value?: unknown } | undefined - const val = subBlock?.value - return typeof val === 'string' ? val : undefined - } - - return { - credentialId: getStringValue('credential'), - domain: getStringValue('domain'), - projectId: getStringValue('projectId'), - planId: getStringValue('planId'), - teamId: getStringValue('teamId'), - knowledgeBaseId: getStringValue('knowledgeBaseId'), - 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'), - } + return buildSelectorContextFromBlock(block.type, block.subBlocks, { workflowId }) } /** @@ -277,8 +221,8 @@ export async function resolveValueForDisplay( const subBlockConfig = blockConfig?.subBlocks.find((sb) => sb.id === context.subBlockId) const semanticFallback = getSemanticFallback(context.subBlockId, subBlockConfig) - const extendedContext = context.blockId - ? extractExtendedContext(context.blockId, context.currentState) + const selectorCtx = context.blockId + ? extractSelectorContext(context.blockId, context.currentState, context.workflowId) : {} // Credential fields (oauth-input or credential subBlockId) @@ -311,29 +255,10 @@ export async function resolveValueForDisplay( // Selector types that require hydration (file-selector, sheet-selector, etc.) // These support external service IDs like Google Drive file IDs if (subBlockConfig && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlockConfig.type)) { - const resolution = resolveSelectorForSubBlock(subBlockConfig, { - workflowId: context.workflowId, - credentialId: extendedContext.credentialId, - domain: extendedContext.domain, - projectId: extendedContext.projectId, - planId: extendedContext.planId, - teamId: extendedContext.teamId, - knowledgeBaseId: extendedContext.knowledgeBaseId, - siteId: extendedContext.siteId, - collectionId: extendedContext.collectionId, - spreadsheetId: extendedContext.spreadsheetId, - baseId: extendedContext.baseId, - datasetId: extendedContext.datasetId, - serviceDeskId: extendedContext.serviceDeskId, - }) + const resolution = resolveSelectorForSubBlock(subBlockConfig, selectorCtx) if (resolution?.key) { - const label = await resolveSelectorValue( - value, - resolution.key, - extendedContext, - context.workflowId - ) + const label = await resolveSelectorValue(value, resolution.key, selectorCtx) if (label) { return { original: value, displayLabel: label, resolved: true } } diff --git a/apps/sim/lib/workflows/subblocks/context.test.ts b/apps/sim/lib/workflows/subblocks/context.test.ts new file mode 100644 index 00000000000..c30f1f1b0af --- /dev/null +++ b/apps/sim/lib/workflows/subblocks/context.test.ts @@ -0,0 +1,125 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' + +vi.unmock('@/blocks/registry') + +import { getAllBlocks } from '@/blocks/registry' +import { buildSelectorContextFromBlock, SELECTOR_CONTEXT_FIELDS } from './context' +import { buildCanonicalIndex, isCanonicalPair } from './visibility' + +describe('buildSelectorContextFromBlock', () => { + it('should extract knowledgeBaseId from knowledgeBaseSelector via canonical mapping', () => { + const ctx = buildSelectorContextFromBlock('knowledge', { + operation: { id: 'operation', type: 'dropdown', value: 'search' }, + knowledgeBaseSelector: { + id: 'knowledgeBaseSelector', + type: 'knowledge-base-selector', + value: 'kb-uuid-123', + }, + }) + + expect(ctx.knowledgeBaseId).toBe('kb-uuid-123') + }) + + it('should extract knowledgeBaseId from manualKnowledgeBaseId via canonical mapping', () => { + const ctx = buildSelectorContextFromBlock('knowledge', { + operation: { id: 'operation', type: 'dropdown', value: 'search' }, + manualKnowledgeBaseId: { + id: 'manualKnowledgeBaseId', + type: 'short-input', + value: 'manual-kb-id', + }, + }) + + expect(ctx.knowledgeBaseId).toBe('manual-kb-id') + }) + + it('should skip null/empty values', () => { + const ctx = buildSelectorContextFromBlock('knowledge', { + knowledgeBaseSelector: { + id: 'knowledgeBaseSelector', + type: 'knowledge-base-selector', + value: '', + }, + }) + + expect(ctx.knowledgeBaseId).toBeUndefined() + }) + + it('should return empty context for unknown block types', () => { + const ctx = buildSelectorContextFromBlock('nonexistent_block', { + foo: { id: 'foo', type: 'short-input', value: 'bar' }, + }) + + expect(ctx).toEqual({}) + }) + + it('should pass through workflowId from opts', () => { + const ctx = buildSelectorContextFromBlock( + 'knowledge', + { operation: { id: 'operation', type: 'dropdown', value: 'search' } }, + { workflowId: 'wf-123' } + ) + + expect(ctx.workflowId).toBe('wf-123') + }) + + it('should ignore subblock keys not in SELECTOR_CONTEXT_FIELDS', () => { + const ctx = buildSelectorContextFromBlock('knowledge', { + operation: { id: 'operation', type: 'dropdown', value: 'search' }, + query: { id: 'query', type: 'short-input', value: 'some search query' }, + }) + + expect((ctx as Record).query).toBeUndefined() + expect((ctx as Record).operation).toBeUndefined() + }) +}) + +describe('SELECTOR_CONTEXT_FIELDS validation', () => { + it('every entry must be a canonicalParamId (if a canonical pair exists) or a direct subblock ID', () => { + const allCanonicalParamIds = new Set() + const allSubBlockIds = new Set() + const idsInCanonicalPairs = new Set() + + for (const block of getAllBlocks()) { + const index = buildCanonicalIndex(block.subBlocks) + + for (const sb of block.subBlocks) { + allSubBlockIds.add(sb.id) + if (sb.canonicalParamId) { + allCanonicalParamIds.add(sb.canonicalParamId) + } + } + + for (const group of Object.values(index.groupsById)) { + if (!isCanonicalPair(group)) continue + if (group.basicId) idsInCanonicalPairs.add(group.basicId) + for (const advId of group.advancedIds) idsInCanonicalPairs.add(advId) + } + } + + const errors: string[] = [] + + for (const field of SELECTOR_CONTEXT_FIELDS) { + const f = field as string + if (allCanonicalParamIds.has(f)) continue + + if (idsInCanonicalPairs.has(f)) { + errors.push( + `"${f}" is a member subblock ID inside a canonical pair — use the canonicalParamId instead` + ) + continue + } + + if (!allSubBlockIds.has(f)) { + errors.push(`"${f}" is not a canonicalParamId or subblock ID in any block definition`) + } + } + + if (errors.length > 0) { + throw new Error(`SELECTOR_CONTEXT_FIELDS validation failed:\n${errors.join('\n')}`) + } + }) +}) diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts new file mode 100644 index 00000000000..9b43bc892f3 --- /dev/null +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -0,0 +1,60 @@ +import { getBlock } from '@/blocks' +import type { SelectorContext } from '@/hooks/selectors/types' +import type { SubBlockState } from '@/stores/workflows/workflow/types' +import { buildCanonicalIndex } from './visibility' + +/** + * Canonical param IDs (or raw subblock IDs) that correspond to SelectorContext fields. + * A subblock's resolved canonical key is set on the context only if it appears here. + */ +export const SELECTOR_CONTEXT_FIELDS = new Set([ + 'oauthCredential', + 'domain', + 'teamId', + 'projectId', + 'knowledgeBaseId', + 'planId', + 'siteId', + 'collectionId', + 'spreadsheetId', + 'fileId', + 'baseId', + 'datasetId', + 'serviceDeskId', +]) + +/** + * Builds a SelectorContext from a block's subBlocks using the canonical index. + * + * Iterates all subblocks, resolves each through canonicalIdBySubBlockId to get + * the canonical key, then checks it against SELECTOR_CONTEXT_FIELDS. + * This avoids hardcoding subblock IDs and automatically handles basic/advanced + * renames. + */ +export function buildSelectorContextFromBlock( + blockType: string, + subBlocks: Record, + opts?: { workflowId?: string } +): SelectorContext { + const context: SelectorContext = {} + if (opts?.workflowId) context.workflowId = opts.workflowId + + const blockConfig = getBlock(blockType) + if (!blockConfig) return context + + const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks) + + for (const [subBlockId, subBlock] of Object.entries(subBlocks)) { + const val = subBlock?.value + if (val === null || val === undefined) continue + const strValue = typeof val === 'string' ? val : String(val) + if (!strValue) continue + + const canonicalKey = canonicalIndex.canonicalIdBySubBlockId[subBlockId] ?? subBlockId + if (SELECTOR_CONTEXT_FIELDS.has(canonicalKey as keyof SelectorContext)) { + context[canonicalKey as keyof SelectorContext] = strValue + } + } + + return context +} From 94abc424be7f091ffcf0d3cdc93f5dadd90791e1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 6 Mar 2026 18:18:25 -0800 Subject: [PATCH 2/5] fix resolve values fallback --- apps/sim/lib/workflows/comparison/resolve-values.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index 0df8ce91e12..5080a8645a1 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -223,7 +223,7 @@ export async function resolveValueForDisplay( const selectorCtx = context.blockId ? extractSelectorContext(context.blockId, context.currentState, context.workflowId) - : {} + : { workflowId: context.workflowId } // Credential fields (oauth-input or credential subBlockId) const isCredentialField = From adea9db89d117a123ac9937cc20d196d056f4ff5 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 6 Mar 2026 18:25:36 -0800 Subject: [PATCH 3/5] another workflowid pass through --- apps/sim/lib/workflows/comparison/resolve-values.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index 5080a8645a1..1eb31d97ea3 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -192,7 +192,7 @@ function extractSelectorContext( workflowId: string ): SelectorContext { const block = currentState.blocks?.[blockId] - if (!block?.subBlocks) return {} + if (!block?.subBlocks) return { workflowId } return buildSelectorContextFromBlock(block.type, block.subBlocks, { workflowId }) } From fc5df60d8f4eab99ef9cc7be025cb9f1724badd0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 6 Mar 2026 18:37:32 -0800 Subject: [PATCH 4/5] remove dead code --- .../workflows/comparison/resolve-values.ts | 57 ++----------------- 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index 1eb31d97ea3..cd675824b16 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -40,56 +40,8 @@ interface ResolutionContext { blockId?: string } -function getSemanticFallback(subBlockId: string, subBlockConfig?: SubBlockConfig): string { - if (subBlockConfig?.title) { - return subBlockConfig.title.toLowerCase() - } - - const patterns: Record = { - credential: 'credential', - channel: 'channel', - channelId: 'channel', - user: 'user', - userId: 'user', - workflow: 'workflow', - workflowId: 'workflow', - file: 'file', - fileId: 'file', - folder: 'folder', - folderId: 'folder', - project: 'project', - projectId: 'project', - team: 'team', - teamId: 'team', - sheet: 'sheet', - sheetId: 'sheet', - document: 'document', - documentId: 'document', - knowledgeBase: 'knowledge base', - knowledgeBaseId: 'knowledge base', - server: 'server', - serverId: 'server', - tool: 'tool', - toolId: 'tool', - calendar: 'calendar', - calendarId: 'calendar', - label: 'label', - labelId: 'label', - site: 'site', - siteId: 'site', - collection: 'collection', - collectionId: 'collection', - item: 'item', - itemId: 'item', - contact: 'contact', - contactId: 'contact', - task: 'task', - taskId: 'task', - chat: 'chat', - chatId: 'chat', - } - - return patterns[subBlockId] || 'value' +function getSemanticFallback(subBlockConfig: SubBlockConfig): string { + return (subBlockConfig.title ?? subBlockConfig.id).toLowerCase() } async function resolveCredential(credentialId: string, workflowId: string): Promise { @@ -219,7 +171,10 @@ export async function resolveValueForDisplay( const blockConfig = getBlock(context.blockType) const subBlockConfig = blockConfig?.subBlocks.find((sb) => sb.id === context.subBlockId) - const semanticFallback = getSemanticFallback(context.subBlockId, subBlockConfig) + if (!subBlockConfig) { + return { original: value, displayLabel: formatValueForDisplay(value), resolved: false } + } + const semanticFallback = getSemanticFallback(subBlockConfig) const selectorCtx = context.blockId ? extractSelectorContext(context.blockId, context.currentState, context.workflowId) From ddb2d85d51ccfde764204271462bc3820f0ac766 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 6 Mar 2026 19:26:19 -0800 Subject: [PATCH 5/5] make workspace id required --- apps/sim/lib/workflows/persistence/utils.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index fa6a9bb5171..89b7b7f6029 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -117,6 +117,10 @@ export async function loadDeployedWorkflowState( resolvedWorkspaceId = wfRow?.workspaceId ?? undefined } + if (!resolvedWorkspaceId) { + throw new Error(`Workflow ${workflowId} has no workspace`) + } + const { blocks: migratedBlocks } = await applyBlockMigrations( state.blocks || {}, resolvedWorkspaceId @@ -139,7 +143,7 @@ export async function loadDeployedWorkflowState( interface MigrationContext { blocks: Record - workspaceId?: string + workspaceId: string migrated: boolean } @@ -148,7 +152,7 @@ type BlockMigration = (ctx: MigrationContext) => MigrationContext | Promise, - workspaceId?: string + workspaceId: string ): Promise<{ blocks: Record; migrated: boolean }> => { let ctx: MigrationContext = { blocks, workspaceId, migrated: false } for (const migration of migrations) { @@ -170,7 +174,6 @@ const applyBlockMigrations = createMigrationPipeline([ }), async (ctx) => { - if (!ctx.workspaceId) return ctx const { blocks, migrated } = await migrateCredentialIds(ctx.blocks, ctx.workspaceId) return { ...ctx, blocks, migrated: ctx.migrated || migrated } }, @@ -409,9 +412,13 @@ export async function loadWorkflowFromNormalizedTables( blocksMap[block.id] = assembled }) + if (!workflowRow?.workspaceId) { + throw new Error(`Workflow ${workflowId} has no workspace`) + } + const { blocks: finalBlocks, migrated } = await applyBlockMigrations( blocksMap, - workflowRow?.workspaceId ?? undefined + workflowRow.workspaceId ) if (migrated) {