瀏覽代碼

feat: demo-free-layout-simple add loop & batch sub canvas demo (#1029)

* feat: add batch and loop container nodes with sub-canvas support

* refactor: reorganize node registries into modular structure

* chore: remove obsolete demo-sub-canvas application

* fix(docs): resolve style pollution in free-layout-simple demo
Louis Young 3 周之前
父節點
當前提交
2dab0b3610
共有 49 個文件被更改,包括 483 次插入1190 次删除
  1. 4 1
      .vscode/settings.json
  2. 1 0
      apps/demo-free-layout-simple/package.json
  3. 23 7
      apps/demo-free-layout-simple/src/components/node-add-panel.tsx
  4. 19 1
      apps/demo-free-layout-simple/src/components/tools.tsx
  5. 27 3
      apps/demo-free-layout-simple/src/hooks/use-editor-props.tsx
  6. 160 45
      apps/demo-free-layout-simple/src/initial-data.ts
  7. 0 153
      apps/demo-free-layout-simple/src/node-registries.tsx
  8. 0 0
      apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function-json.ts
  9. 0 0
      apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function-lines.ts
  10. 0 0
      apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function.ts
  11. 1 1
      apps/demo-free-layout-simple/src/nodes/batch-function/form-meta.tsx
  12. 0 0
      apps/demo-free-layout-simple/src/nodes/batch-function/index.ts
  13. 0 0
      apps/demo-free-layout-simple/src/nodes/batch-function/registry.ts
  14. 0 0
      apps/demo-free-layout-simple/src/nodes/batch-function/relation.ts
  15. 0 0
      apps/demo-free-layout-simple/src/nodes/batch/index.ts
  16. 0 1
      apps/demo-free-layout-simple/src/nodes/block-end/form-meta.tsx
  17. 0 1
      apps/demo-free-layout-simple/src/nodes/block-end/index.ts
  18. 0 1
      apps/demo-free-layout-simple/src/nodes/block-start/form-meta.tsx
  19. 0 1
      apps/demo-free-layout-simple/src/nodes/block-start/index.ts
  20. 28 0
      apps/demo-free-layout-simple/src/nodes/chain/index.ts
  21. 70 0
      apps/demo-free-layout-simple/src/nodes/condition/form-meta.tsx
  22. 16 0
      apps/demo-free-layout-simple/src/nodes/condition/index.ts
  23. 12 0
      apps/demo-free-layout-simple/src/nodes/custom/index.ts
  24. 20 0
      apps/demo-free-layout-simple/src/nodes/end/index.ts
  25. 34 0
      apps/demo-free-layout-simple/src/nodes/index.ts
  26. 4 2
      apps/demo-free-layout-simple/src/nodes/loop/form-meta.tsx
  27. 1 1
      apps/demo-free-layout-simple/src/nodes/loop/index.ts
  28. 16 0
      apps/demo-free-layout-simple/src/nodes/start/index.ts
  29. 13 0
      apps/demo-free-layout-simple/src/nodes/tool/index.ts
  30. 18 0
      apps/demo-free-layout-simple/src/nodes/twoway/index.ts
  31. 0 20
      apps/demo-sub-canvas/.eslintrc.js
  32. 0 12
      apps/demo-sub-canvas/index.html
  33. 0 58
      apps/demo-sub-canvas/package.json
  34. 0 19
      apps/demo-sub-canvas/rsbuild.config.ts
  35. 0 121
      apps/demo-sub-canvas/src/add-node.tsx
  36. 0 29
      apps/demo-sub-canvas/src/app.tsx
  37. 0 282
      apps/demo-sub-canvas/src/initial-data.ts
  38. 0 35
      apps/demo-sub-canvas/src/minimap.tsx
  39. 0 46
      apps/demo-sub-canvas/src/node-registries.tsx
  40. 0 40
      apps/demo-sub-canvas/src/node-render.tsx
  41. 0 112
      apps/demo-sub-canvas/src/tools.tsx
  42. 0 97
      apps/demo-sub-canvas/src/use-editor-props.tsx
  43. 0 23
      apps/demo-sub-canvas/tsconfig.json
  44. 9 0
      apps/docs/components/free-layout-simple/index.less
  45. 2 0
      apps/docs/components/free-layout-simple/index.tsx
  46. 2 2
      apps/docs/components/free-layout-simple/preview.tsx
  47. 0 8
      common/config/rush/command-line.json
  48. 3 58
      common/config/rush/pnpm-lock.yaml
  49. 0 10
      rush.json

+ 4 - 1
.vscode/settings.json

@@ -141,5 +141,8 @@
   },
   "[mdx]": {
     "editor.defaultFormatter": "unifiedjs.vscode-mdx"
-  }
+  },
+  "cSpell.words": [
+    "Twoway"
+  ]
 }

+ 1 - 0
apps/demo-free-layout-simple/package.json

@@ -33,6 +33,7 @@
     "@flowgram.ai/free-layout-editor": "workspace:*",
     "@flowgram.ai/free-snap-plugin": "workspace:*",
     "@flowgram.ai/minimap-plugin": "workspace:*",
+    "@flowgram.ai/free-container-plugin": "workspace:*",
     "react": "^18",
     "react-dom": "^18"
   },

+ 23 - 7
apps/demo-free-layout-simple/src/components/node-add-panel.tsx

@@ -5,12 +5,21 @@
 
 import React from 'react';
 
-import { WorkflowDragService, useService } from '@flowgram.ai/free-layout-editor';
+import {
+  WorkflowDocument,
+  WorkflowDragService,
+  useClientContext,
+  useService,
+} from '@flowgram.ai/free-layout-editor';
 
-const cardkeys = ['Node1', 'Node2', 'Condition', 'Chain', 'Tool', 'Twoway'];
+import { createBatchFunction } from '../nodes/batch-function';
+
+const cardkeys = ['Node1', 'Node2', 'Condition', 'Chain', 'Tool', 'Twoway', 'Loop', 'Batch'];
 
 export const NodeAddPanel: React.FC = (props) => {
-  const startDragSerivce = useService<WorkflowDragService>(WorkflowDragService);
+  const startDragService = useService(WorkflowDragService);
+  const workflowDocument = useService(WorkflowDocument);
+  const context = useClientContext();
 
   return (
     <div className="demo-free-sidebar">
@@ -18,14 +27,21 @@ export const NodeAddPanel: React.FC = (props) => {
         <div
           key={nodeType}
           className="demo-free-card"
-          onMouseDown={(e) =>
-            startDragSerivce.startDragCard(nodeType.toLowerCase(), e, {
+          onMouseDown={async (e) => {
+            const type = nodeType.toLowerCase();
+            const registry = workflowDocument.getNodeRegistry(type);
+            const json = registry.onAdd?.(context);
+            const node = await startDragService.startDragCard(type, e, {
+              ...json,
               data: {
                 title: `New ${nodeType}`,
                 content: 'xxxx',
               },
-            })
-          }
+            });
+            if (node?.flowNodeType === 'batch') {
+              createBatchFunction(node, node.getNodeMeta().position);
+            }
+          }}
         >
           {nodeType}
         </div>

+ 19 - 1
apps/demo-free-layout-simple/src/components/tools.tsx

@@ -7,6 +7,8 @@ import { useEffect, useState } from 'react';
 
 import { usePlaygroundTools, useClientContext, LineType } from '@flowgram.ai/free-layout-editor';
 
+import { getBatchID } from '../nodes/batch-function';
+
 export function Tools() {
   const { history } = useClientContext();
   const tools = usePlaygroundTools();
@@ -28,7 +30,23 @@ export function Tools() {
       <button onClick={() => tools.zoomin()}>ZoomIn</button>
       <button onClick={() => tools.zoomout()}>ZoomOut</button>
       <button onClick={() => tools.fitView()}>Fitview</button>
-      <button onClick={() => tools.autoLayout()}>AutoLayout</button>
+      <button
+        onClick={() =>
+          tools.autoLayout({
+            getFollowNode: (node, context) => {
+              if (node.entity.flowNodeType !== 'batch_function') {
+                return;
+              }
+              const batchNodeID = getBatchID(node.entity.id);
+              return {
+                followTo: batchNodeID,
+              };
+            },
+          })
+        }
+      >
+        AutoLayout
+      </button>
       <button
         onClick={() =>
           tools.switchLineType(

+ 27 - 3
apps/demo-free-layout-simple/src/hooks/use-editor-props.tsx

@@ -13,9 +13,11 @@ import {
   WorkflowNodeRenderer,
   Field,
   useNodeRender,
+  FlowNodeMeta,
 } from '@flowgram.ai/free-layout-editor';
+import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
 
-import { nodeRegistries } from '../node-registries';
+import { nodeRegistries } from '../nodes';
 import { initialData } from '../initial-data';
 
 export const useEditorProps = () =>
@@ -91,9 +93,14 @@ export const useEditorProps = () =>
          * Render Node
          */
         renderDefaultNode: (props: WorkflowNodeProps) => {
-          const { form } = useNodeRender();
+          const { node, form } = useNodeRender();
+          const meta = node.getNodeMeta<FlowNodeMeta>();
           return (
-            <WorkflowNodeRenderer className="demo-free-node" node={props.node}>
+            <WorkflowNodeRenderer
+              className="demo-free-node"
+              node={props.node}
+              style={meta.wrapperStyle}
+            >
               {form?.render()}
             </WorkflowNodeRenderer>
           );
@@ -105,6 +112,18 @@ export const useEditorProps = () =>
       onContentChange(ctx, event) {
         console.log('Auto Save: ', event, ctx.document.toJSON());
       },
+      canDeleteLine: (ctx, line) => {
+        if (line.from?.flowNodeType === 'batch' && line.to?.flowNodeType === 'batch_function') {
+          return false;
+        }
+        return true;
+      },
+      isHideArrowLine(ctx, line) {
+        if (line.from?.flowNodeType === 'batch' && line.to?.flowNodeType === 'batch_function') {
+          return true;
+        }
+        return false;
+      },
       // /**
       //  * Node engine enable, you can configure formMeta in the FlowNodeRegistry
       //  */
@@ -171,6 +190,11 @@ export const useEditorProps = () =>
           alignLineWidth: 1,
           alignCrossWidth: 8,
         }),
+        /**
+         * This is used for the rendering of the loop node sub-canvas
+         * 这个用于 loop 节点子画布的渲染
+         */
+        createContainerNodePlugin({}),
       ],
     }),
     []

+ 160 - 45
apps/demo-free-layout-simple/src/initial-data.ts

@@ -12,8 +12,8 @@ export const initialData: WorkflowJSON = {
       type: 'start',
       meta: {
         position: {
-          x: 150,
-          y: 100,
+          x: 86.5,
+          y: 57.5,
         },
       },
       data: {
@@ -26,11 +26,12 @@ export const initialData: WorkflowJSON = {
       type: 'condition',
       meta: {
         position: {
-          x: 550,
-          y: 100,
+          x: 359.5,
+          y: 43.25,
         },
       },
       data: {
+        portKeys: ['if', 'else'],
         title: 'Condition',
         content: 'Condition node content',
         ports: ['if', 'else'],
@@ -41,8 +42,8 @@ export const initialData: WorkflowJSON = {
       type: 'end',
       meta: {
         position: {
-          x: 1350,
-          y: 100,
+          x: 1393.5,
+          y: 52.5,
         },
       },
       data: {
@@ -51,100 +52,207 @@ export const initialData: WorkflowJSON = {
       },
     },
     {
-      id: '144150',
-      type: 'node1',
+      id: '100260',
+      type: 'tool',
       meta: {
         position: {
-          x: 950,
-          y: 0,
+          x: 86.5,
+          y: 399.75,
         },
       },
       data: {
-        title: 'New Node1',
+        title: 'New Tool',
         content: 'xxxx',
       },
     },
     {
-      id: '118937',
-      type: 'node2',
+      id: '105108',
+      type: 'tool',
       meta: {
         position: {
-          x: 950,
-          y: 200,
+          x: 359.5,
+          y: 399.75,
         },
       },
       data: {
-        title: 'New Node2',
+        title: 'New Tool',
         content: 'xxxx',
       },
     },
     {
-      id: 'chain0',
-      type: 'chain',
+      id: '106070',
+      type: 'twoway',
       meta: {
         position: {
-          x: 150,
-          y: 246,
+          x: 86.5,
+          y: 563.75,
         },
       },
       data: {
-        title: 'Chain',
+        title: 'New Twoway',
         content: 'xxxx',
       },
     },
     {
-      id: '100260',
-      type: 'tool',
+      id: '122116',
+      type: 'twoway',
       meta: {
         position: {
-          x: 55.8,
-          y: 410,
+          x: 359.5,
+          y: 563.75,
         },
       },
       data: {
-        title: 'New Tool',
+        title: 'New Twoway',
         content: 'xxxx',
       },
     },
     {
-      id: '105108',
-      type: 'tool',
+      id: 'BatchFunction_193210',
+      type: 'batch_function',
+      meta: {
+        position: {
+          x: 626,
+          y: 420.38853503184714,
+        },
+      },
+      data: {},
+      blocks: [
+        {
+          id: '118937',
+          type: 'node2',
+          meta: {
+            position: {
+              x: 250.5,
+              y: 0,
+            },
+          },
+          data: {
+            title: 'New Node2',
+            content: 'xxxx',
+          },
+        },
+        {
+          id: 'block_start_Y04Mt',
+          type: 'block_start',
+          meta: {
+            position: {
+              x: 32,
+              y: 0,
+            },
+          },
+          data: {},
+        },
+        {
+          id: 'block_end_7QesT',
+          type: 'block_end',
+          meta: {
+            position: {
+              x: 469,
+              y: 0,
+            },
+          },
+          data: {},
+        },
+      ],
+      edges: [
+        {
+          sourceNodeID: 'block_start_Y04Mt',
+          targetNodeID: '118937',
+        },
+        {
+          sourceNodeID: '118937',
+          targetNodeID: 'block_end_7QesT',
+        },
+      ],
+    },
+    {
+      id: 'loop_9OpIm',
+      type: 'loop',
       meta: {
         position: {
-          x: 280.5,
-          y: 410,
+          x: 626,
+          y: 0,
         },
       },
       data: {
-        title: 'New Tool',
+        title: 'New Loop',
         content: 'xxxx',
       },
+      blocks: [
+        {
+          id: '144150',
+          type: 'node1',
+          meta: {
+            position: {
+              x: 250.5,
+              y: 1.4210854715202004e-14,
+            },
+          },
+          data: {
+            title: 'New Node1',
+            content: 'xxxx',
+          },
+        },
+        {
+          id: 'block_start_ptqXx',
+          type: 'block_start',
+          meta: {
+            position: {
+              x: 32,
+              y: 0,
+            },
+          },
+          data: {},
+        },
+        {
+          id: 'block_end_1zf_a',
+          type: 'block_end',
+          meta: {
+            position: {
+              x: 469,
+              y: 0,
+            },
+          },
+          data: {},
+        },
+      ],
+      edges: [
+        {
+          sourceNodeID: 'block_start_ptqXx',
+          targetNodeID: '144150',
+        },
+        {
+          sourceNodeID: '144150',
+          targetNodeID: 'block_end_1zf_a',
+        },
+      ],
     },
     {
-      id: '106070',
-      type: 'twoway',
+      id: '193210',
+      type: 'batch',
       meta: {
         position: {
-          x: 550,
-          y: 310,
+          x: 876.5,
+          y: 197.69426751592357,
         },
       },
       data: {
-        title: 'New Twoway',
+        title: 'New Batch',
         content: 'xxxx',
       },
     },
     {
-      id: '122116',
-      type: 'twoway',
+      id: 'chain0',
+      type: 'chain',
       meta: {
         position: {
-          x: 866.0091156462586,
-          y: 422.4669387755102,
+          x: 221.02229299363057,
+          y: 197.69426751592357,
         },
       },
       data: {
-        title: 'New Twoway',
+        title: 'Chain',
         content: 'xxxx',
       },
     },
@@ -156,20 +264,21 @@ export const initialData: WorkflowJSON = {
     },
     {
       sourceNodeID: 'node_0',
-      targetNodeID: '144150',
+      targetNodeID: 'loop_9OpIm',
       sourcePortID: 'if',
     },
     {
       sourceNodeID: 'node_0',
-      targetNodeID: '118937',
+      targetNodeID: '193210',
       sourcePortID: 'else',
     },
     {
-      sourceNodeID: '118937',
+      sourceNodeID: '193210',
       targetNodeID: 'end_0',
+      sourcePortID: 'batch-output',
     },
     {
-      sourceNodeID: '144150',
+      sourceNodeID: 'loop_9OpIm',
       targetNodeID: 'end_0',
     },
     {
@@ -194,5 +303,11 @@ export const initialData: WorkflowJSON = {
       sourcePortID: 'output-right',
       targetPortID: 'input-left',
     },
+    {
+      sourceNodeID: '193210',
+      targetNodeID: 'BatchFunction_193210',
+      sourcePortID: 'batch-output-to-function',
+      targetPortID: 'batch-function-input',
+    },
   ],
 };

+ 0 - 153
apps/demo-free-layout-simple/src/node-registries.tsx

@@ -1,153 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import {
-  WorkflowNodeRegistry,
-  Field,
-  DataEvent,
-  EffectFuncProps,
-  WorkflowPorts,
-} from '@flowgram.ai/free-layout-editor';
-
-const CONDITION_ITEM_HEIGHT = 30;
-/**
- * You can customize your own node registry
- * 你可以自定义节点的注册器
- */
-export const nodeRegistries: WorkflowNodeRegistry[] = [
-  {
-    type: 'start',
-    meta: {
-      isStart: true, // Mark as start
-      deleteDisable: true, // The start node cannot be deleted
-      copyDisable: true, // The start node cannot be copied
-      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
-    },
-  },
-  {
-    type: 'condition',
-    meta: {
-      defaultPorts: [{ type: 'input' }],
-    },
-    formMeta: {
-      /**
-       * Initialize the form values
-       * @param value
-       */
-      formatOnInit: (value) => ({
-        portKeys: ['if', 'else'],
-        ...value,
-      }),
-      effect: {
-        /**
-         * Listen for "portsKeys" changes and update ports
-         */
-        portKeys: [
-          {
-            event: DataEvent.onValueInitOrChange,
-            effect: ({ value, context }: EffectFuncProps<Array<string>, FormData>) => {
-              const { node } = context;
-              const defaultPorts: WorkflowPorts = [{ type: 'input' }];
-              const newPorts: WorkflowPorts = value.map((portID: string, i: number) => ({
-                type: 'output',
-                portID,
-                location: 'right',
-                locationConfig: {
-                  right: 0,
-                  top: (i + 1) * CONDITION_ITEM_HEIGHT,
-                },
-              }));
-              node.ports.updateAllPorts([...defaultPorts, ...newPorts]);
-            },
-          },
-        ],
-      },
-      render: () => (
-        <>
-          <Field<string> name="title">
-            {({ field }) => <div className="demo-free-node-title">{field.value}</div>}
-          </Field>
-          <Field<Array<string>> name="portKeys">
-            {({ field: { value, onChange } }) => (
-              <div
-                className="demo-free-node-content"
-                style={{
-                  width: 160,
-                  height: value.length * CONDITION_ITEM_HEIGHT,
-                  minHeight: 2 * CONDITION_ITEM_HEIGHT,
-                }}
-              >
-                <div>
-                  <button onClick={() => onChange(value.concat(`if_${value.length}`))}>
-                    Add Port
-                  </button>
-                </div>
-                <div style={{ marginTop: 8 }}>
-                  <button
-                    onClick={() => onChange(value.filter((v, i, arr) => i !== arr.length - 1))}
-                  >
-                    Delete Port
-                  </button>
-                </div>
-              </div>
-            )}
-          </Field>
-        </>
-      ),
-    },
-  },
-  {
-    type: 'chain',
-    meta: {
-      defaultPorts: [
-        { type: 'input' },
-        { type: 'output' },
-        {
-          portID: 'p4',
-          location: 'bottom',
-          locationConfig: { left: '33%', bottom: 0 },
-          type: 'output',
-        },
-        {
-          portID: 'p5',
-          location: 'bottom',
-          locationConfig: { left: '66%', bottom: 0 },
-          type: 'output',
-        },
-      ],
-    },
-  },
-  {
-    type: 'tool',
-    meta: {
-      defaultPorts: [{ location: 'top', type: 'input' }],
-    },
-  },
-  {
-    // 支持双向连接, Support two-way connection
-    type: 'twoway',
-    meta: {
-      defaultPorts: [
-        { type: 'input', portID: 'input-left', location: 'left' },
-        { type: 'output', portID: 'output-left', location: 'left' },
-        { type: 'input', portID: 'input-right', location: 'right' },
-        { type: 'output', portID: 'output-right', location: 'right' },
-      ],
-    },
-  },
-  {
-    type: 'end',
-    meta: {
-      deleteDisable: true,
-      copyDisable: true,
-      defaultPorts: [{ type: 'input' }],
-    },
-  },
-  {
-    type: 'custom',
-    meta: {},
-    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports
-  },
-];

+ 0 - 0
apps/demo-sub-canvas/src/nodes/batch-function/create-batch-function-json.ts → apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function-json.ts


+ 0 - 0
apps/demo-sub-canvas/src/nodes/batch-function/create-batch-function-lines.ts → apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function-lines.ts


+ 0 - 0
apps/demo-sub-canvas/src/nodes/batch-function/create-batch-function.ts → apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function.ts


+ 1 - 1
apps/demo-sub-canvas/src/nodes/batch-function/form-meta.tsx → apps/demo-free-layout-simple/src/nodes/batch-function/form-meta.tsx

@@ -10,7 +10,7 @@ const formHeight = 48;
 
 export const BatchFunctionFormRender = () => (
   <>
-    BATCH FUNCTION
+    <div className="demo-free-node-title">Batch Function</div>
     <SubCanvasRender offsetY={-formHeight} />
   </>
 );

+ 0 - 0
apps/demo-sub-canvas/src/nodes/batch-function/index.ts → apps/demo-free-layout-simple/src/nodes/batch-function/index.ts


+ 0 - 0
apps/demo-sub-canvas/src/nodes/batch-function/registry.ts → apps/demo-free-layout-simple/src/nodes/batch-function/registry.ts


+ 0 - 0
apps/demo-sub-canvas/src/nodes/batch-function/relation.ts → apps/demo-free-layout-simple/src/nodes/batch-function/relation.ts


+ 0 - 0
apps/demo-sub-canvas/src/nodes/batch/index.ts → apps/demo-free-layout-simple/src/nodes/batch/index.ts


+ 0 - 1
apps/demo-sub-canvas/src/nodes/block-end/form-meta.tsx → apps/demo-free-layout-simple/src/nodes/block-end/form-meta.tsx

@@ -20,7 +20,6 @@ export const renderForm = () => (
         style={{
           width: 40,
           height: 40,
-          borderRadius: '50%',
           cursor: 'move',
           display: 'flex',
           alignItems: 'center',

+ 0 - 1
apps/demo-sub-canvas/src/nodes/block-end/index.ts → apps/demo-free-layout-simple/src/nodes/block-end/index.ts

@@ -24,7 +24,6 @@ export const BlockEndNodeRegistry: FlowNodeRegistry = {
       minWidth: 'unset',
       width: '100%',
       borderWidth: 2,
-      borderRadius: '50%',
       cursor: 'move',
     },
   },

+ 0 - 1
apps/demo-sub-canvas/src/nodes/block-start/form-meta.tsx → apps/demo-free-layout-simple/src/nodes/block-start/form-meta.tsx

@@ -20,7 +20,6 @@ export const renderForm = () => (
         style={{
           width: 40,
           height: 40,
-          borderRadius: '50%',
           cursor: 'move',
           display: 'flex',
           alignItems: 'center',

+ 0 - 1
apps/demo-sub-canvas/src/nodes/block-start/index.ts → apps/demo-free-layout-simple/src/nodes/block-start/index.ts

@@ -24,7 +24,6 @@ export const BlockStartNodeRegistry: FlowNodeRegistry = {
       minWidth: 'unset',
       width: '100%',
       borderWidth: 2,
-      borderRadius: '50%',
       cursor: 'move',
     },
   },

+ 28 - 0
apps/demo-free-layout-simple/src/nodes/chain/index.ts

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
+
+export const ChainNodeRegistry: FlowNodeRegistry = {
+  type: 'chain',
+  meta: {
+    defaultPorts: [
+      { type: 'input' },
+      { type: 'output' },
+      {
+        portID: 'p4',
+        location: 'bottom',
+        locationConfig: { left: '33%', bottom: 0 },
+        type: 'output',
+      },
+      {
+        portID: 'p5',
+        location: 'bottom',
+        locationConfig: { left: '66%', bottom: 0 },
+        type: 'output',
+      },
+    ],
+  },
+};

+ 70 - 0
apps/demo-free-layout-simple/src/nodes/condition/form-meta.tsx

@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import {
+  Field,
+  DataEvent,
+  EffectFuncProps,
+  WorkflowPorts,
+  FormMeta,
+} from '@flowgram.ai/free-layout-editor';
+
+const CONDITION_ITEM_HEIGHT = 35;
+
+export const formMeta: FormMeta = {
+  formatOnInit: (value) => ({
+    portKeys: ['if', 'else'],
+    ...value,
+  }),
+  effect: {
+    portKeys: [
+      {
+        event: DataEvent.onValueInitOrChange,
+        effect: ({ value, context }: EffectFuncProps<Array<string>, FormData>) => {
+          const { node } = context;
+          const defaultPorts: WorkflowPorts = [{ type: 'input' }];
+          const newPorts: WorkflowPorts = value.map((portID: string, i: number) => ({
+            type: 'output',
+            portID,
+            location: 'right',
+            locationConfig: {
+              right: 0,
+              top: (i + 1) * CONDITION_ITEM_HEIGHT,
+            },
+          }));
+          node.ports.updateAllPorts([...defaultPorts, ...newPorts]);
+        },
+      },
+    ],
+  },
+  render: () => (
+    <>
+      <Field<string> name="title">
+        {({ field }) => <div className="demo-free-node-title">{field.value}</div>}
+      </Field>
+      <Field<Array<string>> name="portKeys">
+        {({ field: { value, onChange } }) => (
+          <div
+            className="demo-free-node-content"
+            style={{
+              width: 160,
+              height: value.length * CONDITION_ITEM_HEIGHT,
+              minHeight: 2 * CONDITION_ITEM_HEIGHT,
+            }}
+          >
+            <div>
+              <button onClick={() => onChange(value.concat(`if_${value.length}`))}>Add Port</button>
+            </div>
+            <div style={{ marginTop: 8 }}>
+              <button onClick={() => onChange(value.filter((v, i, arr) => i !== arr.length - 1))}>
+                Delete Port
+              </button>
+            </div>
+          </div>
+        )}
+      </Field>
+    </>
+  ),
+};

+ 16 - 0
apps/demo-free-layout-simple/src/nodes/condition/index.ts

@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
+
+import { formMeta } from './form-meta';
+
+export const ConditionNodeRegistry: FlowNodeRegistry = {
+  type: 'condition',
+  meta: {
+    defaultPorts: [{ type: 'input' }],
+  },
+  formMeta,
+};

+ 12 - 0
apps/demo-free-layout-simple/src/nodes/custom/index.ts

@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
+
+export const CustomNodeRegistry: FlowNodeRegistry = {
+  type: 'custom',
+  meta: {},
+  defaultPorts: [{ type: 'output' }, { type: 'input' }],
+};

+ 20 - 0
apps/demo-free-layout-simple/src/nodes/end/index.ts

@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
+
+export const EndNodeRegistry: FlowNodeRegistry = {
+  type: 'end',
+  meta: {
+    deleteDisable: true,
+    copyDisable: true,
+    defaultPorts: [{ type: 'input' }],
+  },
+};

+ 34 - 0
apps/demo-free-layout-simple/src/nodes/index.ts

@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
+
+import { TwowayNodeRegistry } from './twoway';
+import { ToolNodeRegistry } from './tool';
+import { StartNodeRegistry } from './start';
+import { LoopNodeRegistry } from './loop';
+import { EndNodeRegistry } from './end';
+import { CustomNodeRegistry } from './custom';
+import { ConditionNodeRegistry } from './condition';
+import { ChainNodeRegistry } from './chain';
+import { BlockStartNodeRegistry } from './block-start';
+import { BlockEndNodeRegistry } from './block-end';
+import { BatchFunctionNodeRegistry } from './batch-function';
+import { BatchNodeRegistry } from './batch';
+
+export const nodeRegistries: WorkflowNodeRegistry[] = [
+  LoopNodeRegistry,
+  BlockStartNodeRegistry,
+  BlockEndNodeRegistry,
+  BatchNodeRegistry,
+  BatchFunctionNodeRegistry,
+  StartNodeRegistry,
+  ConditionNodeRegistry,
+  ChainNodeRegistry,
+  ToolNodeRegistry,
+  TwowayNodeRegistry,
+  EndNodeRegistry,
+  CustomNodeRegistry,
+];

+ 4 - 2
apps/demo-sub-canvas/src/nodes/loop/form-meta.tsx → apps/demo-free-layout-simple/src/nodes/loop/form-meta.tsx

@@ -3,14 +3,16 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { FormMeta } from '@flowgram.ai/free-layout-editor';
+import { Field, FormMeta } from '@flowgram.ai/free-layout-editor';
 import { SubCanvasRender } from '@flowgram.ai/free-container-plugin';
 
 const formHeight = 48;
 
 export const LoopFormRender = () => (
   <>
-    LOOP
+    <Field<string> name="title">
+      {({ field }) => <div className="demo-free-node-title">{field.value}</div>}
+    </Field>
     <SubCanvasRender offsetY={-formHeight} />
   </>
 );

+ 1 - 1
apps/demo-sub-canvas/src/nodes/loop/index.ts → apps/demo-free-layout-simple/src/nodes/loop/index.ts

@@ -75,7 +75,7 @@ export const LoopNodeRegistry: FlowNodeRegistry = {
       id: `loop_${nanoid(5)}`,
       type: 'loop',
       data: {
-        title: `Loop_${++index}`,
+        title: `loop_${++index}`,
       },
       blocks: [
         {

+ 16 - 0
apps/demo-free-layout-simple/src/nodes/start/index.ts

@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
+
+export const StartNodeRegistry: FlowNodeRegistry = {
+  type: 'start',
+  meta: {
+    isStart: true,
+    deleteDisable: true,
+    copyDisable: true,
+    defaultPorts: [{ type: 'output' }],
+  },
+};

+ 13 - 0
apps/demo-free-layout-simple/src/nodes/tool/index.ts

@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
+
+export const ToolNodeRegistry: FlowNodeRegistry = {
+  type: 'tool',
+  meta: {
+    defaultPorts: [{ location: 'top', type: 'input' }],
+  },
+};

+ 18 - 0
apps/demo-free-layout-simple/src/nodes/twoway/index.ts

@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
+
+export const TwowayNodeRegistry: FlowNodeRegistry = {
+  type: 'twoway',
+  meta: {
+    defaultPorts: [
+      { type: 'input', portID: 'input-left', location: 'left' },
+      { type: 'output', portID: 'output-left', location: 'left' },
+      { type: 'input', portID: 'input-right', location: 'right' },
+      { type: 'output', portID: 'output-right', location: 'right' },
+    ],
+  },
+};

+ 0 - 20
apps/demo-sub-canvas/.eslintrc.js

@@ -1,20 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-const { defineConfig } = require('@flowgram.ai/eslint-config');
-
-module.exports = defineConfig({
-  preset: 'web',
-  packageRoot: __dirname,
-  rules: {
-    'no-console': 'off',
-    'react/prop-types': 'off',
-  },
-  settings: {
-    react: {
-      version: 'detect', // 自动检测 React 版本
-    },
-  },
-});

+ 0 - 12
apps/demo-sub-canvas/index.html

@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-<html lang="en" data-bundler="rspack">
-  <head>
-    <meta charset="UTF-8" />
-    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Flow FreeLayoutEditor Demo</title>
-  </head>
-  <body>
-    <div id="root"></div>
-  </body>
-</html>

+ 0 - 58
apps/demo-sub-canvas/package.json

@@ -1,58 +0,0 @@
-{
-  "name": "@flowgram.ai/demo-sub-canvas",
-  "version": "0.1.0",
-  "description": "",
-  "keywords": [],
-  "license": "MIT",
-  "main": "./src/index.tsx",
-  "files": [
-    "src/",
-    ".eslintrc.js",
-    ".gitignore",
-    "index.html",
-    "package.json",
-    "rsbuild.config.ts",
-    "tsconfig.json"
-  ],
-  "scripts": {
-    "build": "exit 0",
-    "build:fast": "exit 0",
-    "build:watch": "exit 0",
-    "build:prod": "cross-env MODE=app NODE_ENV=production rsbuild build",
-    "clean": "rimraf dist",
-    "dev": "cross-env MODE=app NODE_ENV=development rsbuild dev --open",
-    "lint": "eslint ./src --cache",
-    "lint:fix": "eslint ./src --fix",
-    "ts-check": "tsc --noEmit",
-    "start": "cross-env NODE_ENV=development rsbuild dev --open",
-    "test": "exit",
-    "test:cov": "exit",
-    "watch": "exit 0"
-  },
-  "dependencies": {
-    "@flowgram.ai/free-layout-editor": "workspace:*",
-    "@flowgram.ai/free-snap-plugin": "workspace:*",
-    "@flowgram.ai/free-container-plugin": "workspace:*",
-    "@flowgram.ai/minimap-plugin": "workspace:*",
-    "react": "^18",
-    "react-dom": "^18"
-  },
-  "devDependencies": {
-    "@flowgram.ai/ts-config": "workspace:*",
-    "@flowgram.ai/eslint-config": "workspace:*",
-    "@rsbuild/core": "^1.2.16",
-    "@rsbuild/plugin-react": "^1.1.1",
-    "@types/lodash-es": "^4.17.12",
-    "@types/node": "^18",
-    "@types/react": "^18",
-    "@types/react-dom": "^18",
-    "@types/styled-components": "^5",
-    "typescript": "^5.8.3",
-    "eslint": "^8.54.0",
-    "cross-env": "~7.0.3"
-  },
-  "publishConfig": {
-    "access": "public",
-    "registry": "https://registry.npmjs.org/"
-  }
-}

+ 0 - 19
apps/demo-sub-canvas/rsbuild.config.ts

@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { pluginReact } from '@rsbuild/plugin-react';
-import { defineConfig } from '@rsbuild/core';
-
-export default defineConfig({
-  plugins: [pluginReact()],
-  source: {
-    entry: {
-      index: './src/app.tsx',
-    },
-  },
-  html: {
-    title: 'demo-free-layout-simple',
-  },
-});

+ 0 - 121
apps/demo-sub-canvas/src/add-node.tsx

@@ -1,121 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import {
-  getAntiOverlapPosition,
-  IPoint,
-  useService,
-  WorkflowDocument,
-  WorkflowNodeEntity,
-  WorkflowSelectService,
-} from '@flowgram.ai/free-layout-editor';
-
-import { LoopNodeRegistry } from './nodes/loop';
-import { createBatchFunction } from './nodes/batch-function';
-
-const getNodeDefaultPosition = (document: WorkflowDocument, nodeType: string): IPoint => {
-  const { size } = document.getNodeRegistry(nodeType).meta || {};
-  // 当前可视区域的中心位置
-  let position = document.playgroundConfig.getViewport(true).center;
-  if (size) {
-    position = {
-      x: position.x,
-      y: position.y - size.height / 2,
-    };
-  }
-  // 去掉叠加的
-  return getAntiOverlapPosition(document, position);
-};
-
-export const AddNode = () => {
-  const workflowDocument = useService(WorkflowDocument);
-  const selectService = useService(WorkflowSelectService);
-
-  return (
-    <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 8, display: 'flex', gap: 8 }}>
-      <button
-        style={{
-          border: '1px solid #e0e0e0',
-          borderRadius: '50%',
-          cursor: 'pointer',
-          padding: '4px',
-          color: '#ffffff',
-          background: '#7e72e8',
-          width: 70,
-          height: 70,
-          fontSize: 14,
-        }}
-        onClick={() => {
-          const node: WorkflowNodeEntity = workflowDocument.createWorkflowNodeByType(
-            'custom',
-            undefined, // position undefined means create node in center of canvas - position undefined 可以在画布中间创建节点
-            {
-              data: {
-                title: 'New Node',
-              },
-            }
-          );
-          selectService.selectNode(node);
-        }}
-      >
-        + Node
-      </button>
-      <button
-        style={{
-          border: '1px solid #e0e0e0',
-          borderRadius: '50%',
-          cursor: 'pointer',
-          padding: '4px',
-          color: '#ffffff',
-          background: '#7e72e8',
-          width: 70,
-          height: 70,
-          fontSize: 14,
-        }}
-        onClick={() => {
-          const json = LoopNodeRegistry.onAdd();
-          const node: WorkflowNodeEntity = workflowDocument.createWorkflowNodeByType(
-            'loop',
-            undefined, // position undefined means create node in center of canvas - position undefined 可以在画布中间创建节点
-            json
-          );
-          selectService.selectNode(node);
-        }}
-      >
-        + Loop
-      </button>
-      <button
-        style={{
-          border: '1px solid #e0e0e0',
-          borderRadius: '50%',
-          cursor: 'pointer',
-          padding: '4px',
-          color: '#ffffff',
-          background: '#7e72e8',
-          width: 70,
-          height: 70,
-          fontSize: 14,
-        }}
-        onClick={() => {
-          const position = getNodeDefaultPosition(workflowDocument, 'batch');
-
-          const node: WorkflowNodeEntity = workflowDocument.createWorkflowNodeByType(
-            'batch',
-            position, // position undefined means create node in center of canvas - position undefined 可以在画布中间创建节点
-            {
-              data: {
-                title: 'New batch node',
-              },
-            }
-          );
-          createBatchFunction(node, position);
-          selectService.selectNode(node);
-        }}
-      >
-        + Batch
-      </button>
-    </div>
-  );
-};

+ 0 - 29
apps/demo-sub-canvas/src/app.tsx

@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import '@flowgram.ai/free-layout-editor/index.css';
-import { createRoot } from 'react-dom/client';
-import { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';
-
-import { useEditorProps } from './use-editor-props';
-import { Tools } from './tools';
-import { Minimap } from './minimap';
-import { AddNode } from './add-node';
-
-export const FlowGramApp = () => {
-  const editorProps = useEditorProps();
-  return (
-    <FreeLayoutEditorProvider {...editorProps}>
-      <EditorRenderer />
-      <Tools />
-      <Minimap />
-      <AddNode />
-    </FreeLayoutEditorProvider>
-  );
-};
-
-const app = createRoot(document.getElementById('root')!);
-
-app.render(<FlowGramApp />);

+ 0 - 282
apps/demo-sub-canvas/src/initial-data.ts

@@ -1,282 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
-
-export const initialData: WorkflowJSON = {
-  nodes: [
-    {
-      id: '1',
-      type: 'start',
-      meta: {
-        position: {
-          x: 140,
-          y: 240.5,
-        },
-      },
-      data: {
-        title: 'Start Node',
-      },
-    },
-    {
-      id: '5',
-      type: 'end',
-      meta: {
-        position: {
-          x: 2196,
-          y: 313.75,
-        },
-      },
-      data: {
-        title: 'End Node',
-      },
-    },
-    {
-      id: 'loop_yvdFr',
-      type: 'loop',
-      meta: {
-        position: {
-          x: 840,
-          y: 52.5,
-        },
-      },
-      data: {
-        title: 'Loop_1',
-      },
-      blocks: [
-        {
-          id: 'block_start_MYgCG',
-          type: 'block_start',
-          meta: {
-            position: {
-              x: 44,
-              y: 94,
-            },
-          },
-          data: {},
-        },
-        {
-          id: 'block_end_hVfcS',
-          type: 'block_end',
-          meta: {
-            position: {
-              x: 612,
-              y: 94,
-            },
-          },
-          data: {},
-        },
-        {
-          id: '144478',
-          type: 'custom',
-          meta: {
-            position: {
-              x: 328,
-              y: 0,
-            },
-          },
-          data: {
-            title: 'New Node',
-          },
-        },
-        {
-          id: '152294',
-          type: 'custom',
-          meta: {
-            position: {
-              x: 328,
-              y: 188,
-            },
-          },
-          data: {
-            title: 'New Node',
-          },
-        },
-      ],
-      edges: [
-        {
-          sourceNodeID: 'block_start_MYgCG',
-          targetNodeID: '144478',
-        },
-        {
-          sourceNodeID: 'block_start_MYgCG',
-          targetNodeID: '152294',
-        },
-        {
-          sourceNodeID: '144478',
-          targetNodeID: 'block_end_hVfcS',
-        },
-        {
-          sourceNodeID: '152294',
-          targetNodeID: 'block_end_hVfcS',
-        },
-      ],
-    },
-    {
-      id: '190959',
-      type: 'batch',
-      meta: {
-        position: {
-          x: 1168,
-          y: 481,
-        },
-      },
-      data: {
-        title: 'New batch node',
-      },
-    },
-    {
-      id: '172297',
-      type: 'custom',
-      meta: {
-        position: {
-          x: 520,
-          y: 481,
-        },
-      },
-      data: {
-        title: 'New Node',
-      },
-    },
-    {
-      id: 'BatchFunction_190959',
-      type: 'batch_function',
-      meta: {
-        position: {
-          x: 840,
-          y: 721.5,
-        },
-      },
-      data: {},
-      blocks: [
-        {
-          id: 'block_start_fMPc6',
-          type: 'block_start',
-          meta: {
-            position: {
-              x: 44,
-              y: 0,
-            },
-          },
-          data: {},
-        },
-        {
-          id: 'block_end_vSpPm',
-          type: 'block_end',
-          meta: {
-            position: {
-              x: 612,
-              y: 0,
-            },
-          },
-          data: {},
-        },
-        {
-          id: '184839',
-          type: 'custom',
-          meta: {
-            position: {
-              x: 328,
-              y: 0,
-            },
-          },
-          data: {
-            title: 'New Node',
-          },
-        },
-      ],
-      edges: [
-        {
-          sourceNodeID: 'block_start_fMPc6',
-          targetNodeID: '184839',
-        },
-        {
-          sourceNodeID: '184839',
-          targetNodeID: 'block_end_vSpPm',
-        },
-      ],
-    },
-    {
-      id: '153487',
-      type: 'custom',
-      meta: {
-        position: {
-          x: 1816,
-          y: 134,
-        },
-      },
-      data: {
-        title: 'New Node',
-      },
-    },
-    {
-      id: '151923',
-      type: 'custom',
-      meta: {
-        position: {
-          x: 1816,
-          y: 481,
-        },
-      },
-      data: {
-        title: 'New Node',
-      },
-    },
-    {
-      id: '173026',
-      type: 'custom',
-      meta: {
-        position: {
-          x: 520,
-          y: 134,
-        },
-      },
-      data: {
-        title: 'New Node',
-      },
-    },
-  ],
-  edges: [
-    {
-      sourceNodeID: '1',
-      targetNodeID: '173026',
-    },
-    {
-      sourceNodeID: '1',
-      targetNodeID: '172297',
-    },
-    {
-      sourceNodeID: '151923',
-      targetNodeID: '5',
-    },
-    {
-      sourceNodeID: '153487',
-      targetNodeID: '5',
-    },
-    {
-      sourceNodeID: '173026',
-      targetNodeID: 'loop_yvdFr',
-    },
-    {
-      sourceNodeID: 'loop_yvdFr',
-      targetNodeID: '153487',
-    },
-    {
-      sourceNodeID: '172297',
-      targetNodeID: '190959',
-    },
-    {
-      sourceNodeID: '190959',
-      targetNodeID: '151923',
-      sourcePortID: 'batch-output',
-    },
-    {
-      sourceNodeID: '190959',
-      targetNodeID: 'BatchFunction_190959',
-      sourcePortID: 'batch-output-to-function',
-      targetPortID: 'batch-function-input',
-    },
-  ],
-};

+ 0 - 35
apps/demo-sub-canvas/src/minimap.tsx

@@ -1,35 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { MinimapRender } from '@flowgram.ai/minimap-plugin';
-
-export const Minimap = () => (
-  <div
-    style={{
-      position: 'absolute',
-      right: 16,
-      bottom: 72,
-      zIndex: 100,
-      width: 118,
-    }}
-  >
-    <MinimapRender
-      containerStyles={{
-        pointerEvents: 'auto',
-        position: 'relative',
-        top: 'unset',
-        right: 'unset',
-        bottom: 'unset',
-        left: 'unset',
-      }}
-      inactiveStyle={{
-        opacity: 1,
-        scale: 1,
-        translateX: 0,
-        translateY: 0,
-      }}
-    />
-  </div>
-);

+ 0 - 46
apps/demo-sub-canvas/src/node-registries.tsx

@@ -1,46 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
-
-import { LoopNodeRegistry } from './nodes/loop';
-import { BlockStartNodeRegistry } from './nodes/block-start';
-import { BlockEndNodeRegistry } from './nodes/block-end';
-import { BatchFunctionNodeRegistry } from './nodes/batch-function';
-import { BatchNodeRegistry } from './nodes/batch';
-
-/**
- * You can customize your own node registry
- * 你可以自定义节点的注册器
- */
-export const nodeRegistries: WorkflowNodeRegistry[] = [
-  {
-    type: 'start',
-    meta: {
-      isStart: true, // Mark as start
-      deleteDisable: true, // The start node cannot be deleted
-      copyDisable: true, // The start node cannot be copied
-      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port
-    },
-  },
-  {
-    type: 'end',
-    meta: {
-      deleteDisable: true,
-      copyDisable: true,
-      defaultPorts: [{ type: 'input' }],
-    },
-  },
-  {
-    type: 'custom',
-    meta: {},
-    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports
-  },
-  LoopNodeRegistry,
-  BlockStartNodeRegistry,
-  BlockEndNodeRegistry,
-  BatchNodeRegistry,
-  BatchFunctionNodeRegistry,
-];

+ 0 - 40
apps/demo-sub-canvas/src/node-render.tsx

@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import {
-  FlowNodeMeta,
-  useNodeRender,
-  WorkflowNodeProps,
-  WorkflowNodeRenderer,
-} from '@flowgram.ai/free-layout-editor';
-
-export const NodeRender = (props: WorkflowNodeProps) => {
-  const { node, form, selected } = useNodeRender();
-  const meta = node.getNodeMeta<FlowNodeMeta>();
-  return (
-    <WorkflowNodeRenderer
-      style={{
-        minWidth: 280,
-        minHeight: 88,
-        height: 'auto',
-        background: '#fff',
-        border: '1px solid rgba(6, 7, 9, 0.15)',
-        borderColor: selected ? '#4e40e5' : 'rgba(6, 7, 9, 0.15)',
-        borderRadius: 8,
-        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',
-        display: 'flex',
-        flexDirection: 'column',
-        justifyContent: 'center',
-        position: 'relative',
-        padding: 12,
-        cursor: 'move',
-        ...meta.wrapperStyle,
-      }}
-      node={props.node}
-    >
-      {form?.render()}
-    </WorkflowNodeRenderer>
-  );
-};

+ 0 - 112
apps/demo-sub-canvas/src/tools.tsx

@@ -1,112 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { CSSProperties, useEffect, useState } from 'react';
-
-import { usePlaygroundTools, useClientContext, LineType } from '@flowgram.ai/free-layout-editor';
-
-import { getBatchID } from './nodes/batch-function';
-
-export const Tools = () => {
-  const { history } = useClientContext();
-  const tools = usePlaygroundTools();
-  const [canUndo, setCanUndo] = useState(false);
-  const [canRedo, setCanRedo] = useState(false);
-
-  const buttonStyle: CSSProperties = {
-    border: '1px solid #e0e0e0',
-    borderRadius: '4px',
-    cursor: 'pointer',
-    padding: '4px',
-    color: '#141414',
-    background: '#e1e3e4',
-  };
-
-  useEffect(() => {
-    const disposable = history.undoRedoService.onChange(() => {
-      setCanUndo(history.canUndo());
-      setCanRedo(history.canRedo());
-    });
-    return () => disposable.dispose();
-  }, [history]);
-
-  return (
-    <div
-      style={{ position: 'absolute', zIndex: 10, bottom: 34, right: 16, display: 'flex', gap: 8 }}
-    >
-      <button style={buttonStyle} onClick={() => tools.zoomin()}>
-        ZoomIn
-      </button>
-      <button style={buttonStyle} onClick={() => tools.zoomout()}>
-        ZoomOut
-      </button>
-      <span
-        style={{
-          ...buttonStyle,
-          display: 'flex',
-          alignItems: 'center',
-          justifyContent: 'center',
-          cursor: 'default',
-          width: 40,
-        }}
-      >
-        {Math.floor(tools.zoom * 100)}%
-      </span>
-      <button style={buttonStyle} onClick={() => tools.fitView()}>
-        FitView
-      </button>
-      <button
-        style={buttonStyle}
-        onClick={() =>
-          tools.autoLayout({
-            getFollowNode: (node, context) => {
-              if (node.entity.flowNodeType !== 'batch_function') {
-                return;
-              }
-              const batchNodeID = getBatchID(node.entity.id);
-              return {
-                followTo: batchNodeID,
-              };
-            },
-          })
-        }
-      >
-        AutoLayout
-      </button>
-      <button
-        style={buttonStyle}
-        onClick={() =>
-          tools.switchLineType(
-            tools.lineType === LineType.BEZIER ? LineType.LINE_CHART : LineType.BEZIER
-          )
-        }
-      >
-        {tools.lineType === LineType.BEZIER ? 'Bezier' : 'Fold'}
-      </button>
-      <button
-        style={{
-          ...buttonStyle,
-          cursor: canUndo ? 'pointer' : 'not-allowed',
-          color: canUndo ? '#141414' : '#b1b1b1',
-        }}
-        onClick={() => history.undo()}
-        disabled={!canUndo}
-      >
-        Undo
-      </button>
-      <button
-        style={{
-          ...buttonStyle,
-          cursor: canRedo ? 'pointer' : 'not-allowed',
-          color: canRedo ? '#141414' : '#b1b1b1',
-        }}
-        onClick={() => history.redo()}
-        disabled={!canRedo}
-      >
-        Redo
-      </button>
-    </div>
-  );
-};

+ 0 - 97
apps/demo-sub-canvas/src/use-editor-props.tsx

@@ -1,97 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-import { useMemo } from 'react';
-
-import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
-import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';
-import { Field, FreeLayoutPluginContext, FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
-import { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';
-
-import { NodeRender } from './node-render';
-import { nodeRegistries } from './node-registries';
-import { initialData } from './initial-data';
-
-export const useEditorProps = () =>
-  useMemo<FreeLayoutProps>(
-    () => ({
-      plugins: () => [
-        createMinimapPlugin({
-          disableLayer: true,
-          canvasStyle: {
-            canvasWidth: 100,
-            canvasHeight: 50,
-            canvasPadding: 50,
-          },
-        }),
-        createFreeSnapPlugin({}),
-        /**
-         * This is used for the rendering of the loop node sub-canvas
-         * 这个用于 loop 节点子画布的渲染
-         */
-        createContainerNodePlugin({}),
-      ],
-      /**
-       * Content change
-       */ onContentChange: (ctx: FreeLayoutPluginContext, event) => {
-        if (ctx.document.disposed) return;
-
-        console.log('Auto Save: ', event, ctx.document.toJSON());
-      },
-      onAllLayersRendered: (ctx) => {
-        ctx.tools.fitView(false);
-      },
-      materials: {
-        renderDefaultNode: NodeRender,
-      },
-      nodeRegistries,
-      canDeleteNode: () => true,
-      canDeleteLine: (ctx, line) => {
-        if (line.from?.flowNodeType === 'batch' && line.to?.flowNodeType === 'batch_function') {
-          return false;
-        }
-        return true;
-      },
-      isHideArrowLine(ctx, line) {
-        if (line.from?.flowNodeType === 'batch' && line.to?.flowNodeType === 'batch_function') {
-          return true;
-        }
-        return false;
-      },
-      initialData,
-      /**
-       * Node engine enable, you can configure formMeta in the FlowNodeRegistry
-       */
-      nodeEngine: {
-        enable: true,
-      },
-      /**
-       * Redo/Undo enable
-       */
-      history: {
-        enable: true,
-        enableChangeNode: true, // Listen Node engine data change
-      },
-      getNodeDefaultRegistry(type) {
-        return {
-          type,
-          meta: {
-            defaultExpanded: true,
-          },
-          formMeta: {
-            /**
-             * Render form
-             */
-            render: () => (
-              <>
-                <Field<string> name="title">{({ field }) => <div>{field.value}</div>}</Field>
-              </>
-            ),
-          },
-        };
-      },
-    }),
-    []
-  );

+ 0 - 23
apps/demo-sub-canvas/tsconfig.json

@@ -1,23 +0,0 @@
-{
-  "extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
-  "compilerOptions": {
-    "rootDir": "./src",
-    "outDir": "./dist",
-    "experimentalDecorators": true,
-    "target": "es2020",
-    "module": "esnext",
-    "strictPropertyInitialization": false,
-    "strict": true,
-    "esModuleInterop": true,
-    "moduleResolution": "node",
-    "skipLibCheck": true,
-    "noUnusedLocals": true,
-    "noImplicitAny": true,
-    "allowJs": true,
-    "resolveJsonModule": true,
-    "types": ["node"],
-    "jsx": "react-jsx",
-    "lib": ["es6", "dom", "es2020", "es2019.Array"]
-  },
-  "include": ["./src"],
-}

+ 9 - 0
apps/docs/components/free-layout-simple/index.less

@@ -0,0 +1,9 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+.demo-free-node {
+  min-height: unset !important;
+  min-width: unset !important;
+}

+ 2 - 0
apps/docs/components/free-layout-simple/index.tsx

@@ -5,6 +5,8 @@
 
 import React from 'react';
 
+import './index.less';
+
 // https://github.com/web-infra-dev/rspress/issues/553
 const FreeLayoutSimple = React.lazy(() =>
   import('@flowgram.ai/demo-free-layout-simple').then((module) => ({

+ 2 - 2
apps/docs/components/free-layout-simple/preview.tsx

@@ -5,7 +5,7 @@
 
 /* eslint-disable import/no-unresolved */
 
-import nodeRegistriesCode from '@flowgram.ai/demo-free-layout-simple/src/node-registries.tsx?raw';
+import nodesCode from '@flowgram.ai/demo-free-layout-simple/src/nodes/index.ts?raw';
 import dataCode from '@flowgram.ai/demo-free-layout-simple/src/initial-data.ts?raw';
 import useEditorPropsCode from '@flowgram.ai/demo-free-layout-simple/src/hooks/use-editor-props.tsx?raw';
 import editorCode from '@flowgram.ai/demo-free-layout-simple/src/editor.tsx?raw';
@@ -24,7 +24,7 @@ export const FreeLayoutSimplePreview = () => {
     },
     'use-editor-props.tsx': useEditorPropsCode,
     'initial-data.ts': dataCode,
-    'node-registries.ts': nodeRegistriesCode,
+    'nodes/index.ts': nodesCode,
     'node-add-panel.tsx': nodeAddPanelCode,
     'tools.tsx': toolsCode,
     'minimap.tsx': minimapCode,

+ 0 - 8
common/config/rush/command-line.json

@@ -337,14 +337,6 @@
 			"safeForSimultaneousRushProcesses": true,
 			"shellCommand": "concurrently --kill-others --raw --prefix \"{name}\" --names [watch],[demo] -c white,blue \"rush build:watch --to-except @flowgram.ai/demo-free-layout-simple\" \"cd apps/demo-free-layout-simple && rushx ts-check && rushx dev\""
 		},
-		{
-			"name": "dev:demo-sub-canvas",
-			"commandKind": "global",
-			"summary": "⭐️️ run dev in app/demo-sub-canvas",
-			"autoinstallerName": "rush-commands",
-			"safeForSimultaneousRushProcesses": true,
-			"shellCommand": "concurrently --kill-others --raw --prefix \"{name}\" --names [watch],[demo] -c white,blue \"rush build:watch --to-except @flowgram.ai/demo-sub-canvas\" \"cd apps/demo-sub-canvas && rushx ts-check && rushx dev\""
-		},
 		{
 			"name": "dev:demo-nextjs",
 			"commandKind": "global",

+ 3 - 58
common/config/rush/pnpm-lock.yaml

@@ -359,6 +359,9 @@ importers:
 
   ../../apps/demo-free-layout-simple:
     dependencies:
+      '@flowgram.ai/free-container-plugin':
+        specifier: workspace:*
+        version: link:../../packages/plugins/free-container-plugin
       '@flowgram.ai/free-layout-editor':
         specifier: workspace:*
         version: link:../../packages/client/free-layout-editor
@@ -862,64 +865,6 @@ importers:
         specifier: ^5.8.3
         version: 5.9.2
 
-  ../../apps/demo-sub-canvas:
-    dependencies:
-      '@flowgram.ai/free-container-plugin':
-        specifier: workspace:*
-        version: link:../../packages/plugins/free-container-plugin
-      '@flowgram.ai/free-layout-editor':
-        specifier: workspace:*
-        version: link:../../packages/client/free-layout-editor
-      '@flowgram.ai/free-snap-plugin':
-        specifier: workspace:*
-        version: link:../../packages/plugins/free-snap-plugin
-      '@flowgram.ai/minimap-plugin':
-        specifier: workspace:*
-        version: link:../../packages/plugins/minimap-plugin
-      react:
-        specifier: ^18
-        version: 18.3.1
-      react-dom:
-        specifier: ^18
-        version: 18.3.1(react@18.3.1)
-    devDependencies:
-      '@flowgram.ai/eslint-config':
-        specifier: workspace:*
-        version: link:../../config/eslint-config
-      '@flowgram.ai/ts-config':
-        specifier: workspace:*
-        version: link:../../config/ts-config
-      '@rsbuild/core':
-        specifier: ^1.2.16
-        version: 1.5.6
-      '@rsbuild/plugin-react':
-        specifier: ^1.1.1
-        version: 1.4.0(@rsbuild/core@1.5.6)
-      '@types/lodash-es':
-        specifier: ^4.17.12
-        version: 4.17.12
-      '@types/node':
-        specifier: ^18
-        version: 18.19.124
-      '@types/react':
-        specifier: ^18
-        version: 18.3.24
-      '@types/react-dom':
-        specifier: ^18
-        version: 18.3.7(@types/react@18.3.24)
-      '@types/styled-components':
-        specifier: ^5
-        version: 5.1.34
-      cross-env:
-        specifier: ~7.0.3
-        version: 7.0.3
-      eslint:
-        specifier: ^8.54.0
-        version: 8.57.1
-      typescript:
-        specifier: ^5.8.3
-        version: 5.9.2
-
   ../../apps/demo-vite:
     dependencies:
       '@flowgram.ai/form-materials':

+ 0 - 10
rush.json

@@ -962,16 +962,6 @@
                 "demo"
             ]
         },
-        {
-            "packageName": "@flowgram.ai/demo-sub-canvas",
-            "projectFolder": "apps/demo-sub-canvas",
-            "versionPolicyName": "appPolicy",
-            "tags": [
-                "level-1",
-                "team-flow",
-                "demo"
-            ]
-        },
         {
             "packageName": "@flowgram.ai/demo-node-form",
             "projectFolder": "apps/demo-node-form",