Browse Source

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
xiamidaxia 7 months ago
parent
commit
92b3adc5d0
23 changed files with 410 additions and 100 deletions
  1. 2 2
      .github/CODEOWNERS
  2. 6 4
      apps/demo-fixed-layout-simple/src/components/base-node.tsx
  3. 10 0
      apps/demo-fixed-layout-simple/src/components/branch-adder.tsx
  4. 5 4
      apps/demo-fixed-layout-simple/src/components/flow-select.tsx
  5. 7 1
      apps/demo-fixed-layout-simple/src/components/tools.tsx
  6. 2 0
      apps/demo-fixed-layout-simple/src/data/index.ts
  7. 7 0
      apps/demo-fixed-layout-simple/src/data/mindmap.ts
  8. 56 0
      apps/demo-fixed-layout-simple/src/data/trycatch.ts
  9. 6 2
      apps/demo-fixed-layout-simple/src/editor.tsx
  10. 20 0
      apps/demo-fixed-layout-simple/src/hooks/use-editor-props.tsx
  11. 1 36
      apps/demo-fixed-layout-simple/src/node-registries.ts
  12. 18 0
      apps/demo-fixed-layout/src/hooks/use-editor-props.ts
  13. 18 0
      apps/demo-free-layout-simple/src/hooks/use-editor-props.tsx
  14. 18 0
      apps/demo-free-layout/src/hooks/use-editor-props.tsx
  15. 11 14
      packages/canvas-engine/document/src/flow-document.ts
  16. 1 0
      packages/canvas-engine/document/src/typings/flow-transition.ts
  17. 1 0
      packages/canvas-engine/fixed-layout-core/__tests__/__snapshots__/flow-activities.spec.ts.snap
  18. 31 11
      packages/canvas-engine/fixed-layout-core/src/activities/block.ts
  19. 86 16
      packages/canvas-engine/fixed-layout-core/src/activities/input.ts
  20. 60 3
      packages/canvas-engine/fixed-layout-core/src/activities/multi-inputs.ts
  21. 39 1
      packages/canvas-engine/fixed-layout-core/src/activities/multi-outputs.ts
  22. 2 2
      packages/canvas-engine/renderer/src/components/LinesRenderer.tsx
  23. 3 4
      packages/client/fixed-layout-editor/__tests__/services/history-operation-service/move-node.test.ts

+ 2 - 2
.github/CODEOWNERS

@@ -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
 /packages/node-engine/ @xiamidaxia @dragooncjw @YuanHeDx
 /packages/variable-engine/ @xiamidaxia @dragooncjw @sanmaopep

+ 6 - 4
apps/demo-fixed-layout-simple/src/components/base-node.tsx

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

+ 10 - 0
apps/demo-fixed-layout-simple/src/components/branch-adder.tsx

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

+ 5 - 4
apps/demo-fixed-layout-simple/src/components/flow-select.tsx

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

+ 7 - 1
apps/demo-fixed-layout-simple/src/components/tools.tsx

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

+ 2 - 0
apps/demo-fixed-layout-simple/src/data/index.ts

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

+ 7 - 0
apps/demo-fixed-layout-simple/src/data/mindmap.ts

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

+ 56 - 0
apps/demo-fixed-layout-simple/src/data/trycatch.ts

@@ -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',
+      },
+    },
+  ],
+};

+ 6 - 2
apps/demo-fixed-layout-simple/src/editor.tsx

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

+ 20 - 0
apps/demo-fixed-layout-simple/src/hooks/use-editor-props.tsx

@@ -106,6 +106,7 @@ export function useEditorProps(
         },
       },
       /**
+       * Playground init
        * 画布初始化
        */
       onInit: (ctx) => {
@@ -117,11 +118,30 @@ export function useEditorProps(
         console.log('---- Playground Init ----');
       },
       /**
+       * Playground dispose
        * 画布销毁
        */
       onDispose: () => {
         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: () => [
         /**
          * Minimap plugin

+ 1 - 36
apps/demo-fixed-layout-simple/src/node-registries.ts

@@ -1,9 +1,5 @@
 import { nanoid } from 'nanoid';
-import {
-  FlowNodeRegistry,
-  FlowNodeEntity,
-  FlowNodeBaseType,
-} from '@flowgram.ai/fixed-layout-editor';
+import { FlowNodeRegistry } 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',
-  },
 ];

+ 18 - 0
apps/demo-fixed-layout/src/hooks/use-editor-props.ts

@@ -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
        */

+ 18 - 0
apps/demo-free-layout-simple/src/hooks/use-editor-props.tsx

@@ -34,6 +34,24 @@ export const useEditorProps = () =>
        * 节点注册
        */
       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'
        * 提供默认的节点注册,这个会和 nodeRegistries 做合并

+ 18 - 0
apps/demo-free-layout/src/hooks/use-editor-props.tsx

@@ -57,6 +57,24 @@ export function useEditorProps(
           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: {
         hidden: 'transparent',
         default: '#4d53e8',

+ 11 - 14
packages/canvas-engine/document/src/flow-document.ts

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

+ 1 - 0
packages/canvas-engine/document/src/typings/flow-transition.ts

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

+ 1 - 0
packages/canvas-engine/fixed-layout-core/__tests__/__snapshots__/flow-activities.spec.ts.snap

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

+ 31 - 11
packages/canvas-engine/fixed-layout-core/src/activities/block.ts

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

+ 86 - 16
packages/canvas-engine/fixed-layout-core/src/activities/input.ts

@@ -4,6 +4,7 @@ import {
   type FlowTransitionLine,
   FlowTransitionLineEnum,
   LABEL_SIDE_TYPE,
+  FlowTransitionLabelEnum,
 } from '@flowgram.ai/document';
 
 /**
@@ -15,7 +16,7 @@ export const InputRegistry: FlowNodeRegistry = {
   meta: {
     hidden: false,
   },
-  getLines(transition) {
+  getLines(transition, layout) {
     const currentTransform = transition.transform;
     const { isVertical } = transition.entity;
     const lines: FlowTransitionLine[] = [];
@@ -27,32 +28,44 @@ export const InputRegistry: FlowNodeRegistry = {
     // 当有其余分支的时候,绘制一条两个分支之间的线条
     if (hasBranchDraggingAdder) {
       if (isVertical) {
-        const currentOffsetRightX = currentTransform.firstChild?.bounds?.right || 0;
-        const nextOffsetLeftX = currentTransform.next?.firstChild?.bounds?.left || 0;
-        const currentInputPointY = currentTransform.inputPoint.y;
+        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) {
           lines.push({
-            type: FlowTransitionLineEnum.DRAGGING_LINE,
-            from: currentTransform.parent!.inputPoint,
-            to: {
+            type: FlowTransitionLineEnum.MERGE_LINE,
+            isDraggingLine: true,
+            from: {
               x: (currentOffsetRightX + nextOffsetLeftX) / 2,
               y: currentInputPointY,
             },
+            to: currentTransform.parent!.outputPoint,
             side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
           });
         }
       } else {
-        const currentOffsetRightY = currentTransform.firstChild?.bounds?.bottom || 0;
-        const nextOffsetLeftY = currentTransform.next?.firstChild?.bounds?.top || 0;
-        const currentInputPointX = currentTransform.inputPoint.x;
+        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) {
           lines.push({
-            type: FlowTransitionLineEnum.DRAGGING_LINE,
-            from: currentTransform.parent!.inputPoint,
-            to: {
+            type: FlowTransitionLineEnum.MERGE_LINE,
+            isDraggingLine: true,
+            from: {
               x: currentInputPointX,
-              y: (currentOffsetRightY + nextOffsetLeftY) / 2,
+              y: (currentOffsetBottomX + nextOffsetTopX) / 2,
             },
+            to: currentTransform.parent!.outputPoint,
             side: LABEL_SIDE_TYPE.NORMAL_BRANCH,
           });
         }
@@ -71,7 +84,64 @@ export const InputRegistry: FlowNodeRegistry = {
 
     return lines;
   },
-  getLabels() {
-    return [];
+  getLabels(transition) {
+    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];
   },
 };

+ 60 - 3
packages/canvas-engine/fixed-layout-core/src/activities/multi-inputs.ts

@@ -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,
       meta: {
         hidden: true,
+        spacing: 0,
       },
       getLines() {
         return [];
@@ -25,8 +36,54 @@ export const MultiInputsRegistry: FlowNodeRegistry = {
     },
     {
       type: FlowNodeBaseType.INLINE_BLOCKS,
-      getLabels() {
-        return [];
+      meta: {
+        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,
+            },
+          },
+        ];
       },
     },
   ],

+ 39 - 1
packages/canvas-engine/fixed-layout-core/src/activities/multi-outputs.ts

@@ -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';
 
 /**
@@ -13,10 +19,42 @@ import { BlockRegistry } from './block';
 export const MultiOuputsRegistry: FlowNodeRegistry = {
   type: FlowNodeBaseType.MULTI_OUTPUTS,
   extend: FlowNodeSplitType.SIMPLE_SPLIT,
+  meta: {
+    isNodeEnd: true,
+  },
   getLines: (transition, layout) => {
+    // 嵌套在 mutliOutputs 下边
     if (transition.entity.parent?.flowNodeType === FlowNodeBaseType.INLINE_BLOCKS) {
       return BlockRegistry.getLines!(transition, layout);
     }
     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
+      },
+    },
+  ],
 };

+ 2 - 2
packages/canvas-engine/renderer/src/components/LinesRenderer.tsx

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

+ 3 - 4
packages/client/fixed-layout-editor/__tests__/services/history-operation-service/move-node.test.ts

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