mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
feat: support workflow node fold (#2797)
* feat: support workflow node fold * fix * fix
This commit is contained in:
1
packages/global/core/workflow/type/node.d.ts
vendored
1
packages/global/core/workflow/type/node.d.ts
vendored
@@ -104,6 +104,7 @@ export type FlowNodeItemType = FlowNodeTemplateType & {
|
|||||||
response?: ChatHistoryItemResType;
|
response?: ChatHistoryItemResType;
|
||||||
isExpired?: boolean;
|
isExpired?: boolean;
|
||||||
};
|
};
|
||||||
|
isFolded?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// store node type
|
// store node type
|
||||||
|
@@ -123,6 +123,7 @@ export const iconPaths = {
|
|||||||
'core/chat/chatLight': () => import('./icons/core/chat/chatLight.svg'),
|
'core/chat/chatLight': () => import('./icons/core/chat/chatLight.svg'),
|
||||||
'core/chat/chatModelTag': () => import('./icons/core/chat/chatModelTag.svg'),
|
'core/chat/chatModelTag': () => import('./icons/core/chat/chatModelTag.svg'),
|
||||||
'core/chat/chevronDown': () => import('./icons/core/chat/chevronDown.svg'),
|
'core/chat/chevronDown': () => import('./icons/core/chat/chevronDown.svg'),
|
||||||
|
'core/chat/chevronRight': () => import('./icons/core/chat/chevronRight.svg'),
|
||||||
'core/chat/chevronSelector': () => import('./icons/core/chat/chevronSelector.svg'),
|
'core/chat/chevronSelector': () => import('./icons/core/chat/chevronSelector.svg'),
|
||||||
'core/chat/chevronUp': () => import('./icons/core/chat/chevronUp.svg'),
|
'core/chat/chevronUp': () => import('./icons/core/chat/chevronUp.svg'),
|
||||||
'core/chat/export/pdf': () => import('./icons/core/chat/export/pdf.svg'),
|
'core/chat/export/pdf': () => import('./icons/core/chat/export/pdf.svg'),
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 24" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.19277 5.29289C8.80224 5.68342 8.80224 6.31658 9.19277 6.70711L14.4857 12L9.19277 17.2929C8.80224 17.6834 8.80224 18.3166 9.19277 18.7071C9.58329 19.0976 10.2165 19.0976 10.607 18.7071L16.607 12.7071C16.9975 12.3166 16.9975 11.6834 16.607 11.2929L10.607 5.29289C10.2165 4.90237 9.58329 4.90237 9.19277 5.29289Z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 448 B |
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
|
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
|
||||||
import { Flex } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
|
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
@@ -27,6 +27,16 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
targetHandleId,
|
targetHandleId,
|
||||||
style
|
style
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const parentNode = useMemo(() => {
|
||||||
|
for (const node of nodeList) {
|
||||||
|
if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) {
|
||||||
|
return nodeList.find((parent) => parent.nodeId === node.parentNodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [nodeList, source, target]);
|
||||||
|
|
||||||
const defaultZIndex = useMemo(
|
const defaultZIndex = useMemo(
|
||||||
() => (nodeList.find((node) => node.nodeId === source && node.parentNodeId) ? 1001 : 0),
|
() => (nodeList.find((node) => node.nodeId === source && node.parentNodeId) ? 1001 : 0),
|
||||||
[nodeList, source]
|
[nodeList, source]
|
||||||
@@ -127,55 +137,59 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
})();
|
})();
|
||||||
return (
|
return (
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<Flex
|
<Box hidden={parentNode?.isFolded}>
|
||||||
display={isHover || highlightEdge ? 'flex' : 'none'}
|
|
||||||
alignItems={'center'}
|
|
||||||
justifyContent={'center'}
|
|
||||||
position={'absolute'}
|
|
||||||
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
|
|
||||||
pointerEvents={'all'}
|
|
||||||
w={'17px'}
|
|
||||||
h={'17px'}
|
|
||||||
bg={'white'}
|
|
||||||
borderRadius={'17px'}
|
|
||||||
cursor={'pointer'}
|
|
||||||
zIndex={9999}
|
|
||||||
onClick={() => onDelConnect(id)}
|
|
||||||
>
|
|
||||||
<MyIcon name={'core/workflow/closeEdge'} w={'100%'}></MyIcon>
|
|
||||||
</Flex>
|
|
||||||
{!isToolEdge && (
|
|
||||||
<Flex
|
<Flex
|
||||||
|
display={isHover || highlightEdge ? 'flex' : 'none'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
justifyContent={'center'}
|
justifyContent={'center'}
|
||||||
position={'absolute'}
|
position={'absolute'}
|
||||||
transform={arrowTransform}
|
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||||
pointerEvents={'all'}
|
pointerEvents={'all'}
|
||||||
w={highlightEdge ? '14px' : '10px'}
|
w={'17px'}
|
||||||
h={highlightEdge ? '14px' : '10px'}
|
h={'17px'}
|
||||||
// bg={'white'}
|
bg={'white'}
|
||||||
zIndex={highlightEdge ? 1000 : defaultZIndex}
|
borderRadius={'17px'}
|
||||||
|
cursor={'pointer'}
|
||||||
|
zIndex={9999}
|
||||||
|
onClick={() => onDelConnect(id)}
|
||||||
>
|
>
|
||||||
<MyIcon
|
<MyIcon name={'core/workflow/closeEdge'} w={'100%'}></MyIcon>
|
||||||
name={'core/workflow/edgeArrow'}
|
|
||||||
w={'100%'}
|
|
||||||
color={edgeColor}
|
|
||||||
{...(highlightEdge
|
|
||||||
? {
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
: {})}
|
|
||||||
></MyIcon>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
{!isToolEdge && (
|
||||||
|
<Flex
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'center'}
|
||||||
|
position={'absolute'}
|
||||||
|
transform={arrowTransform}
|
||||||
|
pointerEvents={'all'}
|
||||||
|
w={highlightEdge ? '14px' : '10px'}
|
||||||
|
h={highlightEdge ? '14px' : '10px'}
|
||||||
|
// bg={'white'}
|
||||||
|
zIndex={highlightEdge ? 1000 : defaultZIndex}
|
||||||
|
>
|
||||||
|
<MyIcon
|
||||||
|
name={'core/workflow/edgeArrow'}
|
||||||
|
w={'100%'}
|
||||||
|
color={edgeColor}
|
||||||
|
{...(highlightEdge
|
||||||
|
? {
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
></MyIcon>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</EdgeLabelRenderer>
|
</EdgeLabelRenderer>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
|
parentNode?.isFolded,
|
||||||
isHover,
|
isHover,
|
||||||
highlightEdge,
|
highlightEdge,
|
||||||
labelX,
|
labelX,
|
||||||
labelY,
|
labelY,
|
||||||
isToolEdge,
|
isToolEdge,
|
||||||
|
defaultZIndex,
|
||||||
edgeColor,
|
edgeColor,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
newTargetX,
|
newTargetX,
|
||||||
@@ -214,7 +228,8 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
targetY={newTargetY}
|
targetY={newTargetY}
|
||||||
style={{
|
style={{
|
||||||
...edgeStyle,
|
...edgeStyle,
|
||||||
stroke: edgeColor
|
stroke: edgeColor,
|
||||||
|
display: parentNode?.isFolded ? 'none' : 'block'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -227,7 +242,8 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
style,
|
style,
|
||||||
highlightEdge
|
highlightEdge,
|
||||||
|
parentNode?.isFolded
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,5 +1,13 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Background, ControlButton, MiniMap, Panel, useReactFlow, useViewport } from 'reactflow';
|
import {
|
||||||
|
Background,
|
||||||
|
ControlButton,
|
||||||
|
MiniMap,
|
||||||
|
MiniMapNodeProps,
|
||||||
|
Panel,
|
||||||
|
useReactFlow,
|
||||||
|
useViewport
|
||||||
|
} from 'reactflow';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../context';
|
import { WorkflowContext } from '../../context';
|
||||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||||
@@ -26,7 +34,8 @@ const FlowController = React.memo(function FlowController() {
|
|||||||
canUndo,
|
canUndo,
|
||||||
workflowControlMode,
|
workflowControlMode,
|
||||||
setWorkflowControlMode,
|
setWorkflowControlMode,
|
||||||
mouseInCanvas
|
mouseInCanvas,
|
||||||
|
nodeList
|
||||||
} = useContextSelector(WorkflowContext, (v) => v);
|
} = useContextSelector(WorkflowContext, (v) => v);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -52,6 +61,20 @@ const FlowController = React.memo(function FlowController() {
|
|||||||
zoomOut();
|
zoomOut();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MiniMapNode = useCallback(
|
||||||
|
({ x, y, width, height, color, id }: MiniMapNodeProps) => {
|
||||||
|
const node = nodeList.find((node) => node.nodeId === id);
|
||||||
|
const parentNode = nodeList.find((n) => n.nodeId === node?.parentNodeId);
|
||||||
|
|
||||||
|
if (parentNode?.isFolded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <rect x={x} y={y} width={width} height={height} fill={color} />;
|
||||||
|
},
|
||||||
|
[nodeList]
|
||||||
|
);
|
||||||
|
|
||||||
const Render = useMemo(() => {
|
const Render = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -64,6 +87,7 @@ const FlowController = React.memo(function FlowController() {
|
|||||||
boxShadow: '0px 0px 1px rgba(19, 51, 107, 0.10), 0px 4px 10px rgba(19, 51, 107, 0.10)'
|
boxShadow: '0px 0px 1px rgba(19, 51, 107, 0.10), 0px 4px 10px rgba(19, 51, 107, 0.10)'
|
||||||
}}
|
}}
|
||||||
pannable
|
pannable
|
||||||
|
nodeComponent={MiniMapNode}
|
||||||
/>
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
position={'bottom-right'}
|
position={'bottom-right'}
|
||||||
@@ -180,9 +204,10 @@ const FlowController = React.memo(function FlowController() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
|
MiniMapNode,
|
||||||
workflowControlMode,
|
workflowControlMode,
|
||||||
isMac,
|
|
||||||
t,
|
t,
|
||||||
|
isMac,
|
||||||
undo,
|
undo,
|
||||||
canUndo,
|
canUndo,
|
||||||
redo,
|
redo,
|
||||||
|
@@ -13,7 +13,8 @@ import {
|
|||||||
getNodesBounds,
|
getNodesBounds,
|
||||||
Rect,
|
Rect,
|
||||||
NodeRemoveChange,
|
NodeRemoveChange,
|
||||||
NodeSelectionChange
|
NodeSelectionChange,
|
||||||
|
Position
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
@@ -30,6 +31,8 @@ import {
|
|||||||
Input_Template_Node_Width
|
Input_Template_Node_Width
|
||||||
} from '@fastgpt/global/core/workflow/template/input';
|
} from '@fastgpt/global/core/workflow/template/input';
|
||||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||||
|
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
|
import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Compute helper lines for snapping nodes to each other
|
Compute helper lines for snapping nodes to each other
|
||||||
@@ -367,8 +370,8 @@ export const useWorkflow = () => {
|
|||||||
const checkNodeOverLoopNode = useMemoizedFn((node: Node) => {
|
const checkNodeOverLoopNode = useMemoizedFn((node: Node) => {
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
// 获取所有与当前节点相交的节点
|
// 获取所有与当前节点相交的节点,不包含折叠的节点
|
||||||
const intersections = getIntersectingNodes(node);
|
const intersections = getIntersectingNodes(node).filter((node) => !node.data.isFolded);
|
||||||
// 获取所有与当前节点相交的节点中,类型为 loop 的节点
|
// 获取所有与当前节点相交的节点中,类型为 loop 的节点
|
||||||
const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop);
|
const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop);
|
||||||
|
|
||||||
@@ -544,9 +547,19 @@ export const useWorkflow = () => {
|
|||||||
/* connect */
|
/* connect */
|
||||||
const onConnectStart = useCallback(
|
const onConnectStart = useCallback(
|
||||||
(event: any, params: OnConnectStartParams) => {
|
(event: any, params: OnConnectStartParams) => {
|
||||||
|
if (!params.nodeId) return;
|
||||||
|
const sourceNode = nodes.find((node) => node.id === params.nodeId);
|
||||||
|
if (sourceNode?.data.isFolded) {
|
||||||
|
return onChangeNode({
|
||||||
|
nodeId: params.nodeId,
|
||||||
|
type: 'attr',
|
||||||
|
key: 'isFolded',
|
||||||
|
value: false
|
||||||
|
});
|
||||||
|
}
|
||||||
setConnectingEdge(params);
|
setConnectingEdge(params);
|
||||||
},
|
},
|
||||||
[setConnectingEdge]
|
[nodes, setConnectingEdge, onChangeNode]
|
||||||
);
|
);
|
||||||
const onConnectEnd = useCallback(() => {
|
const onConnectEnd = useCallback(() => {
|
||||||
setConnectingEdge(undefined);
|
setConnectingEdge(undefined);
|
||||||
@@ -581,7 +594,7 @@ export const useWorkflow = () => {
|
|||||||
connect
|
connect
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onConnect, t, toast]
|
[onConnect, t, toast, nodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
/* edge */
|
/* edge */
|
||||||
|
@@ -22,7 +22,7 @@ import { WorkflowContext } from '../../../context';
|
|||||||
|
|
||||||
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { nodeId, inputs, outputs } = data;
|
const { nodeId, inputs, outputs, isFolded } = data;
|
||||||
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
|
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
|
||||||
|
|
||||||
const { nodeWidth, nodeHeight } = useMemo(() => {
|
const { nodeWidth, nodeHeight } = useMemo(() => {
|
||||||
@@ -53,14 +53,14 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
return (
|
return (
|
||||||
<NodeCard
|
<NodeCard
|
||||||
selected={selected}
|
selected={selected}
|
||||||
maxW={'full'}
|
maxW="full"
|
||||||
minW={900}
|
{...(!isFolded && {
|
||||||
minH={900}
|
minW: '900px',
|
||||||
w={nodeWidth}
|
minH: '900px',
|
||||||
h={nodeHeight}
|
w: nodeWidth,
|
||||||
menuForbid={{
|
h: nodeHeight
|
||||||
copy: true
|
})}
|
||||||
}}
|
menuForbid={{ copy: true }}
|
||||||
{...data}
|
{...data}
|
||||||
>
|
>
|
||||||
<Container position={'relative'} flex={1}>
|
<Container position={'relative'} flex={1}>
|
||||||
|
@@ -94,17 +94,15 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
<NodeCard
|
<NodeCard
|
||||||
selected={selected}
|
selected={selected}
|
||||||
{...data}
|
{...data}
|
||||||
w={'420px'}
|
|
||||||
h={'176px'}
|
|
||||||
menuForbid={{
|
menuForbid={{
|
||||||
copy: true,
|
copy: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
debug: true
|
debug: true
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box px={4}>
|
<Box px={4} w={'420px'} h={'116px'}>
|
||||||
{!loopItemInputType ? (
|
{!loopItemInputType ? (
|
||||||
<EmptyTip text={t('workflow:loop_start_tip')} py={0} mt={0} iconSize={'32px'} />
|
<EmptyTip text={t('workflow:loop_start_tip')} py={0} mt={4} iconSize={'32px'} />
|
||||||
) : (
|
) : (
|
||||||
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
|
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
|
@@ -56,11 +56,6 @@ const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
value: options.filter((input) => input.key !== item.key)
|
value: options.filter((input) => input.key !== item.key)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onChangeNode({
|
|
||||||
nodeId,
|
|
||||||
type: 'delOutput',
|
|
||||||
key: item.key
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</MyTooltip>
|
</MyTooltip>
|
||||||
|
@@ -6,7 +6,13 @@ import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
|||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../../../context';
|
import { WorkflowContext } from '../../../../context';
|
||||||
|
|
||||||
export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => {
|
export const ConnectionSourceHandle = ({
|
||||||
|
nodeId,
|
||||||
|
isFoldNode
|
||||||
|
}: {
|
||||||
|
nodeId: string;
|
||||||
|
isFoldNode?: boolean;
|
||||||
|
}) => {
|
||||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||||
@@ -26,8 +32,7 @@ export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => {
|
|||||||
const rightTargetConnected = edges.some(
|
const rightTargetConnected = edges.some(
|
||||||
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Right)
|
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Right)
|
||||||
);
|
);
|
||||||
|
if (!isFoldNode && (!node || !node?.sourceHandle?.right || rightTargetConnected)) return null;
|
||||||
if (!node || !node?.sourceHandle?.right || rightTargetConnected) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SourceHandle
|
<SourceHandle
|
||||||
@@ -102,7 +107,7 @@ export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => {
|
|||||||
TopHandlee,
|
TopHandlee,
|
||||||
BottomHandlee
|
BottomHandlee
|
||||||
};
|
};
|
||||||
}, [connectingEdge, edges, nodeId, nodeList]);
|
}, [connectingEdge, edges, nodeId, nodeList, isFoldNode]);
|
||||||
|
|
||||||
return showSourceHandle ? (
|
return showSourceHandle ? (
|
||||||
<>
|
<>
|
||||||
|
@@ -32,8 +32,8 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
|||||||
|
|
||||||
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
|
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
|
||||||
const connected = edges.some((edge) => edge.sourceHandle === handleId);
|
const connected = edges.some((edge) => edge.sourceHandle === handleId);
|
||||||
|
const nodeFolded = node?.data.isFolded && edges.some((edge) => edge.source === nodeId);
|
||||||
const nodeIsHover = hoverNodeId === nodeId;
|
const nodeIsHover = hoverNodeId === nodeId;
|
||||||
|
|
||||||
const active = useMemo(
|
const active = useMemo(
|
||||||
() => nodeIsHover || node?.selected || connectingEdge?.handleId === handleId,
|
() => nodeIsHover || node?.selected || connectingEdge?.handleId === handleId,
|
||||||
[nodeIsHover, node?.selected, connectingEdge, handleId]
|
[nodeIsHover, node?.selected, connectingEdge, handleId]
|
||||||
@@ -73,7 +73,7 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connected) {
|
if (connected || nodeFolded) {
|
||||||
return {
|
return {
|
||||||
styles: {
|
styles: {
|
||||||
...connectedStyle,
|
...connectedStyle,
|
||||||
@@ -89,7 +89,7 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
|||||||
styles: undefined,
|
styles: undefined,
|
||||||
showAddIcon: false
|
showAddIcon: false
|
||||||
};
|
};
|
||||||
}, [active, connected, highlightStyle, translateStr, transform, connectedStyle]);
|
}, [active, connected, nodeFolded, highlightStyle, translateStr, transform, connectedStyle]);
|
||||||
|
|
||||||
const RenderHandle = useMemo(() => {
|
const RenderHandle = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Box, Button, Card, Flex, Image } from '@chakra-ui/react';
|
import { Box, Button, Card, Flex, Image } from '@chakra-ui/react';
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||||
@@ -61,7 +61,8 @@ const NodeCard = (props: Props) => {
|
|||||||
menuForbid,
|
menuForbid,
|
||||||
isTool = false,
|
isTool = false,
|
||||||
isError = false,
|
isError = false,
|
||||||
debugResult
|
debugResult,
|
||||||
|
isFolded
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
@@ -81,7 +82,12 @@ const NodeCard = (props: Props) => {
|
|||||||
[isTool, nodeList]
|
[isTool, nodeList]
|
||||||
);
|
);
|
||||||
|
|
||||||
const node = nodeList.find((node) => node.nodeId === nodeId);
|
const { node, parentNode } = useMemo(() => {
|
||||||
|
const node = nodeList.find((node) => node.nodeId === nodeId);
|
||||||
|
const parentNode = nodeList.find((n) => n.nodeId === node?.parentNodeId);
|
||||||
|
return { node, parentNode };
|
||||||
|
}, [nodeList, nodeId]);
|
||||||
|
|
||||||
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
|
const { openConfirm: onOpenConfirmSync, ConfirmModal: ConfirmSyncModal } = useConfirm({
|
||||||
content: t('app:module.Confirm Sync')
|
content: t('app:module.Confirm Sync')
|
||||||
});
|
});
|
||||||
@@ -155,6 +161,31 @@ const NodeCard = (props: Props) => {
|
|||||||
|
|
||||||
{/* avatar and name */}
|
{/* avatar and name */}
|
||||||
<Flex alignItems={'center'}>
|
<Flex alignItems={'center'}>
|
||||||
|
<Box
|
||||||
|
mr={2}
|
||||||
|
cursor={'pointer'}
|
||||||
|
rounded={'sm'}
|
||||||
|
_hover={{ bg: 'myGray.200' }}
|
||||||
|
onClick={() => {
|
||||||
|
onChangeNode({
|
||||||
|
nodeId,
|
||||||
|
type: 'attr',
|
||||||
|
key: 'isFolded',
|
||||||
|
value: !isFolded
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isFolded ? (
|
||||||
|
<MyIcon name={'core/chat/chevronDown'} w={'24px'} h={'24px'} color={'myGray.500'} />
|
||||||
|
) : (
|
||||||
|
<MyIcon
|
||||||
|
name={'core/chat/chevronRight'}
|
||||||
|
w={'24px'}
|
||||||
|
h={'24px'}
|
||||||
|
color={'myGray.500'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
<Avatar src={avatar} borderRadius={'sm'} objectFit={'contain'} w={'30px'} h={'30px'} />
|
<Avatar src={avatar} borderRadius={'sm'} objectFit={'contain'} w={'30px'} h={'30px'} />
|
||||||
<Box ml={3} fontSize={'md'} fontWeight={'medium'}>
|
<Box ml={3} fontSize={'md'} fontWeight={'medium'}>
|
||||||
{t(name as any)}
|
{t(name as any)}
|
||||||
@@ -250,19 +281,21 @@ const NodeCard = (props: Props) => {
|
|||||||
ConfirmSyncModal,
|
ConfirmSyncModal,
|
||||||
onOpenCustomTitleModal,
|
onOpenCustomTitleModal,
|
||||||
onChangeNode,
|
onChangeNode,
|
||||||
toast
|
toast,
|
||||||
|
isFolded
|
||||||
]);
|
]);
|
||||||
const RenderHandle = useMemo(() => {
|
const RenderHandle = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConnectionSourceHandle nodeId={nodeId} />
|
<ConnectionSourceHandle nodeId={nodeId} isFoldNode={isFolded} />
|
||||||
<ConnectionTargetHandle nodeId={nodeId} />
|
<ConnectionTargetHandle nodeId={nodeId} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [nodeId]);
|
}, [nodeId, isFolded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
hidden={parentNode?.isFolded}
|
||||||
flexDirection={'column'}
|
flexDirection={'column'}
|
||||||
minW={minW}
|
minW={minW}
|
||||||
maxW={maxW}
|
maxW={maxW}
|
||||||
@@ -298,7 +331,7 @@ const NodeCard = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
|
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
|
||||||
{Header}
|
{Header}
|
||||||
{children}
|
{!isFolded && children}
|
||||||
{RenderHandle}
|
{RenderHandle}
|
||||||
|
|
||||||
<EditTitleModal maxLength={20} />
|
<EditTitleModal maxLength={20} />
|
||||||
|
@@ -561,9 +561,9 @@ const WorkflowContextProvider = ({
|
|||||||
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||||
|
|
||||||
if (!checkResults) {
|
if (!checkResults) {
|
||||||
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
|
const storeWorkflow = uiWorkflow2StoreWorkflow({ nodes, edges });
|
||||||
|
|
||||||
return storeNodes;
|
return storeWorkflow;
|
||||||
} else if (!hideTip) {
|
} else if (!hideTip) {
|
||||||
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
|
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
|
||||||
toast({
|
toast({
|
||||||
|
@@ -25,7 +25,8 @@ export const uiWorkflow2StoreWorkflow = ({
|
|||||||
version: item.data.version,
|
version: item.data.version,
|
||||||
inputs: item.data.inputs,
|
inputs: item.data.inputs,
|
||||||
outputs: item.data.outputs,
|
outputs: item.data.outputs,
|
||||||
pluginId: item.data.pluginId
|
pluginId: item.data.pluginId,
|
||||||
|
isFolded: item.data.isFolded
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// get all handle
|
// get all handle
|
||||||
@@ -49,6 +50,8 @@ export const uiWorkflow2StoreWorkflow = ({
|
|||||||
(item) => {
|
(item) => {
|
||||||
// Not in react flow page
|
// Not in react flow page
|
||||||
if (!reactFlowViewport) return true;
|
if (!reactFlowViewport) return true;
|
||||||
|
const currentSourceNode = nodes.find((node) => node.data.nodeId === item.source);
|
||||||
|
if (currentSourceNode?.data.isFolded) return true;
|
||||||
return handleIdList.includes(item.sourceHandle) && handleIdList.includes(item.targetHandle);
|
return handleIdList.includes(item.sourceHandle) && handleIdList.includes(item.targetHandle);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -532,7 +532,8 @@ export const compareSnapshot = (
|
|||||||
name: node.data.name,
|
name: node.data.name,
|
||||||
intro: node.data.intro,
|
intro: node.data.intro,
|
||||||
avatar: node.data.avatar,
|
avatar: node.data.avatar,
|
||||||
version: node.data.version
|
version: node.data.version,
|
||||||
|
isFolded: node.data.isFolded
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user