feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: Stream <Stream_2@qq.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com> Co-authored-by: zhsama <torvalds@linux.do> Co-authored-by: Harry <xh001x@hotmail.com> Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: yessenia <yessenia.contact@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: WTW0313 <twwu@dify.ai> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -22,3 +22,5 @@ export * from './use-DSL'
|
||||
export * from './use-inspect-vars-crud'
|
||||
export * from './use-set-workflow-vars-with-value'
|
||||
export * from './use-workflow-search'
|
||||
export * from './use-auto-generate-webhook-url'
|
||||
export * from './use-serial-async-callback'
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { fetchWebhookUrl } from '@/service/apps'
|
||||
|
||||
export const useAutoGenerateWebhookUrl = () => {
|
||||
const reactFlowStore = useStoreApi()
|
||||
|
||||
return useCallback(async (nodeId: string) => {
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
if (!appId)
|
||||
return
|
||||
|
||||
const { getNodes } = reactFlowStore.getState()
|
||||
const node = getNodes().find(n => n.id === nodeId)
|
||||
if (!node || node.data.type !== BlockEnum.TriggerWebhook)
|
||||
return
|
||||
|
||||
if (node.data.webhook_url && node.data.webhook_url.length > 0)
|
||||
return
|
||||
|
||||
try {
|
||||
const response = await fetchWebhookUrl({ appId, nodeId })
|
||||
const { getNodes: getLatestNodes, setNodes } = reactFlowStore.getState()
|
||||
let hasUpdated = false
|
||||
const updatedNodes = produce(getLatestNodes(), (draft) => {
|
||||
const targetNode = draft.find(n => n.id === nodeId)
|
||||
if (!targetNode || targetNode.data.type !== BlockEnum.TriggerWebhook)
|
||||
return
|
||||
|
||||
targetNode.data = {
|
||||
...targetNode.data,
|
||||
webhook_url: response.webhook_url,
|
||||
webhook_debug_url: response.webhook_debug_url,
|
||||
}
|
||||
hasUpdated = true
|
||||
})
|
||||
|
||||
if (hasUpdated)
|
||||
setNodes(updatedNodes)
|
||||
}
|
||||
catch (error: unknown) {
|
||||
console.error('Failed to auto-generate webhook URL:', error)
|
||||
}
|
||||
}, [reactFlowStore])
|
||||
}
|
||||
@@ -21,7 +21,9 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
|
||||
} = useNodesMetaData()
|
||||
const availableNodesType = useMemo(() => availableNodes.map(node => node.metaData.type), [availableNodes])
|
||||
const availablePrevBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource)
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource
|
||||
|| nodeType === BlockEnum.TriggerPlugin || nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerSchedule)
|
||||
return []
|
||||
|
||||
return availableNodesType
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useEdges, useNodes, useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
CommonEdgeType,
|
||||
CommonNodeType,
|
||||
Edge,
|
||||
Node,
|
||||
@@ -21,20 +22,22 @@ import {
|
||||
getToolCheckParams,
|
||||
getValidTreeNodes,
|
||||
} from '../utils'
|
||||
import { getTriggerCheckParams } from '../utils/trigger'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
} from '../constants'
|
||||
import {
|
||||
useGetToolIcon,
|
||||
useWorkflow,
|
||||
useNodesMetaData,
|
||||
} from '../hooks'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import type { AgentNodeType } from '../nodes/agent/types'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { useDatasetsDetailStore } from '../datasets-detail-store/store'
|
||||
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
@@ -42,6 +45,7 @@ import { fetchDatasets } from '@/service/datasets'
|
||||
import { MAX_TREE_DEPTH } from '@/config'
|
||||
import useNodesAvailableVarList, { useGetNodesAvailableVarList } from './use-nodes-available-var-list'
|
||||
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { KnowledgeBaseNodeType } from '../nodes/knowledge-base/types'
|
||||
@@ -50,6 +54,25 @@ import {
|
||||
useAllCustomTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type ChecklistItem = {
|
||||
id: string
|
||||
type: BlockEnum | string
|
||||
title: string
|
||||
toolIcon?: string | Emoji
|
||||
unConnected?: boolean
|
||||
errorMessage?: string
|
||||
canNavigate: boolean
|
||||
}
|
||||
|
||||
const START_NODE_TYPES: BlockEnum[] = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
|
||||
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -60,9 +83,11 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const { data: strategyProviders } = useStrategyProviders()
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail)
|
||||
const { getStartNodes } = useWorkflow()
|
||||
const getToolIcon = useGetToolIcon()
|
||||
const appMode = useAppStore.getState().appDetail?.mode
|
||||
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const map = useNodesAvailableVarList(nodes)
|
||||
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
@@ -92,16 +117,10 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
return checkData
|
||||
}, [datasetsDetail, embeddingModelList, rerankModelList])
|
||||
|
||||
const needWarningNodes = useMemo(() => {
|
||||
const list = []
|
||||
const needWarningNodes = useMemo<ChecklistItem[]>(() => {
|
||||
const list: ChecklistItem[] = []
|
||||
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
|
||||
const startNodes = getStartNodes(filteredNodes)
|
||||
const validNodesFlattened = startNodes.map(startNode => getValidTreeNodes(startNode, filteredNodes, edges))
|
||||
const validNodes = validNodesFlattened.reduce((acc, curr) => {
|
||||
if (curr.validNodes)
|
||||
acc.push(...curr.validNodes)
|
||||
return acc
|
||||
}, [] as Node[])
|
||||
const { validNodes } = getValidTreeNodes(filteredNodes, edges)
|
||||
|
||||
for (let i = 0; i < filteredNodes.length; i++) {
|
||||
const node = filteredNodes[i]
|
||||
@@ -114,6 +133,9 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
if (node.data.type === BlockEnum.DataSource)
|
||||
moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language)
|
||||
|
||||
if (node.data.type === BlockEnum.TriggerPlugin)
|
||||
moreDataForCheckValid = getTriggerCheckParams(node.data as PluginTriggerNodeType, triggerPlugins, language)
|
||||
|
||||
const toolIcon = getToolIcon(node.data)
|
||||
if (node.data.type === BlockEnum.Agent) {
|
||||
const data = node.data as AgentNodeType
|
||||
@@ -133,7 +155,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
if (node.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node.data)
|
||||
let { errorMessage } = nodesExtraData![node.data.type].checkValid(checkData, t, moreDataForCheckValid)
|
||||
const validator = nodesExtraData?.[node.data.type as BlockEnum]?.checkValid
|
||||
let errorMessage = validator ? validator(checkData, t, moreDataForCheckValid).errorMessage : undefined
|
||||
|
||||
if (!errorMessage) {
|
||||
const availableVars = map[node.id].availableVars
|
||||
@@ -153,19 +176,43 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMessage || !validNodes.find(n => n.id === node.id)) {
|
||||
|
||||
// Start nodes and Trigger nodes should not show unConnected error if they have validation errors
|
||||
// or if they are valid start nodes (even without incoming connections)
|
||||
const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false
|
||||
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
|
||||
|
||||
const isUnconnected = !validNodes.find(n => n.id === node.id)
|
||||
const shouldShowError = errorMessage || (isUnconnected && !canSkipConnectionCheck)
|
||||
|
||||
if (shouldShowError) {
|
||||
list.push({
|
||||
id: node.id,
|
||||
type: node.data.type,
|
||||
title: node.data.title,
|
||||
toolIcon,
|
||||
unConnected: !validNodes.find(n => n.id === node.id),
|
||||
unConnected: isUnconnected && !canSkipConnectionCheck,
|
||||
errorMessage,
|
||||
canNavigate: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for start nodes (including triggers)
|
||||
if (shouldCheckStartNode) {
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
if (startNodesFiltered.length === 0) {
|
||||
list.push({
|
||||
id: 'start-node-required',
|
||||
type: BlockEnum.Start,
|
||||
title: t('workflow.panel.startNode'),
|
||||
errorMessage: t('workflow.common.needStartNode'),
|
||||
canNavigate: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
|
||||
isRequiredNodesType.forEach((type: string) => {
|
||||
@@ -175,12 +222,13 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
type,
|
||||
title: t(`workflow.blocks.${type}`),
|
||||
errorMessage: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }),
|
||||
canNavigate: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
}, [nodes, getStartNodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map])
|
||||
}, [nodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map, shouldCheckStartNode])
|
||||
|
||||
return needWarningNodes
|
||||
}
|
||||
@@ -194,7 +242,6 @@ export const useChecklistBeforePublish = () => {
|
||||
const { data: strategyProviders } = useStrategyProviders()
|
||||
const updateDatasetsDetail = useDatasetsDetailStore(s => s.updateDatasetsDetail)
|
||||
const updateTime = useRef(0)
|
||||
const { getStartNodes } = useWorkflow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { getNodesAvailableVarList } = useGetNodesAvailableVarList()
|
||||
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
@@ -241,20 +288,11 @@ export const useChecklistBeforePublish = () => {
|
||||
} = workflowStore.getState()
|
||||
const nodes = getNodes()
|
||||
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
|
||||
const startNodes = getStartNodes(filteredNodes)
|
||||
const validNodesFlattened = startNodes.map(startNode => getValidTreeNodes(startNode, filteredNodes, edges))
|
||||
const validNodes = validNodesFlattened.reduce((acc, curr) => {
|
||||
if (curr.validNodes)
|
||||
acc.push(...curr.validNodes)
|
||||
return acc
|
||||
}, [] as Node[])
|
||||
const maxDepthArr = validNodesFlattened.map(item => item.maxDepth)
|
||||
const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges)
|
||||
|
||||
for (let i = 0; i < maxDepthArr.length; i++) {
|
||||
if (maxDepthArr[i] > MAX_TREE_DEPTH) {
|
||||
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
|
||||
return false
|
||||
}
|
||||
if (maxDepth > MAX_TREE_DEPTH) {
|
||||
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
|
||||
return false
|
||||
}
|
||||
// Before publish, we need to fetch datasets detail, in case of the settings of datasets have been changed
|
||||
const knowledgeRetrievalNodes = filteredNodes.filter(node => node.data.type === BlockEnum.KnowledgeRetrieval)
|
||||
@@ -334,10 +372,18 @@ export const useChecklistBeforePublish = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||
return false
|
||||
}
|
||||
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
|
||||
for (let i = 0; i < isRequiredNodesType.length; i++) {
|
||||
const type = isRequiredNodesType[i]
|
||||
|
||||
if (!filteredNodes.find(node => node.data.type === type)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }) })
|
||||
return false
|
||||
@@ -345,9 +391,31 @@ export const useChecklistBeforePublish = () => {
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, getStartNodes, workflowStore, buildInTools, customTools, workflowTools])
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools])
|
||||
|
||||
return {
|
||||
handleCheckBeforePublish,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowRunValidation = () => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const edges = useEdges<CommonEdgeType>()
|
||||
const needWarningNodes = useChecklist(nodes, edges)
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const validateBeforeRun = useCallback(() => {
|
||||
if (needWarningNodes.length > 0) {
|
||||
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, [needWarningNodes, notify, t])
|
||||
|
||||
return {
|
||||
validateBeforeRun,
|
||||
hasValidationErrors: needWarningNodes.length > 0,
|
||||
warningNodes: needWarningNodes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum, type CommonNodeType } from '../types'
|
||||
import { getWorkflowEntryNode } from '../utils/workflow-entry'
|
||||
import { type TestRunOptions, type TriggerOption, TriggerType } from '../header/test-run-menu'
|
||||
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { useStore } from '../store'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
|
||||
export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
|
||||
return useMemo(() => {
|
||||
const allTriggers: TriggerOption[] = []
|
||||
let userInput: TriggerOption | undefined
|
||||
|
||||
for (const node of nodes) {
|
||||
const nodeData = node.data as CommonNodeType
|
||||
|
||||
if (!nodeData?.type) continue
|
||||
|
||||
if (nodeData.type === BlockEnum.Start) {
|
||||
userInput = {
|
||||
id: node.id,
|
||||
type: TriggerType.UserInput,
|
||||
name: nodeData.title || t('workflow.blocks.start'),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.Start}
|
||||
size='md'
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerSchedule) {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Schedule,
|
||||
name: nodeData.title || t('workflow.blocks.trigger-schedule'),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerSchedule}
|
||||
size='md'
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerWebhook) {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Webhook,
|
||||
name: nodeData.title || t('workflow.blocks.trigger-webhook'),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerWebhook}
|
||||
size='md'
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerPlugin) {
|
||||
let triggerIcon: string | any
|
||||
|
||||
if (nodeData.provider_id) {
|
||||
const targetTriggers = triggerPlugins || []
|
||||
triggerIcon = targetTriggers.find(toolWithProvider => toolWithProvider.name === nodeData.provider_id)?.icon
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
size='md'
|
||||
toolIcon={triggerIcon}
|
||||
/>
|
||||
)
|
||||
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Plugin,
|
||||
name: nodeData.title || (nodeData as any).plugin_name || t('workflow.blocks.trigger-plugin'),
|
||||
icon,
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!userInput) {
|
||||
const startNode = getWorkflowEntryNode(nodes as any[])
|
||||
if (startNode && startNode.data?.type === BlockEnum.Start) {
|
||||
userInput = {
|
||||
id: startNode.id,
|
||||
type: TriggerType.UserInput,
|
||||
name: (startNode.data as CommonNodeType)?.title || t('workflow.blocks.start'),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.Start}
|
||||
size='md'
|
||||
/>
|
||||
),
|
||||
nodeId: startNode.id,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const triggerNodeIds = allTriggers
|
||||
.map(trigger => trigger.nodeId)
|
||||
.filter((nodeId): nodeId is string => Boolean(nodeId))
|
||||
|
||||
const runAll: TriggerOption | undefined = triggerNodeIds.length > 1 ? {
|
||||
id: 'run-all',
|
||||
type: TriggerType.All,
|
||||
name: t('workflow.common.runAllTriggers'),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-purple-purple-500 text-white shadow-md">
|
||||
<TriggerAll className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
),
|
||||
relatedNodeIds: triggerNodeIds,
|
||||
enabled: true,
|
||||
} : undefined
|
||||
|
||||
return {
|
||||
userInput,
|
||||
triggers: allTriggers,
|
||||
runAll,
|
||||
}
|
||||
}, [nodes, buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, t])
|
||||
}
|
||||
@@ -1,12 +1,40 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { Node } from '../types'
|
||||
import { BlockEnum, isTriggerNode } from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
|
||||
// Entry node (Start/Trigger) wrapper offsets
|
||||
// The EntryNodeContainer adds a wrapper with status indicator above the actual node
|
||||
// These offsets ensure alignment happens on the inner node, not the wrapper
|
||||
const ENTRY_NODE_WRAPPER_OFFSET = {
|
||||
x: 0, // No horizontal padding on wrapper (px-0)
|
||||
y: 21, // Actual measured: pt-0.5 (2px) + status bar height (~19px)
|
||||
} as const
|
||||
|
||||
export const useHelpline = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// Check if a node is an entry node (Start or Trigger)
|
||||
const isEntryNode = useCallback((node: Node): boolean => {
|
||||
return isTriggerNode(node.data.type as any) || node.data.type === BlockEnum.Start
|
||||
}, [])
|
||||
|
||||
// Get the actual alignment position of a node (accounting for wrapper offset)
|
||||
const getNodeAlignPosition = useCallback((node: Node) => {
|
||||
if (isEntryNode(node)) {
|
||||
return {
|
||||
x: node.position.x + ENTRY_NODE_WRAPPER_OFFSET.x,
|
||||
y: node.position.y + ENTRY_NODE_WRAPPER_OFFSET.y,
|
||||
}
|
||||
}
|
||||
return {
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
}
|
||||
}, [isEntryNode])
|
||||
|
||||
const handleSetHelpline = useCallback((node: Node) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
@@ -29,6 +57,9 @@ export const useHelpline = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the actual alignment position for the dragging node
|
||||
const nodeAlignPos = getNodeAlignPosition(node)
|
||||
|
||||
const showHorizontalHelpLineNodes = nodes.filter((n) => {
|
||||
if (n.id === node.id)
|
||||
return false
|
||||
@@ -39,33 +70,52 @@ export const useHelpline = () => {
|
||||
if (n.data.isInLoop)
|
||||
return false
|
||||
|
||||
const nY = Math.ceil(n.position.y)
|
||||
const nodeY = Math.ceil(node.position.y)
|
||||
// Get actual alignment position for comparison node
|
||||
const nAlignPos = getNodeAlignPosition(n)
|
||||
const nY = Math.ceil(nAlignPos.y)
|
||||
const nodeY = Math.ceil(nodeAlignPos.y)
|
||||
|
||||
if (nY - nodeY < 5 && nY - nodeY > -5)
|
||||
return true
|
||||
|
||||
return false
|
||||
}).sort((a, b) => a.position.x - b.position.x)
|
||||
}).sort((a, b) => {
|
||||
const aPos = getNodeAlignPosition(a)
|
||||
const bPos = getNodeAlignPosition(b)
|
||||
return aPos.x - bPos.x
|
||||
})
|
||||
|
||||
const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
|
||||
if (showHorizontalHelpLineNodesLength > 0) {
|
||||
const first = showHorizontalHelpLineNodes[0]
|
||||
const last = showHorizontalHelpLineNodes[showHorizontalHelpLineNodesLength - 1]
|
||||
|
||||
// Use actual alignment positions for help line rendering
|
||||
const firstPos = getNodeAlignPosition(first)
|
||||
const lastPos = getNodeAlignPosition(last)
|
||||
|
||||
// For entry nodes, we need to subtract the offset from width since lastPos already includes it
|
||||
const lastIsEntryNode = isEntryNode(last)
|
||||
const lastNodeWidth = lastIsEntryNode ? last.width! - ENTRY_NODE_WRAPPER_OFFSET.x : last.width!
|
||||
|
||||
const helpLine = {
|
||||
top: first.position.y,
|
||||
left: first.position.x,
|
||||
width: last.position.x + last.width! - first.position.x,
|
||||
top: firstPos.y,
|
||||
left: firstPos.x,
|
||||
width: lastPos.x + lastNodeWidth - firstPos.x,
|
||||
}
|
||||
|
||||
if (node.position.x < first.position.x) {
|
||||
helpLine.left = node.position.x
|
||||
helpLine.width = first.position.x + first.width! - node.position.x
|
||||
if (nodeAlignPos.x < firstPos.x) {
|
||||
const firstIsEntryNode = isEntryNode(first)
|
||||
const firstNodeWidth = firstIsEntryNode ? first.width! - ENTRY_NODE_WRAPPER_OFFSET.x : first.width!
|
||||
helpLine.left = nodeAlignPos.x
|
||||
helpLine.width = firstPos.x + firstNodeWidth - nodeAlignPos.x
|
||||
}
|
||||
|
||||
if (node.position.x > last.position.x)
|
||||
helpLine.width = node.position.x + node.width! - first.position.x
|
||||
if (nodeAlignPos.x > lastPos.x) {
|
||||
const nodeIsEntryNode = isEntryNode(node)
|
||||
const nodeWidth = nodeIsEntryNode ? node.width! - ENTRY_NODE_WRAPPER_OFFSET.x : node.width!
|
||||
helpLine.width = nodeAlignPos.x + nodeWidth - firstPos.x
|
||||
}
|
||||
|
||||
setHelpLineHorizontal(helpLine)
|
||||
}
|
||||
@@ -81,33 +131,52 @@ export const useHelpline = () => {
|
||||
if (n.data.isInLoop)
|
||||
return false
|
||||
|
||||
const nX = Math.ceil(n.position.x)
|
||||
const nodeX = Math.ceil(node.position.x)
|
||||
// Get actual alignment position for comparison node
|
||||
const nAlignPos = getNodeAlignPosition(n)
|
||||
const nX = Math.ceil(nAlignPos.x)
|
||||
const nodeX = Math.ceil(nodeAlignPos.x)
|
||||
|
||||
if (nX - nodeX < 5 && nX - nodeX > -5)
|
||||
return true
|
||||
|
||||
return false
|
||||
}).sort((a, b) => a.position.x - b.position.x)
|
||||
}).sort((a, b) => {
|
||||
const aPos = getNodeAlignPosition(a)
|
||||
const bPos = getNodeAlignPosition(b)
|
||||
return aPos.x - bPos.x
|
||||
})
|
||||
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
|
||||
|
||||
if (showVerticalHelpLineNodesLength > 0) {
|
||||
const first = showVerticalHelpLineNodes[0]
|
||||
const last = showVerticalHelpLineNodes[showVerticalHelpLineNodesLength - 1]
|
||||
|
||||
// Use actual alignment positions for help line rendering
|
||||
const firstPos = getNodeAlignPosition(first)
|
||||
const lastPos = getNodeAlignPosition(last)
|
||||
|
||||
// For entry nodes, we need to subtract the offset from height since lastPos already includes it
|
||||
const lastIsEntryNode = isEntryNode(last)
|
||||
const lastNodeHeight = lastIsEntryNode ? last.height! - ENTRY_NODE_WRAPPER_OFFSET.y : last.height!
|
||||
|
||||
const helpLine = {
|
||||
top: first.position.y,
|
||||
left: first.position.x,
|
||||
height: last.position.y + last.height! - first.position.y,
|
||||
top: firstPos.y,
|
||||
left: firstPos.x,
|
||||
height: lastPos.y + lastNodeHeight - firstPos.y,
|
||||
}
|
||||
|
||||
if (node.position.y < first.position.y) {
|
||||
helpLine.top = node.position.y
|
||||
helpLine.height = first.position.y + first.height! - node.position.y
|
||||
if (nodeAlignPos.y < firstPos.y) {
|
||||
const firstIsEntryNode = isEntryNode(first)
|
||||
const firstNodeHeight = firstIsEntryNode ? first.height! - ENTRY_NODE_WRAPPER_OFFSET.y : first.height!
|
||||
helpLine.top = nodeAlignPos.y
|
||||
helpLine.height = firstPos.y + firstNodeHeight - nodeAlignPos.y
|
||||
}
|
||||
|
||||
if (node.position.y > last.position.y)
|
||||
helpLine.height = node.position.y + node.height! - first.position.y
|
||||
if (nodeAlignPos.y > lastPos.y) {
|
||||
const nodeIsEntryNode = isEntryNode(node)
|
||||
const nodeHeight = nodeIsEntryNode ? node.height! - ENTRY_NODE_WRAPPER_OFFSET.y : node.height!
|
||||
helpLine.height = nodeAlignPos.y + nodeHeight - firstPos.y
|
||||
}
|
||||
|
||||
setHelpLineVertical(helpLine)
|
||||
}
|
||||
@@ -119,7 +188,7 @@ export const useHelpline = () => {
|
||||
showHorizontalHelpLineNodes,
|
||||
showVerticalHelpLineNodes,
|
||||
}
|
||||
}, [store, workflowStore])
|
||||
}, [store, workflowStore, getNodeAlignPosition])
|
||||
|
||||
return {
|
||||
handleSetHelpline,
|
||||
|
||||
@@ -5,13 +5,35 @@ import {
|
||||
useSysVarValues,
|
||||
} from '@/service/use-workflow'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { produce } from 'immer'
|
||||
import { BlockEnum } from '../types'
|
||||
|
||||
const varsAppendStartNodeKeys = ['query', 'files']
|
||||
const useInspectVarsCrud = () => {
|
||||
const nodesWithInspectVars = useStore(s => s.nodesWithInspectVars)
|
||||
const partOfNodesWithInspectVars = useStore(s => s.nodesWithInspectVars)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const isRagPipeline = configsMap?.flowType === FlowType.ragPipeline
|
||||
const { data: conversationVars } = useConversationVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const { data: systemVars } = useSysVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const { data: allSystemVars } = useSysVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const { varsAppendStartNode, systemVars } = (() => {
|
||||
if(allSystemVars?.length === 0)
|
||||
return { varsAppendStartNode: [], systemVars: [] }
|
||||
const varsAppendStartNode = allSystemVars?.filter(({ name }) => varsAppendStartNodeKeys.includes(name)) || []
|
||||
const systemVars = allSystemVars?.filter(({ name }) => !varsAppendStartNodeKeys.includes(name)) || []
|
||||
return { varsAppendStartNode, systemVars }
|
||||
})()
|
||||
const nodesWithInspectVars = (() => {
|
||||
if(!partOfNodesWithInspectVars || partOfNodesWithInspectVars.length === 0)
|
||||
return []
|
||||
|
||||
const nodesWithInspectVars = produce(partOfNodesWithInspectVars, (draft) => {
|
||||
draft.forEach((nodeWithVars) => {
|
||||
if(nodeWithVars.nodeType === BlockEnum.Start)
|
||||
nodeWithVars.vars = [...nodeWithVars.vars, ...varsAppendStartNode]
|
||||
})
|
||||
})
|
||||
return nodesWithInspectVars
|
||||
})()
|
||||
const hasNodeInspectVars = useHooksStore(s => s.hasNodeInspectVars)
|
||||
const hasSetInspectVar = useHooksStore(s => s.hasSetInspectVar)
|
||||
const fetchInspectVarValue = useHooksStore(s => s.fetchInspectVarValue)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { SyncCallback } from './use-nodes-sync-draft'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
|
||||
@@ -28,12 +29,19 @@ export const useNodeDataUpdate = () => {
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleNodeDataUpdateWithSyncDraft = useCallback((payload: NodeDataUpdatePayload) => {
|
||||
const handleNodeDataUpdateWithSyncDraft = useCallback((
|
||||
payload: NodeDataUpdatePayload,
|
||||
options?: {
|
||||
sync?: boolean
|
||||
notRefreshWhenSyncError?: boolean
|
||||
callback?: SyncCallback
|
||||
},
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
handleNodeDataUpdate(payload)
|
||||
handleSyncWorkflowDraft()
|
||||
handleSyncWorkflowDraft(options?.sync, options?.notRefreshWhenSyncError, options?.callback)
|
||||
}, [handleSyncWorkflowDraft, handleNodeDataUpdate, getNodesReadOnly])
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { BlockEnum, type CommonNodeType } from '../types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
useInvalidToolsByType,
|
||||
} from '@/service/use-tools'
|
||||
import {
|
||||
useAllTriggerPlugins,
|
||||
useInvalidateAllTriggerPlugins,
|
||||
} from '@/service/use-triggers'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { useStore } from '../store'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
type InstallationState = {
|
||||
isChecking: boolean
|
||||
isMissing: boolean
|
||||
uniqueIdentifier?: string
|
||||
canInstall: boolean
|
||||
onInstallSuccess: () => void
|
||||
shouldDim: boolean
|
||||
}
|
||||
|
||||
const useToolInstallation = (data: ToolNodeType): InstallationState => {
|
||||
const builtInQuery = useAllBuiltInTools()
|
||||
const customQuery = useAllCustomTools()
|
||||
const workflowQuery = useAllWorkflowTools()
|
||||
const mcpQuery = useAllMCPTools()
|
||||
const invalidateTools = useInvalidToolsByType(data.provider_type)
|
||||
|
||||
const collectionInfo = useMemo(() => {
|
||||
switch (data.provider_type) {
|
||||
case CollectionType.builtIn:
|
||||
return {
|
||||
list: builtInQuery.data,
|
||||
isLoading: builtInQuery.isLoading,
|
||||
}
|
||||
case CollectionType.custom:
|
||||
return {
|
||||
list: customQuery.data,
|
||||
isLoading: customQuery.isLoading,
|
||||
}
|
||||
case CollectionType.workflow:
|
||||
return {
|
||||
list: workflowQuery.data,
|
||||
isLoading: workflowQuery.isLoading,
|
||||
}
|
||||
case CollectionType.mcp:
|
||||
return {
|
||||
list: mcpQuery.data,
|
||||
isLoading: mcpQuery.isLoading,
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}, [
|
||||
builtInQuery.data,
|
||||
builtInQuery.isLoading,
|
||||
customQuery.data,
|
||||
customQuery.isLoading,
|
||||
data.provider_type,
|
||||
mcpQuery.data,
|
||||
mcpQuery.isLoading,
|
||||
workflowQuery.data,
|
||||
workflowQuery.isLoading,
|
||||
])
|
||||
|
||||
const collection = collectionInfo?.list
|
||||
const isLoading = collectionInfo?.isLoading ?? false
|
||||
const isResolved = !!collectionInfo && !isLoading
|
||||
|
||||
const matchedCollection = useMemo(() => {
|
||||
if (!collection || !collection.length)
|
||||
return undefined
|
||||
|
||||
return collection.find((toolWithProvider) => {
|
||||
if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id)
|
||||
return true
|
||||
if (canFindTool(toolWithProvider.id, data.provider_id))
|
||||
return true
|
||||
if (toolWithProvider.name === data.provider_name)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
}, [collection, data.plugin_id, data.provider_id, data.provider_name])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
if (invalidateTools)
|
||||
invalidateTools()
|
||||
}, [invalidateTools])
|
||||
|
||||
const shouldDim = (!!collectionInfo && !isResolved) || (isResolved && !matchedCollection)
|
||||
|
||||
return {
|
||||
isChecking: !!collectionInfo && !isResolved,
|
||||
isMissing: isResolved && !matchedCollection,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
shouldDim,
|
||||
}
|
||||
}
|
||||
|
||||
const useTriggerInstallation = (data: PluginTriggerNodeType): InstallationState => {
|
||||
const triggerPluginsQuery = useAllTriggerPlugins()
|
||||
const invalidateTriggers = useInvalidateAllTriggerPlugins()
|
||||
|
||||
const triggerProviders = triggerPluginsQuery.data
|
||||
const isLoading = triggerPluginsQuery.isLoading
|
||||
|
||||
const matchedProvider = useMemo(() => {
|
||||
if (!triggerProviders || !triggerProviders.length)
|
||||
return undefined
|
||||
|
||||
return triggerProviders.find(provider =>
|
||||
provider.name === data.provider_name
|
||||
|| provider.id === data.provider_id
|
||||
|| (data.plugin_id && provider.plugin_id === data.plugin_id),
|
||||
)
|
||||
}, [
|
||||
data.plugin_id,
|
||||
data.provider_id,
|
||||
data.provider_name,
|
||||
triggerProviders,
|
||||
])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
invalidateTriggers()
|
||||
}, [invalidateTriggers])
|
||||
|
||||
const shouldDim = isLoading || (!isLoading && !!triggerProviders && !matchedProvider)
|
||||
|
||||
return {
|
||||
isChecking: isLoading,
|
||||
isMissing: !isLoading && !!triggerProviders && !matchedProvider,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
shouldDim,
|
||||
}
|
||||
}
|
||||
|
||||
const useDataSourceInstallation = (data: DataSourceNodeType): InstallationState => {
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const invalidateDataSourceList = useInvalidDataSourceList()
|
||||
|
||||
const matchedPlugin = useMemo(() => {
|
||||
if (!dataSourceList || !dataSourceList.length)
|
||||
return undefined
|
||||
|
||||
return dataSourceList.find((item) => {
|
||||
if (data.plugin_unique_identifier && item.plugin_unique_identifier === data.plugin_unique_identifier)
|
||||
return true
|
||||
if (data.plugin_id && item.plugin_id === data.plugin_id)
|
||||
return true
|
||||
if (data.provider_name && item.provider === data.provider_name)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
}, [data.plugin_id, data.plugin_unique_identifier, data.provider_name, dataSourceList])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
invalidateDataSourceList()
|
||||
}, [invalidateDataSourceList])
|
||||
|
||||
const hasLoadedList = dataSourceList !== undefined
|
||||
|
||||
const shouldDim = !hasLoadedList || (hasLoadedList && !matchedPlugin)
|
||||
|
||||
return {
|
||||
isChecking: !hasLoadedList,
|
||||
isMissing: hasLoadedList && !matchedPlugin,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
shouldDim,
|
||||
}
|
||||
}
|
||||
|
||||
export const useNodePluginInstallation = (data: CommonNodeType): InstallationState => {
|
||||
const toolInstallation = useToolInstallation(data as ToolNodeType)
|
||||
const triggerInstallation = useTriggerInstallation(data as PluginTriggerNodeType)
|
||||
const dataSourceInstallation = useDataSourceInstallation(data as DataSourceNodeType)
|
||||
|
||||
switch (data.type as BlockEnum) {
|
||||
case BlockEnum.Tool:
|
||||
return toolInstallation
|
||||
case BlockEnum.TriggerPlugin:
|
||||
return triggerInstallation
|
||||
case BlockEnum.DataSource:
|
||||
return dataSourceInstallation
|
||||
default:
|
||||
return {
|
||||
isChecking: false,
|
||||
isMissing: false,
|
||||
uniqueIdentifier: undefined,
|
||||
canInstall: false,
|
||||
onInstallSuccess: () => undefined,
|
||||
shouldDim: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type { DataSourceDefaultValue, ToolDefaultValue } from '../block-selector/types'
|
||||
import type { PluginDefaultValue } from '../block-selector/types'
|
||||
import type { Edge, Node, OnNodeAdd } from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { BlockEnum, isTriggerNode } from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import {
|
||||
CUSTOM_EDGE,
|
||||
@@ -63,6 +63,15 @@ import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import useInspectVarsCrud from './use-inspect-vars-crud'
|
||||
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
|
||||
|
||||
// Entry node deletion restriction has been removed to allow empty workflows
|
||||
|
||||
// Entry node (Start/Trigger) wrapper offsets for alignment
|
||||
// Must match the values in use-helpline.ts
|
||||
const ENTRY_NODE_WRAPPER_OFFSET = {
|
||||
x: 0,
|
||||
y: 21, // Adjusted based on visual testing feedback
|
||||
} as const
|
||||
|
||||
export const useNodesInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
@@ -138,21 +147,51 @@ export const useNodesInteractions = () => {
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(n => n.id === node.id)!
|
||||
|
||||
if (showVerticalHelpLineNodesLength > 0)
|
||||
currentNode.position.x = showVerticalHelpLineNodes[0].position.x
|
||||
else if (restrictPosition.x !== undefined)
|
||||
currentNode.position.x = restrictPosition.x
|
||||
else if (restrictLoopPosition.x !== undefined)
|
||||
currentNode.position.x = restrictLoopPosition.x
|
||||
else currentNode.position.x = node.position.x
|
||||
// Check if current dragging node is an entry node
|
||||
const isCurrentEntryNode = isTriggerNode(node.data.type as any) || node.data.type === BlockEnum.Start
|
||||
|
||||
if (showHorizontalHelpLineNodesLength > 0)
|
||||
currentNode.position.y = showHorizontalHelpLineNodes[0].position.y
|
||||
else if (restrictPosition.y !== undefined)
|
||||
// X-axis alignment with offset consideration
|
||||
if (showVerticalHelpLineNodesLength > 0) {
|
||||
const targetNode = showVerticalHelpLineNodes[0]
|
||||
const isTargetEntryNode = isTriggerNode(targetNode.data.type as any) || targetNode.data.type === BlockEnum.Start
|
||||
|
||||
// Calculate the wrapper position needed to align the inner nodes
|
||||
// Target inner position = target.position + target.offset
|
||||
// Current inner position should equal target inner position
|
||||
// So: current.position + current.offset = target.position + target.offset
|
||||
// Therefore: current.position = target.position + target.offset - current.offset
|
||||
const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0
|
||||
const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0
|
||||
currentNode.position.x = targetNode.position.x + targetOffset - currentOffset
|
||||
}
|
||||
else if (restrictPosition.x !== undefined) {
|
||||
currentNode.position.x = restrictPosition.x
|
||||
}
|
||||
else if (restrictLoopPosition.x !== undefined) {
|
||||
currentNode.position.x = restrictLoopPosition.x
|
||||
}
|
||||
else {
|
||||
currentNode.position.x = node.position.x
|
||||
}
|
||||
|
||||
// Y-axis alignment with offset consideration
|
||||
if (showHorizontalHelpLineNodesLength > 0) {
|
||||
const targetNode = showHorizontalHelpLineNodes[0]
|
||||
const isTargetEntryNode = isTriggerNode(targetNode.data.type as any) || targetNode.data.type === BlockEnum.Start
|
||||
|
||||
const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
|
||||
const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
|
||||
currentNode.position.y = targetNode.position.y + targetOffset - currentOffset
|
||||
}
|
||||
else if (restrictPosition.y !== undefined) {
|
||||
currentNode.position.y = restrictPosition.y
|
||||
else if (restrictLoopPosition.y !== undefined)
|
||||
}
|
||||
else if (restrictLoopPosition.y !== undefined) {
|
||||
currentNode.position.y = restrictLoopPosition.y
|
||||
else currentNode.position.y = node.position.y
|
||||
}
|
||||
else {
|
||||
currentNode.position.y = node.position.y
|
||||
}
|
||||
})
|
||||
setNodes(newNodes)
|
||||
},
|
||||
@@ -357,6 +396,7 @@ export const useNodesInteractions = () => {
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE) return
|
||||
if (node.type === CUSTOM_LOOP_START_NODE) return
|
||||
if (node.data.type === BlockEnum.DataSourceEmpty) return
|
||||
if (node.data._pluginInstallLocked) return
|
||||
handleNodeSelect(node.id)
|
||||
},
|
||||
[handleNodeSelect],
|
||||
@@ -735,7 +775,7 @@ export const useNodesInteractions = () => {
|
||||
nodeType,
|
||||
sourceHandle = 'source',
|
||||
targetHandle = 'target',
|
||||
toolDefaultValue,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{ prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle },
|
||||
) => {
|
||||
@@ -756,7 +796,7 @@ export const useNodesInteractions = () => {
|
||||
nodesWithSameType.length > 0
|
||||
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
|
||||
: defaultValue.title,
|
||||
...toolDefaultValue,
|
||||
...pluginDefaultValue,
|
||||
selected: true,
|
||||
_showAddVariablePopup:
|
||||
(nodeType === BlockEnum.VariableAssigner
|
||||
@@ -1286,7 +1326,7 @@ export const useNodesInteractions = () => {
|
||||
currentNodeId: string,
|
||||
nodeType: BlockEnum,
|
||||
sourceHandle: string,
|
||||
toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue,
|
||||
pluginDefaultValue?: PluginDefaultValue,
|
||||
) => {
|
||||
if (getNodesReadOnly()) return
|
||||
|
||||
@@ -1310,7 +1350,7 @@ export const useNodesInteractions = () => {
|
||||
nodesWithSameType.length > 0
|
||||
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
|
||||
: defaultValue.title,
|
||||
...toolDefaultValue,
|
||||
...pluginDefaultValue,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
selected: currentNode.data.selected,
|
||||
@@ -1656,7 +1696,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const nodes = getNodes()
|
||||
const bundledNodes = nodes.filter(
|
||||
node => node.data._isBundled && node.data.type !== BlockEnum.Start,
|
||||
node => node.data._isBundled,
|
||||
)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
@@ -1669,7 +1709,7 @@ export const useNodesInteractions = () => {
|
||||
if (edgeSelected) return
|
||||
|
||||
const selectedNode = nodes.find(
|
||||
node => node.data.selected && node.data.type !== BlockEnum.Start,
|
||||
node => node.data.selected,
|
||||
)
|
||||
|
||||
if (selectedNode) handleNodeDelete(selectedNode.id)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useStore,
|
||||
} from '../store'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from './use-workflow'
|
||||
import { useStore } from '../store'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export type SyncCallback = {
|
||||
onSuccess?: () => void
|
||||
onError?: () => void
|
||||
onSettled?: () => void
|
||||
}
|
||||
|
||||
export const useNodesSyncDraft = () => {
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft)
|
||||
@@ -16,11 +18,7 @@ export const useNodesSyncDraft = () => {
|
||||
const handleSyncWorkflowDraft = useCallback((
|
||||
sync?: boolean,
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: {
|
||||
onSuccess?: () => void
|
||||
onError?: () => void
|
||||
onSettled?: () => void
|
||||
},
|
||||
callback?: SyncCallback,
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react'
|
||||
|
||||
export const useSerialAsyncCallback = <Args extends any[], Result = void>(
|
||||
fn: (...args: Args) => Promise<Result> | Result,
|
||||
shouldSkip?: () => boolean,
|
||||
) => {
|
||||
const queueRef = useRef<Promise<unknown>>(Promise.resolve())
|
||||
|
||||
return useCallback((...args: Args) => {
|
||||
if (shouldSkip?.())
|
||||
return Promise.resolve(undefined as Result)
|
||||
|
||||
const lastPromise = queueRef.current.catch(() => undefined)
|
||||
const nextPromise = lastPromise.then(() => fn(...args))
|
||||
queueRef.current = nextPromise
|
||||
|
||||
return nextPromise
|
||||
}, [fn, shouldSkip])
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
useWorkflowCanvasMaximize,
|
||||
useWorkflowMoveMode,
|
||||
useWorkflowOrganize,
|
||||
useWorkflowStartRun,
|
||||
} from '.'
|
||||
|
||||
export const useShortcuts = (): void => {
|
||||
@@ -28,7 +27,6 @@ export const useShortcuts = (): void => {
|
||||
dimOtherNodes,
|
||||
undimAllNodes,
|
||||
} = useNodesInteractions()
|
||||
const { handleStartWorkflowRun } = useWorkflowStartRun()
|
||||
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleEdgeDelete } = useEdgesInteractions()
|
||||
@@ -61,9 +59,8 @@ export const useShortcuts = (): void => {
|
||||
}
|
||||
|
||||
const shouldHandleShortcut = useCallback((e: KeyboardEvent) => {
|
||||
const { showFeaturesPanel } = workflowStore.getState()
|
||||
return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement)
|
||||
}, [workflowStore])
|
||||
return !isEventTargetInputArea(e.target as HTMLElement)
|
||||
}, [])
|
||||
|
||||
useKeyPress(['delete', 'backspace'], (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
@@ -99,7 +96,11 @@ export const useShortcuts = (): void => {
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleStartWorkflowRun()
|
||||
// @ts-expect-error - Dynamic property added by run-and-history component
|
||||
if (window._toggleTestRunDropdown) {
|
||||
// @ts-expect-error - Dynamic property added by run-and-history component
|
||||
window._toggleTestRunDropdown()
|
||||
}
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import type {
|
||||
Node,
|
||||
} from '../types'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '../types'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import type { Node, ToolWithProvider } from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { canFindTool } from '@/utils'
|
||||
import {
|
||||
@@ -20,6 +10,32 @@ import {
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import type { TriggerWithProvider } from '../block-selector/types'
|
||||
|
||||
const isTriggerPluginNode = (data: Node['data']): data is PluginTriggerNodeType => data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
const isToolNode = (data: Node['data']): data is ToolNodeType => data.type === BlockEnum.Tool
|
||||
|
||||
const isDataSourceNode = (data: Node['data']): data is DataSourceNodeType => data.type === BlockEnum.DataSource
|
||||
|
||||
const findTriggerPluginIcon = (
|
||||
identifiers: (string | undefined)[],
|
||||
triggers: TriggerWithProvider[] | undefined,
|
||||
) => {
|
||||
const targetTriggers = triggers || []
|
||||
for (const identifier of identifiers) {
|
||||
if (!identifier)
|
||||
continue
|
||||
const matched = targetTriggers.find(trigger => trigger.id === identifier || canFindTool(trigger.id, identifier))
|
||||
if (matched?.icon)
|
||||
return matched.icon
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const useToolIcon = (data?: Node['data']) => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
@@ -27,26 +43,78 @@ export const useToolIcon = (data?: Node['data']) => {
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
// const a = useStore(s => s.data)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
|
||||
const toolIcon = useMemo(() => {
|
||||
if (!data)
|
||||
return ''
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
// eslint-disable-next-line sonarjs/no-dead-store
|
||||
let targetTools = buildInTools || []
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
targetTools = buildInTools || []
|
||||
else if (data.provider_type === CollectionType.custom)
|
||||
targetTools = customTools || []
|
||||
else if (data.provider_type === CollectionType.mcp)
|
||||
targetTools = mcpTools || []
|
||||
else
|
||||
targetTools = workflowTools || []
|
||||
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
|
||||
|
||||
if (isTriggerPluginNode(data)) {
|
||||
const icon = findTriggerPluginIcon(
|
||||
[
|
||||
data.plugin_id,
|
||||
data.provider_id,
|
||||
data.provider_name,
|
||||
],
|
||||
triggerPlugins,
|
||||
)
|
||||
if (icon)
|
||||
return icon
|
||||
}
|
||||
if (data.type === BlockEnum.DataSource)
|
||||
return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon
|
||||
}, [data, dataSourceList, buildInTools, customTools, mcpTools, workflowTools])
|
||||
|
||||
if (isToolNode(data)) {
|
||||
let primaryCollection: ToolWithProvider[] | undefined
|
||||
switch (data.provider_type) {
|
||||
case CollectionType.custom:
|
||||
primaryCollection = customTools
|
||||
break
|
||||
case CollectionType.mcp:
|
||||
primaryCollection = mcpTools
|
||||
break
|
||||
case CollectionType.workflow:
|
||||
primaryCollection = workflowTools
|
||||
break
|
||||
case CollectionType.builtIn:
|
||||
default:
|
||||
primaryCollection = buildInTools
|
||||
break
|
||||
}
|
||||
|
||||
const collectionsToSearch = [
|
||||
primaryCollection,
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
] as Array<ToolWithProvider[] | undefined>
|
||||
|
||||
const seen = new Set<ToolWithProvider[]>()
|
||||
for (const collection of collectionsToSearch) {
|
||||
if (!collection || seen.has(collection))
|
||||
continue
|
||||
seen.add(collection)
|
||||
const matched = collection.find((toolWithProvider) => {
|
||||
if (canFindTool(toolWithProvider.id, data.provider_id))
|
||||
return true
|
||||
if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id)
|
||||
return true
|
||||
return data.provider_name === toolWithProvider.name
|
||||
})
|
||||
if (matched?.icon)
|
||||
return matched.icon
|
||||
}
|
||||
|
||||
if (data.provider_icon)
|
||||
return data.provider_icon
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isDataSourceNode(data))
|
||||
return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon || ''
|
||||
|
||||
return ''
|
||||
}, [data, dataSourceList, buildInTools, customTools, workflowTools, mcpTools, triggerPlugins])
|
||||
|
||||
return toolIcon
|
||||
}
|
||||
@@ -55,27 +123,80 @@ export const useGetToolIcon = () => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const getToolIcon = useCallback((data: Node['data']) => {
|
||||
const {
|
||||
buildInTools: storeBuiltInTools,
|
||||
customTools: storeCustomTools,
|
||||
workflowTools: storeWorkflowTools,
|
||||
mcpTools: storeMcpTools,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
// eslint-disable-next-line sonarjs/no-dead-store
|
||||
let targetTools = buildInTools || []
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
targetTools = buildInTools || []
|
||||
else if (data.provider_type === CollectionType.custom)
|
||||
targetTools = customTools || []
|
||||
else
|
||||
targetTools = workflowTools || []
|
||||
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
|
||||
if (isTriggerPluginNode(data)) {
|
||||
return findTriggerPluginIcon(
|
||||
[
|
||||
data.plugin_id,
|
||||
data.provider_id,
|
||||
data.provider_name,
|
||||
],
|
||||
triggerPlugins,
|
||||
)
|
||||
}
|
||||
|
||||
if (data.type === BlockEnum.DataSource)
|
||||
if (isToolNode(data)) {
|
||||
const primaryCollection = (() => {
|
||||
switch (data.provider_type) {
|
||||
case CollectionType.custom:
|
||||
return storeCustomTools ?? customTools
|
||||
case CollectionType.mcp:
|
||||
return storeMcpTools ?? mcpTools
|
||||
case CollectionType.workflow:
|
||||
return storeWorkflowTools ?? workflowTools
|
||||
case CollectionType.builtIn:
|
||||
default:
|
||||
return storeBuiltInTools ?? buildInTools
|
||||
}
|
||||
})()
|
||||
|
||||
const collectionsToSearch = [
|
||||
primaryCollection,
|
||||
storeBuiltInTools ?? buildInTools,
|
||||
storeCustomTools ?? customTools,
|
||||
storeWorkflowTools ?? workflowTools,
|
||||
storeMcpTools ?? mcpTools,
|
||||
] as Array<ToolWithProvider[] | undefined>
|
||||
|
||||
const seen = new Set<ToolWithProvider[]>()
|
||||
for (const collection of collectionsToSearch) {
|
||||
if (!collection || seen.has(collection))
|
||||
continue
|
||||
seen.add(collection)
|
||||
const matched = collection.find((toolWithProvider) => {
|
||||
if (canFindTool(toolWithProvider.id, data.provider_id))
|
||||
return true
|
||||
if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id)
|
||||
return true
|
||||
return data.provider_name === toolWithProvider.name
|
||||
})
|
||||
if (matched?.icon)
|
||||
return matched.icon
|
||||
}
|
||||
|
||||
if (data.provider_icon)
|
||||
return data.provider_icon
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isDataSourceNode(data))
|
||||
return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon
|
||||
}, [workflowStore])
|
||||
|
||||
return undefined
|
||||
}, [workflowStore, triggerPlugins, buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
return getToolIcon
|
||||
}
|
||||
|
||||
@@ -316,7 +316,10 @@ export const useWorkflowUpdate = () => {
|
||||
edges: initialEdges(edges, nodes),
|
||||
},
|
||||
} as any)
|
||||
setViewport(viewport)
|
||||
|
||||
// Only set viewport if it exists and is valid
|
||||
if (viewport && typeof viewport.x === 'number' && typeof viewport.y === 'number' && typeof viewport.zoom === 'number')
|
||||
setViewport(viewport)
|
||||
}, [eventEmitter, reactflow])
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,10 +4,17 @@ export const useWorkflowStartRun = () => {
|
||||
const handleStartWorkflowRun = useHooksStore(s => s.handleStartWorkflowRun)
|
||||
const handleWorkflowStartRunInWorkflow = useHooksStore(s => s.handleWorkflowStartRunInWorkflow)
|
||||
const handleWorkflowStartRunInChatflow = useHooksStore(s => s.handleWorkflowStartRunInChatflow)
|
||||
|
||||
const handleWorkflowTriggerScheduleRunInWorkflow = useHooksStore(s => s.handleWorkflowTriggerScheduleRunInWorkflow)
|
||||
const handleWorkflowTriggerWebhookRunInWorkflow = useHooksStore(s => s.handleWorkflowTriggerWebhookRunInWorkflow)
|
||||
const handleWorkflowTriggerPluginRunInWorkflow = useHooksStore(s => s.handleWorkflowTriggerPluginRunInWorkflow)
|
||||
const handleWorkflowRunAllTriggersInWorkflow = useHooksStore(s => s.handleWorkflowRunAllTriggersInWorkflow)
|
||||
return {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowTriggerScheduleRunInWorkflow,
|
||||
handleWorkflowTriggerWebhookRunInWorkflow,
|
||||
handleWorkflowTriggerPluginRunInWorkflow,
|
||||
handleWorkflowRunAllTriggersInWorkflow,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
getWorkflowEntryNode,
|
||||
isWorkflowEntryNode,
|
||||
} from '../utils/workflow-entry'
|
||||
import {
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
} from '../constants'
|
||||
@@ -36,11 +40,12 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { useNodesMetaData } from '.'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
|
||||
return appDetail?.mode === 'advanced-chat'
|
||||
return appDetail?.mode === AppModeEnum.ADVANCED_CHAT
|
||||
}
|
||||
|
||||
export const useWorkflow = () => {
|
||||
@@ -63,6 +68,7 @@ export const useWorkflow = () => {
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
// let startNode = getWorkflowEntryNode(nodes)
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
let startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
|
||||
@@ -232,6 +238,33 @@ export const useWorkflow = () => {
|
||||
return nodes.filter(node => node.parentId === nodeId)
|
||||
}, [store])
|
||||
|
||||
const isFromStartNode = useCallback((nodeId: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (!currentNode)
|
||||
return false
|
||||
|
||||
if (isWorkflowEntryNode(currentNode.data.type))
|
||||
return true
|
||||
|
||||
const checkPreviousNodes = (node: Node) => {
|
||||
const previousNodes = getBeforeNodeById(node.id)
|
||||
|
||||
for (const prevNode of previousNodes) {
|
||||
if (isWorkflowEntryNode(prevNode.data.type))
|
||||
return true
|
||||
if (checkPreviousNodes(prevNode))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return checkPreviousNodes(currentNode)
|
||||
}, [store, getBeforeNodeById])
|
||||
|
||||
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
@@ -391,6 +424,13 @@ export const useWorkflow = () => {
|
||||
return !hasCycle(targetNode)
|
||||
}, [store, getAvailableBlocks])
|
||||
|
||||
const getNode = useCallback((nodeId?: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
return nodes.find(node => node.id === nodeId) || getWorkflowEntryNode(nodes)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
getNodeById,
|
||||
getTreeLeafNodes,
|
||||
@@ -407,6 +447,8 @@ export const useWorkflow = () => {
|
||||
getLoopNodeChildren,
|
||||
getRootNodesById,
|
||||
getStartNodes,
|
||||
isFromStartNode,
|
||||
getNode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,14 +472,14 @@ export const useNodesReadOnly = () => {
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
|
||||
const getNodesReadOnly = useCallback(() => {
|
||||
const getNodesReadOnly = useCallback((): boolean => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
historyWorkflowData,
|
||||
isRestoring,
|
||||
} = workflowStore.getState()
|
||||
|
||||
return workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring
|
||||
return !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring)
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user