Browse Source

Merge branch '0.3.x'

master
smallstonesk 4 years ago
parent
commit
8388f604a5
  1. 105
      example/mock/db.json
  2. 119
      packages/compile-code/src/compileForOnline.ts
  3. 34
      packages/compile-code/src/compileForProject.ts
  4. 37
      packages/compile-code/src/index.ts
  5. 7
      packages/compile-code/src/template/context.ts
  6. 21
      packages/compile-code/src/template/logic.ts
  7. 74
      packages/compile-code/src/template/runOnline.ts
  8. 9
      packages/core/package.json
  9. 2
      packages/core/rollup.config.js
  10. 1
      packages/core/src/common/baseCell/behavior.ts
  11. 1
      packages/core/src/common/baseCell/branch.ts
  12. 1
      packages/core/src/common/baseCell/start.ts
  13. 15
      packages/core/src/common/shortcuts.ts
  14. 36
      packages/core/src/components/codeEditor/index.tsx
  15. 62
      packages/core/src/components/codeRun/index.module.less
  16. 129
      packages/core/src/components/codeRun/index.tsx
  17. 24
      packages/core/src/components/codeRun/inputPanel/index.module.less
  18. 166
      packages/core/src/components/codeRun/inputPanel/index.tsx
  19. 52
      packages/core/src/components/console/index.module.less
  20. 202
      packages/core/src/components/console/index.tsx
  21. 9
      packages/core/src/components/schemaForm/index.module.less
  22. 109
      packages/core/src/components/schemaForm/index.tsx
  23. 1
      packages/core/src/components/xIcon/index.less
  24. 2
      packages/core/src/components/xIcon/index.tsx
  25. 8
      packages/core/src/global.index.less
  26. 65
      packages/core/src/hooks/useClickAway.ts
  27. 11
      packages/core/src/mods/flowChart/codeEditorModel/index.module.less
  28. 129
      packages/core/src/mods/flowChart/codeEditorModel/index.tsx
  29. 8
      packages/core/src/mods/flowChart/codeRunModal/index.module.less
  30. 50
      packages/core/src/mods/flowChart/codeRunModal/index.tsx
  31. 115
      packages/core/src/mods/flowChart/contextMenu/index.tsx
  32. 24
      packages/core/src/mods/flowChart/contextMenu/menuConfig/blank.tsx
  33. 7
      packages/core/src/mods/flowChart/contextMenu/menuConfig/index.ts
  34. 75
      packages/core/src/mods/flowChart/contextMenu/menuConfig/node.tsx
  35. 18
      packages/core/src/mods/flowChart/createFlowChart.ts
  36. 7
      packages/core/src/mods/flowChart/index.module.less
  37. 82
      packages/core/src/mods/flowChart/index.tsx
  38. 1
      packages/core/src/mods/flowChart/registerServerStorage.ts
  39. 2
      packages/core/src/mods/header/connectStatus/editModal.tsx
  40. 18
      packages/core/src/mods/header/export/exportModal.tsx
  41. 279
      packages/core/src/mods/header/guide/guideModal.tsx
  42. 22
      packages/core/src/mods/header/guide/index.less
  43. 24
      packages/core/src/mods/header/guide/index.tsx
  44. 2
      packages/core/src/mods/header/index.tsx
  45. 1
      packages/core/src/mods/settingBar/components/checkbox/index.tsx
  46. 27
      packages/core/src/mods/settingBar/components/code/index.module.less
  47. 112
      packages/core/src/mods/settingBar/components/code/index.tsx
  48. 7
      packages/core/src/mods/settingBar/components/input/index.module.less
  49. 2
      packages/core/src/mods/settingBar/components/input/index.tsx
  50. 7
      packages/core/src/mods/settingBar/components/json/index.less
  51. 32
      packages/core/src/mods/settingBar/components/json/index.module.less
  52. 203
      packages/core/src/mods/settingBar/components/json/index.tsx
  53. 133
      packages/core/src/mods/settingBar/components/json/json.ts
  54. 14
      packages/core/src/mods/settingBar/index.tsx
  55. 13
      packages/core/src/mods/settingBar/mods/basic/index.module.less
  56. 75
      packages/core/src/mods/settingBar/mods/basic/index.tsx
  57. 23
      packages/core/src/mods/settingBar/mods/config/index.module.less
  58. 123
      packages/core/src/mods/settingBar/mods/config/index.tsx
  59. 63
      packages/core/src/mods/settingBar/mods/testCase/index.tsx
  60. 2
      packages/core/src/mods/toolBar/widgets/bgColor.tsx
  61. 9
      packages/core/src/mods/toolBar/widgets/bringToBack.tsx
  62. 9
      packages/core/src/mods/toolBar/widgets/bringToTop.tsx
  63. 2
      packages/core/src/mods/toolBar/widgets/textColor.tsx
  64. 10
      packages/core/src/typings.d.ts
  65. 49
      packages/core/src/utils/analyzeDeps.ts
  66. 8
      packages/core/src/utils/flowChartUtils.ts
  67. 23
      packages/core/src/utils/index.ts

105
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
}
]
}

119
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;

34
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;

37
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;

7
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;
}

21
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);
}
}
`;

74
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;

9
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",

2
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: [
{

1
packages/core/src/common/baseCell/behavior.ts

@ -13,6 +13,7 @@ const schema = {
},
label: {
fill: '#333',
textWrap: { width: '100%' },
},
},
ports: {

1
packages/core/src/common/baseCell/branch.ts

@ -20,6 +20,7 @@ const schema = {
fontSize: 14,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
textWrap: { width: '100%' },
},
},
ports: {

1
packages/core/src/common/baseCell/start.ts

@ -14,6 +14,7 @@ const schema = {
},
label: {
fill: '#FFF',
textWrap: { width: '100%' },
},
},
ports: {

15
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;

36
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<ControlledEditorProps> = (props) => {
const {options, ...rest} = props;
interface IProps extends ControlledEditorProps {
onSave?: (code: string) => void;
}
export const CodeEditor: React.FC<IProps> = (props) => {
const {options, editorDidMount, onSave, ...rest} = props;
const [editorInst, setEditorInst] = useState<any>();
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 (
<ControlledEditor
theme={'monokai'}
language={'javascript'}
options={editorOptions}
editorDidMount={onEditorDidMount}
{...rest}
/>
);

62
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;
}
}
}

129
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<ICardProps> = (props) => {
const { title } = props;
return (
<div className={styles.card}>
<div className={styles.cardTitleText}>{title}</div>
<div className={styles.cardBody}>
{props.children}
</div>
</div>
);
};
interface ICodeRunProps {
flowChart: Graph;
}
const CodeRun: React.FC<ICodeRunProps> = (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 (
<div className={styles.container}>
<SplitPane split={'horizontal'}>
<Pane initialSize={'380px'} minSize={'43px'}>
<SplitPane split={'vertical'}>
<Pane className={styles.pane} minSize={'360px'} maxSize={'660px'}>
<div className={styles.runWrapper}>
{isRunning ? (
<Button size={'large'} type={'link'}>
<LoadingOutlined /> ...
</Button>
) : (
<Button size={'large'} type={'link'} onClick={onClickRun}>
<PlayCircleFilled />
</Button>
)}
</div>
<Card title={'输入'}>
<InputPanel
data={input}
onChange={onChangeInput}
/>
</Card>
</Pane>
<Pane className={styles.pane}>
<Card title={'输出'}>
<JsonView
name={null}
collapsed={false}
enableClipboard={false}
displayDataTypes={false}
displayObjectSize={false}
src={output}
/>
</Card>
</Pane>
</SplitPane>
</Pane>
<Pane className={styles.pane} minSize={'40px'}>
<Console />
</Pane>
</SplitPane>
</div>
);
}
export default CodeRun;

24
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;
}
}

166
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<IVisualFormItemProps> = (props) => {
const { type, desc } = props;
return (
<React.Fragment>
<div className={styles.itemHeader}>
<span className={styles.itemTitleText}>{type}</span>
<span className={styles.itemDescText}>{desc}</span>
</div>
<Form.List name={type}>
{(fields, { add, remove }) => (
<React.Fragment>
{fields.map(field => (
<Space key={field.key} align={'baseline'}>
<Form.Item
{...field}
label={'key'}
name={[field.name, 'key']}
fieldKey={[field.fieldKey, 'key']}
>
<Input />
</Form.Item>
<Form.Item
{...field}
label={'value'}
name={[field.name, 'value']}
fieldKey={[field.fieldKey, 'value']}
>
<Input />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(field.name)} />
</Space>
))}
<Form.Item>
<Button
block={true}
type={'dashed'}
icon={<PlusOutlined />}
onClick={() => add()}
>
</Button>
</Form.Item>
</React.Fragment>
)}
</Form.List>
</React.Fragment>
);
};
interface IVisualPanelProps {
data: any;
onChange: (value: object) => void
}
const VisualPanel: React.FC<IVisualPanelProps> = (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 (
<div className={styles.visualInput}>
<Form
form={form}
autoComplete={'on'}
onChange={onFormChange}
>
{inputItems.map(({ type, desc }, index) => (
<VisualFormItem
key={index}
type={type}
desc={desc}
/>
))}
</Form>
</div>
);
};
interface IInputPanelProps {
data: any;
onChange: (data: any) => void;
}
const InputPanel: React.FC<IInputPanelProps> = (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 (
<Tabs type={'card'} defaultActiveKey={'visualTab'}>
<Tabs.TabPane className={styles.tabPane} tab={'可视化模式'} key={'visualTab'}>
<VisualPanel data={data} onChange={onVisualChange} />
</Tabs.TabPane>
<Tabs.TabPane className={styles.tabPane} tab={'源码模式'} key={'jsonTab'}>
<CodeEditor
height={'100%'}
language={'json'}
value={JSON.stringify(data, null, 2)}
onChange={onEditorChange}
/>
</Tabs.TabPane>
</Tabs>
);
};
export default InputPanel;

52
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;
}
}
}

202
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: <div className={styles.logIcon} />,
originMethod: console.log
},
info: {
bgColor: '#272823',
textColor: '#ffffff',
borderColor: 'rgba(128, 128, 128, 0.35)',
icon: <InfoCircleOutlined className={styles.logIcon} />,
originMethod: console.info
},
warn: {
bgColor: 'rgb(51, 42, 0)',
textColor: 'rgb(245, 211, 150)',
borderColor: 'rgb(102, 85, 0)',
icon: <WarningOutlined className={styles.logIcon} />,
originMethod: console.warn
},
debug: {
bgColor: '#272823',
textColor: 'rgb(77, 136, 255)',
borderColor: 'rgba(128, 128, 128, 0.35)',
icon: <BugOutlined className={styles.logIcon} />,
originMethod: console.debug
},
error: {
bgColor: 'rgb(40, 0, 0)',
textColor: 'rgb(254, 127, 127)',
borderColor: 'rgb(91, 0, 0)',
icon: <CloseCircleOutlined className={styles.logIcon} />,
originMethod: console.error
}
}
const MyConsole: React.FC = () => {
const [filter, setFilter] = useState('');
const [level, setLevel] = useState('all');
const [logList, setLogList] = useState<ILog[]>([]);
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<HTMLInputElement>) => {
setFilter(evt.target.value);
}, []);
const onChangeLevel = useCallback((level: string) => {
setLevel(level);
}, []);
const onClickClear = useCallback(() => {
setLogList([]);
cache.current.allLogs = [];
}, []);
const renderToolBar = () => {
return (
<div className={styles.toolBar}>
<Input
allowClear={true}
placeholder={'过滤'}
prefix={<FilterOutlined />}
onChange={onChangeFilter}
/>
<Select className={styles.levels} defaultValue={'all'} onChange={onChangeLevel}>
{['all', 'info', 'warn', 'error', 'debug'].map(method => (
<Select.Option key={method} value={method}>
{method}
</Select.Option>
))}
</Select>
<Button type={'link'} onClick={onClickClear}>
<ClearOutlined />
</Button>
</div>
);
};
const renderLogPanel = (logList: ILog[]) => {
return (
<div className={styles.logPanel}>
{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 (
<div key={index} className={styles.logLine} style={logLineStyle}>
{icon}
<div className={styles.logText}>
{log.strVal}
</div>
</div>
);
})}
</div>
);
};
return (
<div className={styles.container}>
<Tabs type={'card'} tabBarExtraContent={renderToolBar()}>
<Tabs.TabPane tab={'控制台'} key={'log'}>
{renderLogPanel(logList)}
</Tabs.TabPane>
</Tabs>
</div>
);
};
export default MyConsole;

9
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;
}
}

109
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<IProps> = (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 ?
<Form
form={form}
initialValues={configData}
{...formLayout}
>
{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 = <Input />
}
// 编辑框
if (obj.type === 'string' && obj.format === 'textarea') {
ele = <Input.TextArea />
}
// switch
if (obj.type === 'boolean' && obj['ui:widget'] === 'switch') {
ele = <Switch />
}
// 下拉单选
if (obj.type === 'string' && obj.hasOwnProperty('enum') && !obj.hasOwnProperty('ui:widget')) {
ele = <Select allowClear>{options.map(item =>
<Option key={item.value} value={item.value}>{item.label}</Option>
)}</Select>
}
// 下拉多选
if (obj.type === 'array' && obj.hasOwnProperty('enum') && obj['ui:widget'] === 'multiSelect') {
ele = <Select allowClear mode="multiple">
{options.map(item => <Option key={item.value} value={item.value}>{item.label}</Option>)}
</Select>
}
// 点击多选
if (obj.type === 'array' && obj.hasOwnProperty('enum') && !obj.hasOwnProperty('ui:widget')) {
ele = <Checkbox.Group options={options} />
}
// 时间选择
if (obj.type === 'string' && obj.format === 'time') {
ele = <TimePicker defaultValue={moment('00:00:00', 'HH:mm:ss')} />
}
// 日期范围
if (obj.type === 'range' && obj.format === 'date') {
ele = <DatePicker />
}
// 日期选择
if (obj.type === 'string' && obj.format === 'date') {
ele = <RangePicker />
}
return <FormItem
label={obj.title}
name={key}
>
{ele}
</FormItem>
})}
<div className={styles.btnWrap}>
<Button size='small' className={styles.btn} type={'primary'} onClick={onClickSave}></Button>
</div>
</Form> :
<Empty
description={'请编辑投放配置schema'}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
);
};
export default SchemaForm

1
packages/core/src/components/xIcon/index.less

@ -1,3 +1,4 @@
#icon-select-all,
#icon-align-top,
#icon-align-left,
#icon-align-vertical-center,

2
packages/core/src/components/xIcon/index.tsx

@ -8,7 +8,7 @@ interface IconFontProps {
}
const XIcon: React.SFC<IconFontProps> = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_2024452_zgg5gvivois.js',
scriptUrl: '//at.alicdn.com/t/font_2024452_r7nz0cw949.js',
});
export default XIcon;

8
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 {

65
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 = HTMLElement> =
| (() => T | null)
| T
| null
| MutableRefObject<T | null | undefined>;
const defaultEvent = 'click';
const getTargetElement = (
target: BasicTarget<TargetElement>,
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;

11
packages/core/src/mods/flowChart/codeEditorModel/index.module.less

@ -0,0 +1,11 @@
.depsInfoModalContent {
padding-top: 20px;
}
.modal {
:global {
.ant-modal-body {
padding: 0;
}
}
}

129
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<IProps> = (props) => {
const { title = '编辑代码', flowChart } = props;
const [code, setCode] = useState<string>('');
const [visible, setVisible] = useState<boolean>(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: (
<div className={styles.depsInfoModalContent}>
<JsonView
src={deps}
name={'dependencies'}
collapsed={false}
enableClipboard={false}
displayDataTypes={false}
displayObjectSize={false}
/>
</div>
),
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 (
<Modal
className={styles.modal}
width={1000}
title={title}
visible={visible}
onCancel={onCancel}
footer={[
<Button key={'cancel'} onClick={onCancel}></Button>,
<Button key={'runCode'} type={'primary'} ghost onClick={onRunCode}></Button>,
<Button key={'saveCode'} type={'primary'} onClick={onOk}></Button>,
]}
>
<CodeEditor
value={code}
width={'100%'}
height={'600px'}
onChange={onChangeCode}
onSave={onSaveCode}
/>
</Modal>
);
};
export default CodeEditModal;

8
packages/core/src/mods/flowChart/codeRunModal/index.module.less

@ -0,0 +1,8 @@
.modal {
:global {
.ant-modal-body {
padding: 0;
height: 653px;
}
}
}

50
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<IEditModalProps> = (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 (
<Modal
className={styles.modal}
width={1000}
title={title}
visible={visible}
footer={null}
onCancel={onClose}
>
<CodeRun flowChart={flowChart}/>
</Modal>
);
};
export default CodeRunModal;

115
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<IProps> = 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 : (
<div
ref={menuRef}
className={styles.contextMenu}
style={{ left: x, top: y }}
>
<Menu
mode={'vertical'}
selectable={false}
onClick={onClickMenu}
>
{Helper.makeMenuContent(flowChart, menuConfig)}
</Menu>
</div>
);
};
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 = (
<Menu.SubMenu key={key} icon={icon} title={title} disabled={disabled}>
{loop(children)}
</Menu.SubMenu>
);
} else {
content = (
<Menu.Item key={key} icon={icon} disabled={disabled}>
{title}
</Menu.Item>
);
}
return [
content,
showDividerBehind && <Menu.Divider />
];
});
};
return loop(menuConfig);
}
};
export default FlowChartContextMenu;

24
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: <XIcon type={'icon-select-all'} />,
handler: shortcuts.selectAll.handler
},
{
key: 'paste',
title: '粘贴',
icon: <SnippetsOutlined />,
disabled: (flowChart: Graph) => flowChart.isClipboardEmpty(),
handler: shortcuts.paste.handler
}
];
export default blankMenuConfig;

7
packages/core/src/mods/flowChart/contextMenu/menuConfig/index.ts

@ -0,0 +1,7 @@
import nodeMenuConfig from './node';
import blankMenuConfig from './blank';
export {
nodeMenuConfig,
blankMenuConfig
};

75
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: <CopyOutlined />,
handler: shortcuts.copy.handler
},
{
key: 'delete',
title: '删除',
icon: <DeleteOutlined />,
handler: shortcuts.delete.handler
},
{
key: 'rename',
title: '编辑文本',
icon: <EditOutlined />,
showDividerBehind: true,
handler() {
// TODO
}
},
{
key: 'bringToTop',
title: '置于顶层',
icon: <XIcon type={'icon-bring-to-top'} />,
handler: shortcuts.bringToTop.handler
},
{
key: 'bringToBack',
title: '置于底层',
icon: <XIcon type={'icon-bring-to-bottom'} />,
showDividerBehind: true,
handler: shortcuts.bringToBack.handler
},
{
key: 'editCode',
title: '编辑代码',
icon: <FormOutlined />,
disabled(flowChart: Graph) {
return getSelectedNodes(flowChart).length !== 1;
},
handler(flowChart: Graph) {
flowChart.trigger('graph:editCode');
}
},
{
key: 'executeCode',
title: '执行代码',
icon: <CodeOutlined />,
disabled(flowChart: Graph) {
return getSelectedNodes(flowChart).length !== 1;
},
handler(flowChart: Graph) {
flowChart.trigger('graph:runCode');
}
}
];
export default nodeMenuConfig;

18
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

7
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;
}
}

82
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<IProps> = (props) => {
const { onReady } = props;
const wrapperRef = useRef<HTMLDivElement>(null);
const graphRef = useRef<HTMLDivElement>(null);
const miniMapRef = useRef<HTMLDivElement>(null);
const [flowChart, setFlowChart] = useState<Graph>();
const [contextMenuInfo, setContextMenuInfo] = useState<IMenuInfo>(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<IProps> = (props) => {
};
return (
<div className={styles.container}>
<div className={styles.container} ref={wrapperRef}>
<div className={styles.flowChart} ref={graphRef} />
<div className={styles.miniMap} ref={miniMapRef} />
{flowChart && <CodeRunModal flowChart={flowChart}/>}
{flowChart && <CodeEditorModal flowChart={flowChart}/>}
{flowChart && <FlowChartContextMenu {...contextMenuInfo} flowChart={flowChart} />}
</div>
);
};

1
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());
});
});

2
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';

18
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<IExportModalProps> = (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 (

279
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 <pre>{code}</pre>
}
const QuickStart: React.FC = () => {
return <div>
<h2></h2>
{renderCode(`$ git clone git@github.com:imgcook/imove.git\n
$ cd imove\n
$ npm install\n
$ npm run example`)}
<a href="http://localhost:8000/">http://localhost:8000/</a>
<img width="100%" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610523659304-84fc5548-3203-48b6-9ec8-81f0b0228245.png" />
<h2></h2>
<p></p>
<img width="100%" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610529557793-63ef6842-079c-498a-8ad3-dfbdad7549bb.png" />
<img width="100%" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610523745104-24e94b10-f8d7-4eb2-a282-ca32a19f7ccd.png" />
<h2>使</h2>
<p></p>
<ol>
<li>线</li>
<li></li>
</ol>
<h3>1. 线</h3>
<img width="100%" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610523805396-4211e9c2-b501-4b23-aeae-f55147d67e8d.png" />
<p> zip 使</p>
<h3>2. </h3>
<p> CLI</p>
<pre>
$ npm install -g @imove/cli
</pre>
<p>imove </p>
<pre>
$ cd yourProject
$ imove --init # imove -i
</pre>
<p></p>
<pre>
$ imove --dev # imove -d
</pre>
<p></p>
<img width="100%" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610529177065-5000bb28-10f2-4053-a4bb-af98139f488e.png" />
<p> <b> Ctrl + S</b> src <strong>logic</strong> imove </p>
<b></b>
<ol>
<li> logic.on ctx.emit </li>
<li> logic.invoke </li>
</ol>
</div>
}
const CreateNode: React.FC = () => {
return (
<div>
<h2></h2>
<p></p>
<img width="100%" alt="创建节点" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610528033956-df4c7cb8-2b35-4065-8f51-a8d98c02f532.png"></img>
</div>
)
}
const NodeType: React.FC = () => {
return (
<div>
<h2></h2>
<p> iMove 3 </p>
<ol>
<li>/</li>
<li>//</li>
<li></li>
</ol>
<b> </b>
<p><b></b></p>
<img width="100%" alt="节点类型" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610528367101-6ff34826-8658-41e8-bb8b-66b254f14cde.png"></img>
</div>
)
}
const ConfigNode: React.FC = () => {
return (
<div>
<h2></h2>
<p></p>
<img width="100%" alt="配置节点" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610528399738-19c54ce9-25f4-4fc0-93a8-580f9eb9f20f.png"></img>
<p>使</p>
<ul>
<li></li>
<li></li>
<li>使</li>
</ul>
</div>
)
}
const CodeStyle: React.FC = () => {
return (
<div>
<h2></h2>
<p></p>
<img width="100%" alt="节点代码规范" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610528493678-c08c4d08-380e-44ca-8dfd-33adb5c1604f.png"></img>
<p> js import npm export iMove export promise</p>
<p> <b></b> </p>
{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
`)}
<p><b></b> boolean </p>
</div>
)
}
const NodeConnect: React.FC = () => {
return (
<div>
<h2></h2>
<p> iMove pipe<b></b> boolean boolean </p>
<p><b>profile接口</b><b></b></p>
{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
`)}
<b>ctx.getPipe</b> <b></b> <b>ctx.emit('eventName', data)</b> 使
</div>
)
}
const HowToUse: React.FC = () => {
return (
<div>
<h2>使</h2>
<p></p>
<ol>
<li>线</li>
<li></li>
</ol>
<h3>1. 线</h3>
<img width="100%" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610523805396-4211e9c2-b501-4b23-aeae-f55147d67e8d.png" />
<p> zip 使</p>
<h3>2. </h3>
<p> CLI</p>
<pre>
$ npm install -g @imove/cli
</pre>
<p>imove </p>
<pre>
$ cd yourProject
$ imove --init # imove -i
</pre>
<p></p>
<pre>
$ imove --dev # imove -d
</pre>
<p></p>
<img width="100%" src="https://intranetproxy.alipay.com/skylark/lark/0/2021/png/317407/1610529177065-5000bb28-10f2-4053-a4bb-af98139f488e.png" />
<p> <b> Ctrl + S</b> src <strong>logic</strong> imove </p>
<b></b>
<ol>
<li> logic.on ctx.emit </li>
<li> logic.invoke </li>
</ol>
</div>
)
}
const Document: React.FC = () => {
return (
<div>
<h2>使</h2>
<div><a target="blank" href="https://www.yuque.com/imove/docs">使</a></div>
<div><a target="blank" href="https://github.com/imgcook/imove/blob/master/README.md">github README</a></div>
</div>
)
}
const GuideModal: React.FC<IExportModalProps> = (props) => {
const { visible, onClose } = props;
return (
<Modal
width={1000}
visible={visible}
footer={null}
title={'帮助文档'}
onOk={onClose}
onCancel={onClose}
>
<div className="guide-container">
<Tabs tabPosition='left'>
<TabPane className="tabPane" tab="简单上手" key="1">
<QuickStart />
</TabPane>
<TabPane className="tabPane" tab="创建节点" key="2">
<CreateNode />
</TabPane>
<TabPane className="tabPane" tab="节点类型" key="3">
<NodeType />
</TabPane>
<TabPane className="tabPane" tab="配置节点" key="4">
<ConfigNode />
</TabPane>
<TabPane className="tabPane" tab="节点代码规范" key="5">
<CodeStyle />
</TabPane>
<TabPane className="tabPane" tab="节点间数据通信" key="6">
<NodeConnect />
</TabPane>
<TabPane className="tabPane" tab="如何在项目中使用" key="7">
<HowToUse />
</TabPane>
<TabPane className="tabPane" tab="使用文档" key="8">
<Document />
</TabPane>
</Tabs>
</div>
</Modal>
);
};
export default GuideModal;

22
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;
}
}
}

24
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<boolean>(false);
const onOpenModal = () => setModalVisible(true);
const onCloseModal = () => setModalVisible(false);
return (
<div>
<Button type={'primary'} size={'small'} onClick={onOpenModal}></Button>
<GuideModal
visible={modalVisible}
onClose={onCloseModal}
/>
</div>
);
};
export default Export;

2
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<IProps> = (props) => {
<span className={styles.titleText}>iMove</span>
</a>
<div className={styles.widgets}>
<Guide />
<Export flowChart={flowChart} />
<ImportDSL flowChart={flowChart} />
<ConnectStatus flowChart={flowChart} />

1
packages/core/src/mods/settingBar/components/checkbox/index.tsx

@ -18,7 +18,6 @@ interface IProps {
const Checkbox: React.FC<IProps> = (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);
}

27
packages/core/src/mods/settingBar/components/code/index.module.less

@ -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;
}
}

112
packages/core/src/mods/settingBar/components/code/index.tsx

@ -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<IProps> = (props) => {
const { title, value, onValueChange, flowChart } = props;
const [visible, setVisible] = useState<boolean>(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 (
<div className={styles.container}>
<p className={styles.titleText}>{title}</p>
<Button block={true} className={styles.btn} onClick={onClickEdit}>
</Button>
<EditModal
title={title}
value={value}
visible={visible}
onOk={onClickOk}
onCancel={onClickCancel}
/>
</div>
);
};
interface IEditorModalProps {
visible: boolean;
title: string;
value: string;
onOk: (val: string) => void;
onCancel: () => void;
}
const EditModal: React.FC<IEditorModalProps> = (props) => {
const { visible, title, value, onOk, onCancel } = props;
const [code, setCode] = useState<string>(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 (
<Modal
className={styles.editModal}
width={800}
title={title}
okText={'保存'}
visible={visible}
cancelText={'取消'}
onOk={onClickOk}
onCancel={onCancel}
>
<CodeEditor
value={code}
width={'100%'}
height={'600px'}
onChange={onChangeCode}
/>
</Modal>
);
};
export default Code;

7
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;
}
}

2
packages/core/src/mods/settingBar/components/input/index.tsx

@ -22,7 +22,7 @@ const Input: React.FC<IProps> = (props) => {
}
};
return (
<div className={styles.container}>
<div>
<p className={styles.titleText}>{title}</p>
<AntdInput name={name} value={value} disabled={disabled} onChange={onChange} />
</div>

7
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;
}

32
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;
}
}

203
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<IJsonProps> = (props) => {
const { title, value, onValueChange } = props;
const { title, value, isConfig, onValueChange, selectedCell } = props;
const [visible, setVisible] = useState<boolean>(false);
const [schema, setSchema] = useState<ISchemaProps>({ type: '', properties: {} })
const [configData, setConfigData] = useState<IConfigData>({});
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<IJsonProps> = (props) => {
setVisible(false);
onValueChange(newJson);
};
const changeSchema = (schema: ISchemaProps) => {
setVisible(false);
setSchema(schema);
}
const changeConfigData = (configData: IConfigData) => {
selectedCell && selectedCell.setData({ configData });
}
return (
<div className={styles.container}>
<p className={styles.titleText}>{title}</p>
<JsonView
<Card title={title} extra={<Button type="link" onClick={onClickEdit}></Button>}>
{!isConfig && <JsonView
name={null}
collapsed={true}
enableClipboard={false}
displayDataTypes={false}
displayObjectSize={false}
src={safeParse(value)}
/>
<Button block={true} className={styles.btn} onClick={onClickEdit}>
</Button>
/>}
{isConfig && <SchemaForm
schema={schema}
changeConfigData={changeConfigData}
configData={defaultConfigData} />
}
<EditModal
title={title}
value={value}
visible={visible}
isConfig={props.isConfig}
onOk={onClickOk}
onCancel={onClickCancel}
changeSchema={changeSchema}
defaultSchema={defaultSchema}
/>
</div>
</Card>
);
};
@ -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<IEditorModalProps> = (props) => {
const { visible, title, value, onOk, onCancel } = props;
const { visible, title, value, onOk, onCancel, defaultSchema } = props;
const [json, setJson] = useState<string>('');
const [tab, setTab] = useState<string>(VISUAL_TAB)
const formRef = useRef<FormInstance>(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<IEditorModalProps> = (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 (
<Modal
className={styles.editModal}
width={props.isConfig ? 1200 : 800}
title={title}
okText={'保存'}
visible={visible}
@ -107,12 +215,45 @@ const EditModal: React.FC<IEditorModalProps> = (props) => {
onOk={onClickOk}
onCancel={onCancel}
>
<CodeEditor
value={json}
width={'100%'}
height={'600px'}
onChange={onChangeJson}
/>
{props.isConfig ?
<Tabs
type="card"
tabBarGutter={0}
defaultActiveKey={'basic'}
onChange={tabChange}
tabBarStyle={{ display: 'flex', flex: 1, justifyContent: 'center', alignItems: 'center' }}
>
<TabPane className={styles.tabPane} tab={'可视化'} key={'VisualTab'}>
<div className={styles.form}>
<Generator
ref={formRef}
settings={[{
title: '表单组件库',
widgets: compData,
}]}
defaultValue={defaultValue}
/>
</div>
</TabPane>
<TabPane className={styles.tabPane} tab={'JSON'} key={'JsonTab'}>
<div className={styles.configInput}>
<CodeEditor
width={'100%'}
language={'json'}
value={json}
onChange={onJsonChange}
/>
</div>
</TabPane>
</Tabs> :
<CodeEditor
value={json}
width={'100%'}
height={'600px'}
language={'json'}
onChange={onJsonChange}
/>
}
</Modal>
);
};

133
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
}

14
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<IProps> = (props) => {
defaultActiveKey={'basic'}
tabBarStyle={{ display: 'flex', flex: 1, justifyContent: 'center', alignItems: 'center' }}
>
<TabPane tab={'基础信息'} key={'basic'}>
<Basic selectedCell={nodes[0]} flowChart={flowChart}/>
</TabPane>
<TabPane tab={'投放配置'} key={'config'}>
<Config selectedCell={nodes[0]} />
<TabPane tab={'节点配置'} key={'basic'}>
<Basic selectedCell={nodes[0]} flowChart={flowChart} />
</TabPane>
{/* <TabPane tab={''} key={'testCase'} forceRender>
<TestCase selectedCell={nodes[0]} flowChart={flowChart} />
</TabPane> */}
</Tabs>
</div>
);

13
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;
}
}

75
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<IProps> = (props) => {
const { selectedCell, flowChart } = props;
const [data, setData] = useState<IBasicData>();
const { label, code, trigger, dependencies, configSchema } = data || {};
const [data, setData] = useState<IBasicData>(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<IProps> = (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 (
<div className={styles.container}>
<Input name={'label'} title={'显示名称'} value={label} onValueChange={onChangeLabel} />
{selectedCell.shape === 'imove-start' && (
<Input
name={'trigger'}
title={'逻辑触发名称'}
value={trigger}
onValueChange={onChangeTrigger}
/>
)}
<Code name={'code'} title={'代码'} value={code} onValueChange={onChangeCode} flowChart={flowChart} />
<Json
name={'configSchema'}
title={'投放配置schema'}
value={configSchema}
onValueChange={onChangeConfigSchema}
/>
<Card title="名称">
<Input name={'label'} title={'节点显示名称'} value={label} onValueChange={onChangeLabel} />
{selectedCell.shape === 'imove-start' && (
<div className={styles.input}>
<Input
name={'trigger'}
title={'逻辑触发名称'}
value={trigger}
onValueChange={onChangeTrigger}
/>
</div>
)}
</Card>
<Json
name={'dependencies'}
title={'依赖'}
value={dependencies}
isConfig={false}
onValueChange={onChangeDependencies}
/>
<Json
name={'configSchema'}
title={'投放配置'}
selectedCell={selectedCell}
value={configSchema}
isConfig={true}
onValueChange={onChangeConfigSchema}
/>
</div>
);
};

23
packages/core/src/mods/settingBar/mods/config/index.module.less

@ -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;
}
}
}

123
packages/core/src/mods/settingBar/mods/config/index.tsx

@ -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<IProps> = (props) => {
const { selectedCell } = props;
const [mode, setMode] = useState<Mode>(Mode.readOnly);
const [configData, setConfigData] = useState<IConfig>({});
const [configSchema, setConfigSchema] = useState<IConfig>({});
// 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 (
<div className={styles.container}>
<Empty
className={styles.empty}
description={'请编辑投放配置schema'}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
);
} else {
return (
<div className={styles.container}>
{Object.keys(configSchema).map((key, idx) => {
const { title, description, type } = configSchema[key];
const FieldComponent = Helper.getFieldComponent(type);
return (
<FieldComponent
key={idx}
name={key}
title={title}
value={configData[key]}
description={description}
disabled={mode === Mode.readOnly}
onValueChange={(value: any) => onFieldValueChange(key, value)}
/>
);
})}
{mode === Mode.readOnly ? (
<div className={styles.footContainer}>
<Button block onClick={onClickEdit}>
</Button>
</div>
) : (
<div className={styles.footContainer}>
<Button onClick={onClickCancel}></Button>
<Button className={styles.saveBtn} type={'primary'} onClick={onClickSave}>
</Button>
</div>
)}
</div>
);
}
};
const Helper = {
getFieldComponent(type: string) {
if (type === 'boolean') {
return Checkbox;
} else {
return Input;
}
},
};
export default Config;

63
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<IProps> = (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 (
<EditModal
visible={visible}
flowChart={flowChart}
onClose={closeModal}
/>
);
};
interface IEditModalProps {
visible: boolean;
flowChart: Graph;
onClose: () => void;
}
const EditModal: React.FC<IEditModalProps> = (props): JSX.Element => {
const { visible, flowChart, onClose } = props;
return (
<Modal
width={1000}
centered={true}
bodyStyle={{ padding: 0, height: 650 }}
title={'执行代码'}
visible={visible}
footer={null}
onCancel={onClose}
>
<CodeRun flowChart={flowChart}/>
</Modal>
);
};
export default TestCase;

2
packages/core/src/mods/toolBar/widgets/bgColor.tsx

@ -28,7 +28,7 @@ const options = {
const bgColor = options.getCurBgColor(flowChart);
return (
<div className={styles.bgColorContainer}>
<XIcon className={styles.fillIcon} type={'icon-tianchong'} />
<XIcon className={styles.fillIcon} type={'icon-fill'} />
<div className={styles.colorPreview} style={{ backgroundColor: bgColor }} />
</div>
);

9
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<IProps> = makeBtnWidget({
tooltip: '置于底层',
handler: shortcuts.bringToBack.handler,
getIcon() {
return <XIcon type={'icon-bringtobottom'} />;
},
handler(flowChart: Graph) {
getSelectedNodes(flowChart).forEach((node) => node.toBack());
return <XIcon type={'icon-bring-to-bottom'} />;
},
disabled(flowChart: Graph) {
return !hasNodeSelected(flowChart);

9
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<IProps> = makeBtnWidget({
tooltip: '置于顶层',
handler: shortcuts.bringToTop.handler,
getIcon() {
return <XIcon type={'icon-bringtotop'} />;
},
handler(flowChart: Graph) {
getSelectedNodes(flowChart).forEach((node) => node.toFront());
return <XIcon type={'icon-bring-to-top'} />;
},
disabled(flowChart: Graph) {
return !hasNodeSelected(flowChart);

2
packages/core/src/mods/toolBar/widgets/textColor.tsx

@ -28,7 +28,7 @@ const options = {
const textColor = options.getCurTextColor(flowChart);
return (
<div className={styles.textColorContainer}>
<XIcon className={styles.textIcon} type={'icon-A'} />
<XIcon className={styles.textIcon} type={'icon-font'} />
<div className={styles.colorPreview} style={{ backgroundColor: textColor }} />
</div>
);

10
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<FRProps> { }
export default FRGenerator;
}

49
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<string[]> => {
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;

8
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))
};
}

23
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);
};

Loading…
Cancel
Save