Skip to content

Commit 0ab738e

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(workflows): honor explicit canonical modes
1 parent 4ff339e commit 0ab738e

44 files changed

Lines changed: 1663 additions & 176 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,32 @@ import {
88
isMcpToolAlreadySelected,
99
isWorkflowAlreadySelected,
1010
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils'
11+
import { getDependencyCanonicalModeOverrides } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-dependency-block-type'
12+
13+
describe('nested tool dependency modes', () => {
14+
it.each([
15+
{
16+
name: 'uses the nested tool modes instead of the host modes',
17+
context: { blockType: 'table', canonicalModeOverrides: { tableId: 'advanced' as const } },
18+
host: { '0:tableId': 'basic' as const },
19+
expected: { tableId: 'advanced' },
20+
},
21+
{
22+
name: 'keeps missing nested modes missing instead of inheriting host modes',
23+
context: { blockType: 'table', canonicalModeOverrides: undefined },
24+
host: { '0:tableId': 'advanced' as const },
25+
expected: undefined,
26+
},
27+
{
28+
name: 'uses host modes outside a nested tool',
29+
context: null,
30+
host: { tableId: 'basic' as const },
31+
expected: { tableId: 'basic' },
32+
},
33+
] as const)('$name', ({ context, host, expected }) =>
34+
expect(getDependencyCanonicalModeOverrides(context, host)).toEqual(expected)
35+
)
36+
})
1137

1238
describe('isMcpToolAlreadySelected', () => {
1339
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 | undefined
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),

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { useCallback, useMemo } from 'react'
22
import {
33
buildCanonicalIndex,
44
evaluateSubBlockCondition,
5+
getCanonicalSubBlocksForSurface,
6+
isPureTriggerBlockConfig,
57
isSubBlockFeatureEnabled,
68
isSubBlockHidden,
79
isSubBlockVisibleForMode,
810
isSubBlockVisibleForTriggerMode,
911
isToolInputOnlySubBlock,
10-
shouldUseSubBlockForTriggerModeCanonicalIndex,
1112
} from '@/lib/workflows/subblocks/visibility'
1213
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
1314
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -43,9 +44,17 @@ export function useEditorSubblockLayout(
4344
)
4445
const { config: permissionConfig } = usePermissionConfig()
4546

47+
const canonicalSubBlocks = useMemo(() => {
48+
const subBlocks = config?.subBlocks || []
49+
return getCanonicalSubBlocksForSurface(
50+
subBlocks,
51+
displayTriggerMode || isPureTriggerBlockConfig(config)
52+
)
53+
}, [config?.subBlocks, displayTriggerMode])
54+
4655
// Evaluate reactive conditions (hooks-based, must be called before useMemo)
4756
const hiddenByReactiveCondition = useReactiveConditions(
48-
config?.subBlocks || [],
57+
canonicalSubBlocks,
4958
blockId,
5059
activeWorkflowId,
5160
blockDataFromStore?.canonicalModes
@@ -102,10 +111,7 @@ export function useEditorSubblockLayout(
102111
{}
103112
)
104113

105-
const subBlocksForCanonical = displayTriggerMode
106-
? (config.subBlocks || []).filter(shouldUseSubBlockForTriggerModeCanonicalIndex)
107-
: config.subBlocks || []
108-
const canonicalIndex = buildCanonicalIndex(subBlocksForCanonical)
114+
const canonicalIndex = buildCanonicalIndex(canonicalSubBlocks)
109115
const effectiveAdvanced = displayAdvancedMode
110116
const canonicalModeOverrides = blockData?.canonicalModes
111117

@@ -169,5 +175,6 @@ export function useEditorSubblockLayout(
169175
blockDataFromStore,
170176
hiddenByReactiveCondition,
171177
permissionConfig.disableSkills,
178+
canonicalSubBlocks,
172179
])
173180
}

0 commit comments

Comments
 (0)