fix(custom-bot-slack): dependsOn incorrectly set for bot_token (#2214)

* fix(custom-bot-slack): dependsOn incorrectly set for bot_token"

* fix other references to be compatible

* fix dependsOn for things depending on authMethod"
This commit is contained in:
Vikhyath Mondreti
2025-12-05 13:54:52 -08:00
committed by GitHub
parent 4fd5f0051f
commit fb4c9827f8
8 changed files with 136 additions and 33 deletions

View File

@@ -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<Array<{ label: string; id: string }>>
/** 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<string | string[]>(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)

View File

@@ -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

View File

@@ -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<string, unknown>
// If previewContextValues are provided (e.g., tool parameters), use those first
if (previewContextValues) {
return dependsOn.map((depKey) => normalizeDependencyValue(previewContextValues[depKey]))
const map: Record<string, unknown> = {}
for (const key of allDependsOnFields) {
map[key] = normalizeDependencyValue(previewContextValues[key])
}
return map
}
if (!activeWorkflowId) {
const map: Record<string, unknown> = {}
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<string, unknown> = {}
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

View File

@@ -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

View File

@@ -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<Record<string, string>>((accumulator, dependency) => {
const fields = getDependsOnFields(subBlock?.dependsOn)
if (!fields.length) return {}
return fields.reduce<Record<string, string>>((accumulator, dependency) => {
const dependencyValue = getStringValue(dependency)
if (dependencyValue) {
accumulator[dependency] = dependencyValue

View File

@@ -67,6 +67,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
'reactions:write',
],
placeholder: 'Select Slack workspace',
dependsOn: ['authMethod'],
condition: {
field: 'authMethod',
value: 'oauth',
@@ -78,6 +79,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
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<SlackResponse> = {
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'],

View File

@@ -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

View File

@@ -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<string, OutputFieldDefinition>