Skip to content

Commit fb4c982

Browse files
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"
1 parent 4fd5f00 commit fb4c982

File tree

8 files changed

+136
-33
lines changed

8 files changed

+136
-33
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { Badge } from '@/components/emcn'
33
import { Combobox, type ComboboxOption } from '@/components/emcn/components'
44
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
5+
import type { SubBlockConfig } from '@/blocks/types'
6+
import { getDependsOnFields } from '@/blocks/utils'
57
import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler'
68
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
79
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -43,7 +45,7 @@ interface DropdownProps {
4345
subBlockId: string
4446
) => Promise<Array<{ label: string; id: string }>>
4547
/** Field dependencies that trigger option refetch when changed */
46-
dependsOn?: string[]
48+
dependsOn?: SubBlockConfig['dependsOn']
4749
}
4850

4951
/**
@@ -67,23 +69,25 @@ export function Dropdown({
6769
placeholder = 'Select an option...',
6870
multiSelect = false,
6971
fetchOptions,
70-
dependsOn = [],
72+
dependsOn,
7173
}: DropdownProps) {
7274
const [storeValue, setStoreValue] = useSubBlockValue<string | string[]>(blockId, subBlockId) as [
7375
string | string[] | null | undefined,
7476
(value: string | string[]) => void,
7577
]
7678

79+
const dependsOnFields = useMemo(() => getDependsOnFields(dependsOn), [dependsOn])
80+
7781
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
7882
const dependencyValues = useSubBlockStore(
7983
useCallback(
8084
(state) => {
81-
if (dependsOn.length === 0 || !activeWorkflowId) return []
85+
if (dependsOnFields.length === 0 || !activeWorkflowId) return []
8286
const workflowValues = state.workflowValues[activeWorkflowId] || {}
8387
const blockValues = workflowValues[blockId] || {}
84-
return dependsOn.map((depKey) => blockValues[depKey] ?? null)
88+
return dependsOnFields.map((depKey) => blockValues[depKey] ?? null)
8589
},
86-
[dependsOn, activeWorkflowId, blockId]
90+
[dependsOnFields, activeWorkflowId, blockId]
8791
)
8892
)
8993

@@ -301,7 +305,7 @@ export function Dropdown({
301305
* This ensures options are refetched with new dependency values (e.g., new credentials)
302306
*/
303307
useEffect(() => {
304-
if (fetchOptions && dependsOn.length > 0) {
308+
if (fetchOptions && dependsOnFields.length > 0) {
305309
const currentDependencyValuesStr = JSON.stringify(dependencyValues)
306310
const previousDependencyValuesStr = previousDependencyValuesRef.current
307311

@@ -314,7 +318,7 @@ export function Dropdown({
314318

315319
previousDependencyValuesRef.current = currentDependencyValuesStr
316320
}
317-
}, [dependencyValues, fetchOptions, dependsOn.length])
321+
}, [dependencyValues, fetchOptions, dependsOnFields.length])
318322

319323
/**
320324
* Effect to fetch options when needed (on mount, when enabled, or when dependencies change)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
99
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
1010
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
1111
import type { SubBlockConfig } from '@/blocks/types'
12+
import { isDependency } from '@/blocks/utils'
1213
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
1314
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
1415
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -92,7 +93,7 @@ export function FileSelectorInput({
9293
!selectorResolution.context.domain
9394
const missingProject =
9495
selectorResolution?.key === 'jira.issues' &&
95-
subBlock.dependsOn?.includes('projectId') &&
96+
isDependency(subBlock.dependsOn, 'projectId') &&
9697
!selectorResolution.context.projectId
9798
const missingPlan =
9899
selectorResolution?.key === 'microsoft.planner' && !selectorResolution.context.planId

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,40 @@ import type { SubBlockConfig } from '@/blocks/types'
55
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
66
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
77

8+
type DependsOnConfig = string[] | { all?: string[]; any?: string[] }
9+
10+
/**
11+
* Parses dependsOn config and returns normalized all/any arrays
12+
*/
13+
function parseDependsOn(dependsOn: DependsOnConfig | undefined): {
14+
allFields: string[]
15+
anyFields: string[]
16+
allDependsOnFields: string[]
17+
} {
18+
if (!dependsOn) {
19+
return { allFields: [], anyFields: [], allDependsOnFields: [] }
20+
}
21+
22+
if (Array.isArray(dependsOn)) {
23+
// Simple array format: all fields required (AND logic)
24+
return { allFields: dependsOn, anyFields: [], allDependsOnFields: dependsOn }
25+
}
26+
27+
// Object format with all/any
28+
const allFields = dependsOn.all || []
29+
const anyFields = dependsOn.any || []
30+
return {
31+
allFields,
32+
anyFields,
33+
allDependsOnFields: [...allFields, ...anyFields],
34+
}
35+
}
36+
837
/**
938
* Centralized dependsOn gating for sub-block components.
1039
* - Computes dependency values from the active workflow/block
1140
* - Returns a stable disabled flag to pass to inputs and to guard effects
41+
* - Supports both AND (all) and OR (any) dependency logic
1242
*/
1343
export function useDependsOnGate(
1444
blockId: string,
@@ -21,8 +51,14 @@ export function useDependsOnGate(
2151

2252
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
2353

24-
// Use only explicit dependsOn from block config. No inference.
25-
const dependsOn: string[] = (subBlock.dependsOn as string[] | undefined) || []
54+
// Parse dependsOn config to get all/any field lists
55+
const { allFields, anyFields, allDependsOnFields } = useMemo(
56+
() => parseDependsOn(subBlock.dependsOn),
57+
[subBlock.dependsOn]
58+
)
59+
60+
// For backward compatibility, expose flat list of all dependency fields
61+
const dependsOn = allDependsOnFields
2662

2763
const normalizeDependencyValue = (rawValue: unknown): unknown => {
2864
if (rawValue === null || rawValue === undefined) return null
@@ -47,33 +83,64 @@ export function useDependsOnGate(
4783
return rawValue
4884
}
4985

50-
const dependencyValues = useSubBlockStore((state) => {
51-
if (dependsOn.length === 0) return [] as any[]
86+
// Get values for all dependency fields (both all and any)
87+
const dependencyValuesMap = useSubBlockStore((state) => {
88+
if (allDependsOnFields.length === 0) return {} as Record<string, unknown>
5289

5390
// If previewContextValues are provided (e.g., tool parameters), use those first
5491
if (previewContextValues) {
55-
return dependsOn.map((depKey) => normalizeDependencyValue(previewContextValues[depKey]))
92+
const map: Record<string, unknown> = {}
93+
for (const key of allDependsOnFields) {
94+
map[key] = normalizeDependencyValue(previewContextValues[key])
95+
}
96+
return map
97+
}
98+
99+
if (!activeWorkflowId) {
100+
const map: Record<string, unknown> = {}
101+
for (const key of allDependsOnFields) {
102+
map[key] = null
103+
}
104+
return map
56105
}
57106

58-
if (!activeWorkflowId) return dependsOn.map(() => null)
59107
const workflowValues = state.workflowValues[activeWorkflowId] || {}
60108
const blockValues = (workflowValues as any)[blockId] || {}
61-
return dependsOn.map((depKey) => normalizeDependencyValue((blockValues as any)[depKey]))
62-
}) as any[]
109+
const map: Record<string, unknown> = {}
110+
for (const key of allDependsOnFields) {
111+
map[key] = normalizeDependencyValue((blockValues as any)[key])
112+
}
113+
return map
114+
})
115+
116+
// For backward compatibility, also provide array of values
117+
const dependencyValues = useMemo(
118+
() => allDependsOnFields.map((key) => dependencyValuesMap[key]),
119+
[allDependsOnFields, dependencyValuesMap]
120+
) as any[]
121+
122+
const isValueSatisfied = (value: unknown): boolean => {
123+
if (value === null || value === undefined) return false
124+
if (typeof value === 'string') return value.trim().length > 0
125+
if (Array.isArray(value)) return value.length > 0
126+
return value !== ''
127+
}
63128

64129
const depsSatisfied = useMemo(() => {
65-
if (dependsOn.length === 0) return true
66-
return dependencyValues.every((value) => {
67-
if (value === null || value === undefined) return false
68-
if (typeof value === 'string') return value.trim().length > 0
69-
if (Array.isArray(value)) return value.length > 0
70-
return value !== ''
71-
})
72-
}, [dependencyValues, dependsOn])
130+
// Check all fields (AND logic) - all must be satisfied
131+
const allSatisfied =
132+
allFields.length === 0 || allFields.every((key) => isValueSatisfied(dependencyValuesMap[key]))
133+
134+
// Check any fields (OR logic) - at least one must be satisfied
135+
const anySatisfied =
136+
anyFields.length === 0 || anyFields.some((key) => isValueSatisfied(dependencyValuesMap[key]))
137+
138+
return allSatisfied && anySatisfied
139+
}, [allFields, anyFields, dependencyValuesMap])
73140

74141
// Block everything except the credential field itself until dependencies are set
75142
const blocked =
76-
!isPreview && dependsOn.length > 0 && !depsSatisfied && subBlock.type !== 'oauth-input'
143+
!isPreview && allDependsOnFields.length > 0 && !depsSatisfied && subBlock.type !== 'oauth-input'
77144

78145
const finalDisabled = disabledProp || isPreview || blocked
79146

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/sub-block/components/file-selector/file-selector-input.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
99
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
1010
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
1111
import type { SubBlockConfig } from '@/blocks/types'
12+
import { isDependency } from '@/blocks/utils'
1213
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
1314
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
1415
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -92,7 +93,7 @@ export function FileSelectorInput({
9293
!selectorResolution.context.domain
9394
const missingProject =
9495
selectorResolution?.key === 'jira.issues' &&
95-
subBlock.dependsOn?.includes('projectId') &&
96+
isDependency(subBlock.dependsOn, 'projectId') &&
9697
!selectorResolution?.context.projectId
9798
const missingPlan =
9899
selectorResolution?.key === 'microsoft.planner' && !selectorResolution?.context.planId

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
useBlockDimensions,
3131
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
3232
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
33+
import { getDependsOnFields } from '@/blocks/utils'
3334
import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp'
3435
import { useCredentialName } from '@/hooks/queries/oauth-credentials'
3536
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
@@ -261,8 +262,9 @@ const SubBlockRow = ({
261262
)
262263

263264
const dependencyValues = useMemo(() => {
264-
if (!subBlock?.dependsOn?.length) return {}
265-
return subBlock.dependsOn.reduce<Record<string, string>>((accumulator, dependency) => {
265+
const fields = getDependsOnFields(subBlock?.dependsOn)
266+
if (!fields.length) return {}
267+
return fields.reduce<Record<string, string>>((accumulator, dependency) => {
266268
const dependencyValue = getStringValue(dependency)
267269
if (dependencyValue) {
268270
accumulator[dependency] = dependencyValue

apps/sim/blocks/blocks/slack.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
6767
'reactions:write',
6868
],
6969
placeholder: 'Select Slack workspace',
70+
dependsOn: ['authMethod'],
7071
condition: {
7172
field: 'authMethod',
7273
value: 'oauth',
@@ -78,6 +79,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
7879
type: 'short-input',
7980
placeholder: 'Enter your Slack bot token (xoxb-...)',
8081
password: true,
82+
dependsOn: ['authMethod'],
8183
condition: {
8284
field: 'authMethod',
8385
value: 'bot_token',
@@ -91,7 +93,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
9193
serviceId: 'slack',
9294
placeholder: 'Select Slack channel',
9395
mode: 'basic',
94-
dependsOn: ['credential', 'authMethod'],
96+
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] },
9597
condition: {
9698
field: 'operation',
9799
value: ['list_channels', 'list_users', 'get_user'],

apps/sim/blocks/types.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,15 @@ export interface SubBlockConfig {
246246
placeholder?: string // Custom placeholder for the prompt input
247247
maintainHistory?: boolean // Whether to maintain conversation history
248248
}
249-
// Declarative dependency hints for cross-field clearing or invalidation
250-
// Example: dependsOn: ['credential'] means this field should be cleared when credential changes
251-
dependsOn?: string[]
249+
/**
250+
* Declarative dependency hints for cross-field clearing or invalidation.
251+
* Supports two formats:
252+
* - Simple array: `['credential']` - all fields must have values (AND logic)
253+
* - Object with all/any: `{ all: ['authMethod'], any: ['credential', 'botToken'] }`
254+
* - `all`: all listed fields must have values (AND logic)
255+
* - `any`: at least one field must have a value (OR logic)
256+
*/
257+
dependsOn?: string[] | { all?: string[]; any?: string[] }
252258
// Copyable-text specific: Use webhook URL from webhook management hook
253259
useWebhookUrl?: boolean
254260
// Trigger-save specific: The trigger ID for validation and saving

apps/sim/blocks/utils.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,24 @@
1-
import type { BlockOutput, OutputFieldDefinition } from '@/blocks/types'
1+
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
2+
3+
/**
4+
* Checks if a field is included in the dependsOn config.
5+
* Handles both simple array format and object format with all/any fields.
6+
*/
7+
export function isDependency(dependsOn: SubBlockConfig['dependsOn'], field: string): boolean {
8+
if (!dependsOn) return false
9+
if (Array.isArray(dependsOn)) return dependsOn.includes(field)
10+
return dependsOn.all?.includes(field) || dependsOn.any?.includes(field) || false
11+
}
12+
13+
/**
14+
* Gets all dependency fields as a flat array.
15+
* Handles both simple array format and object format with all/any fields.
16+
*/
17+
export function getDependsOnFields(dependsOn: SubBlockConfig['dependsOn']): string[] {
18+
if (!dependsOn) return []
19+
if (Array.isArray(dependsOn)) return dependsOn
20+
return [...(dependsOn.all || []), ...(dependsOn.any || [])]
21+
}
222

323
export function resolveOutputType(
424
outputs: Record<string, OutputFieldDefinition>

0 commit comments

Comments
 (0)