diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx index 96dfa8108af..f81cb1b7721 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx @@ -9,6 +9,7 @@ import { import type { SelectorKey } from '@/lib/selectors/manifest' import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state' import { getDependsOnFields } from '@/lib/workflows/subblocks/dependencies' +import { withCanonicalModes } from '@/lib/workflows/subblocks/visibility' import { staleSelectionOptions } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/stale-selections' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight' @@ -172,10 +173,7 @@ export const Dropdown = memo(function Dropdown({ ) const evaluatedOptions = useMemo(() => { if (typeof options !== 'function') return options ?? EMPTY_OPTIONS - const values = blockValues ?? {} - return options({ - values: canonicalModes ? { ...values, __canonicalModes: canonicalModes } : values, - }) + return options({ values: withCanonicalModes(blockValues ?? {}, canonicalModes) }) }, [options, blockValues, canonicalModes]) const reactiveOptions = useMemo( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts index d9feec0800e..000f0701c29 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts @@ -1,12 +1,14 @@ import { useCallback, useMemo } from 'react' import { buildCanonicalIndexForSurface, + buildSubBlockValues, evaluateSubBlockCondition, isSubBlockFeatureEnabled, isSubBlockHidden, isSubBlockVisibleForMode, isSubBlockVisibleForTriggerMode, isToolInputOnlySubBlock, + withCanonicalModes, } from '@/lib/workflows/subblocks/visibility' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -93,24 +95,10 @@ export function useEditorSubblockLayout( }) } - // Filter visible blocks and those that meet their conditions - const rawValues = Object.entries(stateToUse).reduce>( - (acc, [key, entry]) => { - acc[key] = entry?.value - return acc - }, - {} - ) - const canonicalIndex = buildCanonicalIndexForSurface(config.subBlocks || [], displayTriggerMode) const effectiveAdvanced = displayAdvancedMode const canonicalModeOverrides = blockData?.canonicalModes - - // Expose canonical mode overrides to condition functions so they can - // react to basic/advanced credential toggles (e.g. SERVICE_ACCOUNT_SUBBLOCKS). - if (canonicalModeOverrides) { - rawValues.__canonicalModes = canonicalModeOverrides - } + const rawValues = withCanonicalModes(buildSubBlockValues(stateToUse), canonicalModeOverrides) const visibleSubBlocks = (config.subBlocks || []).filter((block) => { if (block.hidden) return false diff --git a/apps/sim/lib/workflows/autolayout/utils.ts b/apps/sim/lib/workflows/autolayout/utils.ts index 91e9eefba2d..a4364464ac3 100644 --- a/apps/sim/lib/workflows/autolayout/utils.ts +++ b/apps/sim/lib/workflows/autolayout/utils.ts @@ -37,6 +37,7 @@ import { isSubBlockVisibleForMode, isToolInputOnlySubBlock, isTriggerModeSubBlock, + withCanonicalModes, } from '@/lib/workflows/subblocks/visibility' import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' @@ -188,15 +189,14 @@ function getVisiblePreviewSubBlocks(block: BlockState): { } } - const rawValues = buildSubBlockValues(block.subBlocks || {}) const canonicalModeOverrides = typeof block.data?.canonicalModes === 'object' && block.data.canonicalModes !== null ? (block.data.canonicalModes as CanonicalModeOverrides) : undefined - - if (canonicalModeOverrides) { - rawValues.__canonicalModes = canonicalModeOverrides - } + const rawValues = withCanonicalModes( + buildSubBlockValues(block.subBlocks || {}), + canonicalModeOverrides + ) const effectiveAdvanced = Boolean(block.advancedMode) const effectiveTrigger = Boolean(block.triggerMode) diff --git a/apps/sim/lib/workflows/blocks/canvas-card-fields.test.ts b/apps/sim/lib/workflows/blocks/canvas-card-fields.test.ts index 796f673414a..f6c946d06a8 100644 --- a/apps/sim/lib/workflows/blocks/canvas-card-fields.test.ts +++ b/apps/sim/lib/workflows/blocks/canvas-card-fields.test.ts @@ -95,4 +95,19 @@ describe('getCardSubBlocks', () => { const onCard = triggerCardIds(BLOCK_REGISTRY.webflow) expect(onCard).toEqual(expect.arrayContaining(['triggerCredentials', 'triggerSiteId'])) }) + + it('reads the active start frame member when picking video settings', () => { + // The stale upload is the inactive member, so the card shows the prompt-only settings. + const videoGenerator = BLOCK_REGISTRY.video_generator_v3 + const onCard = getCardSubBlocks(videoGenerator, { + advanced: true, + values: { + provider: 'falai', + model: 'alibaba/happy-horse/text-to-video', + visualReferenceUpload: { name: 'frame.png', key: 'k', size: 3, type: 'image/png' }, + }, + canonicalModeOverrides: { visualReference: 'advanced' }, + }).map((subBlock) => subBlock.id) + expect(onCard).toContain('falAspectRatio') + }) }) diff --git a/apps/sim/lib/workflows/blocks/canvas-card-fields.ts b/apps/sim/lib/workflows/blocks/canvas-card-fields.ts index 8dfb5de1260..39388c1390d 100644 --- a/apps/sim/lib/workflows/blocks/canvas-card-fields.ts +++ b/apps/sim/lib/workflows/blocks/canvas-card-fields.ts @@ -7,6 +7,7 @@ import { isSubBlockVisibleForMode, isToolInputOnlySubBlock, isTriggerModeSubBlock, + withCanonicalModes, } from '@/lib/workflows/subblocks/visibility' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' @@ -88,6 +89,7 @@ export function getCardSubBlocks( } = options const canonicalIndex = buildCanonicalIndexForSurface(config.subBlocks, triggerMode) + const conditionValues = withCanonicalModes(values, canonicalModeOverrides) const isPureTriggerBlock = Boolean(config.triggers?.enabled && config.category === 'triggers') return config.subBlocks.filter((subBlock) => { @@ -114,7 +116,8 @@ export function getCardSubBlocks( return false } - if (subBlock.condition && !evaluateSubBlockCondition(subBlock.condition, values)) return false + if (subBlock.condition && !evaluateSubBlockCondition(subBlock.condition, conditionValues)) + return false /* The operation is the card's heading here, so it is not also a field. */ if (titleOperationSubBlockId && subBlock.id === titleOperationSubBlockId) return false diff --git a/apps/sim/lib/workflows/subblocks/visibility.ts b/apps/sim/lib/workflows/subblocks/visibility.ts index 15b797fe72f..62eb82b169d 100644 --- a/apps/sim/lib/workflows/subblocks/visibility.ts +++ b/apps/sim/lib/workflows/subblocks/visibility.ts @@ -94,6 +94,18 @@ export function buildSubBlockValues( }, {}) } +/** + * The values a block's sub-block conditions read: its values plus, when set, its canonical mode + * overrides under `__canonicalModes`, so a condition reads the active member of a basic/advanced + * pair instead of guessing it from which member holds a value. + */ +export function withCanonicalModes( + values: Record, + canonicalModes: CanonicalModeOverrides | undefined +): Record { + return canonicalModes ? { ...values, __canonicalModes: canonicalModes } : values +} + /** * Build canonical group indices for a block's subblocks. */ diff --git a/apps/sim/serializer/field-analysis.test.ts b/apps/sim/serializer/field-analysis.test.ts index 30ea8241171..6aa8300a48f 100644 --- a/apps/sim/serializer/field-analysis.test.ts +++ b/apps/sim/serializer/field-analysis.test.ts @@ -138,6 +138,27 @@ describe('collectBlockFieldIssues', () => { }) }) +const FRAME_FILE = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' } +const FAL_VALUES = { provider: 'falai', apiKey: 'test-key', prompt: 'A cinematic sunrise' } + +/** Serializes a Video Generator block and maps its params to the video tool body. */ +function serializeVideoBlock( + values: Record, + canonicalModes?: Record +) { + svcConfig.value = VideoGeneratorV3Block + const params = extractBlockParams( + block({ + type: 'svc', + data: canonicalModes ? { canonicalModes } : {}, + subBlocks: Object.fromEntries( + Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }]) + ), + }) + ) + return videoToolBodySchema.parse(VideoGeneratorV3Block.tools.config?.params?.(params)) +} + describe('extractBlockParams', () => { it.each([ { @@ -191,19 +212,8 @@ describe('extractBlockParams', () => { }, }, ])('preserves $name through serialization into the provider request', ({ settings, payload }) => { - svcConfig.value = VideoGeneratorV3Block - const values = { apiKey: 'test-key', prompt: 'A cinematic sunrise', ...settings } - const params = extractBlockParams( - block({ - type: 'svc', - subBlocks: Object.fromEntries( - Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }]) - ), - }) - ) - const mapped = VideoGeneratorV3Block.tools.config?.params?.(params) const request = buildDirectVideoRequest({ - ...videoToolBodySchema.parse(mapped), + ...serializeVideoBlock({ apiKey: 'test-key', prompt: 'A cinematic sunrise', ...settings }), provider: settings.provider, }) expect(request.body).toMatchObject(payload) @@ -227,34 +237,13 @@ describe('extractBlockParams', () => { ])( 'sends the model default for a fal setting the endpoint rejects when $name', ({ model, aspectRatio, frame, endpoint }) => { - svcConfig.value = VideoGeneratorV3Block - const frameFile = { - id: 'f', - name: 'frame.png', - url: '', - size: 3, - type: 'image/png', - key: 'k', - } - const values = { - provider: 'falai', - apiKey: 'test-key', - model, - prompt: 'A cinematic sunrise', - falAspectRatio: aspectRatio, - ...(frame ? { visualReferenceInput: frameFile } : {}), - } - const params = extractBlockParams( - block({ - type: 'svc', - subBlocks: Object.fromEntries( - Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }]) - ), - }) - ) - const mapped = VideoGeneratorV3Block.tools.config?.params?.(params) const request = buildFalVideoRequest({ - ...videoToolBodySchema.parse(mapped), + ...serializeVideoBlock({ + ...FAL_VALUES, + model, + falAspectRatio: aspectRatio, + ...(frame ? { visualReferenceInput: FRAME_FILE } : {}), + }), provider: 'falai', }) expect(request.endpoint).toBe(endpoint) @@ -262,28 +251,44 @@ describe('extractBlockParams', () => { } ) + it.each([ + { + name: 'a frame left in the inactive upload', + mode: 'advanced', + frameField: 'visualReferenceUpload', + }, + { + name: 'a frame left in the inactive reference input', + mode: 'basic', + frameField: 'visualReferenceInput', + }, + ])('keeps a prompt-only fal setting with $name', ({ mode, frameField }) => { + const request = buildFalVideoRequest({ + ...serializeVideoBlock( + { + ...FAL_VALUES, + model: 'alibaba/happy-horse/text-to-video', + falAspectRatio: '16:9', + [frameField]: FRAME_FILE, + }, + { visualReference: mode } + ), + provider: 'falai', + }) + expect(request.endpoint).toBe('alibaba/happy-horse/text-to-video') + expect(request.body).toMatchObject({ aspect_ratio: '16:9' }) + }) + it('sends the model default for a Runway setting the start frame endpoint rejects', () => { - svcConfig.value = VideoGeneratorV3Block - const frameFile = { id: 'f', name: 'frame.png', url: '', size: 3, type: 'image/png', key: 'k' } - const values = { - provider: 'runway', - apiKey: 'test-key', - model: 'kling_4', - prompt: 'A cinematic sunrise', - aspectRatio: '1920:1080', - visualReferenceInput: frameFile, - } - const params = extractBlockParams( - block({ - type: 'svc', - subBlocks: Object.fromEntries( - Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }]) - ), - }) - ) - const mapped = VideoGeneratorV3Block.tools.config?.params?.(params) const request = buildDirectVideoRequest({ - ...videoToolBodySchema.parse(mapped), + ...serializeVideoBlock({ + provider: 'runway', + apiKey: 'test-key', + model: 'kling_4', + prompt: 'A cinematic sunrise', + aspectRatio: '1920:1080', + visualReferenceInput: FRAME_FILE, + }), provider: 'runway', }) expect(request.body).not.toHaveProperty('ratio') diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index d7e44f100f3..05f9dbc8b8d 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -20,6 +20,7 @@ import { isSubBlockHidden, isToolInputOnlySubBlock, resolveCanonicalMode, + withCanonicalModes, } from '@/lib/workflows/subblocks/visibility' import { getBlock } from '@/blocks' import { isCustomBlockType, RESERVED_PARAMS } from '@/blocks/custom/build-config' @@ -551,7 +552,7 @@ export function extractBlockParams(block: BlockState): Record { // canonical-index-unscoped: `shouldSerializeSubBlock` drops the inactive surface's members, and // the collapse below reads `params` (the filtered map), never `allValues`. const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks) - const allValues = buildSubBlockValues(block.subBlocks) + const allValues = withCanonicalModes(buildSubBlockValues(block.subBlocks), canonicalModeOverrides) Object.entries(block.subBlocks).forEach(([id, subBlock]) => { const matchingConfigs = blockConfig.subBlocks.filter((config) => config.id === id) @@ -707,7 +708,7 @@ export function collectBlockFieldIssues( // trigger-mode block returns above before reaching here at all. const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks || []) const canonicalModeOverrides = block.data?.canonicalModes - const allValues = buildSubBlockValues(block.subBlocks) + const allValues = withCanonicalModes(buildSubBlockValues(block.subBlocks), canonicalModeOverrides) // Get the tool configuration to check parameter visibility const toolAccess = blockConfig.tools?.access