fix: redo and undo problem when update item after additem, closes #2019

This commit is contained in:
Yanyan-Wang 2020-09-02 22:43:09 +08:00 committed by Yanyan Wang
parent 343e7658f0
commit 154cdeae3e
8 changed files with 305 additions and 97 deletions

View File

@ -2,9 +2,9 @@
#### 3.7.1
- fix: hide the tooltip plugin when drag node and contextmenu, #1975;
- fix: hide the tooltip plugin when drag node and contextmenu, closes #1975;
- fix: processParellelEdges without edge id problem;
- fix: label background with left, right position problem, #1861;
- fix: label background with left, right position problem, closes #1861;
- fix: create-edge and redo undo problem, #1976;
- fix: tooltip plugin with shouldBegin problem, closes #2006;
- fix: tooltip behavior with shouldBegin problem, closes #2016;
@ -12,7 +12,9 @@
- fix: fisheye destroy and new problem, closes: #2018;
- fix: node event with wrong canvasX and canvasY problem, closes: #2027;
- feat: improve the performance on the combos;
- feat: hide shapes beside keyShape while zooming.
- fix: redo and undo problem when update item after additem, closes #2019;
- feat: hide shapes beside keyShape while zooming;
- feat: improve the performance on the combos.
#### 3.7.0

View File

@ -107,6 +107,11 @@ export default {
} else {
this.targets.push(item);
}
const beforeDragNodes = [];
this.targets.forEach(t => {
beforeDragNodes.push(clone(t.getModel()));
});
this.set('beforeDragNodes', beforeDragNodes);
this.origin = {
x: evt.x,
@ -193,7 +198,15 @@ export default {
// 拖动结束后,入栈
if (graph.get('enabledStack')) {
graph.pushStack('update', clone(graph.save()));
const stackData = {
before: { nodes: this.get('beforeDragNodes'), edges: [], combos: [] },
after: { nodes: [], edges: [], combos: [] }
}
this.targets.forEach(target => {
stackData.after.nodes.push(target.getModel());
})
graph.pushStack('update', clone(stackData));
}
this.point = {};

View File

@ -395,7 +395,7 @@ export default class ItemController {
}
// 若移除的是节点,需要将与之相连的边一同删除
const edges = (item as INode).getEdges();
for (let i = edges.length; i >= 0; i--) {
for (let i = edges.length - 1; i >= 0; i--) {
graph.removeItem(edges[i], false);
}
if (comboId) graph.updateCombo(comboId);
@ -560,7 +560,7 @@ export default class ItemController {
* @param {boolean} visible
* @memberof ItemController
*/
public changeItemVisibility(item: Item | string, visible: boolean): void {
public changeItemVisibility(item: Item | string, visible: boolean): Item {
const { graph } = this;
if (isString(item)) {
@ -620,6 +620,7 @@ export default class ItemController {
});
}
graph.emit('afteritemvisibilitychange', { item, visible });
return item;
}
public destroy() {

View File

@ -853,13 +853,29 @@ export default class Graph extends EventEmitter implements IGraph {
*/
public showItem(item: Item | string, stack: boolean = true): void {
const itemController: ItemController = this.get('itemController');
itemController.changeItemVisibility(item, true);
const object = itemController.changeItemVisibility(item, true);
if (stack && this.get('enabledStack')) {
if (isString(item)) {
this.pushStack('visible', item);
} else {
this.pushStack('visible', (item as Item).getID());
let id = object.getID();
const type = object.getType();
const before: GraphData = {};
const after: GraphData = {};
switch (type) {
case 'node':
before.nodes = [{ id, visible: false }];
after.nodes = [{ id, visible: true }];
break;
case 'edge':
before.nodes = [{ id, visible: false }];
after.edges = [{ id, visible: true }];
break;
case 'combo':
before.nodes = [{ id, visible: false }];
after.combos = [{ id, visible: true }];
break;
default:
break;
}
this.pushStack('visible', { before, after });
}
}
@ -870,13 +886,29 @@ export default class Graph extends EventEmitter implements IGraph {
*/
public hideItem(item: Item | string, stack: boolean = true): void {
const itemController: ItemController = this.get('itemController');
itemController.changeItemVisibility(item, false);
const object = itemController.changeItemVisibility(item, false);
if (stack && this.get('enabledStack')) {
if (isString(item)) {
this.pushStack('visible', item);
} else {
this.pushStack('visible', (item as Item).getID());
let id = object.getID();
const type = object.getType();
const before: GraphData = {};
const after: GraphData = {};
switch (type) {
case 'node':
before.nodes = [{ id, visible: true }];
after.nodes = [{ id, visible: false }];
break;
case 'edge':
before.nodes = [{ id, visible: true }];
after.edges = [{ id, visible: false }];
break;
case 'combo':
before.nodes = [{ id, visible: true }];
after.combos = [{ id, visible: false }];
break;
default:
break;
}
this.pushStack('visible', { before, after });
}
}
@ -929,9 +961,34 @@ export default class Graph extends EventEmitter implements IGraph {
// 将删除的元素入栈
if (stack && this.get('enabledStack')) {
this.pushStack('delete', {
const deletedModel = {
...(nodeItem as Item).getModel(),
type,
}
const before: GraphData = {};
switch (type) {
case 'node':
before.nodes = [deletedModel as NodeConfig];
before.edges = [];
const edges = (nodeItem as INode).getEdges();
for (let i = edges.length - 1; i >= 0; i--) {
before.edges.push({
...edges[i].getModel(),
type: 'edge'
});
}
break;
case 'edge':
before.edges = [deletedModel as EdgeConfig];
break;
case 'combo':
before.combos = [deletedModel as ComboConfig];
break;
default:
break;
}
this.pushStack('delete', {
before, after: {}
});
}
@ -1079,9 +1136,26 @@ export default class Graph extends EventEmitter implements IGraph {
this.autoPaint();
if (stack && this.get('enabledStack')) {
this.pushStack('add', {
const addedModel = {
...item.getModel(),
type,
type
}
const after: GraphData = {};
switch (type) {
case 'node':
after.nodes = [addedModel];
break;
case 'edge':
after.edges = [addedModel];
break;
case 'combo':
after.combos = [addedModel];
break;
default:
break;
}
this.pushStack('add', {
before: {}, after
});
}
@ -1117,6 +1191,8 @@ export default class Graph extends EventEmitter implements IGraph {
currentItem = item;
}
const UnupdateModel = clone(currentItem.getModel());
let type = '';
if (currentItem.getType) type = currentItem.getType();
const states = [...currentItem.getStates()];
@ -1130,7 +1206,32 @@ export default class Graph extends EventEmitter implements IGraph {
}
if (stack && this.get('enabledStack')) {
this.pushStack();
const before = { nodes: [], edges: [], combos: [] };
const after = { nodes: [], edges: [], combos: [] };
const afterModel = {
id: UnupdateModel.id,
...cfg
};
switch (type) {
case 'node':
before.nodes.push(UnupdateModel);
after.nodes.push(afterModel);
break;
case 'edge':
before.edges.push(UnupdateModel);
after.edges.push(afterModel);
break;
case 'combo':
before.combos.push(UnupdateModel);
after.combos.push(afterModel);
break;
default:
break;
}
if (type === 'node') {
before.nodes.push(UnupdateModel);
}
this.pushStack('update', { before, after });
}
}
@ -1339,13 +1440,16 @@ export default class Graph extends EventEmitter implements IGraph {
* @return {object} this
*/
public changeData(data?: GraphData | TreeGraphData, stack: boolean = true): Graph {
if (stack && this.get('enabledStack')) {
this.pushStack('update', data);
}
const self = this;
if (!data) {
return this;
}
if (stack && this.get('enabledStack')) {
this.pushStack('changedata', {
before: self.save(),
after: data
});
}
this.set('comboSorted', false);
// 更改数据源后,取消所有状态
@ -3029,7 +3133,10 @@ export default class Graph extends EventEmitter implements IGraph {
return;
}
const stackData = data ? clone(data) : clone(this.save());
const stackData = data ? clone(data) : {
before: {},
after: clone(this.save())
};
if (stackType === 'redo') {
this.redoStack.push({

View File

@ -189,39 +189,61 @@ export default class ToolBar extends Base {
const currentData = undoStack.pop();
if (currentData) {
let { action, data } = currentData;
graph.pushStack(action, clone(data), 'redo');
let { action } = currentData;
graph.pushStack(action, clone(currentData.data), 'redo');
let data = currentData.data.before;
if (undoStack.length > 0 && (action === 'render' || action === 'update')) {
const current = undoStack.peek();
action = current.action;
data = current.data;
if (action === 'add') {
data = currentData.data.after;
}
if (!data) return;
switch (action) {
case 'visible': {
let item = data;
if (isString(data)) {
item = graph.findById(data);
}
if (item.get('visible')) {
graph.hideItem(item, false);
} else {
graph.showItem(item, false);
}
Object.keys(data).forEach(key => {
const array = data[key];
array && array.forEach(model => {
const item = graph.findById(model.id);
if (model.visible) {
graph.showItem(item, false);
} else {
graph.hideItem(item, false);
}
});
});
break;
}
case 'render':
case 'update':
Object.keys(data).forEach(key => {
const array = data[key];
array && array.forEach(model => {
graph.updateItem(model.id, model, false);
});
});
break;
case 'changedata':
graph.changeData(data, false);
break;
case 'delete': {
const { type, ...model } = data;
graph.addItem(type, model, false);
Object.keys(data).forEach(key => {
const array = data[key];
array && array.forEach(model => {
const type = model.type;
delete model.type;
graph.addItem(type, model, false);
});
});
break;
}
case 'add':
graph.removeItem(data.id, false);
Object.keys(data).forEach(key => {
const array = data[key];
array && array.forEach(model => {
graph.removeItem(model.id, false);
});
});
break;
default:
}
@ -241,38 +263,62 @@ export default class ToolBar extends Base {
let currentData = redoStack.pop();
if (currentData) {
let { action, data } = currentData;
graph.pushStack(action, clone(data));
if (action === 'render') {
currentData = redoStack.pop();
action = currentData.action;
data = currentData.data;
graph.pushStack(action, clone(data));
let { action } = currentData;
let data = currentData.data.after;
graph.pushStack(action, clone(currentData.data));
if (action === 'delete') {
data = currentData.data.before;
}
if (!data) return;
switch (action) {
case 'visible': {
let item = data;
if (isString(data)) {
item = graph.findById(data);
}
if (item.get('visible')) {
graph.hideItem(item, false);
} else {
graph.showItem(item, false);
}
Object.keys(data).forEach(key => {
const array = data[key];
array && array.forEach(model => {
const item = graph.findById(model.id);
if (model.visible) {
graph.showItem(item, false);
} else {
graph.hideItem(item, false);
}
});
});
break;
}
case 'render':
case 'update':
Object.keys(data).forEach(key => {
const array = data[key];
array && array.forEach(model => {
graph.updateItem(model.id, model, false);
});
});
break;
case 'changedata':
graph.changeData(data, false);
break;
case 'delete':
graph.removeItem(data.id, false);
data.edges && data.edges.forEach(model => {
graph.removeItem(model.id, false);
});
data.nodes && data.nodes.forEach(model => {
graph.removeItem(model.id, false);
});
data.combos && data.combos.forEach(model => {
graph.removeItem(model.id, false);
});
break;
case 'add': {
const { type, ...model } = data;
graph.addItem(type, model, false);
Object.keys(data).forEach(key => {
const array = data[key];
array && array.forEach(model => {
const type = model.type;
delete model.type;
graph.addItem(type, model, false);
});
});
break;
}
default:

View File

@ -5,8 +5,31 @@ import { IGraph } from '../../../src/interface/graph';
let graph: IGraph = null;
const data = {
nodes: [],
edges: [],
nodes: [{
id: '1'
}, {
id: '2'
},],
edges: [{
source: '1',
target: '2'
}],
};
const data2 = {
nodes: [{
id: 'new1'
}, {
id: 'new2'
}, {
id: '1'
},],
edges: [{
source: '1',
target: 'new2'
}, {
source: 'new1',
target: 'new2'
}],
};
const ToolBar = () => {
const container = React.useRef();
@ -21,7 +44,7 @@ const ToolBar = () => {
// 设置为true启用 redo & undo 栈功能
enabledStack: true,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
default: ['zoom-canvas', 'drag-node', { type: 'brush-select', }],
},
defaultNode: {
size: 50
@ -32,7 +55,7 @@ const ToolBar = () => {
graph.on('stackchange', e => {
// console.log(e)
})
graph.addItem('node', {
const node4 = graph.addItem('node', {
id: '4',
label: 'node-4',
x: 100,
@ -40,11 +63,11 @@ const ToolBar = () => {
description: 'This is node-4.',
subdescription: 'This is subdescription of node-3.',
})
graph.addItem('node', {
const node5 = graph.addItem('node', {
id: '5',
label: 'node-5',
x: 200,
y: 300,
y: 100,
description: 'This is node-5.',
subdescription: 'This is subdescription of node-5.',
})
@ -54,6 +77,22 @@ const ToolBar = () => {
target: '5',
})
graph.updateItem(node4, {
x: 100,
y: 200
})
graph.on('canvas:click', e => {
graph.removeItem(graph.getNodes()[0])
})
graph.on('node:click', e => {
graph.hideItem(e.item);
})
graph.on('canvas:dragstart', e => {
graph.changeData(data2);
})
}
});

View File

@ -1605,7 +1605,7 @@ describe('redo stack & undo stack', () => {
let redoStack = stackData.redoStack;
expect(undoStack.length).toBe(1);
expect(undoStack[0].action).toEqual('render');
expect(undoStack[0].data.nodes.length).toEqual(2);
expect(undoStack[0].data.after.nodes.length).toEqual(2);
expect(redoStack.length).toBe(0);
// update 后undo stack 中有 2 条数据,一条 render一条 update
@ -1620,9 +1620,9 @@ describe('redo stack & undo stack', () => {
let firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('update');
expect(firstStackData.data.nodes[0].id).toEqual('node1');
expect(firstStackData.data.nodes[0].x).toEqual(120);
expect(firstStackData.data.nodes[0].y).toEqual(200);
expect(firstStackData.data.after.nodes[0].id).toEqual('node1');
expect(firstStackData.data.after.nodes[0].x).toEqual(120);
expect(firstStackData.data.after.nodes[0].y).toEqual(200);
// 执行 update 后undo stack 中有3条数据
graph.update('node2', {
@ -1636,9 +1636,9 @@ describe('redo stack & undo stack', () => {
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('update');
expect(firstStackData.data.nodes[1].id).toEqual('node2');
expect(firstStackData.data.nodes[1].x).toEqual(120);
expect(firstStackData.data.nodes[1].y).toEqual(350);
expect(firstStackData.data.after.nodes[0].id).toEqual('node2');
expect(firstStackData.data.after.nodes[0].x).toEqual(120);
expect(firstStackData.data.after.nodes[0].y).toEqual(350);
// addItem 后undo 栈中有4条数据1个render、2个update、1个add
graph.addItem('node', {
@ -1654,9 +1654,9 @@ describe('redo stack & undo stack', () => {
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('add');
expect(firstStackData.data.id).toEqual('node3');
expect(firstStackData.data.x).toEqual(150);
expect(firstStackData.data.y).toEqual(150);
expect(firstStackData.data.after.nodes[0].id).toEqual('node3');
expect(firstStackData.data.after.nodes[0].x).toEqual(150);
expect(firstStackData.data.after.nodes[0].y).toEqual(150);
// hideItem 后undo 栈中有5条数据1个render、2个update、1个add、1个visible
graph.hideItem('node1');
@ -1667,7 +1667,7 @@ describe('redo stack & undo stack', () => {
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('visible');
expect(firstStackData.data).toEqual('node1');
expect(firstStackData.data.after.nodes[0].id).toEqual('node1');
// remove 后undo 栈中有6条数据1个render、2个update、1个add、1个visible、1个delete
graph.remove('node2');
@ -1678,8 +1678,8 @@ describe('redo stack & undo stack', () => {
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('delete');
expect(firstStackData.data.id).toEqual('node2');
expect(firstStackData.data.type).toEqual('node');
expect(firstStackData.data.before.nodes[0].id).toEqual('node2');
expect(firstStackData.data.before.nodes[0].type).toEqual('node');
});
it('clear stack', () => {

View File

@ -47,7 +47,7 @@ describe('toolbar', () => {
expect(undoStack.length).toBe(1);
expect(undoStack[0].action).toEqual('render');
expect(undoStack[0].data.nodes.length).toEqual(2);
expect(undoStack[0].data.after.nodes.length).toEqual(2);
expect(redoStack.length).toBe(0);
// update 后undo stack 中有 2 条数据,一条 render一条 update
@ -62,9 +62,9 @@ describe('toolbar', () => {
let firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('update');
expect(firstStackData.data.nodes[0].id).toEqual('node1');
expect(firstStackData.data.nodes[0].x).toEqual(120);
expect(firstStackData.data.nodes[0].y).toEqual(200);
expect(firstStackData.data.after.nodes[0].id).toEqual('node1');
expect(firstStackData.data.after.nodes[0].x).toEqual(120);
expect(firstStackData.data.after.nodes[0].y).toEqual(200);
// 执行 update 后undo stack 中有3条数据
graph.update('node2', {
@ -78,9 +78,9 @@ describe('toolbar', () => {
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('update');
expect(firstStackData.data.nodes[1].id).toEqual('node2');
expect(firstStackData.data.nodes[1].x).toEqual(120);
expect(firstStackData.data.nodes[1].y).toEqual(350);
expect(firstStackData.data.after.nodes[0].id).toEqual('node2');
expect(firstStackData.data.after.nodes[0].x).toEqual(120);
expect(firstStackData.data.after.nodes[0].y).toEqual(350);
// addItem 后undo 栈中有4条数据1个render、2个update、1个add
graph.addItem('node', {
@ -96,9 +96,9 @@ describe('toolbar', () => {
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('add');
expect(firstStackData.data.id).toEqual('node3');
expect(firstStackData.data.x).toEqual(150);
expect(firstStackData.data.y).toEqual(150);
expect(firstStackData.data.after.nodes[0].id).toEqual('node3');
expect(firstStackData.data.after.nodes[0].x).toEqual(150);
expect(firstStackData.data.after.nodes[0].y).toEqual(150);
// hideItem 后undo 栈中有5条数据1个render、2个update、1个add、1个visible
graph.hideItem('node1');
@ -109,7 +109,7 @@ describe('toolbar', () => {
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('visible');
expect(firstStackData.data).toEqual('node1');
expect(firstStackData.data.after.nodes[0].id).toEqual('node1');
// remove 后undo 栈中有6条数据1个render、2个update、1个add、1个visible、1个delete
graph.remove('node2');
@ -120,8 +120,8 @@ describe('toolbar', () => {
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('delete');
expect(firstStackData.data.id).toEqual('node2');
expect(firstStackData.data.type).toEqual('node');
expect(firstStackData.data.before.nodes[0].id).toEqual('node2');
expect(firstStackData.data.before.nodes[0].type).toEqual('node');
// 第一次 undo 后,撤销 remove node2 操作
toolbar.undo();
@ -134,13 +134,13 @@ describe('toolbar', () => {
firstStackData = redoStack[0];
expect(firstStackData.action).toEqual('delete');
expect(firstStackData.data.id).toEqual('node2');
expect(firstStackData.data.type).toEqual('node');
expect(firstStackData.data.before.nodes[0].id).toEqual('node2');
expect(firstStackData.data.before.nodes[0].type).toEqual('node');
// 此时 undo stack 中第一个元素应该是 visible node1 的数据
firstStackData = undoStack[0];
expect(firstStackData.action).toEqual('visible');
expect(firstStackData.data).toEqual('node1');
expect(firstStackData.data.after.nodes[0].id).toEqual('node1');
// 第二次 undo 后,撤销 hide node1 的操作
toolbar.undo();
@ -152,7 +152,7 @@ describe('toolbar', () => {
firstStackData = redoStack[0];
expect(firstStackData.action).toEqual('visible');
expect(firstStackData.data).toEqual('node1');
expect(firstStackData.data.after.nodes[0].id).toEqual('node1');
graph.destroy();
});