Skip to content

Commit 5bc2955

Browse files
fix(workspace-forking): keep a repointed custom block's inputs configurable after the mapping is saved (#6877)
Mapping a custom block to a different block and syncing showed "no changes required" with no fields to fill, so the inputs #6871 added were unreachable on every sync after the one where the mapping was picked. `parentChanged` comes from `shouldReconfigureEntry`, which asks whether the target was edited IN THIS SESSION. Saving the mapping makes it false, and the reconfigure listing then keeps only fields that are both required and empty — so an optional input disappeared entirely and a filled required one never came back. That test is right for every other kind: an unchanged credential or table mapping leaves its stored dependent picks valid, and 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 — the mapping standing IS the reason to configure, whenever it was made. A custom block mapped to a different block is now always actionable; mapped to itself ("keep the same block across environments") it is not, since its own field ids still describe it. An in-session re-pick still wins over the saved target. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 40aa8ad commit 5bc2955

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.test.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
} from '@/ee/workspace-forking/components/fork-sync/dependent-value'
5050
import {
5151
type ForkSyncController,
52+
shouldReconfigureEntry,
5253
useForkSync,
5354
} from '@/ee/workspace-forking/components/fork-sync/use-fork-sync'
5455

@@ -556,3 +557,71 @@ describe('useForkSync post-sync reset', () => {
556557
expect(get().dirty).toBe(true)
557558
})
558559
})
560+
561+
describe('shouldReconfigureEntry', () => {
562+
const entry = (overrides: Partial<ForkMappingEntry> = {}): ForkMappingEntry =>
563+
({
564+
kind: 'credential',
565+
sourceId: 'cred-src',
566+
targetId: 'cred-tgt',
567+
suggested: false,
568+
...overrides,
569+
}) as ForkMappingEntry
570+
571+
it('is false for a settled non-custom-block mapping', () => {
572+
// An unchanged credential mapping leaves its stored dependent picks valid, so its fields
573+
// stay out of the way until something actually changes.
574+
expect(shouldReconfigureEntry(entry(), {})).toBe(false)
575+
})
576+
577+
it('is true for a custom block mapped to a DIFFERENT block, even once saved', () => {
578+
// The regression: `parentChanged` drove the reconfigure UI off "was this edited in this
579+
// session", so a saved mapping read as settled and the modal showed "no changes required"
580+
// with no inputs. A custom block pointed elsewhere has sub-blocks keyed by the SOURCE
581+
// block's field ids — they describe nothing on the target and nothing carries over — so it
582+
// needs configuring for as long as the mapping stands, not just the session it was made in.
583+
const saved = entry({
584+
kind: 'custom-block',
585+
sourceId: 'custom_block_prod01',
586+
targetId: 'custom_block_uat0001',
587+
})
588+
589+
expect(shouldReconfigureEntry(saved, {})).toBe(true)
590+
})
591+
592+
it('is false for a custom block mapped to itself', () => {
593+
// "Keep the same block across environments": the type never changes, so the source's own
594+
// field ids still describe it and its values carry across untouched.
595+
const identity = entry({
596+
kind: 'custom-block',
597+
sourceId: 'custom_block_prod01',
598+
targetId: 'custom_block_prod01',
599+
})
600+
601+
expect(shouldReconfigureEntry(identity, {})).toBe(false)
602+
})
603+
604+
it('follows an in-session custom-block re-pick rather than the saved target', () => {
605+
const saved = entry({
606+
kind: 'custom-block',
607+
sourceId: 'custom_block_prod01',
608+
targetId: 'custom_block_uat0001',
609+
})
610+
const key = `${saved.kind}:${saved.sourceId}`
611+
612+
// Re-pointed back at itself in-session: nothing to configure.
613+
expect(shouldReconfigureEntry(saved, { [key]: 'custom_block_prod01' })).toBe(false)
614+
// Re-pointed at a third block: configure against that one.
615+
expect(shouldReconfigureEntry(saved, { [key]: 'custom_block_sbx0001' })).toBe(true)
616+
})
617+
618+
it('is false for an unmapped custom block, which the promote blocks instead', () => {
619+
const unmapped = entry({
620+
kind: 'custom-block',
621+
sourceId: 'custom_block_prod01',
622+
targetId: null,
623+
})
624+
625+
expect(shouldReconfigureEntry(unmapped, {})).toBe(false)
626+
})
627+
})

apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,21 @@ const entryKey = (entry: ForkMappingEntry) => forkRefKey(entry)
236236
* the dependents). Pure over (entry, in-session targets) so the inline render, the Sync
237237
* gate, and the payload build share one predicate instead of drifting copies.
238238
*/
239-
function shouldReconfigureEntry(entry: ForkMappingEntry, targets: Record<string, string>): boolean {
239+
export function shouldReconfigureEntry(
240+
entry: ForkMappingEntry,
241+
targets: Record<string, string>
242+
): boolean {
240243
const next = targets[entryKey(entry)] ?? entry.targetId ?? ''
241244
if (next === '') return false
245+
// A custom block pointed at a DIFFERENT block needs its inputs configured for as long as
246+
// that mapping stands, not only in the session where it was picked. Every other kind can
247+
// fall through to the in-session test because an unchanged mapping leaves its stored
248+
// dependent values valid — a Gmail label picked under the same credential still resolves.
249+
// A custom block has no such continuity: its sub-blocks are keyed by the SOURCE block's
250+
// Start field ids, so under a different target they describe fields that do not exist and
251+
// nothing carries over. Treating it as settled once saved is what left the fields hidden
252+
// behind "no changes required" on every sync after the first.
253+
if (entry.kind === 'custom-block') return next !== entry.sourceId
242254
return entry.suggested || next !== (entry.targetId ?? '')
243255
}
244256

0 commit comments

Comments
 (0)