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 5b4c25b716..6da2c16031 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 @@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Badge } from '@/components/emcn' import { Combobox, type ComboboxOption } from '@/components/emcn/components' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import type { SubBlockConfig } from '@/blocks/types' +import { getDependsOnFields } from '@/blocks/utils' import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -43,7 +45,7 @@ interface DropdownProps { subBlockId: string ) => Promise> /** Field dependencies that trigger option refetch when changed */ - dependsOn?: string[] + dependsOn?: SubBlockConfig['dependsOn'] } /** @@ -67,23 +69,25 @@ export function Dropdown({ placeholder = 'Select an option...', multiSelect = false, fetchOptions, - dependsOn = [], + dependsOn, }: DropdownProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) as [ string | string[] | null | undefined, (value: string | string[]) => void, ] + const dependsOnFields = useMemo(() => getDependsOnFields(dependsOn), [dependsOn]) + const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) const dependencyValues = useSubBlockStore( useCallback( (state) => { - if (dependsOn.length === 0 || !activeWorkflowId) return [] + if (dependsOnFields.length === 0 || !activeWorkflowId) return [] const workflowValues = state.workflowValues[activeWorkflowId] || {} const blockValues = workflowValues[blockId] || {} - return dependsOn.map((depKey) => blockValues[depKey] ?? null) + return dependsOnFields.map((depKey) => blockValues[depKey] ?? null) }, - [dependsOn, activeWorkflowId, blockId] + [dependsOnFields, activeWorkflowId, blockId] ) ) @@ -301,7 +305,7 @@ export function Dropdown({ * This ensures options are refetched with new dependency values (e.g., new credentials) */ useEffect(() => { - if (fetchOptions && dependsOn.length > 0) { + if (fetchOptions && dependsOnFields.length > 0) { const currentDependencyValuesStr = JSON.stringify(dependencyValues) const previousDependencyValuesStr = previousDependencyValuesRef.current @@ -314,7 +318,7 @@ export function Dropdown({ previousDependencyValuesRef.current = currentDependencyValuesStr } - }, [dependencyValues, fetchOptions, dependsOn.length]) + }, [dependencyValues, fetchOptions, dependsOnFields.length]) /** * Effect to fetch options when needed (on mount, when enabled, or when dependencies change) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx index 23568d31e9..8e762c795c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx @@ -9,6 +9,7 @@ import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/c import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' +import { isDependency } from '@/blocks/utils' import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -92,7 +93,7 @@ export function FileSelectorInput({ !selectorResolution.context.domain const missingProject = selectorResolution?.key === 'jira.issues' && - subBlock.dependsOn?.includes('projectId') && + isDependency(subBlock.dependsOn, 'projectId') && !selectorResolution.context.projectId const missingPlan = selectorResolution?.key === 'microsoft.planner' && !selectorResolution.context.planId diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts index 19b188d49d..3c145f52fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts @@ -5,10 +5,40 @@ import type { SubBlockConfig } from '@/blocks/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' +type DependsOnConfig = string[] | { all?: string[]; any?: string[] } + +/** + * Parses dependsOn config and returns normalized all/any arrays + */ +function parseDependsOn(dependsOn: DependsOnConfig | undefined): { + allFields: string[] + anyFields: string[] + allDependsOnFields: string[] +} { + if (!dependsOn) { + return { allFields: [], anyFields: [], allDependsOnFields: [] } + } + + if (Array.isArray(dependsOn)) { + // Simple array format: all fields required (AND logic) + return { allFields: dependsOn, anyFields: [], allDependsOnFields: dependsOn } + } + + // Object format with all/any + const allFields = dependsOn.all || [] + const anyFields = dependsOn.any || [] + return { + allFields, + anyFields, + allDependsOnFields: [...allFields, ...anyFields], + } +} + /** * Centralized dependsOn gating for sub-block components. * - Computes dependency values from the active workflow/block * - Returns a stable disabled flag to pass to inputs and to guard effects + * - Supports both AND (all) and OR (any) dependency logic */ export function useDependsOnGate( blockId: string, @@ -21,8 +51,14 @@ export function useDependsOnGate( const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) - // Use only explicit dependsOn from block config. No inference. - const dependsOn: string[] = (subBlock.dependsOn as string[] | undefined) || [] + // Parse dependsOn config to get all/any field lists + const { allFields, anyFields, allDependsOnFields } = useMemo( + () => parseDependsOn(subBlock.dependsOn), + [subBlock.dependsOn] + ) + + // For backward compatibility, expose flat list of all dependency fields + const dependsOn = allDependsOnFields const normalizeDependencyValue = (rawValue: unknown): unknown => { if (rawValue === null || rawValue === undefined) return null @@ -47,33 +83,64 @@ export function useDependsOnGate( return rawValue } - const dependencyValues = useSubBlockStore((state) => { - if (dependsOn.length === 0) return [] as any[] + // Get values for all dependency fields (both all and any) + const dependencyValuesMap = useSubBlockStore((state) => { + if (allDependsOnFields.length === 0) return {} as Record // If previewContextValues are provided (e.g., tool parameters), use those first if (previewContextValues) { - return dependsOn.map((depKey) => normalizeDependencyValue(previewContextValues[depKey])) + const map: Record = {} + for (const key of allDependsOnFields) { + map[key] = normalizeDependencyValue(previewContextValues[key]) + } + return map + } + + if (!activeWorkflowId) { + const map: Record = {} + for (const key of allDependsOnFields) { + map[key] = null + } + return map } - if (!activeWorkflowId) return dependsOn.map(() => null) const workflowValues = state.workflowValues[activeWorkflowId] || {} const blockValues = (workflowValues as any)[blockId] || {} - return dependsOn.map((depKey) => normalizeDependencyValue((blockValues as any)[depKey])) - }) as any[] + const map: Record = {} + for (const key of allDependsOnFields) { + map[key] = normalizeDependencyValue((blockValues as any)[key]) + } + return map + }) + + // For backward compatibility, also provide array of values + const dependencyValues = useMemo( + () => allDependsOnFields.map((key) => dependencyValuesMap[key]), + [allDependsOnFields, dependencyValuesMap] + ) as any[] + + const isValueSatisfied = (value: unknown): boolean => { + if (value === null || value === undefined) return false + if (typeof value === 'string') return value.trim().length > 0 + if (Array.isArray(value)) return value.length > 0 + return value !== '' + } const depsSatisfied = useMemo(() => { - if (dependsOn.length === 0) return true - return dependencyValues.every((value) => { - if (value === null || value === undefined) return false - if (typeof value === 'string') return value.trim().length > 0 - if (Array.isArray(value)) return value.length > 0 - return value !== '' - }) - }, [dependencyValues, dependsOn]) + // Check all fields (AND logic) - all must be satisfied + const allSatisfied = + allFields.length === 0 || allFields.every((key) => isValueSatisfied(dependencyValuesMap[key])) + + // Check any fields (OR logic) - at least one must be satisfied + const anySatisfied = + anyFields.length === 0 || anyFields.some((key) => isValueSatisfied(dependencyValuesMap[key])) + + return allSatisfied && anySatisfied + }, [allFields, anyFields, dependencyValuesMap]) // Block everything except the credential field itself until dependencies are set const blocked = - !isPreview && dependsOn.length > 0 && !depsSatisfied && subBlock.type !== 'oauth-input' + !isPreview && allDependsOnFields.length > 0 && !depsSatisfied && subBlock.type !== 'oauth-input' const finalDisabled = disabledProp || isPreview || blocked diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx index 14a76f0070..4aab30a971 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx @@ -9,6 +9,7 @@ import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/c import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' +import { isDependency } from '@/blocks/utils' import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -92,7 +93,7 @@ export function FileSelectorInput({ !selectorResolution.context.domain const missingProject = selectorResolution?.key === 'jira.issues' && - subBlock.dependsOn?.includes('projectId') && + isDependency(subBlock.dependsOn, 'projectId') && !selectorResolution?.context.projectId const missingPlan = selectorResolution?.key === 'microsoft.planner' && !selectorResolution?.context.planId diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 79c652ed4b..bf54a375c8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -30,6 +30,7 @@ import { useBlockDimensions, } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' +import { getDependsOnFields } from '@/blocks/utils' import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp' import { useCredentialName } from '@/hooks/queries/oauth-credentials' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' @@ -261,8 +262,9 @@ const SubBlockRow = ({ ) const dependencyValues = useMemo(() => { - if (!subBlock?.dependsOn?.length) return {} - return subBlock.dependsOn.reduce>((accumulator, dependency) => { + const fields = getDependsOnFields(subBlock?.dependsOn) + if (!fields.length) return {} + return fields.reduce>((accumulator, dependency) => { const dependencyValue = getStringValue(dependency) if (dependencyValue) { accumulator[dependency] = dependencyValue diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 9e7cbc3f62..88434484e6 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -67,6 +67,7 @@ export const SlackBlock: BlockConfig = { 'reactions:write', ], placeholder: 'Select Slack workspace', + dependsOn: ['authMethod'], condition: { field: 'authMethod', value: 'oauth', @@ -78,6 +79,7 @@ export const SlackBlock: BlockConfig = { type: 'short-input', placeholder: 'Enter your Slack bot token (xoxb-...)', password: true, + dependsOn: ['authMethod'], condition: { field: 'authMethod', value: 'bot_token', @@ -91,7 +93,7 @@ export const SlackBlock: BlockConfig = { serviceId: 'slack', placeholder: 'Select Slack channel', mode: 'basic', - dependsOn: ['credential', 'authMethod'], + dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }, condition: { field: 'operation', value: ['list_channels', 'list_users', 'get_user'], diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index fc6db61d99..121c94a2dd 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -246,9 +246,15 @@ export interface SubBlockConfig { placeholder?: string // Custom placeholder for the prompt input maintainHistory?: boolean // Whether to maintain conversation history } - // Declarative dependency hints for cross-field clearing or invalidation - // Example: dependsOn: ['credential'] means this field should be cleared when credential changes - dependsOn?: string[] + /** + * Declarative dependency hints for cross-field clearing or invalidation. + * Supports two formats: + * - Simple array: `['credential']` - all fields must have values (AND logic) + * - Object with all/any: `{ all: ['authMethod'], any: ['credential', 'botToken'] }` + * - `all`: all listed fields must have values (AND logic) + * - `any`: at least one field must have a value (OR logic) + */ + dependsOn?: string[] | { all?: string[]; any?: string[] } // Copyable-text specific: Use webhook URL from webhook management hook useWebhookUrl?: boolean // Trigger-save specific: The trigger ID for validation and saving diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 5d4fef147a..8a96ca2ae0 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -1,4 +1,24 @@ -import type { BlockOutput, OutputFieldDefinition } from '@/blocks/types' +import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' + +/** + * Checks if a field is included in the dependsOn config. + * Handles both simple array format and object format with all/any fields. + */ +export function isDependency(dependsOn: SubBlockConfig['dependsOn'], field: string): boolean { + if (!dependsOn) return false + if (Array.isArray(dependsOn)) return dependsOn.includes(field) + return dependsOn.all?.includes(field) || dependsOn.any?.includes(field) || false +} + +/** + * Gets all dependency fields as a flat array. + * Handles both simple array format and object format with all/any fields. + */ +export function getDependsOnFields(dependsOn: SubBlockConfig['dependsOn']): string[] { + if (!dependsOn) return [] + if (Array.isArray(dependsOn)) return dependsOn + return [...(dependsOn.all || []), ...(dependsOn.any || [])] +} export function resolveOutputType( outputs: Record