From 0df2ce380840809d8c005f93f3e1c802a4e9fb21 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 14 Aug 2026 19:27:07 -0700 Subject: [PATCH 1/3] fix(forking): hide satisfied dependent configuration --- .../fork-sync/dependent-value.test.ts | 87 +++++++++++++++++++ .../components/fork-sync/dependent-value.ts | 21 +++++ .../components/fork-sync/fork-sync-view.tsx | 41 ++++++--- 3 files changed, 137 insertions(+), 12 deletions(-) diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts index 0f4963adcad..82870f4458a 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts @@ -7,6 +7,7 @@ import { dependentKey, effectiveCopyDependentValue, effectiveDependentValue, + isDependentConfigurationActionable, } from '@/ee/workspace-forking/components/fork-sync/dependent-value' const field = (overrides: Partial = {}): ForkDependentReconfig => ({ @@ -97,3 +98,89 @@ describe('effectiveCopyDependentValue', () => { expect(effectiveCopyDependentValue(f, {})).toBe('') }) }) + +describe('isDependentConfigurationActionable', () => { + it('hides stored values when the mapped parent is unchanged', () => { + expect( + isDependentConfigurationActionable( + field({ required: true, currentValue: 'INBOX' }), + {}, + { + parentResolved: true, + parentChanged: false, + copying: false, + } + ) + ).toBe(false) + }) + + it('shows a required value that is missing under an unchanged mapped parent', () => { + expect( + isDependentConfigurationActionable( + field({ required: true, currentValue: '' }), + {}, + { + parentResolved: true, + parentChanged: false, + copying: false, + } + ) + ).toBe(true) + }) + + it('hides a missing optional value under an unchanged mapped parent', () => { + expect( + isDependentConfigurationActionable( + field({ required: false, currentValue: '' }), + {}, + { + parentResolved: true, + parentChanged: false, + copying: false, + } + ) + ).toBe(false) + }) + + it('shows every dependent when the mapped parent changed', () => { + expect( + isDependentConfigurationActionable( + field({ required: false, currentValue: 'INBOX' }), + {}, + { + parentResolved: true, + parentChanged: true, + copying: false, + } + ) + ).toBe(true) + }) + + it('shows every dependent when the parent will be copied', () => { + expect( + isDependentConfigurationActionable( + field({ required: false, currentValue: 'INBOX' }), + {}, + { + parentResolved: true, + parentChanged: false, + copying: true, + } + ) + ).toBe(true) + }) + + it('hides dependents until their parent is resolved', () => { + expect( + isDependentConfigurationActionable( + field({ required: true, currentValue: '' }), + {}, + { + parentResolved: false, + parentChanged: false, + copying: false, + } + ) + ).toBe(false) + }) +}) diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts index 04f8d213377..dddd0884535 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts @@ -37,3 +37,24 @@ export function effectiveCopyDependentValue( if (repicked !== undefined) return repicked return field.currentValue || field.sourceValue } + +interface DependentConfigurationState { + parentResolved: boolean + parentChanged: boolean + copying: boolean +} + +/** + * Whether a dependent selector needs to be shown. A changed or copied parent requires review + * because its children resolve in a different scope. An unchanged mapping only needs a selector + * when a required value is missing; its stored values are already valid and sync-ready. + */ +export function isDependentConfigurationActionable( + field: ForkDependentReconfig, + reconfig: Record, + state: DependentConfigurationState +): boolean { + if (!state.parentResolved) return false + if (state.parentChanged || state.copying) return true + return field.required && effectiveDependentValue(field, reconfig, false) === '' +} diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx index a88d06c0d84..8ee02e251f2 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx @@ -37,6 +37,7 @@ import { dependentKey, effectiveCopyDependentValue, effectiveDependentValue, + isDependentConfigurationActionable, } from '@/ee/workspace-forking/components/fork-sync/dependent-value' import type { ForkKindSummary, @@ -89,6 +90,7 @@ interface DependentBlock { targetBlockId: string blockName: string fields: ForkDependentReconfig[] + configurableFields: ForkDependentReconfig[] } interface WorkflowDependents { @@ -103,7 +105,8 @@ interface WorkflowDependents { */ function groupDependentsByWorkflow( workflows: ForkResourceUsage['workflows'], - dependents: ForkDependentReconfig[] + dependents: ForkDependentReconfig[], + configurableDependents: ReadonlySet ): WorkflowDependents[] { const byWorkflow = new Map() for (const dependent of dependents) { @@ -116,15 +119,23 @@ function groupDependentsByWorkflow( for (const field of byWorkflow.get(workflow.workflowId) ?? []) { let block = byBlock.get(field.targetBlockId) if (!block) { - block = { targetBlockId: field.targetBlockId, blockName: field.blockName, fields: [] } + block = { + targetBlockId: field.targetBlockId, + blockName: field.blockName, + fields: [], + configurableFields: [], + } byBlock.set(field.targetBlockId, block) } block.fields.push(field) + if (configurableDependents.has(field)) block.configurableFields.push(field) } return { workflowId: workflow.workflowId, workflowName: workflow.workflowName, - blocks: Array.from(byBlock.values()).sort((a, b) => a.blockName.localeCompare(b.blockName)), + blocks: Array.from(byBlock.values()) + .filter((block) => block.configurableFields.length > 0) + .sort((a, b) => a.blockName.localeCompare(b.blockName)), } }) } @@ -260,7 +271,7 @@ function DependentWorkflowCard({ setReconfig, }: DependentWorkflowCardProps) { const [collapsed, setCollapsed] = useState( - () => !workflow.blocks.some((block) => block.fields.some((field) => field.required)) + () => !workflow.blocks.some((block) => block.configurableFields.some((field) => field.required)) ) return (
{workflow.blocks.map((block) => { - const topLevel = block.fields.filter((field) => !field.toolName) + const topLevel = block.configurableFields.filter((field) => !field.toolName) const byTool = new Map() - for (const field of block.fields) { + for (const field of block.configurableFields) { if (!field.toolName) continue const list = byTool.get(field.toolName) if (list) list.push(field) @@ -357,12 +368,18 @@ function MappingEntry({ controller, group, entry }: MappingEntryProps) { const usages = controller.usagesForEntry(entry) const dependents = controller.dependentsForEntry(entry) - // Group once per (usages, dependents) change - both keep stable references from the - // controller's memoized maps, so this skips recompute across the page's frequent re-renders. - const workflows = useMemo( - () => groupDependentsByWorkflow(usages, dependents), - [usages, dependents] - ) + const workflows = useMemo(() => { + const configurableDependents = new Set( + dependents.filter((field) => + isDependentConfigurationActionable(field, controller.reconfig, { + parentResolved: target !== '' || copying, + parentChanged, + copying, + }) + ) + ) + return groupDependentsByWorkflow(usages, dependents, configurableDependents) + }, [usages, dependents, controller.reconfig, target, parentChanged, copying]) const configurable = workflows.filter((workflow) => workflow.blocks.length > 0) const usedOnly = workflows.filter((workflow) => workflow.blocks.length === 0) From 6b8648617c490b8f2502829e01cf22372ef9017f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 14 Aug 2026 21:45:47 -0700 Subject: [PATCH 2/3] fix(forking): keep dependent chains configurable --- .../fork-sync/dependent-value.test.ts | 89 +++++++++++++++++++ .../components/fork-sync/dependent-value.ts | 35 +++++++- .../components/fork-sync/fork-sync-view.tsx | 32 +++---- 3 files changed, 140 insertions(+), 16 deletions(-) diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts index 82870f4458a..7bfda245378 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts @@ -7,6 +7,7 @@ import { dependentKey, effectiveCopyDependentValue, effectiveDependentValue, + getActionableDependentFields, isDependentConfigurationActionable, } from '@/ee/workspace-forking/components/fork-sync/dependent-value' @@ -184,3 +185,91 @@ describe('isDependentConfigurationActionable', () => { ).toBe(false) }) }) + +describe('getActionableDependentFields', () => { + const unchangedMappedParent = { + parentResolved: true, + parentChanged: false, + copying: false, + } + + it('includes the context provider for a required missing child', () => { + const spreadsheet = field({ + subBlockKey: 'spreadsheetId', + title: 'Spreadsheet', + currentValue: '', + providesContextKey: 'spreadsheetId', + }) + const sheet = field({ + subBlockKey: 'sheetName', + title: 'Sheet', + currentValue: '', + required: true, + consumesContextKeys: ['spreadsheetId'], + }) + + expect( + getActionableDependentFields([spreadsheet, sheet], {}, unchangedMappedParent).map( + (dependent) => dependent.subBlockKey + ) + ).toEqual(['spreadsheetId', 'sheetName']) + }) + + it('keeps a saved context provider visible while its child needs configuration', () => { + const spreadsheet = field({ + subBlockKey: 'spreadsheetId', + title: 'Spreadsheet', + currentValue: 'spreadsheet-target', + providesContextKey: 'spreadsheetId', + }) + const sheet = field({ + subBlockKey: 'sheetName', + title: 'Sheet', + currentValue: '', + required: true, + consumesContextKeys: ['spreadsheetId'], + }) + + expect( + getActionableDependentFields([spreadsheet, sheet], {}, unchangedMappedParent).map( + (dependent) => dependent.subBlockKey + ) + ).toEqual(['spreadsheetId', 'sheetName']) + }) + + it('walks transitive providers and leaves unrelated optional fields hidden', () => { + const unrelated = field({ + subBlockKey: 'optionalLabel', + title: 'Optional label', + currentValue: '', + }) + const site = field({ + subBlockKey: 'siteId', + title: 'Site', + currentValue: '', + providesContextKey: 'siteId', + }) + const drive = field({ + subBlockKey: 'driveId', + title: 'Drive', + currentValue: '', + providesContextKey: 'driveId', + consumesContextKeys: ['siteId'], + }) + const spreadsheet = field({ + subBlockKey: 'spreadsheetId', + title: 'Spreadsheet', + currentValue: '', + required: true, + consumesContextKeys: ['driveId'], + }) + + expect( + getActionableDependentFields( + [unrelated, site, drive, spreadsheet], + {}, + unchangedMappedParent + ).map((dependent) => dependent.subBlockKey) + ).toEqual(['siteId', 'driveId', 'spreadsheetId']) + }) +}) diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts index dddd0884535..21eeb689cdf 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts @@ -38,7 +38,7 @@ export function effectiveCopyDependentValue( return field.currentValue || field.sourceValue } -interface DependentConfigurationState { +export interface DependentConfigurationState { parentResolved: boolean parentChanged: boolean copying: boolean @@ -58,3 +58,36 @@ export function isDependentConfigurationActionable( if (state.parentChanged || state.copying) return true return field.required && effectiveDependentValue(field, reconfig, false) === '' } + +/** + * Actionable fields plus the transitive in-block providers that scope them. A provider belongs + * in the configuration UI whenever one of its descendants needs action, even if its saved value + * is present, so the user can see and change the context in which the child is selected. + */ +export function getActionableDependentFields( + fields: ForkDependentReconfig[], + reconfig: Record, + state: DependentConfigurationState +): ForkDependentReconfig[] { + const actionable = new Set( + fields.filter((field) => isDependentConfigurationActionable(field, reconfig, state)) + ) + const providersByContextKey = new Map() + for (const field of fields) { + if (field.providesContextKey) providersByContextKey.set(field.providesContextKey, field) + } + + const pending = Array.from(actionable) + for (let index = 0; index < pending.length; index += 1) { + const field = pending[index] + if (!field) continue + for (const contextKey of field.consumesContextKeys) { + const provider = providersByContextKey.get(contextKey) + if (!provider || actionable.has(provider)) continue + actionable.add(provider) + pending.push(provider) + } + } + + return fields.filter((field) => actionable.has(field)) +} diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx index 8ee02e251f2..15bd6821b6b 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx @@ -34,10 +34,11 @@ import { import { forkRefKey } from '@/ee/workspace-forking/components/fork-sync/copy-reconciliation' import { DependentFieldSelector } from '@/ee/workspace-forking/components/fork-sync/dependent-field-selector' import { + type DependentConfigurationState, dependentKey, effectiveCopyDependentValue, effectiveDependentValue, - isDependentConfigurationActionable, + getActionableDependentFields, } from '@/ee/workspace-forking/components/fork-sync/dependent-value' import type { ForkKindSummary, @@ -106,7 +107,8 @@ interface WorkflowDependents { function groupDependentsByWorkflow( workflows: ForkResourceUsage['workflows'], dependents: ForkDependentReconfig[], - configurableDependents: ReadonlySet + reconfig: Record, + state: DependentConfigurationState ): WorkflowDependents[] { const byWorkflow = new Map() for (const dependent of dependents) { @@ -128,12 +130,15 @@ function groupDependentsByWorkflow( byBlock.set(field.targetBlockId, block) } block.fields.push(field) - if (configurableDependents.has(field)) block.configurableFields.push(field) } return { workflowId: workflow.workflowId, workflowName: workflow.workflowName, blocks: Array.from(byBlock.values()) + .map((block) => ({ + ...block, + configurableFields: getActionableDependentFields(block.fields, reconfig, state), + })) .filter((block) => block.configurableFields.length > 0) .sort((a, b) => a.blockName.localeCompare(b.blockName)), } @@ -368,18 +373,15 @@ function MappingEntry({ controller, group, entry }: MappingEntryProps) { const usages = controller.usagesForEntry(entry) const dependents = controller.dependentsForEntry(entry) - const workflows = useMemo(() => { - const configurableDependents = new Set( - dependents.filter((field) => - isDependentConfigurationActionable(field, controller.reconfig, { - parentResolved: target !== '' || copying, - parentChanged, - copying, - }) - ) - ) - return groupDependentsByWorkflow(usages, dependents, configurableDependents) - }, [usages, dependents, controller.reconfig, target, parentChanged, copying]) + const workflows = useMemo( + () => + groupDependentsByWorkflow(usages, dependents, controller.reconfig, { + parentResolved: target !== '' || copying, + parentChanged, + copying, + }), + [usages, dependents, controller.reconfig, target, parentChanged, copying] + ) const configurable = workflows.filter((workflow) => workflow.blocks.length > 0) const usedOnly = workflows.filter((workflow) => workflow.blocks.length === 0) From c761851660c55c685249cee1b3e4bc1f275f82a1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 14 Aug 2026 22:13:26 -0700 Subject: [PATCH 3/3] fix(forking): invalidate stale dependent selectors --- .../fork-sync/dependent-value.test.ts | 69 +++++++++++++++++ .../components/fork-sync/dependent-value.ts | 34 +++++++++ .../components/fork-sync/fork-sync-view.tsx | 27 +------ .../lib/mapping/dependent-reconfigs.test.ts | 74 ++++++++++++++++++- .../lib/mapping/dependent-reconfigs.ts | 4 +- 5 files changed, 182 insertions(+), 26 deletions(-) diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts index 7bfda245378..d91517dd389 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest' import type { ForkDependentReconfig } from '@/lib/api/contracts/workspace-fork' import { + applyDependentRepick, dependentKey, effectiveCopyDependentValue, effectiveDependentValue, @@ -100,6 +101,74 @@ describe('effectiveCopyDependentValue', () => { }) }) +describe('applyDependentRepick', () => { + it('clears direct and transitive descendants without touching unrelated fields', () => { + const site = field({ + subBlockKey: 'siteId', + currentValue: 'site-old', + providesContextKey: 'siteId', + }) + const drive = field({ + subBlockKey: 'driveId', + currentValue: 'drive-old', + providesContextKey: 'driveId', + consumesContextKeys: ['siteId'], + }) + const spreadsheet = field({ + subBlockKey: 'spreadsheetId', + currentValue: 'spreadsheet-old', + providesContextKey: 'spreadsheetId', + consumesContextKeys: ['driveId'], + }) + const sheet = field({ + subBlockKey: 'sheetName', + currentValue: 'Sheet1', + consumesContextKeys: ['spreadsheetId'], + }) + const unrelated = field({ subBlockKey: 'label', currentValue: 'keep-me' }) + const previous = { + [dependentKey(drive)]: 'drive-repicked', + [dependentKey(spreadsheet)]: 'spreadsheet-repicked', + [dependentKey(sheet)]: 'Sheet2', + [dependentKey(unrelated)]: 'still-keep-me', + } + + const next = applyDependentRepick( + previous, + site, + [site, drive, spreadsheet, sheet, unrelated], + 'site-new' + ) + + expect(next).toEqual({ + [dependentKey(site)]: 'site-new', + [dependentKey(drive)]: '', + [dependentKey(spreadsheet)]: '', + [dependentKey(sheet)]: '', + [dependentKey(unrelated)]: 'still-keep-me', + }) + expect(effectiveDependentValue(drive, next, false)).toBe('') + expect(effectiveCopyDependentValue(sheet, next)).toBe('') + }) + + it('only changes the selected field when it provides no selector context', () => { + const leaf = field({ subBlockKey: 'issueKey', currentValue: 'ISSUE-1' }) + const unrelated = field({ subBlockKey: 'label', currentValue: 'keep-me' }) + + expect( + applyDependentRepick( + { [dependentKey(unrelated)]: 'still-keep-me' }, + leaf, + [leaf, unrelated], + 'ISSUE-2' + ) + ).toEqual({ + [dependentKey(leaf)]: 'ISSUE-2', + [dependentKey(unrelated)]: 'still-keep-me', + }) + }) +}) + describe('isDependentConfigurationActionable', () => { it('hides stored values when the mapped parent is unchanged', () => { expect( diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts index 21eeb689cdf..84cd3a2dc36 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.ts @@ -5,6 +5,40 @@ export function dependentKey(dependent: ForkDependentReconfig): string { return `${dependent.targetWorkflowId}:${dependent.targetBlockId}:${dependent.subBlockKey}` } +/** + * Store a dependent re-pick and clear every selector transitively scoped by it. Empty-string + * overrides are intentional: an absent override means "fall back to the stored value", while a + * changed provider makes every stored descendant stale for both mapped and copied parents. + */ +export function applyDependentRepick( + reconfig: Record, + changedField: ForkDependentReconfig, + blockFields: ForkDependentReconfig[], + value: string +): Record { + const changedKey = dependentKey(changedField) + const nextState = { ...reconfig, [changedKey]: value } + if (!changedField.providesContextKey) return nextState + + const pendingContextKeys = [changedField.providesContextKey] + const visitedFields = new Set([changedKey]) + for (let index = 0; index < pendingContextKeys.length; index += 1) { + const contextKey = pendingContextKeys[index] + if (!contextKey) continue + + for (const field of blockFields) { + const fieldKey = dependentKey(field) + if (visitedFields.has(fieldKey) || !field.consumesContextKeys.includes(contextKey)) continue + + visitedFields.add(fieldKey) + nextState[fieldKey] = '' + if (field.providesContextKey) pendingContextKeys.push(field.providesContextKey) + } + } + + return nextState +} + /** * The value sent + displayed for a dependent: the user's in-session re-pick if present, else the * stored value (`currentValue`). Blank when the parent target changed in-session, since the old diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx index 15bd6821b6b..2042ab03b61 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-sync/fork-sync-view.tsx @@ -34,6 +34,7 @@ import { import { forkRefKey } from '@/ee/workspace-forking/components/fork-sync/copy-reconciliation' import { DependentFieldSelector } from '@/ee/workspace-forking/components/fork-sync/dependent-field-selector' import { + applyDependentRepick, type DependentConfigurationState, dependentKey, effectiveCopyDependentValue, @@ -162,28 +163,6 @@ function blockChainState( return { providedValues, providedContextKeys } } -/** Store a re-pick and invalidate in-block children chained off the changed field. */ -function applyDependentRepick( - setReconfig: Dispatch>>, - field: ForkDependentReconfig, - blockFields: ForkDependentReconfig[], - value: string -) { - setReconfig((prev) => { - const nextState = { ...prev, [dependentKey(field)]: value } - // A changed parent invalidates its children's stale re-picks. - const providedKey = field.providesContextKey - if (providedKey) { - for (const sibling of blockFields) { - if (sibling.consumesContextKeys.includes(providedKey)) { - delete nextState[dependentKey(sibling)] - } - } - } - return nextState - }) -} - interface DependentSelectorProps { field: ForkDependentReconfig block: DependentBlock @@ -241,7 +220,9 @@ function DependentSelector({ }} enabled={parentValue !== '' && ready} value={effectiveValue(field)} - onChange={(value) => applyDependentRepick(setReconfig, field, block.fields, value)} + onChange={(value) => + setReconfig((current) => applyDependentRepick(current, field, block.fields, value)) + } title={field.title} /> ) 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 50accd71868..542deab06e8 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 @@ -20,10 +20,19 @@ const blockWith = (subBlocks: SubBlockConfig[]): BlockConfig => const sourceState = ( blockType: string, - subBlocks: Record + subBlocks: Record, + data?: Record ): WorkflowState => ({ - blocks: { 'block-1': { id: 'block-1', type: blockType, name: 'Block', subBlocks } }, + blocks: { + 'block-1': { + id: 'block-1', + type: blockType, + name: 'Block', + subBlocks, + ...(data && { data }), + }, + }, edges: [], loops: {}, parallels: {}, @@ -325,6 +334,67 @@ describe('collectForkDependentReconfigs', () => { expect(sheet?.context.spreadsheetId).toBe('ss-src') }) + it('uses the persisted canonical mode when building a dependent selector context', () => { + vi.mocked(getBlock).mockReturnValue( + blockWith([ + { id: 'credential', title: 'Credential', type: 'oauth-input' }, + { id: 'domain', title: 'Domain', type: 'short-input' }, + { + id: 'projectId', + title: 'Project', + type: 'project-selector', + canonicalParamId: 'projectId', + mode: 'basic', + selectorKey: 'jira.projects', + dependsOn: ['credential', 'domain'], + }, + { + id: 'manualProjectId', + title: 'Project ID', + type: 'short-input', + canonicalParamId: 'projectId', + mode: 'advanced', + dependsOn: ['credential', 'domain'], + }, + { + id: 'issueKey', + title: 'Issue', + type: 'file-selector', + selectorKey: 'jira.issues', + dependsOn: ['credential', 'domain', 'projectId'], + required: true, + }, + ]) + ) + const states = new Map([ + [ + 'wf-src', + sourceState( + 'jira', + { + credential: { value: 'cred-src' }, + domain: { value: 'example.atlassian.net' }, + projectId: { value: 'project-basic-stale' }, + manualProjectId: { value: 'project-advanced' }, + issueKey: { value: 'ADV-1' }, + }, + { canonicalModes: { projectId: 'advanced' } } + ), + ], + ]) + + const result = collectForkDependentReconfigs([replaceItem], states, resolve) + + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ + subBlockKey: 'issueKey', + context: { + domain: 'example.atlassian.net', + projectId: 'project-advanced', + }, + }) + }) + it('emits a credential-dependent selector nested inside a tool-input tool', () => { vi.mocked(getBlock).mockImplementation((type) => { if (type === 'agent') return blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }]) 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 4a4bbbf2a5e..e064216e588 100644 --- a/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts +++ b/apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts @@ -109,7 +109,9 @@ function emitAnchoredDependents(params: EmitAnchoredParams): void { chaining, out, } = params - const fullContext = buildSelectorContextFromBlock(contextBlockType, contextSubBlocks) + const fullContext = buildSelectorContextFromBlock(contextBlockType, contextSubBlocks, { + canonicalModes, + }) 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]))