import type { FC, ReactElement, } from 'react' import type { NodeProps } from '@/app/components/workflow/types' import { cloneElement, memo, useContext, useMemo, useRef, } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useZustandStore } from 'zustand' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import BlockIcon from '@/app/components/workflow/block-icon' import { ToolTypeEnum } from '@/app/components/workflow/block-selector/types' import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration' import { WorkflowContext } from '@/app/components/workflow/context' import { useNodesReadOnly, useToolIcon } from '@/app/components/workflow/hooks' import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud' import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation' import { useNodeIterationInteractions } from '@/app/components/workflow/nodes/iteration/use-interactions' import { useNodeLoopInteractions } from '@/app/components/workflow/nodes/loop/use-interactions' import CopyID from '@/app/components/workflow/nodes/tool/components/copy-id' import { createWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum, ControlMode, NodeRunningStatus, } from '@/app/components/workflow/types' import { hasErrorHandleNode, hasRetryNode } from '@/app/components/workflow/utils' import { useAppContext } from '@/context/app-context' import { cn } from '@/utils/classnames' import AddVariablePopupWithPosition from './components/add-variable-popup-with-position' import EntryNodeContainer, { StartNodeTypeEnum } from './components/entry-node-container' import ErrorHandleOnNode from './components/error-handle/error-handle-on-node' import NodeControl from './components/node-control' import { NodeSourceHandle, NodeTargetHandle, } from './components/node-handle' import NodeResizer from './components/node-resizer' import RetryOnNode from './components/retry/retry-on-node' import { NodeBody, NodeDescription, NodeHeaderMeta, } from './node-sections' import { getLoopIndexTextKey, getNodeStatusBorders, isContainerNode, isEntryWorkflowNode, } from './node.helpers' import useNodeResizeObserver from './use-node-resize-observer' type NodeChildProps = { id: string data: NodeProps['data'] } type BaseNodeProps = { children: ReactElement> id: NodeProps['id'] data: NodeProps['data'] } const BaseNode: FC = ({ id, data, children, }) => { const { t } = useTranslation() const nodeRef = useRef(null) const { nodesReadOnly } = useNodesReadOnly() const { _subGraphEntry } = data const iconType = data._iconTypeOverride ?? data.type const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions() const { handleNodeLoopChildSizeChange } = useNodeLoopInteractions() const toolIcon = useToolIcon(data) const { userProfile } = useAppContext() const workflowStore = useContext(WorkflowContext) const fallbackWorkflowStoreRef = useRef | null>(null) if (!fallbackWorkflowStoreRef.current) fallbackWorkflowStoreRef.current = createWorkflowStore({}) const resolvedWorkflowStore = workflowStore ?? fallbackWorkflowStoreRef.current const appId = useZustandStore(resolvedWorkflowStore, s => s.appId) const controlMode = useZustandStore(resolvedWorkflowStore, s => s.controlMode) const { nodePanelPresence } = useCollaboration(appId as string) const { shouldDim: pluginDimmed, isChecking: pluginIsChecking, isMissing: pluginIsMissing, canInstall: pluginCanInstall, uniqueIdentifier: pluginUniqueIdentifier } = useNodePluginInstallation(data) const pluginInstallLocked = !pluginIsChecking && pluginIsMissing && pluginCanInstall && Boolean(pluginUniqueIdentifier) const currentUserPresence = useMemo(() => { const userId = userProfile?.id || '' const username = userProfile?.name || userProfile?.email || 'User' const avatar = userProfile?.avatar_url || userProfile?.avatar || null return { avatar, userId, username, } }, [userProfile?.avatar, userProfile?.avatar_url, userProfile?.email, userProfile?.id, userProfile?.name]) const viewingUsers = useMemo(() => { const presence = nodePanelPresence?.[id] if (!presence) return [] return Object.values(presence) .filter(viewer => viewer.userId && viewer.userId !== currentUserPresence.userId) .map(viewer => ({ avatar_url: viewer.avatar || null, id: viewer.userId, name: viewer.username, })) }, [currentUserPresence.userId, id, nodePanelPresence]) useNodeResizeObserver({ enabled: Boolean(data.selected && data.isInIteration), nodeRef, onResize: () => handleNodeIterationChildSizeChange(id), }) useNodeResizeObserver({ enabled: Boolean(data.selected && data.isInLoop), nodeRef, onResize: () => handleNodeLoopChildSizeChange(id), }) const { hasNodeInspectVars } = useInspectVarsCrud() const isLoading = data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running const hasVarValue = hasNodeInspectVars(id) const showSelectedBorder = Boolean(data.selected || data._isBundled || data._isEntering) const { showRunningBorder, showSuccessBorder, showFailedBorder, showExceptionBorder, } = useMemo(() => getNodeStatusBorders(data._runningStatus, hasVarValue, showSelectedBorder), [data._runningStatus, hasVarValue, showSelectedBorder]) const LoopIndex = useMemo(() => { const translationKey = getLoopIndexTextKey(data._runningStatus) const text = translationKey ? t(translationKey, { ns: 'workflow', count: data._loopIndex }) : '' if (text) { return (
{text}
) } return null }, [data._loopIndex, data._runningStatus, t]) if (_subGraphEntry) { return (
{data.title}
) } const nodeContent = (
{(data._dimmed || pluginDimmed || pluginInstallLocked) && (
e.stopPropagation() : undefined} data-testid="workflow-node-install-overlay" /> )} { data.type === BlockEnum.DataSource && (
{t('blocks.datasource', { ns: 'workflow' })}
) }
{ data._showAddVariablePopup && ( ) } { data.type === BlockEnum.Iteration && ( ) } { data.type === BlockEnum.Loop && ( ) } { !data._isCandidate && ( ) } { data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && ( ) } { !data._runningStatus && !nodesReadOnly && !data._isCandidate && ( ) }
{data.title}
{viewingUsers.length > 0 && (
)}
)} /> { hasRetryNode(data.type) && ( ) } { hasErrorHandleNode(data.type) && ( ) } {data.type === BlockEnum.Tool && data.provider_type === ToolTypeEnum.MCP && (
)}
) const isStartNode = data.type === BlockEnum.Start const isEntryNode = isEntryWorkflowNode(data.type) const shouldWrapEntryNode = isEntryNode && !(isStartNode && _subGraphEntry) return shouldWrapEntryNode ? ( {nodeContent} ) : nodeContent } export default memo(BaseNode)