From 7c133e1088e11171b85013716f3ff3ab43844172 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 21 Aug 2026 15:07:38 -0700 Subject: [PATCH 1/4] fix(selectors): respect trigger credentials in trigger mode --- .../sub-block/hooks/use-fetched-options.ts | 1 + .../lib/mapping/dependent-reconfigs.ts | 5 ++ .../hooks/queries/dynamic-subblock-options.ts | 1 + .../workflows/comparison/resolve-values.ts | 1 + .../lib/workflows/subblocks/context.test.ts | 48 +++++++++++++++++++ apps/sim/lib/workflows/subblocks/context.ts | 28 +++++++++-- 6 files changed, 81 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts index a919c965318..1e2941efae8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts @@ -131,6 +131,7 @@ export function useFetchedOptions({ workflowId: activeWorkflowId ?? undefined, workspaceId: workspaceId ?? undefined, canonicalModes: block.data?.canonicalModes, + triggerMode: block.triggerMode, }) if (selectorExcludeSelf && activeWorkflowId) context.excludeWorkflowId = activeWorkflowId return context diff --git a/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts b/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts index 9d2357de99e..5c022210e9a 100644 --- a/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts +++ b/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts @@ -75,6 +75,8 @@ interface EmitAnchoredParams { targetWorkflowId: string /** Canonical-mode overrides for resolving the active parent member (undefined -> value heuristic). */ canonicalModes?: CanonicalModeOverrides + /** Applies trigger-only canonical precedence for a top-level trigger-mode block. */ + triggerMode?: boolean /** Memoized so the deterministic target block id is derived at most once per block. */ resolveTargetBlockId: () => string /** Map a dependent's config id to its wire `subBlockKey` (identity, or nested `tools[i].id`). */ @@ -117,6 +119,7 @@ function emitAnchoredDependents(params: EmitAnchoredParams): void { blockName, targetWorkflowId, canonicalModes, + triggerMode, resolveTargetBlockId, makeSubBlockKey, makeTitle, @@ -127,6 +130,7 @@ function emitAnchoredDependents(params: EmitAnchoredParams): void { } = params const fullContext = buildSelectorContextFromBlock(contextBlockType, contextSubBlocks, { canonicalModes, + triggerMode, }) const canonicalIndex = buildCanonicalIndex(config.subBlocks) const gates = createCanonicalModeGates(config.subBlocks, values, canonicalModes) @@ -327,6 +331,7 @@ export function collectForkDependentReconfigs( blockName: block.name, targetWorkflowId: item.targetWorkflowId, canonicalModes: block.data?.canonicalModes, + triggerMode: block.triggerMode, resolveTargetBlockId: resolveBlockId, makeSubBlockKey: (id) => id, makeTitle: (dependent) => dependent.title ?? dependent.id ?? '', diff --git a/apps/sim/hooks/queries/dynamic-subblock-options.ts b/apps/sim/hooks/queries/dynamic-subblock-options.ts index d62f68f3293..0bc64829ac3 100644 --- a/apps/sim/hooks/queries/dynamic-subblock-options.ts +++ b/apps/sim/hooks/queries/dynamic-subblock-options.ts @@ -89,6 +89,7 @@ export function useDynamicSubBlockOptionDisplayName({ workflowId: activeWorkflowId ?? undefined, workspaceId, canonicalModes: block.data?.canonicalModes, + triggerMode: block.triggerMode, }) }, [block, liveValues, activeWorkflowId, workspaceId]) diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index 64cfc114cef..f2b25b2b15b 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -179,6 +179,7 @@ function extractSelectorContext( workflowId, workspaceId, canonicalModes: block.data?.canonicalModes, + triggerMode: block.triggerMode, }) } diff --git a/apps/sim/lib/workflows/subblocks/context.test.ts b/apps/sim/lib/workflows/subblocks/context.test.ts index 515856e121e..605c34b3c0c 100644 --- a/apps/sim/lib/workflows/subblocks/context.test.ts +++ b/apps/sim/lib/workflows/subblocks/context.test.ts @@ -152,6 +152,54 @@ describe('buildSelectorContextFromBlock', () => { ).toBe('advanced-team') }) + it('preserves Gmail action credential resolution in basic and advanced modes', () => { + const subBlocks = { + credential: { id: 'credential', type: 'oauth-input', value: 'action-basic' }, + manualCredential: { + id: 'manualCredential', + type: 'short-input', + value: 'action-advanced', + }, + } + + expect(buildSelectorContextFromBlock('gmail', subBlocks).oauthCredential).toBe('action-basic') + expect( + buildSelectorContextFromBlock('gmail', subBlocks, { + canonicalModes: { oauthCredential: 'advanced' }, + }).oauthCredential + ).toBe('action-advanced') + }) + + it('uses the trigger credential when a Gmail action block is converted to trigger mode', () => { + const ctx = buildSelectorContextFromBlock( + 'gmail', + { + credential: { id: 'credential', type: 'oauth-input', value: 'dormant-action' }, + triggerCredentials: { + id: 'triggerCredentials', + type: 'oauth-input', + value: 'active-trigger', + }, + }, + { triggerMode: true } + ) + + expect(ctx.oauthCredential).toBe('active-trigger') + }) + + it('does not leak a dormant Gmail action credential when the trigger credential is blank', () => { + const ctx = buildSelectorContextFromBlock( + 'gmail', + { + credential: { id: 'credential', type: 'oauth-input', value: 'dormant-action' }, + triggerCredentials: { id: 'triggerCredentials', type: 'oauth-input', value: '' }, + }, + { triggerMode: true } + ) + + expect(ctx.oauthCredential).toBeUndefined() + }) + it('should ignore subblock keys not in SELECTOR_CONTEXT_FIELDS', () => { const ctx = buildSelectorContextFromBlock('knowledge', { operation: { id: 'operation', type: 'dropdown', value: 'search' }, diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index dff3f396ebb..137773afa91 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -5,6 +5,7 @@ import { buildCanonicalIndex, buildSubBlockValues, type CanonicalModeOverrides, + evaluateSubBlockCondition, resolveActiveCanonicalValue, } from './visibility' @@ -66,7 +67,12 @@ export const SELECTOR_CONTEXT_FIELDS = new Set([ export function buildSelectorContextFromBlock( blockType: string, subBlocks: Record, - opts?: { workflowId?: string; workspaceId?: string; canonicalModes?: CanonicalModeOverrides } + opts?: { + workflowId?: string + workspaceId?: string + canonicalModes?: CanonicalModeOverrides + triggerMode?: boolean + } ): SelectorContext { const context: SelectorContext = {} if (opts?.workflowId) context.workflowId = opts.workflowId @@ -78,6 +84,13 @@ export function buildSelectorContextFromBlock( const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks) const values = buildSubBlockValues(subBlocks) const resolvedGroups = new Set() + const visibleTriggerSubBlocks = opts?.triggerMode + ? blockConfig.subBlocks.filter( + (subBlock) => + (subBlock.mode === 'trigger' || subBlock.mode === 'trigger-advanced') && + evaluateSubBlockCondition(subBlock.condition, values) + ) + : [] const setField = (key: string, value: unknown) => { if (value === null || value === undefined) return @@ -88,6 +101,14 @@ export function buildSelectorContextFromBlock( } } + if (opts?.triggerMode) { + const triggerCanonicalIndex = buildCanonicalIndex(visibleTriggerSubBlocks) + for (const group of Object.values(triggerCanonicalIndex.groupsById)) { + resolvedGroups.add(group.canonicalId) + setField(group.canonicalId, resolveActiveCanonicalValue(group, values, opts.canonicalModes)) + } + } + for (const [subBlockId, subBlock] of Object.entries(subBlocks)) { const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlockId] if (canonicalId) { @@ -110,9 +131,10 @@ export function buildSelectorContextFromBlock( // // Only fills a gap: a block that does declare the canonical id has already set it above, // including the basic/advanced active-member resolution this loop cannot express. - if (!context.oauthCredential) { + if (!context.oauthCredential && !resolvedGroups.has('oauthCredential')) { + const credentialConfigs = opts?.triggerMode ? visibleTriggerSubBlocks : blockConfig.subBlocks for (const [subBlockId, subBlock] of Object.entries(subBlocks)) { - if (blockConfig.subBlocks.find((cfg) => cfg.id === subBlockId)?.type !== 'oauth-input') { + if (credentialConfigs.find((cfg) => cfg.id === subBlockId)?.type !== 'oauth-input') { continue } const value = subBlock?.value From 531945897b47791a81c05e84b361908767ed8cf3 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 21 Aug 2026 15:23:35 -0700 Subject: [PATCH 2/4] fix(selectors): isolate trigger selector context --- .../sub-block/hooks/use-fetched-options.ts | 24 +++++-- .../lib/workflows/subblocks/context.test.ts | 71 +++++++++++-------- apps/sim/lib/workflows/subblocks/context.ts | 50 +++++++------ 3 files changed, 91 insertions(+), 54 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts index 1e2941efae8..2333a91a77b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts @@ -2,7 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { getErrorMessage } from '@sim/utils/errors' import { isEqual } from 'es-toolkit' import { useStoreWithEqualityFn } from 'zustand/traditional' -import { buildSelectorContextFromBlock } from '@/lib/workflows/subblocks/context' +import { + buildSelectorContextFromBlock, + getSelectorContextSubBlocks, +} from '@/lib/workflows/subblocks/context' import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility' import { getBlock } from '@/blocks/registry' import { getSelectorDefinition, loadAllSelectorOptions } from '@/hooks/selectors/registry' @@ -93,10 +96,6 @@ export function useFetchedOptions({ const blockState = useWorkflowStore((state) => state.blocks[blockId]) const blockConfig = blockState?.type ? getBlock(blockState.type) : null const canonicalModeOverrides = blockState?.data?.canonicalModes - const canonicalIndex = useMemo( - () => buildCanonicalIndex(blockConfig?.subBlocks || []), - [blockConfig?.subBlocks] - ) const dependencyValues = useStoreWithEqualityFn( useSubBlockStore, @@ -105,11 +104,24 @@ export function useFetchedOptions({ if (dependsOnFields.length === 0 || !activeWorkflowId) return [] const workflowValues = state.workflowValues[activeWorkflowId] || {} const blockValues = workflowValues[blockId] || {} + const contextConfigs = getSelectorContextSubBlocks( + blockConfig?.subBlocks || [], + blockValues, + blockState?.triggerMode + ) + const canonicalIndex = buildCanonicalIndex(contextConfigs) return dependsOnFields.map((depKey) => resolveDependencyValue(depKey, blockValues, canonicalIndex, canonicalModeOverrides) ) }, - [dependsOnFields, activeWorkflowId, blockId, canonicalIndex, canonicalModeOverrides] + [ + dependsOnFields, + activeWorkflowId, + blockId, + blockConfig?.subBlocks, + blockState?.triggerMode, + canonicalModeOverrides, + ] ), isEqual ) diff --git a/apps/sim/lib/workflows/subblocks/context.test.ts b/apps/sim/lib/workflows/subblocks/context.test.ts index 605c34b3c0c..013942712d9 100644 --- a/apps/sim/lib/workflows/subblocks/context.test.ts +++ b/apps/sim/lib/workflows/subblocks/context.test.ts @@ -7,8 +7,12 @@ vi.unmock('@/blocks/registry') import * as blocksBarrel from '@/blocks' import { getAllBlocks, getBlock as getRealBlock } from '@/blocks/registry' -import { buildSelectorContextFromBlock, SELECTOR_CONTEXT_FIELDS } from './context' -import { buildCanonicalIndex, isCanonicalPair } from './visibility' +import { + buildSelectorContextFromBlock, + getSelectorContextSubBlocks, + SELECTOR_CONTEXT_FIELDS, +} from './context' +import { buildCanonicalIndex, isCanonicalPair, resolveDependencyValue } from './visibility' /** * Under `isolate: false` the module under test may already be cached from an @@ -23,6 +27,10 @@ afterAll(() => { getBlockSpy.mockRestore() }) +function subBlocksFromValues(values: Record): Record { + return Object.fromEntries(Object.entries(values).map(([id, value]) => [id, { value }])) +} + describe('buildSelectorContextFromBlock', () => { it('should extract knowledgeBaseId from knowledgeBaseSelector via canonical mapping', () => { const ctx = buildSelectorContextFromBlock('knowledge', { @@ -153,14 +161,10 @@ describe('buildSelectorContextFromBlock', () => { }) it('preserves Gmail action credential resolution in basic and advanced modes', () => { - const subBlocks = { - credential: { id: 'credential', type: 'oauth-input', value: 'action-basic' }, - manualCredential: { - id: 'manualCredential', - type: 'short-input', - value: 'action-advanced', - }, - } + const subBlocks = subBlocksFromValues({ + credential: 'action-basic', + manualCredential: 'action-advanced', + }) expect(buildSelectorContextFromBlock('gmail', subBlocks).oauthCredential).toBe('action-basic') expect( @@ -170,30 +174,41 @@ describe('buildSelectorContextFromBlock', () => { ).toBe('action-advanced') }) - it('uses the trigger credential when a Gmail action block is converted to trigger mode', () => { - const ctx = buildSelectorContextFromBlock( - 'gmail', + it('uses trigger credentials with and without canonical metadata after action conversion', () => { + const clickupValues = { + selectedTriggerId: 'clickup_task_created', + credential: 'dormant-action', + triggerCredentials: 'active-trigger', + } + const cases = [ + { blockType: 'clickup', values: clickupValues }, { - credential: { id: 'credential', type: 'oauth-input', value: 'dormant-action' }, - triggerCredentials: { - id: 'triggerCredentials', - type: 'oauth-input', - value: 'active-trigger', - }, + blockType: 'airtable', + values: { credential: 'dormant-action', triggerCredentials: 'active-trigger' }, }, - { triggerMode: true } - ) + ] + + for (const { blockType, values } of cases) { + expect( + buildSelectorContextFromBlock(blockType, subBlocksFromValues(values), { + triggerMode: true, + }).oauthCredential + ).toBe('active-trigger') + } - expect(ctx.oauthCredential).toBe('active-trigger') + const clickupConfig = getRealBlock('clickup') + const triggerCanonicalIndex = buildCanonicalIndex( + getSelectorContextSubBlocks(clickupConfig?.subBlocks ?? [], clickupValues, true) + ) + expect(resolveDependencyValue('triggerCredentials', clickupValues, triggerCanonicalIndex)).toBe( + 'active-trigger' + ) }) - it('does not leak a dormant Gmail action credential when the trigger credential is blank', () => { + it('does not leak a dormant action credential when an unmapped trigger credential is blank', () => { const ctx = buildSelectorContextFromBlock( - 'gmail', - { - credential: { id: 'credential', type: 'oauth-input', value: 'dormant-action' }, - triggerCredentials: { id: 'triggerCredentials', type: 'oauth-input', value: '' }, - }, + 'airtable', + subBlocksFromValues({ credential: 'dormant-action', triggerCredentials: '' }), { triggerMode: true } ) diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index 137773afa91..3c99d1845a9 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -1,4 +1,5 @@ import { getBlock } from '@/blocks' +import type { SubBlockConfig } from '@/blocks/types' import type { SelectorContext } from '@/hooks/selectors/types' import type { SubBlockState } from '@/stores/workflows/workflow/types' import { @@ -56,10 +57,26 @@ export const SELECTOR_CONTEXT_FIELDS = new Set([ 'password', ]) +/** + * Selects the block fields allowed to contribute to selector context for the active mode. + */ +export function getSelectorContextSubBlocks( + subBlocks: SubBlockConfig[], + values: Record, + triggerMode?: boolean +): SubBlockConfig[] { + if (!triggerMode) return subBlocks + return subBlocks.filter( + (subBlock) => + (subBlock.mode === 'trigger' || subBlock.mode === 'trigger-advanced') && + evaluateSubBlockCondition(subBlock.condition, values) + ) +} + /** * Builds a SelectorContext from a block's subBlocks using the canonical index. * - * Iterates all subblocks, resolves each through canonicalIdBySubBlockId to get + * Iterates the active mode's 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. @@ -81,16 +98,17 @@ export function buildSelectorContextFromBlock( const blockConfig = getBlock(blockType) if (!blockConfig) return context - const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks) const values = buildSubBlockValues(subBlocks) + const contextConfigs = getSelectorContextSubBlocks( + blockConfig.subBlocks, + values, + opts?.triggerMode + ) + const canonicalIndex = buildCanonicalIndex(contextConfigs) + const contextSubBlockIds = opts?.triggerMode + ? new Set(contextConfigs.map((subBlock) => subBlock.id)) + : undefined const resolvedGroups = new Set() - const visibleTriggerSubBlocks = opts?.triggerMode - ? blockConfig.subBlocks.filter( - (subBlock) => - (subBlock.mode === 'trigger' || subBlock.mode === 'trigger-advanced') && - evaluateSubBlockCondition(subBlock.condition, values) - ) - : [] const setField = (key: string, value: unknown) => { if (value === null || value === undefined) return @@ -101,15 +119,8 @@ export function buildSelectorContextFromBlock( } } - if (opts?.triggerMode) { - const triggerCanonicalIndex = buildCanonicalIndex(visibleTriggerSubBlocks) - for (const group of Object.values(triggerCanonicalIndex.groupsById)) { - resolvedGroups.add(group.canonicalId) - setField(group.canonicalId, resolveActiveCanonicalValue(group, values, opts.canonicalModes)) - } - } - for (const [subBlockId, subBlock] of Object.entries(subBlocks)) { + if (contextSubBlockIds && !contextSubBlockIds.has(subBlockId)) continue const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlockId] if (canonicalId) { // A canonical group resolves to its ACTIVE member only (no last-write-wins between a @@ -131,10 +142,9 @@ export function buildSelectorContextFromBlock( // // Only fills a gap: a block that does declare the canonical id has already set it above, // including the basic/advanced active-member resolution this loop cannot express. - if (!context.oauthCredential && !resolvedGroups.has('oauthCredential')) { - const credentialConfigs = opts?.triggerMode ? visibleTriggerSubBlocks : blockConfig.subBlocks + if (!context.oauthCredential && (!opts?.triggerMode || !resolvedGroups.has('oauthCredential'))) { for (const [subBlockId, subBlock] of Object.entries(subBlocks)) { - if (credentialConfigs.find((cfg) => cfg.id === subBlockId)?.type !== 'oauth-input') { + if (contextConfigs.find((cfg) => cfg.id === subBlockId)?.type !== 'oauth-input') { continue } const value = subBlock?.value From 7729a71bd76c59a462accafeb28757ba1185644c Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 21 Aug 2026 15:39:45 -0700 Subject: [PATCH 3/4] fix(selectors): isolate action credential fallback --- apps/sim/lib/workflows/subblocks/context.test.ts | 6 ++++++ apps/sim/lib/workflows/subblocks/context.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/workflows/subblocks/context.test.ts b/apps/sim/lib/workflows/subblocks/context.test.ts index 013942712d9..6126d883196 100644 --- a/apps/sim/lib/workflows/subblocks/context.test.ts +++ b/apps/sim/lib/workflows/subblocks/context.test.ts @@ -172,6 +172,12 @@ describe('buildSelectorContextFromBlock', () => { canonicalModes: { oauthCredential: 'advanced' }, }).oauthCredential ).toBe('action-advanced') + expect( + buildSelectorContextFromBlock( + 'gmail', + subBlocksFromValues({ credential: '', triggerCredentials: 'dormant-trigger' }) + ).oauthCredential + ).toBeUndefined() }) it('uses trigger credentials with and without canonical metadata after action conversion', () => { diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index 3c99d1845a9..3cea8720d1b 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -142,7 +142,7 @@ export function buildSelectorContextFromBlock( // // Only fills a gap: a block that does declare the canonical id has already set it above, // including the basic/advanced active-member resolution this loop cannot express. - if (!context.oauthCredential && (!opts?.triggerMode || !resolvedGroups.has('oauthCredential'))) { + if (!context.oauthCredential && !resolvedGroups.has('oauthCredential')) { for (const [subBlockId, subBlock] of Object.entries(subBlocks)) { if (contextConfigs.find((cfg) => cfg.id === subBlockId)?.type !== 'oauth-input') { continue From 45c7abf99b0380d8721d167f6c8a2db6d55adf3c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 21 Aug 2026 19:01:21 -0700 Subject: [PATCH 4/4] fix(selectors): scope fork reconfigs to trigger mode --- .../lib/mapping/dependent-reconfigs.test.ts | 85 +++++++++++++++++++ .../lib/mapping/dependent-reconfigs.ts | 16 ++-- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.test.ts b/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.test.ts index be7eeaeba31..ed8afc7c651 100644 --- a/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.test.ts +++ b/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.test.ts @@ -131,6 +131,91 @@ describe('collectForkDependentReconfigs', () => { ]) }) + it('anchors duplicate trigger selectors to the active trigger credential', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { + id: 'credential', + title: 'Credential', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + }, + { + id: 'workspacePicker', + title: 'Workspace', + type: 'project-selector', + canonicalParamId: 'workspaceSlug', + selectorKey: 'bitbucket.workspaces', + dependsOn: ['credential'], + mode: 'basic', + }, + { + id: 'selectedTriggerId', + title: 'Trigger Type', + type: 'dropdown', + mode: 'trigger', + }, + { + id: 'triggerCredentials', + title: 'Trigger Credential', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: 'bitbucket_push' }, + }, + { + id: 'workspacePicker', + title: 'Workspace', + type: 'project-selector', + canonicalParamId: 'workspaceSlug', + selectorKey: 'bitbucket.workspaces', + dependsOn: ['triggerCredentials'], + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: 'bitbucket_push' }, + }, + { + id: 'triggerCredentials', + title: 'Trigger Credential', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: 'bitbucket_pull_request_created' }, + }, + { + id: 'workspacePicker', + title: 'Workspace', + type: 'project-selector', + canonicalParamId: 'workspaceSlug', + selectorKey: 'bitbucket.workspaces', + dependsOn: ['triggerCredentials'], + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: 'bitbucket_pull_request_created' }, + }, + ]) + ) + const state = sourceState('bitbucket', { + credential: { value: 'dormant-action-credential' }, + selectedTriggerId: { value: 'bitbucket_push' }, + triggerCredentials: { value: 'active-trigger-credential' }, + workspacePicker: { value: 'acme' }, + }) + state.blocks['block-1'].triggerMode = true + + const result = collectForkDependentReconfigs( + [replaceItem], + new Map([['wf-src', state]]), + resolve + ) + + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ + parentSourceId: 'active-trigger-credential', + subBlockKey: 'workspacePicker', + currentValue: 'acme', + }) + }) + it('skips an anchor whose canonical pair is in advanced (manual) mode - the value passes through', () => { vi.mocked(getBlock).mockReturnValue( blockWith([ diff --git a/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts b/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts index 5c022210e9a..d311a4160a0 100644 --- a/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts +++ b/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts @@ -4,6 +4,7 @@ import { coerceObjectArray } from '@/lib/workflows/persistence/remap-internal-id import { getToolInputParamConfigs } from '@/lib/workflows/search-replace/indexer' import { buildSelectorContextFromBlock, + getSelectorContextSubBlocks, SELECTOR_CONTEXT_FIELDS, } from '@/lib/workflows/subblocks/context' import { @@ -75,7 +76,7 @@ interface EmitAnchoredParams { targetWorkflowId: string /** Canonical-mode overrides for resolving the active parent member (undefined -> value heuristic). */ canonicalModes?: CanonicalModeOverrides - /** Applies trigger-only canonical precedence for a top-level trigger-mode block. */ + /** Restricts a top-level trigger-mode block to its condition-visible trigger fields. */ triggerMode?: boolean /** Memoized so the deterministic target block id is derived at most once per block. */ resolveTargetBlockId: () => string @@ -132,17 +133,18 @@ function emitAnchoredDependents(params: EmitAnchoredParams): void { canonicalModes, triggerMode, }) - const canonicalIndex = buildCanonicalIndex(config.subBlocks) - const gates = createCanonicalModeGates(config.subBlocks, values, canonicalModes) - const configById = new Map(config.subBlocks.filter((cfg) => cfg.id).map((cfg) => [cfg.id, cfg])) + const scanSubBlocks = getSelectorContextSubBlocks(config.subBlocks, values, triggerMode) + const canonicalIndex = buildCanonicalIndex(scanSubBlocks) + const gates = createCanonicalModeGates(scanSubBlocks, values, canonicalModes) + const configById = new Map(scanSubBlocks.filter((cfg) => cfg.id).map((cfg) => [cfg.id, cfg])) // Shared with `applyDependentOverrides`, so what the modal offers is exactly what the sync // can write back — the two encoded this rule separately once and drifted. - const reconfigurableIds = reconfigurableDependentIds(config.subBlocks) + const reconfigurableIds = reconfigurableDependentIds(scanSubBlocks) // A field could hang off two anchors (or be reachable via two paths); emit it once. const seen = new Set() for (const anchor of PARENT_ANCHORS) { - for (const anchorCfg of config.subBlocks) { + for (const anchorCfg of scanSubBlocks) { if (anchorCfg.type !== anchor.subBlockType || !anchorCfg.id) continue // An anchor whose canonical pair is in ADVANCED (manual) mode is skipped entirely: the // active value is the user-owned manual member's, which is verbatim by policy - a sync @@ -173,7 +175,7 @@ function emitAnchoredDependents(params: EmitAnchoredParams): void { if (typeof value === 'string' && value) context[key] = value } - for (const clear of getTransitiveSubBlockDependents(config.subBlocks, [anchorCfg.id])) { + for (const clear of getTransitiveSubBlockDependents(scanSubBlocks, [anchorCfg.id])) { const dependent = configById.get(clear.subBlockId) // A dependent is offered when the modal can actually render a control for it: a // registered selector, or a plain text field. Anything else is skipped and the