mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
committed by
GitHub
parent
4fd5f0051f
commit
fb4c9827f8
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user