Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export function useFetchedOptions({
workflowId: activeWorkflowId ?? undefined,
workspaceId: workspaceId ?? undefined,
canonicalModes: block.data?.canonicalModes,
triggerMode: block.triggerMode,
})
if (selectorExcludeSelf && activeWorkflowId) context.excludeWorkflowId = activeWorkflowId
return context
Expand Down
131 changes: 131 additions & 0 deletions apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,137 @@ describe('collectForkDependentReconfigs', () => {
])
})

it('uses trigger canonical context for top-level trigger-mode reconfiguration', () => {
vi.mocked(getBlock).mockReturnValue(
blockWith([
{
id: 'credential',
title: 'Action Credential',
type: 'oauth-input',
canonicalParamId: 'oauthCredential',
mode: 'basic',
},
{
id: 'workspaceSelector',
title: 'Action Workspace',
type: 'project-selector',
canonicalParamId: 'teamId',
mode: 'basic',
},
{
id: 'triggerCredentials',
title: 'Trigger Credential',
type: 'oauth-input',
canonicalParamId: 'oauthCredential',
mode: 'trigger',
},
{
id: 'triggerWorkspaceId',
title: 'Trigger Workspace',
type: 'dropdown',
canonicalParamId: 'teamId',
dependsOn: ['triggerCredentials'],
selectorKey: 'clickup.workspaces',
mode: 'trigger',
},
])
)
const state = sourceState('clickup', {
credential: { value: 'action-credential' },
workspaceSelector: { value: 'action-workspace' },
triggerCredentials: { value: 'trigger-credential' },
triggerWorkspaceId: { value: 'trigger-workspace' },
})
state.blocks['block-1'].triggerMode = true

const result = collectForkDependentReconfigs(
[replaceItem],
new Map([['wf-src', state]]),
resolve
)

expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
parentSourceId: 'trigger-credential',
subBlockKey: 'triggerWorkspaceId',
context: { teamId: 'trigger-workspace' },
})
})

it('keeps nested tool selector context in action mode inside a trigger block', () => {
const agentConfig = blockWith([{ id: 'tools', title: 'Tools', type: 'tool-input' }])
const clickupToolConfig = blockWith([
{
id: 'credential',
title: 'Action Credential',
type: 'oauth-input',
canonicalParamId: 'oauthCredential',
mode: 'basic',
},
{
id: 'workspaceSelector',
title: 'Action Workspace',
type: 'project-selector',
canonicalParamId: 'teamId',
mode: 'basic',
},
{
id: 'triggerCredentials',
title: 'Trigger Credential',
type: 'oauth-input',
canonicalParamId: 'oauthCredential',
mode: 'trigger',
},
{
id: 'triggerWorkspaceId',
title: 'Trigger Workspace',
type: 'dropdown',
canonicalParamId: 'teamId',
mode: 'trigger',
},
{
id: 'listId',
title: 'List',
type: 'short-input',
dependsOn: ['credential'],
required: true,
},
])
vi.mocked(getBlock).mockImplementation((type) =>
type === 'agent' ? agentConfig : clickupToolConfig
)
const state = sourceState('agent', {
tools: {
value: [
{
type: 'clickup',
params: {
credential: 'action-credential',
workspaceSelector: 'action-workspace',
triggerCredentials: 'trigger-credential',
triggerWorkspaceId: 'trigger-workspace',
listId: 'list-1',
},
},
],
},
})
state.blocks['block-1'].triggerMode = true

const result = collectForkDependentReconfigs(
[replaceItem],
new Map([['wf-src', state]]),
resolve
)

expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
parentSourceId: 'action-credential',
subBlockKey: 'tools[0].listId',
context: { teamId: 'action-workspace' },
})
})

it('skips an anchor whose canonical pair is in advanced (manual) mode - the value passes through', () => {
vi.mocked(getBlock).mockReturnValue(
blockWith([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ interface EmitAnchoredParams {
targetWorkflowId: string
/** Canonical-mode overrides for resolving the active parent member (undefined -> value heuristic). */
canonicalModes?: CanonicalModeOverrides
/** Applies trigger-only canonical precedence for a top-level trigger-mode block. */
triggerMode?: boolean
/** Memoized so the deterministic target block id is derived at most once per block. */
resolveTargetBlockId: () => string
/** Map a dependent's config id to its wire `subBlockKey` (identity, or nested `tools[i].id`). */
Expand Down Expand Up @@ -117,6 +119,7 @@ function emitAnchoredDependents(params: EmitAnchoredParams): void {
blockName,
targetWorkflowId,
canonicalModes,
triggerMode,
resolveTargetBlockId,
makeSubBlockKey,
makeTitle,
Expand All @@ -127,6 +130,7 @@ function emitAnchoredDependents(params: EmitAnchoredParams): void {
} = params
const fullContext = buildSelectorContextFromBlock(contextBlockType, contextSubBlocks, {
canonicalModes,
triggerMode,
})
const canonicalIndex = buildCanonicalIndex(config.subBlocks)
const gates = createCanonicalModeGates(config.subBlocks, values, canonicalModes)
Expand Down Expand Up @@ -327,6 +331,7 @@ export function collectForkDependentReconfigs(
blockName: block.name,
targetWorkflowId: item.targetWorkflowId,
canonicalModes: block.data?.canonicalModes,
triggerMode: block.triggerMode,
resolveTargetBlockId: resolveBlockId,
makeSubBlockKey: (id) => id,
makeTitle: (dependent) => dependent.title ?? dependent.id ?? '',
Expand Down
69 changes: 68 additions & 1 deletion apps/sim/hooks/queries/dynamic-subblock-options.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,33 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'

const { mockGetSelectorDefinition } = vi.hoisted(() => ({
const { mockBuildSelectorContextFromBlock, mockGetSelectorDefinition } = vi.hoisted(() => ({
mockBuildSelectorContextFromBlock: vi.fn(
(_blockType: string, _subBlocks: unknown, opts?: { workspaceId?: string }) => ({
workspaceId: opts?.workspaceId,
})
),
mockGetSelectorDefinition: vi.fn(),
}))

vi.mock('@/hooks/selectors/registry', () => ({
getSelectorDefinition: mockGetSelectorDefinition,
}))

vi.mock('@/lib/workflows/subblocks/context', () => ({
buildSelectorContextFromBlock: mockBuildSelectorContextFromBlock,
}))

import type { SubBlockConfig } from '@/blocks/types'
import {
dynamicSubBlockOptionKeys,
useDynamicSubBlockOptionDisplayName,
} from '@/hooks/queries/dynamic-subblock-options'
import type { SelectorDefinition, SelectorKey } from '@/hooks/selectors/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState } from '@/stores/workflows/workflow/types'

/** Any registered key; the hook only uses it to look the definition up. */
const SELECTOR_KEY = 'workspace.credentialGroups' as SelectorKey
Expand Down Expand Up @@ -70,6 +83,9 @@ describe('useDynamicSubBlockOptionDisplayName', () => {

afterEach(() => {
mounted.splice(0).forEach((unmount) => unmount())
useWorkflowRegistry.setState({ activeWorkflowId: null })
useWorkflowStore.setState({ blocks: {} })
useSubBlockStore.setState({ workflowValues: {} })
vi.clearAllMocks()
})

Expand Down Expand Up @@ -130,6 +146,57 @@ describe('useDynamicSubBlockOptionDisplayName', () => {
await waitForResult(() => expect(hook.result()).toBe('Gmail, Slack'))
})

it('uses trigger mode when hydrating a trigger field label', async () => {
const block = {
id: 'block-1',
type: 'gmail',
name: 'Gmail trigger',
position: { x: 0, y: 0 },
subBlocks: {},
outputs: {},
enabled: true,
triggerMode: true,
} satisfies BlockState
useWorkflowRegistry.setState({ activeWorkflowId: 'workflow-1' })
useWorkflowStore.setState({ blocks: { 'block-1': block } })
useSubBlockStore.setState({
workflowValues: {
'workflow-1': {
'block-1': { triggerCredentials: 'trigger-credential' },
},
},
})

const fetchById = vi.fn(async ({ detailId }: { detailId?: string }) => ({
id: detailId as string,
label: 'Inbox',
}))
mockDefinition({ key: SELECTOR_KEY, getQueryKey: () => [SELECTOR_KEY], fetchById })
const subBlock = {
id: 'labelIds',
title: 'Labels',
type: 'dropdown',
selectorKey: SELECTOR_KEY,
} satisfies SubBlockConfig

const hook = renderHookWithClient(() =>
useDynamicSubBlockOptionDisplayName({
workspaceId: 'workspace-1',
blockId: 'block-1',
subBlock,
value: 'INBOX',
})
)
mounted.push(hook.unmount)

await waitForResult(() => expect(hook.result()).toBe('Inbox'))
expect(mockBuildSelectorContextFromBlock).toHaveBeenCalledWith(
'gmail',
expect.objectContaining({ triggerCredentials: { value: 'trigger-credential' } }),
expect.objectContaining({ triggerMode: true })
)
})

it('re-resolves a label when the sibling its selector depends on changes', () => {
// The bug: `fetchById` reads sibling context, but the cache key did not, so a label
// resolved before a credential group was picked (null) stayed cached after it was, and the
Expand Down
1 change: 1 addition & 0 deletions apps/sim/hooks/queries/dynamic-subblock-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function useDynamicSubBlockOptionDisplayName({
workflowId: activeWorkflowId ?? undefined,
workspaceId,
canonicalModes: block.data?.canonicalModes,
triggerMode: block.triggerMode,
})
}, [block, liveValues, activeWorkflowId, workspaceId])

Expand Down
41 changes: 38 additions & 3 deletions apps/sim/lib/workflows/comparison/format-description.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockGetBlock } = vi.hoisted(() => ({
const { mockBuildSelectorContextFromBlock, mockGetBlock } = vi.hoisted(() => ({
mockBuildSelectorContextFromBlock: vi.fn(() => ({})),
mockGetBlock: vi.fn(),
}))

Expand Down Expand Up @@ -32,7 +33,7 @@ vi.mock('@/blocks/registry', () => ({
}))

vi.mock('@/lib/workflows/subblocks/context', () => ({
buildSelectorContextFromBlock: vi.fn(() => ({})),
buildSelectorContextFromBlock: mockBuildSelectorContextFromBlock,
}))

vi.mock('@/hooks/queries/oauth/oauth-credentials', () => ({
Expand All @@ -54,7 +55,11 @@ import {
formatDiffSummaryForDescriptionAsync,
generateWorkflowDiffSummary,
} from '@/lib/workflows/comparison/compare'
import { formatValueForDisplay, resolveFieldLabel } from '@/lib/workflows/comparison/resolve-values'
import {
formatValueForDisplay,
resolveFieldLabel,
resolveValueForDisplay,
} from '@/lib/workflows/comparison/resolve-values'

function emptyDiffSummary(overrides: Partial<WorkflowDiffSummary> = {}): WorkflowDiffSummary {
return {
Expand Down Expand Up @@ -131,6 +136,36 @@ describe('formatValueForDisplay', () => {
})
})

describe('resolveValueForDisplay', () => {
it('builds off-canvas selector context with the block trigger mode', async () => {
mockGetBlock.mockReturnValue({
subBlocks: [{ id: 'labelIds', title: 'Labels', type: 'short-input' }],
})

await resolveValueForDisplay('INBOX', {
blockType: 'gmail',
subBlockId: 'labelIds',
workflowId: 'workflow-1',
currentState: {
blocks: {
'block-1': {
type: 'gmail',
triggerMode: true,
subBlocks: { triggerCredentials: { value: 'trigger-credential' } },
},
},
} as never,
blockId: 'block-1',
})

expect(mockBuildSelectorContextFromBlock).toHaveBeenCalledWith(
'gmail',
expect.any(Object),
expect.objectContaining({ triggerMode: true })
)
})
})

describe('formatDiffSummaryForDescription', () => {
it('returns no-changes message for empty diff', () => {
const result = formatDiffSummaryForDescription(emptyDiffSummary())
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/workflows/comparison/resolve-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ function extractSelectorContext(
workflowId,
workspaceId,
canonicalModes: block.data?.canonicalModes,
triggerMode: block.triggerMode,
})
}

Expand Down
Loading
Loading