fix(fixed-layout): multi-outputs/multi-inputs collapsed and move branches (#249)

* feat(demo): demo-fixed-layout-simple add tryCatch node

* feat(demo): use-editor-props add fromNodeJSON/toNodeJSON config

* fix(demo): demo-fixed-layout-simple readonly refresh

* fix(fixed-layout): multi-outputs collapsed and move branches

* chore: update codeowners

* fix(fixed-layout): multi-inputs branch adder

* test(fixed-layout-core): test snapshots update

* test(fixed-layout-editor): move block to other dynamicSplit
This commit is contained in:
xiamidaxia 2025-05-21 18:06:41 +08:00 committed by GitHub
parent 245bee3389
commit 92b3adc5d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 410 additions and 100 deletions

4
.github/CODEOWNERS vendored
View File

@ -1,9 +1,9 @@
# 文件路径与代码负责人分配 # 文件路径与代码负责人分配
# 对整个仓库设置代码负责人 # 对整个仓库设置代码负责人
* @xiamidaxia @luics @dragooncjw * @xiamidaxia @luics @dragooncjw @YuanHeDx @sanmaopep @louisyoungx
# 对特定目录设置代码负责人 # 对特定目录设置代码负责人
/apps/docs/ @xiamidaxia @dragooncjw @YuanHeDx @sanmaopep /apps/docs/ @xiamidaxia @dragooncjw @YuanHeDx @sanmaopep @louisyoungx
/apps/demo-node-form/ @xiamidaxia @dragooncjw @YuanHeDx /apps/demo-node-form/ @xiamidaxia @dragooncjw @YuanHeDx
/packages/node-engine/ @xiamidaxia @dragooncjw @YuanHeDx /packages/node-engine/ @xiamidaxia @dragooncjw @YuanHeDx
/packages/variable-engine/ @xiamidaxia @dragooncjw @sanmaopep /packages/variable-engine/ @xiamidaxia @dragooncjw @sanmaopep

View File

@ -38,10 +38,12 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}), ...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),
}} }}
> >
<IconDeleteStroked {!nodeRender.readonly && (
style={{ position: 'absolute', right: 4, top: 4 }} <IconDeleteStroked
onClick={() => ctx.operation.deleteNode(nodeRender.node)} style={{ position: 'absolute', right: 4, top: 4 }}
/> onClick={() => ctx.operation.deleteNode(nodeRender.node)}
/>
)}
{form?.render()} {form?.render()}
</div> </div>
); );

View File

@ -25,6 +25,15 @@ export function BranchAdder(props: PropsType) {
content: '', content: '',
}, },
}); });
} else if (node.flowNodeType === 'multiInputs') {
block = operation.addBlock(node, {
id: `input_${nanoid(5)}`,
type: 'input',
data: {
title: 'New Input',
content: '',
},
});
} else { } else {
block = operation.addBlock(node, { block = operation.addBlock(node, {
id: `branch_${nanoid(5)}`, id: `branch_${nanoid(5)}`,
@ -43,6 +52,7 @@ export function BranchAdder(props: PropsType) {
}); });
}, 10); }, 10);
} }
if (playground.config.readonlyOrDisabled) return null; if (playground.config.readonlyOrDisabled) return null;
const className = [ const className = [

View File

@ -13,9 +13,10 @@ export function FlowSelect() {
const targetDemoJSON = FLOW_LIST[demoKey]; const targetDemoJSON = FLOW_LIST[demoKey];
if (targetDemoJSON) { if (targetDemoJSON) {
clientContext.history.stop(); // Stop redo/undo clientContext.history.stop(); // Stop redo/undo
clientContext.document.fromJSON(targetDemoJSON); clientContext.history.clear(); // Clear redo/undo
console.log(clientContext.document.toString()); clientContext.document.fromJSON(targetDemoJSON); // Reload Data
clientContext.history.start(); console.log(clientContext.document.toString()); // Print the document tree
clientContext.history.start(); // Restart redo/undo
clientContext.document.setLayout( clientContext.document.setLayout(
targetDemoJSON.defaultLayout || FlowLayoutDefault.VERTICAL_FIXED_LAYOUT targetDemoJSON.defaultLayout || FlowLayoutDefault.VERTICAL_FIXED_LAYOUT
); );
@ -26,7 +27,7 @@ export function FlowSelect() {
} }
// Fit View // Fit View
setTimeout(() => { setTimeout(() => {
clientContext.playground.config.fitView(clientContext.document.root.bounds); clientContext.playground.config.fitView(clientContext.document.root.bounds, true, 40);
}, 20); }, 20);
} }
}, [demoKey]); }, [demoKey]);

View File

@ -1,12 +1,13 @@
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor'; import { usePlaygroundTools, useClientContext, useRefresh } from '@flowgram.ai/fixed-layout-editor';
import { IconButton, Space } from '@douyinfe/semi-ui'; import { IconButton, Space } from '@douyinfe/semi-ui';
import { IconUnlock, IconLock } from '@douyinfe/semi-icons'; import { IconUnlock, IconLock } from '@douyinfe/semi-icons';
export function Tools() { export function Tools() {
const { history, playground } = useClientContext(); const { history, playground } = useClientContext();
const tools = usePlaygroundTools(); const tools = usePlaygroundTools();
const refresh = useRefresh();
const [canUndo, setCanUndo] = useState(false); const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false); const [canRedo, setCanRedo] = useState(false);
const toggleReadonly = useCallback(() => { const toggleReadonly = useCallback(() => {
@ -21,6 +22,11 @@ export function Tools() {
return () => disposable.dispose(); return () => disposable.dispose();
}, [history]); }, [history]);
useEffect(() => {
const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh());
return () => disposable.dispose();
}, [playground]);
return ( return (
<Space <Space
style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }} style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}

View File

@ -1,9 +1,11 @@
import { FlowDocumentJSON, FlowLayoutDefault } from '@flowgram.ai/fixed-layout-editor'; import { FlowDocumentJSON, FlowLayoutDefault } from '@flowgram.ai/fixed-layout-editor';
import { tryCatch } from './trycatch';
import { mindmap } from './mindmap'; import { mindmap } from './mindmap';
import { condition } from './condition'; import { condition } from './condition';
export const FLOW_LIST: Record<string, FlowDocumentJSON & { defaultLayout?: FlowLayoutDefault }> = { export const FLOW_LIST: Record<string, FlowDocumentJSON & { defaultLayout?: FlowLayoutDefault }> = {
condition, condition,
mindmap: { ...mindmap, defaultLayout: FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT }, mindmap: { ...mindmap, defaultLayout: FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT },
tryCatch,
}; };

View File

@ -20,6 +20,13 @@ export const mindmap: FlowDocumentJSON = {
title: 'input_1', title: 'input_1',
}, },
}, },
{
id: 'input_3',
type: 'input',
data: {
title: 'input_3',
},
},
// { // {
// id: 'multiInputs_2', // id: 'multiInputs_2',
// type: 'multiInputs', // type: 'multiInputs',

View File

@ -0,0 +1,56 @@
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
export const tryCatch: FlowDocumentJSON = {
nodes: [
// 开始节点
{
id: 'start_0',
type: 'start',
data: {
title: 'Start',
content: 'start content',
},
blocks: [],
},
// 分支节点
{
id: 'tryCatch_0',
type: 'tryCatch',
data: {
title: 'TryCatch',
},
blocks: [
{
id: 'tryBlock_0',
type: 'tryBlock',
blocks: [],
},
{
id: 'catchBlock_0',
type: 'catchBlock',
data: {
title: 'Catch Block 1',
},
blocks: [],
},
{
id: 'catchBlock_1',
type: 'catchBlock',
data: {
title: 'Catch Block 2',
},
blocks: [],
},
],
},
// 结束节点
{
id: 'end_0',
type: 'end',
data: {
title: 'End',
content: 'end content',
},
},
],
};

View File

@ -6,12 +6,16 @@ import './index.css';
import { nodeRegistries } from './node-registries'; import { nodeRegistries } from './node-registries';
import { initialData } from './initial-data'; import { initialData } from './initial-data';
import { useEditorProps } from './hooks/use-editor-props'; import { useEditorProps } from './hooks/use-editor-props';
import { FLOW_LIST } from './data';
import { Tools } from './components/tools'; import { Tools } from './components/tools';
import { Minimap } from './components/minimap'; import { Minimap } from './components/minimap';
import { FlowSelect } from './components/flow-select'; import { FlowSelect } from './components/flow-select';
export const Editor = () => { export const Editor = (props: { demoKey?: string }) => {
const editorProps = useEditorProps(initialData, nodeRegistries); const editorProps = useEditorProps(
props.demoKey ? FLOW_LIST[props.demoKey] : initialData,
nodeRegistries
);
return ( return (
<FixedLayoutEditorProvider {...editorProps}> <FixedLayoutEditorProvider {...editorProps}>
<div className="demo-fixed-container"> <div className="demo-fixed-container">

View File

@ -106,6 +106,7 @@ export function useEditorProps(
}, },
}, },
/** /**
* Playground init
* *
*/ */
onInit: (ctx) => { onInit: (ctx) => {
@ -117,11 +118,30 @@ export function useEditorProps(
console.log('---- Playground Init ----'); console.log('---- Playground Init ----');
}, },
/** /**
* Playground dispose
* *
*/ */
onDispose: () => { onDispose: () => {
console.log('---- Playground Dispose ----'); console.log('---- Playground Dispose ----');
}, },
/**
* , ctx.document.fromJSON
* Node data transformation, called by ctx.document.fromJSON
* @param node
* @param json
*/
fromNodeJSON(node, json) {
return json;
},
/**
* , ctx.document.toJSON
* Node data transformation, called by ctx.document.toJSON
* @param node
* @param json
*/
toNodeJSON(node, json) {
return json;
},
plugins: () => [ plugins: () => [
/** /**
* Minimap plugin * Minimap plugin

View File

@ -1,9 +1,5 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { import { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
FlowNodeRegistry,
FlowNodeEntity,
FlowNodeBaseType,
} from '@flowgram.ai/fixed-layout-editor';
/** /**
* *
@ -77,35 +73,4 @@ export const nodeRegistries: FlowNodeRegistry[] = [
}; };
}, },
}, },
{
type: 'multiStart2',
extend: 'dynamicSplit',
meta: {
isStart: true,
},
onCreate(node, json) {
const doc = node.document;
const addedNodes: FlowNodeEntity[] = [];
const blocks = json.blocks || [];
if (blocks.length > 0) {
// 水平布局
const inlineBlocksNode = doc.addNode({
id: `$inlineBlocks$${node.id}`,
type: FlowNodeBaseType.INLINE_BLOCKS,
originParent: node,
parent: node,
});
addedNodes.push(inlineBlocksNode);
blocks.forEach((blockData) => {
doc.addBlock(node, blockData, addedNodes);
});
}
return addedNodes;
},
},
{
type: 'tree',
extend: 'simpleSplit',
},
]; ];

View File

@ -63,6 +63,24 @@ export function useEditorProps(
}, },
}; };
}, },
/**
* , ctx.document.fromJSON
* Node data transformation, called by ctx.document.fromJSON
* @param node
* @param json
*/
fromNodeJSON(node, json) {
return json;
},
/**
* , ctx.document.toJSON
* Node data transformation, called by ctx.document.toJSON
* @param node
* @param json
*/
toNodeJSON(node, json) {
return json;
},
/** /**
* Set default layout * Set default layout
*/ */

View File

@ -34,6 +34,24 @@ export const useEditorProps = () =>
* *
*/ */
nodeRegistries, nodeRegistries,
/**
* , ctx.document.fromJSON
* Node data transformation, called by ctx.document.fromJSON
* @param node
* @param json
*/
fromNodeJSON(node, json) {
return json;
},
/**
* , ctx.document.toJSON
* Node data transformation, called by ctx.document.toJSON
* @param node
* @param json
*/
toNodeJSON(node, json) {
return json;
},
/** /**
* Get the default node registry, which will be merged with the 'nodeRegistries' * Get the default node registry, which will be merged with the 'nodeRegistries'
* nodeRegistries * nodeRegistries

View File

@ -57,6 +57,24 @@ export function useEditorProps(
formMeta: defaultFormMeta, formMeta: defaultFormMeta,
}; };
}, },
/**
* , ctx.document.fromJSON
* Node data transformation, called by ctx.document.fromJSON
* @param node
* @param json
*/
fromNodeJSON(node, json) {
return json;
},
/**
* , ctx.document.toJSON
* Node data transformation, called by ctx.document.toJSON
* @param node
* @param json
*/
toNodeJSON(node, json) {
return json;
},
lineColor: { lineColor: {
hidden: 'transparent', hidden: 'transparent',
default: '#4d53e8', default: '#4d53e8',

View File

@ -344,20 +344,17 @@ export class FlowDocument<T = FlowDocumentJSON> implements Disposable {
parent: node, parent: node,
}); });
addedNodes.push(blockIconNode); addedNodes.push(blockIconNode);
// inlineblocks 为空则不创建 // 水平布局
if (blocks.length > 0) { const inlineBlocksNode = this.addNode({
// 水平布局 id: `$inlineBlocks$${node.id}`,
const inlineBlocksNode = this.addNode({ type: FlowNodeBaseType.INLINE_BLOCKS,
id: `$inlineBlocks$${node.id}`, originParent: node,
type: FlowNodeBaseType.INLINE_BLOCKS, parent: node,
originParent: node, });
parent: node, addedNodes.push(inlineBlocksNode);
}); blocks.forEach((blockData) => {
addedNodes.push(inlineBlocksNode); this.addBlock(node, blockData, addedNodes);
blocks.forEach((blockData) => { });
this.addBlock(node, blockData, addedNodes);
});
}
return addedNodes; return addedNodes;
} }

View File

@ -29,6 +29,7 @@ export interface FlowTransitionLine {
arrow?: boolean; // 是否有箭头 arrow?: boolean; // 是否有箭头
renderKey?: string; // 只有自定义线条需要 renderKey?: string; // 只有自定义线条需要
isHorizontal?: boolean; // 是否为水平布局 isHorizontal?: boolean; // 是否为水平布局
isDraggingLine?: boolean; // 是否是拖拽线条
activated?: boolean; // 是否激活态 activated?: boolean; // 是否激活态
side?: LABEL_SIDE_TYPE; // 区分是否分支前缀线条 side?: LABEL_SIDE_TYPE; // 区分是否分支前缀线条
style?: React.CSSProperties; style?: React.CSSProperties;

View File

@ -118,6 +118,7 @@ exports[`flow-activities > extend block addChild 1`] = `
|-------- $blockOrderIcon$test-extend-block |-------- $blockOrderIcon$test-extend-block
|-- empty-split |-- empty-split
|---- $blockIcon$empty-split |---- $blockIcon$empty-split
|---- $inlineBlocks$empty-split
|-- end_0" |-- end_0"
`; `;

View File

@ -38,8 +38,13 @@ export const BlockRegistry: FlowNodeRegistry = {
// 当有其余分支的时候,绘制一条两个分支之间的线条 // 当有其余分支的时候,绘制一条两个分支之间的线条
if (hasBranchDraggingAdder) { if (hasBranchDraggingAdder) {
if (isVertical) { if (isVertical) {
const currentOffsetRightX = currentTransform.firstChild?.bounds?.right || 0; const currentOffsetRightX = currentTransform.firstChild
const nextOffsetLeftX = currentTransform.next?.firstChild?.bounds?.left || 0; ? currentTransform.firstChild.bounds.right
: currentTransform.bounds.right;
const nextOffsetLeftX =
(currentTransform.next?.firstChild
? currentTransform.next?.firstChild.bounds?.left
: currentTransform.next?.bounds?.left) || 0;
const currentInputPointY = currentTransform.inputPoint.y; const currentInputPointY = currentTransform.inputPoint.y;
if (currentTransform?.next) { if (currentTransform?.next) {
lines.push({ lines.push({
@ -53,8 +58,13 @@ export const BlockRegistry: FlowNodeRegistry = {
}); });
} }
} else { } else {
const currentOffsetRightY = currentTransform.firstChild?.bounds?.bottom || 0; const currentOffsetBottomX = currentTransform.firstChild
const nextOffsetLeftY = currentTransform.next?.firstChild?.bounds?.top || 0; ? currentTransform.firstChild.bounds.bottom
: currentTransform.bounds.bottom;
const nextOffsetTopX =
(currentTransform.next?.firstChild
? currentTransform.next?.firstChild.bounds?.top
: currentTransform.next?.bounds?.top) || 0;
const currentInputPointX = currentTransform.inputPoint.x; const currentInputPointX = currentTransform.inputPoint.x;
if (currentTransform?.next) { if (currentTransform?.next) {
lines.push({ lines.push({
@ -62,7 +72,7 @@ export const BlockRegistry: FlowNodeRegistry = {
from: currentTransform.parent!.inputPoint, from: currentTransform.parent!.inputPoint,
to: { to: {
x: currentInputPointX, x: currentInputPointX,
y: (currentOffsetRightY + nextOffsetLeftY) / 2, y: (currentOffsetBottomX + nextOffsetTopX) / 2,
}, },
side: LABEL_SIDE_TYPE.NORMAL_BRANCH, side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
}); });
@ -112,8 +122,13 @@ export const BlockRegistry: FlowNodeRegistry = {
// 获取两个分支节点中间点作为拖拽标签插入位置 // 获取两个分支节点中间点作为拖拽标签插入位置
if (hasBranchDraggingAdder) { if (hasBranchDraggingAdder) {
if (isVertical) { if (isVertical) {
const currentOffsetRightX = currentTransform.firstChild?.bounds?.right || 0; const currentOffsetRightX = currentTransform.firstChild
const nextOffsetLeftX = currentTransform.next?.firstChild?.bounds?.left || 0; ? currentTransform.firstChild.bounds.right
: currentTransform.bounds.right;
const nextOffsetLeftX =
(currentTransform.next?.firstChild
? currentTransform.next.firstChild.bounds?.left
: currentTransform.next?.bounds?.left) || 0;
const currentInputPointY = currentTransform.inputPoint.y; const currentInputPointY = currentTransform.inputPoint.y;
if (currentTransform?.next) { if (currentTransform?.next) {
draggingLabel.push({ draggingLabel.push({
@ -129,17 +144,22 @@ export const BlockRegistry: FlowNodeRegistry = {
}); });
} }
} else { } else {
const currentOffsetRightY = currentTransform.firstChild?.bounds?.bottom || 0; const currentOffsetBottomX = currentTransform.firstChild
const nextOffsetLeftY = currentTransform.next?.firstChild?.bounds?.top || 0; ? currentTransform.firstChild.bounds.bottom
: currentTransform.bounds.bottom;
const nextOffsetTopX =
(currentTransform.next?.firstChild
? currentTransform.next.firstChild.bounds?.top
: currentTransform.next?.bounds?.top) || 0;
const currentInputPointX = currentTransform.inputPoint.x; const currentInputPointX = currentTransform.inputPoint.x;
if (currentTransform?.next) { if (currentTransform?.next) {
draggingLabel.push({ draggingLabel.push({
offset: { offset: {
x: currentInputPointX, x: currentInputPointX,
y: (currentOffsetRightY + nextOffsetLeftY) / 2, y: (currentOffsetBottomX + nextOffsetTopX) / 2,
}, },
type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL, type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,
width: nextOffsetLeftY - currentOffsetRightY, width: nextOffsetTopX - currentOffsetBottomX,
props: { props: {
side: LABEL_SIDE_TYPE.NORMAL_BRANCH, side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
}, },

View File

@ -4,6 +4,7 @@ import {
type FlowTransitionLine, type FlowTransitionLine,
FlowTransitionLineEnum, FlowTransitionLineEnum,
LABEL_SIDE_TYPE, LABEL_SIDE_TYPE,
FlowTransitionLabelEnum,
} from '@flowgram.ai/document'; } from '@flowgram.ai/document';
/** /**
@ -15,7 +16,7 @@ export const InputRegistry: FlowNodeRegistry = {
meta: { meta: {
hidden: false, hidden: false,
}, },
getLines(transition) { getLines(transition, layout) {
const currentTransform = transition.transform; const currentTransform = transition.transform;
const { isVertical } = transition.entity; const { isVertical } = transition.entity;
const lines: FlowTransitionLine[] = []; const lines: FlowTransitionLine[] = [];
@ -27,32 +28,44 @@ export const InputRegistry: FlowNodeRegistry = {
// 当有其余分支的时候,绘制一条两个分支之间的线条 // 当有其余分支的时候,绘制一条两个分支之间的线条
if (hasBranchDraggingAdder) { if (hasBranchDraggingAdder) {
if (isVertical) { if (isVertical) {
const currentOffsetRightX = currentTransform.firstChild?.bounds?.right || 0; const currentOffsetRightX = currentTransform.firstChild
const nextOffsetLeftX = currentTransform.next?.firstChild?.bounds?.left || 0; ? currentTransform.firstChild.bounds.right
const currentInputPointY = currentTransform.inputPoint.y; : currentTransform.bounds.right;
const nextOffsetLeftX =
(currentTransform.next?.firstChild
? currentTransform.next?.firstChild.bounds?.left
: currentTransform.next?.bounds?.left) || 0;
const currentInputPointY = currentTransform.outputPoint.y;
if (currentTransform?.next) { if (currentTransform?.next) {
lines.push({ lines.push({
type: FlowTransitionLineEnum.DRAGGING_LINE, type: FlowTransitionLineEnum.MERGE_LINE,
from: currentTransform.parent!.inputPoint, isDraggingLine: true,
to: { from: {
x: (currentOffsetRightX + nextOffsetLeftX) / 2, x: (currentOffsetRightX + nextOffsetLeftX) / 2,
y: currentInputPointY, y: currentInputPointY,
}, },
to: currentTransform.parent!.outputPoint,
side: LABEL_SIDE_TYPE.NORMAL_BRANCH, side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
}); });
} }
} else { } else {
const currentOffsetRightY = currentTransform.firstChild?.bounds?.bottom || 0; const currentOffsetBottomX = currentTransform.firstChild
const nextOffsetLeftY = currentTransform.next?.firstChild?.bounds?.top || 0; ? currentTransform.firstChild.bounds.bottom
const currentInputPointX = currentTransform.inputPoint.x; : currentTransform.bounds.bottom;
const nextOffsetTopX =
(currentTransform.next?.firstChild
? currentTransform.next?.firstChild.bounds?.top
: currentTransform.next?.bounds?.top) || 0;
const currentInputPointX = currentTransform.outputPoint.x;
if (currentTransform?.next) { if (currentTransform?.next) {
lines.push({ lines.push({
type: FlowTransitionLineEnum.DRAGGING_LINE, type: FlowTransitionLineEnum.MERGE_LINE,
from: currentTransform.parent!.inputPoint, isDraggingLine: true,
to: { from: {
x: currentInputPointX, x: currentInputPointX,
y: (currentOffsetRightY + nextOffsetLeftY) / 2, y: (currentOffsetBottomX + nextOffsetTopX) / 2,
}, },
to: currentTransform.parent!.outputPoint,
side: LABEL_SIDE_TYPE.NORMAL_BRANCH, side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
}); });
} }
@ -71,7 +84,64 @@ export const InputRegistry: FlowNodeRegistry = {
return lines; return lines;
}, },
getLabels() { getLabels(transition) {
return []; const currentTransform = transition.transform;
const { isVertical } = transition.entity;
const draggingLabel = [];
const hasBranchDraggingAdder =
currentTransform && currentTransform.entity.isInlineBlock && transition.renderData.draggable;
// 获取两个分支节点中间点作为拖拽标签插入位置
if (hasBranchDraggingAdder) {
if (isVertical) {
const currentOffsetRightX = currentTransform.firstChild
? currentTransform.firstChild.bounds.right
: currentTransform.bounds.right;
const nextOffsetLeftX =
(currentTransform.next?.firstChild
? currentTransform.next.firstChild.bounds?.left
: currentTransform.next?.bounds?.left) || 0;
const currentInputPointY = currentTransform.outputPoint.y;
if (currentTransform?.next) {
draggingLabel.push({
offset: {
x: (currentOffsetRightX + nextOffsetLeftX) / 2,
y: currentInputPointY,
},
type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,
width: nextOffsetLeftX - currentOffsetRightX,
props: {
side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
},
});
}
} else {
const currentOffsetBottomX = currentTransform.firstChild
? currentTransform.firstChild.bounds.bottom
: currentTransform.bounds.bottom;
const nextOffsetTopX =
(currentTransform.next?.firstChild
? currentTransform.next.firstChild.bounds?.top
: currentTransform.next?.bounds?.top) || 0;
const currentInputPointX = currentTransform.outputPoint.x;
if (currentTransform?.next) {
draggingLabel.push({
offset: {
x: currentInputPointX,
y: (currentOffsetBottomX + nextOffsetTopX) / 2,
},
type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,
width: nextOffsetTopX - currentOffsetBottomX,
props: {
side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
},
});
}
}
}
return [...draggingLabel];
}, },
}; };

View File

@ -1,4 +1,14 @@
import { FlowNodeBaseType, FlowNodeSplitType, type FlowNodeRegistry } from '@flowgram.ai/document'; import { Point } from '@flowgram.ai/utils';
import { FlowRendererKey } from '@flowgram.ai/renderer';
import {
FlowNodeBaseType,
type FlowNodeRegistry,
FlowNodeRenderData,
FlowTransitionLabelEnum,
FlowNodeSplitType,
getDefaultSpacing,
ConstantKeys,
} from '@flowgram.ai/document';
/** /**
* , * ,
@ -15,6 +25,7 @@ export const MultiInputsRegistry: FlowNodeRegistry = {
type: FlowNodeBaseType.BLOCK_ICON, type: FlowNodeBaseType.BLOCK_ICON,
meta: { meta: {
hidden: true, hidden: true,
spacing: 0,
}, },
getLines() { getLines() {
return []; return [];
@ -25,8 +36,54 @@ export const MultiInputsRegistry: FlowNodeRegistry = {
}, },
{ {
type: FlowNodeBaseType.INLINE_BLOCKS, type: FlowNodeBaseType.INLINE_BLOCKS,
getLabels() { meta: {
return []; inlineSpacingPre: 0,
},
getLabels(transition) {
const isVertical = transition.entity.isVertical;
const currentTransform = transition.transform;
const spacing = getDefaultSpacing(
transition.entity,
ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM
);
if (currentTransform.collapsed || transition.entity.childrenLength === 0) {
return [
{
type: FlowTransitionLabelEnum.CUSTOM_LABEL,
renderKey: FlowRendererKey.BRANCH_ADDER,
offset: Point.move(
currentTransform.outputPoint,
isVertical ? { y: spacing } : { x: spacing }
),
props: {
// 激活状态
activated: transition.entity.getData(FlowNodeRenderData)!.activated,
transform: currentTransform,
// 传给外部使用的 node 信息
node: currentTransform.originParent?.entity,
},
},
];
}
return [
{
type: FlowTransitionLabelEnum.CUSTOM_LABEL,
renderKey: FlowRendererKey.BRANCH_ADDER,
offset: Point.move(
currentTransform.outputPoint,
isVertical ? { y: -spacing / 2 } : { x: -spacing / 2 }
),
props: {
// 激活状态
activated: transition.entity.getData(FlowNodeRenderData)!.activated,
transform: currentTransform,
// 传给外部使用的 node 信息
node: currentTransform.originParent?.entity,
},
},
];
}, },
}, },
], ],

View File

@ -1,5 +1,11 @@
import { FlowNodeBaseType, type FlowNodeRegistry, FlowNodeSplitType } from '@flowgram.ai/document'; import {
FlowLayoutDefault,
type FlowNodeRegistry,
FlowNodeSplitType,
FlowNodeBaseType,
} from '@flowgram.ai/document';
import { DynamicSplitRegistry } from './dynamic-split';
import { BlockRegistry } from './block'; import { BlockRegistry } from './block';
/** /**
@ -13,10 +19,42 @@ import { BlockRegistry } from './block';
export const MultiOuputsRegistry: FlowNodeRegistry = { export const MultiOuputsRegistry: FlowNodeRegistry = {
type: FlowNodeBaseType.MULTI_OUTPUTS, type: FlowNodeBaseType.MULTI_OUTPUTS,
extend: FlowNodeSplitType.SIMPLE_SPLIT, extend: FlowNodeSplitType.SIMPLE_SPLIT,
meta: {
isNodeEnd: true,
},
getLines: (transition, layout) => { getLines: (transition, layout) => {
// 嵌套在 mutliOutputs 下边
if (transition.entity.parent?.flowNodeType === FlowNodeBaseType.INLINE_BLOCKS) { if (transition.entity.parent?.flowNodeType === FlowNodeBaseType.INLINE_BLOCKS) {
return BlockRegistry.getLines!(transition, layout); return BlockRegistry.getLines!(transition, layout);
} }
return []; return [];
}, },
getLabels: (transition, layout) => [
...DynamicSplitRegistry.getLabels!(transition, layout),
...BlockRegistry.getLabels!(transition, layout),
],
getOutputPoint(transform, layout) {
const isVertical = FlowLayoutDefault.isVertical(layout);
const lastChildOutput = transform.lastChild?.outputPoint;
if (isVertical) {
return {
x: lastChildOutput ? lastChildOutput.x : transform.bounds.center.x,
y: transform.bounds.bottom,
};
}
return {
x: transform.bounds.right,
y: lastChildOutput ? lastChildOutput.y : transform.bounds.center.y,
};
},
extendChildRegistries: [
{
type: FlowNodeBaseType.BLOCK_ICON,
meta: {
// isNodeEnd: true
},
},
],
}; };

View File

@ -37,11 +37,11 @@ export function createLines(props: PropsType): void {
const { lineActivated } = renderData || {}; const { lineActivated } = renderData || {};
const draggingLineHide = const draggingLineHide =
line.type === FlowTransitionLineEnum.DRAGGING_LINE && (line.type === FlowTransitionLineEnum.DRAGGING_LINE || line.isDraggingLine) &&
!dragService.isDroppableBranch(data.entity, line.side); !dragService.isDroppableBranch(data.entity, line.side);
const draggingLineActivated = const draggingLineActivated =
line.type === FlowTransitionLineEnum.DRAGGING_LINE && (line.type === FlowTransitionLineEnum.DRAGGING_LINE || line.isDraggingLine) &&
data.entity?.id === dragService.dropNodeId && data.entity?.id === dragService.dropNodeId &&
line.side === dragService.labelSide; line.side === dragService.labelSide;

View File

@ -89,11 +89,10 @@ describe('history-operation-service moveNode', () => {
const split = flowDocument.getNode('dynamicSplit_0'); const split = flowDocument.getNode('dynamicSplit_0');
const split1 = flowDocument.getNode('dynamicSplit_1'); const split1 = flowDocument.getNode('dynamicSplit_1');
// 没有执行成功,因为没有 children 的分支节点,$inlineBlocks$dynamicSplit_1 不存在 expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_2']);
expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_1', 'block_2']); expect(getNodeChildrenIds(split1, true)).toEqual(['block_1']);
expect(getNodeChildrenIds(split1, true)).toEqual([]);
expect(historyService.canUndo()).toBe(false); expect(historyService.canUndo()).toBe(true);
}); });
it('move node without parent and index', async () => { it('move node without parent and index', async () => {