diff --git a/packages/core/package.json b/packages/core/package.json index 8aa4d25..b3bc3ad 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,11 +40,14 @@ "@antv/x6": "^0.13.0", "ace-builds": "^1.4.12", "antd": "^4.5.3", + "lodash.merge": "^4.6.2", + "query-string": "^6.13.7", "react-ace": "^9.1.3", "react-color": "^2.18.1", "react-json-view": "^1.19.1" }, "devDependencies": { + "@types/lodash.merge": "^4.6.6", "@types/react": "^16.9.34", "@types/react-color": "^3.0.4", "@types/react-dom": "^16.9.7", diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 68c735c..710499c 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -3,10 +3,18 @@ export interface ILocalConfig { port: string; } +export type ActionType = 'create' | 'update' | 'remove'; + +export interface IModifyGraphAction { + type: string; + actionType: ActionType; + data: any; +} + export const localConfig: ILocalConfig = { ip: '127.0.0.1', port: '3500' -}; +} export const updateLocalConfig = (config: ILocalConfig) => { localConfig.ip = config.ip || localConfig.ip; @@ -27,3 +35,31 @@ export const localSave = (data: any) => { body: JSON.stringify(data) }); } + +export const queryGraph = (projectId: string) => { + return fetch('/api/queryGraph', { + method: 'POST', + headers: {'Content-Type': 'application/json;charset=utf-8'}, + body: JSON.stringify({ + projectId + }) + }).then(res => res.json()).then(res => { + const {success} = res; + if(success) { + return res; + } else { + return Promise.reject(res); + } + }) +} + +export const modifyGraph = (projectId: string, actions: IModifyGraphAction[]) => { + return fetch('/api/modifyGraph', { + method: 'POST', + headers: {'Content-Type': 'application/json;charset=utf-8'}, + body: JSON.stringify({ + projectId, + actions + }) + }); +} diff --git a/packages/core/src/mods/flowChart/createFlowChart.ts b/packages/core/src/mods/flowChart/createFlowChart.ts index 3942c4d..e83f604 100644 --- a/packages/core/src/mods/flowChart/createFlowChart.ts +++ b/packages/core/src/mods/flowChart/createFlowChart.ts @@ -3,6 +3,7 @@ import {Cell, Edge, Graph, Node} from '@antv/x6'; import {MIN_ZOOM, MAX_ZOOM} from '../../common/const'; import baseCellSchemaMap from '../../common/baseCell'; import previewCellSchemaMap from '../../common/previewCell'; +import {registerServerStorage} from './registerServerStorage'; import MiniMapSimpleNode from '../../components/miniMapSimpleNode'; // X6 register base/preview cell shape @@ -32,25 +33,6 @@ const registerEvents = (flowChart: Graph): void => { } } }); - flowChart.on('edge:mouseenter', (args) => { - const cell = args.cell as Cell; - cell.addTools([ - { - name: 'target-arrowhead', - args: { - attrs: { - d: 'M -10.5 -6 1 0 -10.5 6 Z', - 'stroke-width': 0, - fill: '#333' - } - } - } - ]); - }); - flowChart.on('edge:mouseleave', (args) => { - const cell = args.cell as Cell; - cell.removeTools(['target-arrowhead']); - }); }; const registerShortcuts = (flowChart: Graph): void => { @@ -156,6 +138,7 @@ const createFlowChart = (container: HTMLDivElement, miniMapContainer: HTMLDivEle }); registerEvents(flowChart); registerShortcuts(flowChart); + registerServerStorage(flowChart); return flowChart; }; diff --git a/packages/core/src/mods/flowChart/index.tsx b/packages/core/src/mods/flowChart/index.tsx index 336c6c0..b6a1ae2 100644 --- a/packages/core/src/mods/flowChart/index.tsx +++ b/packages/core/src/mods/flowChart/index.tsx @@ -1,8 +1,11 @@ -import React, {useRef, useEffect} from 'react'; +import React, { useRef, useEffect } from 'react'; import styles from './index.module.less'; -import {Graph} from '@antv/x6'; +import { message } from 'antd'; +import { Graph } from '@antv/x6'; +import { queryGraph } from '../../api'; +import { parseQuery } from '../../utils'; import createFlowChart from './createFlowChart'; interface IProps { @@ -10,15 +13,29 @@ interface IProps { } const FlowChart: React.FC = props => { + const {onReady} = props; const graphRef = useRef(null); const miniMapRef = useRef(null); + useEffect(() => { - if(graphRef.current && miniMapRef.current) { + if (graphRef.current && miniMapRef.current) { const graph = createFlowChart(graphRef.current, miniMapRef.current); onReady(graph); + fetchData(graph); } }, []); + + const fetchData = (graph: Graph) => { + const { projectId } = parseQuery(); + queryGraph(projectId as string).then(res => { + const { data: dsl } = res; + graph.fromJSON(dsl); + }).catch(err => { + message.error(err.msg); + }); + }; + return (
diff --git a/packages/core/src/mods/flowChart/registerServerStorage.ts b/packages/core/src/mods/flowChart/registerServerStorage.ts new file mode 100644 index 0000000..63b080d --- /dev/null +++ b/packages/core/src/mods/flowChart/registerServerStorage.ts @@ -0,0 +1,96 @@ +import { Graph } from '@antv/x6'; +import merge from 'lodash.merge'; +import { parseQuery } from '../../utils'; +import { modifyGraph, ActionType, IModifyGraphAction } from '../../api'; + +const { projectId } = parseQuery(); +const memQueue: IModifyGraphAction[] = []; + +const validate = (type: string, data: any) => { + if (type === 'node') { + return true; + } else if (type === 'edge') { + const { source, target } = data; + return source.cell && target.cell; + } else { + return false; + } +} + +const enqueue = (type: string, actionType: ActionType, data: any) => { + + if (!validate(type, data)) { + return; + } + + const foundIndex = memQueue.findIndex(item => item.type === type && item.actionType === actionType); + if (foundIndex > -1) { + const deleted = memQueue.splice(foundIndex, 1)[0]; + data = merge(deleted, data); + } + memQueue.push({ type, actionType, data }); +}; + +let modifyActionTimer = -1; +const save = (flowChart: Graph, type: string, actionType: ActionType, data: any) => { + enqueue(type, actionType, data); + clearTimeout(modifyActionTimer); + modifyActionTimer = setTimeout(() => { + const pushedActions = memQueue.slice(0); + if(pushedActions.length > 0) { + flowChart.trigger('graph:change:modify'); + modifyGraph(projectId, memQueue).then(res => { + memQueue.splice(0, pushedActions.length); + flowChart.trigger('graph:modified', {success: true}); + }).catch(err => { + flowChart.trigger('graph:modified', {success: true, error: err}); + }); + } + }, 100); +}; + +type ActionEventMap = {[key: string]: string[]}; +const NODE_ACTION_EVENT_MAP: ActionEventMap = { + create: ['node:added'], + remove: ['node:removed'], + update: [ + 'node:moved', + 'node:resized', + 'node:rotated', + 'node:change:ports', + 'node:change:attrs', + 'node:change:data', + 'node:change:zIndex' + ] +}; + +const EDGE_ACTION_EVENT_MAP: ActionEventMap = { + create: ['edge:connected'], + remove: ['edge:removed'], + update: [ + 'edge:moved', + ] +}; + +export const registerServerStorage = (flowChart: Graph) => { + + Object.keys(NODE_ACTION_EVENT_MAP).forEach((actionType) => { + const events = NODE_ACTION_EVENT_MAP[actionType]; + events.forEach(event => { + flowChart.on(event, (args: any) => { + console.log('node event:', event, 'args:', args); + save(flowChart, 'node', actionType as ActionType, args.node.toJSON()); + }); + }); + }); + + Object.keys(EDGE_ACTION_EVENT_MAP).forEach((actionType) => { + const events = EDGE_ACTION_EVENT_MAP[actionType]; + events.forEach(event => { + flowChart.on(event, (args: any) => { + console.log('edge event:', event, 'args:', args); + save(flowChart, 'edge', actionType as ActionType, args.edge.toJSON()); + }); + }); + }); +}; diff --git a/packages/core/src/mods/toolBar/index.tsx b/packages/core/src/mods/toolBar/index.tsx index e374771..8b23dbd 100644 --- a/packages/core/src/mods/toolBar/index.tsx +++ b/packages/core/src/mods/toolBar/index.tsx @@ -5,6 +5,7 @@ import styles from './index.module.less'; import {Graph} from '@antv/x6'; import widgets from './widgets'; +import ModifyStatus from './widgets/modifyStatus'; interface IProps { flowChart: Graph; @@ -31,6 +32,7 @@ const ToolBar: React.FC = props => { })}
))} +
); }; diff --git a/packages/core/src/mods/toolBar/widgets/index.module.less b/packages/core/src/mods/toolBar/widgets/index.module.less index 4f1b086..5472385 100644 --- a/packages/core/src/mods/toolBar/widgets/index.module.less +++ b/packages/core/src/mods/toolBar/widgets/index.module.less @@ -50,6 +50,10 @@ } } +.modifyStatusContainer { + margin-left: 8px; +} + .bgColorContainer, .textColorContainer, .borderColorContainer { position: relative; diff --git a/packages/core/src/mods/toolBar/widgets/modifyStatus.tsx b/packages/core/src/mods/toolBar/widgets/modifyStatus.tsx new file mode 100644 index 0000000..f64e08e --- /dev/null +++ b/packages/core/src/mods/toolBar/widgets/modifyStatus.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useState } from 'react'; + +import styles from './index.module.less'; + +import { Graph } from '@antv/x6'; + +enum Status { + pending = 0, + syncing = 1, + successful = 2, + failed = 3 +} + +interface IProps { + flowChart: Graph +} + +const STATUS_TEXT_MAP = { + [Status.pending]: '', + [Status.syncing]: { + color: '#999', + text: '正在保存...' + }, + [Status.successful]: { + color: '#999', + text: '所有更改已保存' + }, + [Status.failed]: { + color: '#EC5B56', + text: '同步失败,进入离线模式' + } +}; + +const ModifyStatus: React.FC = (props) => { + + const {flowChart} = props; + const [status, setStatus] = useState(Status.pending); + + useEffect(() => { + flowChart.on('graph:change:modify', () => { + setStatus(Status.syncing); + }); + flowChart.on('graph:modified', (res: any) => { + const {success} = res; + if(success) { + setStatus(Status.successful); + } else { + setStatus(Status.failed); + } + }); + return () => { + flowChart.off('graph:change:modify'); + flowChart.off('graph:modified'); + }; + }, []); + + if(status === Status.pending) { + return null; + } else { + const {color, text} = STATUS_TEXT_MAP[status]; + return ( +
+ {text} +
+ ); + } +}; + +export default ModifyStatus; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index f69106d..3ef74f3 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,3 +1,5 @@ +import { parse } from 'query-string'; + export const safeParse = (json: string): Object => { try { return JSON.parse(json); @@ -24,3 +26,14 @@ export const safeGet = (obj: any, keyChain: string, defaultVal?: any): any => { return retVal; }; + +const PARSE_CONFIG = { + skipNull: true, + skipEmptyString: true, + parseNumbers: false, + parseBooleans: false, +}; + +export const parseQuery = (): {[key: string]: any} => { + return parse(location.search, PARSE_CONFIG); +};