Skip to content

Commit ac84737

Browse files
icecrasher321claude
andcommitted
fix(workspace-forking): let an optional custom-block boolean return to its default
A two-segment switch has no transition back to "nothing selected", so once a user picked True or False there was no way to stop overriding the target workflow's declared default — a single click pinned the flag for every later sync. An optional boolean now carries a third `Default` segment, trailing the two real values because choosing one is the common action and reverting is the escape hatch. A required boolean keeps two: the Sync gate demands a value, so unset is not a state it can end in and offering it would present an unsubmittable choice. Reported by Greptile on #6871. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8973f61 commit ac84737

3 files changed

Lines changed: 80 additions & 4 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import {
6+
CUSTOM_BLOCK_BOOLEAN_FALSE,
7+
CUSTOM_BLOCK_BOOLEAN_TRUE,
8+
CUSTOM_BLOCK_BOOLEAN_UNSET,
9+
customBlockBooleanOptions,
10+
customBlockInputControl,
11+
} from '@/ee/workspace-forking/components/fork-sync/custom-block-input-control'
12+
13+
describe('customBlockInputControl', () => {
14+
it('matches how the canvas renders each field type', () => {
15+
// Mirrors `subBlockTypeForField`: a field configured here must behave the way it will
16+
// once the block is open in the editor.
17+
expect(customBlockInputControl('boolean')).toBe('switch')
18+
expect(customBlockInputControl('object')).toBe('textarea')
19+
expect(customBlockInputControl('array')).toBe('textarea')
20+
expect(customBlockInputControl('string')).toBe('input')
21+
expect(customBlockInputControl('number')).toBe('input')
22+
})
23+
24+
it('falls back to a plain input for an unknown or absent type', () => {
25+
expect(customBlockInputControl('something-new')).toBe('input')
26+
expect(customBlockInputControl(undefined)).toBe('input')
27+
})
28+
})
29+
30+
describe('customBlockBooleanOptions', () => {
31+
it('lets an OPTIONAL flag return to the workflow default', () => {
32+
// Without this a single click permanently pins the flag: a two-segment switch has no
33+
// transition back to "nothing selected", so every later sync would keep overriding the
34+
// child's declared default.
35+
const options = customBlockBooleanOptions(false)
36+
37+
expect(options.map((o) => o.value)).toEqual([
38+
CUSTOM_BLOCK_BOOLEAN_TRUE,
39+
CUSTOM_BLOCK_BOOLEAN_FALSE,
40+
CUSTOM_BLOCK_BOOLEAN_UNSET,
41+
])
42+
})
43+
44+
it('offers a REQUIRED flag only real values', () => {
45+
// The Sync gate demands a value, so "unset" is not a state it can end in — offering it
46+
// would present a choice that cannot be submitted.
47+
const options = customBlockBooleanOptions(true)
48+
49+
expect(options.map((o) => o.value)).toEqual([
50+
CUSTOM_BLOCK_BOOLEAN_TRUE,
51+
CUSTOM_BLOCK_BOOLEAN_FALSE,
52+
])
53+
})
54+
})

apps/sim/ee/workspace-forking/components/fork-sync/custom-block-input-control.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,30 @@ export function customBlockInputControl(fieldType: string | undefined): CustomBl
3131
export const CUSTOM_BLOCK_BOOLEAN_TRUE = 'true'
3232
export const CUSTOM_BLOCK_BOOLEAN_FALSE = 'false'
3333

34-
/** Segments for a boolean input, in the order a switch reads them. */
35-
export const CUSTOM_BLOCK_BOOLEAN_OPTIONS = [
34+
/**
35+
* The unset value. Distinct from `false`: it means the sync writes no value at all, so the
36+
* target workflow's Start field keeps whatever default it declares.
37+
*/
38+
export const CUSTOM_BLOCK_BOOLEAN_UNSET = ''
39+
40+
const BOOLEAN_VALUE_OPTIONS = [
3641
{ value: CUSTOM_BLOCK_BOOLEAN_TRUE, label: 'True' },
3742
{ value: CUSTOM_BLOCK_BOOLEAN_FALSE, label: 'False' },
3843
] as const
44+
45+
const BOOLEAN_OPTIONAL_OPTIONS = [
46+
...BOOLEAN_VALUE_OPTIONS,
47+
// Trails the two real values: choosing one is the common action, returning to the default
48+
// is the escape hatch. Without it a single click would permanently pin an optional flag,
49+
// since a two-segment switch has no transition back to "nothing selected".
50+
{ value: CUSTOM_BLOCK_BOOLEAN_UNSET, label: 'Default' },
51+
] as const
52+
53+
/**
54+
* Segments for a boolean input. An OPTIONAL field gets a third `Default` segment so the user
55+
* can stop overriding the child workflow's declared default; a REQUIRED one does not, because
56+
* the Sync gate demands a value and "unset" is not a state it can end in.
57+
*/
58+
export function customBlockBooleanOptions(required: boolean) {
59+
return required ? BOOLEAN_VALUE_OPTIONS : BOOLEAN_OPTIONAL_OPTIONS
60+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +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_OPTIONS,
37+
customBlockBooleanOptions,
3838
customBlockInputControl,
3939
} from '@/ee/workspace-forking/components/fork-sync/custom-block-input-control'
4040
import { DependentFieldSelector } from '@/ee/workspace-forking/components/fork-sync/dependent-field-selector'
@@ -233,7 +233,7 @@ function DependentSelector({
233233
return (
234234
<ChipModalField {...shared} type='custom'>
235235
<ChipSwitch
236-
options={CUSTOM_BLOCK_BOOLEAN_OPTIONS}
236+
options={customBlockBooleanOptions(field.required)}
237237
// Passed through unmapped: an unset field is `''`, which matches neither
238238
// segment, so the switch renders with nothing selected. Coercing it to False
239239
// would show a required flag as configured while the Sync gate still reads it

0 commit comments

Comments
 (0)