Browse Source

feat(history): free layout supports move node into container operation

liuyangxing 10 months ago
parent
commit
304cf69387

+ 21 - 3
packages/canvas-engine/document/src/services/flow-operation-base-service.ts

@@ -17,6 +17,7 @@ import {
   FlowNodeJSON,
   FlowNodeJSON,
   MoveNodeConfig,
   MoveNodeConfig,
   OnNodeAddEvent,
   OnNodeAddEvent,
+  OnNodeMoveEvent,
 } from '../typings';
 } from '../typings';
 import { FlowDocument } from '../flow-document';
 import { FlowDocument } from '../flow-document';
 import { FlowNodeEntity } from '../entities';
 import { FlowNodeEntity } from '../entities';
@@ -38,9 +39,13 @@ export class FlowOperationBaseServiceImpl implements FlowOperationBaseService {
 
 
   protected toDispose = new DisposableCollection();
   protected toDispose = new DisposableCollection();
 
 
+  private onNodeMoveEmitter = new Emitter<OnNodeMoveEvent>();
+
+  readonly onNodeMove = this.onNodeMoveEmitter.event;
+
   @postConstruct()
   @postConstruct()
   protected init() {
   protected init() {
-    this.toDispose.push(this.onNodeAddEmitter);
+    this.toDispose.pushAll([this.onNodeAddEmitter, this.onNodeMoveEmitter]);
   }
   }
 
 
   addNode(nodeJSON: FlowNodeJSON, config: AddNodeConfig = {}): FlowNodeEntity {
   addNode(nodeJSON: FlowNodeJSON, config: AddNodeConfig = {}): FlowNodeEntity {
@@ -127,7 +132,7 @@ export class FlowOperationBaseServiceImpl implements FlowOperationBaseService {
       return;
       return;
     }
     }
 
 
-    let toIndex = typeof index === 'undefined' ? parent.children.length : index;
+    let toIndex = typeof index === 'undefined' ? newParentEntity.collapsedChildren.length : index;
 
 
     return this.doMoveNode(entity, newParentEntity, toIndex);
     return this.doMoveNode(entity, newParentEntity, toIndex);
   }
   }
@@ -301,10 +306,23 @@ export class FlowOperationBaseServiceImpl implements FlowOperationBaseService {
   }
   }
 
 
   protected doMoveNode(node: FlowNodeEntity, newParent: FlowNodeEntity, index: number) {
   protected doMoveNode(node: FlowNodeEntity, newParent: FlowNodeEntity, index: number) {
-    return this.document.moveChildNodes({
+    if (!node.parent) {
+      throw new Error('root node cannot move');
+    }
+
+    const event: OnNodeMoveEvent = {
+      node,
+      fromParent: node.parent,
+      toParent: newParent,
+      fromIndex: this.getNodeIndex(node),
+      toIndex: index,
+    };
+
+    this.document.moveChildNodes({
       nodeIds: [this.toId(node)],
       nodeIds: [this.toId(node)],
       toParentId: this.toId(newParent),
       toParentId: this.toId(newParent),
       toIndex: index,
       toIndex: index,
     });
     });
+    this.onNodeMoveEmitter.fire(event);
   }
   }
 }
 }

+ 16 - 0
packages/canvas-engine/document/src/typings/flow-operation.ts

@@ -233,6 +233,17 @@ export interface OnNodeAddEvent {
   data: AddNodeData;
   data: AddNodeData;
 }
 }
 
 
+/**
+ * 节点移动事件
+ */
+export interface OnNodeMoveEvent {
+  node: FlowNodeEntity;
+  fromParent: FlowNodeEntity;
+  fromIndex: number;
+  toParent: FlowNodeEntity;
+  toIndex: number;
+}
+
 export interface FlowOperationBaseService extends Disposable {
 export interface FlowOperationBaseService extends Disposable {
   /**
   /**
    * 执行操作
    * 执行操作
@@ -300,6 +311,11 @@ export interface FlowOperationBaseService extends Disposable {
    * 添加节点的回调
    * 添加节点的回调
    */
    */
   onNodeAdd: Event<OnNodeAddEvent>;
   onNodeAdd: Event<OnNodeAddEvent>;
+
+  /**
+   * 节点移动的回调
+   */
+  onNodeMove: Event<OnNodeMoveEvent>;
 }
 }
 
 
 export const FlowOperationBaseService = Symbol('FlowOperationBaseService');
 export const FlowOperationBaseService = Symbol('FlowOperationBaseService');

+ 1 - 0
packages/canvas-engine/free-layout-core/src/service/index.ts

@@ -2,3 +2,4 @@ export * from './workflow-select-service';
 export * from './workflow-hover-service';
 export * from './workflow-hover-service';
 export * from './workflow-drag-service';
 export * from './workflow-drag-service';
 export * from './workflow-reset-layout-service';
 export * from './workflow-reset-layout-service';
+export * from './workflow-operation-base-service';

+ 45 - 0
packages/canvas-engine/free-layout-core/src/service/workflow-operation-base-service.ts

@@ -0,0 +1,45 @@
+import { inject } from 'inversify';
+import { IPoint, Emitter } from '@flowgram.ai/utils';
+import { FlowNodeEntityOrId, FlowOperationBaseServiceImpl } from '@flowgram.ai/document';
+import { TransformData } from '@flowgram.ai/core';
+
+import { WorkflowDocument } from '../workflow-document';
+import {
+  NodePostionUpdateEvent,
+  WorkflowOperationBaseService,
+} from '../typings/workflow-operation';
+
+export class WorkflowOperationBaseServiceImpl
+  extends FlowOperationBaseServiceImpl
+  implements WorkflowOperationBaseService
+{
+  @inject(WorkflowDocument)
+  protected declare document: WorkflowDocument;
+
+  private onNodePostionUpdateEmitter = new Emitter<NodePostionUpdateEvent>();
+
+  public readonly onNodePostionUpdate = this.onNodePostionUpdateEmitter.event;
+
+  updateNodePosition(nodeOrId: FlowNodeEntityOrId, position: IPoint): void {
+    const node = this.toNodeEntity(nodeOrId);
+
+    if (!node) {
+      return;
+    }
+
+    const transformData = node.getData(TransformData);
+    const oldPosition = {
+      x: transformData.position.x,
+      y: transformData.position.y,
+    };
+    transformData.update({
+      position,
+    });
+
+    this.onNodePostionUpdateEmitter.fire({
+      node,
+      oldPosition,
+      newPosition: position,
+    });
+  }
+}

+ 1 - 0
packages/canvas-engine/free-layout-core/src/typings/index.ts

@@ -4,6 +4,7 @@ export * from './workflow-node';
 export * from './workflow-registry';
 export * from './workflow-registry';
 export * from './workflow-line';
 export * from './workflow-line';
 export * from './workflow-sub-canvas';
 export * from './workflow-sub-canvas';
+export * from './workflow-operation';
 
 
 export const URLParams = Symbol('');
 export const URLParams = Symbol('');
 
 

+ 28 - 0
packages/canvas-engine/free-layout-core/src/typings/workflow-operation.ts

@@ -0,0 +1,28 @@
+import { IPoint, Event } from '@flowgram.ai/utils';
+import {
+  FlowNodeEntity,
+  FlowNodeEntityOrId,
+  FlowOperationBaseService,
+} from '@flowgram.ai/document';
+
+export interface NodePostionUpdateEvent {
+  node: FlowNodeEntity;
+  oldPosition: IPoint;
+  newPosition: IPoint;
+}
+
+export interface WorkflowOperationBaseService extends FlowOperationBaseService {
+  /**
+   * 节点位置更新事件
+   */
+  readonly onNodePostionUpdate: Event<NodePostionUpdateEvent>;
+  /**
+   * 更新节点位置
+   * @param nodeOrId
+   * @param position
+   * @returns
+   */
+  updateNodePosition(nodeOrId: FlowNodeEntityOrId, position: IPoint): void;
+}
+
+export const WorkflowOperationBaseService = Symbol('WorkflowOperationBaseService');

+ 6 - 4
packages/canvas-engine/free-layout-core/src/workflow-document-container-module.ts

@@ -1,6 +1,6 @@
 import { ContainerModule } from 'inversify';
 import { ContainerModule } from 'inversify';
-import { FlowDocument, FlowDocumentContribution } from '@flowgram.ai/document';
 import { bindContributions } from '@flowgram.ai/utils';
 import { bindContributions } from '@flowgram.ai/utils';
+import { FlowDocument, FlowDocumentContribution } from '@flowgram.ai/document';
 
 
 import { WorkflowLinesManager } from './workflow-lines-manager';
 import { WorkflowLinesManager } from './workflow-lines-manager';
 import {
 import {
@@ -10,12 +10,13 @@ import {
 import { WorkflowDocumentContribution } from './workflow-document-contribution';
 import { WorkflowDocumentContribution } from './workflow-document-contribution';
 import { WorkflowDocument, WorkflowDocumentProvider } from './workflow-document';
 import { WorkflowDocument, WorkflowDocumentProvider } from './workflow-document';
 import { getUrlParams } from './utils/get-url-params';
 import { getUrlParams } from './utils/get-url-params';
-import { URLParams } from './typings';
+import { URLParams, WorkflowOperationBaseService } from './typings';
 import {
 import {
   WorkflowDragService,
   WorkflowDragService,
   WorkflowHoverService,
   WorkflowHoverService,
   WorkflowSelectService,
   WorkflowSelectService,
   WorkflowResetLayoutService,
   WorkflowResetLayoutService,
+  WorkflowOperationBaseServiceImpl,
 } from './service';
 } from './service';
 import { FreeLayout } from './layout';
 import { FreeLayout } from './layout';
 
 
@@ -28,6 +29,7 @@ export const WorkflowDocumentContainerModule = new ContainerModule(
     bind(WorkflowSelectService).toSelf().inSingletonScope();
     bind(WorkflowSelectService).toSelf().inSingletonScope();
     bind(WorkflowHoverService).toSelf().inSingletonScope();
     bind(WorkflowHoverService).toSelf().inSingletonScope();
     bind(WorkflowResetLayoutService).toSelf().inSingletonScope();
     bind(WorkflowResetLayoutService).toSelf().inSingletonScope();
+    bind(WorkflowOperationBaseService).to(WorkflowOperationBaseServiceImpl).inSingletonScope();
     bind(URLParams)
     bind(URLParams)
       .toDynamicValue(() => getUrlParams())
       .toDynamicValue(() => getUrlParams())
       .inSingletonScope();
       .inSingletonScope();
@@ -37,7 +39,7 @@ export const WorkflowDocumentContainerModule = new ContainerModule(
     });
     });
     rebind(FlowDocument).toService(WorkflowDocument);
     rebind(FlowDocument).toService(WorkflowDocument);
     bind(WorkflowDocumentProvider)
     bind(WorkflowDocumentProvider)
-      .toDynamicValue(ctx => () => ctx.container.get(WorkflowDocument))
+      .toDynamicValue((ctx) => () => ctx.container.get(WorkflowDocument))
       .inSingletonScope();
       .inSingletonScope();
-  },
+  }
 );
 );

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

@@ -105,6 +105,32 @@ describe('history-operation-service moveNode', () => {
     expect(getNodeChildrenIds(root)).toEqual(['start_0', 'dynamicSplit_0', 'end_0']);
     expect(getNodeChildrenIds(root)).toEqual(['start_0', 'dynamicSplit_0', 'end_0']);
   });
   });
 
 
+  it('move node with parent and without index', async () => {
+    const root = flowDocument.getNode('root');
+    const block0 = flowDocument.getNode('block_0');
+
+    flowOperationService.addNode(
+      { id: 'test0', type: 'test' },
+      {
+        parent: block0,
+      }
+    );
+
+    flowDocument.addFromNode('start_0', {
+      type: 'test',
+      id: 'test1',
+    });
+
+    flowOperationService.moveNode('test1', { parent: 'block_0' });
+
+    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'dynamicSplit_0', 'end_0']);
+    expect(getNodeChildrenIds(block0)).toEqual(['$blockOrderIcon$block_0', 'test0', 'test1']);
+
+    await historyService.undo();
+    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'test1', 'dynamicSplit_0', 'end_0']);
+    expect(getNodeChildrenIds(block0)).toEqual(['$blockOrderIcon$block_0', 'test0']);
+  });
+
   it('move node with parent and index', async () => {
   it('move node with parent and index', async () => {
     const root = flowDocument.getNode('root');
     const root = flowDocument.getNode('root');
     flowDocument.addFromNode('dynamicSplit_0', {
     flowDocument.addFromNode('dynamicSplit_0', {

+ 39 - 1
packages/plugins/free-history-plugin/src/free-history-manager.ts

@@ -7,12 +7,14 @@ import {
   WorkflowDocument,
   WorkflowDocument,
   WorkflowResetLayoutService,
   WorkflowResetLayoutService,
   WorkflowDragService,
   WorkflowDragService,
+  WorkflowOperationBaseService,
 } from '@flowgram.ai/free-layout-core';
 } from '@flowgram.ai/free-layout-core';
 import { FlowNodeFormData } from '@flowgram.ai/form-core';
 import { FlowNodeFormData } from '@flowgram.ai/form-core';
 import { FormManager } from '@flowgram.ai/form-core';
 import { FormManager } from '@flowgram.ai/form-core';
+import { OperationType } from '@flowgram.ai/document';
 import { type PluginContext, PositionData } from '@flowgram.ai/core';
 import { type PluginContext, PositionData } from '@flowgram.ai/core';
 
 
-import { type FreeHistoryPluginOptions, FreeOperationType } from './types';
+import { DragNodeOperationValue, type FreeHistoryPluginOptions, FreeOperationType } from './types';
 import { HistoryEntityManager } from './history-entity-manager';
 import { HistoryEntityManager } from './history-entity-manager';
 import { DragNodesHandler } from './handlers/drag-nodes-handler';
 import { DragNodesHandler } from './handlers/drag-nodes-handler';
 import { ChangeNodeDataHandler } from './handlers/change-node-data-handler';
 import { ChangeNodeDataHandler } from './handlers/change-node-data-handler';
@@ -40,6 +42,9 @@ export class FreeHistoryManager {
 
 
   private _toDispose: DisposableCollection = new DisposableCollection();
   private _toDispose: DisposableCollection = new DisposableCollection();
 
 
+  @inject(WorkflowOperationBaseService)
+  private _operationService: WorkflowOperationBaseService;
+
   onInit(ctx: PluginContext, opts: FreeHistoryPluginOptions) {
   onInit(ctx: PluginContext, opts: FreeHistoryPluginOptions) {
     const document = ctx.get<WorkflowDocument>(WorkflowDocument);
     const document = ctx.get<WorkflowDocument>(WorkflowDocument);
     const historyService = ctx.get<HistoryService>(HistoryService);
     const historyService = ctx.get<HistoryService>(HistoryService);
@@ -101,6 +106,39 @@ export class FreeHistoryManager {
           { noApply: true }
           { noApply: true }
         );
         );
       }),
       }),
+      this._operationService.onNodeMove(({ node, fromParent, fromIndex, toParent, toIndex }) => {
+        historyService.pushOperation(
+          {
+            type: OperationType.moveChildNodes,
+            value: {
+              fromParentId: fromParent.id,
+              fromIndex,
+              nodeIds: [node.id],
+              toParentId: toParent.id,
+              toIndex,
+            },
+          },
+          {
+            noApply: true,
+          }
+        );
+      }),
+      this._operationService.onNodePostionUpdate((event) => {
+        const value: DragNodeOperationValue = {
+          ids: [event.node.id],
+          value: [event.newPosition],
+          oldValue: [event.oldPosition],
+        };
+        historyService.pushOperation(
+          {
+            type: FreeOperationType.dragNodes,
+            value,
+          },
+          {
+            noApply: true,
+          }
+        );
+      }),
     ]);
     ]);
   }
   }
 
 

+ 4 - 5
packages/plugins/free-history-plugin/src/operation-metas/drag-nodes.ts

@@ -1,7 +1,7 @@
+import { type OperationMeta } from '@flowgram.ai/history';
+import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
 import { FlowNodeTransformData } from '@flowgram.ai/document';
 import { FlowNodeTransformData } from '@flowgram.ai/document';
 import { type PluginContext, TransformData } from '@flowgram.ai/core';
 import { type PluginContext, TransformData } from '@flowgram.ai/core';
-import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
-import { type OperationMeta } from '@flowgram.ai/history';
 
 
 import { type DragNodeOperationValue, FreeOperationType } from '../types';
 import { type DragNodeOperationValue, FreeOperationType } from '../types';
 import { baseOperationMeta } from './base';
 import { baseOperationMeta } from './base';
@@ -9,7 +9,7 @@ import { baseOperationMeta } from './base';
 export const dragNodesOperationMeta: OperationMeta<DragNodeOperationValue, PluginContext, void> = {
 export const dragNodesOperationMeta: OperationMeta<DragNodeOperationValue, PluginContext, void> = {
   ...baseOperationMeta,
   ...baseOperationMeta,
   type: FreeOperationType.dragNodes,
   type: FreeOperationType.dragNodes,
-  inverse: op => ({
+  inverse: (op) => ({
     ...op,
     ...op,
     value: {
     value: {
       ...op.value,
       ...op.value,
@@ -35,7 +35,7 @@ export const dragNodesOperationMeta: OperationMeta<DragNodeOperationValue, Plugi
       });
       });
       // 嵌套情况下需将子节点 transform 设为 dirty
       // 嵌套情况下需将子节点 transform 设为 dirty
       if (node.collapsedChildren?.length > 0) {
       if (node.collapsedChildren?.length > 0) {
-        node.collapsedChildren.forEach(childNode => {
+        node.collapsedChildren.forEach((childNode) => {
           const childNodeTransformData =
           const childNodeTransformData =
             childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
             childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
           childNodeTransformData.fireChange();
           childNodeTransformData.fireChange();
@@ -43,5 +43,4 @@ export const dragNodesOperationMeta: OperationMeta<DragNodeOperationValue, Plugi
       }
       }
     });
     });
   },
   },
-  shouldMerge: () => false,
 };
 };

+ 2 - 0
packages/plugins/free-history-plugin/src/operation-metas/index.ts

@@ -1,4 +1,5 @@
 import { resetLayoutOperationMeta } from './reset-layout';
 import { resetLayoutOperationMeta } from './reset-layout';
+import { moveChildNodesOperationMeta } from './move-child-nodes';
 import { dragNodesOperationMeta } from './drag-nodes';
 import { dragNodesOperationMeta } from './drag-nodes';
 import { deleteNodeOperationMeta } from './delete-node';
 import { deleteNodeOperationMeta } from './delete-node';
 import { deleteLineOperationMeta } from './delete-line';
 import { deleteLineOperationMeta } from './delete-line';
@@ -14,4 +15,5 @@ export const operationMetas = [
   changeNodeDataOperationMeta,
   changeNodeDataOperationMeta,
   resetLayoutOperationMeta,
   resetLayoutOperationMeta,
   dragNodesOperationMeta,
   dragNodesOperationMeta,
+  moveChildNodesOperationMeta,
 ];
 ];

+ 37 - 0
packages/plugins/free-history-plugin/src/operation-metas/move-child-nodes.ts

@@ -0,0 +1,37 @@
+import { OperationMeta } from '@flowgram.ai/history';
+import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
+import { MoveChildNodesOperationValue, OperationType } from '@flowgram.ai/document';
+import { FlowNodeBaseType } from '@flowgram.ai/document';
+import { PluginContext, TransformData } from '@flowgram.ai/core';
+
+import { baseOperationMeta } from './base';
+
+export const moveChildNodesOperationMeta: OperationMeta<
+  MoveChildNodesOperationValue,
+  PluginContext,
+  void
+> = {
+  ...baseOperationMeta,
+  type: OperationType.moveChildNodes,
+  inverse: (op) => ({
+    ...op,
+    value: {
+      ...op.value,
+      fromIndex: op.value.toIndex,
+      toIndex: op.value.fromIndex,
+      fromParentId: op.value.toParentId,
+      toParentId: op.value.fromParentId,
+    },
+  }),
+  apply: (operation, ctx: PluginContext) => {
+    const document = ctx.get<WorkflowDocument>(WorkflowDocument);
+    document.moveChildNodes(operation.value);
+    const fromContainer = document.getNode(operation.value.fromParentId);
+    requestAnimationFrame(() => {
+      if (fromContainer && fromContainer.flowNodeType !== FlowNodeBaseType.ROOT) {
+        const fromContainerTransformData = fromContainer.getData(TransformData);
+        fromContainerTransformData.fireChange();
+      }
+    });
+  },
+};

+ 1 - 0
packages/plugins/free-history-plugin/src/types.ts

@@ -21,6 +21,7 @@ export enum FreeOperationType {
   changeNodeData = 'changeNodeData',
   changeNodeData = 'changeNodeData',
   resetLayout = 'resetLayout',
   resetLayout = 'resetLayout',
   dragNodes = 'dragNodes',
   dragNodes = 'dragNodes',
+  moveChildNodes = 'moveChildNodes',
 }
 }
 
 
 export interface AddOrDeleteLineOperationValue extends WorkflowLinePortInfo {
 export interface AddOrDeleteLineOperationValue extends WorkflowLinePortInfo {