Bläddra i källkod

feat(demo-fixed-layout): add case node for condition branches (#302)

* refactor: FlowNodeRegistry.addChild depreacted

* feat: FlowDocument.toString(true) support showType

* feat(demo-fixed-layout): add case node and catch-block node
xiamidaxia 7 månader sedan
förälder
incheckning
f0d9c5062c

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

@@ -15,7 +15,7 @@ export function FlowSelect() {
       clientContext.history.stop(); // Stop redo/undo
       clientContext.history.clear(); // Clear redo/undo
       clientContext.document.fromJSON(targetDemoJSON); // Reload Data
-      console.log(clientContext.document.toString()); // Print the document tree
+      console.log(clientContext.document.toString(true)); // Print the document tree
       clientContext.history.start(); // Restart redo/undo
       clientContext.document.setLayout(
         targetDemoJSON.defaultLayout || FlowLayoutDefault.VERTICAL_FIXED_LAYOUT

+ 8 - 2
apps/demo-fixed-layout/src/components/branch-adder/index.tsx

@@ -1,7 +1,8 @@
 import { type FlowNodeEntity, useClientContext } from '@flowgram.ai/fixed-layout-editor';
 import { IconPlus } from '@douyinfe/semi-icons';
 
-import { BlockNodeRegistry } from '../../nodes/block';
+import { CatchBlockNodeRegistry } from '../../nodes/catch-block';
+import { CaseNodeRegistry } from '../../nodes/case';
 import { Container } from './styles';
 
 interface PropsType {
@@ -17,7 +18,12 @@ export default function BranchAdder(props: PropsType) {
   const { isVertical } = node;
 
   function addBranch() {
-    const block = operation.addBlock(node, BlockNodeRegistry.onAdd!(ctx, node));
+    const block = operation.addBlock(
+      node,
+      node.flowNodeType === 'condition'
+        ? CaseNodeRegistry.onAdd!(ctx, node)
+        : CatchBlockNodeRegistry.onAdd!(ctx, node)
+    );
 
     setTimeout(() => {
       playground.scrollToView({

+ 5 - 4
apps/demo-fixed-layout/src/hooks/use-editor-props.ts

@@ -1,5 +1,6 @@
 import { useMemo } from 'react';
 
+import { debounce } from 'lodash-es';
 import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
 import { createGroupPlugin } from '@flowgram.ai/group-plugin';
 import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
@@ -139,10 +140,10 @@ export function useEditorProps(
       history: {
         enable: true,
         enableChangeNode: true, // Listen Node engine data change
-        onApply(ctx, opt) {
+        onApply: debounce((ctx, opt) => {
           // Listen change to trigger auto save
-          // console.log('auto save: ', ctx.document.toJSON(), opt);
-        },
+          console.log('auto save: ', ctx.document.toJSON());
+        }, 100),
       },
       /**
        * Node engine enable, you can configure formMeta in the FlowNodeRegistry
@@ -201,7 +202,7 @@ export function useEditorProps(
         setTimeout(() => {
           ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
         }, 10);
-        console.log(ctx.document.toString()); // Get the document tree
+        console.log(ctx.document.toString(true)); // Get the document tree
       },
       /**
        * Playground dispose

+ 4 - 4
apps/demo-fixed-layout/src/initial-data.ts

@@ -106,8 +106,8 @@ export const initialData: FlowDocumentJSON = {
           },
           blocks: [
             {
-              id: 'branch_0',
-              type: 'block',
+              id: 'case_0',
+              type: 'case',
               data: {
                 title: 'If_0',
                 inputsValues: {
@@ -126,8 +126,8 @@ export const initialData: FlowDocumentJSON = {
               blocks: [],
             },
             {
-              id: 'branch_1',
-              type: 'block',
+              id: 'case_1',
+              type: 'case',
               data: {
                 title: 'If_1',
                 inputsValues: {

+ 0 - 0
apps/demo-fixed-layout/src/nodes/block/form-meta.tsx → apps/demo-fixed-layout/src/nodes/case/form-meta.tsx


+ 10 - 11
apps/demo-fixed-layout/src/nodes/block/index.ts → apps/demo-fixed-layout/src/nodes/case/index.ts

@@ -5,8 +5,13 @@ import iconIf from '../../assets/icon-if.png';
 import { formMeta } from './form-meta';
 
 let id = 2;
-export const BlockNodeRegistry: FlowNodeRegistry = {
-  type: 'block',
+export const CaseNodeRegistry: FlowNodeRegistry = {
+  type: 'case',
+  /**
+   * 分支节点需要继承自 block
+   * Branch nodes need to inherit from 'block'
+   */
+  extend: 'block',
   meta: {
     copyDisable: true,
   },
@@ -15,19 +20,13 @@ export const BlockNodeRegistry: FlowNodeRegistry = {
     description: 'Execute the branch when the condition is met.',
   },
   canAdd: () => false,
-  canDelete: (ctx, node) => {
-    if (node.originParent!.flowNodeType === 'tryCatch') {
-      return node.parent!.blocks.length >= 2;
-    }
-    return node.parent!.blocks.length >= 3;
-  },
+  canDelete: (ctx, node) => node.parent!.blocks.length >= 3,
   onAdd(ctx, from) {
-    const isTryCatch = from.flowNodeType === 'tryCatch';
     return {
       id: `if_${nanoid(5)}`,
-      type: isTryCatch ? 'catchBlock' : 'block',
+      type: 'case',
       data: {
-        title: isTryCatch ? `Catch Block ${id++}` : `If_${id++}`,
+        title: `If_${id++}`,
         inputs: {
           type: 'object',
           required: ['condition'],

+ 32 - 0
apps/demo-fixed-layout/src/nodes/catch-block/form-meta.tsx

@@ -0,0 +1,32 @@
+import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
+
+import { FlowNodeJSON } from '../../typings';
+import { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';
+
+export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (
+  <>
+    <FormHeader />
+    <FormContent>
+      <FormInputs />
+      <FormOutputs />
+    </FormContent>
+  </>
+);
+
+export const formMeta: FormMeta<FlowNodeJSON['data']> = {
+  render: renderForm,
+  validateTrigger: ValidateTrigger.onChange,
+  validate: {
+    'inputsValues.*': ({ value, context, formValues, name }) => {
+      const valuePropetyKey = name.replace(/^inputsValues\./, '');
+      const required = formValues.inputs?.required || [];
+      if (
+        required.includes(valuePropetyKey) &&
+        (value === '' || value === undefined || value?.content === '')
+      ) {
+        return `${valuePropetyKey} is required`;
+      }
+      return undefined;
+    },
+  },
+};

+ 41 - 0
apps/demo-fixed-layout/src/nodes/catch-block/index.ts

@@ -0,0 +1,41 @@
+import { nanoid } from 'nanoid';
+
+import { FlowNodeRegistry } from '../../typings';
+import iconIf from '../../assets/icon-if.png';
+import { formMeta } from './form-meta';
+
+let id = 3;
+export const CatchBlockNodeRegistry: FlowNodeRegistry = {
+  type: 'catchBlock',
+  meta: {
+    copyDisable: true,
+  },
+  info: {
+    icon: iconIf,
+    description: 'Execute the catch branch when the condition is met.',
+  },
+  canAdd: () => false,
+  canDelete: (ctx, node) => node.parent!.blocks.length >= 2,
+  onAdd(ctx, from) {
+    return {
+      id: `Catch_${nanoid(5)}`,
+      type: 'catchblock',
+      data: {
+        title: `Catch Block ${id++}`,
+        inputs: {
+          type: 'object',
+          required: ['condition'],
+          inputsValues: {
+            condition: '',
+          },
+          properties: {
+            condition: {
+              type: 'string',
+            },
+          },
+        },
+      },
+    };
+  },
+  formMeta,
+};

+ 2 - 2
apps/demo-fixed-layout/src/nodes/condition/index.ts

@@ -27,7 +27,7 @@ export const ConditionNodeRegistry: FlowNodeRegistry = {
       blocks: [
         {
           id: nanoid(5),
-          type: 'block',
+          type: 'case',
           data: {
             title: 'If_0',
             inputsValues: {
@@ -47,7 +47,7 @@ export const ConditionNodeRegistry: FlowNodeRegistry = {
         },
         {
           id: nanoid(5),
-          type: 'block',
+          type: 'case',
           data: {
             title: 'If_1',
             inputsValues: {

+ 4 - 2
apps/demo-fixed-layout/src/nodes/index.ts

@@ -5,7 +5,8 @@ import { LoopNodeRegistry } from './loop';
 import { LLMNodeRegistry } from './llm';
 import { EndNodeRegistry } from './end';
 import { ConditionNodeRegistry } from './condition';
-import { BlockNodeRegistry } from './block';
+import { CatchBlockNodeRegistry } from './catch-block';
+import { CaseNodeRegistry } from './case';
 
 export const FlowNodeRegistries: FlowNodeRegistry[] = [
   StartNodeRegistry,
@@ -13,6 +14,7 @@ export const FlowNodeRegistries: FlowNodeRegistry[] = [
   ConditionNodeRegistry,
   LLMNodeRegistry,
   LoopNodeRegistry,
-  BlockNodeRegistry,
+  CaseNodeRegistry,
   TryCatchNodeRegistry,
+  CatchBlockNodeRegistry,
 ];

+ 12 - 1
apps/docs/src/zh/guide/question.mdx

@@ -1,3 +1,14 @@
 # 常见问题
 
-todo
+
+## 运行报报错
+
+
+## 如何修改节点的数据
+
+
+## 是否支持 vue
+
+
+##
+

+ 2 - 2
packages/canvas-engine/document/src/flow-document.ts

@@ -581,8 +581,8 @@ export class FlowDocument<T = FlowDocumentJSON> implements Disposable {
     return this.entityManager.getEntities(FlowNodeEntity);
   }
 
-  toString(): string {
-    return this.originTree.toString();
+  toString(showType?: boolean): string {
+    return this.originTree.toString(showType);
   }
 
   /**

+ 11 - 3
packages/canvas-engine/document/src/flow-virtual-tree.ts

@@ -1,10 +1,14 @@
 import { type Disposable, Emitter } from '@flowgram.ai/utils';
 
+import { type FlowNodeType } from './typings';
+
 /**
  * 存储节点的 tree 结构信息
  * 策略是 "重修改轻查询",即修改时候做的事情更多,查询都通过指针来操作
  */
-export class FlowVirtualTree<T extends { id: string }> implements Disposable {
+export class FlowVirtualTree<T extends { id: string; flowNodeType?: FlowNodeType }>
+  implements Disposable
+{
   protected onTreeChangeEmitter = new Emitter<void>();
 
   /**
@@ -208,13 +212,17 @@ export class FlowVirtualTree<T extends { id: string }> implements Disposable {
     return this.map.size;
   }
 
-  toString(): string {
+  toString(showType?: boolean): string {
     const ret: string[] = [];
     this.traverse((node, depth) => {
       if (depth === 0) {
         ret.push(node.id);
       } else {
-        ret.push(`|${new Array(depth).fill('--').join('')} ${node.id}`);
+        ret.push(
+          `|${new Array(depth).fill('--').join('')} ${
+            showType ? `${node.flowNodeType}(${node.id})` : node.id
+          }`
+        );
       }
     });
     return `${ret.join('\n')}`;

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

@@ -261,6 +261,7 @@ export interface FlowNodeRegistry<M extends FlowNodeMeta = FlowNodeMeta> {
   extendChildRegistries?: FlowNodeRegistry[];
 
   /**
+   * @deprecated
    * 自定义子节点添加逻辑
    * @param node 节点
    * @param json 添加的节点 JSON

+ 3 - 0
packages/canvas-engine/fixed-layout-core/src/activities/block.ts

@@ -171,6 +171,9 @@ export const BlockRegistry: FlowNodeRegistry = {
     return [...draggingLabel];
   },
 
+  /**
+   * @depreacted
+   */
   addChild(node, json, options = {}) {
     const { index } = options;
     const document = node.document;

+ 3 - 0
packages/canvas-engine/fixed-layout-core/src/activities/dynamic-split.ts

@@ -68,6 +68,9 @@ export const DynamicSplitRegistry: FlowNodeRegistry = {
     };
   },
 
+  /**
+   * @depreacted
+   */
   addChild(node, json, options = {}) {
     const { index } = options;
     const document = node.document;

+ 3 - 0
packages/canvas-engine/fixed-layout-core/src/activities/loop.ts

@@ -177,6 +177,9 @@ export const LoopRegistry: FlowNodeRegistry = {
     LoopInlineBlocksNodeRegistry,
   ],
 
+  /**
+   * @depreacted
+   */
   addChild(node, json, options = {}) {
     const { index } = options;
     const document = node.document;

+ 2 - 2
packages/canvas-engine/fixed-layout-core/src/activities/try-catch.ts

@@ -60,7 +60,7 @@ export const TryCatchRegistry: FlowNodeRegistry = {
     });
     const tryBlockNode = document.addNode({
       id: tryBlock.id,
-      type: TryCatchTypeEnum.TRY_BLOCK,
+      type: tryBlock.type || TryCatchTypeEnum.TRY_BLOCK,
       originParent: node,
       parent: mainBlockNode,
       data: tryBlock.data,
@@ -109,7 +109,7 @@ export const TryCatchRegistry: FlowNodeRegistry = {
     const parent = node.document.getNode(`$catchInlineBlocks$${node.id}`);
     const block = node.document.addNode({
       id: blockData.id,
-      type: TryCatchTypeEnum.CATCH_BLOCK,
+      type: node.type || TryCatchTypeEnum.CATCH_BLOCK,
       originParent: node,
       parent,
       data: blockData.data,