diff --git a/example/mock/db.json b/example/mock/db.json index dc3743b..e22eac4 100644 --- a/example/mock/db.json +++ b/example/mock/db.json @@ -2,7 +2,7 @@ "default": [ { "position": { - "x": 450, + "x": 460, "y": 50 }, "size": { @@ -12,7 +12,7 @@ "shape": "imove-behavior", "data": { "label": "处理", - "configSchema": "{\n \n}", + "configSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"textarea_FyaArd\": {\n \"title\": \"Textarea\",\n \"type\": \"string\",\n \"format\": \"textarea\",\n \"description\": \"文本编辑框\"\n },\n \"select_jrGW8n\": {\n \"title\": \"Select\",\n \"type\": \"string\",\n \"enum\": [\n \"a\",\n \"b\",\n \"c\"\n ],\n \"enumNames\": [\n \"早\",\n \"中\",\n \"晚\"\n ],\n \"description\": \"下拉单选\"\n }\n },\n \"ui:displayType\": \"row\",\n \"ui:showDescIcon\": true\n },\n \"displayType\": \"row\",\n \"showDescIcon\": true\n}", "configData": {}, "dependencies": "{\n \n}", "code": "export default async function(ctx) {\n console.log('1212')\n}" @@ -102,7 +102,7 @@ "shape": "imove-start", "data": { "label": "开始", - "configSchema": "{\n \n}", + "configSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"input_1aWdPV\": {\n \"title\": \"Input\",\n \"type\": \"string\",\n \"description\": \"输入框\"\n },\n \"dateRangeSelect_mPNhq_\": {\n \"title\": \"DateRange\",\n \"type\": \"range\",\n \"format\": \"date\",\n \"description\": \"日期范围选择\"\n }\n },\n \"ui:displayType\": \"row\",\n \"ui:showDescIcon\": true\n },\n \"displayType\": \"row\",\n \"showDescIcon\": true\n}", "configData": {}, "trigger": "start", "dependencies": "{\n \n}", @@ -193,7 +193,7 @@ "shape": "imove-branch", "data": { "label": "判断", - "configSchema": "{\n \n}", + "configSchema": "{\"schema\":{\"type\":\"object\",\"properties\":{\"input_MudoMs\":{\"title\":\"Input\",\"type\":\"string\",\"description\":\"输入框\"}},\"ui:displayType\":\"row\",\"ui:showDescIcon\":true},\"displayType\":\"row\",\"showDescIcon\":true}", "configData": {}, "dependencies": "{\n \n}", "ports": { @@ -344,7 +344,7 @@ "shape": "imove-behavior", "data": { "label": "处理", - "configSchema": "{\n \n}", + "configSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"textarea_rdO5RK\": {\n \"title\": \"Textarea\",\n \"type\": \"string\",\n \"format\": \"textarea\",\n \"description\": \"文本编辑框\"\n }\n },\n \"ui:displayType\": \"row\",\n \"ui:showDescIcon\": true\n },\n \"displayType\": \"row\",\n \"showDescIcon\": true\n}", "configData": {}, "dependencies": "{\n \n}", "code": "export default async function(ctx) {\n console.log('test')\n}" @@ -443,6 +443,101 @@ } } ] + }, + { + "position": { + "x": 56, + "y": 380 + }, + "size": { + "width": 100, + "height": 40 + }, + "attrs": { + "label": { + "text": "在线运行测试" + } + }, + "shape": "imove-behavior", + "data": { + "label": "在线运行测试", + "configSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"input_CCw1sQ\": {\n \"title\": \"Input\",\n \"type\": \"string\",\n \"description\": \"输入框\"\n },\n \"allBoolean_fREQq-\": {\n \"title\": \"Switch\",\n \"type\": \"boolean\",\n \"ui:widget\": \"switch\",\n \"description\": \"开关控制\"\n }\n },\n \"ui:displayType\": \"row\",\n \"ui:showDescIcon\": true\n },\n \"displayType\": \"row\",\n \"showDescIcon\": true\n}", + "configData": {}, + "dependencies": "{\n \"lodash.get\": \"4.4.2\"\n}", + "code": "import fpget from 'lodash.get';\n\nexport default async function(ctx) {\n const obj = {a: {b: 'hello imove~'}};\n console.log(fpget(obj, 'a.b'));\n return ctx.getPipe();\n}" + }, + "ports": { + "groups": { + "top": { + "position": "top", + "attrs": { + "circle": { + "r": 3, + "magnet": true, + "stroke": "#666", + "strokeWidth": 1, + "fill": "#fff" + } + } + }, + "right": { + "position": "right", + "attrs": { + "circle": { + "r": 3, + "magnet": true, + "stroke": "#666", + "strokeWidth": 1, + "fill": "#fff" + } + } + }, + "bottom": { + "position": "bottom", + "attrs": { + "circle": { + "r": 3, + "magnet": true, + "stroke": "#666", + "strokeWidth": 1, + "fill": "#fff" + } + } + }, + "left": { + "position": "left", + "attrs": { + "circle": { + "r": 3, + "magnet": true, + "stroke": "#666", + "strokeWidth": 1, + "fill": "#fff" + } + } + } + }, + "items": [ + { + "id": "top", + "group": "top" + }, + { + "id": "right", + "group": "right" + }, + { + "id": "bottom", + "group": "bottom" + }, + { + "id": "left", + "group": "left" + } + ] + }, + "id": "1d174369-929b-4bd5-8841-f271399c4a4f", + "zIndex": 9 } ] } \ No newline at end of file diff --git a/packages/compile-code/src/compileForOnline.ts b/packages/compile-code/src/compileForOnline.ts new file mode 100644 index 0000000..ee9d5d9 --- /dev/null +++ b/packages/compile-code/src/compileForOnline.ts @@ -0,0 +1,119 @@ +import {Cell} from '@antv/x6'; +import makeCode from './template/runOnline'; +import simplifyDSL from './simplifyDSL'; + +interface DSL { + cells: Cell.Properties[]; +} + +/** + * Solution + * + * 1. find the source node form dsl, and if it is not imove-start, + * then insert a vitural imove-start at first. + * + * 2. transform node funciton, follows should be noted: + * - import statement should be replaced with import('packge/from/network') + * - export statement should be replace with return function + * - each node function should be wrapped within a new function to avoid duplicate declaration global variable + * + * 3. assemble Logic, Context, simplyfied dsl and nodeFns map into one file + * + */ + +const INSERT_DSL_COMMENT = '// define dsl here'; +const INSERT_NODE_FNS_COMMENT = '// define nodeFns here'; +const importRegex = /import\s+([\s\S]*?)\s+from\s+(?:('[@\.\/\-\w]+')|("[@\.\/\-\w]+"))\s*;?/mg; +const virtualSourceNode = { + id: 'virtual-imove-start', + shape: 'imove-start', + data: { + trigger: 'virtual-imove-start', + configData: {}, + code: 'export default async function(ctx) {\n \n}' + } +}; + +const findStartNode = (dsl: DSL): Cell.Properties => { + + const nodes = dsl.cells.filter(cell => cell.shape !== 'edge'); + const edges = dsl.cells.filter(cell => cell.shape === 'edge'); + + if(nodes.length === 0) { + throw new Error('Compile failed, no node is selected'); + } + + let foundEdge = null; + let startNode = nodes[0]; + while(foundEdge = edges.find(edge => edge.target.cell === startNode.id)) { + const newSourceId = foundEdge.source.cell; + startNode = nodes.find(node => node.id === newSourceId) as Cell.Properties; + } + + if(startNode.shape !== 'imove-start') { + dsl.cells.push( + virtualSourceNode, + { + shape: "edge", + source: { + cell: 'virtual-imove-start', + }, + target: { + cell: startNode.id + } + } + ); + startNode = virtualSourceNode; + } + + return startNode; +}; + +const getNextNode = (curNode: Cell.Properties, dsl: DSL) => { + + const nodes = dsl.cells.filter(cell => cell.shape !== 'edge'); + const edges = dsl.cells.filter(cell => cell.shape === 'edge'); + + const foundEdge = edges.find(edge => edge.source.cell === curNode.id); + if(foundEdge) { + return nodes.find(node => node.id === foundEdge.target.cell); + } +}; + +const compileSimplifiedDSL = (dsl: DSL): string => { + const simplyfiedDSL = JSON.stringify(simplifyDSL(dsl), null, 2); + return `const dsl = ${simplyfiedDSL};`; +}; + +const compileNodeFn = (node: Cell.Properties): string => { + const {data: {label, code}} = node; + const newCode = code.replace(importRegex, (match: string, p1: string, p2: string, p3: string) => { + const pkgName = (p2 || p3).replace(/('|")/g, ''); + return `const ${p1} = (await import('https://jspm.dev/${pkgName}')).default;`; + }).replace(/export\s+default/, 'return'); + + return `await (async function() { + ${newCode} + }())`; +}; + +const compileNodeFnsMap = (dsl: DSL): string => { + const nodes = dsl.cells.filter(cell => cell.shape !== 'edge'); + const kvs = nodes.map(node => { + const {id} = node; + return `'${id}': ${compileNodeFn(node)}`; + }); + + return `const nodeFns = {\n ${kvs.join(',\n ')}\n}`; +}; + +const compile = (dsl: DSL, mockInput: any): string => { + const startNode = findStartNode(dsl); + const mockNode = getNextNode(startNode, dsl); + return makeCode(mockNode, mockInput) + .replace(INSERT_DSL_COMMENT, compileSimplifiedDSL(dsl)) + .replace(INSERT_NODE_FNS_COMMENT, compileNodeFnsMap(dsl)) + .replace('$TRIGGER$', startNode.data.trigger); +}; + +export default compile; diff --git a/packages/compile-code/src/compileForProject.ts b/packages/compile-code/src/compileForProject.ts new file mode 100644 index 0000000..2d48d3a --- /dev/null +++ b/packages/compile-code/src/compileForProject.ts @@ -0,0 +1,34 @@ +import {Cell} from '@antv/x6'; +import addPlugins from './addPlugins'; +import simplifyDSL from './simplifyDSL'; +import extractNodeFns from './extractNodeFns'; +import logicTpl from './template/logic'; +import indexTpl from './template/index'; +import contextTpl from './template/context'; + +interface DSL { + cells: Cell.Properties[]; +} + +interface IOutput { + 'nodeFns': { + [fileName: string]: string + }; + 'context.js': string; + 'dsl.json': string; + 'index.js': string; + 'logic.js': string; +} + +const compile = (dsl: DSL, plugins = []): IOutput => { + const output: IOutput = { + 'nodeFns': extractNodeFns(dsl), + "context.js": contextTpl, + 'dsl.json': JSON.stringify(simplifyDSL(dsl), null, 2), + 'index.js': addPlugins(indexTpl, plugins), + 'logic.js': logicTpl + }; + return output; +}; + +export default compile; diff --git a/packages/compile-code/src/index.ts b/packages/compile-code/src/index.ts index 2d48d3a..70d3ddb 100644 --- a/packages/compile-code/src/index.ts +++ b/packages/compile-code/src/index.ts @@ -1,34 +1,7 @@ -import {Cell} from '@antv/x6'; -import addPlugins from './addPlugins'; -import simplifyDSL from './simplifyDSL'; -import extractNodeFns from './extractNodeFns'; -import logicTpl from './template/logic'; -import indexTpl from './template/index'; -import contextTpl from './template/context'; +import compileForOnline from './compileForOnline'; +import compileForProject from './compileForProject'; -interface DSL { - cells: Cell.Properties[]; -} - -interface IOutput { - 'nodeFns': { - [fileName: string]: string - }; - 'context.js': string; - 'dsl.json': string; - 'index.js': string; - 'logic.js': string; -} - -const compile = (dsl: DSL, plugins = []): IOutput => { - const output: IOutput = { - 'nodeFns': extractNodeFns(dsl), - "context.js": contextTpl, - 'dsl.json': JSON.stringify(simplifyDSL(dsl), null, 2), - 'index.js': addPlugins(indexTpl, plugins), - 'logic.js': logicTpl - }; - return output; +export { + compileForOnline, + compileForProject }; - -export default compile; diff --git a/packages/compile-code/src/template/context.ts b/packages/compile-code/src/template/context.ts index 962c8d0..7d41a35 100644 --- a/packages/compile-code/src/template/context.ts +++ b/packages/compile-code/src/template/context.ts @@ -6,7 +6,7 @@ export default `export default class Context { _init(opts = {}) { const { payload = {} } = opts; this.curNode = null; - this.context = Object.create(null); + this.context = {}; this.payload = Object.freeze({ ...payload }); } @@ -15,11 +15,6 @@ export default `export default class Context { this.lastRet = lastRet; } - prepare(opts = {}) { - this.payload = opts.payload; - this.context = {}; - } - getConfig() { return this.curNode.data.configData; } diff --git a/packages/compile-code/src/template/logic.ts b/packages/compile-code/src/template/logic.ts index 563f9ad..19cb51e 100644 --- a/packages/compile-code/src/template/logic.ts +++ b/packages/compile-code/src/template/logic.ts @@ -2,7 +2,7 @@ export default `import nodeFns from './nodeFns'; import Context from './context'; import EventEmitter from 'eventemitter3'; -const LIFECYCLE = new Set(['ctxCreated']); +const LIFECYCLE = new Set(['ctxCreated', 'enterNode', 'leaveNode']); const SHAPES = { START: 'imove-start', BRANCH: 'imove-branch', @@ -32,6 +32,11 @@ export default class Logic extends EventEmitter { return this.cells.filter((cell) => cell.shape === 'edge'); } + _getUnsafeCtx() { + // NOTE: don't use in prod + return this._unsafeCtx; + } + _runLifecycleEvent(eventName, ctx) { if (!LIFECYCLE.has(eventName)) { return console.warn(\`Lifecycle \${eventName} is not supported!\`); @@ -108,28 +113,32 @@ export default class Logic extends EventEmitter { } } - async _execNode(ctx, curNode, lastRet) { + async _execNode(ctx, curNode, lastRet, callback) { ctx._transitTo(curNode, lastRet); const fn = nodeFns[curNode.id]; + this._runLifecycleEvent('enterNode', ctx); const curRet = await fn(ctx); + this._runLifecycleEvent('leaveNode', ctx); if (curNode.shape !== SHAPES.BRANCH) { lastRet = curRet; } const nextNodes = this._getNextNodes(ctx, curNode, curRet); if (nextNodes.length > 0) { nextNodes.forEach(async (node) => { - await this._execNode(ctx, node, lastRet); + await this._execNode(ctx, node, lastRet, callback); }); + } else { + callback && callback(lastRet); } } - async invoke(trigger, data) { + async invoke(trigger, data, callback) { const curNode = this._getStartNode(trigger); if (!curNode) { return Promise.reject(new Error(\`Invoke failed! No logic-start named \${trigger} found!\`)); } - const ctx = this._createCtx({ payload: data }); - await this._execNode(ctx, curNode); + this._unsafeCtx = this._createCtx({ payload: data }); + await this._execNode(this._unsafeCtx, curNode, undefined, callback); } } `; diff --git a/packages/compile-code/src/template/runOnline.ts b/packages/compile-code/src/template/runOnline.ts new file mode 100644 index 0000000..7ba812f --- /dev/null +++ b/packages/compile-code/src/template/runOnline.ts @@ -0,0 +1,74 @@ +import logic from './logic'; +import context from './context'; + +const makeCode = (mockNode: any, mockInput: any) => ` +(async function run() { + // Context + ${context.replace(/export\s+default/, '')} + + // Logic + ${logic + .split('\n') + .filter(line => !line.match(/import nodeFns/) && !line.match(/import Context/)) + .join('\n') + .replace(/export\s+default/, '') + .replace( + `import EventEmitter from 'eventemitter3';`, + `const EventEmitter = (await import('https://jspm.dev/eventemitter3')).default;` + ) + } + + // DSL + // define dsl here + + // nodeFns map + // define nodeFns here + + // imove plugin + const mockPlugin = () => { + const mockNode = ${JSON.stringify(mockNode)}; + const mockInput = ${JSON.stringify(mockInput)}; + const toMockTargets = [ + ['pipe', 'getPipe'], + ['config', 'getConfig'], + ['payload', 'getPayload'], + ['context', 'getContext'], + ]; + return { + enterNode(ctx) { + // hijack + if(ctx.curNode.id === mockNode.id) { + toMockTargets.forEach(item => { + const [type, method] = item; + item[2] = ctx[method]; + ctx[method] = () => mockInput[type]; + }); + } + }, + leaveNode(ctx) { + // restore + if(ctx.curNode.id === mockNode.id) { + toMockTargets.forEach(item => { + const [type, method, originMethod] = item; + ctx[method] = originMethod; + }); + } + } + }; + }; + + // instantiation and invoke + const logic = new Logic({ dsl }); + logic.use(mockPlugin); + logic.invoke('$TRIGGER$', {}, (pipe) => { + const ctx = logic._getUnsafeCtx(); + const context = ctx.getContext(); + window.dispatchEvent(new CustomEvent('iMoveOnlineExecEnds', {detail: {pipe, context}})); + }); +})().catch(err => { + console.error(err.message); + window.dispatchEvent(new CustomEvent('iMoveOnlineExecEnds', {detail: {error: {message: err.message}}})); +}); +`; + +export default makeCode; diff --git a/packages/core/package.json b/packages/core/package.json index fe767d7..6fa4d13 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@imove/core", - "version": "0.3.5", + "version": "0.3.7", "description": "流程编排", "keywords": [ "imove", @@ -37,16 +37,19 @@ }, "dependencies": { "@ant-design/icons": "^4.0.6", - "@antv/x6": "^1.3.0", + "@antv/x6": "^1.7.12", "@imove/compile-code": "^0.0.1", "@monaco-editor/react": "^3.7.4", "antd": "^4.5.3", "axios": "^0.21.0", + "form-render": "^0.9.5", + "fr-generator": "^1.0.0", "jszip": "^3.5.0", "lodash.merge": "^4.6.2", "query-string": "^6.13.7", "react-color": "^2.18.1", - "react-json-view": "^1.19.1" + "react-json-view": "^1.19.1", + "react-split-pane": "^2.0.3" }, "devDependencies": { "@types/lodash.merge": "^4.6.6", diff --git a/packages/core/rollup.config.js b/packages/core/rollup.config.js index fa56f8f..911690d 100644 --- a/packages/core/rollup.config.js +++ b/packages/core/rollup.config.js @@ -1,5 +1,6 @@ import pkg from './package.json'; import postcss from 'rollup-plugin-postcss'; +import commonjs from '@rollup/plugin-commonjs'; import typescript from 'rollup-plugin-typescript'; import rollupBaseConfig from '../../rollup.config'; @@ -7,6 +8,7 @@ export default Object.assign(rollupBaseConfig, { plugins: [ postcss(), typescript(), + commonjs() ], output: [ { diff --git a/packages/core/src/common/baseCell/behavior.ts b/packages/core/src/common/baseCell/behavior.ts index 456ea34..489a1ec 100644 --- a/packages/core/src/common/baseCell/behavior.ts +++ b/packages/core/src/common/baseCell/behavior.ts @@ -13,6 +13,7 @@ const schema = { }, label: { fill: '#333', + textWrap: { width: '100%' }, }, }, ports: { diff --git a/packages/core/src/common/baseCell/branch.ts b/packages/core/src/common/baseCell/branch.ts index bed06ac..17680e1 100644 --- a/packages/core/src/common/baseCell/branch.ts +++ b/packages/core/src/common/baseCell/branch.ts @@ -20,6 +20,7 @@ const schema = { fontSize: 14, textAnchor: 'middle', textVerticalAnchor: 'middle', + textWrap: { width: '100%' }, }, }, ports: { diff --git a/packages/core/src/common/baseCell/start.ts b/packages/core/src/common/baseCell/start.ts index b9ae380..bd2aa0b 100644 --- a/packages/core/src/common/baseCell/start.ts +++ b/packages/core/src/common/baseCell/start.ts @@ -14,6 +14,7 @@ const schema = { }, label: { fill: '#FFF', + textWrap: { width: '100%' }, }, }, ports: { diff --git a/packages/core/src/common/shortcuts.ts b/packages/core/src/common/shortcuts.ts index 8e03c9b..ea250c0 100644 --- a/packages/core/src/common/shortcuts.ts +++ b/packages/core/src/common/shortcuts.ts @@ -1,7 +1,9 @@ import { safeGet } from '../utils'; import { localSave } from '../api'; +import { message } from 'antd'; import { Cell, Edge, Graph, Node } from '@antv/x6'; import { MIN_ZOOM, MAX_ZOOM, ZOOM_STEP } from './const'; +import { getSelectedNodes } from '../utils/flowChartUtils'; interface Shortcut { keys: string | string[]; @@ -52,6 +54,7 @@ const shortcuts: { [key: string]: Shortcut } = { const cells = flowChart.getSelectedCells(); if (cells.length > 0) { flowChart.copy(cells); + message.success('复制成功'); } return false; }, @@ -144,6 +147,18 @@ const shortcuts: { [key: string]: Shortcut } = { return false; }, }, + bringToTop: { + keys: 'meta + ]', + handler(flowChart: Graph) { + getSelectedNodes(flowChart).forEach((node) => node.toFront()); + } + }, + bringToBack: { + keys: 'meta + [', + handler(flowChart: Graph) { + getSelectedNodes(flowChart).forEach((node) => node.toBack()); + } + } }; export default shortcuts; diff --git a/packages/core/src/components/codeEditor/index.tsx b/packages/core/src/components/codeEditor/index.tsx index 03459ea..f0c56f4 100644 --- a/packages/core/src/components/codeEditor/index.tsx +++ b/packages/core/src/components/codeEditor/index.tsx @@ -1,14 +1,23 @@ -import React, {useMemo} from 'react'; +import React, { + useMemo, + useState, + useEffect +} from 'react'; import { monaco, + EditorDidMount, ControlledEditor, ControlledEditorProps } from '@monaco-editor/react'; import monokaiTheme from './theme-monokai'; +let KeyMod: any = {}; +let KeyCode: any = {}; monaco.init().then((monaco) => { + KeyMod = monaco.KeyMod; + KeyCode = monaco.KeyCode; monaco.editor.defineTheme('monokai', monokaiTheme); }); @@ -16,17 +25,38 @@ const CODE_EDITOR_OPTIONS = { fontSize: 14 }; -export const CodeEditor: React.FC = (props) => { - const {options, ...rest} = props; +interface IProps extends ControlledEditorProps { + onSave?: (code: string) => void; +} + +export const CodeEditor: React.FC = (props) => { + const {options, editorDidMount, onSave, ...rest} = props; + const [editorInst, setEditorInst] = useState(); + const editorOptions = useMemo(() => { return Object.assign({}, CODE_EDITOR_OPTIONS, options); }, [options]); + useEffect(() => { + if (editorInst) { + // NOTE: how to add command(https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandalonecodeeditor.html#addcommand) + editorInst.addCommand(KeyMod.CtrlCmd | KeyCode.KEY_S, () => { + onSave && onSave(editorInst.getValue()); + }); + } + }, [editorInst, onSave]); + + const onEditorDidMount: EditorDidMount = (getEditorValue, editor) => { + setEditorInst(editor); + editorDidMount && editorDidMount(getEditorValue, editor); + }; + return ( ); diff --git a/packages/core/src/components/codeRun/index.module.less b/packages/core/src/components/codeRun/index.module.less new file mode 100644 index 0000000..6b489be --- /dev/null +++ b/packages/core/src/components/codeRun/index.module.less @@ -0,0 +1,62 @@ +.container { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + + .pane { + position: relative; + display: flex; + flex: 1; + + .runWrapper { + position: absolute; + top: 0; + right: 0; + } + } +} + +.card { + display: flex; + flex-direction: column; + flex: 1; + background-color: #ffffff; + + .cardTitleText { + margin: 10px 24px 0; + font-size: 16px; + font-weight: 500; + color: #000000; + } + + .cardBody { + padding: 10px 24px; + height: 100%; + overflow: scroll; + } +} + +:global { + .ant-tabs { + height: 100%; + } + + .ant-tabs-content-holder { + height: 100%; + } + + .ant-tabs-content { + height: 100%; + overflow: scroll; + } + + .sc-bdVaJa.cjjWdp { + opacity: 1; + background-color: #efefef; + + &:hover { + border-color: transparent; + } + } +} diff --git a/packages/core/src/components/codeRun/index.tsx b/packages/core/src/components/codeRun/index.tsx new file mode 100644 index 0000000..a03d9d8 --- /dev/null +++ b/packages/core/src/components/codeRun/index.tsx @@ -0,0 +1,129 @@ +import React, { + useState, + useEffect, + useCallback, +} from 'react'; + +import styles from './index.module.less'; + +import { Button } from 'antd'; +import { Graph } from '@antv/x6'; +import Console from '../console'; +import InputPanel from './inputPanel'; +import JsonView from 'react-json-view'; +import { executeScript } from '../../utils'; +import { PlayCircleFilled, LoadingOutlined } from '@ant-design/icons'; +import { compileForOnline } from '@imove/compile-code'; +import { toSelectedCellsJSON } from '../../utils/flowChartUtils'; + +// FIXME: https://github.com/tomkp/react-split-pane/issues/541 +// @ts-ignore +import SplitPane from 'react-split-pane/lib/SplitPane'; +// @ts-ignore +import Pane from 'react-split-pane/lib/Pane'; + +const defaultInput = { + pipe: {}, + context: {}, + payload: {}, + config: {} +}; + +interface ICardProps { + title: string; +} + +const Card: React.FC = (props) => { + const { title } = props; + + return ( +
+
{title}
+
+ {props.children} +
+
+ ); +}; + +interface ICodeRunProps { + flowChart: Graph; +} + +const CodeRun: React.FC = (props) => { + + const {flowChart} = props; + const [isRunning, setIsRunning] = useState(false); + const [input, setInput] = useState(defaultInput); + const [output, setOutput] = useState({}); + + useEffect(() => { + // NOTE: listen the event that iMove online exec ends + const handler = (data: any) => { + setIsRunning(false); + setOutput(data.detail || {}); + }; + window.addEventListener('iMoveOnlineExecEnds', handler); + return () => { + window.removeEventListener('iMoveOnlineExecEnds', handler); + }; + }, []); + + const onClickRun = useCallback(() => { + setIsRunning(true); + const selectedCelssJson = toSelectedCellsJSON(flowChart); + const compiledCode = compileForOnline(selectedCelssJson, input); + executeScript(compiledCode); + }, [flowChart, input]); + + const onChangeInput = useCallback((val: any) => { + setInput(val); + }, []); + + return ( +
+ + + + +
+ {isRunning ? ( + + ) : ( + + )} +
+ + + +
+ + + + + +
+
+ + + +
+
+ ); +} + +export default CodeRun; diff --git a/packages/core/src/components/codeRun/inputPanel/index.module.less b/packages/core/src/components/codeRun/inputPanel/index.module.less new file mode 100644 index 0000000..2469859 --- /dev/null +++ b/packages/core/src/components/codeRun/inputPanel/index.module.less @@ -0,0 +1,24 @@ +.itemHeader { + margin-bottom: 8px; + + .itemTitleText { + font-size: 15px; + font-weight: 500; + } + + .itemDescText { + margin-left: 5px; + font-size: 12px; + color: #999; + } +} + +:global { + .ant-tabs-nav { + margin-bottom: 5px; + } + + .ant-form-item { + margin-bottom: 8px; + } +} diff --git a/packages/core/src/components/codeRun/inputPanel/index.tsx b/packages/core/src/components/codeRun/inputPanel/index.tsx new file mode 100644 index 0000000..3aa47cd --- /dev/null +++ b/packages/core/src/components/codeRun/inputPanel/index.tsx @@ -0,0 +1,166 @@ +import React, { + useRef, + useEffect, +} from 'react'; + +import styles from './index.module.less'; + +import CodeEditor from '../../codeEditor'; +import { Tabs, Form, Input, Button, Space } from 'antd'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; + +const inputItems = [ + { type: 'pipe', desc: '上一节点返回值' }, + { type: 'context', desc: '当前逻辑流程公用数据' }, + { type: 'payload', desc: '当前逻辑流程通用数据' }, + { type: 'config', desc: '当前节点投放配置' }, +]; + +interface IVisualFormItemProps { + type: string; + desc: string; +} + +const VisualFormItem: React.FC = (props) => { + const { type, desc } = props; + + return ( + +
+ {type} + {desc} +
+ + {(fields, { add, remove }) => ( + + {fields.map(field => ( + + + + + + + + remove(field.name)} /> + + ))} + + + + + )} + +
+ ); +}; + +interface IVisualPanelProps { + data: any; + onChange: (value: object) => void +} + +const VisualPanel: React.FC = (props) => { + const { data, onChange } = props; + const [form] = Form.useForm(); + + useEffect(() => { + const filedsValue: { [key: string]: any } = {}; + for (const { type } of inputItems) { + const mockValue = data[type] || {}; + filedsValue[type] = Object.keys(mockValue).map(key => ({ key, value: mockValue[key] })); + } + form.setFieldsValue(filedsValue); + }, [data]); + + const onFormChange = () => { + const input: { [key: string]: any } = {}; + const filedsValue = form.getFieldsValue(); + for (const { type } of inputItems) { + const mockValue = filedsValue[type] || []; + input[type] = mockValue.reduce((prev: any, cur: { key: string, value: any }) => { + const { key, value } = cur; + prev[key] = value; + return prev; + }, {}); + } + onChange(input); + }; + + return ( +
+
+ {inputItems.map(({ type, desc }, index) => ( + + ))} + +
+ ); +}; + +interface IInputPanelProps { + data: any; + onChange: (data: any) => void; +} + +const InputPanel: React.FC = (props) => { + const cache: any = useRef({}); + const { data, onChange } = props; + + const onVisualChange = (newForm: any) => { + clearTimeout(cache.current.timer); + cache.current.timer = setTimeout(() => onChange(newForm), 1000); + }; + const onEditorChange = (ev: any, newCode: string | undefined = '') => { + clearTimeout(cache.current.timer); + cache.current.timer = setTimeout(() => { + try { + onChange(JSON.parse(newCode)); + } catch (err) { + console.log(err); + } + }, 1000); + }; + + return ( + + + + + + + + + ); +}; + +export default InputPanel; diff --git a/packages/core/src/components/console/index.module.less b/packages/core/src/components/console/index.module.less new file mode 100644 index 0000000..d225106 --- /dev/null +++ b/packages/core/src/components/console/index.module.less @@ -0,0 +1,52 @@ +.container { + width: 100%; + height: 100%; + overflow: scroll; + + :global { + .ant-tabs-nav { + margin-bottom: 0; + } + } + + .logPanel { + height: 100%; + background: #272823; + + .logLine { + display: flex; + flex-direction: row; + position: relative; + margin-top: -1px; + padding-left: 10px; + font-size: 13px; + border: 1px solid transparent; + + &:first-child { + margin-top: 0; + } + + .logIcon { + padding-top: 0.6rem; + width: 1rem; + height: 1rem; + } + + .logText { + margin-left: 8px; + min-height: 18px; + padding: 0.4rem 1.5rem 0.4rem 0px; + } + } + } + + .toolBar { + display: flex; + flex-direction: row; + + .levels { + margin-left: 4px; + width: 150px; + } + } +} \ No newline at end of file diff --git a/packages/core/src/components/console/index.tsx b/packages/core/src/components/console/index.tsx new file mode 100644 index 0000000..006192e --- /dev/null +++ b/packages/core/src/components/console/index.tsx @@ -0,0 +1,202 @@ +import React, { + useRef, + useState, + useEffect, + useCallback, + ChangeEvent +} from 'react'; + +import styles from './index.module.less'; + +import { + InfoCircleOutlined, + WarningOutlined, + BugOutlined, + CloseCircleOutlined, + ClearOutlined, + FilterOutlined +} from '@ant-design/icons'; +import { Button, Input, Select, Tabs } from 'antd'; + +interface ILog { + type: string; + data: any[]; + strVal: string; +} + +const Helper = { + isPlainObject(value: any): boolean { + return typeof value === 'object' && value !== null; + }, + getArgsToString(args: any[]): string { + return args.map(o => { + if (Helper.isPlainObject(o)) { + return JSON.stringify(o); + } else { + return o; + } + }).join(' '); + } +} + +const hijackMap: { [key: string]: any } = { + log: { + bgColor: '#272823', + textColor: '#ffffff', + borderColor: 'rgba(128, 128, 128, 0.35)', + icon:
, + originMethod: console.log + }, + info: { + bgColor: '#272823', + textColor: '#ffffff', + borderColor: 'rgba(128, 128, 128, 0.35)', + icon: , + originMethod: console.info + }, + warn: { + bgColor: 'rgb(51, 42, 0)', + textColor: 'rgb(245, 211, 150)', + borderColor: 'rgb(102, 85, 0)', + icon: , + originMethod: console.warn + }, + debug: { + bgColor: '#272823', + textColor: 'rgb(77, 136, 255)', + borderColor: 'rgba(128, 128, 128, 0.35)', + icon: , + originMethod: console.debug + }, + error: { + bgColor: 'rgb(40, 0, 0)', + textColor: 'rgb(254, 127, 127)', + borderColor: 'rgb(91, 0, 0)', + icon: , + originMethod: console.error + } +} + +const MyConsole: React.FC = () => { + const [filter, setFilter] = useState(''); + const [level, setLevel] = useState('all'); + const [logList, setLogList] = useState([]); + const cache = useRef<{ allLogs: ILog[] }>({ allLogs: [] }); + + useEffect(() => { + hijackConsole(); + return () => { + resetConsole(); + }; + }, []); + + useEffect(() => { + const filteredLogs = cache.current.allLogs + .filter(log => { + if (level === 'all') { + return true; + } else { + return log.type === level; + } + }) + .filter(log => { + return log.strVal.indexOf(filter) > -1; + }); + setLogList(filteredLogs); + }, [filter, level]); + + const hijackConsole = () => { + Object.keys(hijackMap).forEach(method => { + // @ts-ignore + window.console[method] = (...args: any[]) => { + hijackMap[method].originMethod(...args); + cache.current.allLogs = cache.current.allLogs.concat({ + type: method, + data: args, + strVal: Helper.getArgsToString(args) + }); + setLogList(cache.current.allLogs); + }; + }); + } + + const resetConsole = () => { + Object.keys(hijackMap).forEach(method => { + // @ts-ignore + window.console[method] = hijackMap[method].originMethod; + }); + }; + + const onChangeFilter = useCallback((evt: ChangeEvent) => { + setFilter(evt.target.value); + }, []); + + const onChangeLevel = useCallback((level: string) => { + setLevel(level); + }, []); + + const onClickClear = useCallback(() => { + setLogList([]); + cache.current.allLogs = []; + }, []); + + const renderToolBar = () => { + return ( +
+ } + onChange={onChangeFilter} + /> + + +
+ ); + }; + + const renderLogPanel = (logList: ILog[]) => { + return ( +
+ {logList.map((log: ILog, index: number) => { + const { icon, textColor, bgColor, borderColor } = hijackMap[log.type]; + const logLineStyle = { + color: textColor, + backgroundColor: bgColor, + borderTopColor: borderColor, + borderBottomColor: borderColor + }; + // TODO: use react-json-view to render object + return ( +
+ {icon} +
+ {log.strVal} +
+
+ ); + })} +
+ ); + }; + + return ( +
+ + + {renderLogPanel(logList)} + + +
+ ); +}; + +export default MyConsole; diff --git a/packages/core/src/components/schemaForm/index.module.less b/packages/core/src/components/schemaForm/index.module.less new file mode 100644 index 0000000..7d7fdc1 --- /dev/null +++ b/packages/core/src/components/schemaForm/index.module.less @@ -0,0 +1,9 @@ +.btnWrap{ + width:100%; + display:flex; + flex-direction: row; + justify-content:flex-end; + .btn{ + margin-top: 20px; + } +} diff --git a/packages/core/src/components/schemaForm/index.tsx b/packages/core/src/components/schemaForm/index.tsx new file mode 100644 index 0000000..3e35240 --- /dev/null +++ b/packages/core/src/components/schemaForm/index.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { Form, Input, Switch, Select, Checkbox, DatePicker, TimePicker, Empty, Button, message } from 'antd'; +import moment from 'moment'; +import styles from './index.module.less' +const FormItem = Form.Item +const { RangePicker } = DatePicker; +const { Option } = Select; +interface INum { + label: string, + value: string +} + +interface IConfigData { + [key: string]: any +} +interface IProps { + schema: { + type: string, + properties: { [key: string]: any } + }, + configData: IConfigData, + changeConfigData: (data: IConfigData) => void +} + +const formLayout = { + labelCol: { span: 8 }, + wrapperCol: { span: 16 } +}; + +const SchemaForm: React.FC = (props) => { + const { schema, configData, changeConfigData } = props + const [form] = Form.useForm() + const onClickSave = (): void => { + changeConfigData(form.getFieldsValue()) + message.success('保存成功!') + } + + return ( + schema && Object.keys(schema).length > 0 ? +
+ {schema?.properties && Object.keys(schema.properties).map((key: string) => { + const obj = schema.properties[key] + const options: INum[] = obj.enum ? obj.enum.map((item: string, idx: number) => { + return { label: item, value: obj.enumNames[idx] } + }) : [] + let ele = null + // 输入框 + if (obj.type === 'string' && !obj.hasOwnProperty('format') && !obj.hasOwnProperty('enum')) { + ele = + } + // 编辑框 + if (obj.type === 'string' && obj.format === 'textarea') { + ele = + } + // switch + if (obj.type === 'boolean' && obj['ui:widget'] === 'switch') { + ele = + } + // 下拉单选 + if (obj.type === 'string' && obj.hasOwnProperty('enum') && !obj.hasOwnProperty('ui:widget')) { + ele = + } + // 下拉多选 + if (obj.type === 'array' && obj.hasOwnProperty('enum') && obj['ui:widget'] === 'multiSelect') { + ele = + } + // 点击多选 + if (obj.type === 'array' && obj.hasOwnProperty('enum') && !obj.hasOwnProperty('ui:widget')) { + ele = + } + // 时间选择 + if (obj.type === 'string' && obj.format === 'time') { + ele = + } + // 日期范围 + if (obj.type === 'range' && obj.format === 'date') { + ele = + } + // 日期选择 + if (obj.type === 'string' && obj.format === 'date') { + ele = + } + return + {ele} + + })} +
+ +
+ : + + ); +}; + +export default SchemaForm diff --git a/packages/core/src/components/xIcon/index.less b/packages/core/src/components/xIcon/index.less index 68118fc..9ed32cb 100644 --- a/packages/core/src/components/xIcon/index.less +++ b/packages/core/src/components/xIcon/index.less @@ -1,3 +1,4 @@ +#icon-select-all, #icon-align-top, #icon-align-left, #icon-align-vertical-center, diff --git a/packages/core/src/components/xIcon/index.tsx b/packages/core/src/components/xIcon/index.tsx index f06bb3d..2a3cc83 100644 --- a/packages/core/src/components/xIcon/index.tsx +++ b/packages/core/src/components/xIcon/index.tsx @@ -8,7 +8,7 @@ interface IconFontProps { } const XIcon: React.SFC = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/font_2024452_zgg5gvivois.js', + scriptUrl: '//at.alicdn.com/t/font_2024452_r7nz0cw949.js', }); export default XIcon; diff --git a/packages/core/src/global.index.less b/packages/core/src/global.index.less index 930e48c..8e8a4f7 100644 --- a/packages/core/src/global.index.less +++ b/packages/core/src/global.index.less @@ -1,3 +1,11 @@ +.react-json-view { + .collapsed-icon, .expanded-icon { + svg { + vertical-align: middle !important; + } + } +} + .x6-node { .x6-port-body { diff --git a/packages/core/src/hooks/useClickAway.ts b/packages/core/src/hooks/useClickAway.ts new file mode 100644 index 0000000..d1a3366 --- /dev/null +++ b/packages/core/src/hooks/useClickAway.ts @@ -0,0 +1,65 @@ +import { + useRef, + useEffect, + MutableRefObject +} from 'react'; + +type EventType = MouseEvent | TouchEvent; +type TargetElement = HTMLElement | Element | Document | Window; +type BasicTarget = + | (() => T | null) + | T + | null + | MutableRefObject; + +const defaultEvent = 'click'; + +const getTargetElement = ( + target: BasicTarget, + defaultElement?: TargetElement, +): TargetElement | undefined | null => { + if (!target) { + return defaultElement; + } + + let targetElement: TargetElement | undefined | null; + if (typeof target === 'function') { + targetElement = target(); + } else if ('current' in target) { + targetElement = target.current; + } else { + targetElement = target; + } + + return targetElement; +}; + +const useClickAway = ( + onClickAway: (event: EventType) => void, + target: BasicTarget | BasicTarget[], + eventName: string = defaultEvent, +): void => { + const onClickAwayRef = useRef(onClickAway); + onClickAwayRef.current = onClickAway; + useEffect(() => { + const handler = (event: any) => { + const targets = Array.isArray(target) ? target : [target]; + if ( + targets.some((targetItem) => { + const targetElement = getTargetElement(targetItem) as HTMLElement; + return !targetElement || targetElement?.contains(event.target); + }) + ) { + return; + } + onClickAwayRef.current(event); + }; + document.addEventListener(eventName, handler); + + return () => { + document.removeEventListener(eventName, handler); + }; + }, [target, eventName]); +}; + +export default useClickAway; diff --git a/packages/core/src/mods/flowChart/codeEditorModel/index.module.less b/packages/core/src/mods/flowChart/codeEditorModel/index.module.less new file mode 100644 index 0000000..5d29363 --- /dev/null +++ b/packages/core/src/mods/flowChart/codeEditorModel/index.module.less @@ -0,0 +1,11 @@ +.depsInfoModalContent { + padding-top: 20px; +} + +.modal { + :global { + .ant-modal-body { + padding: 0; + } + } +} diff --git a/packages/core/src/mods/flowChart/codeEditorModel/index.tsx b/packages/core/src/mods/flowChart/codeEditorModel/index.tsx new file mode 100644 index 0000000..b82fb55 --- /dev/null +++ b/packages/core/src/mods/flowChart/codeEditorModel/index.tsx @@ -0,0 +1,129 @@ +import React, { + useState, + useEffect +} from 'react'; + +import 'antd/es/modal/style'; +import styles from './index.module.less'; + +import { Graph } from '@antv/x6'; +import { Button, Modal, message } from 'antd'; +import JsonView from 'react-json-view'; +import { safeParse } from '../../../utils'; +import analyzeDeps from '../../../utils/analyzeDeps'; +import CodeEditor from '../../../components/codeEditor'; + +interface IProps { + title?: string; + flowChart: Graph; +} + +const CodeEditModal: React.FC = (props) => { + + const { title = '编辑代码', flowChart } = props; + const [code, setCode] = useState(''); + const [visible, setVisible] = useState(false); + + const updateNodeCode = (code: string): void => { + + const cell = flowChart.getSelectedCells()[0]; + const { code: oldCode, dependencies } = cell.getData(); + if(code === oldCode) { + return; + } + + cell.setData({ code }); + message.success('代码保存成功', 1); + const excludeDeps = safeParse(dependencies); + analyzeDeps(code, Object.keys(excludeDeps)).then((deps): void => { + if (Object.keys(deps).length > 0) { + Modal.info({ + title: '检测到您的代码有新依赖,已为您自动更新', + content: ( +
+ +
+ ), + onOk() { + const newDeps = { ...excludeDeps, ...deps }; + cell.setData({ + code, + dependencies: JSON.stringify(newDeps, null, 2) + }); + // NOTE: notify basic panel to update dependency + flowChart.trigger('settingBar.basicPanel:forceUpdate'); + }, + }); + } + }); + }; + + // life + useEffect(() => { + const handler = () => setVisible(true); + flowChart.on('graph:editCode', handler); + return () => { + flowChart.off('graph:editCode', handler); + }; + }, []); + useEffect(() => { + if (visible) { + const cell = flowChart.getSelectedCells()[0]; + const { code } = cell.getData() || {}; + setCode(code); + } else { + setCode(''); + } + }, [visible]); + + // events + const onOk = (): void => { + setVisible(false); + updateNodeCode(code); + }; + const onCancel = (): void => { + setVisible(false); + }; + const onRunCode = (): void => { + updateNodeCode(code); + flowChart.trigger('graph:runCode'); + }; + const onChangeCode = (ev: any, newCode: string | undefined = ''): void => { + setCode(newCode); + }; + const onSaveCode = (newCode: string) => { + updateNodeCode(newCode); + }; + + return ( + 取消, + , + , + ]} + > + + + ); +}; + +export default CodeEditModal; diff --git a/packages/core/src/mods/flowChart/codeRunModal/index.module.less b/packages/core/src/mods/flowChart/codeRunModal/index.module.less new file mode 100644 index 0000000..43440f3 --- /dev/null +++ b/packages/core/src/mods/flowChart/codeRunModal/index.module.less @@ -0,0 +1,8 @@ +.modal { + :global { + .ant-modal-body { + padding: 0; + height: 653px; + } + } +} \ No newline at end of file diff --git a/packages/core/src/mods/flowChart/codeRunModal/index.tsx b/packages/core/src/mods/flowChart/codeRunModal/index.tsx new file mode 100644 index 0000000..2bc7e26 --- /dev/null +++ b/packages/core/src/mods/flowChart/codeRunModal/index.tsx @@ -0,0 +1,50 @@ +import React, { + useState, + useEffect, + useCallback +} from 'react'; + +import styles from './index.module.less'; + +import { Modal } from 'antd'; +import { Graph } from '@antv/x6'; +import CodeRun from '../../../components/codeRun'; + +interface IEditModalProps { + title?: string; + flowChart: Graph; +} + +const CodeRunModal: React.FC = (props): JSX.Element => { + + const { title = '执行代码', flowChart } = props; + const [visible, setVisible] = useState(false); + + useEffect(() => { + const handler = () => setVisible(true); + flowChart.on('graph:runCode', handler); + return () => { + flowChart.off('graph:runCode', handler); + }; + }, [flowChart]); + + // events + const onClose = useCallback((): void => { + setVisible(false); + }, []); + + return ( + + + + ); +}; + +export default CodeRunModal; diff --git a/packages/core/src/mods/flowChart/contextMenu/index.tsx b/packages/core/src/mods/flowChart/contextMenu/index.tsx new file mode 100644 index 0000000..dc22fb3 --- /dev/null +++ b/packages/core/src/mods/flowChart/contextMenu/index.tsx @@ -0,0 +1,115 @@ +import React, { + useRef, + useCallback +} from 'react'; + +import styles from '../index.module.less'; + +import { Menu } from 'antd'; +import { Graph } from '@antv/x6'; +import useClickAway from '../../../hooks/useClickAway'; +import { nodeMenuConfig, blankMenuConfig } from './menuConfig'; + +interface IProps { + x: number; + y: number; + scene: string; + visible: boolean; + flowChart: Graph; +} + +interface IMenuConfig { + key: string; + title: string; + icon?: React.ReactElement; + children?: IMenuConfig[]; + showDividerBehind?: boolean; + disabled?: boolean | ((flowChart: Graph) => boolean); + handler: (flowChart: Graph) => void; +} + +const menuConfigMap: { [scene: string]: IMenuConfig[] } = { + node: nodeMenuConfig, + blank: blankMenuConfig +}; + +const FlowChartContextMenu: React.FC = props => { + const menuRef = useRef(null); + const { x, y, scene, visible, flowChart } = props; + const menuConfig = menuConfigMap[scene]; + + useClickAway(() => onClickAway(), menuRef); + + const onClickAway = useCallback(() => flowChart.trigger('graph:hideContextMenu'), [flowChart]); + const onClickMenu = useCallback(({ key }) => { + const handlerMap = Helper.makeMenuHandlerMap(menuConfig); + const handler = handlerMap[key]; + if (handler) { + onClickAway(); + handler(flowChart); + } + }, [flowChart, menuConfig]); + + return !visible ? null : ( +
+ + {Helper.makeMenuContent(flowChart, menuConfig)} + +
+ ); +}; + +const Helper = { + makeMenuHandlerMap(config: IMenuConfig[]) { + const queue = config.slice(0); + const handlerMap: { [key: string]: (flowChart: Graph) => void } = {}; + while (queue.length > 0) { + const { key, handler, children } = queue.pop() as IMenuConfig; + if (children && children.length > 0) { + queue.push(...children); + } else { + handlerMap[key] = handler; + } + } + return handlerMap; + }, + makeMenuContent(flowChart: Graph, menuConfig: IMenuConfig[]) { + const loop = (config: IMenuConfig[]) => { + return config.map(item => { + let content = null; + let { key, title, icon, children, disabled = false, showDividerBehind } = item; + if (typeof disabled === 'function') { + disabled = disabled(flowChart); + } + if (children && children.length > 0) { + content = ( + + {loop(children)} + + ); + } else { + content = ( + + {title} + + ); + } + return [ + content, + showDividerBehind && + ]; + }); + }; + return loop(menuConfig); + } +}; + +export default FlowChartContextMenu; diff --git a/packages/core/src/mods/flowChart/contextMenu/menuConfig/blank.tsx b/packages/core/src/mods/flowChart/contextMenu/menuConfig/blank.tsx new file mode 100644 index 0000000..3a3eea8 --- /dev/null +++ b/packages/core/src/mods/flowChart/contextMenu/menuConfig/blank.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { Graph } from '@antv/x6'; +import XIcon from '../../../../components/xIcon'; +import shortcuts from '../../../../common/shortcuts'; +import { SnippetsOutlined } from '@ant-design/icons'; + +const blankMenuConfig = [ + { + key: 'selectAll', + title: '全选', + icon: , + handler: shortcuts.selectAll.handler + }, + { + key: 'paste', + title: '粘贴', + icon: , + disabled: (flowChart: Graph) => flowChart.isClipboardEmpty(), + handler: shortcuts.paste.handler + } +]; + +export default blankMenuConfig; diff --git a/packages/core/src/mods/flowChart/contextMenu/menuConfig/index.ts b/packages/core/src/mods/flowChart/contextMenu/menuConfig/index.ts new file mode 100644 index 0000000..e32bd3d --- /dev/null +++ b/packages/core/src/mods/flowChart/contextMenu/menuConfig/index.ts @@ -0,0 +1,7 @@ +import nodeMenuConfig from './node'; +import blankMenuConfig from './blank'; + +export { + nodeMenuConfig, + blankMenuConfig +}; diff --git a/packages/core/src/mods/flowChart/contextMenu/menuConfig/node.tsx b/packages/core/src/mods/flowChart/contextMenu/menuConfig/node.tsx new file mode 100644 index 0000000..75f39a5 --- /dev/null +++ b/packages/core/src/mods/flowChart/contextMenu/menuConfig/node.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import { + CopyOutlined, + EditOutlined, + CodeOutlined, + FormOutlined, + DeleteOutlined, +} from '@ant-design/icons'; + +import { Graph } from '@antv/x6'; +import XIcon from '../../../../components/xIcon'; +import shortcuts from '../../../../common/shortcuts'; +import { getSelectedNodes } from '../../../../utils/flowChartUtils'; + +const nodeMenuConfig = [ + { + key: 'copy', + title: '复制', + icon: , + handler: shortcuts.copy.handler + }, + { + key: 'delete', + title: '删除', + icon: , + handler: shortcuts.delete.handler + }, + { + key: 'rename', + title: '编辑文本', + icon: , + showDividerBehind: true, + handler() { + // TODO + } + }, + { + key: 'bringToTop', + title: '置于顶层', + icon: , + handler: shortcuts.bringToTop.handler + }, + { + key: 'bringToBack', + title: '置于底层', + icon: , + showDividerBehind: true, + handler: shortcuts.bringToBack.handler + }, + { + key: 'editCode', + title: '编辑代码', + icon: , + disabled(flowChart: Graph) { + return getSelectedNodes(flowChart).length !== 1; + }, + handler(flowChart: Graph) { + flowChart.trigger('graph:editCode'); + } + }, + { + key: 'executeCode', + title: '执行代码', + icon: , + disabled(flowChart: Graph) { + return getSelectedNodes(flowChart).length !== 1; + }, + handler(flowChart: Graph) { + flowChart.trigger('graph:runCode'); + } + } +]; + +export default nodeMenuConfig; diff --git a/packages/core/src/mods/flowChart/createFlowChart.ts b/packages/core/src/mods/flowChart/createFlowChart.ts index 0680058..1f8cd62 100644 --- a/packages/core/src/mods/flowChart/createFlowChart.ts +++ b/packages/core/src/mods/flowChart/createFlowChart.ts @@ -55,6 +55,23 @@ const registerEvents = (flowChart: Graph): void => { args.edge.attr('line/strokeWidth', '2px'); } }); + flowChart.on('node:dblclick', () => { + flowChart.trigger('graph:editCode'); + }); + flowChart.on('blank:contextmenu', (args) => { + const { e: { clientX, clientY } } = args; + flowChart.cleanSelection(); + flowChart.trigger('graph:showContextMenu', { x: clientX, y: clientY, scene: 'blank' }); + }); + flowChart.on('node:contextmenu', (args) => { + const { e: { clientX, clientY }, node } = args; + // NOTE: if the clicked node is not in the selected nodes, then clear selection + if(!flowChart.getSelectedCells().includes(node)) { + flowChart.cleanSelection(); + flowChart.select(node); + } + flowChart.trigger('graph:showContextMenu', { x: clientX, y: clientY, scene: 'node' }); + }); }; const registerShortcuts = (flowChart: Graph): void => { @@ -108,6 +125,7 @@ const createFlowChart = (container: HTMLDivElement, miniMapContainer: HTMLDivEle multiple: true, rubberband: true, movable: true, + strict: true, showNodeSelectionBox: true }, // https://x6.antv.vision/zh/docs/tutorial/basic/snapline diff --git a/packages/core/src/mods/flowChart/index.module.less b/packages/core/src/mods/flowChart/index.module.less index fff309d..8447223 100644 --- a/packages/core/src/mods/flowChart/index.module.less +++ b/packages/core/src/mods/flowChart/index.module.less @@ -22,4 +22,11 @@ } } } + + .contextMenu { + z-index: 1003; + position: fixed; + min-width: 200px; + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05) !important; + } } \ No newline at end of file diff --git a/packages/core/src/mods/flowChart/index.tsx b/packages/core/src/mods/flowChart/index.tsx index e52751c..f534bfe 100644 --- a/packages/core/src/mods/flowChart/index.tsx +++ b/packages/core/src/mods/flowChart/index.tsx @@ -1,36 +1,99 @@ -import React, { useRef, useEffect } from 'react'; +import React, { + useRef, + useState, + useEffect +} from 'react'; import styles from './index.module.less'; -import { message } from 'antd'; import { Graph } from '@antv/x6'; import { queryGraph } from '../../api'; import { parseQuery } from '../../utils'; import createFlowChart from './createFlowChart'; +import CodeRunModal from './codeRunModal'; +import CodeEditorModal from './codeEditorModel'; +import FlowChartContextMenu from './contextMenu'; interface IProps { onReady: (graph: Graph) => void; } +interface IMenuInfo { + x: number; + y: number; + scene: string; + visible: boolean; +} + +const defaultMenuInfo = { + x: 0, + y: 0, + scene: 'blank', + visible: false +}; + const FlowChart: React.FC = (props) => { const { onReady } = props; + const wrapperRef = useRef(null); const graphRef = useRef(null); const miniMapRef = useRef(null); + const [flowChart, setFlowChart] = useState(); + const [contextMenuInfo, setContextMenuInfo] = useState(defaultMenuInfo); useEffect(() => { if (graphRef.current && miniMapRef.current) { - const graph = createFlowChart(graphRef.current, miniMapRef.current); - onReady(graph); - fetchData(graph); + const flowChart = createFlowChart(graphRef.current, miniMapRef.current); + onReady(flowChart); + fetchData(flowChart); + setFlowChart(flowChart); } }, []); - const fetchData = (graph: Graph) => { + // resize flowChart's size when window size changes + useEffect(() => { + const handler = () => { + requestAnimationFrame(() => { + if(flowChart && wrapperRef && wrapperRef.current) { + const width = wrapperRef.current.clientWidth; + const height = wrapperRef.current.clientHeight; + flowChart.resize(width, height); + } + }); + }; + window.addEventListener('resize', handler); + return () => { + window.removeEventListener('resize', handler); + }; + }, [flowChart, wrapperRef]); + + // NOTE: listen toggling context menu event + useEffect(() => { + const showHandler = (info: IMenuInfo) => { + flowChart?.lockScroller(); + setContextMenuInfo({ ...info, visible: true }); + }; + const hideHandler = () => { + flowChart?.unlockScroller(); + setContextMenuInfo({ ...contextMenuInfo, visible: false }); + }; + if (flowChart) { + flowChart.on('graph:showContextMenu', showHandler); + flowChart.on('graph:hideContextMenu', hideHandler); + } + return () => { + if (flowChart) { + flowChart.off('graph:showContextMenu', showHandler); + flowChart.off('graph:hideContextMenu', hideHandler); + } + } + }, [flowChart]); + + const fetchData = (flowChart: Graph) => { const { projectId } = parseQuery(); queryGraph(projectId as string) .then((res) => { const { data: dsl } = res; - graph.fromJSON(dsl); + flowChart.fromJSON(dsl); }) .catch((error) => { console.log('query graph data failed, the error is:', error); @@ -38,9 +101,12 @@ const FlowChart: React.FC = (props) => { }; return ( -
+
+ {flowChart && } + {flowChart && } + {flowChart && }
); }; diff --git a/packages/core/src/mods/flowChart/registerServerStorage.ts b/packages/core/src/mods/flowChart/registerServerStorage.ts index 4c7390b..2e9816b 100644 --- a/packages/core/src/mods/flowChart/registerServerStorage.ts +++ b/packages/core/src/mods/flowChart/registerServerStorage.ts @@ -80,7 +80,6 @@ export const registerServerStorage = (flowChart: Graph) => { const events = nodeActionEventMap[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()); }); }); diff --git a/packages/core/src/mods/header/connectStatus/editModal.tsx b/packages/core/src/mods/header/connectStatus/editModal.tsx index bce54ac..e94e968 100644 --- a/packages/core/src/mods/header/connectStatus/editModal.tsx +++ b/packages/core/src/mods/header/connectStatus/editModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useRef } from 'react'; import 'antd/es/form/style'; import 'antd/es/modal/style'; diff --git a/packages/core/src/mods/header/export/exportModal.tsx b/packages/core/src/mods/header/export/exportModal.tsx index b086755..b1c2e8e 100644 --- a/packages/core/src/mods/header/export/exportModal.tsx +++ b/packages/core/src/mods/header/export/exportModal.tsx @@ -5,9 +5,8 @@ import styles from './index.module.less'; import JSZip from 'jszip'; import { Modal } from 'antd'; -import { Graph } from '@antv/x6'; -import compileCode from '@imove/compile-code'; -import { base64Url2Blob, downloadFile } from '../../../utils'; +import { DataUri, Graph } from '@antv/x6'; +import { compileForProject } from '@imove/compile-code'; interface IExportModalProps { flowChart: Graph; @@ -21,22 +20,21 @@ const ExportModal: React.FC = (props) => { const onExportDSL = () => { const dsl = JSON.stringify(flowChart.toJSON(), null, 2); const blob = new Blob([dsl], { type: 'text/plain' }); - downloadFile('imove.dsl.json', blob); + DataUri.downloadBlob(blob, 'imove.dsl.json'); }; const onExportCode = () => { const zip = new JSZip(); const dsl = flowChart.toJSON(); - const output = compileCode(dsl); + const output = compileForProject(dsl); Helper.recursiveZip(zip, output); zip.generateAsync({ type: 'blob' }).then((blob) => { - downloadFile('logic.zip', blob); + DataUri.downloadBlob(blob, 'logic.zip'); }); }; const onExportFlowChart = () => { - flowChart.toJPEG((dataUri: string) => { - const blob = base64Url2Blob(dataUri); - downloadFile('flowChart.png', blob); - }); + flowChart.toPNG((dataUri: string) => { + DataUri.downloadDataUri(dataUri, 'flowChart.png'); + }, { padding: 50, ratio: '3.0' }); }; return ( diff --git a/packages/core/src/mods/header/guide/guideModal.tsx b/packages/core/src/mods/header/guide/guideModal.tsx new file mode 100644 index 0000000..f2a8532 --- /dev/null +++ b/packages/core/src/mods/header/guide/guideModal.tsx @@ -0,0 +1,279 @@ +import React from 'react'; +import 'antd/es/modal/style'; +import './index.less' +import { Modal, Tabs } from 'antd'; +const { TabPane } = Tabs; + +interface IExportModalProps { + visible: boolean; + onClose: () => void; +} + +const renderCode = (code: any) => { + return
{code}
+} + +const QuickStart: React.FC = () => { + return
+

启动项目

+ {renderCode(`$ git clone git@github.com:imgcook/imove.git\n +$ cd imove\n +$ npm install\n +$ npm run example`)} + + 打开 http://localhost:8000/ + + +

绘制流程图、编写代码

+

根据你的业务逻辑绘制流程图。双击各节点,完成每个节点的函数编写。

+ + + +

在项目中使用

+

这时有两种方式:

+
    +
  1. 在线打包出码
  2. +
  3. 本地启动开发模式
  4. +
+ +

1. 在线打包出码

+ +

点击页面右上方的导出按钮后,可以在弹窗内选择导出 代码,此时流程图编译后的代码将以 zip 包的形式下载到本地,你可以解压后再引入项目中使用。

+ +

2. 本地命令行出码

+

安装 CLI

+
+      $ npm install -g @imove/cli
+    
+

进入项目根目录,imove 初始化

+
+      $ cd yourProject
+      $ imove --init # 或 imove -i
+    
+

本地启动开发模式

+
+      $ imove --dev # 或 imove -d
+    
+ +

本地启动成功之后,可以看到原来的页面右上角会显示连接成功。

+ + +

此时页面上触发 保存快捷键 Ctrl + S 时,可以看到当前项目的 src 目录下会多出一个 logic 目录,这就是 imove 编译生成的代码,此时你只要在你的组件中调用它即可。

+ + 如上代码所示,需要注意两点: +
    +
  1. 通过 logic.on 方法监听事件,事件名和参数与流程图中节点代码的 ctx.emit 相对应
  2. +
  3. 通过 logic.invoke 方法调用逻辑,事件名与流程图中的开始节点的 逻辑触发名称 相对应,不然会调用失败
  4. +
+
+} + +const CreateNode: React.FC = () => { + return ( +
+

创建节点

+

项目启动成功后,可以从左侧面板中依次拖动节点至中间的画布,从而完成流程图的绘制工作

+ 创建节点 +
+ ) +} + +const NodeType: React.FC = () => { + return ( +
+

节点类型

+

在 iMove 中,我们将流程图的节点一共分为以下 3 种类型:

+
    +
  1. 开始节点:逻辑起始,是所有流程的开始,可以是一次生命周期初始化/一次点击
  2. +
  3. 行为节点:逻辑执行,可以是一次网络请求/一次改变状态/发送埋点等
  4. +
  5. 分支节点:逻辑路由,根据不同的逻辑执行结果跳转到不同的节点
  6. +
+ (注:一条逻辑流程必须以 开始节点 为起始) +

根据上述的规范描述,我们可以绘制出各种各样的逻辑流程图,例如获取个人数据的逻辑流程图如下所示:

+ 节点类型 +
+ ) +} + +const ConfigNode: React.FC = () => { + return ( +
+

配置节点

+

依次选中图中的节点,完成右边面板中的基础配置信息填写

+ 配置节点 +

通常来说,基础信息配置使用频率最高的有:

+
    +
  • 显示名称:更改节点名称
  • +
  • 代码:编辑节点代码
  • +
  • 逻辑触发名称:开始节点类型专属配置,项目代码使用时根据这个值触发逻辑调用
  • +
+
+ ) +} + +const CodeStyle: React.FC = () => { + return ( +
+

节点代码规范

+

点击编辑按钮,可打开代码编辑器

+ 节点代码规范 +

每个节点的代码等价于一个 js 文件,因此你不用担心全局变量的命名污染问题,甚至可以 import 现有的 npm 包,但最后必须 export 出一个函数。需要注意的是,由于 iMove 天生支持节点代码的异步调用,因此 export 出的函数默认是一个 promise。

+

就拿 是否登录 这个分支节点为例,我们来看下节点代码该如何编写:

+ {renderCode(`export default async function() {\n + return fetch('/api/isLogin')\n + .then(res => res.json())\n + .then(res => {\n + const {success, data: {isLogin} = {}} = res;\n + return success && isLogin;\n + }).catch(err => {\n + console.log('fetch /api/isLogin failed, the err is:', err);\n + return false;\n + });\n +}\n +`)} +

注:由于该节点是分支节点,因此其 boolean 返回值决定了整个流程的走向

+
+ ) +} + +const NodeConnect: React.FC = () => { + return ( +
+

节点间数据通信

+

在 iMove 中,数据是以流(pipe)的形式从前往后进行流动的,也就是说前一个节点的返回值会是下一个节点的输入。不过也有一个例外,由于分支节点的返回值会是 boolean 类型,因此它的下游节点拿到的输入必将是一个 boolean 类型值,从而造成数据流的中断。为此,我们进行了一定的改造,分支节点的作用只负责数据流的转发,就像一个开关一样,只决定数据流的走向,但不改变流向下游的数据流值。

+

因此,前文例子中的请求profile接口返回数据两个节点会成为数据流的上下游关系。我们再来看下他们之间是如何进行数据通信的:

+ {renderCode(`// 节点: 请求profile接口\n +export default async function() {\n + return fetch('/api/profile')\n + .then(res => res.json())\n + .then(res => {\n + const {success, data} = res;\n + return {success, data};\n + }).catch(err => {\n + console.log('fetch /api/isLogin failed, the err is:', err);\n + return {success: false};\n + });\n +}\n +`)} + {renderCode(`// 节点: 接口成功\n +export default async function(ctx) {\n + // 获取上游数据\n + const pipe = ctx.getPipe() || {};\n + return pipe.success;\n +}\n +`)} + {renderCode(`// 节点: 返回数据\n +const doSomething = (data) => {\n + // TODO: 数据加工处理\n + return data;\n +};\n +export default async function(ctx) {\n + // 这里获取到的上游数据,不是"接口成功"这个分支节点的返回值,而是"请求profile接口"这个节点的返回值\n + const pipe = ctx.getPipe() || {};\n + ctx.emit('updateUI', {profileData: doSomething(pipe.data)});\n +}\n +`)} + 如上代码所述,每个下游节点可以调用 ctx.getPipe 方法获取上游节点返回的数据流。另外,需要注意的是 返回数据 节点的最后一行代码 ctx.emit('eventName', data) 需要和你项目中的代码配合使用。 +
+ ) +} + +const HowToUse: React.FC = () => { + return ( +
+

如何在项目中使用

+

这时有两种方式:

+
    +
  1. 在线打包出码
  2. +
  3. 本地启动开发模式
  4. +
+ +

1. 在线打包出码

+ +

点击页面右上方的导出按钮后,可以在弹窗内选择导出 代码,此时流程图编译后的代码将以 zip 包的形式下载到本地,你可以解压后再引入项目中使用。

+ +

2. 本地命令行出码

+

安装 CLI

+
+        $ npm install -g @imove/cli
+    
+

进入项目根目录,imove 初始化

+
+        $ cd yourProject
+        $ imove --init # 或 imove -i
+    
+

本地启动开发模式

+
+        $ imove --dev # 或 imove -d
+    
+ +

本地启动成功之后,可以看到原来的页面右上角会显示连接成功。

+ + +

此时页面上触发 保存快捷键 Ctrl + S 时,可以看到当前项目的 src 目录下会多出一个 logic 目录,这就是 imove 编译生成的代码,此时你只要在你的组件中调用它即可。

+ + 如上代码所示,需要注意两点: +
    +
  1. 通过 logic.on 方法监听事件,事件名和参数与流程图中节点代码的 ctx.emit 相对应
  2. +
  3. 通过 logic.invoke 方法调用逻辑,事件名与流程图中的开始节点的 逻辑触发名称 相对应,不然会调用失败
  4. +
+
+ ) +} + +const Document: React.FC = () => { + return ( +
+

使用文档

+ + +
+ ) +} + +const GuideModal: React.FC = (props) => { + const { visible, onClose } = props; + + return ( + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); +}; + +export default GuideModal; diff --git a/packages/core/src/mods/header/guide/index.less b/packages/core/src/mods/header/guide/index.less new file mode 100644 index 0000000..3c9ba31 --- /dev/null +++ b/packages/core/src/mods/header/guide/index.less @@ -0,0 +1,22 @@ +.guide-container{ + height:600px; + + .tabPane{ + height:600px; + overflow-y:auto; + + &::-webkit-scrollbar { + width: 0; + } + + img{ + margin:10px 0; + } + + pre{ + color:#c1811d; + font-size:12px; + line-height:13px; + } + } +} diff --git a/packages/core/src/mods/header/guide/index.tsx b/packages/core/src/mods/header/guide/index.tsx new file mode 100644 index 0000000..ddb76bd --- /dev/null +++ b/packages/core/src/mods/header/guide/index.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; + +import { Button } from 'antd'; +import GuideModal from './guideModal'; + +const Export: React.FC = props => { + + const [modalVisible, setModalVisible] = useState(false); + + const onOpenModal = () => setModalVisible(true); + const onCloseModal = () => setModalVisible(false); + + return ( +
+ + +
+ ); +}; + +export default Export; diff --git a/packages/core/src/mods/header/index.tsx b/packages/core/src/mods/header/index.tsx index ef6520d..1d538f9 100644 --- a/packages/core/src/mods/header/index.tsx +++ b/packages/core/src/mods/header/index.tsx @@ -3,6 +3,7 @@ import React from 'react'; import styles from './index.module.less'; import { Graph } from '@antv/x6'; +import Guide from './guide'; import Export from './export'; import ImportDSL from './importDSL'; import ConnectStatus from './connectStatus'; @@ -19,6 +20,7 @@ const Header: React.FC = (props) => { iMove
+ diff --git a/packages/core/src/mods/settingBar/components/checkbox/index.tsx b/packages/core/src/mods/settingBar/components/checkbox/index.tsx index f42d670..6fc89c7 100644 --- a/packages/core/src/mods/settingBar/components/checkbox/index.tsx +++ b/packages/core/src/mods/settingBar/components/checkbox/index.tsx @@ -18,7 +18,6 @@ interface IProps { const Checkbox: React.FC = (props) => { const { name, title, value, disabled, onValueChange } = props; const onChange = (evt: CheckboxChangeEvent): void => { - console.log(evt.target.checked); if (evt.target.checked !== value) { onValueChange(evt.target.checked); } diff --git a/packages/core/src/mods/settingBar/components/code/index.module.less b/packages/core/src/mods/settingBar/components/code/index.module.less deleted file mode 100644 index 1b602f6..0000000 --- a/packages/core/src/mods/settingBar/components/code/index.module.less +++ /dev/null @@ -1,27 +0,0 @@ -.container { - - margin-top: 20px; - - .titleText { - font-size: 14px; - margin-bottom: 10px; - } - - .btn { - margin-top: 5px; - } -} - -.editModal { - - :global { - .ant-modal-body { - padding: 0; - } - } - - .editor { - width: 100%; - height: 400; - } -} diff --git a/packages/core/src/mods/settingBar/components/code/index.tsx b/packages/core/src/mods/settingBar/components/code/index.tsx deleted file mode 100644 index 13f0aaf..0000000 --- a/packages/core/src/mods/settingBar/components/code/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import 'antd/es/modal/style'; -import 'antd/es/button/style'; -import styles from './index.module.less'; - -import { Graph } from '@antv/x6'; -import { Button, Modal } from 'antd'; -import CodeEditor from '../../../../components/codeEditor'; - -interface IProps { - value: any; - name: string; - title: string; - flowChart: Graph; - onValueChange: (value: string) => void; -} - -const Code: React.FC = (props) => { - const { title, value, onValueChange, flowChart } = props; - const [visible, setVisible] = useState(false); - - useEffect(() => { - flowChart.on('node:dblclick', () => { - onClickEdit(); - }); - }, []); - - // events - const onClickEdit = (): void => { - setVisible(true); - }; - const onClickCancel = (): void => { - setVisible(false); - }; - const onClickOk = (newJson: string): void => { - setVisible(false); - onValueChange(newJson); - }; - - return ( -
-

{title}

- - -
- ); -}; - -interface IEditorModalProps { - visible: boolean; - title: string; - value: string; - onOk: (val: string) => void; - onCancel: () => void; -} - -const EditModal: React.FC = (props) => { - const { visible, title, value, onOk, onCancel } = props; - const [code, setCode] = useState(value); - - // life - useEffect(() => { - // set value when opening modal - // clear content when closing modal - if (visible) { - setCode(value); - } else { - setCode(''); - } - }, [visible]); - - // events - const onClickOk = (): void => { - onOk(code); - }; - const onChangeCode = (ev: any, newCode: string | undefined = ''): void => { - if (newCode !== code) { - setCode(newCode); - } - }; - - return ( - - - - ); -}; - -export default Code; diff --git a/packages/core/src/mods/settingBar/components/input/index.module.less b/packages/core/src/mods/settingBar/components/input/index.module.less index 8b3412a..ebb7619 100644 --- a/packages/core/src/mods/settingBar/components/input/index.module.less +++ b/packages/core/src/mods/settingBar/components/input/index.module.less @@ -1,9 +1,4 @@ -.container { - - margin-top: 20px; - .titleText { font-size: 14px; - margin-bottom: 10px; + width:150px; } -} diff --git a/packages/core/src/mods/settingBar/components/input/index.tsx b/packages/core/src/mods/settingBar/components/input/index.tsx index 3ee7431..8153cee 100644 --- a/packages/core/src/mods/settingBar/components/input/index.tsx +++ b/packages/core/src/mods/settingBar/components/input/index.tsx @@ -22,7 +22,7 @@ const Input: React.FC = (props) => { } }; return ( -
+

{title}

diff --git a/packages/core/src/mods/settingBar/components/json/index.less b/packages/core/src/mods/settingBar/components/json/index.less new file mode 100644 index 0000000..ca29c6f --- /dev/null +++ b/packages/core/src/mods/settingBar/components/json/index.less @@ -0,0 +1,7 @@ +.ant-card-bordered{ + border:none; +} +.ant-card-head{ + border:none; + height:35px; +} diff --git a/packages/core/src/mods/settingBar/components/json/index.module.less b/packages/core/src/mods/settingBar/components/json/index.module.less index 4adbf15..99c3b83 100644 --- a/packages/core/src/mods/settingBar/components/json/index.module.less +++ b/packages/core/src/mods/settingBar/components/json/index.module.less @@ -1,27 +1,33 @@ .container { - margin-top: 20px; - .titleText { - font-size: 14px; - margin-bottom: 10px; - } - - .btn { - margin-top: 5px; + .header{ + display:flex; + flex-direction: row; + justify-content:space-between; + + .titleText { + font-size: 14px; + margin-bottom: 10px; + } } } .editModal { + .tabPane{ + .form{ + height:600px; + overflow-y: auto; + margin:0 20px; + } + .configInput{ + height: 600px; + } + } :global { .ant-modal-body { padding: 0; } } - - .editor { - width: 100%; - height: 400; - } } diff --git a/packages/core/src/mods/settingBar/components/json/index.tsx b/packages/core/src/mods/settingBar/components/json/index.tsx index 10f1148..e9eb3a4 100644 --- a/packages/core/src/mods/settingBar/components/json/index.tsx +++ b/packages/core/src/mods/settingBar/components/json/index.tsx @@ -1,27 +1,63 @@ -import React, { useState, useEffect } from 'react'; - +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import Generator from 'fr-generator'; import 'antd/es/modal/style'; import 'antd/es/button/style'; import 'antd/es/message/style'; -import styles from './index.module.less'; - import JsonView from 'react-json-view'; -import { Button, Modal, message } from 'antd'; +import { Button, Tabs, Modal, Card, message } from 'antd'; import { safeParse } from '../../../../utils'; import CodeEditor from '../../../../components/codeEditor'; +import SchemaForm from '../../../../components/schemaForm' +import { compData } from './json' +import styles from './index.module.less'; +const { TabPane } = Tabs; + +const JSON_TAB = 'JsonTab' +const VISUAL_TAB = 'VisualTab' interface IJsonProps { value: any; name: string; title: string; + selectedCell?: any; + isConfig?: boolean; onValueChange: (value: string) => void; } +interface IConfigData { + [key: string]: any +} + +interface ISchemaProps { + type: string, + properties: { [key: string]: any } +} + const Json: React.FC = (props) => { - const { title, value, onValueChange } = props; + const { title, value, isConfig, onValueChange, selectedCell } = props; const [visible, setVisible] = useState(false); + const [schema, setSchema] = useState({ type: '', properties: {} }) + const [configData, setConfigData] = useState({}); + + const defaultSchema = useMemo(() => { + if (selectedCell) { + const { configSchema } = selectedCell.getData(); + if (configSchema) { + const schema = JSON.parse(configSchema).schema + setSchema(schema) + return schema + } + } + }, [selectedCell]) + + const defaultConfigData = useMemo(() => { + if (selectedCell) { + const { configData = {} } = selectedCell.getData(); + setConfigData(configData); + return configData + } + }, [selectedCell]) - // events const onClickEdit = (): void => { setVisible(true); }; @@ -32,29 +68,42 @@ const Json: React.FC = (props) => { setVisible(false); onValueChange(newJson); }; + const changeSchema = (schema: ISchemaProps) => { + setVisible(false); + setSchema(schema); + } + const changeConfigData = (configData: IConfigData) => { + selectedCell && selectedCell.setData({ configData }); + } return ( -
-

{title}

- 编辑}> + {!isConfig && - + />} + + {isConfig && + } + -
+ ); }; @@ -62,18 +111,69 @@ interface IEditorModalProps { visible: boolean; title: string; value: string; + isConfig?: boolean; + defaultSchema: object; onOk: (val: string) => void; onCancel: () => void; + changeSchema: (val: ISchemaProps) => void +} + +interface FormInstance { + setValue: (val: object) => void; + getValue: () => { + schema: ISchemaProps + } } const EditModal: React.FC = (props) => { - const { visible, title, value, onOk, onCancel } = props; + const { visible, title, value, onOk, onCancel, defaultSchema } = props; const [json, setJson] = useState(''); + const [tab, setTab] = useState(VISUAL_TAB) + const formRef = useRef(null); + + const defaultValue = useMemo(() => { + if (defaultSchema && Object.keys(defaultSchema).length > 0) { + let codeObj = { + "schema": Object.assign({}, defaultSchema), + "displayType": "row", + "showDescIcon": true + } + return codeObj + } + }, [defaultSchema]) + + const tabChange = (tab: string) => { + if (tab === JSON_TAB) { + setTab(JSON_TAB) + form2code() + } + if (tab === VISUAL_TAB) { + setTab(VISUAL_TAB) + code2form() + } + } + + const form2code = () => { + const formSchema = formRef.current && formRef.current.getValue() + setJson(JSON.stringify(formSchema, null, 2)) + } + + const code2form = () => { + try { + const codeObj = JSON.parse(json) + formRef.current && formRef.current.setValue(codeObj) + } catch (error) { + console.log('can\'t parse code string to form schema, the error is:', error.message); + } + } + + const onJsonChange = (ev: any, newJson: string | undefined = ''): void => { + setJson(newJson) + props.isConfig && code2form() + } // life useEffect(() => { - // set value when opening modal - // clear content when closing modal if (visible) { setJson(value); } else { @@ -84,22 +184,30 @@ const EditModal: React.FC = (props) => { // events const onClickOk = (): void => { try { - JSON.parse(json); - onOk(json); + if (props.isConfig) { + if (tab === VISUAL_TAB && formRef.current) { + const value = formRef.current.getValue() + props.changeSchema(formRef.current.getValue().schema) + setJson(JSON.stringify(value, null, 2)) + onOk(JSON.stringify(value)); + } else { + props.changeSchema(JSON.parse(json).schema) + onOk(json); + } + } else { + JSON.parse(json); + onOk(json); + } } catch (error) { message.error('内容必须是合法json'); console.log('save failed, the error is:', error.message); } }; - const onChangeJson = (ev: any, newJson: string | undefined = ''): void => { - if (newJson !== json) { - setJson(newJson); - } - }; return ( = (props) => { onOk={onClickOk} onCancel={onCancel} > - + {props.isConfig ? + + +
+ +
+
+ +
+ +
+
+
: + + }
); }; diff --git a/packages/core/src/mods/settingBar/components/json/json.ts b/packages/core/src/mods/settingBar/components/json/json.ts new file mode 100644 index 0000000..7904d4e --- /dev/null +++ b/packages/core/src/mods/settingBar/components/json/json.ts @@ -0,0 +1,133 @@ +const compData = [ + { + "text": 'Input', + "name": 'input', + "schema": { + "title": "Input", + "type": "string", + "description": "输入框" + } + }, + { + "text": 'Textarea', + "name": 'textarea', + "schema": { + "title": "Textarea", + "type": "string", + "format": "textarea", + "description": "文本编辑框" + } + }, + { + "text": 'Switch', + "name": 'allBoolean', + "schema": { + "title": "Switch", + "type": "boolean", + "ui:widget": "switch", + "description": "开关控制" + } + }, + { + "text": 'Select', + "name": 'select', + "schema": { + "title": "Select", + "type": "string", + "enum": [ + "a", + "b", + "c" + ], + "enumNames": [ + "早", + "中", + "晚" + ], + "description": "下拉单选" + } + }, + { + "text": 'MultiSelect', + "name": 'multiSelect', + "schema": { + "title": "MultiSelect", + "description": "下拉多选", + "type": "array", + "items": { + "type": "string" + }, + "enum": [ + "A", + "B", + "C", + "D" + ], + "enumNames": [ + "1", + "2", + "3", + "4" + ], + "ui:widget": "multiSelect" + } + }, + { + "text": 'Checkbox', + "name": 'checkbox', + "schema": { + "title": "Checkbox", + "description": "点击多选", + "type": "array", + "items": { + "type": "string" + }, + "enum": [ + "A", + "B", + "C", + "D" + ], + "enumNames": [ + "1", + "2", + "3", + "4" + ] + } + }, + { + "text": 'TimePicker', + "name": 'timeSelect', + "schema": { + "title": "TimePicker", + "type": "string", + "format": "time", + "description": "时间选择" + } + }, + { + "text": 'DatePicker', + "name": 'dateSelect', + "schema": { + "title": "DatePicker", + "type": "string", + "format": "date", + "description": "日期选择" + } + }, + { + "text": 'DateRange', + "name": 'dateRangeSelect', + "schema": { + "title": "DateRange", + "type": "range", + "format": "date", + "description": "日期范围选择" + } + } +] + +export { + compData +} diff --git a/packages/core/src/mods/settingBar/index.tsx b/packages/core/src/mods/settingBar/index.tsx index 721e82f..de4a3b0 100644 --- a/packages/core/src/mods/settingBar/index.tsx +++ b/packages/core/src/mods/settingBar/index.tsx @@ -4,10 +4,10 @@ import 'antd/es/tabs/style'; import 'antd/es/empty/style'; import styles from './index.module.less'; +import { Graph } from '@antv/x6'; import { Empty, Tabs } from 'antd'; import Basic from './mods/basic'; -import Config from './mods/config'; -import { Graph } from '@antv/x6'; +// import TestCase from './mods/testCase'; const { TabPane } = Tabs; @@ -35,12 +35,12 @@ const SettingBar: React.FC = (props) => { defaultActiveKey={'basic'} tabBarStyle={{ display: 'flex', flex: 1, justifyContent: 'center', alignItems: 'center' }} > - - - - - + + + {/* + + */}
); diff --git a/packages/core/src/mods/settingBar/mods/basic/index.module.less b/packages/core/src/mods/settingBar/mods/basic/index.module.less index 2d50989..26c1d7d 100644 --- a/packages/core/src/mods/settingBar/mods/basic/index.module.less +++ b/packages/core/src/mods/settingBar/mods/basic/index.module.less @@ -1,5 +1,12 @@ .container { - padding: 0 20px; - height: 100%; - overflow: scroll; + height: 900px; + overflow-y: auto; + + &::-webkit-scrollbar { + display: none; + } + + .input{ + margin-top:20px; + } } diff --git a/packages/core/src/mods/settingBar/mods/basic/index.tsx b/packages/core/src/mods/settingBar/mods/basic/index.tsx index d3cadda..b6811d9 100644 --- a/packages/core/src/mods/settingBar/mods/basic/index.tsx +++ b/packages/core/src/mods/settingBar/mods/basic/index.tsx @@ -1,10 +1,13 @@ -import React, { useState, useEffect } from 'react'; +import React, { + useState, + useEffect, +} from 'react'; import styles from './index.module.less'; +import { Card } from 'antd'; import { Cell, Graph } from '@antv/x6'; import Json from '../../components/json'; -import Code from '../../components/code'; import Input from '../../components/input'; interface IProps { @@ -14,7 +17,6 @@ interface IProps { interface IBasicData { label: string; - code: string; trigger?: string; dependencies: string; configSchema: string; @@ -22,19 +24,28 @@ interface IBasicData { const Basic: React.FC = (props) => { const { selectedCell, flowChart } = props; - const [data, setData] = useState(); - const { label, code, trigger, dependencies, configSchema } = data || {}; + const [data, setData] = useState(selectedCell.getData()); + const { label, trigger, dependencies, configSchema } = data || {}; // life useEffect(() => { setData(selectedCell.getData()); - + }, [selectedCell]); + useEffect(() => { + const handler = () => setData(selectedCell.getData()); + flowChart.on('settingBar.basicPanel:forceUpdate', handler); + return () => { + flowChart.off('settingBar.basicPanel:forceUpdate', handler); + }; }, [selectedCell]); // events + const batchUpdate = (newData: { [key: string]: any }): void => { + selectedCell.setData(newData); + setData(Object.assign({}, data, newData)); + }; const commonChange = (key: string, val: string): void => { - selectedCell.setData({ [key]: val }); - setData(Object.assign({}, data, { [key]: val })); + batchUpdate({ [key]: val }); }; const onChangeLabel = (val: string): void => { commonChange('label', val); @@ -42,36 +53,44 @@ const Basic: React.FC = (props) => { }; const onChangeConfigSchema = (val: string): void => { commonChange('configSchema', val); - selectedCell.trigger('change:configSchema', { configSchema: val }); }; - const onChangeCode = (val: string): void => commonChange('code', val); - const onChangeTrigger = (val: string): void => commonChange('trigger', val); - const onChangeDependencies = (val: string): void => commonChange('dependencies', val); + const onChangeTrigger = (val: string): void => { + commonChange('trigger', val); + }; + const onChangeDependencies = (val: string): void => { + commonChange('dependencies', val); + }; return (
- - {selectedCell.shape === 'imove-start' && ( - - )} - - + + + {selectedCell.shape === 'imove-start' && ( +
+ +
+ )} +
+
); }; diff --git a/packages/core/src/mods/settingBar/mods/config/index.module.less b/packages/core/src/mods/settingBar/mods/config/index.module.less deleted file mode 100644 index 05e7233..0000000 --- a/packages/core/src/mods/settingBar/mods/config/index.module.less +++ /dev/null @@ -1,23 +0,0 @@ -.container { - - display: flex; - flex-direction: column; - padding: 0 20px; - height: 100%; - - .empty { - margin-top: 100px; - } - - .footContainer { - - display: flex; - flex-direction: row; - justify-content: flex-end; - margin-top: 20px; - - .saveBtn { - margin-left: 20px; - } - } -} \ No newline at end of file diff --git a/packages/core/src/mods/settingBar/mods/config/index.tsx b/packages/core/src/mods/settingBar/mods/config/index.tsx deleted file mode 100644 index ca70cee..0000000 --- a/packages/core/src/mods/settingBar/mods/config/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import styles from './index.module.less'; - -import { Cell } from '@antv/x6'; -import { Button, Empty } from 'antd'; -import Input from '../../components/input'; -import Checkbox from '../../components/checkbox'; -import { safeParse } from '../../../../utils'; - -// FIXME -// save original config in tempConfig when change to edit mode -// and recover original config when click cancel button -let tempConfig: any = null; - -enum Mode { - edit = 'edit', - readOnly = 'readOnly', -} - -interface IConfig { - [key: string]: any; -} - -interface IProps { - selectedCell: Cell; -} - -const Config: React.FC = (props) => { - const { selectedCell } = props; - const [mode, setMode] = useState(Mode.readOnly); - const [configData, setConfigData] = useState({}); - const [configSchema, setConfigSchema] = useState({}); - - // life - useEffect(() => { - const { configSchema, configData = {} } = selectedCell.getData() || {}; - setConfigData(configData); - setConfigSchema(safeParse(configSchema)); - selectedCell.on('change:configSchema', (data: { configSchema: string }) => { - setConfigSchema(safeParse(data.configSchema)); - }); - return () => { - selectedCell.off('change:configSchema'); - }; - }, [selectedCell]); - - // events - const onFieldValueChange = (key: string, value: any) => { - setConfigData(Object.assign({}, configData, { [key]: value })); - }; - const onClickEdit = (): void => { - setMode(Mode.edit); - tempConfig = configData; - }; - const onClickCancel = (): void => { - setMode(Mode.readOnly); - setConfigData(tempConfig); - }; - const onClickSave = (): void => { - setMode(Mode.readOnly); - selectedCell.setData({ configData }); - }; - - // no config schema - if (!configSchema || Object.keys(configSchema).length === 0) { - return ( -
- -
- ); - } else { - return ( -
- {Object.keys(configSchema).map((key, idx) => { - const { title, description, type } = configSchema[key]; - const FieldComponent = Helper.getFieldComponent(type); - return ( - onFieldValueChange(key, value)} - /> - ); - })} - {mode === Mode.readOnly ? ( -
- -
- ) : ( -
- - -
- )} -
- ); - } -}; - -const Helper = { - getFieldComponent(type: string) { - if (type === 'boolean') { - return Checkbox; - } else { - return Input; - } - }, -}; - -export default Config; diff --git a/packages/core/src/mods/settingBar/mods/testCase/index.tsx b/packages/core/src/mods/settingBar/mods/testCase/index.tsx new file mode 100644 index 0000000..ca028ef --- /dev/null +++ b/packages/core/src/mods/settingBar/mods/testCase/index.tsx @@ -0,0 +1,63 @@ +import React, { + useState, + useEffect, + useCallback +} from 'react'; + +import { Modal } from 'antd'; +import { Graph } from '@antv/x6'; +import CodeRun from '../../../../components/codeRun'; + +interface IProps { + flowChart: Graph; + selectedCell: any; +} + +const TestCase: React.FC = (props) => { + const { flowChart } = props; + const [visible, setVisible] = useState(false); + + useEffect(() => { + flowChart.on('settingBar:runCode', showModal); + return () => { + flowChart.off('settingBar:runCode', showModal); + }; + }, [flowChart]); + + const showModal = useCallback(() => setVisible(true), []); + const closeModal = useCallback(() => setVisible(false), []); + + return ( + + ); +}; + +interface IEditModalProps { + visible: boolean; + flowChart: Graph; + onClose: () => void; +} + +const EditModal: React.FC = (props): JSX.Element => { + const { visible, flowChart, onClose } = props; + + return ( + + + + ); +}; + +export default TestCase; diff --git a/packages/core/src/mods/toolBar/widgets/bgColor.tsx b/packages/core/src/mods/toolBar/widgets/bgColor.tsx index 2d03f44..316c932 100644 --- a/packages/core/src/mods/toolBar/widgets/bgColor.tsx +++ b/packages/core/src/mods/toolBar/widgets/bgColor.tsx @@ -28,7 +28,7 @@ const options = { const bgColor = options.getCurBgColor(flowChart); return (
- +
); diff --git a/packages/core/src/mods/toolBar/widgets/bringToBack.tsx b/packages/core/src/mods/toolBar/widgets/bringToBack.tsx index c32c6a6..fa1a0a5 100644 --- a/packages/core/src/mods/toolBar/widgets/bringToBack.tsx +++ b/packages/core/src/mods/toolBar/widgets/bringToBack.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { Graph } from '@antv/x6'; import XIcon from '../../../components/xIcon'; +import shortcuts from '../../../common/shortcuts'; import makeBtnWidget from './common/makeBtnWidget'; -import { getSelectedNodes, hasNodeSelected } from '../../../utils/flowChartUtils'; +import { hasNodeSelected } from '../../../utils/flowChartUtils'; interface IProps { flowChart: Graph; @@ -11,11 +12,9 @@ interface IProps { const BringToBack: React.FC = makeBtnWidget({ tooltip: '置于底层', + handler: shortcuts.bringToBack.handler, getIcon() { - return ; - }, - handler(flowChart: Graph) { - getSelectedNodes(flowChart).forEach((node) => node.toBack()); + return ; }, disabled(flowChart: Graph) { return !hasNodeSelected(flowChart); diff --git a/packages/core/src/mods/toolBar/widgets/bringToTop.tsx b/packages/core/src/mods/toolBar/widgets/bringToTop.tsx index 3fceb5b..bfdb59a 100644 --- a/packages/core/src/mods/toolBar/widgets/bringToTop.tsx +++ b/packages/core/src/mods/toolBar/widgets/bringToTop.tsx @@ -3,8 +3,9 @@ import React from 'react'; import { Graph } from '@antv/x6'; import XIcon from '../../../components/xIcon'; +import shortcuts from '../../../common/shortcuts'; import makeBtnWidget from './common/makeBtnWidget'; -import { getSelectedNodes, hasNodeSelected } from '../../../utils/flowChartUtils'; +import { hasNodeSelected } from '../../../utils/flowChartUtils'; interface IProps { flowChart: Graph; @@ -12,11 +13,9 @@ interface IProps { const BringToTop: React.FC = makeBtnWidget({ tooltip: '置于顶层', + handler: shortcuts.bringToTop.handler, getIcon() { - return ; - }, - handler(flowChart: Graph) { - getSelectedNodes(flowChart).forEach((node) => node.toFront()); + return ; }, disabled(flowChart: Graph) { return !hasNodeSelected(flowChart); diff --git a/packages/core/src/mods/toolBar/widgets/textColor.tsx b/packages/core/src/mods/toolBar/widgets/textColor.tsx index ce7e476..10cb638 100644 --- a/packages/core/src/mods/toolBar/widgets/textColor.tsx +++ b/packages/core/src/mods/toolBar/widgets/textColor.tsx @@ -28,7 +28,7 @@ const options = { const textColor = options.getCurTextColor(flowChart); return (
- +
); diff --git a/packages/core/src/typings.d.ts b/packages/core/src/typings.d.ts index 252b58b..d00bbf6 100644 --- a/packages/core/src/typings.d.ts +++ b/packages/core/src/typings.d.ts @@ -7,3 +7,13 @@ declare module '*.less' { const classes: { [key: string]: string }; export default classes; } + + +declare module 'fr-generator' { + import React from 'react'; + export interface FRProps { + settings?: object + } + class FormRender extends React.Component { } + export default FRGenerator; +} diff --git a/packages/core/src/utils/analyzeDeps.ts b/packages/core/src/utils/analyzeDeps.ts new file mode 100644 index 0000000..c2ae7c4 --- /dev/null +++ b/packages/core/src/utils/analyzeDeps.ts @@ -0,0 +1,49 @@ +import axios from 'axios'; +import { safeGet } from './index'; + +const regex = /import\s([\s\S]*?)\sfrom\s(?:('[@\.\/\-\w]+')|("[@\.\/\-\w]+"))/mg; + +const extractPkgs = (code: string, excludePkgs?: string[]): string[] => { + let matchRet = null; + const pkgNames: string[] = []; + while ((matchRet = regex.exec(code)) != null) { + let pkgName = (matchRet[2] || matchRet[3]); + pkgName = pkgName.slice(1, pkgName.length - 1); + // NOTE: ignore relative path (./ and ../) and excludePkgs + if ( + pkgName.indexOf('./') === -1 && + pkgName.indexOf('../') === -1 && + excludePkgs?.indexOf(pkgName) === -1 + ) { + pkgNames.push(pkgName); + } + } + return pkgNames; +}; + +const getPkgLatestVersion = (pkg: string): Promise => { + return axios.get(`https://registry.npm.taobao.org/${pkg}`) + .then(res => { + return [pkg, safeGet(res, 'data.dist-tags.latest', '*')]; + }).catch(err => { + console.log(`get package ${pkg} info failed, the error is:`, err); + return [pkg, '*']; + }); +}; + +const analyzeDeps = (code: string, excludePkgs?: string[]): Promise<{ [pkgName: string]: string }> => { + const pkgs = extractPkgs(code, excludePkgs); + return Promise + .all(pkgs.map(pkg => getPkgLatestVersion(pkg))) + .then(pkgResults => { + const map: any = {}; + pkgResults.forEach(([pkg, version]) => { + map[pkg] = version; + }); + return map; + }).catch(err => { + console.log('analyze deps failed, the error is:', err); + }); +}; + +export default analyzeDeps; diff --git a/packages/core/src/utils/flowChartUtils.ts b/packages/core/src/utils/flowChartUtils.ts index 90a628c..0a67488 100644 --- a/packages/core/src/utils/flowChartUtils.ts +++ b/packages/core/src/utils/flowChartUtils.ts @@ -19,3 +19,11 @@ export const getSelectedNodes = (flowChart: Graph): Cell[] => { export const getSelectedEdges = (flowChart: Graph): Cell[] => { return flowChart.getSelectedCells().filter((cell: Cell) => cell.isEdge()); }; + +export const toSelectedCellsJSON = (flowChart: Graph): {cells: Cell.Properties[]} => { + const json = flowChart.toJSON(); + const selectedCells = flowChart.getSelectedCells(); + return { + cells: json.cells.filter(cell => selectedCells.find(o => o.id === cell.id)) + }; +} \ No newline at end of file diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index b8b1757..c134e24 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -37,22 +37,9 @@ export const parseQuery = (): { [key: string]: any } => { return parse(location.search, parseConfig); }; -export const base64Url2Blob = (base64: string) => { - const bytes = window.atob(base64.split(',')[1]); - const buffer = new ArrayBuffer(bytes.length); - const arr = new Uint8Array(buffer); - for (let i = 0; i < bytes.length; i++) { - arr[i] = bytes.charCodeAt(i); - } - return new Blob([arr], { type: 'image/png' }); -}; - -export const downloadFile = (fileName: string, blob: Blob) => { - const elem = document.createElement('a'); - elem.href = URL.createObjectURL(blob); - elem.download = fileName; - document.body.appendChild(elem); - elem.click(); - URL.revokeObjectURL(elem.href); - document.body.removeChild(elem); +export const executeScript = (code: string, type: string = 'module') => { + const script = document.createElement('script'); + script.type = type; + script.text = code; + document.body.appendChild(script); };