From 304cf69387ffe9687813f1b416ce2962da16c081 Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 16:58:39 +0800 Subject: [PATCH 01/22] feat(history): free layout supports move node into container operation --- .../services/flow-operation-base-service.ts | 24 ++++++++-- .../document/src/typings/flow-operation.ts | 16 +++++++ .../free-layout-core/src/service/index.ts | 1 + .../workflow-operation-base-service.ts | 45 +++++++++++++++++++ .../free-layout-core/src/typings/index.ts | 1 + .../src/typings/workflow-operation.ts | 28 ++++++++++++ .../src/workflow-document-container-module.ts | 10 +++-- .../move-node.test.ts | 26 +++++++++++ .../src/free-history-manager.ts | 40 ++++++++++++++++- .../src/operation-metas/drag-nodes.ts | 9 ++-- .../src/operation-metas/index.ts | 2 + .../src/operation-metas/move-child-nodes.ts | 37 +++++++++++++++ .../plugins/free-history-plugin/src/types.ts | 1 + 13 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 packages/canvas-engine/free-layout-core/src/service/workflow-operation-base-service.ts create mode 100644 packages/canvas-engine/free-layout-core/src/typings/workflow-operation.ts create mode 100644 packages/plugins/free-history-plugin/src/operation-metas/move-child-nodes.ts diff --git a/packages/canvas-engine/document/src/services/flow-operation-base-service.ts b/packages/canvas-engine/document/src/services/flow-operation-base-service.ts index 0217bf1c..d9986423 100644 --- a/packages/canvas-engine/document/src/services/flow-operation-base-service.ts +++ b/packages/canvas-engine/document/src/services/flow-operation-base-service.ts @@ -17,6 +17,7 @@ import { FlowNodeJSON, MoveNodeConfig, OnNodeAddEvent, + OnNodeMoveEvent, } from '../typings'; import { FlowDocument } from '../flow-document'; import { FlowNodeEntity } from '../entities'; @@ -38,9 +39,13 @@ export class FlowOperationBaseServiceImpl implements FlowOperationBaseService { protected toDispose = new DisposableCollection(); + private onNodeMoveEmitter = new Emitter(); + + readonly onNodeMove = this.onNodeMoveEmitter.event; + @postConstruct() protected init() { - this.toDispose.push(this.onNodeAddEmitter); + this.toDispose.pushAll([this.onNodeAddEmitter, this.onNodeMoveEmitter]); } addNode(nodeJSON: FlowNodeJSON, config: AddNodeConfig = {}): FlowNodeEntity { @@ -127,7 +132,7 @@ export class FlowOperationBaseServiceImpl implements FlowOperationBaseService { 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); } @@ -301,10 +306,23 @@ export class FlowOperationBaseServiceImpl implements FlowOperationBaseService { } 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)], toParentId: this.toId(newParent), toIndex: index, }); + this.onNodeMoveEmitter.fire(event); } } diff --git a/packages/canvas-engine/document/src/typings/flow-operation.ts b/packages/canvas-engine/document/src/typings/flow-operation.ts index fafc9cf5..d549d3ae 100644 --- a/packages/canvas-engine/document/src/typings/flow-operation.ts +++ b/packages/canvas-engine/document/src/typings/flow-operation.ts @@ -233,6 +233,17 @@ export interface OnNodeAddEvent { data: AddNodeData; } +/** + * 节点移动事件 + */ +export interface OnNodeMoveEvent { + node: FlowNodeEntity; + fromParent: FlowNodeEntity; + fromIndex: number; + toParent: FlowNodeEntity; + toIndex: number; +} + export interface FlowOperationBaseService extends Disposable { /** * 执行操作 @@ -300,6 +311,11 @@ export interface FlowOperationBaseService extends Disposable { * 添加节点的回调 */ onNodeAdd: Event; + + /** + * 节点移动的回调 + */ + onNodeMove: Event; } export const FlowOperationBaseService = Symbol('FlowOperationBaseService'); diff --git a/packages/canvas-engine/free-layout-core/src/service/index.ts b/packages/canvas-engine/free-layout-core/src/service/index.ts index 445a60a8..c321744e 100644 --- a/packages/canvas-engine/free-layout-core/src/service/index.ts +++ b/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-drag-service'; export * from './workflow-reset-layout-service'; +export * from './workflow-operation-base-service'; diff --git a/packages/canvas-engine/free-layout-core/src/service/workflow-operation-base-service.ts b/packages/canvas-engine/free-layout-core/src/service/workflow-operation-base-service.ts new file mode 100644 index 00000000..da442676 --- /dev/null +++ b/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(); + + 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, + }); + } +} diff --git a/packages/canvas-engine/free-layout-core/src/typings/index.ts b/packages/canvas-engine/free-layout-core/src/typings/index.ts index 4d7a3312..2888d43d 100644 --- a/packages/canvas-engine/free-layout-core/src/typings/index.ts +++ b/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-line'; export * from './workflow-sub-canvas'; +export * from './workflow-operation'; export const URLParams = Symbol(''); diff --git a/packages/canvas-engine/free-layout-core/src/typings/workflow-operation.ts b/packages/canvas-engine/free-layout-core/src/typings/workflow-operation.ts new file mode 100644 index 00000000..a8ed1c25 --- /dev/null +++ b/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; + /** + * 更新节点位置 + * @param nodeOrId + * @param position + * @returns + */ + updateNodePosition(nodeOrId: FlowNodeEntityOrId, position: IPoint): void; +} + +export const WorkflowOperationBaseService = Symbol('WorkflowOperationBaseService'); diff --git a/packages/canvas-engine/free-layout-core/src/workflow-document-container-module.ts b/packages/canvas-engine/free-layout-core/src/workflow-document-container-module.ts index 892cec4b..06fdbeee 100644 --- a/packages/canvas-engine/free-layout-core/src/workflow-document-container-module.ts +++ b/packages/canvas-engine/free-layout-core/src/workflow-document-container-module.ts @@ -1,6 +1,6 @@ import { ContainerModule } from 'inversify'; -import { FlowDocument, FlowDocumentContribution } from '@flowgram.ai/document'; import { bindContributions } from '@flowgram.ai/utils'; +import { FlowDocument, FlowDocumentContribution } from '@flowgram.ai/document'; import { WorkflowLinesManager } from './workflow-lines-manager'; import { @@ -10,12 +10,13 @@ import { import { WorkflowDocumentContribution } from './workflow-document-contribution'; import { WorkflowDocument, WorkflowDocumentProvider } from './workflow-document'; import { getUrlParams } from './utils/get-url-params'; -import { URLParams } from './typings'; +import { URLParams, WorkflowOperationBaseService } from './typings'; import { WorkflowDragService, WorkflowHoverService, WorkflowSelectService, WorkflowResetLayoutService, + WorkflowOperationBaseServiceImpl, } from './service'; import { FreeLayout } from './layout'; @@ -28,6 +29,7 @@ export const WorkflowDocumentContainerModule = new ContainerModule( bind(WorkflowSelectService).toSelf().inSingletonScope(); bind(WorkflowHoverService).toSelf().inSingletonScope(); bind(WorkflowResetLayoutService).toSelf().inSingletonScope(); + bind(WorkflowOperationBaseService).to(WorkflowOperationBaseServiceImpl).inSingletonScope(); bind(URLParams) .toDynamicValue(() => getUrlParams()) .inSingletonScope(); @@ -37,7 +39,7 @@ export const WorkflowDocumentContainerModule = new ContainerModule( }); rebind(FlowDocument).toService(WorkflowDocument); bind(WorkflowDocumentProvider) - .toDynamicValue(ctx => () => ctx.container.get(WorkflowDocument)) + .toDynamicValue((ctx) => () => ctx.container.get(WorkflowDocument)) .inSingletonScope(); - }, + } ); diff --git a/packages/client/fixed-layout-editor/__tests__/services/history-operation-service/move-node.test.ts b/packages/client/fixed-layout-editor/__tests__/services/history-operation-service/move-node.test.ts index 40cd0530..7c64569d 100644 --- a/packages/client/fixed-layout-editor/__tests__/services/history-operation-service/move-node.test.ts +++ b/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']); }); + 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 () => { const root = flowDocument.getNode('root'); flowDocument.addFromNode('dynamicSplit_0', { diff --git a/packages/plugins/free-history-plugin/src/free-history-manager.ts b/packages/plugins/free-history-plugin/src/free-history-manager.ts index a0ab8ffb..eb797f68 100644 --- a/packages/plugins/free-history-plugin/src/free-history-manager.ts +++ b/packages/plugins/free-history-plugin/src/free-history-manager.ts @@ -7,12 +7,14 @@ import { WorkflowDocument, WorkflowResetLayoutService, WorkflowDragService, + WorkflowOperationBaseService, } from '@flowgram.ai/free-layout-core'; import { FlowNodeFormData } 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 FreeHistoryPluginOptions, FreeOperationType } from './types'; +import { DragNodeOperationValue, type FreeHistoryPluginOptions, FreeOperationType } from './types'; import { HistoryEntityManager } from './history-entity-manager'; import { DragNodesHandler } from './handlers/drag-nodes-handler'; import { ChangeNodeDataHandler } from './handlers/change-node-data-handler'; @@ -40,6 +42,9 @@ export class FreeHistoryManager { private _toDispose: DisposableCollection = new DisposableCollection(); + @inject(WorkflowOperationBaseService) + private _operationService: WorkflowOperationBaseService; + onInit(ctx: PluginContext, opts: FreeHistoryPluginOptions) { const document = ctx.get(WorkflowDocument); const historyService = ctx.get(HistoryService); @@ -101,6 +106,39 @@ export class FreeHistoryManager { { 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, + } + ); + }), ]); } diff --git a/packages/plugins/free-history-plugin/src/operation-metas/drag-nodes.ts b/packages/plugins/free-history-plugin/src/operation-metas/drag-nodes.ts index 952918b9..d68e5306 100644 --- a/packages/plugins/free-history-plugin/src/operation-metas/drag-nodes.ts +++ b/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 { 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 { baseOperationMeta } from './base'; @@ -9,7 +9,7 @@ import { baseOperationMeta } from './base'; export const dragNodesOperationMeta: OperationMeta = { ...baseOperationMeta, type: FreeOperationType.dragNodes, - inverse: op => ({ + inverse: (op) => ({ ...op, value: { ...op.value, @@ -35,7 +35,7 @@ export const dragNodesOperationMeta: OperationMeta 0) { - node.collapsedChildren.forEach(childNode => { + node.collapsedChildren.forEach((childNode) => { const childNodeTransformData = childNode.getData(FlowNodeTransformData); childNodeTransformData.fireChange(); @@ -43,5 +43,4 @@ export const dragNodesOperationMeta: OperationMeta false, }; diff --git a/packages/plugins/free-history-plugin/src/operation-metas/index.ts b/packages/plugins/free-history-plugin/src/operation-metas/index.ts index a1362e24..62f96f1c 100644 --- a/packages/plugins/free-history-plugin/src/operation-metas/index.ts +++ b/packages/plugins/free-history-plugin/src/operation-metas/index.ts @@ -1,4 +1,5 @@ import { resetLayoutOperationMeta } from './reset-layout'; +import { moveChildNodesOperationMeta } from './move-child-nodes'; import { dragNodesOperationMeta } from './drag-nodes'; import { deleteNodeOperationMeta } from './delete-node'; import { deleteLineOperationMeta } from './delete-line'; @@ -14,4 +15,5 @@ export const operationMetas = [ changeNodeDataOperationMeta, resetLayoutOperationMeta, dragNodesOperationMeta, + moveChildNodesOperationMeta, ]; diff --git a/packages/plugins/free-history-plugin/src/operation-metas/move-child-nodes.ts b/packages/plugins/free-history-plugin/src/operation-metas/move-child-nodes.ts new file mode 100644 index 00000000..f5f19854 --- /dev/null +++ b/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); + 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(); + } + }); + }, +}; diff --git a/packages/plugins/free-history-plugin/src/types.ts b/packages/plugins/free-history-plugin/src/types.ts index 350007a4..5bb513a6 100644 --- a/packages/plugins/free-history-plugin/src/types.ts +++ b/packages/plugins/free-history-plugin/src/types.ts @@ -21,6 +21,7 @@ export enum FreeOperationType { changeNodeData = 'changeNodeData', resetLayout = 'resetLayout', dragNodes = 'dragNodes', + moveChildNodes = 'moveChildNodes', } export interface AddOrDeleteLineOperationValue extends WorkflowLinePortInfo { From 2607ec93d3fb4d7cfcc290907cbfd5c89cd7f8cc Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 17:09:14 +0800 Subject: [PATCH 02/22] feat(canvas-core): drag service supports detailed event params --- .../src/service/workflow-drag-service.ts | 124 +++++++++++------- .../free-layout-core/src/typings/index.ts | 1 + .../src/typings/workflow-drag.ts | 49 +++++++ 3 files changed, 126 insertions(+), 48 deletions(-) create mode 100644 packages/canvas-engine/free-layout-core/src/typings/workflow-drag.ts diff --git a/packages/canvas-engine/free-layout-core/src/service/workflow-drag-service.ts b/packages/canvas-engine/free-layout-core/src/service/workflow-drag-service.ts index 9d813500..2ee582b1 100644 --- a/packages/canvas-engine/free-layout-core/src/service/workflow-drag-service.ts +++ b/packages/canvas-engine/free-layout-core/src/service/workflow-drag-service.ts @@ -7,14 +7,15 @@ import { type IPoint, PromiseDeferred, Emitter, + type PositionSchema, DisposableCollection, Rectangle, delay, Disposable, } from '@flowgram.ai/utils'; -import type { PositionSchema } from '@flowgram.ai/utils'; import { FlowNodeTransformData, + FlowNodeType, FlowOperationBaseService, type FlowNodeEntity, } from '@flowgram.ai/document'; @@ -31,6 +32,7 @@ import { WorkflowLinesManager } from '../workflow-lines-manager'; import { WorkflowDocumentOptions } from '../workflow-document-option'; import { WorkflowDocument } from '../workflow-document'; import { WorkflowCommands } from '../workflow-commands'; +import { LineEventProps, NodesDragEvent, OnDragLineEnd } from '../typings/workflow-drag'; import { type WorkflowNodeJSON, type WorkflowNodeMeta } from '../typings'; import { WorkflowNodePortsData } from '../entity-datas'; import { @@ -60,36 +62,6 @@ function checkDragSuccess( return false; } -interface LineEventProps { - type: 'onDrag' | 'onDragEnd'; - onDragNodeId?: string; - event?: MouseEvent; -} - -interface INodesDragEvent { - type: string; - nodes: FlowNodeEntity[]; - startPositions: IPoint[]; - altKey: boolean; -} - -export interface NodesDragEndEvent extends INodesDragEvent { - type: 'onDragEnd'; -} - -export type NodesDragEvent = NodesDragEndEvent; - -export type onDragLineEndParams = { - fromPort: WorkflowPortEntity; - toPort?: WorkflowPortEntity; - mousePos: PositionSchema; - line?: WorkflowLineEntity; - originLine?: WorkflowLineEntity; - event: PlaygroundDragEvent; -}; - -export type OnDragLineEnd = (params: onDragLineEndParams) => Promise; - @injectable() export class WorkflowDragService { @inject(PlaygroundConfigEntity) @@ -147,9 +119,9 @@ export class WorkflowDragService { /** * 拖拽选中节点 - * @param event + * @param triggerEvent */ - startDragSelectedNodes(event: MouseEvent | React.MouseEvent): Promise { + startDragSelectedNodes(triggerEvent: MouseEvent | React.MouseEvent): Promise { let { selectedNodes } = this.selectService; if ( selectedNodes.length === 0 || @@ -162,7 +134,7 @@ export class WorkflowDragService { if (sameParent && sameParent.flowNodeType !== FlowNodeBaseType.ROOT) { selectedNodes = [sameParent]; } - const { altKey } = event; + const { altKey } = triggerEvent; // 节点整体开始位置 let startPosition = this.getNodesPosition(selectedNodes); // 单个节点开始位置 @@ -173,11 +145,20 @@ export class WorkflowDragService { let dragSuccess = false; const startTime = Date.now(); const dragger = new PlaygroundDrag({ - onDragStart: () => { + onDragStart: (dragEvent) => { this.isDragging = true; + this._nodesDragEmitter.fire({ + type: 'onDragStart', + nodes: selectedNodes, + startPositions, + altKey, + dragEvent, + triggerEvent, + dragger, + }); }, - onDrag: (e) => { - if (!dragSuccess && checkDragSuccess(Date.now() - startTime, e)) { + onDrag: (dragEvent) => { + if (!dragSuccess && checkDragSuccess(Date.now() - startTime, dragEvent)) { dragSuccess = true; if (altKey) { // 按住 alt 为复制 @@ -207,11 +188,13 @@ export class WorkflowDragService { // 计算拖拽偏移量 const offset: IPoint = this.getDragPosOffset({ - event: e, + event: dragEvent, selectedNodes, startPosition, }); + const positions: PositionSchema[] = []; + selectedNodes.forEach((node, index) => { const transform = node.getData(TransformData); const nodeStartPosition = startPositions[index]; @@ -230,21 +213,36 @@ export class WorkflowDragService { transform.update({ position: newPosition, }); + positions.push(newPosition); + }); + + this._nodesDragEmitter.fire({ + type: 'onDragging', + nodes: selectedNodes, + startPositions, + positions, + altKey, + dragEvent, + triggerEvent, + dragger, }); }, - onDragEnd: () => { + onDragEnd: (dragEvent) => { this.isDragging = false; this._nodesDragEmitter.fire({ type: 'onDragEnd', nodes: selectedNodes, startPositions, altKey, + dragEvent, + triggerEvent, + dragger, }); }, }); return dragger - .start(event.clientX, event.clientY, this.playgroundConfig) - .then(() => dragSuccess); + .start(triggerEvent.clientX, triggerEvent.clientY, this.playgroundConfig) + ?.then(() => dragSuccess); } /** @@ -332,6 +330,13 @@ export class WorkflowDragService { }, onDragEnd: async (e) => { const dropNode = this._dropNode; + const { allowDrop } = this.canDropToNode({ + dragNodeType: type, + dropNode, + }); + if (!allowDrop) { + return this.clearDrop(); + } const dragNode = await this.dropCard(type, e, data, dropNode); this.clearDrop(); if (dragNode) { @@ -396,6 +401,27 @@ export class WorkflowDragService { }; } + /** + * 判断是否可以放置节点 + */ + public canDropToNode(params: { dragNodeType?: FlowNodeType; dropNode?: WorkflowNodeEntity }): { + allowDrop: boolean; + message?: string; + dropNode?: WorkflowNodeEntity; + } { + const { dragNodeType, dropNode } = params; + if (!dragNodeType) { + return { + allowDrop: false, + message: 'Please select a node to drop', + }; + } + return { + allowDrop: true, + dropNode, + }; + } + /** * 获取拖拽偏移 */ @@ -440,10 +466,12 @@ export class WorkflowDragService { } return this.nodeSelectable(entity); }) - .filter((transform) => { - const { entity } = transform; - return entity.flowNodeType === FlowNodeBaseType.SUB_CANVAS; - }); + .filter((transform) => this.isContainer(transform.entity)); + } + + /** 是否容器节点 */ + private isContainer(node?: WorkflowNodeEntity): boolean { + return node?.getNodeMeta().isContainer ?? false; } /** @@ -519,8 +547,8 @@ export class WorkflowDragService { return { hasError: false, }; - } else if (toNode.flowNodeType === FlowNodeBaseType.SUB_CANVAS) { - // 在子画布内进行连线的情况,需忽略 + } else if (this.isContainer(toNode)) { + // 在容器内进行连线的情况,需忽略 return { hasError: false, }; @@ -631,7 +659,7 @@ export class WorkflowDragService { }); this.setLineColor(line, this.linesManager.lineColor.drawing); - if (toNode && toNode.flowNodeType !== FlowNodeBaseType.SUB_CANVAS) { + if (toNode && !this.isContainer(toNode)) { // 如果鼠标 hover 在 node 中的时候,默认连线到这个 node 的初始位置 const portsData = toNode.getData(WorkflowNodePortsData)!; toPort = portsData.inputPorts[0]; diff --git a/packages/canvas-engine/free-layout-core/src/typings/index.ts b/packages/canvas-engine/free-layout-core/src/typings/index.ts index 2888d43d..b63d0813 100644 --- a/packages/canvas-engine/free-layout-core/src/typings/index.ts +++ b/packages/canvas-engine/free-layout-core/src/typings/index.ts @@ -5,6 +5,7 @@ export * from './workflow-registry'; export * from './workflow-line'; export * from './workflow-sub-canvas'; export * from './workflow-operation'; +export * from './workflow-drag'; export const URLParams = Symbol(''); diff --git a/packages/canvas-engine/free-layout-core/src/typings/workflow-drag.ts b/packages/canvas-engine/free-layout-core/src/typings/workflow-drag.ts new file mode 100644 index 00000000..c028fb77 --- /dev/null +++ b/packages/canvas-engine/free-layout-core/src/typings/workflow-drag.ts @@ -0,0 +1,49 @@ +import type React from 'react'; + +import { type PositionSchema } from '@flowgram.ai/utils'; +import { type FlowNodeEntity } from '@flowgram.ai/document'; +import { PlaygroundDrag, type PlaygroundDragEvent } from '@flowgram.ai/core'; + +import { type WorkflowLineEntity, type WorkflowPortEntity } from '../entities'; + +export interface LineEventProps { + type: 'onDrag' | 'onDragEnd'; + onDragNodeId?: string; + event?: MouseEvent; +} + +interface INodesDragEvent { + type: string; + nodes: FlowNodeEntity[]; + startPositions: PositionSchema[]; + altKey: boolean; + dragEvent: PlaygroundDragEvent; + triggerEvent: MouseEvent | React.MouseEvent; + dragger: PlaygroundDrag; +} + +export interface NodesDragStartEvent extends INodesDragEvent { + type: 'onDragStart'; +} + +export interface NodesDragEndEvent extends INodesDragEvent { + type: 'onDragEnd'; +} + +export interface NodesDraggingEvent extends INodesDragEvent { + type: 'onDragging'; + positions: PositionSchema[]; +} + +export type NodesDragEvent = NodesDragStartEvent | NodesDraggingEvent | NodesDragEndEvent; + +export type onDragLineEndParams = { + fromPort: WorkflowPortEntity; + toPort?: WorkflowPortEntity; + mousePos: PositionSchema; + line?: WorkflowLineEntity; + originLine?: WorkflowLineEntity; + event: PlaygroundDragEvent; +}; + +export type OnDragLineEnd = (params: onDragLineEndParams) => Promise; From 5f1f2a6a0371ca970b75e3d9b7f35b45397598e0 Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 17:11:59 +0800 Subject: [PATCH 03/22] feat(snap): node snapping and snap line rending can be disabled --- .../plugins/free-snap-plugin/src/service.ts | 135 ++++++++++-------- 1 file changed, 78 insertions(+), 57 deletions(-) diff --git a/packages/plugins/free-snap-plugin/src/service.ts b/packages/plugins/free-snap-plugin/src/service.ts index dadd0858..b30eea3f 100644 --- a/packages/plugins/free-snap-plugin/src/service.ts +++ b/packages/plugins/free-snap-plugin/src/service.ts @@ -1,11 +1,11 @@ import { inject, injectable } from 'inversify'; +import { Disposable, Emitter, Rectangle } from '@flowgram.ai/utils'; +import { IPoint } from '@flowgram.ai/utils'; +import { WorkflowNodeEntity, WorkflowDocument } from '@flowgram.ai/free-layout-core'; +import { WorkflowDragService } from '@flowgram.ai/free-layout-core'; import { FlowNodeTransformData } from '@flowgram.ai/document'; import { FlowNodeBaseType } from '@flowgram.ai/document'; import { EntityManager, PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core'; -import { WorkflowNodeEntity, WorkflowDocument } from '@flowgram.ai/free-layout-core'; -import { WorkflowDragService } from '@flowgram.ai/free-layout-core'; -import { Disposable, Emitter, Rectangle } from '@flowgram.ai/utils'; -import { IPoint } from '@flowgram.ai/utils'; import { isEqual, isGreaterThan, isLessThan, isLessThanOrEqual, isNumber } from './utils'; import type { @@ -44,6 +44,8 @@ export class WorkflowSnapService { public readonly onSnap = this.snapEmitter.event; + private _disabled = false; + public init(params: Partial = {}): void { this.options = { ...SnapDefaultOptions, @@ -53,18 +55,46 @@ export class WorkflowSnapService { } public dispose(): void { - this.disposers.forEach(disposer => disposer.dispose()); + this.disposers.forEach((disposer) => disposer.dispose()); + } + + public get disabled(): boolean { + return this._disabled; + } + + public disable(): void { + if (this._disabled) { + return; + } + this._disabled = true; + this.clear(); + } + + public enable(): void { + if (!this._disabled) { + return; + } + this._disabled = false; + this.clear(); } private mountListener(): void { - const dragAdjusterDisposer = this.dragService.registerPosAdjuster(params => - this.snapping({ - targetNodes: params.selectedNodes, - position: params.position, - }), - ); - const dragEndDisposer = this.dragService.onNodesDrag(event => { - if (event.type !== 'onDragEnd') { + const dragAdjusterDisposer = this.dragService.registerPosAdjuster((params) => { + const { selectedNodes: targetNodes, position } = params; + const isMultiSnapping = this.options.enableMultiSnapping ? false : targetNodes.length !== 1; + if (this._disabled || !this.options.enableEdgeSnapping || isMultiSnapping) { + return { + x: 0, + y: 0, + }; + } + return this.snapping({ + targetNodes, + position, + }); + }); + const dragEndDisposer = this.dragService.onNodesDrag((event) => { + if (event.type !== 'onDragEnd' || this._disabled) { return; } if (this.options.enableGridSnapping) { @@ -81,24 +111,15 @@ export class WorkflowSnapService { } private snapping(params: { targetNodes: WorkflowNodeEntity[]; position: IPoint }): IPoint { - const { targetNodes: targetNodes, position } = params; + const { targetNodes, position } = params; - const isMultiSnapping = this.options.enableMultiSnapping ? false : targetNodes.length !== 1; - - if (!this.options.enableEdgeSnapping || isMultiSnapping) { - return { - x: 0, - y: 0, - }; - } - - const selectedBounds = this.getBounds(targetNodes); + const targetBounds = this.getBounds(targetNodes); const targetRect = new Rectangle( position.x, position.y, - selectedBounds.width, - selectedBounds.height, + targetBounds.width, + targetBounds.height ); const snapNodeRects = this.getSnapNodeRects({ @@ -127,7 +148,7 @@ export class WorkflowSnapService { position.x + offset.x, position.y + offset.y, targetRect.width, - targetRect.height, + targetRect.height ); this.snapEmitter.fire({ @@ -155,23 +176,23 @@ export class WorkflowSnapService { }); // 找到最近的线条 - const topYClosestLine = snapLines.horizontal.find(line => - isLessThanOrEqual(Math.abs(line.y - targetRect.top), edgeThreshold), + const topYClosestLine = snapLines.horizontal.find((line) => + isLessThanOrEqual(Math.abs(line.y - targetRect.top), edgeThreshold) ); - const bottomYClosestLine = snapLines.horizontal.find(line => - isLessThanOrEqual(Math.abs(line.y - targetRect.bottom), edgeThreshold), + const bottomYClosestLine = snapLines.horizontal.find((line) => + isLessThanOrEqual(Math.abs(line.y - targetRect.bottom), edgeThreshold) ); - const leftXClosestLine = snapLines.vertical.find(line => - isLessThanOrEqual(Math.abs(line.x - targetRect.left), edgeThreshold), + const leftXClosestLine = snapLines.vertical.find((line) => + isLessThanOrEqual(Math.abs(line.x - targetRect.left), edgeThreshold) ); - const rightXClosestLine = snapLines.vertical.find(line => - isLessThanOrEqual(Math.abs(line.x - targetRect.right), edgeThreshold), + const rightXClosestLine = snapLines.vertical.find((line) => + isLessThanOrEqual(Math.abs(line.x - targetRect.right), edgeThreshold) ); - const midYClosestLine = snapLines.midHorizontal.find(line => - isLessThanOrEqual(Math.abs(line.y - targetRect.center.y), edgeThreshold), + const midYClosestLine = snapLines.midHorizontal.find((line) => + isLessThanOrEqual(Math.abs(line.y - targetRect.center.y), edgeThreshold) ); - const midXClosestLine = snapLines.midVertical.find(line => - isLessThanOrEqual(Math.abs(line.x - targetRect.center.x), edgeThreshold), + const midXClosestLine = snapLines.midVertical.find((line) => + isLessThanOrEqual(Math.abs(line.x - targetRect.center.x), edgeThreshold) ); // 计算最近坐标 @@ -227,11 +248,11 @@ export class WorkflowSnapService { x: snappedPosition.x - rect.x, y: snappedPosition.y - rect.y, }; - targetNodes.forEach(node => + targetNodes.forEach((node) => this.updateNodePositionWithOffset({ node, offset, - }), + }) ); } @@ -256,7 +277,7 @@ export class WorkflowSnapService { const midHorizontalLines: SnapMidHorizontalLine[] = []; const midVerticalLines: SnapMidVerticalLine[] = []; - snapNodeRects.forEach(snapNodeRect => { + snapNodeRects.forEach((snapNodeRect) => { const nodeBounds = snapNodeRect.rect; const nodeCenter = nodeBounds.center; // 边缘横线 @@ -310,11 +331,11 @@ export class WorkflowSnapService { const targetCenter = targetRect.center; const targetContainerId = targetNodes[0].parent?.id ?? this.document.root.id; - const disabledNodeIds = targetNodes.map(n => n.id); + const disabledNodeIds = targetNodes.map((n) => n.id); disabledNodeIds.push(FlowNodeBaseType.ROOT); const availableNodes = this.nodes - .filter(n => n.parent?.id === targetContainerId) - .filter(n => !disabledNodeIds.includes(n.id)) + .filter((n) => n.parent?.id === targetContainerId) + .filter((n) => !disabledNodeIds.includes(n.id)) .sort((nodeA, nodeB) => { const nodeCenterA = nodeA.getData(FlowNodeTransformData)!.bounds.center; const nodeCenterB = nodeB.getData(FlowNodeTransformData)!.bounds.center; @@ -340,7 +361,7 @@ export class WorkflowSnapService { const availableNodes = this.getAvailableNodes(params); const viewRect = this.viewRect(); return availableNodes - .map(node => { + .map((node) => { const snapNodeRect: SnapNodeRect = { id: node.id, rect: node.getData(FlowNodeTransformData).bounds, @@ -367,7 +388,7 @@ export class WorkflowSnapService { if (nodes.length === 0) { return Rectangle.EMPTY; } - return Rectangle.enlarge(nodes.map(n => n.getData(FlowNodeTransformData)!.bounds)); + return Rectangle.enlarge(nodes.map((n) => n.getData(FlowNodeTransformData)!.bounds)); } private updateNodePositionWithOffset(params: { node: WorkflowNodeEntity; offset: IPoint }): void { @@ -379,7 +400,7 @@ export class WorkflowSnapService { }; if (node.collapsedChildren?.length > 0) { // 嵌套情况下需将子节点 transform 设为 dirty - node.collapsedChildren.forEach(childNode => { + node.collapsedChildren.forEach((childNode) => { const childNodeTransformData = childNode.getData(FlowNodeTransformData); childNodeTransformData.fireChange(); @@ -451,7 +472,7 @@ export class WorkflowSnapService { const rightAlignX = alignRects.right[0].rect.left - alignSpacing.right; const isAlignRight = isLessThanOrEqual( Math.abs(targetRect.right - rightAlignX), - alignThreshold, + alignThreshold ); if (isAlignRight) { rightX = rightAlignX - targetRect.width; @@ -463,7 +484,7 @@ export class WorkflowSnapService { const leftAlignX = alignRects.left[0].rect.right + alignSpacing.midHorizontal; const isAlignMidHorizontal = isLessThanOrEqual( Math.abs(targetRect.left - leftAlignX), - alignThreshold, + alignThreshold ); if (isAlignMidHorizontal) { midX = leftAlignX; @@ -475,7 +496,7 @@ export class WorkflowSnapService { const topAlignY = alignRects.top[0].rect.bottom + alignSpacing.midVertical; const isAlignMidVertical = isLessThanOrEqual( Math.abs(targetRect.top - topAlignY), - alignThreshold, + alignThreshold ); if (isAlignMidVertical) { midY = topAlignY; @@ -552,7 +573,7 @@ export class WorkflowSnapService { const leftHorizontalRects: AlignRect[] = []; const rightHorizontalRects: AlignRect[] = []; - snapNodeRects.forEach(snapNodeRect => { + snapNodeRects.forEach((snapNodeRect) => { const nodeRect = snapNodeRect.rect; const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection(nodeRect, targetRect); @@ -612,7 +633,7 @@ export class WorkflowSnapService { } const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection( rectA, - rectB, + rectB ); if (isIntersection) { return; @@ -620,13 +641,13 @@ export class WorkflowSnapService { if (isHorizontal && isHorizontalIntersection && !isVerticalIntersection) { const betweenSpacing = Math.min( Math.abs(rectA.left - rectB.right), - Math.abs(rectA.right - rectB.left), + Math.abs(rectA.right - rectB.left) ); return (betweenSpacing - targetRect.width) / 2; } else if (!isHorizontal && isVerticalIntersection && !isHorizontalIntersection) { const betweenSpacing = Math.min( Math.abs(rectA.top - rectB.bottom), - Math.abs(rectA.bottom - rectB.top), + Math.abs(rectA.bottom - rectB.top) ); return (betweenSpacing - targetRect.height) / 2; } @@ -646,7 +667,7 @@ export class WorkflowSnapService { const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection( rectA, - rectB, + rectB ); if (isIntersection) { @@ -663,7 +684,7 @@ export class WorkflowSnapService { private intersection( rectA: Rectangle, - rectB: Rectangle, + rectB: Rectangle ): { isHorizontalIntersection: boolean; isVerticalIntersection: boolean; From dba6190e1aba4215289529d1c214bec55c00d7a2 Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 17:14:02 +0800 Subject: [PATCH 04/22] feat(canvas-core): container node use meta.isContainer tag instead of SUB_CANVAS type --- .../free-layout-core/src/typings/workflow-node.ts | 1 + .../free-layout-core/src/workflow-document.ts | 6 +++--- .../plugins/free-node-panel-plugin/src/service.ts | 15 ++++++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/canvas-engine/free-layout-core/src/typings/workflow-node.ts b/packages/canvas-engine/free-layout-core/src/typings/workflow-node.ts index 5c425328..1ac5480b 100644 --- a/packages/canvas-engine/free-layout-core/src/typings/workflow-node.ts +++ b/packages/canvas-engine/free-layout-core/src/typings/workflow-node.ts @@ -18,6 +18,7 @@ export interface WorkflowNodeMeta extends FlowNodeMeta { defaultPorts?: WorkflowPorts; // 默认点位 useDynamicPort?: boolean; // 使用动态点位,会计算 data-port-key subCanvas?: (node: WorkflowNodeEntity) => WorkflowSubCanvas | undefined; + isContainer?: boolean; // 是否容器节点 } /** diff --git a/packages/canvas-engine/free-layout-core/src/workflow-document.ts b/packages/canvas-engine/free-layout-core/src/workflow-document.ts index a90f7c39..34b97a64 100644 --- a/packages/canvas-engine/free-layout-core/src/workflow-document.ts +++ b/packages/canvas-engine/free-layout-core/src/workflow-document.ts @@ -367,11 +367,11 @@ export class WorkflowDocument extends FlowDocument { const endNodeId = allNode.find((node) => node.isNodeEnd)!.id; // 子画布内节点无需开始/结束 - const nodeInSubCanvas = allNode - .filter((node) => node.parent?.flowNodeType === FlowNodeBaseType.SUB_CANVAS) + const nodeInContainer = allNode + .filter((node) => node.parent?.getNodeMeta().isContainer) .map((node) => node.id); - const associatedCache = new Set([endNodeId, ...nodeInSubCanvas]); + const associatedCache = new Set([endNodeId, ...nodeInContainer]); const bfs = (nodeId: string) => { if (associatedCache.has(nodeId)) { return; diff --git a/packages/plugins/free-node-panel-plugin/src/service.ts b/packages/plugins/free-node-panel-plugin/src/service.ts index a8cb0dd2..20a1f72f 100644 --- a/packages/plugins/free-node-panel-plugin/src/service.ts +++ b/packages/plugins/free-node-panel-plugin/src/service.ts @@ -8,12 +8,12 @@ import { WorkflowPortEntity, WorkflowNodePortsData, WorkflowNodeEntity, + WorkflowNodeMeta, } from '@flowgram.ai/free-layout-core'; import { WorkflowSelectService } from '@flowgram.ai/free-layout-core'; import { WorkflowNodeJSON } from '@flowgram.ai/free-layout-core'; import { FreeOperationType, HistoryService } from '@flowgram.ai/free-history-plugin'; import { FlowNodeTransformData } from '@flowgram.ai/document'; -import { FlowNodeBaseType } from '@flowgram.ai/document'; import { PlaygroundConfigEntity } from '@flowgram.ai/core'; import { TransformData } from '@flowgram.ai/core'; @@ -293,7 +293,7 @@ export class WorkflowNodePanelService { } const fromNode = fromPort?.node; const fromContainer = fromNode?.parent; - if (fromNode?.flowNodeType === FlowNodeBaseType.SUB_CANVAS) { + if (this.isContainer(fromNode)) { // 子画布内部输入连线 return fromNode; } @@ -303,7 +303,7 @@ export class WorkflowNodePanelService { /** 获取端口矩形 */ private getPortBox(port: WorkflowPortEntity, offset: IPoint = { x: 0, y: 0 }): Rectangle { const node = port.node; - if (node.flowNodeType === FlowNodeBaseType.SUB_CANVAS) { + if (this.isContainer(node)) { // 子画布内部端口需要虚拟节点 const { point } = port; if (port.portType === 'input') { @@ -424,7 +424,7 @@ export class WorkflowNodePanelService { /** 获取后续节点 */ private getSubsequentNodes(node: WorkflowNodeEntity): WorkflowNodeEntity[] { - if (node.flowNodeType === FlowNodeBaseType.SUB_CANVAS) { + if (this.isContainer(node)) { return []; } const brothers = node.parent?.collapsedChildren ?? []; @@ -436,7 +436,7 @@ export class WorkflowNodePanelService { } if ( !line.to?.id || - line.to.flowNodeType === FlowNodeBaseType.SUB_CANVAS // 子画布内部成环 + this.isContainer(line.to) // 子画布内部成环 ) { return; } @@ -519,4 +519,9 @@ export class WorkflowNodePanelService { }, }); } + + /** 是否容器节点 */ + private isContainer(node?: WorkflowNodeEntity): boolean { + return node?.getNodeMeta().isContainer ?? false; + } } From 23fa16b80c936bea6c2a326a12f6fea69c92276b Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 17:15:12 +0800 Subject: [PATCH 05/22] feat(select): only support shift key to trigger multi-select --- .../src/hooks/use-node-render.tsx | 4 +- .../free-hover-plugin/src/hover-layer.tsx | 43 ++++++++++--------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/canvas-engine/free-layout-core/src/hooks/use-node-render.tsx b/packages/canvas-engine/free-layout-core/src/hooks/use-node-render.tsx index d334eb52..b7df9408 100644 --- a/packages/canvas-engine/free-layout-core/src/hooks/use-node-render.tsx +++ b/packages/canvas-engine/free-layout-core/src/hooks/use-node-render.tsx @@ -57,7 +57,7 @@ export function useNodeRender(nodeFromProps?: WorkflowNodeEntity): NodeRenderRet } isDragging.current = true; // 拖拽选中的节点 - dragService.startDragSelectedNodes(e).finally(() => + dragService.startDragSelectedNodes(e)?.finally(() => setTimeout(() => { isDragging.current = false; }) @@ -75,7 +75,7 @@ export function useNodeRender(nodeFromProps?: WorkflowNodeEntity): NodeRenderRet return; } // 追加选择 - if (e.metaKey || e.shiftKey || e.ctrlKey) { + if (e.shiftKey) { selectionService.toggleSelect(node); } else { selectionService.selectNode(node); diff --git a/packages/plugins/free-hover-plugin/src/hover-layer.tsx b/packages/plugins/free-hover-plugin/src/hover-layer.tsx index 95c21a04..3a01da84 100644 --- a/packages/plugins/free-hover-plugin/src/hover-layer.tsx +++ b/packages/plugins/free-hover-plugin/src/hover-layer.tsx @@ -1,6 +1,17 @@ /* eslint-disable complexity */ import { inject, injectable } from 'inversify'; +import { type IPoint } from '@flowgram.ai/utils'; import { SelectorBoxConfigEntity } from '@flowgram.ai/renderer'; +import { + WorkflowDocument, + WorkflowDragService, + WorkflowHoverService, + WorkflowLineEntity, + WorkflowLinesManager, + WorkflowNodeEntity, + WorkflowSelectService, +} from '@flowgram.ai/free-layout-core'; +import { WorkflowPortEntity } from '@flowgram.ai/free-layout-core'; import { FlowNodeTransformData } from '@flowgram.ai/document'; import { EditorState, @@ -12,17 +23,6 @@ import { observeEntityDatas, type LayerOptions, } from '@flowgram.ai/core'; -import { - WorkflowDocument, - WorkflowDragService, - WorkflowHoverService, - WorkflowLineEntity, - WorkflowLinesManager, - WorkflowNodeEntity, - WorkflowSelectService, -} from '@flowgram.ai/free-layout-core'; -import { WorkflowPortEntity } from '@flowgram.ai/free-layout-core'; -import { type IPoint } from '@flowgram.ai/utils'; import { getSelectionBounds } from './selection-utils'; const PORT_BG_CLASS_NAME = 'workflow-port-bg'; @@ -79,9 +79,9 @@ export class HoverLayer extends Layer { autorun(): void { const { activatedNode } = this.selectionService; this.nodeTransformsWithSort = this.nodeTransforms - .filter(n => n.entity.id !== 'root') + .filter((n) => n.entity.id !== 'root') .reverse() // 后创建的排在前面 - .sort(n1 => (n1.entity === activatedNode ? -1 : 0)); + .sort((n1) => (n1.entity === activatedNode ? -1 : 0)); } /** @@ -145,17 +145,18 @@ export class HoverLayer extends Layer { const selectionBounds = getSelectionBounds( this.selectionService.selection, // 这里只考虑多选模式,单选模式已经下沉到 use-node-render 中 - true, + true ); if (selectionBounds.width > 0 && selectionBounds.contains(mousePos.x, mousePos.y)) { /** * 拖拽选择框 */ - this.dragService.startDragSelectedNodes(e).then(dragSuccess => { + this.dragService.startDragSelectedNodes(e)?.then((dragSuccess) => { if (!dragSuccess) { // 拖拽没有成功触发了点击 if (hoveredNode && hoveredNode instanceof WorkflowNodeEntity) { - if (e.metaKey || e.shiftKey || e.ctrlKey) { + // 追加选择 + if (e.shiftKey) { this.selectionService.toggleSelect(hoveredNode); } else { this.selectionService.selectNode(hoveredNode); @@ -188,8 +189,8 @@ export class HoverLayer extends Layer { const portHovered = this.linesManager.getPortFromMousePos(mousePos); const lineDomNodes = this.playgroundNode.querySelectorAll(LINE_CLASS_NAME); - const checkTargetFromLine = [...lineDomNodes].some(lineDom => - lineDom.contains(target as HTMLElement), + const checkTargetFromLine = [...lineDomNodes].some((lineDom) => + lineDom.contains(target as HTMLElement) ); // 默认 只有 output 点位可以 hover if (portHovered) { @@ -212,13 +213,13 @@ export class HoverLayer extends Layer { } const nodeHovered = nodeTransforms.find((trans: FlowNodeTransformData) => - trans.bounds.contains(mousePos.x, mousePos.y), + trans.bounds.contains(mousePos.x, mousePos.y) )?.entity as WorkflowNodeEntity; // 判断当前鼠标位置所在元素是否在节点内部 const nodeDomNodes = this.playgroundNode.querySelectorAll(NODE_CLASS_NAME); - const checkTargetFromNode = [...nodeDomNodes].some(nodeDom => - nodeDom.contains(target as HTMLElement), + const checkTargetFromNode = [...nodeDomNodes].some((nodeDom) => + nodeDom.contains(target as HTMLElement) ); if (nodeHovered || checkTargetFromNode) { From f5abe5b1df560e1f54a8413184b245eb53861767 Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 17:16:03 +0800 Subject: [PATCH 06/22] feat(canvas-core): node padding area should be include in port bounds calculation --- .../free-layout-core/src/entities/workflow-port-entity.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts b/packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts index c31fdb17..ce522a5d 100644 --- a/packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts +++ b/packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts @@ -1,4 +1,5 @@ import { type IPoint, Rectangle, Emitter } from '@flowgram.ai/utils'; +import { FlowNodeTransformData } from '@flowgram.ai/document'; import { Entity, type EntityOpts, @@ -129,7 +130,7 @@ export class WorkflowPortEntity extends Entity { get point(): IPoint { const { targetElement } = this; - const { bounds } = this.node.getData(TransformData)!; + const { bounds } = this.node.getData(FlowNodeTransformData)!; if (targetElement) { const pos = domReactToBounds(targetElement.getBoundingClientRect()).center; return this.entityManager @@ -164,7 +165,7 @@ export class WorkflowPortEntity extends Entity { */ get relativePosition(): IPoint { const { point } = this; - const { bounds } = this.node.getData(TransformData)!; + const { bounds } = this.node.getData(FlowNodeTransformData)!; return { x: point.x - bounds.x, y: point.y - bounds.y, From 855d87bea35cd2264590c417b95ceec088bf2e39 Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 17:22:36 +0800 Subject: [PATCH 07/22] fix(ci): tsc error --- .../free-layout-core/src/workflow-document-option.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/canvas-engine/free-layout-core/src/workflow-document-option.ts b/packages/canvas-engine/free-layout-core/src/workflow-document-option.ts index 9d3eed27..65dc0ba0 100644 --- a/packages/canvas-engine/free-layout-core/src/workflow-document-option.ts +++ b/packages/canvas-engine/free-layout-core/src/workflow-document-option.ts @@ -4,8 +4,13 @@ import { TransformData } from '@flowgram.ai/core'; import { type WorkflowLinesManager } from './workflow-lines-manager'; import { initFormDataFromJSON, toFormJSON } from './utils/flow-node-form-data'; -import { LineColor, LineRenderType, WorkflowNodeJSON, WorkflowNodeMeta } from './typings'; -import { onDragLineEndParams } from './service'; +import { + LineColor, + LineRenderType, + onDragLineEndParams, + WorkflowNodeJSON, + WorkflowNodeMeta, +} from './typings'; import { type WorkflowLineEntity, type WorkflowLinePortInfo, From 19cbe6a560ef5c925db1a42ee40dd973a560f839 Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 17:42:08 +0800 Subject: [PATCH 08/22] feat(container): free container plugin with renderer & services --- .../free-container-plugin/.eslintrc.js | 6 + .../__tests__/computing.test.ts | 136 +++++++ .../__tests__/manager.test.ts | 228 +++++++++++ .../__tests__/type.mock.ts | 65 +++ .../__tests__/utils.mock.ts | 123 ++++++ .../free-container-plugin/package.json | 65 +++ .../components/background/index.tsx | 32 ++ .../components/background/style.ts | 6 + .../components/border/index.tsx | 7 + .../components/border/style.ts | 35 ++ .../components/container/index.tsx | 65 +++ .../components/container/style.ts | 18 + .../components/header/index.tsx | 26 ++ .../components/header/style.ts | 77 ++++ .../container-node-render/components/index.ts | 5 + .../components/ports/index.tsx | 33 ++ .../src/container-node-render/constant.ts | 1 + .../src/container-node-render/hooks/index.ts | 1 + .../hooks/use-render-props.ts | 22 + .../src/container-node-render/index.ts | 3 + .../src/container-node-render/render.tsx | 19 + .../src/container-node-render/type.ts | 16 + .../free-container-plugin/src/index.ts | 4 + .../src/node-into-container/constant.ts | 4 + .../src/node-into-container/index.ts | 3 + .../src/node-into-container/service.ts | 376 ++++++++++++++++++ .../src/node-into-container/type.ts | 20 + .../free-container-plugin/src/plugin.ts | 26 ++ .../plugins/free-container-plugin/src/type.ts | 3 + .../free-container-plugin/tsconfig.json | 7 + .../free-container-plugin/vitest.config.ts | 26 ++ .../free-container-plugin/vitest.setup.ts | 1 + 32 files changed, 1459 insertions(+) create mode 100644 packages/plugins/free-container-plugin/.eslintrc.js create mode 100644 packages/plugins/free-container-plugin/__tests__/computing.test.ts create mode 100644 packages/plugins/free-container-plugin/__tests__/manager.test.ts create mode 100644 packages/plugins/free-container-plugin/__tests__/type.mock.ts create mode 100644 packages/plugins/free-container-plugin/__tests__/utils.mock.ts create mode 100644 packages/plugins/free-container-plugin/package.json create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/background/index.tsx create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/background/style.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/container/style.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/index.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/ports/index.tsx create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/constant.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/hooks/index.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/hooks/use-render-props.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/index.ts create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/render.tsx create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/type.ts create mode 100644 packages/plugins/free-container-plugin/src/index.ts create mode 100644 packages/plugins/free-container-plugin/src/node-into-container/constant.ts create mode 100644 packages/plugins/free-container-plugin/src/node-into-container/index.ts create mode 100644 packages/plugins/free-container-plugin/src/node-into-container/service.ts create mode 100644 packages/plugins/free-container-plugin/src/node-into-container/type.ts create mode 100644 packages/plugins/free-container-plugin/src/plugin.ts create mode 100644 packages/plugins/free-container-plugin/src/type.ts create mode 100644 packages/plugins/free-container-plugin/tsconfig.json create mode 100644 packages/plugins/free-container-plugin/vitest.config.ts create mode 100644 packages/plugins/free-container-plugin/vitest.setup.ts diff --git a/packages/plugins/free-container-plugin/.eslintrc.js b/packages/plugins/free-container-plugin/.eslintrc.js new file mode 100644 index 00000000..3aef16ba --- /dev/null +++ b/packages/plugins/free-container-plugin/.eslintrc.js @@ -0,0 +1,6 @@ +const { defineConfig } = require('@flowgram.ai/eslint-config'); + +module.exports = defineConfig({ + preset: 'web', + packageRoot: __dirname, +}); diff --git a/packages/plugins/free-container-plugin/__tests__/computing.test.ts b/packages/plugins/free-container-plugin/__tests__/computing.test.ts new file mode 100644 index 00000000..b376782d --- /dev/null +++ b/packages/plugins/free-container-plugin/__tests__/computing.test.ts @@ -0,0 +1,136 @@ +import { it, expect, beforeEach, describe } from 'vitest'; +import { interfaces } from 'inversify'; +import { + WorkflowDocument, + WorkflowHoverService, + WorkflowLineEntity, + WorkflowLinesManager, + WorkflowSelectService, +} from '@flowgram.ai/free-layout-core'; +import { EntityManager } from '@flowgram.ai/core'; + +import { StackingComputing } from '../src/stacking-computing'; +import { StackingContextManager } from '../src/manager'; +import { createWorkflowContainer, workflowJSON } from './utils.mock'; +import { IStackingComputing, IStackingContextManager } from './type.mock'; + +let container: interfaces.Container; +let document: WorkflowDocument; +let stackingContextManager: IStackingContextManager; +let stackingComputing: IStackingComputing; + +beforeEach(async () => { + container = createWorkflowContainer(); + container.bind(StackingContextManager).to(StackingContextManager); + document = container.get(WorkflowDocument); + stackingContextManager = container.get( + StackingContextManager + ) as unknown as IStackingContextManager; + await document.fromJSON(workflowJSON); + stackingContextManager.init(); + stackingComputing = new StackingComputing() as unknown as IStackingComputing; +}); + +describe('StackingComputing compute', () => { + it('should create instance', () => { + const computing = new StackingComputing(); + expect(computing).not.toBeUndefined(); + }); + it('should execute compute', () => { + const { nodeLevel, lineLevel, topLevel, maxLevel } = stackingComputing.compute({ + root: document.root, + nodes: stackingContextManager.nodes, + context: stackingContextManager.context, + }); + expect(topLevel).toBe(8); + expect(maxLevel).toBe(16); + expect(Object.fromEntries(nodeLevel)).toEqual({ + start_0: 1, + condition_0: 2, + end_0: 3, + loop_0: 4, + break_0: 6, + variable_0: 7, + }); + expect(Object.fromEntries(lineLevel)).toEqual({ + 'start_0_-condition_0_': 0, + 'start_0_-loop_0_': 0, + 'condition_0_if-end_0_': 0, + 'condition_0_else-end_0_': 0, + 'loop_0_-end_0_': 0, + 'break_0_-variable_0_': 5, + }); + }); + it('should put hovered line on max level', () => { + const hoverService = container.get(WorkflowHoverService); + const hoveredLineId = 'start_0_-loop_0_'; + hoverService.updateHoveredKey(hoveredLineId); + const { lineLevel, maxLevel } = stackingComputing.compute({ + root: document.root, + nodes: stackingContextManager.nodes, + context: stackingContextManager.context, + }); + const hoveredLineLevel = lineLevel.get(hoveredLineId); + expect(hoveredLineLevel).toBe(maxLevel); + }); + it('should put selected line on max level', () => { + const entityManager = container.get(EntityManager); + const selectService = container.get(WorkflowSelectService); + const selectedLineId = 'start_0_-loop_0_'; + const selectedLine = entityManager.getEntityById(selectedLineId)!; + selectService.selection = [selectedLine]; + const { lineLevel, maxLevel } = stackingComputing.compute({ + root: document.root, + nodes: stackingContextManager.nodes, + context: stackingContextManager.context, + }); + const selectedLineLevel = lineLevel.get(selectedLineId); + expect(selectedLineLevel).toBe(maxLevel); + }); + it('should put drawing line on max level', () => { + const linesManager = container.get(WorkflowLinesManager); + const drawingLine = linesManager.createLine({ + from: 'start_0', + drawingTo: { x: 100, y: 100 }, + })!; + const { lineLevel, maxLevel } = stackingComputing.compute({ + root: document.root, + nodes: stackingContextManager.nodes, + context: stackingContextManager.context, + }); + const drawingLineLevel = lineLevel.get(drawingLine.id); + expect(drawingLineLevel).toBe(maxLevel); + }); + it('should put selected nodes on top level', () => { + const selectService = container.get(WorkflowSelectService); + const selectedNodeId = 'start_0'; + const selectedNode = document.getNode(selectedNodeId)!; + selectService.selectNode(selectedNode); + const { nodeLevel, topLevel } = stackingComputing.compute({ + root: document.root, + nodes: stackingContextManager.nodes, + context: stackingContextManager.context, + }); + const selectedNodeLevel = nodeLevel.get(selectedNodeId); + expect(selectedNodeLevel).toBe(topLevel); + }); +}); + +describe('StackingComputing builtin methods', () => { + it('computeNodeIndexesMap', () => { + const nodeIndexes = stackingComputing.computeNodeIndexesMap(stackingContextManager.nodes); + expect(Object.fromEntries(nodeIndexes)).toEqual({ + root: 0, + start_0: 1, + condition_0: 2, + end_0: 3, + loop_0: 4, + break_0: 5, + variable_0: 6, + }); + }); + it('computeTopLevel', () => { + const topLevel = stackingComputing.computeTopLevel(stackingContextManager.nodes); + expect(topLevel).toEqual(8); + }); +}); diff --git a/packages/plugins/free-container-plugin/__tests__/manager.test.ts b/packages/plugins/free-container-plugin/__tests__/manager.test.ts new file mode 100644 index 00000000..e242a21a --- /dev/null +++ b/packages/plugins/free-container-plugin/__tests__/manager.test.ts @@ -0,0 +1,228 @@ +import { it, expect, beforeEach, describe, vi } from 'vitest'; +import { debounce } from 'lodash'; +import { interfaces } from 'inversify'; +import { + delay, + WorkflowDocument, + WorkflowHoverService, + WorkflowSelectService, +} from '@flowgram.ai/free-layout-core'; +import { FlowNodeRenderData } from '@flowgram.ai/document'; +import { + EntityManager, + PipelineRegistry, + PipelineRenderer, + PlaygroundConfigEntity, +} from '@flowgram.ai/core'; + +import { StackingContextManager } from '../src/manager'; +import { StackingComputeMode } from '../src/constant'; +import { createWorkflowContainer, workflowJSON } from './utils.mock'; +import { IStackingContextManager } from './type.mock'; + +let container: interfaces.Container; +let document: WorkflowDocument; +let stackingContextManager: IStackingContextManager; + +beforeEach(async () => { + container = createWorkflowContainer(); + container.bind(StackingContextManager).to(StackingContextManager); + document = container.get(WorkflowDocument); + stackingContextManager = container.get( + StackingContextManager + ) as unknown as IStackingContextManager; + await document.fromJSON(workflowJSON); +}); + +describe('StackingContextManager public methods', () => { + it('should create instance', () => { + const stackingContextManager = container.get(StackingContextManager); + expect(stackingContextManager.node).toMatchInlineSnapshot(` +
+ `); + expect(stackingContextManager).not.toBeUndefined(); + }); + it('should execute init', () => { + stackingContextManager.init(); + const pipelineRenderer = container.get(PipelineRenderer); + expect(pipelineRenderer.node).toMatchInlineSnapshot( + ` +
+
+
+ ` + ); + expect(stackingContextManager.mode).toEqual(StackingComputeMode.Stacking); + expect(stackingContextManager.disposers).toHaveLength(4); + }); + it('should init with mode', () => { + stackingContextManager.init(StackingComputeMode.Stacking); + expect(stackingContextManager.mode).toEqual(StackingComputeMode.Stacking); + }); + it('should execute ready', () => { + stackingContextManager.compute = vi.fn(); + stackingContextManager.ready(); + expect(stackingContextManager.compute).toBeCalled(); + }); + it('should dispose', () => { + expect(stackingContextManager.disposers).toHaveLength(0); + stackingContextManager.init(); + expect(stackingContextManager.disposers).toHaveLength(4); + const mockDispose = { dispose: vi.fn() }; + stackingContextManager.disposers.push(mockDispose); + stackingContextManager.dispose(); + expect(mockDispose.dispose).toBeCalled(); + }); +}); + +describe('StackingContextManager private methods', () => { + it('should compute with debounce', async () => { + const compute = vi.fn(); + vi.spyOn(stackingContextManager, 'compute').mockImplementation(debounce(compute, 10)); + stackingContextManager.compute(); + await delay(1); + stackingContextManager.compute(); + await delay(1); + stackingContextManager.compute(); + await delay(1); + stackingContextManager.compute(); + expect(compute).toBeCalledTimes(0); + await delay(20); + expect(compute).toBeCalledTimes(1); + }); + + it('should get nodes and lines', async () => { + const nodeIds = stackingContextManager.nodes.map((n) => n.id); + const lineIds = stackingContextManager.lines.map((l) => l.id); + expect(nodeIds).toEqual([ + 'root', + 'start_0', + 'condition_0', + 'end_0', + 'loop_0', + 'break_0', + 'variable_0', + ]); + expect(lineIds).toEqual([ + 'break_0_-variable_0_', + 'start_0_-condition_0_', + 'condition_0_if-end_0_', + 'condition_0_else-end_0_', + 'loop_0_-end_0_', + 'start_0_-loop_0_', + ]); + }); + + it('should generate context', async () => { + const hoverService = container.get(WorkflowHoverService); + const selectService = container.get(WorkflowSelectService); + expect(stackingContextManager.context).toStrictEqual({ + hoveredEntity: undefined, + hoveredEntityID: undefined, + selectedEntities: [], + selectedIDs: [], + }); + hoverService.updateHoveredKey('start_0'); + const breakNode = document.getNode('break_0')!; + const variableNode = document.getNode('variable_0')!; + selectService.selection = [breakNode, variableNode]; + expect(stackingContextManager.context.hoveredEntityID).toEqual('start_0'); + expect(stackingContextManager.context.selectedIDs).toEqual(['break_0', 'variable_0']); + }); + + it('should callback compute when onZoom trigger', () => { + const entityManager = container.get(EntityManager); + const pipelineRegistry = container.get(PipelineRegistry); + const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {}); + const playgroundConfig = + entityManager.getEntity(PlaygroundConfigEntity)!; + pipelineRegistry.ready(); + stackingContextManager.mountListener(); + playgroundConfig.updateConfig({ + zoom: 1.5, + }); + expect(stackingContextManager.node.style.transform).toBe('scale(1.5)'); + playgroundConfig.updateConfig({ + zoom: 2, + }); + expect(stackingContextManager.node.style.transform).toBe('scale(2)'); + playgroundConfig.updateConfig({ + zoom: 1, + }); + expect(stackingContextManager.node.style.transform).toBe('scale(1)'); + expect(compute).toBeCalledTimes(3); + }); + + it('should callback compute when onHover trigger', () => { + const hoverService = container.get(WorkflowHoverService); + const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {}); + stackingContextManager.mountListener(); + hoverService.updateHoveredKey('start_0'); + hoverService.updateHoveredKey('end_0'); + expect(compute).toBeCalledTimes(2); + }); + + it('should callback compute when onEntityChange trigger', () => { + const entityManager = container.get(EntityManager); + const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {}); + const node = document.getNode('start_0')!; + stackingContextManager.mountListener(); + entityManager.fireEntityChanged(node); + expect(compute).toBeCalledTimes(1); + }); + + it('should callback compute when onSelect trigger', () => { + const selectService = container.get(WorkflowSelectService); + const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {}); + stackingContextManager.mountListener(); + const breakNode = document.getNode('break_0')!; + const variableNode = document.getNode('variable_0')!; + selectService.selectNode(breakNode); + selectService.selectNode(variableNode); + expect(compute).toBeCalledTimes(2); + }); + + it('should mount listeners', () => { + const hoverService = container.get(WorkflowHoverService); + const selectService = container.get(WorkflowSelectService); + const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {}); + stackingContextManager.mountListener(); + // onHover + hoverService.updateHoveredKey('start_0'); + hoverService.updateHoveredKey('end_0'); + expect(compute).toBeCalledTimes(2); + compute.mockReset(); + // select callback + const breakNode = document.getNode('break_0')!; + const variableNode = document.getNode('variable_0')!; + selectService.selectNode(breakNode); + selectService.selectNode(variableNode); + expect(compute).toBeCalledTimes(2); + }); + + it('should trigger compute in layers mode', async () => { + stackingContextManager.init(StackingComputeMode.Layers); + stackingContextManager.ready(); + await delay(200); + const node = document.getNode('loop_0')!; + const nodeRenderData = node.getData(FlowNodeRenderData); + const element = nodeRenderData.node; + expect(element.style.zIndex).toBe('9'); + }); + + it('should trigger compute in stacking mode', async () => { + stackingContextManager.init(StackingComputeMode.Stacking); + stackingContextManager.ready(); + await delay(200); + const node = document.getNode('loop_0')!; + const nodeRenderData = node.getData(FlowNodeRenderData); + const element = nodeRenderData.node; + expect(element.style.zIndex).toBe('12'); + }); +}); diff --git a/packages/plugins/free-container-plugin/__tests__/type.mock.ts b/packages/plugins/free-container-plugin/__tests__/type.mock.ts new file mode 100644 index 00000000..2ef9dda1 --- /dev/null +++ b/packages/plugins/free-container-plugin/__tests__/type.mock.ts @@ -0,0 +1,65 @@ +import type { Disposable } from '@flowgram.ai/utils'; +import type { + WorkflowDocument, + WorkflowHoverService, + WorkflowLineEntity, + WorkflowNodeEntity, + WorkflowSelectService, +} from '@flowgram.ai/free-layout-core'; +import type { EntityManager, PipelineRegistry, PipelineRenderer } from '@flowgram.ai/core'; + +import type { StackingContext } from '../src/type'; +import type { StackingComputeMode } from '../src/constant'; + +/** mock类型便于测试内部方法 */ +export interface IStackingContextManager { + document: WorkflowDocument; + entityManager: EntityManager; + pipelineRenderer: PipelineRenderer; + pipelineRegistry: PipelineRegistry; + hoverService: WorkflowHoverService; + selectService: WorkflowSelectService; + node: HTMLDivElement; + disposers: Disposable[]; + mode: StackingComputeMode; + init(mode?: StackingComputeMode): void; + ready(): void; + dispose(): void; + compute(): void; + _compute(): void; + stackingCompute(): void; + nodes: WorkflowNodeEntity[]; + lines: WorkflowLineEntity[]; + context: StackingContext; + mountListener(): void; + onZoom(): Disposable; + onHover(): Disposable; + onEntityChange(): Disposable; + onSelect(): Disposable; +} + +export interface IStackingComputing { + currentLevel: number; + topLevel: number; + maxLevel: number; + nodeIndexes: Map; + nodeLevel: Map; + lineLevel: Map; + context: StackingContext; + compute(params: { + root: WorkflowNodeEntity; + nodes: WorkflowNodeEntity[]; + context: StackingContext; + }): { + nodeLevel: Map; + lineLevel: Map; + topLevel: number; + maxLevel: number; + }; + clearCache(): void; + computeNodeIndexesMap(nodes: WorkflowNodeEntity[]): Map; + computeTopLevel(nodes: WorkflowNodeEntity[]): number; + layerHandler(nodes: WorkflowNodeEntity[], pinTop?: boolean): void; + getLevel(pinTop: boolean): number; + levelIncrease(): void; +} diff --git a/packages/plugins/free-container-plugin/__tests__/utils.mock.ts b/packages/plugins/free-container-plugin/__tests__/utils.mock.ts new file mode 100644 index 00000000..c00eb5cc --- /dev/null +++ b/packages/plugins/free-container-plugin/__tests__/utils.mock.ts @@ -0,0 +1,123 @@ +import { interfaces } from 'inversify'; +import { + WorkflowJSON, + WorkflowDocumentContainerModule, + WorkflowLinesManager, + WorkflowSimpleLineContribution, +} from '@flowgram.ai/free-layout-core'; +import { FlowDocumentContainerModule } from '@flowgram.ai/document'; +import { PlaygroundMockTools } from '@flowgram.ai/core'; + +export function createWorkflowContainer(): interfaces.Container { + const container = PlaygroundMockTools.createContainer([ + FlowDocumentContainerModule, + WorkflowDocumentContainerModule, + ]); + const linesManager = container.get(WorkflowLinesManager); + linesManager.registerContribution(WorkflowSimpleLineContribution); + linesManager.switchLineType(WorkflowSimpleLineContribution.type); + return container; +} + +export const workflowJSON: WorkflowJSON = { + nodes: [ + { + id: 'start_0', + type: 'start', + meta: { + position: { x: 0, y: 0 }, + testRun: { + showError: undefined, + }, + }, + data: undefined, + }, + { + id: 'condition_0', + type: 'condition', + meta: { + position: { x: 400, y: 0 }, + testRun: { + showError: undefined, + }, + }, + data: undefined, + }, + { + id: 'end_0', + type: 'end', + meta: { + position: { x: 800, y: 0 }, + testRun: { + showError: undefined, + }, + }, + data: undefined, + }, + { + id: 'loop_0', + type: 'loop', + meta: { + position: { x: 1200, y: 0 }, + testRun: { + showError: undefined, + }, + }, + data: undefined, + blocks: [ + { + id: 'break_0', + type: 'break', + meta: { + position: { x: 0, y: 0 }, + testRun: { + showError: undefined, + }, + }, + data: undefined, + }, + { + id: 'variable_0', + type: 'variable', + meta: { + position: { x: 400, y: 0 }, + testRun: { + showError: undefined, + }, + }, + data: undefined, + }, + ], + edges: [ + { + sourceNodeID: 'break_0', + targetNodeID: 'variable_0', + }, + ], + }, + ], + edges: [ + { + sourceNodeID: 'start_0', + targetNodeID: 'condition_0', + }, + { + sourceNodeID: 'condition_0', + sourcePortID: 'if', + targetNodeID: 'end_0', + }, + { + sourceNodeID: 'condition_0', + sourcePortID: 'else', + targetNodeID: 'end_0', + }, + { + sourceNodeID: 'loop_0', + targetNodeID: 'end_0', + }, + { + sourceNodeID: 'start_0', + targetNodeID: 'loop_0', + }, + ], +}; diff --git a/packages/plugins/free-container-plugin/package.json b/packages/plugins/free-container-plugin/package.json new file mode 100644 index 00000000..4fec42c5 --- /dev/null +++ b/packages/plugins/free-container-plugin/package.json @@ -0,0 +1,65 @@ +{ + "name": "@flowgram.ai/free-container-plugin", + "version": "0.1.8", + "homepage": "https://flowgram.ai/", + "repository": "https://github.com/bytedance/flowgram.ai", + "license": "MIT", + "exports": { + "import": "./dist/esm/index.js", + "require": "./dist/index.js" + }, + "main": "./dist/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "npm run build:fast -- --dts-resolve", + "build:fast": "tsup src/index.ts --format cjs,esm --sourcemap --legacy-output", + "build:watch": "npm run build:fast -- --dts-resolve", + "clean": "rimraf dist", + "test": "vitest run", + "test:cov": "vitest run --coverage", + "ts-check": "tsc --noEmit", + "watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist" + }, + "dependencies": { + "@flowgram.ai/free-history-plugin": "workspace:*", + "@flowgram.ai/free-lines-plugin": "workspace:*", + "@flowgram.ai/core": "workspace:*", + "@flowgram.ai/document": "workspace:*", + "@flowgram.ai/free-layout-core": "workspace:*", + "@flowgram.ai/renderer": "workspace:*", + "@flowgram.ai/utils": "workspace:*", + "inversify": "^6.0.1", + "reflect-metadata": "~0.2.2", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@flowgram.ai/eslint-config": "workspace:*", + "@flowgram.ai/ts-config": "workspace:*", + "@types/bezier-js": "4.1.3", + "@types/lodash": "^4.14.137", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/styled-components": "^5", + "@vitest/coverage-v8": "^0.32.0", + "eslint": "^8.54.0", + "react": "^18", + "react-dom": "^18", + "styled-components": "^5", + "tsup": "^8.0.1", + "typescript": "^5.0.4", + "vitest": "^0.34.6" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17", + "styled-components": ">=4" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/background/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/background/index.tsx new file mode 100644 index 00000000..1983b7d4 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/background/index.tsx @@ -0,0 +1,32 @@ +import React, { type FC } from 'react'; + +import { useNodeRender } from '@flowgram.ai/free-layout-core'; + +import { ContainerNodeBackgroundStyle } from './style'; + +export const ContainerNodeBackground: FC = () => { + const { node } = useNodeRender(); + return ( + + + + + + + + + ); +}; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/background/style.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/background/style.ts new file mode 100644 index 00000000..e194a68d --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/background/style.ts @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +export const ContainerNodeBackgroundStyle = styled.div` + position: absolute; + inset: 56px 18px 18px; +`; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx new file mode 100644 index 00000000..b9a1aec0 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx @@ -0,0 +1,7 @@ +import React, { type FC } from 'react'; + +import { ContainerNodeBorderStyle } from './style'; + +export const ContainerNodeBorder: FC = () => ( + +); diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts new file mode 100644 index 00000000..6b8f8313 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts @@ -0,0 +1,35 @@ +import styled from 'styled-components'; + +export const ContainerNodeBorderStyle = styled.div` + pointer-events: none; + + position: absolute; + + display: flex; + align-items: center; + + width: 100%; + height: 100%; + + background-color: transparent; + border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%)); + border-color: var(--coz-bg-plus, rgb(249, 249, 249)); + border-style: solid; + border-width: 48px 8px 8px; + border-radius: 8px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 4%), 0 4px 12px 0 rgba(0, 0, 0, 2%); + + &::before { + content: ''; + + position: absolute; + z-index: 0; + inset: -4px; + + background-color: transparent; + border-color: var(--coz-bg-plus, rgb(249, 249, 249)); + border-style: solid; + border-width: 4px; + border-radius: 8px; + } +`; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx new file mode 100644 index 00000000..930b21d9 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useState, type FC, type ReactNode } from 'react'; + +import { useNodeRender } from '@flowgram.ai/free-layout-core'; +import { FlowNodeTransformData } from '@flowgram.ai/document'; + +import { useContainerNodeRenderProps } from '../../hooks'; +import { ContainerNodeContainerStyle } from './style'; + +interface IContainerNodeContainer { + children: ReactNode[]; +} + +export const ContainerNodeContainer: FC = ({ children }) => { + const { node, selected, selectNode, nodeRef } = useNodeRender(); + const nodeMeta = node.getNodeMeta(); + const { size = { width: 300, height: 200 } } = nodeMeta; + const { style = {} } = useContainerNodeRenderProps(); + + const transform = node.getData(FlowNodeTransformData); + const [width, setWidth] = useState(size.width); + const [height, setHeight] = useState(size.height); + + useEffect(() => { + const updateSize = () => { + // 无子节点时 + if (node.blocks.length === 0) { + setWidth(size.width); + setHeight(size.height); + return; + } + // 存在子节点时,只监听宽高变化 + if (width !== transform.bounds.width) { + setWidth(transform.bounds.width); + } + if (height !== transform.bounds.height) { + setHeight(transform.bounds.height); + } + }; + updateSize(); + const dispose = transform.onDataChange(() => { + updateSize(); + }); + return () => dispose.dispose(); + }, [transform]); + + return ( + { + selectNode(e); + }} + > + {children} + + ); +}; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/container/style.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/container/style.ts new file mode 100644 index 00000000..c8287d77 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/container/style.ts @@ -0,0 +1,18 @@ +import styled from 'styled-components'; + +export const ContainerNodeContainerStyle = styled.div` + display: flex; + align-items: flex-start; + + box-sizing: border-box; + min-width: 400px; + min-height: 300px; + + background-color: #f2f3f5; + border-radius: 8px; + outline: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%)); + + .container-node-container-selected { + outline: 1px solid var(--coz-stroke-hglt, #4e40e5); + } +`; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx new file mode 100644 index 00000000..078187a6 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx @@ -0,0 +1,26 @@ +import React, { type FC } from 'react'; + +import { useNodeRender } from '@flowgram.ai/free-layout-core'; + +import { useContainerNodeRenderProps } from '../../hooks'; +import { ContainerNodeHeaderStyle } from './style'; + +export const ContainerNodeHeader: FC = () => { + const { startDrag, onFocus, onBlur } = useNodeRender(); + + const { title } = useContainerNodeRenderProps(); + + return ( + { + startDrag(e); + }} + onFocus={onFocus} + onBlur={onBlur} + > +

{title}

+
+ ); +}; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts new file mode 100644 index 00000000..b1e35684 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts @@ -0,0 +1,77 @@ +import styled from 'styled-components'; + +export const ContainerNodeHeaderStyle = styled.div` + cursor: move; + + z-index: 0; + + display: flex; + gap: 8px; + align-items: center; + justify-content: flex-start; + + width: 100%; + height: 48px; + padding: 16px; + + border-radius: 8px 8px 0 0; + + .container-node-logo { + position: relative; + + flex-shrink: 0; + + width: 24px; + height: 24px; + + border-radius: 4px; + + &::after { + content: ''; + + position: absolute; + top: 0; + left: 0; + + display: block; + + width: 100%; + height: 100%; + + border-radius: 4px; + box-shadow: inset 0 0 0 1px var(--coz-stroke-plus); + } + + > img { + width: 24px; + height: 24px; + border-radius: 4px; + } + } + + .container-node-title { + margin: 0; + padding: 0; + + font-size: 16px; + font-weight: 500; + font-style: normal; + line-height: 22px; + color: var(--coz-fg-primary, rgba(6, 7, 9, 80%)); + text-overflow: ellipsis; + } + + .container-node-tooltip { + height: 100%; + } + + .container-node-tooltip-icon { + display: flex; + align-items: center; + + width: 16px; + height: 100%; + + color: #a7a9b0; + } +`; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/index.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/index.ts new file mode 100644 index 00000000..a14b5834 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/index.ts @@ -0,0 +1,5 @@ +export { ContainerNodeHeader } from './header'; +export { ContainerNodeBackground } from './background'; +export { ContainerNodeContainer } from './container'; +export { ContainerNodeBorder } from './border'; +export { ContainerNodePorts } from './ports'; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/ports/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/ports/index.tsx new file mode 100644 index 00000000..f15b1860 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/ports/index.tsx @@ -0,0 +1,33 @@ +import React, { useEffect, type FC } from 'react'; + +import { WorkflowPortRender } from '@flowgram.ai/free-lines-plugin'; +import { WorkflowNodePortsData, useNodeRender } from '@flowgram.ai/free-layout-core'; + +import { useContainerNodeRenderProps } from '../../hooks'; + +export const ContainerNodePorts: FC = () => { + const { node, ports } = useNodeRender(); + const { renderPorts } = useContainerNodeRenderProps(); + + useEffect(() => { + const portsData = node.getData(WorkflowNodePortsData); + portsData.updateDynamicPorts(); + }, [node]); + + return ( + <> + {renderPorts.map((p) => ( +
+ ))} + {ports.map((p) => ( + + ))} + + ); +}; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/constant.ts b/packages/plugins/free-container-plugin/src/container-node-render/constant.ts new file mode 100644 index 00000000..1b335d75 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/constant.ts @@ -0,0 +1 @@ +export const ContainerNodeRenderKey = 'container-node-render-key'; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/hooks/index.ts b/packages/plugins/free-container-plugin/src/container-node-render/hooks/index.ts new file mode 100644 index 00000000..b132eba9 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/hooks/index.ts @@ -0,0 +1 @@ +export { useContainerNodeRenderProps } from './use-render-props'; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/hooks/use-render-props.ts b/packages/plugins/free-container-plugin/src/container-node-render/hooks/use-render-props.ts new file mode 100644 index 00000000..181f9c60 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/hooks/use-render-props.ts @@ -0,0 +1,22 @@ +import { useNodeRender } from '@flowgram.ai/free-layout-core'; + +import { type ContainerNodeMetaRenderProps } from '../type'; + +export const useContainerNodeRenderProps = (): ContainerNodeMetaRenderProps => { + const { node } = useNodeRender(); + const nodeMeta = node.getNodeMeta(); + + const { + title = '', + tooltip, + renderPorts = [], + style = {}, + } = (nodeMeta?.renderContainerNode?.() ?? {}) as Partial; + + return { + title, + tooltip, + renderPorts, + style, + }; +}; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/index.ts b/packages/plugins/free-container-plugin/src/container-node-render/index.ts new file mode 100644 index 00000000..b213a88a --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/index.ts @@ -0,0 +1,3 @@ +export { ContainerNodeRenderKey } from './constant'; +export { ContainerNodeRender } from './render'; +export type { ContainerNodeMetaRenderProps, ContainerNodeRenderProps } from './type'; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/render.tsx b/packages/plugins/free-container-plugin/src/container-node-render/render.tsx new file mode 100644 index 00000000..61a23755 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/render.tsx @@ -0,0 +1,19 @@ +import React, { type FC } from 'react'; + +import type { ContainerNodeRenderProps } from './type'; +import { + ContainerNodeBackground, + ContainerNodeHeader, + ContainerNodePorts, + ContainerNodeBorder, + ContainerNodeContainer, +} from './components'; + +export const ContainerNodeRender: FC = (props) => ( + + + + + + +); diff --git a/packages/plugins/free-container-plugin/src/container-node-render/type.ts b/packages/plugins/free-container-plugin/src/container-node-render/type.ts new file mode 100644 index 00000000..726c4716 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/type.ts @@ -0,0 +1,16 @@ +import type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core'; + +export interface ContainerNodeMetaRenderProps { + title: string; + tooltip?: string; + renderPorts: { + id: string; + type: 'input' | 'output'; + style: React.CSSProperties; + }[]; + style: React.CSSProperties; +} + +export interface ContainerNodeRenderProps { + node: WorkflowNodeEntity; +} diff --git a/packages/plugins/free-container-plugin/src/index.ts b/packages/plugins/free-container-plugin/src/index.ts new file mode 100644 index 00000000..811261a5 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/index.ts @@ -0,0 +1,4 @@ +export * from './container-node-render'; +export * from './node-into-container'; +export { createContainerNodePlugin } from './plugin'; +export type { WorkflowContainerPluginOptions } from './type'; diff --git a/packages/plugins/free-container-plugin/src/node-into-container/constant.ts b/packages/plugins/free-container-plugin/src/node-into-container/constant.ts new file mode 100644 index 00000000..b834f121 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/node-into-container/constant.ts @@ -0,0 +1,4 @@ +export enum NodeIntoContainerType { + In = 'in', + Out = 'out', +} diff --git a/packages/plugins/free-container-plugin/src/node-into-container/index.ts b/packages/plugins/free-container-plugin/src/node-into-container/index.ts new file mode 100644 index 00000000..00173bac --- /dev/null +++ b/packages/plugins/free-container-plugin/src/node-into-container/index.ts @@ -0,0 +1,3 @@ +export { NodeIntoContainerType } from './constant'; +export { NodeIntoContainerService } from './service'; +export { NodeIntoContainerState, NodeIntoContainerEvent } from './type'; diff --git a/packages/plugins/free-container-plugin/src/node-into-container/service.ts b/packages/plugins/free-container-plugin/src/node-into-container/service.ts new file mode 100644 index 00000000..42fcd243 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/node-into-container/service.ts @@ -0,0 +1,376 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion -- no need */ +import { throttle } from 'lodash'; +import { inject, injectable } from 'inversify'; +import { + type PositionSchema, + Rectangle, + type Disposable, + DisposableCollection, + Emitter, +} from '@flowgram.ai/utils'; +import { + type NodesDragEvent, + WorkflowDocument, + WorkflowDragService, + WorkflowLinesManager, + type WorkflowNodeEntity, + WorkflowNodeMeta, + WorkflowOperationBaseService, + WorkflowSelectService, +} from '@flowgram.ai/free-layout-core'; +import { HistoryService } from '@flowgram.ai/free-history-plugin'; +import { FlowNodeTransformData, FlowNodeRenderData } from '@flowgram.ai/document'; +import { PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core'; + +import type { NodeIntoContainerEvent, NodeIntoContainerState } from './type'; +import { NodeIntoContainerType } from './constant'; + +@injectable() +export class NodeIntoContainerService { + public state: NodeIntoContainerState; + + @inject(WorkflowDragService) + private dragService: WorkflowDragService; + + @inject(WorkflowDocument) + private document: WorkflowDocument; + + @inject(PlaygroundConfigEntity) + private playgroundConfig: PlaygroundConfigEntity; + + @inject(WorkflowOperationBaseService) + private operationService: WorkflowOperationBaseService; + + @inject(WorkflowLinesManager) + private linesManager: WorkflowLinesManager; + + @inject(HistoryService) private historyService: HistoryService; + + @inject(WorkflowSelectService) private selectService: WorkflowSelectService; + + private emitter = new Emitter(); + + private toDispose = new DisposableCollection(); + + public readonly on = this.emitter.event; + + public init(): void { + this.initState(); + this.toDispose.push(this.emitter); + } + + public ready(): void { + this.toDispose.push(this.listenDragToContainer()); + } + + public dispose(): void { + this.initState(); + this.toDispose.dispose(); + } + + /** 移除节点连线 */ + public async removeNodeLines(node: WorkflowNodeEntity): Promise { + const lines = this.linesManager.getAllLines(); + lines.forEach((line) => { + if (line.from.id !== node.id && line.to?.id !== node.id) { + return; + } + line.dispose(); + }); + await this.nextFrame(); + } + + /** 将节点移出容器 */ + public async moveOutContainer(params: { node: WorkflowNodeEntity }): Promise { + const { node } = params; + const parentNode = node.parent; + const containerNode = parentNode?.parent; + const nodeJSON = await this.document.toNodeJSON(node); + if ( + !parentNode || + !containerNode || + !this.isContainer(parentNode) || + !nodeJSON.meta?.position + ) { + return; + } + this.operationService.moveNode(node, { + parent: containerNode, + }); + const parentTransform = parentNode.getData(TransformData); + this.operationService.updateNodePosition(node, { + x: parentTransform.position.x + nodeJSON.meta!.position!.x, + y: parentTransform.position.y + nodeJSON.meta!.position!.y, + }); + await this.nextFrame(); + parentTransform.fireChange(); + this.emitter.fire({ + type: NodeIntoContainerType.Out, + node, + sourceContainer: parentNode, + targetContainer: containerNode, + }); + } + + /** 能否将节点移出容器 */ + public canMoveOutContainer(node: WorkflowNodeEntity): boolean { + const parentNode = node.parent; + const containerNode = parentNode?.parent; + if (!parentNode || !containerNode || !this.isContainer(parentNode)) { + return false; + } + return true; + } + + /** 初始化状态 */ + private initState(): void { + this.state = { + isDraggingNode: false, + isSkipEvent: false, + transforms: undefined, + dragNode: undefined, + dropNode: undefined, + sourceParent: undefined, + }; + } + + /** 监听节点拖拽 */ + private listenDragToContainer(): Disposable { + const draggingNode = (e: NodesDragEvent) => this.draggingNode(e); + const throttledDraggingNode = throttle(draggingNode, 200); // 200ms触发一次计算 + return this.dragService.onNodesDrag(async (event) => { + if (this.selectService.selectedNodes.length !== 1) { + return; + } + if (event.type === 'onDragStart') { + if (this.state.isSkipEvent) { + // 拖出容器后重新进入 + this.state.isSkipEvent = false; + return; + } + this.historyService.startTransaction(); // 开始合并历史记录 + this.state.isDraggingNode = true; + this.state.transforms = this.getContainerTransforms(); + this.state.dragNode = this.selectService.selectedNodes[0]; + this.state.dropNode = undefined; + this.state.sourceParent = this.state.dragNode?.parent; + await this.dragOutContainer(event); // 检查是否需拖出容器 + } + if (event.type === 'onDragging') { + throttledDraggingNode(event); + } + if (event.type === 'onDragEnd') { + if (this.state.isSkipEvent) { + // 拖出容器情况下需跳过本次事件 + return; + } + throttledDraggingNode.cancel(); + draggingNode(event); // 直接触发一次计算,防止延迟 + await this.dropNodeToContainer(); // 放置节点 + await this.clearInvalidLines(); // 清除非法线条 + this.setDropNode(undefined); + this.initState(); // 重置状态 + this.historyService.endTransaction(); // 结束合并历史记录 + } + }); + } + + /** 监听节点拖拽出容器 */ + private async dragOutContainer(event: NodesDragEvent): Promise { + const { dragNode } = this.state; + const activated = event.triggerEvent.metaKey || event.triggerEvent.ctrlKey; + if ( + !activated || // 必须按住指定按键 + !dragNode || // 必须有一个节点 + !this.canMoveOutContainer(dragNode) // 需要能被移出容器 + ) { + return; + } + this.moveOutContainer({ node: dragNode }); + this.state.isSkipEvent = true; + event.dragger.stop(event.dragEvent.clientX, event.dragEvent.clientY); + await this.nextFrame(); + this.dragService.startDragSelectedNodes(event.triggerEvent); + } + + /** 移除节点所有非法连线 */ + private async clearInvalidLines(): Promise { + const { dragNode, sourceParent } = this.state; + if (!dragNode) { + return; + } + if (dragNode.parent === sourceParent) { + return; + } + const lines = this.linesManager.getAllLines(); + lines.forEach((line) => { + if (line.from.id !== dragNode.id && line.to?.id !== dragNode.id) { + return; + } + line.dispose(); + }); + await this.nextFrame(); + } + + /** 获取重叠位置 */ + private getCollisionTransform(params: { + transforms: FlowNodeTransformData[]; + targetRect?: Rectangle; + targetPoint?: PositionSchema; + withPadding?: boolean; + }): FlowNodeTransformData | undefined { + const { targetRect, targetPoint, transforms, withPadding = false } = params; + const collisionTransform = transforms.find((transform) => { + const { bounds, entity } = transform; + const padding = withPadding ? this.document.layout.getPadding(entity) : { left: 0, right: 0 }; + const transformRect = new Rectangle( + bounds.x + padding.left + padding.right, + bounds.y, + bounds.width, + bounds.height + ); + // 检测两个正方形是否相互碰撞 + if (targetRect) { + return this.isRectIntersects(targetRect, transformRect); + } + if (targetPoint) { + return this.isPointInRect(targetPoint, transformRect); + } + return false; + }); + return collisionTransform; + } + + /** 设置放置节点高亮 */ + private setDropNode(dropNode?: WorkflowNodeEntity) { + if (this.state.dropNode === dropNode) { + return; + } + if (this.state.dropNode) { + // 清除上一个节点高亮 + const renderData = this.state.dropNode.getData(FlowNodeRenderData); + const renderDom = renderData.node?.children?.[0] as HTMLElement; + if (renderDom) { + renderDom.style.outline = ''; + } + } + this.state.dropNode = dropNode; + if (!dropNode) { + return; + } + // 设置当前节点高亮 + const renderData = dropNode.getData(FlowNodeRenderData); + const renderDom = renderData.node?.children?.[0] as HTMLElement; + if (renderDom) { + renderDom.style.outline = '1px solid var(--coz-stroke-hglt,#4e40e5)'; + } + } + + /** 获取容器节点transforms */ + private getContainerTransforms(): FlowNodeTransformData[] { + return this.document + .getRenderDatas(FlowNodeTransformData, false) + .filter((transform) => { + const { entity } = transform; + if (entity.originParent) { + return entity.getNodeMeta().selectable && entity.originParent.getNodeMeta().selectable; + } + return entity.getNodeMeta().selectable; + }) + .filter((transform) => this.isContainer(transform.entity)); + } + + /** 放置节点到容器 */ + private async dropNodeToContainer(): Promise { + const { dropNode, dragNode, isDraggingNode } = this.state; + if (!isDraggingNode || !dragNode || !dropNode) { + return; + } + return await this.moveIntoContainer({ + node: dragNode, + containerNode: dropNode, + }); + } + + /** 拖拽节点 */ + private draggingNode(nodeDragEvent: NodesDragEvent): void { + const { dragNode, isDraggingNode, transforms } = this.state; + if (!isDraggingNode || !dragNode || this.isContainer(dragNode) || !transforms?.length) { + return this.setDropNode(undefined); + } + const mousePos = this.playgroundConfig.getPosFromMouseEvent(nodeDragEvent.dragEvent); + const collisionTransform = this.getCollisionTransform({ + targetPoint: mousePos, + transforms: this.state.transforms ?? [], + }); + const dropNode = collisionTransform?.entity; + if (!dropNode || dragNode.parent?.id === dropNode.id) { + return this.setDropNode(undefined); + } + const canDrop = this.dragService.canDropToNode({ + dragNodeType: dragNode.flowNodeType, + dropNode, + }); + if (!canDrop.allowDrop) { + return this.setDropNode(undefined); + } + return this.setDropNode(canDrop.dropNode); + } + + /** 将节点移入容器 */ + private async moveIntoContainer(params: { + node: WorkflowNodeEntity; + containerNode: WorkflowNodeEntity; + }): Promise { + const { node, containerNode } = params; + const parentNode = node.parent; + const nodeJSON = await this.document.toNodeJSON(node); + + this.operationService.moveNode(node, { + parent: containerNode, + }); + + this.operationService.updateNodePosition( + node, + this.dragService.adjustSubNodePosition( + nodeJSON.type as string, + containerNode, + nodeJSON.meta!.position + ) + ); + + await this.nextFrame(); + + this.emitter.fire({ + type: NodeIntoContainerType.In, + node, + sourceContainer: parentNode, + targetContainer: containerNode, + }); + } + + private isContainer(node?: WorkflowNodeEntity): boolean { + return node?.getNodeMeta().isContainer ?? false; + } + + /** 判断点是否在矩形内 */ + private isPointInRect(point: PositionSchema, rect: Rectangle): boolean { + return ( + point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom + ); + } + + /** 判断两个矩形是否相交 */ + private isRectIntersects(rectA: Rectangle, rectB: Rectangle): boolean { + // 检查水平方向是否有重叠 + const hasHorizontalOverlap = rectA.right > rectB.left && rectA.left < rectB.right; + // 检查垂直方向是否有重叠 + const hasVerticalOverlap = rectA.bottom > rectB.top && rectA.top < rectB.bottom; + // 只有当水平和垂直方向都有重叠时,两个矩形才相交 + return hasHorizontalOverlap && hasVerticalOverlap; + } + + private async nextFrame(): Promise { + await new Promise((resolve) => requestAnimationFrame(resolve)); + } +} diff --git a/packages/plugins/free-container-plugin/src/node-into-container/type.ts b/packages/plugins/free-container-plugin/src/node-into-container/type.ts new file mode 100644 index 00000000..ee16f792 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/node-into-container/type.ts @@ -0,0 +1,20 @@ +import type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core'; +import type { FlowNodeTransformData } from '@flowgram.ai/document'; + +import type { NodeIntoContainerType } from './constant'; + +export interface NodeIntoContainerState { + isDraggingNode: boolean; + isSkipEvent: boolean; + transforms?: FlowNodeTransformData[]; + dragNode?: WorkflowNodeEntity; + dropNode?: WorkflowNodeEntity; + sourceParent?: WorkflowNodeEntity; +} + +export interface NodeIntoContainerEvent { + type: NodeIntoContainerType; + node: WorkflowNodeEntity; + sourceContainer?: WorkflowNodeEntity; + targetContainer: WorkflowNodeEntity; +} diff --git a/packages/plugins/free-container-plugin/src/plugin.ts b/packages/plugins/free-container-plugin/src/plugin.ts new file mode 100644 index 00000000..dc7aa1ea --- /dev/null +++ b/packages/plugins/free-container-plugin/src/plugin.ts @@ -0,0 +1,26 @@ +import { FlowRendererRegistry } from '@flowgram.ai/renderer'; +import { definePluginCreator } from '@flowgram.ai/core'; + +import type { WorkflowContainerPluginOptions } from './type'; +import { NodeIntoContainerService } from './node-into-container'; +import { ContainerNodeRenderKey, ContainerNodeRender } from './container-node-render'; + +export const createContainerNodePlugin = definePluginCreator({ + onBind: ({ bind }) => { + bind(NodeIntoContainerService).toSelf().inSingletonScope(); + }, + onInit(ctx) { + ctx.get(NodeIntoContainerService).init(); + + const registry = ctx.get(FlowRendererRegistry); + registry.registerReactComponent(ContainerNodeRenderKey, ContainerNodeRender); + }, + onReady(ctx, options) { + if (options.disableNodeIntoContainer !== true) { + ctx.get(NodeIntoContainerService).ready(); + } + }, + onDispose(ctx) { + ctx.get(NodeIntoContainerService).dispose(); + }, +}); diff --git a/packages/plugins/free-container-plugin/src/type.ts b/packages/plugins/free-container-plugin/src/type.ts new file mode 100644 index 00000000..b62aeba6 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/type.ts @@ -0,0 +1,3 @@ +export interface WorkflowContainerPluginOptions { + disableNodeIntoContainer?: boolean; +} diff --git a/packages/plugins/free-container-plugin/tsconfig.json b/packages/plugins/free-container-plugin/tsconfig.json new file mode 100644 index 00000000..923598d3 --- /dev/null +++ b/packages/plugins/free-container-plugin/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json", + "compilerOptions": { + }, + "include": ["./src"], + "exclude": ["node_modules"] +} diff --git a/packages/plugins/free-container-plugin/vitest.config.ts b/packages/plugins/free-container-plugin/vitest.config.ts new file mode 100644 index 00000000..97c9de9b --- /dev/null +++ b/packages/plugins/free-container-plugin/vitest.config.ts @@ -0,0 +1,26 @@ +const path = require('path'); +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + build: { + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + test: { + globals: true, + mockReset: false, + environment: 'jsdom', + setupFiles: [path.resolve(__dirname, './vitest.setup.ts')], + include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'], + exclude: [ + '**/__mocks__**', + '**/node_modules/**', + '**/dist/**', + '**/lib/**', // lib 编译结果忽略掉 + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + ], + }, +}); diff --git a/packages/plugins/free-container-plugin/vitest.setup.ts b/packages/plugins/free-container-plugin/vitest.setup.ts new file mode 100644 index 00000000..d2c9bc6e --- /dev/null +++ b/packages/plugins/free-container-plugin/vitest.setup.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; From c83d06b5930a839c0aff8a2572f83934dc9ffd36 Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 19:18:56 +0800 Subject: [PATCH 09/22] feat(container): accessing form render --- .../components/border/index.tsx | 20 ++++++++++++++++--- .../components/border/style.ts | 2 +- .../components/container/index.tsx | 12 +++++++++-- .../components/form/index.tsx | 13 ++++++++++++ .../components/form/style.ts | 6 ++++++ .../components/header/index.tsx | 13 ++++++------ .../components/header/style.ts | 5 +---- .../container-node-render/components/index.ts | 1 + .../src/container-node-render/render.tsx | 9 ++++++--- 9 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/form/index.tsx create mode 100644 packages/plugins/free-container-plugin/src/container-node-render/components/form/style.ts diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx index b9a1aec0..73f3d55e 100644 --- a/packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/border/index.tsx @@ -1,7 +1,21 @@ import React, { type FC } from 'react'; +import { useNodeRender } from '@flowgram.ai/free-layout-core'; +import { FlowNodeTransformData } from '@flowgram.ai/document'; + import { ContainerNodeBorderStyle } from './style'; -export const ContainerNodeBorder: FC = () => ( - -); +export const ContainerNodeBorder: FC = () => { + const { node } = useNodeRender(); + const transformData = node.getData(FlowNodeTransformData); + const topWidth = Math.max(transformData.padding.top - 50, 50); + + return ( + + ); +}; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts index 6b8f8313..06e4a6ff 100644 --- a/packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/border/style.ts @@ -15,7 +15,7 @@ export const ContainerNodeBorderStyle = styled.div` border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%)); border-color: var(--coz-bg-plus, rgb(249, 249, 249)); border-style: solid; - border-width: 48px 8px 8px; + border-width: 8px; border-radius: 8px; box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 4%), 0 4px 12px 0 rgba(0, 0, 0, 2%); diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx index 930b21d9..2d29dc93 100644 --- a/packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/container/index.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useState, type FC, type ReactNode } from 'react'; -import { useNodeRender } from '@flowgram.ai/free-layout-core'; +import { useNodeRender, WorkflowNodePortsData } from '@flowgram.ai/free-layout-core'; import { FlowNodeTransformData } from '@flowgram.ai/document'; import { useContainerNodeRenderProps } from '../../hooks'; import { ContainerNodeContainerStyle } from './style'; interface IContainerNodeContainer { - children: ReactNode[]; + children: ReactNode | ReactNode[]; } export const ContainerNodeContainer: FC = ({ children }) => { @@ -20,6 +20,13 @@ export const ContainerNodeContainer: FC = ({ children } const [width, setWidth] = useState(size.width); const [height, setHeight] = useState(size.height); + const updatePorts = () => { + requestAnimationFrame(() => { + const portsData = node.getData(WorkflowNodePortsData); + portsData.updateDynamicPorts(); + }); + }; + useEffect(() => { const updateSize = () => { // 无子节点时 @@ -39,6 +46,7 @@ export const ContainerNodeContainer: FC = ({ children } updateSize(); const dispose = transform.onDataChange(() => { updateSize(); + updatePorts(); }); return () => dispose.dispose(); }, [transform]); diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/form/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/form/index.tsx new file mode 100644 index 00000000..a460f315 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/form/index.tsx @@ -0,0 +1,13 @@ +import React, { type FC } from 'react'; + +import { useNodeRender } from '@flowgram.ai/free-layout-core'; + +import { ContainerNodeFormStyle } from './style'; + +export const ContainerNodeForm: FC = () => { + const { form } = useNodeRender(); + if (!form) { + return null; + } + return {form.render()}; +}; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/form/style.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/form/style.ts new file mode 100644 index 00000000..805df877 --- /dev/null +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/form/style.ts @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +export const ContainerNodeFormStyle = styled.div` + background-color: #fff; + border-radius: 8px 8px 0 0; +`; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx b/packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx index 078187a6..38cf0d50 100644 --- a/packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/header/index.tsx @@ -1,14 +1,15 @@ -import React, { type FC } from 'react'; +import React, { ReactNode, type FC } from 'react'; import { useNodeRender } from '@flowgram.ai/free-layout-core'; -import { useContainerNodeRenderProps } from '../../hooks'; import { ContainerNodeHeaderStyle } from './style'; -export const ContainerNodeHeader: FC = () => { - const { startDrag, onFocus, onBlur } = useNodeRender(); +interface IContainerNodeHeader { + children?: ReactNode | ReactNode[]; +} - const { title } = useContainerNodeRenderProps(); +export const ContainerNodeHeader: FC = ({ children }) => { + const { startDrag, onFocus, onBlur } = useNodeRender(); return ( { onFocus={onFocus} onBlur={onBlur} > -

{title}

+ {children}
); }; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts index b1e35684..f2f61f50 100644 --- a/packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/header/style.ts @@ -1,8 +1,6 @@ import styled from 'styled-components'; export const ContainerNodeHeaderStyle = styled.div` - cursor: move; - z-index: 0; display: flex; @@ -11,8 +9,7 @@ export const ContainerNodeHeaderStyle = styled.div` justify-content: flex-start; width: 100%; - height: 48px; - padding: 16px; + height: auto; border-radius: 8px 8px 0 0; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/components/index.ts b/packages/plugins/free-container-plugin/src/container-node-render/components/index.ts index a14b5834..62e684a8 100644 --- a/packages/plugins/free-container-plugin/src/container-node-render/components/index.ts +++ b/packages/plugins/free-container-plugin/src/container-node-render/components/index.ts @@ -3,3 +3,4 @@ export { ContainerNodeBackground } from './background'; export { ContainerNodeContainer } from './container'; export { ContainerNodeBorder } from './border'; export { ContainerNodePorts } from './ports'; +export { ContainerNodeForm } from './form'; diff --git a/packages/plugins/free-container-plugin/src/container-node-render/render.tsx b/packages/plugins/free-container-plugin/src/container-node-render/render.tsx index 61a23755..a5dd7c84 100644 --- a/packages/plugins/free-container-plugin/src/container-node-render/render.tsx +++ b/packages/plugins/free-container-plugin/src/container-node-render/render.tsx @@ -7,13 +7,16 @@ import { ContainerNodePorts, ContainerNodeBorder, ContainerNodeContainer, + ContainerNodeForm, } from './components'; -export const ContainerNodeRender: FC = (props) => ( +export const ContainerNodeRender: FC = () => ( - - + + + + ); From e7656d4cec986915f90350bb64095ff792898c2d Mon Sep 17 00:00:00 2001 From: liuyangxing Date: Mon, 17 Mar 2025 19:19:36 +0800 Subject: [PATCH 10/22] feat(free-demo): create loop node --- apps/demo-free-layout/package.json | 1 + .../demo-free-layout/src/assets/icon-loop.jpg | Bin 0 -> 25572 bytes .../src/components/base-node/index.tsx | 4 +- .../src/components/base-node/styles.tsx | 2 - .../src/context/node-render-context.ts | 5 +- .../form-components/form-content/index.tsx | 7 +- .../src/form-components/form-header/index.tsx | 18 +--- .../form-components/form-header/styles.tsx | 3 +- .../src/form-components/form-inputs/index.tsx | 7 +- .../form-components/properties-edit/index.tsx | 6 +- .../src/hooks/use-editor-props.tsx | 2 + .../condition/condition-inputs/index.tsx | 7 +- apps/demo-free-layout/src/nodes/index.ts | 2 + apps/demo-free-layout/src/nodes/loop/index.ts | 54 +++++++++++ apps/docs/package.json | 1 + common/config/rush/pnpm-lock.yaml | 85 ++++++++++++++++++ .../components/form/style.ts | 1 + rush.json | 6 ++ 18 files changed, 172 insertions(+), 39 deletions(-) create mode 100644 apps/demo-free-layout/src/assets/icon-loop.jpg create mode 100644 apps/demo-free-layout/src/nodes/loop/index.ts diff --git a/apps/demo-free-layout/package.json b/apps/demo-free-layout/package.json index de73eb02..8d2de2bd 100644 --- a/apps/demo-free-layout/package.json +++ b/apps/demo-free-layout/package.json @@ -35,6 +35,7 @@ "@flowgram.ai/free-lines-plugin": "workspace:*", "@flowgram.ai/free-node-panel-plugin": "workspace:*", "@flowgram.ai/minimap-plugin": "workspace:*", + "@flowgram.ai/free-container-plugin": "workspace:*", "lodash-es": "^4.17.21", "nanoid": "^4.0.2", "react": "^18", diff --git a/apps/demo-free-layout/src/assets/icon-loop.jpg b/apps/demo-free-layout/src/assets/icon-loop.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc26db1e168dc92f69c9a2aca7d6777943a7692f GIT binary patch literal 25572 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 literal 0 HcmV?d00001 diff --git a/apps/demo-free-layout/src/components/base-node/index.tsx b/apps/demo-free-layout/src/components/base-node/index.tsx index 7e0c953c..b016cf77 100644 --- a/apps/demo-free-layout/src/components/base-node/index.tsx +++ b/apps/demo-free-layout/src/components/base-node/index.tsx @@ -38,9 +38,7 @@ export const BaseNode = ({ node }: { node: FlowNodeEntity }) => { outline: form?.state.invalid ? '1px solid red' : 'none', }} > - - {form?.render()} - + {form?.render()} diff --git a/apps/demo-free-layout/src/components/base-node/styles.tsx b/apps/demo-free-layout/src/components/base-node/styles.tsx index 09d0526f..29acfc0b 100644 --- a/apps/demo-free-layout/src/components/base-node/styles.tsx +++ b/apps/demo-free-layout/src/components/base-node/styles.tsx @@ -6,14 +6,12 @@ export const BaseNodeStyle = styled.div` background-color: #fff; border: 1px solid rgba(6, 7, 9, 0.15); border-radius: 8px; - box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02); display: flex; flex-direction: column; justify-content: center; position: relative; width: 360px; - transition: all 0.3s ease; &.selected { border: 1px solid var(--coz-stroke-hglt, #4e40e5); } diff --git a/apps/demo-free-layout/src/context/node-render-context.ts b/apps/demo-free-layout/src/context/node-render-context.ts index 2820ff8a..5cf001a7 100644 --- a/apps/demo-free-layout/src/context/node-render-context.ts +++ b/apps/demo-free-layout/src/context/node-render-context.ts @@ -1,5 +1,6 @@ import React from 'react'; -import { type NodeRenderReturnType } from '@flowgram.ai/free-layout-editor'; +interface INodeRenderContext {} -export const NodeRenderContext = React.createContext({} as any); +/** 业务自定义节点上下文 */ +export const NodeRenderContext = React.createContext({}); diff --git a/apps/demo-free-layout/src/form-components/form-content/index.tsx b/apps/demo-free-layout/src/form-components/form-content/index.tsx index 42d9efd0..761f5c33 100644 --- a/apps/demo-free-layout/src/form-components/form-content/index.tsx +++ b/apps/demo-free-layout/src/form-components/form-content/index.tsx @@ -1,8 +1,7 @@ -import React, { useContext } from 'react'; +import React from 'react'; -import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor'; +import { FlowNodeRegistry, useNodeRender } from '@flowgram.ai/free-layout-editor'; -import { NodeRenderContext } from '../../context'; import { FormTitleDescription, FormWrapper } from './styles'; /** @@ -10,7 +9,7 @@ import { FormTitleDescription, FormWrapper } from './styles'; * @constructor */ export function FormContent(props: { children?: React.ReactNode }) { - const { expanded, node } = useContext(NodeRenderContext); + const { expanded, node } = useNodeRender(); const registry = node.getNodeRegistry(); return ( diff --git a/apps/demo-free-layout/src/form-components/form-header/index.tsx b/apps/demo-free-layout/src/form-components/form-header/index.tsx index 350502ea..11f8a887 100644 --- a/apps/demo-free-layout/src/form-components/form-header/index.tsx +++ b/apps/demo-free-layout/src/form-components/form-header/index.tsx @@ -1,25 +1,22 @@ -import { useContext } from 'react'; - import { Command, Field, FieldRenderProps, useClientContext, + useNodeRender, } from '@flowgram.ai/free-layout-editor'; -import { IconButton, Dropdown, Typography, Button } from '@douyinfe/semi-ui'; -import { IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons'; +import { IconButton, Dropdown, Typography } from '@douyinfe/semi-ui'; import { IconMore } from '@douyinfe/semi-icons'; import { Feedback } from '../feedback'; import { FlowNodeRegistry } from '../../typings'; -import { NodeRenderContext } from '../../context'; import { getIcon } from './utils'; import { Header, Operators, Title } from './styles'; const { Text } = Typography; function DropdownContent() { - const { node, deleteNode } = useContext(NodeRenderContext); + const { node, deleteNode } = useNodeRender(); const clientContext = useClientContext(); const registry = node.getNodeRegistry(); const handleCopy = () => { @@ -41,7 +38,7 @@ function DropdownContent() { } export function FormHeader() { - const { node, expanded, toggleExpand, readonly } = useContext(NodeRenderContext); + const { node, expanded, toggleExpand, readonly } = useNodeRender(); return (
@@ -56,13 +53,6 @@ export function FormHeader() { )} -