diff --git a/apps/basic/src/pages/dag/toolbar/index.tsx b/apps/basic/src/pages/dag/toolbar/index.tsx index b6c9a62e..afc58058 100644 --- a/apps/basic/src/pages/dag/toolbar/index.tsx +++ b/apps/basic/src/pages/dag/toolbar/index.tsx @@ -1,9 +1,9 @@ -import { PlayCircleOutlined, CopyOutlined } from '@ant-design/icons'; -import type { Edge, NodeOptions, Node } from '@antv/xflow'; +import { CopyOutlined, PlayCircleOutlined } from '@ant-design/icons'; +import type { Edge, Node, NodeOptions } from '@antv/xflow'; import { - useGraphInstance, useClipboard, useGraphEvent, + useGraphInstance, useGraphStore, useKeyboard, } from '@antv/xflow'; @@ -88,7 +88,7 @@ const Toolbar = () => { type="primary" size="small" style={{ fontSize: 12 }} - onClick={handleExcute} + onClick={handleExecute} > 全部执行 diff --git a/apps/basic/src/pages/diff/index.tsx b/apps/basic/src/pages/diff/index.tsx index f7266bbe..8777aa9e 100644 --- a/apps/basic/src/pages/diff/index.tsx +++ b/apps/basic/src/pages/diff/index.tsx @@ -1,5 +1,5 @@ -/* eslint-disable */ import { DiffGraph } from '@antv/xflow-diff'; +import { useState } from 'react'; // 变更前数据 const originalData = { @@ -318,9 +318,23 @@ const currentData = { }; const Page = () => { + const [showDiffDetail, setShowDiffDetail] = useState(true); + + const onClickHandle = () => { + setShowDiffDetail(!showDiffDetail); + }; return ( -
- +
+ +
+ +
); }; diff --git a/packages/diff/README.md b/packages/diff/README.md index 3ee31410..b2da062e 100644 --- a/packages/diff/README.md +++ b/packages/diff/README.md @@ -1,3 +1,104 @@ [English (US)](README.md) | 简体中文 -# Diff +# XFlow Diff + +XFlow Diff 是一个用于跟踪、比较和合并图形结构(如节点和边)差异的工具,适用于图形编辑器 +和可视化应用。 + +## 目录 + +- [特性](#特性) +- [安装](#安装) +- [使用示例](#使用示例) + +## 特性 + +- 支持对比不同版本的图形状态。 +- 支持自定义比较函数和变更详情面板的展示。 + +## 安装 + +您可以通过 npm 安装 XFlow Diff: + +```shell +# npm +$ npm install @antv/xflow --save + +# yarn +$ yarn add @antv/xflow + +# pnpm +$ pnpm add @antv/xflow +``` + +## 使用示例 + +请参照:apps/basic/src/pages/diff/index.tsx + +```tsx +import React from 'react'; +import { DiffGraph } from '@antv/xflow-diff'; + +const originalData = { + nodes: [ + { id: 'node1', label: 'Node 1' }, + { id: 'node2', label: 'Node 2' }, + ], + edges: [{ source: 'node1', target: 'node2' }], +}; + +const currentData = { + nodes: [ + { id: 'node1', label: 'Node 1' }, + { id: 'node2', label: 'Node 2' }, + { id: 'node3', label: 'Node 3' }, + ], + edges: [ + { source: 'node1', target: 'node2' }, + { source: 'node2', target: 'node3' }, + ], +}; + +const App = () => { + return ( + + ); +}; +``` + +## API 详情 + +```tsx +export interface DiffGraphOptions { + /** 原始数据 */ + originalData: GraphData; + /** 变更后数据 */ + currentData: GraphData; + /** 新增颜色 */ + addColor?: string; + /** 新增节点扩展属性 */ + addExtAttr?: object; + /** 删除颜色 */ + delColor?: string; + /** 删除节点扩展属性 */ + delExtAttr?: object; + /** 变更颜色 */ + changeColor?: string; + /** 变更节点扩展属性 */ + changeExtAttr?: object; + /** 画布配置 */ + graphOptions?: GraphOptions; + /** 展示diff详情 */ + showDiffDetail?: boolean; + /** 自定义渲染diff详情 */ + customRenderDiffDetail?: (detail: DiffInfo[]) => React.ReactNode; + /** 节点描述字段属性key */ + nodeDescKey?: string; + /** 边描述字段属性key */ + edgeDescKey?: string; +} +``` diff --git a/packages/diff/package.json b/packages/diff/package.json index 872784df..8245e4fe 100644 --- a/packages/diff/package.json +++ b/packages/diff/package.json @@ -1,28 +1,36 @@ { "name": "@antv/xflow-diff", - "version": "1.0.0", + "version": "1.1.0", + "private": false, "description": "", + "keywords": [ + "xflow", + "x6", + "antv" + ], + "bugs": { + "url": "https://github.com/antvis/XFlow/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/antvis/XFlow.git", + "directory": "packages/diff" + }, "main": "dist/index.cjs.js", "module": "dist/index.esm.js", "types": "dist/typing/index.d.ts", - "private": false, "files": [ "dist", "src" ], - "keywords": [ - "xflow", - "x6", - "antv" - ], "scripts": { - "setup": "tsup src/index.ts", "build": "tsup src/index.ts", "dev": "tsup src/index.ts --watch", - "lint:js": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint:css": "stylelint --allow-empty-input 'src/**/*.{css,less}'", "lint:format": "prettier --check *.md *.json 'src/**/*.{js,jsx,ts,tsx,css,less,md,json}'", + "lint:js": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint:typing": "tsc --noEmit", + "setup": "tsup src/index.ts", "test": "jest --coverage" }, "dependencies": { @@ -39,14 +47,6 @@ "react": ">=16.8.6 || >=17.0.0 || >=18.0.0", "react-dom": ">=16.8.6 || >=17.0.0 || >= 18.0.0" }, - "bugs": { - "url": "https://github.com/antvis/XFlow/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/antvis/XFlow.git", - "directory": "packages/diff" - }, "publishConfig": { "access": "public" } diff --git a/packages/diff/src/components/DiffDetailPanel/index.less b/packages/diff/src/components/DiffDetailPanel/index.less new file mode 100644 index 00000000..f07dbab3 --- /dev/null +++ b/packages/diff/src/components/DiffDetailPanel/index.less @@ -0,0 +1,17 @@ +.xflow-diff-panel { + display: flex; + overflow: auto; + height: 30%; + min-height: 10%; + width: 100%; + + .info-tag { + font-size: small; + padding: 3px; + } + + .table-header { + font-weight: bold; + font-size: 24px; + } +} diff --git a/packages/diff/src/components/DiffDetailPanel/index.tsx b/packages/diff/src/components/DiffDetailPanel/index.tsx new file mode 100644 index 00000000..2fef9a3d --- /dev/null +++ b/packages/diff/src/components/DiffDetailPanel/index.tsx @@ -0,0 +1,111 @@ +import type { EdgeOptions, NodeOptions } from '@antv/xflow'; +import { Table, Tag } from 'antd'; +import React from 'react'; + +import type { DiffDetailPanelProps, DiffInfo } from '@/types'; + +import './index.less'; + +export const DiffDetailPanel: React.FC = (props) => { + const { + showDiffDetail, + diffDetailInfo, + customRenderDiffDetail, + addColor, + delColor, + changeColor, + nodeDescKey = 'label', + edgeDescKey = 'id', + } = props; + if (!showDiffDetail || !diffDetailInfo?.length) { + return ; + } + + /** + * diff详情描述词典 + */ + const DiffTypeDict: { + [keys in Exclude]: { + color: string; + text: string; + }; + } = { + ADD: { + color: addColor, + text: '新增', + }, + DEL: { + color: delColor, + text: '删除', + }, + CHG: { + color: changeColor, + text: '变更', + }, + }; + + const CellDict: { + [keys in DiffInfo['cellType']]: string; + } = { + Edge: '边', + Node: '节点', + }; + + const getDesc = (data: DiffInfo['currentData'], cellType: DiffInfo['cellType']) => { + if (cellType === 'Node') { + return (data as NodeOptions)[nodeDescKey]; + } else { + return (data as EdgeOptions)[edgeDescKey]; + } + }; + + const defaultDiffPanel = ( + + title={() => {'Diff变更详情列表'}} + pagination={false} + bodyStyle={{ textAlign: 'center' }} + bordered + columns={[ + { + title: '变更详情', + dataIndex: 'currentData', + render: (currentData: DiffInfo['currentData'], record) => { + const oriPreStr = record.originalData + ? getDesc(record.originalData, record.cellType) + ' -> ' + : ''; + const curStr = getDesc(currentData, record.cellType); + return `${oriPreStr}${curStr}`; + }, + }, + { + title: '元素类型', + dataIndex: 'cellType', + render: (text: DiffInfo['cellType']) => { + return CellDict[text]; + }, + }, + { + title: '变更类型', + dataIndex: 'diffType', + render: (text: Exclude) => { + const dicVal = DiffTypeDict[text]; + return ( + + {dicVal.text} + + ); + }, + }, + ]} + dataSource={diffDetailInfo} + /> + ); + + return ( +
+ {customRenderDiffDetail + ? customRenderDiffDetail(diffDetailInfo) + : defaultDiffPanel} +
+ ); +}; diff --git a/packages/diff/src/components/DiffGraph/index.tsx b/packages/diff/src/components/DiffGraph/index.tsx index a86ab3d7..8bd84a5d 100644 --- a/packages/diff/src/components/DiffGraph/index.tsx +++ b/packages/diff/src/components/DiffGraph/index.tsx @@ -3,9 +3,11 @@ import { XFlow, XFlowGraph } from '@antv/xflow'; import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; -import type { DiffGraphOptions } from '@/types'; +import type { DiffGraphOptions, DiffInfo } from '@/types'; import { compare, syncGraph } from '@/util'; +import { DiffDetailPanel } from '../DiffDetailPanel'; + import '../../styles/index.less'; import Tool from './tool'; @@ -20,6 +22,8 @@ const DiffGraph: FC = (props) => { changeColor = '#ffc069', // 变更节点的颜色 changeExtAttr, graphOptions, + showDiffDetail = false, + customRenderDiffDetail, } = props; const [originalDataWithDiffInfo, setOriginalDataWithDiffInfo] = useState<{ @@ -32,6 +36,7 @@ const DiffGraph: FC = (props) => { }>({ nodes: [], edges: [] }); const [status, setStatus] = useState<'init' | 'computing' | 'done'>('init'); const [graphs, setGraphs] = useState[]>([]); + const [diffDetailInfo, setDiffDetailInfo] = useState(); useEffect(() => { // 获取 diff 信息,注入 attr @@ -39,6 +44,7 @@ const DiffGraph: FC = (props) => { const { originalDataWithDiffInfo: originalDataWithDiffInfoRe, currentDataWithDiffInfo: currentDataWithDiffInfoRe, + diffDetailInfo: diffDetailInfoRe, } = compare( originalData, currentData, @@ -52,6 +58,7 @@ const DiffGraph: FC = (props) => { setOriginalDataWithDiffInfo(originalDataWithDiffInfoRe); setCurrentDataWithDiffInfo(currentDataWithDiffInfoRe); + setDiffDetailInfo(diffDetailInfoRe); setStatus('done'); }, []); // eslint-disable-line @@ -69,50 +76,61 @@ const DiffGraph: FC = (props) => { }; return ( -
- {/* 左图 */} - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - - {status === 'done' && ( - - )} - + {/* diff详情展示 */} + +
+ {/* 左图 */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + + {status === 'done' && ( + + )} + - + }} + {...graphOptions} + /> + - {/* 右图 */} - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - - {status === 'done' && ( - - )} - + {status === 'done' && ( + + )} + - + }} + {...graphOptions} + /> + +
); }; diff --git a/packages/diff/src/components/DiffGraph/tool.tsx b/packages/diff/src/components/DiffGraph/tool.tsx index 8958a08c..5d92b818 100644 --- a/packages/diff/src/components/DiffGraph/tool.tsx +++ b/packages/diff/src/components/DiffGraph/tool.tsx @@ -1,7 +1,7 @@ import type { EdgeOptions, NodeOptions } from '@antv/xflow'; import { useGraphInstance, useGraphStore } from '@antv/xflow'; -import type { FC } from 'react'; -import React, { useEffect } from 'react'; +import type React from 'react'; +import { useEffect } from 'react'; interface ToolOptions { data: { @@ -11,7 +11,7 @@ interface ToolOptions { addGraph: (graph: ReturnType) => void; } -const Tool: FC = (props) => { +const Tool: React.FC = (props) => { const { data, addGraph } = props; const initData = useGraphStore((state) => state.initData); const graphIns = useGraphInstance(); diff --git a/packages/diff/src/styles/index.less b/packages/diff/src/styles/index.less index ec7e20bb..1390f111 100644 --- a/packages/diff/src/styles/index.less +++ b/packages/diff/src/styles/index.less @@ -2,13 +2,11 @@ position: relative; display: flex; height: 100%; + flex-direction: column; - &::after { - position: absolute; - left: 50%; - width: 1px; - height: 100%; - background-color: #333; - content: ''; + .xflow-container { + flex: 1; + overflow: hidden; + display: flex; } } diff --git a/packages/diff/src/types/index.ts b/packages/diff/src/types/index.ts index d55c1c81..4adb4ec9 100644 --- a/packages/diff/src/types/index.ts +++ b/packages/diff/src/types/index.ts @@ -3,13 +3,30 @@ import type { EdgeOptions, GraphOptions, NodeOptions } from '@antv/xflow'; export interface DiffGraphOptions { originalData: GraphData; currentData: GraphData; - addColor?: ''; + addColor?: string; addExtAttr?: object; - delColor?: ''; + delColor?: string; delExtAttr?: object; - changeColor?: ''; + changeColor?: string; changeExtAttr?: object; graphOptions?: GraphOptions; + /** 展示diff详情 */ + showDiffDetail?: boolean; + /** 自定义渲染diff详情 */ + customRenderDiffDetail?: (detail: DiffInfo[]) => React.ReactNode; + /** 节点描述字段属性key */ + nodeDescKey?: string; + /** 边描述字段属性key */ + edgeDescKey?: string; +} + +export interface DiffDetailPanelProps + extends Pick< + DiffGraphOptions, + 'showDiffDetail' | 'customRenderDiffDetail' | 'nodeDescKey' | 'edgeDescKey' + >, + Required> { + diffDetailInfo?: DiffInfo[]; } export interface GraphData { @@ -17,13 +34,37 @@ export interface GraphData { edges: EdgeOptions[]; } +export type DiffType = 'DEL' | 'ADD' | 'CHG' | 'NONE'; + +type CellType = 'Node' | 'Edge'; + export interface NodeOptionsWithDiffInfo extends NodeOptions { - diffType?: 'DEL' | 'ADD' | 'CHG' | 'NONE'; + diffType?: DiffType; } export interface EdgeOptionsWithDiffInfo extends EdgeOptions { - diffType?: 'DEL' | 'ADD' | 'CHG' | 'NONE'; + diffType?: DiffType; } +export type DiffInfo = + | { + diffType: DiffType; + originalData?: NodeOptions | EdgeOptions; + currentData: NodeOptions | EdgeOptions; + cellType: CellType; + } + | { + diffType: DiffType; + originalData?: NodeOptions; + currentData: NodeOptions; + cellType: 'Node'; + } + | { + diffType: DiffType; + originalData?: EdgeOptions; + currentData: EdgeOptions; + cellType: 'Edge'; + }; + export interface GraphDataWithDiffInfo { nodes: NodeOptionsWithDiffInfo[]; edges: EdgeOptionsWithDiffInfo[]; diff --git a/packages/diff/src/util/index.ts b/packages/diff/src/util/index.ts index 6c2dba97..61143640 100644 --- a/packages/diff/src/util/index.ts +++ b/packages/diff/src/util/index.ts @@ -1,6 +1,6 @@ import type { useGraphInstance } from '@antv/xflow'; -import type { GraphData, GraphDataWithDiffInfo } from '..'; +import type { DiffInfo, GraphData, GraphDataWithDiffInfo } from '..'; export const compare: ( originalData: GraphData, @@ -14,6 +14,7 @@ export const compare: ( ) => { originalDataWithDiffInfo: GraphDataWithDiffInfo; currentDataWithDiffInfo: GraphDataWithDiffInfo; + diffDetailInfo: DiffInfo[]; } = ( originalData, currentData, @@ -33,12 +34,19 @@ export const compare: ( edges: [...currentData.edges], }; + const diffDetailInfo: DiffInfo[] = []; + // 寻找 currentData 中 originalData 没有的数据,即为新增的 const originalIds = originalData.nodes .map((node) => node.id) .concat(originalData.edges.map((edge) => edge.id)); + for (let i = 0; i < currentData.nodes.length; i++) { if (!originalIds.includes(currentData.nodes[i].id)) { + console.log( + currentDataWithDiffInfo.nodes[i], + currentDataWithDiffInfo.nodes[i].diffType, + ); currentDataWithDiffInfo.nodes[i].diffType = 'ADD'; currentDataWithDiffInfo.nodes[i].attrs = { ...currentDataWithDiffInfo.nodes[i].attrs, @@ -48,8 +56,14 @@ export const compare: ( }, ...addExtAttr, }; + diffDetailInfo.push({ + diffType: 'ADD', + currentData: currentData.nodes[i], + cellType: 'Node', + }); } } + for (let i = 0; i < currentData.edges.length; i++) { if (!originalIds.includes(currentData.edges[i].id)) { currentDataWithDiffInfo.edges[i].diffType = 'ADD'; @@ -61,13 +75,19 @@ export const compare: ( }, ...addExtAttr, }; + + diffDetailInfo.push({ + diffType: 'ADD', + currentData: currentData.edges[i], + cellType: 'Edge', + }); } } - // 寻找 originalData 中 currentData 没有的数据,即为新增的 const currentIds = currentData.nodes .map((node) => node.id) .concat(currentData.edges.map((edge) => edge.id)); + for (let i = 0; i < originalData.nodes.length; i++) { if (!currentIds.includes(originalData.nodes[i].id)) { originalDataWithDiffInfo.nodes[i].diffType = 'DEL'; @@ -79,6 +99,12 @@ export const compare: ( }, ...delExtAttr, }; + + diffDetailInfo.push({ + diffType: 'DEL', + currentData: currentData.nodes[i], + cellType: 'Node', + }); } } for (let i = 0; i < originalData.edges.length; i++) { @@ -92,6 +118,12 @@ export const compare: ( }, ...delExtAttr, }; + + diffDetailInfo.push({ + diffType: 'DEL', + currentData: currentData.edges[i], + cellType: 'Edge', + }); } } @@ -121,6 +153,13 @@ export const compare: ( }, ...changeExtAttr, }; + + diffDetailInfo.push({ + diffType: 'CHG', + originalData: originalData.nodes[i], + currentData: currentData.nodes[j], + cellType: 'Node', + }); } } } @@ -149,11 +188,18 @@ export const compare: ( }, ...changeExtAttr, }; + + diffDetailInfo.push({ + diffType: 'CHG', + originalData: originalData.edges[i], + currentData: currentData.edges[j], + cellType: 'Edge', + }); } } } - return { originalDataWithDiffInfo, currentDataWithDiffInfo }; + return { originalDataWithDiffInfo, currentDataWithDiffInfo, diffDetailInfo }; }; // 同步两图的缩放和移动