Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -556,3 +557,71 @@ describe('useForkSync post-sync reset', () => {
expect(get().dirty).toBe(true)
})
})

describe('shouldReconfigureEntry', () => {
const entry = (overrides: Partial<ForkMappingEntry> = {}): 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)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>): boolean {
export function shouldReconfigureEntry(
entry: ForkMappingEntry,
targets: Record<string, string>
): 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 ?? '')
}

Expand Down
Loading