Skip to content

Commit 6b86486

Browse files
committed
fix(forking): keep dependent chains configurable
1 parent 0df2ce3 commit 6b86486

3 files changed

Lines changed: 140 additions & 16 deletions

File tree

apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
dependentKey,
88
effectiveCopyDependentValue,
99
effectiveDependentValue,
10+
getActionableDependentFields,
1011
isDependentConfigurationActionable,
1112
} from '@/ee/workspace-forking/components/fork-sync/dependent-value'
1213

@@ -184,3 +185,91 @@ describe('isDependentConfigurationActionable', () => {
184185
).toBe(false)
185186
})
186187
})
188+
189+
describe('getActionableDependentFields', () => {
190+
const unchangedMappedParent = {
191+
parentResolved: true,
192+
parentChanged: false,
193+
copying: false,
194+
}
195+
196+
it('includes the context provider for a required missing child', () => {
197+
const spreadsheet = field({
198+
subBlockKey: 'spreadsheetId',
199+
title: 'Spreadsheet',
200+
currentValue: '',
201+
providesContextKey: 'spreadsheetId',
202+
})
203+
const sheet = field({
204+
subBlockKey: 'sheetName',
205+
title: 'Sheet',
206+
currentValue: '',
207+
required: true,
208+
consumesContextKeys: ['spreadsheetId'],
209+
})
210+
211+
expect(
212+
getActionableDependentFields([spreadsheet, sheet], {}, unchangedMappedParent).map(
213+
(dependent) => dependent.subBlockKey
214+
)
215+
).toEqual(['spreadsheetId', 'sheetName'])
216+
})
217+
218+
it('keeps a saved context provider visible while its child needs configuration', () => {
219+
const spreadsheet = field({
220+
subBlockKey: 'spreadsheetId',
221+
title: 'Spreadsheet',
222+
currentValue: 'spreadsheet-target',
223+
providesContextKey: 'spreadsheetId',
224+
})
225+
const sheet = field({
226+
subBlockKey: 'sheetName',
227+
title: 'Sheet',
228+
currentValue: '',
229+
required: true,
230+
consumesContextKeys: ['spreadsheetId'],
231+
})
232+
233+
expect(
234+
getActionableDependentFields([spreadsheet, sheet], {}, unchangedMappedParent).map(
235+
(dependent) => dependent.subBlockKey
236+
)
237+
).toEqual(['spreadsheetId', 'sheetName'])
238+
})
239+
240+
it('walks transitive providers and leaves unrelated optional fields hidden', () => {
241+
const unrelated = field({
242+
subBlockKey: 'optionalLabel',
243+
title: 'Optional label',
244+
currentValue: '',
245+
})
246+
const site = field({
247+
subBlockKey: 'siteId',
248+
title: 'Site',
249+
currentValue: '',
250+
providesContextKey: 'siteId',
251+
})
252+
const drive = field({
253+
subBlockKey: 'driveId',
254+
title: 'Drive',
255+
currentValue: '',
256+
providesContextKey: 'driveId',
257+
consumesContextKeys: ['siteId'],
258+
})
259+
const spreadsheet = field({
260+
subBlockKey: 'spreadsheetId',
261+
title: 'Spreadsheet',
262+
currentValue: '',
263+
required: true,
264+
consumesContextKeys: ['driveId'],
265+
})
266+
267+
expect(
268+
getActionableDependentFields(
269+
[unrelated, site, drive, spreadsheet],
270+
{},
271+
unchangedMappedParent
272+
).map((dependent) => dependent.subBlockKey)
273+
).toEqual(['siteId', 'driveId', 'spreadsheetId'])
274+
})
275+
})

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function effectiveCopyDependentValue(
3838
return field.currentValue || field.sourceValue
3939
}
4040

41-
interface DependentConfigurationState {
41+
export interface DependentConfigurationState {
4242
parentResolved: boolean
4343
parentChanged: boolean
4444
copying: boolean
@@ -58,3 +58,36 @@ export function isDependentConfigurationActionable(
5858
if (state.parentChanged || state.copying) return true
5959
return field.required && effectiveDependentValue(field, reconfig, false) === ''
6060
}
61+
62+
/**
63+
* Actionable fields plus the transitive in-block providers that scope them. A provider belongs
64+
* in the configuration UI whenever one of its descendants needs action, even if its saved value
65+
* is present, so the user can see and change the context in which the child is selected.
66+
*/
67+
export function getActionableDependentFields(
68+
fields: ForkDependentReconfig[],
69+
reconfig: Record<string, string>,
70+
state: DependentConfigurationState
71+
): ForkDependentReconfig[] {
72+
const actionable = new Set(
73+
fields.filter((field) => isDependentConfigurationActionable(field, reconfig, state))
74+
)
75+
const providersByContextKey = new Map<string, ForkDependentReconfig>()
76+
for (const field of fields) {
77+
if (field.providesContextKey) providersByContextKey.set(field.providesContextKey, field)
78+
}
79+
80+
const pending = Array.from(actionable)
81+
for (let index = 0; index < pending.length; index += 1) {
82+
const field = pending[index]
83+
if (!field) continue
84+
for (const contextKey of field.consumesContextKeys) {
85+
const provider = providersByContextKey.get(contextKey)
86+
if (!provider || actionable.has(provider)) continue
87+
actionable.add(provider)
88+
pending.push(provider)
89+
}
90+
}
91+
92+
return fields.filter((field) => actionable.has(field))
93+
}

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ import {
3434
import { forkRefKey } from '@/ee/workspace-forking/components/fork-sync/copy-reconciliation'
3535
import { DependentFieldSelector } from '@/ee/workspace-forking/components/fork-sync/dependent-field-selector'
3636
import {
37+
type DependentConfigurationState,
3738
dependentKey,
3839
effectiveCopyDependentValue,
3940
effectiveDependentValue,
40-
isDependentConfigurationActionable,
41+
getActionableDependentFields,
4142
} from '@/ee/workspace-forking/components/fork-sync/dependent-value'
4243
import type {
4344
ForkKindSummary,
@@ -106,7 +107,8 @@ interface WorkflowDependents {
106107
function groupDependentsByWorkflow(
107108
workflows: ForkResourceUsage['workflows'],
108109
dependents: ForkDependentReconfig[],
109-
configurableDependents: ReadonlySet<ForkDependentReconfig>
110+
reconfig: Record<string, string>,
111+
state: DependentConfigurationState
110112
): WorkflowDependents[] {
111113
const byWorkflow = new Map<string, ForkDependentReconfig[]>()
112114
for (const dependent of dependents) {
@@ -128,12 +130,15 @@ function groupDependentsByWorkflow(
128130
byBlock.set(field.targetBlockId, block)
129131
}
130132
block.fields.push(field)
131-
if (configurableDependents.has(field)) block.configurableFields.push(field)
132133
}
133134
return {
134135
workflowId: workflow.workflowId,
135136
workflowName: workflow.workflowName,
136137
blocks: Array.from(byBlock.values())
138+
.map((block) => ({
139+
...block,
140+
configurableFields: getActionableDependentFields(block.fields, reconfig, state),
141+
}))
137142
.filter((block) => block.configurableFields.length > 0)
138143
.sort((a, b) => a.blockName.localeCompare(b.blockName)),
139144
}
@@ -368,18 +373,15 @@ function MappingEntry({ controller, group, entry }: MappingEntryProps) {
368373

369374
const usages = controller.usagesForEntry(entry)
370375
const dependents = controller.dependentsForEntry(entry)
371-
const workflows = useMemo(() => {
372-
const configurableDependents = new Set(
373-
dependents.filter((field) =>
374-
isDependentConfigurationActionable(field, controller.reconfig, {
375-
parentResolved: target !== '' || copying,
376-
parentChanged,
377-
copying,
378-
})
379-
)
380-
)
381-
return groupDependentsByWorkflow(usages, dependents, configurableDependents)
382-
}, [usages, dependents, controller.reconfig, target, parentChanged, copying])
376+
const workflows = useMemo(
377+
() =>
378+
groupDependentsByWorkflow(usages, dependents, controller.reconfig, {
379+
parentResolved: target !== '' || copying,
380+
parentChanged,
381+
copying,
382+
}),
383+
[usages, dependents, controller.reconfig, target, parentChanged, copying]
384+
)
383385
const configurable = workflows.filter((workflow) => workflow.blocks.length > 0)
384386
const usedOnly = workflows.filter((workflow) => workflow.blocks.length === 0)
385387

0 commit comments

Comments
 (0)