Skip to content

Commit 8973f61

Browse files
icecrasher321claude
andcommitted
fix(workspace-forking): keep an unset custom-block boolean unset
Boolean handling collapsed a tri-state. `''` is a flag the user never touched, and it is not `false`. On apply, any non-`'true'` string became `false` — so an untouched optional flag was written as one. `assembleCustomBlockInputMapping` skips `''` but keeps `false`, so that value reached the child's `inputMapping` and overrode whatever default the Start field declares. Only an explicit `'true'`/`'false'` is applied now; anything else leaves the field unset, and the child's own default stands. In the modal the switch mapped `''` to the False segment, so a required flag rendered as configured while the Sync gate still read it as empty — the same display-versus-gate split the previous commit moved into `effectiveDependentValue` to close, reintroduced one layer up. The value is passed through unmapped instead: `''` matches neither segment, so the switch renders with nothing selected, which is what it is. Reported by Greptile and Cursor Bugbot on #6871. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7b2fa42 commit 8973f61

3 files changed

Lines changed: 43 additions & 11 deletions

File tree

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ import {
3434
} from '@/ee/workspace-forking/components/fork-sync/cleared-refs-list'
3535
import { forkRefKey } from '@/ee/workspace-forking/components/fork-sync/copy-reconciliation'
3636
import {
37-
CUSTOM_BLOCK_BOOLEAN_FALSE,
3837
CUSTOM_BLOCK_BOOLEAN_OPTIONS,
39-
CUSTOM_BLOCK_BOOLEAN_TRUE,
4038
customBlockInputControl,
4139
} from '@/ee/workspace-forking/components/fork-sync/custom-block-input-control'
4240
import { DependentFieldSelector } from '@/ee/workspace-forking/components/fork-sync/dependent-field-selector'
@@ -236,11 +234,11 @@ function DependentSelector({
236234
<ChipModalField {...shared} type='custom'>
237235
<ChipSwitch
238236
options={CUSTOM_BLOCK_BOOLEAN_OPTIONS}
239-
value={
240-
value === CUSTOM_BLOCK_BOOLEAN_TRUE
241-
? CUSTOM_BLOCK_BOOLEAN_TRUE
242-
: CUSTOM_BLOCK_BOOLEAN_FALSE
243-
}
237+
// Passed through unmapped: an unset field is `''`, which matches neither
238+
// segment, so the switch renders with nothing selected. Coercing it to False
239+
// would show a required flag as configured while the Sync gate still reads it
240+
// as empty — the display-versus-gate split this whole carve-out exists to avoid.
241+
value={value}
244242
onChange={setValue}
245243
aria-label={field.title}
246244
/>

apps/sim/ee/workspace-forking/lib/copy/copy-workflows.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,4 +687,31 @@ describe('copyWorkflowStateIntoTarget custom-block remap', () => {
687687
// A string field whose value happens to read "true" stays a string.
688688
expect(subBlocks.text?.value).toBe('true')
689689
})
690+
691+
it('leaves an unset boolean unset rather than writing false', async () => {
692+
// The modal submits '' for an untouched optional flag. Coercing that to `false` writes a
693+
// value the user never chose: `assembleCustomBlockInputMapping` skips '' but keeps
694+
// `false`, so it would reach the child's inputMapping and override the Start field's own
695+
// default. Only an explicit 'false' means false.
696+
mockSaveWorkflowToNormalizedTables.mockResolvedValue({ success: true })
697+
698+
await copyWorkflowStateIntoTarget({
699+
...baseParams,
700+
tx: stubTx(),
701+
transformBlockType: (type) => (type === UAT ? PROD : type),
702+
dependentOverrides: new Map([
703+
[
704+
'tgt-blk-cb',
705+
new Map([
706+
[`${PROD}::boolean::untouched`, ''],
707+
[`${PROD}::boolean::explicit-false`, 'false'],
708+
]),
709+
],
710+
]),
711+
})
712+
713+
const subBlocks = writtenBlock().subBlocks ?? {}
714+
expect(subBlocks).not.toHaveProperty('untouched')
715+
expect(subBlocks['explicit-false']?.value).toBe(false)
716+
})
690717
})

apps/sim/ee/workspace-forking/lib/remap/remap-references.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,12 +313,19 @@ export function replaceCustomBlockInputs(
313313
const parsed = parseCustomBlockInputStorageKey(key)
314314
if (!parsed || parsed.targetType !== targetType) continue
315315
if (RESERVED_PARAMS.has(parsed.fieldId)) continue
316-
// A `boolean` field's sub-block is a `switch`, which the canvas stores as a real boolean.
316+
if (parsed.fieldType === 'boolean') {
317+
// A `boolean` field's sub-block is a `switch`, which the canvas stores as a real boolean
318+
// — but only `'true'`/`'false'` mean anything. An untouched optional flag submits `''`,
319+
// and coercing that to `false` would write a value the user never chose:
320+
// `assembleCustomBlockInputMapping` skips `''` and keeps `false`, so it would reach the
321+
// child's `inputMapping` and override the Start field's own default. Leave it unset.
322+
if (value !== 'true' && value !== 'false') continue
323+
next[parsed.fieldId] = { value: value === 'true' }
324+
continue
325+
}
317326
// Everything else is stored as text: `object`/`array` are authored as JSON and parsed by
318327
// the executor, and a number rides a `short-input` like it does on the canvas.
319-
next[parsed.fieldId] = {
320-
value: parsed.fieldType === 'boolean' ? value === 'true' : value,
321-
}
328+
next[parsed.fieldId] = { value }
322329
}
323330
return next
324331
}

0 commit comments

Comments
 (0)