Skip to content

Commit

Permalink
feat: add history plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
NewByVector committed Nov 14, 2023
1 parent a820be5 commit dc8070c
Show file tree
Hide file tree
Showing 15 changed files with 314 additions and 67 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"favicons",
"hljs",
"immer",
"isequal",
"lcov",
"mfsu",
"nocheck",
Expand Down
3 changes: 2 additions & 1 deletion apps/basic/src/pages/basic/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { XFlow, XFlowGraph, Grid, Background, Clipboard } from '@antv/xflow';
import { XFlow, XFlowGraph, Grid, Background, Clipboard, History } from '@antv/xflow';

import styles from './index.less';
import { JSONCode } from './json';
Expand Down Expand Up @@ -28,6 +28,7 @@ const Page = () => {
<Grid type="mesh" options={{ color: '#ccc', thickness: 1 }} />
<JSONCode />
<Clipboard />
<History />
</div>
</XFlow>
</div>
Expand Down
23 changes: 22 additions & 1 deletion apps/basic/src/pages/basic/tools.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useGraphStore, useClipboard } from '@antv/xflow';
import { useGraphStore, useClipboard, useExport, useHistory } from '@antv/xflow';
import { Button } from 'antd';

import styles from './index.less';
Expand Down Expand Up @@ -112,6 +112,8 @@ const ToolsButton = () => {
const removeEdges = useGraphStore((state) => state.removeEdges);
const updateEdge = useGraphStore((state) => state.updateEdge);
const { copy, paste } = useClipboard();
const { exportPNG } = useExport();
const { undo, redo, canUndo, canRedo } = useHistory();

const onInit = () => {
initData(initialData);
Expand Down Expand Up @@ -222,6 +224,18 @@ const ToolsButton = () => {
paste();
};

const onExport = () => {
exportPNG('xflow', { padding: 20, copyStyles: false });
};

const onUndo = () => {
undo();
};

const onRedo = () => {
redo();
};

return (
<div className={styles.tools}>
<Button onClick={onInit}>initData</Button>
Expand All @@ -241,6 +255,13 @@ const ToolsButton = () => {
<Button onClick={onUnAnimateEdge}>unAnimateEdge</Button>
<Button onClick={onCopy}>copy</Button>
<Button onClick={onPaste}>paste</Button>
<Button onClick={onExport}>export</Button>
<Button onClick={onUndo} disabled={!canUndo}>
undo
</Button>
<Button onClick={onRedo} disabled={!canRedo}>
redo
</Button>
</div>
);
};
Expand Down
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,20 @@
"@antv/x6": "^2.15.3",
"@antv/x6-plugin-clipboard": "^2.1.6",
"@antv/x6-plugin-dnd": "^2.1.1",
"@antv/x6-plugin-export": "^2.1.6",
"@antv/x6-plugin-history": "^2.2.4",
"@antv/x6-plugin-keyboard": "^2.2.1",
"@antv/x6-plugin-selection": "^2.2.1",
"@antv/x6-react-shape": "^2.2.2",
"immer": "^10.0.3",
"lodash.isequal": "^4.5.0",
"zustand": "^4.4.3"
},
"devDependencies": {
"@antv/config-tsconfig": "workspace:^",
"@antv/config-tsup": "workspace:^",
"@antv/testing": "workspace:^",
"@types/lodash.isequal": "^4.5.8",
"@types/react": "^18.0.28",
"antd": "^5.0.0"
},
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/Background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Background = (props: Graph.BackgroundManager.Options) => {

useEffect(() => {
if (graph) {
graph.clearBackground();
graph.drawBackground(props);
}
}, [graph, props]);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Grid = <T extends GridTypes>(props: GridProps<T>) => {

useEffect(() => {
if (graph) {
graph.clearGrid();
graph.drawGrid({
...props.options,
type: props.type,
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/components/History.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { History as H } from '@antv/x6-plugin-history';
import { useEffect } from 'react';

import { useGraphInstance } from '../hooks/useGraphInstance';

const History = (props: Omit<H.Options, 'enabled'>) => {
const graph = useGraphInstance();

useEffect(() => {
if (graph) {
if (graph.getPlugin('history')) {
graph.disposePlugins('history');
}
graph.use(
new H({
enabled: true,
...props,
}),
);
}
}, [graph, props]);

return null;
};

export { History };
142 changes: 100 additions & 42 deletions packages/core/src/components/State.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
/* eslint-disable no-case-declarations */
import type { Graph } from '@antv/x6';
import { ObjectExt } from '@antv/x6';
import type { Graph, EventArgs, Cell } from '@antv/x6';
import { FunctionExt, ObjectExt } from '@antv/x6';
import { useEffect, type FC } from 'react';

import { useGraphEvent, useGraphInstance, useGraphStore } from '../hooks';
import type { ChangeItem } from '../store';
import type { GraphOptions, NodeOptions, EdgeOptions, GraphModel } from '../types';

const INNER_CALL = '__inner__';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preprocess = (key: keyof Cell.Properties, value: any) => {
if (key === 'position') {
return {
x: value.x,
y: value.y,
};
}
if (key === 'size') {
return {
width: value.width,
height: value.height,
};
}
return {
[key]: value,
};
};

const XFlowState: FC<
Pick<
GraphOptions,
Expand All @@ -21,16 +42,25 @@ const XFlowState: FC<
const graph = useGraphInstance();
const updateNode = useGraphStore((state) => state.updateNode);
const updateEdge = useGraphStore((state) => state.updateEdge);
const addNodes = useGraphStore((state) => state.addNodes);
const addEdges = useGraphStore((state) => state.addEdges);
const removeNodes = useGraphStore((state) => state.removeNodes);
const removeEdges = useGraphStore((state) => state.removeEdges);
const changeList = useGraphStore((state) => state.changeList);
const clearChangeList = useGraphStore((state) => state.clearChangeList);

const setSelectionStatus = (status: { id: string; selected: boolean }[]) => {
if (graph) {
const added = status.filter((item) => item.selected);
const removed = status.filter((item) => !item.selected);
graph.select(added.map((item) => item.id));
graph.unselect(removed.map((item) => item.id));
graph.select(
added.map((item) => item.id),
{ [INNER_CALL]: true },
);
graph.unselect(
removed.map((item) => item.id),
{ [INNER_CALL]: true },
);
}
};

Expand All @@ -40,11 +70,13 @@ const XFlowState: FC<
const cell = graph.getCellById(item.id);
if (cell) {
if (item.animated) {
cell.attr('line/strokeDasharray', 5);
cell.attr('line/style/animation', 'animated-line 30s infinite linear');
cell.attr('line/strokeDasharray', 5, { [INNER_CALL]: true });
cell.attr('line/style/animation', 'animated-line 30s infinite linear', {
[INNER_CALL]: true,
});
} else {
cell.attr('line/strokeDasharray', 0);
cell.attr('line/style/animation', '');
cell.attr('line/strokeDasharray', 0, { [INNER_CALL]: true });
cell.attr('line/style/animation', '', { [INNER_CALL]: true });
}
}
});
Expand Down Expand Up @@ -107,31 +139,35 @@ const XFlowState: FC<
initData(g, data);
break;
case 'addNodes':
g.addNodes(ObjectExt.cloneDeep(data));
g.addNodes(ObjectExt.cloneDeep(data), { [INNER_CALL]: true });
break;
case 'removeNodes':
g.removeCells(data);
g.removeCells(data, { [INNER_CALL]: true });
break;
case 'updateNode':
const { id: nodeId, data: changedNodeData } = data;
const node = g.getCellById(nodeId);
if (node) {
node.prop(changedNodeData);
g.startBatch('updateNode');
node.prop(changedNodeData, { [INNER_CALL]: true });
handleSpecialPropChange(nodeId, changedNodeData);
g.stopBatch('updateNode');
}
break;
case 'addEdges':
g.addEdges(ObjectExt.cloneDeep(data));
g.addEdges(ObjectExt.cloneDeep(data), { [INNER_CALL]: true });
break;
case 'removeEdges':
g.removeCells(data);
g.removeCells(data, { [INNER_CALL]: true });
break;
case 'updateEdge':
const { id: edgeId, data: changedEdgeData } = data;
const edge = g.getCellById(edgeId);
if (edge) {
edge.prop(changedEdgeData);
g.startBatch('updateEdge');
edge.prop(changedEdgeData, { [INNER_CALL]: true });
handleSpecialPropChange(edgeId, changedEdgeData);
g.stopBatch('updateEdge');
}
break;
default:
Expand All @@ -148,40 +184,62 @@ const XFlowState: FC<
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [changeList, graph]);

useGraphEvent('selection:changed', ({ added, removed }) => {
added.forEach((item) => {
if (item.isNode()) {
updateNode(item.id, { selected: true }, { silent: true });
} else if (item.isEdge()) {
updateEdge(item.id, { selected: true }, { silent: true });
// Add cells for internal operations
useGraphEvent('cell:added', ({ cell, options }) => {
if (!options[INNER_CALL]) {
if (cell.isNode()) {
addNodes([cell.toJSON()], { silent: true });
} else if (cell.isEdge()) {
addEdges([cell.toJSON()], { silent: true });
}
});
}
});

removed.forEach((item) => {
if (item.isNode()) {
updateNode(item.id, { selected: false }, { silent: true });
} else if (item.isEdge()) {
updateEdge(item.id, { selected: false }, { silent: true });
// Remove cells for internal operations
useGraphEvent('cell:removed', ({ cell, options }) => {
if (!options[INNER_CALL]) {
if (cell.isNode()) {
removeNodes([cell.id], { silent: true });
} else if (cell.isEdge()) {
removeEdges([cell.id], { silent: true });
}
});
}
});

useGraphEvent('node:change:position', ({ node }) => {
const { x, y } = node.position();
updateNode(node.id, { x, y }, { silent: true });
});
// Update cells for internal operations
useGraphEvent(
'cell:change:*',
FunctionExt.debounce(
({ cell, key, current, options }: EventArgs['cell:change:*']) => {
if (!options[INNER_CALL]) {
if (cell.isNode()) {
updateNode(cell.id, preprocess(key, current), { silent: true });
} else if (cell.isEdge()) {
updateEdge(cell.id, { [key]: current }, { silent: true });
}
}
},
100,
),
);

useGraphEvent('selection:changed', ({ added, removed, options }) => {
if (!options[INNER_CALL]) {
added.forEach((item) => {
if (item.isNode()) {
updateNode(item.id, { selected: true }, { silent: true });
} else if (item.isEdge()) {
updateEdge(item.id, { selected: true }, { silent: true });
}
});

useGraphEvent('edge:connected', ({ isNew, edge }) => {
const source = edge.getTerminal('source');
const target = edge.getTerminal('target');
if (source && target) {
if (isNew) {
addEdges([{ ...props.connectionEdgeOptions, id: edge.id, source, target }], {
silent: true,
});
} else {
updateEdge(edge.id, { source: source, target: target }, { silent: true });
}
removed.forEach((item) => {
if (item.isNode()) {
updateNode(item.id, { selected: false }, { silent: true });
} else if (item.isEdge()) {
updateEdge(item.id, { selected: false }, { silent: true });
}
});
}
});

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './Graph';
export * from './Grid';
export * from './Background';
export * from './Clipboard';
export * from './History';
2 changes: 2 additions & 0 deletions packages/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from './useGraphStore';
export * from './useGraphEvent';
export * from './useDnd';
export * from './useClipboard';
export * from './useExport';
export * from './useHistory';
16 changes: 4 additions & 12 deletions packages/core/src/hooks/useClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import type { Clipboard } from '@antv/x6-plugin-clipboard';
import { useCallback } from 'react';

import { useGraphInstance } from './useGraphInstance';
import { useGraphStore } from './useGraphStore';

export const useClipboard = () => {
const graph = useGraphInstance();
const addNodes = useGraphStore((state) => state.addNodes);
const addEdges = useGraphStore((state) => state.addEdges);

const isLoaded = useCallback(() => {
const loaded = graph && graph.getPlugin('clipboard');
const loaded = !!(graph && graph.getPlugin('clipboard'));
if (!loaded) {
console.warn('clipboard is not loaded, please use clipboard component first');
}
Expand Down Expand Up @@ -41,16 +38,11 @@ export const useClipboard = () => {
(pasteOptions?: Clipboard.PasteOptions) => {
if (graph && isLoaded()) {
const cells = graph.paste(pasteOptions);
cells.forEach((cell) => {
if (cell.isNode()) {
addNodes([cell.toJSON()], { silent: true });
} else if (cell.isEdge()) {
addEdges([cell.toJSON()], { silent: true });
}
});
return cells;
}
return [];
},
[graph, isLoaded, addNodes, addEdges],
[graph, isLoaded],
);

return { copy, cut, paste };
Expand Down
Loading

0 comments on commit dc8070c

Please sign in to comment.