mirror of
https://gitee.com/ByteDance/flowgram.ai.git
synced 2025-07-07 17:43:29 +08:00
feat(history): free layout supports move node into container operation
This commit is contained in:
parent
6c13bffbf3
commit
304cf69387
@ -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<OnNodeMoveEvent>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<OnNodeAddEvent>;
|
||||
|
||||
/**
|
||||
* 节点移动的回调
|
||||
*/
|
||||
onNodeMove: Event<OnNodeMoveEvent>;
|
||||
}
|
||||
|
||||
export const FlowOperationBaseService = Symbol('FlowOperationBaseService');
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import { inject } from 'inversify';
|
||||
import { IPoint, Emitter } from '@flowgram.ai/utils';
|
||||
import { FlowNodeEntityOrId, FlowOperationBaseServiceImpl } from '@flowgram.ai/document';
|
||||
import { TransformData } from '@flowgram.ai/core';
|
||||
|
||||
import { WorkflowDocument } from '../workflow-document';
|
||||
import {
|
||||
NodePostionUpdateEvent,
|
||||
WorkflowOperationBaseService,
|
||||
} from '../typings/workflow-operation';
|
||||
|
||||
export class WorkflowOperationBaseServiceImpl
|
||||
extends FlowOperationBaseServiceImpl
|
||||
implements WorkflowOperationBaseService
|
||||
{
|
||||
@inject(WorkflowDocument)
|
||||
protected declare document: WorkflowDocument;
|
||||
|
||||
private onNodePostionUpdateEmitter = new Emitter<NodePostionUpdateEvent>();
|
||||
|
||||
public readonly onNodePostionUpdate = this.onNodePostionUpdateEmitter.event;
|
||||
|
||||
updateNodePosition(nodeOrId: FlowNodeEntityOrId, position: IPoint): void {
|
||||
const node = this.toNodeEntity(nodeOrId);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformData = node.getData(TransformData);
|
||||
const oldPosition = {
|
||||
x: transformData.position.x,
|
||||
y: transformData.position.y,
|
||||
};
|
||||
transformData.update({
|
||||
position,
|
||||
});
|
||||
|
||||
this.onNodePostionUpdateEmitter.fire({
|
||||
node,
|
||||
oldPosition,
|
||||
newPosition: position,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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('');
|
||||
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
import { IPoint, Event } from '@flowgram.ai/utils';
|
||||
import {
|
||||
FlowNodeEntity,
|
||||
FlowNodeEntityOrId,
|
||||
FlowOperationBaseService,
|
||||
} from '@flowgram.ai/document';
|
||||
|
||||
export interface NodePostionUpdateEvent {
|
||||
node: FlowNodeEntity;
|
||||
oldPosition: IPoint;
|
||||
newPosition: IPoint;
|
||||
}
|
||||
|
||||
export interface WorkflowOperationBaseService extends FlowOperationBaseService {
|
||||
/**
|
||||
* 节点位置更新事件
|
||||
*/
|
||||
readonly onNodePostionUpdate: Event<NodePostionUpdateEvent>;
|
||||
/**
|
||||
* 更新节点位置
|
||||
* @param nodeOrId
|
||||
* @param position
|
||||
* @returns
|
||||
*/
|
||||
updateNodePosition(nodeOrId: FlowNodeEntityOrId, position: IPoint): void;
|
||||
}
|
||||
|
||||
export const WorkflowOperationBaseService = Symbol('WorkflowOperationBaseService');
|
||||
@ -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();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -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', {
|
||||
|
||||
@ -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>(WorkflowDocument);
|
||||
const historyService = ctx.get<HistoryService>(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,
|
||||
}
|
||||
);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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<DragNodeOperationValue, PluginContext, void> = {
|
||||
...baseOperationMeta,
|
||||
type: FreeOperationType.dragNodes,
|
||||
inverse: op => ({
|
||||
inverse: (op) => ({
|
||||
...op,
|
||||
value: {
|
||||
...op.value,
|
||||
@ -35,7 +35,7 @@ export const dragNodesOperationMeta: OperationMeta<DragNodeOperationValue, Plugi
|
||||
});
|
||||
// 嵌套情况下需将子节点 transform 设为 dirty
|
||||
if (node.collapsedChildren?.length > 0) {
|
||||
node.collapsedChildren.forEach(childNode => {
|
||||
node.collapsedChildren.forEach((childNode) => {
|
||||
const childNodeTransformData =
|
||||
childNode.getData<FlowNodeTransformData>(FlowNodeTransformData);
|
||||
childNodeTransformData.fireChange();
|
||||
@ -43,5 +43,4 @@ export const dragNodesOperationMeta: OperationMeta<DragNodeOperationValue, Plugi
|
||||
}
|
||||
});
|
||||
},
|
||||
shouldMerge: () => false,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
];
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { OperationMeta } from '@flowgram.ai/history';
|
||||
import { WorkflowDocument } from '@flowgram.ai/free-layout-core';
|
||||
import { MoveChildNodesOperationValue, OperationType } from '@flowgram.ai/document';
|
||||
import { FlowNodeBaseType } from '@flowgram.ai/document';
|
||||
import { PluginContext, TransformData } from '@flowgram.ai/core';
|
||||
|
||||
import { baseOperationMeta } from './base';
|
||||
|
||||
export const moveChildNodesOperationMeta: OperationMeta<
|
||||
MoveChildNodesOperationValue,
|
||||
PluginContext,
|
||||
void
|
||||
> = {
|
||||
...baseOperationMeta,
|
||||
type: OperationType.moveChildNodes,
|
||||
inverse: (op) => ({
|
||||
...op,
|
||||
value: {
|
||||
...op.value,
|
||||
fromIndex: op.value.toIndex,
|
||||
toIndex: op.value.fromIndex,
|
||||
fromParentId: op.value.toParentId,
|
||||
toParentId: op.value.fromParentId,
|
||||
},
|
||||
}),
|
||||
apply: (operation, ctx: PluginContext) => {
|
||||
const document = ctx.get<WorkflowDocument>(WorkflowDocument);
|
||||
document.moveChildNodes(operation.value);
|
||||
const fromContainer = document.getNode(operation.value.fromParentId);
|
||||
requestAnimationFrame(() => {
|
||||
if (fromContainer && fromContainer.flowNodeType !== FlowNodeBaseType.ROOT) {
|
||||
const fromContainerTransformData = fromContainer.getData(TransformData);
|
||||
fromContainerTransformData.fireChange();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -21,6 +21,7 @@ export enum FreeOperationType {
|
||||
changeNodeData = 'changeNodeData',
|
||||
resetLayout = 'resetLayout',
|
||||
dragNodes = 'dragNodes',
|
||||
moveChildNodes = 'moveChildNodes',
|
||||
}
|
||||
|
||||
export interface AddOrDeleteLineOperationValue extends WorkflowLinePortInfo {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user