Skip to content

Commit efb0937

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(workflows): honor explicit canonical modes
1 parent c155a57 commit efb0937

40 files changed

Lines changed: 1972 additions & 127 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { useCallback, useEffect, useRef } from 'react'
4+
import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility'
45
import {
56
buildToolSubBlockId,
67
resolveToolParamSync,
@@ -21,6 +22,7 @@ interface ToolSubBlockRendererProps {
2122
/** The tool's block type (e.g. `gmail`), so its params' selectors resolve dependencies. */
2223
toolType: string
2324
toolParams: Record<string, string> | undefined
25+
canonicalModeOverrides?: CanonicalModeOverrides
2426
onParamChange: (toolIndex: number, paramId: string, value: string) => void
2527
disabled: boolean
2628
canonicalToggle?: {
@@ -59,6 +61,7 @@ export function ToolSubBlockRenderer({
5961
effectiveParamId,
6062
toolType,
6163
toolParams,
64+
canonicalModeOverrides,
6265
onParamChange,
6366
disabled,
6467
canonicalToggle,
@@ -132,7 +135,7 @@ export function ToolSubBlockRenderer({
132135
}
133136

134137
return (
135-
<DependencyBlockTypeProvider value={toolType}>
138+
<DependencyBlockTypeProvider value={{ blockType: toolType, canonicalModeOverrides }}>
136139
<SubBlock
137140
blockId={blockId}
138141
config={config}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,82 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5+
import {
6+
buildCanonicalIndex,
7+
getCanonicalSubBlocksForSurface,
8+
resolveDependencyValue,
9+
scopeCanonicalModesForTool,
10+
} from '@/lib/workflows/subblocks/visibility'
511
import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types'
612
import {
713
isCustomToolAlreadySelected,
814
isMcpToolAlreadySelected,
915
isWorkflowAlreadySelected,
1016
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils'
17+
import { getDependencyCanonicalModeOverrides } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-dependency-block-type'
18+
import { TableBlock } from '@/blocks/blocks/table'
19+
20+
describe('nested tool dependency modes', () => {
21+
const tableCanonicalIndex = buildCanonicalIndex(
22+
getCanonicalSubBlocksForSurface(TableBlock.subBlocks, false)
23+
)
24+
25+
it.each([
26+
[0, 'advanced', 'dormant-basic', '', ''],
27+
[1, 'basic', '', 'dormant-advanced', ''],
28+
] as const)(
29+
'honors the explicit mode for same-type tool instance %s',
30+
(toolIndex, mode, basicValue, advancedValue, expected) => {
31+
const hostOverrides = {
32+
'0:tableId': 'advanced' as const,
33+
'1:tableId': 'basic' as const,
34+
}
35+
const scopedOverrides = scopeCanonicalModesForTool(hostOverrides, toolIndex, 'table')
36+
const dependencyOverrides = getDependencyCanonicalModeOverrides(
37+
{ blockType: 'table', canonicalModeOverrides: scopedOverrides },
38+
hostOverrides
39+
)
40+
41+
expect(dependencyOverrides?.tableId).toBe(mode)
42+
expect(
43+
resolveDependencyValue(
44+
'tableId',
45+
{ tableSelector: basicValue, manualTableId: advancedValue },
46+
tableCanonicalIndex,
47+
dependencyOverrides
48+
)
49+
).toBe(expected)
50+
}
51+
)
52+
53+
it('preserves legacy type-scoped modes and missing-mode inference', () => {
54+
const values = { tableSelector: 'legacy-basic', manualTableId: '' }
55+
const legacyScoped = scopeCanonicalModesForTool({ 'table:tableId': 'advanced' }, 3, 'table')
56+
57+
expect(
58+
resolveDependencyValue(
59+
'tableId',
60+
values,
61+
tableCanonicalIndex,
62+
getDependencyCanonicalModeOverrides(
63+
{ blockType: 'table', canonicalModeOverrides: legacyScoped },
64+
undefined
65+
)
66+
)
67+
).toBe('')
68+
expect(
69+
resolveDependencyValue(
70+
'tableId',
71+
values,
72+
tableCanonicalIndex,
73+
getDependencyCanonicalModeOverrides(
74+
{ blockType: 'table', canonicalModeOverrides: undefined },
75+
{ '0:tableId': 'advanced' }
76+
)
77+
)
78+
).toBe('legacy-basic')
79+
})
80+
})
1181

1282
describe('isMcpToolAlreadySelected', () => {
1383
describe('basic functionality', () => {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import {
105105
type CanonicalIndex,
106106
type CanonicalModeOverrides,
107107
evaluateSubBlockCondition,
108+
getCanonicalSubBlocksForSurface,
108109
isCanonicalPair,
109110
reindexToolCanonicalModes,
110111
resolveCanonicalMode,
@@ -526,13 +527,14 @@ export const ToolInput = memo(function ToolInput({
526527
for (const [toolIndex, tool] of selectedTools.entries()) {
527528
const blockConfig = allBlocks.find((b: { type: string }) => b.type === tool.type)
528529
if (!blockConfig?.subBlocks) continue
529-
const toolCanonical = buildCanonicalIndex(blockConfig.subBlocks)
530+
const actionSubBlocks = getCanonicalSubBlocksForSurface(blockConfig.subBlocks, false)
531+
const toolCanonical = buildCanonicalIndex(actionSubBlocks)
530532
const scopedOverrides = scopeCanonicalModesForTool(
531533
canonicalModeOverrides,
532534
toolIndex,
533535
tool.type
534536
)
535-
const reactiveSubBlock = blockConfig.subBlocks.find(
537+
const reactiveSubBlock = actionSubBlocks.find(
536538
(sb: { reactiveCondition?: unknown }) => sb.reactiveCondition
537539
)
538540
const reactiveCond = reactiveSubBlock?.reactiveCondition as
@@ -1744,14 +1746,17 @@ export const ToolInput = memo(function ToolInput({
17441746
)
17451747
: null
17461748

1747-
const toolCanonicalIndex: CanonicalIndex | null = toolBlock?.subBlocks
1748-
? buildCanonicalIndex(toolBlock.subBlocks)
1749+
const toolActionSubBlocks = toolBlock?.subBlocks
1750+
? getCanonicalSubBlocksForSurface(toolBlock.subBlocks, false)
1751+
: null
1752+
const toolCanonicalIndex: CanonicalIndex | null = toolActionSubBlocks
1753+
? buildCanonicalIndex(toolActionSubBlocks)
17491754
: null
17501755

17511756
const toolContextValues = toolCanonicalIndex
17521757
? buildPreviewContextValues(tool.params || {}, {
17531758
blockType: tool.type,
1754-
subBlocks: toolBlock!.subBlocks,
1759+
subBlocks: toolActionSubBlocks!,
17551760
canonicalIndex: toolCanonicalIndex,
17561761
values: { operation: tool.operation, ...tool.params },
17571762
overrides: toolScopedOverrides,
@@ -2149,6 +2154,7 @@ export const ToolInput = memo(function ToolInput({
21492154
effectiveParamId={effectiveParamId}
21502155
toolType={tool.type}
21512156
toolParams={tool.params}
2157+
canonicalModeOverrides={toolScopedOverrides}
21522158
onParamChange={handleParamChange}
21532159
disabled={disabled}
21542160
canonicalToggle={canonicalToggleProp}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { useCallback, useMemo } from 'react'
22
import { isEqual } from 'es-toolkit'
33
import { useStoreWithEqualityFn } from 'zustand/traditional'
4-
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
4+
import {
5+
buildCanonicalIndex,
6+
getCanonicalSubBlocksForSurface,
7+
isPureTriggerBlockConfig,
8+
resolveDependencyValue,
9+
} from '@/lib/workflows/subblocks/visibility'
510
import { getBlock } from '@/blocks/registry'
611
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
712
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -22,10 +27,15 @@ export function useCanonicalSubBlockValue<T = unknown>(
2227
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
2328
const blockState = useWorkflowStore((state) => state.blocks[blockId])
2429
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
25-
const canonicalIndex = useMemo(
26-
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
27-
[blockConfig?.subBlocks]
28-
)
30+
const canonicalIndex = useMemo(() => {
31+
const subBlocks = blockConfig?.subBlocks || []
32+
return buildCanonicalIndex(
33+
getCanonicalSubBlocksForSurface(
34+
subBlocks,
35+
Boolean(blockState?.triggerMode) || isPureTriggerBlockConfig(blockConfig ?? undefined)
36+
)
37+
)
38+
}, [blockConfig?.subBlocks, blockState?.triggerMode])
2939
const canonicalModeOverrides = blockState?.data?.canonicalModes
3040

3141
return useStoreWithEqualityFn(
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
'use client'
22

33
import { createContext, useContext } from 'react'
4+
import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility'
45

5-
const DependencyBlockTypeContext = createContext<string | null>(null)
6+
export interface DependencyBlockContextValue {
7+
blockType: string
8+
canonicalModeOverrides?: CanonicalModeOverrides
9+
}
610

7-
/**
8-
* Provider set by tool-input param rendering (value = the tool's block type, e.g. `gmail`).
9-
*/
11+
const DependencyBlockTypeContext = createContext<DependencyBlockContextValue | null>(null)
12+
13+
/** Provides a nested tool's block type and already-scoped canonical modes. */
1014
export const DependencyBlockTypeProvider = DependencyBlockTypeContext.Provider
1115

12-
/**
13-
* The block type whose config should drive dependency (`dependsOn`) canonical resolution
14-
* for the current subblock. Null for normal blocks (resolve against the host block). Set
15-
* to the tool's type for tool-input params, so a nested tool's selector resolves its
16-
* parents against the TOOL's config (e.g. a Gmail tool's `credential` -> `oauthCredential`,
17-
* which the host Agent block's subblocks don't define) and can fetch its options.
18-
*/
19-
export const useDependencyBlockType = () => useContext(DependencyBlockTypeContext)
16+
export const useDependencyBlockContext = () => useContext(DependencyBlockTypeContext)
17+
18+
export function getDependencyCanonicalModeOverrides(
19+
context: DependencyBlockContextValue | null,
20+
hostOverrides: CanonicalModeOverrides | undefined
21+
): CanonicalModeOverrides | undefined {
22+
// A nested tool with no scoped mode must use legacy inference, not another tool's host keys.
23+
return context ? context.canonicalModeOverrides : hostOverrides
24+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { isEqual } from 'es-toolkit'
55
import { useStoreWithEqualityFn } from 'zustand/traditional'
66
import {
77
buildCanonicalIndex,
8+
getCanonicalSubBlocksForSurface,
89
isNonEmptyValue,
10+
isPureTriggerBlockConfig,
911
normalizeDependencyValue,
1012
parseDependsOn,
1113
resolveDependencyValue,
@@ -15,7 +17,10 @@ import type { SubBlockConfig } from '@/blocks/types'
1517
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1618
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1719
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
18-
import { useDependencyBlockType } from './use-dependency-block-type'
20+
import {
21+
getDependencyCanonicalModeOverrides,
22+
useDependencyBlockContext,
23+
} from './use-dependency-block-type'
1924

2025
/**
2126
* Centralized dependsOn gating for sub-block components.
@@ -35,17 +40,26 @@ export function useDependsOnGate(
3540
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
3641
const blockState = useWorkflowStore((state) => state.blocks[blockId])
3742

38-
const dependencyBlockType = useDependencyBlockType()
43+
const dependencyBlockContext = useDependencyBlockContext()
44+
const dependencyBlockType = dependencyBlockContext?.blockType
3945
const blockConfig = dependencyBlockType
4046
? getBlock(dependencyBlockType)
4147
: blockState?.type
4248
? getBlock(blockState.type)
4349
: null
44-
const canonicalIndex = useMemo(
45-
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
46-
[blockConfig?.subBlocks]
50+
const canonicalIndex = useMemo(() => {
51+
const subBlocks = blockConfig?.subBlocks || []
52+
return buildCanonicalIndex(
53+
getCanonicalSubBlocksForSurface(
54+
subBlocks,
55+
Boolean(blockState?.triggerMode) || isPureTriggerBlockConfig(blockConfig ?? undefined)
56+
)
57+
)
58+
}, [blockConfig?.subBlocks, blockState?.triggerMode])
59+
const canonicalModeOverrides = getDependencyCanonicalModeOverrides(
60+
dependencyBlockContext,
61+
blockState?.data?.canonicalModes
4762
)
48-
const canonicalModeOverrides = blockState?.data?.canonicalModes
4963

5064
// Parse dependsOn config to get all/any field lists
5165
const { allFields, anyFields, allDependsOnFields } = useMemo(

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-fetched-options.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { getErrorMessage } from '@sim/utils/errors'
33
import { isEqual } from 'es-toolkit'
44
import { useStoreWithEqualityFn } from 'zustand/traditional'
5-
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
5+
import {
6+
buildCanonicalIndex,
7+
getCanonicalSubBlocksForSurface,
8+
isPureTriggerBlockConfig,
9+
resolveDependencyValue,
10+
} from '@/lib/workflows/subblocks/visibility'
611
import { getBlock } from '@/blocks/registry'
712
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
813
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -73,10 +78,15 @@ export function useFetchedOptions({
7378
const blockState = useWorkflowStore((state) => state.blocks[blockId])
7479
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
7580
const canonicalModeOverrides = blockState?.data?.canonicalModes
76-
const canonicalIndex = useMemo(
77-
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
78-
[blockConfig?.subBlocks]
79-
)
81+
const canonicalIndex = useMemo(() => {
82+
const subBlocks = blockConfig?.subBlocks || []
83+
return buildCanonicalIndex(
84+
getCanonicalSubBlocksForSurface(
85+
subBlocks,
86+
Boolean(blockState?.triggerMode) || isPureTriggerBlockConfig(blockConfig ?? undefined)
87+
)
88+
)
89+
}, [blockConfig?.subBlocks, blockState?.triggerMode])
8090

8191
const dependencyValues = useStoreWithEqualityFn(
8292
useSubBlockStore,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ import { isRetryEligibleBlock } from '@/lib/workflows/blocks/retry-eligibility'
2323
import {
2424
buildCanonicalIndex,
2525
evaluateSubBlockCondition,
26+
getCanonicalSubBlocksForSurface,
2627
hasAdvancedValues,
2728
isCanonicalPair,
29+
isPureTriggerBlockConfig,
2830
isStandaloneAdvancedMode,
2931
resolveCanonicalMode,
30-
shouldUseSubBlockForTriggerModeCanonicalIndex,
3132
} from '@/lib/workflows/subblocks/visibility'
3233
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
3334
import {
@@ -159,9 +160,11 @@ export function Editor() {
159160

160161
const subBlocksForCanonical = useMemo(() => {
161162
const subBlocks = blockConfig?.subBlocks || []
162-
if (!triggerMode) return subBlocks
163-
return subBlocks.filter(shouldUseSubBlockForTriggerModeCanonicalIndex)
164-
}, [blockConfig?.subBlocks, triggerMode])
163+
return getCanonicalSubBlocksForSurface(
164+
subBlocks,
165+
triggerMode || isPureTriggerBlockConfig(blockConfig ?? undefined)
166+
)
167+
}, [blockConfig, triggerMode])
165168

166169
const canonicalIndex = useMemo(
167170
() => buildCanonicalIndex(subBlocksForCanonical),

0 commit comments

Comments
 (0)