diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.test.tsx b/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.test.tsx index dbf6f10d502..b5c4e09b54c 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.test.tsx +++ b/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.test.tsx @@ -49,6 +49,7 @@ import { } from '@/ee/workspace-forking/components/fork-sync/dependent-value' import { type ForkSyncController, + shouldReconfigureEntry, useForkSync, } from '@/ee/workspace-forking/components/fork-sync/use-fork-sync' @@ -556,3 +557,71 @@ describe('useForkSync post-sync reset', () => { expect(get().dirty).toBe(true) }) }) + +describe('shouldReconfigureEntry', () => { + const entry = (overrides: Partial = {}): ForkMappingEntry => + ({ + kind: 'credential', + sourceId: 'cred-src', + targetId: 'cred-tgt', + suggested: false, + ...overrides, + }) as ForkMappingEntry + + it('is false for a settled non-custom-block mapping', () => { + // An unchanged credential mapping leaves its stored dependent picks valid, so its fields + // stay out of the way until something actually changes. + expect(shouldReconfigureEntry(entry(), {})).toBe(false) + }) + + it('is true for a custom block mapped to a DIFFERENT block, even once saved', () => { + // The regression: `parentChanged` drove the reconfigure UI off "was this edited in this + // session", so a saved mapping read as settled and the modal showed "no changes required" + // with no inputs. A custom block pointed elsewhere has sub-blocks keyed by the SOURCE + // block's field ids — they describe nothing on the target and nothing carries over — so it + // needs configuring for as long as the mapping stands, not just the session it was made in. + const saved = entry({ + kind: 'custom-block', + sourceId: 'custom_block_prod01', + targetId: 'custom_block_uat0001', + }) + + expect(shouldReconfigureEntry(saved, {})).toBe(true) + }) + + it('is false for a custom block mapped to itself', () => { + // "Keep the same block across environments": the type never changes, so the source's own + // field ids still describe it and its values carry across untouched. + const identity = entry({ + kind: 'custom-block', + sourceId: 'custom_block_prod01', + targetId: 'custom_block_prod01', + }) + + expect(shouldReconfigureEntry(identity, {})).toBe(false) + }) + + it('follows an in-session custom-block re-pick rather than the saved target', () => { + const saved = entry({ + kind: 'custom-block', + sourceId: 'custom_block_prod01', + targetId: 'custom_block_uat0001', + }) + const key = `${saved.kind}:${saved.sourceId}` + + // Re-pointed back at itself in-session: nothing to configure. + expect(shouldReconfigureEntry(saved, { [key]: 'custom_block_prod01' })).toBe(false) + // Re-pointed at a third block: configure against that one. + expect(shouldReconfigureEntry(saved, { [key]: 'custom_block_sbx0001' })).toBe(true) + }) + + it('is false for an unmapped custom block, which the promote blocks instead', () => { + const unmapped = entry({ + kind: 'custom-block', + sourceId: 'custom_block_prod01', + targetId: null, + }) + + expect(shouldReconfigureEntry(unmapped, {})).toBe(false) + }) +}) diff --git a/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts b/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts index 10b657a157e..28a26706642 100644 --- a/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts +++ b/apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts @@ -236,9 +236,21 @@ const entryKey = (entry: ForkMappingEntry) => forkRefKey(entry) * the dependents). Pure over (entry, in-session targets) so the inline render, the Sync * gate, and the payload build share one predicate instead of drifting copies. */ -function shouldReconfigureEntry(entry: ForkMappingEntry, targets: Record): boolean { +export function shouldReconfigureEntry( + entry: ForkMappingEntry, + targets: Record +): boolean { const next = targets[entryKey(entry)] ?? entry.targetId ?? '' if (next === '') return false + // A custom block pointed at a DIFFERENT block needs its inputs configured for as long as + // that mapping stands, not only in the session where it was picked. Every other kind can + // fall through to the in-session test because an unchanged mapping leaves its stored + // dependent values valid — a Gmail label picked under the same credential still resolves. + // A custom block has no such continuity: its sub-blocks are keyed by the SOURCE block's + // Start field ids, so under a different target they describe fields that do not exist and + // nothing carries over. Treating it as settled once saved is what left the fields hidden + // behind "no changes required" on every sync after the first. + if (entry.kind === 'custom-block') return next !== entry.sourceId return entry.suggested || next !== (entry.targetId ?? '') }