From bd3e76ad76bdab13ea8e2f8e22fb7a248fea1a86 Mon Sep 17 00:00:00 2001 From: jerryjiang Date: Thu, 3 Aug 2023 19:04:41 +0800 Subject: [PATCH 1/2] feat(extension): new bpmn plugin --- packages/extension/jest.config.js | 198 +++ .../src/bpmn-elements-adapter/README.md | 295 +++++ .../__tests__/adapter_in.test.js | 526 ++++++++ .../__tests__/adapter_out.test.js | 569 +++++++++ .../src/bpmn-elements-adapter/constant.ts | 76 ++ .../src/bpmn-elements-adapter/index.ts | 1112 +++++++++++++++++ .../src/bpmn-elements-adapter/json2xml.ts | 91 ++ .../src/bpmn-elements-adapter/xml2json.ts | 549 ++++++++ .../extension/src/bpmn-elements/README.md | 219 ++++ .../__tests__/definition.test.js | 74 ++ .../extension/src/bpmn-elements/index.d.ts | 26 + packages/extension/src/bpmn-elements/index.ts | 99 ++ .../presets/Event/EndEventFactory.ts | 110 ++ .../presets/Event/IntermediateCatchEvent.ts | 103 ++ .../presets/Event/IntermediateThrowEvent.ts | 104 ++ .../presets/Event/StartEventFactory.ts | 110 ++ .../presets/Event/boundaryEventFactory.ts | 111 ++ .../src/bpmn-elements/presets/Event/index.ts | 14 + .../src/bpmn-elements/presets/Flow/flow.d.ts | 6 + .../src/bpmn-elements/presets/Flow/index.ts | 8 + .../bpmn-elements/presets/Flow/manhattan.ts | 591 +++++++++ .../presets/Flow/sequenceFlow.ts | 63 + .../bpmn-elements/presets/Gateway/gateway.ts | 103 ++ .../bpmn-elements/presets/Gateway/index.ts | 14 + .../src/bpmn-elements/presets/Pool/Lane.ts | 189 +++ .../src/bpmn-elements/presets/Pool/Pool.ts | 286 +++++ .../src/bpmn-elements/presets/Pool/index.ts | 86 ++ .../src/bpmn-elements/presets/Task/index.ts | 101 ++ .../bpmn-elements/presets/Task/subProcess.ts | 172 +++ .../src/bpmn-elements/presets/Task/task.ts | 187 +++ .../src/bpmn-elements/presets/icons.ts | 141 +++ packages/extension/src/bpmn-elements/utils.ts | 26 + packages/extension/src/index.ts | 4 +- 33 files changed, 6362 insertions(+), 1 deletion(-) create mode 100644 packages/extension/jest.config.js create mode 100644 packages/extension/src/bpmn-elements-adapter/README.md create mode 100644 packages/extension/src/bpmn-elements-adapter/__tests__/adapter_in.test.js create mode 100644 packages/extension/src/bpmn-elements-adapter/__tests__/adapter_out.test.js create mode 100644 packages/extension/src/bpmn-elements-adapter/constant.ts create mode 100644 packages/extension/src/bpmn-elements-adapter/index.ts create mode 100644 packages/extension/src/bpmn-elements-adapter/json2xml.ts create mode 100644 packages/extension/src/bpmn-elements-adapter/xml2json.ts create mode 100644 packages/extension/src/bpmn-elements/README.md create mode 100644 packages/extension/src/bpmn-elements/__tests__/definition.test.js create mode 100644 packages/extension/src/bpmn-elements/index.d.ts create mode 100644 packages/extension/src/bpmn-elements/index.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Event/EndEventFactory.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Event/IntermediateCatchEvent.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Event/IntermediateThrowEvent.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Event/StartEventFactory.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Event/boundaryEventFactory.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Event/index.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Flow/flow.d.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Flow/index.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Flow/manhattan.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Flow/sequenceFlow.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Gateway/gateway.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Gateway/index.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Pool/Lane.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Pool/Pool.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Pool/index.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Task/index.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Task/subProcess.ts create mode 100644 packages/extension/src/bpmn-elements/presets/Task/task.ts create mode 100644 packages/extension/src/bpmn-elements/presets/icons.ts create mode 100644 packages/extension/src/bpmn-elements/utils.ts diff --git a/packages/extension/jest.config.js b/packages/extension/jest.config.js new file mode 100644 index 000000000..1c01cb57c --- /dev/null +++ b/packages/extension/jest.config.js @@ -0,0 +1,198 @@ +/* eslint-disable max-len */ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/en/configuration.html + */ + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/xw/v6yjfmzn2hvglj9fm0cb4xbm0000ks/T/jest_gp", + + // Automatically clear mock calls and instances between every test + // clearMocks: false, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: 'v8', + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + moduleNameMapper: { + '^lodash-es$': 'lodash', + }, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: undefined, + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: 'jest-environment-jsdom', + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/packages/extension/src/bpmn-elements-adapter/README.md b/packages/extension/src/bpmn-elements-adapter/README.md new file mode 100644 index 000000000..7ebc7dffa --- /dev/null +++ b/packages/extension/src/bpmn-elements-adapter/README.md @@ -0,0 +1,295 @@ +## what's the difference? + +和旧的bpmn-adapter相比,新的bpmn-adapter在调用adapterIn和adapterOut时,可以额外传入一个对象作为入参 + +``` ts +type ExtraPropsType = { + /** + * retainedAttrsFields retainedAttrsFields会和默认的defaultRetainedProperties: + * ["properties", "startPoint", "endPoint", "pointsList"]合并 + * 这意味着出现在这个数组里的字段当它的值是数组或是对象时不会被视为一个节点而是一个属性 + */ + retainedAttrsFields?: string[]; + + /** + * excludeFields会和默认的defaultExcludeFields合并, + * 出现在这个数组中的字段在导出时会被忽略 + */ + excludeFields?: { + in?: Set; + out?: Set; + }; + + /** + * transformer是一个数组,数组中的每一项都是一个对象,对象中包含in和out两个属性 + * in函数接收两个参数key 和 data,key为当前处理对象的key也就是节点名称,data为当前对象,当导入时会调用这个函数,对被导入数据进行处理,得到我们期望的数据 + * out函数接收一个参数data,data为当前处理节点数据,当导出时会调用这个函数,对需要导出的数据进行处理,得到我们期望的数据 + */ + transformer?: { + [key: string]: { + in?: (key: string, data: any) => any; + out?: (data: any) => any; + } + }; + + mapping?: { + in?: { + [key: string]: string; + }; + out?: { + [key: string]: string; + }; + }; +}; +``` + +## 使用 + +``` ts +// step 1 引入adapter插件 +import { BpmnXmlAdapterV2 } from '@logicflow/extension'; +// step 2 注册插件 +LogicFlow.use(BpmnXmlAdapterV2); +... +// step 3-1 调用默认导出 +const xmlResult = lf.adapterOut(lf.getGraphRawData()) +``` + +很多时候,我们可能需要一些额外的参数帮助我们正确的进行导入导出,这时我们需要用到上面提到的adapterOut额外的入参ExtraPropsType + +``` ts +// step 3-2 使用adapterOut的第二个参数ExtraPropsType导出 + +// 假如我们节点属性中有一个panels属性,它是一个数组,但是我们不希望它被视为一个节点,而是一个属性,那么我们可以这样做 +const extraProps = { + retainedAttrsFields: ['panels'] +} +// 假如我们节点属性中有一个runboost属性,但是我们希望导出时不包含这个属性,那么我们可以这样做 +extraProps.excludeFields = { + out: new Set(['runboost']) +} +``` + +假如我们要将包含判断条件的顺序流(sequenceFlow)以正确的XML格式导出,并且导入时正确处理得到我们想要的数据,那么我们可以这样做: + +``` xml + + + + + + + + + + + + + + + runboost === '1' + + +``` + +``` ts +/** + * 以包含判断条件的顺序流为例, + * 在进行导入时,我们需要把的子内容内的属性提取出来, + * 最终放入父元素bpmn:sequenceFlow的properties属性中 + * 所以导入的时候我们实际需要处理的中的内容, + * 它被处理后,数据会被合入bpmn:sequenceFlow的properties属性中 + */ +extraProps.transformer = { + 'bpmn:sequenceFlow': { + out(data: any) { + const { properties: { expressionType, condition } } = data; + if (condition) { + if (expressionType === 'cdata') { + return { + json: + ``, + }; + } + return { + json: `${condition}`, + }; + } + return { + json: '', + }; + }, + }, + // 返回的数据会被合并进父元素bpmn:sequenceFlow的properties属性中 + 'bpmn:conditionExpression': { + in(_key: string, data: any) { + let condition = ''; + let expressionType = ''; + if (data['#cdata-section']) { + expressionType = 'cdata'; + condition = /^\$\{(.*)\}$/g.exec(data['#cdata-section'])?.[1] || ''; + } else if (data['#text']) { + expressionType = 'normal'; + condition = data['#text']; + } + return { + '-condition': condition, + '-expressionType': expressionType, + }; + }, + }, +} +``` + +***数据导入导出的原则是:导出时处理父元素;导入时处理子元素。*** + +- 导出时,需要从父元素的属性中拿到子元素需要到数据并拼接出子元素。 +- 导入时,需要从子元素中提取我们需要的数据放入父元素的属性中。 + +在配置完需要的extraProps之后,我们就可以调用adapterOut方法导出数据。 + +``` ts + +const xmlResult = lf.adapterOut(lf.getGraphRawData(), extraProps) +``` + +目前,我们内置了一下transformer **(仅做参考)**: + +> 注意:这里内置的transformer仅做参考使用,这些transformer在编写的过程中是用来配合bpmn节点插件的,里面诸如`timerType`, `timerValue`, `definitionId`,都是通过bpmn节点插件的definitionConfig配置的。在实际使用该数据转换插件时,你完全可以不使用bpmn节点插件,通过你自己的方式为节点添加属性,自定义transformer来实现符合你需要的数据转换。 + +```ts + +let defaultTransformer: TransformerType = { + 'bpmn:startEvent': { + out(data: any) { + const { properties } = data; + return defaultTransformer[properties.definitionType]?.out(data) || {}; + }, + }, + 'bpmn:intermediateCatchEvent': { + out(data: any) { + const { properties } = data; + return defaultTransformer[properties.definitionType]?.out(data) || {}; + }, + }, + 'bpmn:intermediateThrowEvent': { + out(data: any) { + const { properties } = data; + return defaultTransformer[properties.definitionType]?.out(data) || {}; + }, + }, + 'bpmn:boundaryEvent': { + out(data: any) { + const { properties } = data; + return defaultTransformer[properties.definitionType]?.out(data) || {}; + }, + }, + 'bpmn:sequenceFlow': { + out(data: any) { + const { + properties: { expressionType, condition }, + } = data; + if (condition) { + if (expressionType === 'cdata') { + return { + json: + ``, + }; + } + return { + json: `${condition}`, + }; + } + return { + json: '', + }; + + }, + }, + 'bpmn:timerEventDefinition': { + out(data: any) { + // 这里的timerType, timerValue, definitionId + // 都是通过通过的Bpmn节点插件扩展节点时,通过definitionConfig配置的属性,实际使用时需要根据自己的设置的属性对out函数进行修改 + const { + properties: { timerType, timerValue, definitionId }, + } = data; + + const typeFunc = () => `${timerValue}`; + + return { + json: + `${typeFunc()}` + : '/>'}`, + }; + }, + in(key: string, data: any) { + const definitionType = key; + const definitionId = data['-id']; + let timerType = ''; + let timerValue = ''; + for (const key of Object.keys(data)) { + if (key.includes('bpmn:')) { + [, timerType] = key.split(':'); + timerValue = data[key]?.['#text']; + } + } + return { + '-definitionId': definitionId, + '-definitionType': definitionType, + '-timerType': timerType, + '-timerValue': timerValue, + }; + }, + }, + 'bpmn:conditionExpression': { + in(_key: string, data: any) { + let condition = ''; + let expressionType = ''; + if (data['#cdata-section']) { + expressionType = 'cdata'; + condition = /^\$\{(.*)\}$/g.exec(data['#cdata-section'])?.[1] || ''; + } else if (data['#text']) { + expressionType = 'normal'; + condition = data['#text']; + } + + return { + '-condition': condition, + '-expressionType': expressionType, + }; + }, + }, +}; + +``` + +传入的transformer和默认的transformer会通过 + +```ts + +const mergeInNOutObject = (target: any, source: any): TransformerType => { + const sourceKeys = Object.keys(source); + sourceKeys.forEach((key) => { + if (target[key]) { + const { in: fnIn, out: fnOut } = source[key]; + if (fnIn) { + target[key].in = fnIn; + } + if (fnOut) { + target[key].out = fnOut; + } + } else { + target[key] = source[key]; + } + }); + return target; +}; +``` + +进行合并 diff --git a/packages/extension/src/bpmn-elements-adapter/__tests__/adapter_in.test.js b/packages/extension/src/bpmn-elements-adapter/__tests__/adapter_in.test.js new file mode 100644 index 000000000..ac0f25077 --- /dev/null +++ b/packages/extension/src/bpmn-elements-adapter/__tests__/adapter_in.test.js @@ -0,0 +1,526 @@ +/* eslint-disable no-useless-concat */ +/* eslint-disable no-template-curly-in-string */ +/* eslint-disable no-new */ +/* eslint-disable no-undef */ +/* eslint-disable no-tabs */ +import { BPMNAdapter } from '..'; + +describe('Test BPMNAdapter: import xml', () => { + const graphData = { + nodes: [ + { + id: 'Event_0rqndvp', + type: 'bpmn:startEvent', + x: 350, + y: 110, + properties: {}, + text: { + x: 350, + y: 150, + value: '开始', + }, + }, + { + id: '121213b3-8fad-4b41-bb1e-a7672e9bfc07', + type: 'bpmn:subProcess', + x: 640, + y: 530, + properties: {}, + children: [ + 'Activity_383p4ds', + 'Event_3nm6g45', + 'Gateway_10p8112', + 'Gateway_36vu52v', + ], + }, + { + id: 'Event_2ffv4vc', + type: 'bpmn:boundaryEvent', + x: 220, + y: 570, + properties: { + attachedToRef: 'Activity_05avavm', + cancelActivity: false, + definitionType: 'bpmn:timerEventDefinition', + timerValue: '', + timerType: '', + definitionId: 'bpmn:timerEventDefinitionEventDefinition_0anvuso', + isBoundaryEvent: true, + }, + text: { + x: 220, + y: 610, + value: '时间边界', + }, + }, + { + id: 'Event_2o2l6ht', + type: 'bpmn:boundaryEvent', + x: 310, + y: 320, + properties: { + attachedToRef: 'Activity_28r64ai', + cancelActivity: false, + definitionType: 'bpmn:timerEventDefinition', + timerValue: 'PT15S', + timerType: 'timeDuration', + definitionId: 'bpmn:timerEventDefinitionEventDefinition_11s0ei9', + isBoundaryEvent: true, + }, + text: { + x: 310, + y: 360, + value: '时间边界', + }, + }, + { + id: 'Event_3nm6g45', + type: 'bpmn:boundaryEvent', + x: 710, + y: 530, + properties: { + attachedToRef: 'Activity_383p4ds', + cancelActivity: false, + definitionType: 'bpmn:timerEventDefinition', + timerValue: 'R5/PT10S', + timerType: 'timeCycle', + definitionId: 'bpmn:timerEventDefinitionEventDefinition_0ukj8qs', + isBoundaryEvent: true, + }, + text: { + x: 710, + y: 570, + value: '时间边界', + }, + }, + { + id: 'Gateway_0ke5iid', + type: 'bpmn:parallelGateway', + x: 500, + y: 140, + properties: {}, + text: { + x: 500, + y: 180, + value: '并行网关', + }, + }, + { + id: 'Gateway_10p8112', + type: 'bpmn:parallelGateway', + x: 490, + y: 530, + properties: { + expr: '${A > B}', + }, + text: { + x: 490, + y: 570, + value: '并行网关', + }, + }, + { + id: 'Activity_05avavm', + type: 'bpmn:userTask', + x: 270, + y: 540, + properties: {}, + text: { + x: 270, + y: 540, + value: '人工任务', + }, + }, + { + id: 'Activity_28r64ai', + type: 'bpmn:userTask', + x: 370, + y: 280, + properties: {}, + text: { + x: 370, + y: 280, + value: '人工任务', + }, + }, + { + id: 'Event_3t9u7bs', + type: 'bpmn:endEvent', + x: 220, + y: 210, + properties: {}, + text: { + x: 220, + y: 250, + value: '结束', + }, + }, + { + id: 'Activity_383p4ds', + type: 'bpmn:serviceTask', + x: 760, + y: 530, + properties: {}, + text: { + x: 760, + y: 530, + value: '服务任务', + }, + }, + { + id: 'Gateway_36vu52v', + type: 'bpmn:inclusiveGateway', + x: 640, + y: 580, + properties: {}, + text: { + x: 640, + y: 620, + value: '包容网关', + }, + }, + ], + edges: [ + { + id: 'Flow_19ep598', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Gateway_0ke5iid', + targetNodeId: 'Event_3t9u7bs', + properties: {}, + pointsList: [ + { + x: 475, + y: 140, + }, + { + x: 445, + y: 140, + }, + { + x: 445, + y: 210, + }, + { + x: 238, + y: 210, + }, + ], + }, + { + id: 'Flow_1cju7v0', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Gateway_0ke5iid', + targetNodeId: '121213b3-8fad-4b41-bb1e-a7672e9bfc07', + properties: {}, + pointsList: [ + { + x: 500, + y: 165, + }, + { + x: 500, + y: 195, + }, + { + x: 640, + y: 195, + }, + { + x: 640, + y: 430, + }, + ], + }, + { + id: 'Flow_0phuver', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Activity_05avavm', + targetNodeId: 'Activity_28r64ai', + properties: {}, + pointsList: [ + { + x: 220, + y: 540, + }, + { + x: 190, + y: 540, + }, + { + x: 190, + y: 410, + }, + { + x: 450, + y: 410, + }, + { + x: 450, + y: 280, + }, + { + x: 420, + y: 280, + }, + ], + }, + { + id: 'Flow_3ql1931', + type: 'bpmn:sequenceFlow', + sourceNodeId: '121213b3-8fad-4b41-bb1e-a7672e9bfc07', + targetNodeId: 'Activity_05avavm', + properties: {}, + pointsList: [ + { + x: 440, + y: 530, + }, + { + x: 380, + y: 530, + }, + { + x: 380, + y: 540, + }, + { + x: 320, + y: 540, + }, + ], + }, + { + id: 'Flow_39cdevi', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Gateway_10p8112', + targetNodeId: 'Activity_383p4ds', + properties: { + expressionType: 'cdata', + condition: 'foo > bar', + }, + pointsList: [ + { + x: 515, + y: 530, + }, + { + x: 545, + y: 530, + }, + { + x: 545, + y: 460, + }, + { + x: 760, + y: 460, + }, + { + x: 760, + y: 490, + }, + ], + }, + { + id: 'Flow_1mpq63n', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Gateway_10p8112', + targetNodeId: 'Gateway_36vu52v', + properties: { + isDefaultFlow: true, + }, + pointsList: [ + { + x: 515, + y: 530, + }, + { + x: 565, + y: 530, + }, + { + x: 565, + y: 580, + }, + { + x: 615, + y: 580, + }, + ], + }, + ], + }; + const xml = ` + + + + Flow_1cju7v0 + Flow_3ql1931 + + Flow_39cdevi + + + R5/PT10S + + + Flow_39cdevi + Flow_1mpq63n + + + Flow_1mpq63n + + + + + + + + + + + PT15S + + + Flow_19ep598 + Flow_1cju7v0 + + + Flow_3ql1931 + Flow_0phuver + + + Flow_0phuver + + + Flow_19ep598 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + const lf = {}; + const adapter = new BPMNAdapter({ lf }); + + // 导入时会处理内部会出boundaryEvents(配合Bpmn节点插件中的task、subProcess使用)但是在这里不需要,所以需要extraProps来过滤掉它 + it('should convert bpmn to graph data', () => { + expect(adapter.adapterXmlIn(xml, { + excludeFields: { + in: ['properties.boundaryEvents'], + }, + })).toEqual(graphData); + }); +}); diff --git a/packages/extension/src/bpmn-elements-adapter/__tests__/adapter_out.test.js b/packages/extension/src/bpmn-elements-adapter/__tests__/adapter_out.test.js new file mode 100644 index 000000000..44bcf5df7 --- /dev/null +++ b/packages/extension/src/bpmn-elements-adapter/__tests__/adapter_out.test.js @@ -0,0 +1,569 @@ +/* eslint-disable no-useless-concat */ +/* eslint-disable no-template-curly-in-string */ +/* eslint-disable no-new */ +/* eslint-disable no-undef */ +/* eslint-disable no-tabs */ +import { BPMNAdapter } from '..'; + +describe('Test BPMNAdapter: export xml', () => { + const graphData = { + nodes: [ + { + id: 'Event_0rqndvp', + type: 'bpmn:startEvent', + x: 350, + y: 110, + properties: {}, + text: { + x: 350, + y: 150, + value: '开始', + }, + }, + { + id: '121213b3-8fad-4b41-bb1e-a7672e9bfc07', + type: 'bpmn:subProcess', + x: 640, + y: 530, + properties: {}, + children: [ + 'Activity_383p4ds', + 'Event_3nm6g45', + 'Gateway_10p8112', + 'Gateway_36vu52v', + ], + }, + { + id: 'Event_2ffv4vc', + type: 'bpmn:boundaryEvent', + x: 220, + y: 570, + properties: { + attachedToRef: 'Activity_05avavm', + cancelActivity: false, + definitionType: 'bpmn:timerEventDefinition', + timerValue: '', + timerType: '', + definitionId: 'bpmn:timerEventDefinitionEventDefinition_0anvuso', + isBoundaryEvent: true, + }, + text: { + x: 220, + y: 610, + value: '时间边界', + }, + }, + { + id: 'Event_2o2l6ht', + type: 'bpmn:boundaryEvent', + x: 310, + y: 320, + properties: { + attachedToRef: 'Activity_28r64ai', + cancelActivity: false, + definitionType: 'bpmn:timerEventDefinition', + timerValue: 'PT15S', + timerType: 'timeDuration', + definitionId: 'bpmn:timerEventDefinitionEventDefinition_11s0ei9', + isBoundaryEvent: true, + }, + text: { + x: 310, + y: 360, + value: '时间边界', + }, + }, + { + id: 'Event_3nm6g45', + type: 'bpmn:boundaryEvent', + x: 710, + y: 530, + properties: { + attachedToRef: 'Activity_383p4ds', + cancelActivity: false, + definitionType: 'bpmn:timerEventDefinition', + timerValue: 'R5/PT10S', + timerType: 'timeCycle', + definitionId: 'bpmn:timerEventDefinitionEventDefinition_0ukj8qs', + isBoundaryEvent: true, + }, + text: { + x: 710, + y: 570, + value: '时间边界', + }, + }, + { + id: 'Gateway_0ke5iid', + type: 'bpmn:parallelGateway', + x: 500, + y: 140, + properties: {}, + text: { + x: 500, + y: 180, + value: '并行网关', + }, + }, + { + id: 'Gateway_10p8112', + type: 'bpmn:parallelGateway', + x: 490, + y: 530, + properties: { + expr: '${A > B}', + }, + text: { + x: 490, + y: 570, + value: '并行网关', + }, + }, + { + id: 'Activity_05avavm', + type: 'bpmn:userTask', + x: 270, + y: 540, + properties: {}, + text: { + x: 270, + y: 540, + value: '人工任务', + }, + }, + { + id: 'Activity_28r64ai', + type: 'bpmn:userTask', + x: 370, + y: 280, + properties: {}, + text: { + x: 370, + y: 280, + value: '人工任务', + }, + }, + { + id: 'Event_3t9u7bs', + type: 'bpmn:endEvent', + x: 220, + y: 210, + properties: {}, + text: { + x: 220, + y: 250, + value: '结束', + }, + }, + { + id: 'Activity_383p4ds', + type: 'bpmn:serviceTask', + x: 760, + y: 530, + properties: {}, + text: { + x: 760, + y: 530, + value: '服务任务', + }, + }, + { + id: 'Gateway_36vu52v', + type: 'bpmn:inclusiveGateway', + x: 640, + y: 580, + properties: {}, + text: { + x: 640, + y: 620, + value: '包容网关', + }, + }, + ], + edges: [ + { + id: 'Flow_19ep598', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Gateway_0ke5iid', + targetNodeId: 'Event_3t9u7bs', + startPoint: { + x: 475, + y: 140, + }, + endPoint: { + x: 238, + y: 210, + }, + properties: {}, + pointsList: [ + { + x: 475, + y: 140, + }, + { + x: 445, + y: 140, + }, + { + x: 445, + y: 210, + }, + { + x: 238, + y: 210, + }, + ], + }, + { + id: 'Flow_1cju7v0', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Gateway_0ke5iid', + targetNodeId: '121213b3-8fad-4b41-bb1e-a7672e9bfc07', + startPoint: { + x: 500, + y: 165, + }, + endPoint: { + x: 640, + y: 430, + }, + properties: {}, + pointsList: [ + { + x: 500, + y: 165, + }, + { + x: 500, + y: 195, + }, + { + x: 640, + y: 195, + }, + { + x: 640, + y: 430, + }, + ], + }, + { + id: 'Flow_0phuver', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Activity_05avavm', + targetNodeId: 'Activity_28r64ai', + startPoint: { + x: 270, + y: 500, + }, + endPoint: { + x: 370, + y: 320, + }, + properties: {}, + pointsList: [ + { + x: 220, + y: 540, + }, + { + x: 190, + y: 540, + }, + { + x: 190, + y: 410, + }, + { + x: 450, + y: 410, + }, + { + x: 450, + y: 280, + }, + { + x: 420, + y: 280, + }, + ], + }, + { + id: 'Flow_3ql1931', + type: 'bpmn:sequenceFlow', + sourceNodeId: '121213b3-8fad-4b41-bb1e-a7672e9bfc07', + targetNodeId: 'Activity_05avavm', + startPoint: { + x: 440, + y: 530, + }, + endPoint: { + x: 320, + y: 540, + }, + properties: {}, + pointsList: [ + { + x: 440, + y: 530, + }, + { + x: 380, + y: 530, + }, + { + x: 380, + y: 540, + }, + { + x: 320, + y: 540, + }, + ], + }, + { + id: 'Flow_39cdevi', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Gateway_10p8112', + targetNodeId: 'Activity_383p4ds', + startPoint: { + x: 515, + y: 530, + }, + endPoint: { + x: 710, + y: 530, + }, + properties: { + expressionType: 'cdata', + condition: 'foo > bar', + }, + pointsList: [ + { + x: 515, + y: 530, + }, + { + x: 545, + y: 530, + }, + { + x: 545, + y: 460, + }, + { + x: 760, + y: 460, + }, + { + x: 760, + y: 490, + }, + ], + }, + { + id: 'Flow_1mpq63n', + type: 'bpmn:sequenceFlow', + sourceNodeId: 'Gateway_10p8112', + targetNodeId: 'Gateway_36vu52v', + startPoint: { + x: 515, + y: 530, + }, + endPoint: { + x: 615, + y: 580, + }, + properties: { + isDefaultFlow: true, + }, + pointsList: [ + { + x: 515, + y: 530, + }, + { + x: 565, + y: 530, + }, + { + x: 565, + y: 580, + }, + { + x: 615, + y: 580, + }, + ], + }, + ], + }; + const xml = ` + + + + Flow_1cju7v0 + Flow_3ql1931 + + Flow_39cdevi + + + R5/PT10S + + + Flow_39cdevi + Flow_1mpq63n + + + Flow_1mpq63n + + + + + + + + + + + PT15S + + + Flow_19ep598 + Flow_1cju7v0 + + + Flow_3ql1931 + Flow_0phuver + + + Flow_0phuver + + + Flow_19ep598 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + const lf = {}; + const adapter = new BPMNAdapter({ lf }); + + it('should transform logic-flow graph data to bpmn xml', () => { + expect(adapter.adapterXmlOut(graphData)).toEqual(xml); + }); +}); diff --git a/packages/extension/src/bpmn-elements-adapter/constant.ts b/packages/extension/src/bpmn-elements-adapter/constant.ts new file mode 100644 index 000000000..5bfbaed3f --- /dev/null +++ b/packages/extension/src/bpmn-elements-adapter/constant.ts @@ -0,0 +1,76 @@ +export const StartEventConfig = { + width: 40, + height: 40, +}; + +export const EndEventConfig = { + width: 40, + height: 40, +}; + +export const BoundaryEventConfig = { + width: 100, + height: 80, +}; + +export const IntermediateEventConfig = { + width: 100, + height: 80, +}; + +export const ParallelGatewayConfig = { + width: 100, + height: 80, +}; + +export const InclusiveGatewayConfig = { + width: 100, + height: 80, +}; + +export const ExclusiveGatewayConfig = { + width: 100, + height: 80, +}; + +export const ServiceTaskConfig = { + width: 100, + height: 80, +}; + +export const UserTaskConfig = { + width: 100, + height: 80, +}; + +export const SubProcessConfig = { + width: 100, + height: 80, +}; + +export const theme = { + rect: { + radius: 5, + stroke: 'rgb(24, 125, 255)', + }, + circle: { + r: 18, + stroke: 'rgb(24, 125, 255)', + }, + polygon: { + stroke: 'rgb(24, 125, 255)', + }, + polyline: { + stroke: 'rgb(24, 125, 255)', + hoverStroke: 'rgb(24, 125, 255)', + selectedStroke: 'rgb(24, 125, 255)', + }, + edgeText: { + background: { + fill: 'white', + height: 14, + stroke: 'transparent', + radius: 3, + }, + }, +}; diff --git a/packages/extension/src/bpmn-elements-adapter/index.ts b/packages/extension/src/bpmn-elements-adapter/index.ts new file mode 100644 index 000000000..855ff8565 --- /dev/null +++ b/packages/extension/src/bpmn-elements-adapter/index.ts @@ -0,0 +1,1112 @@ +/* eslint-disable func-names */ +/* eslint-disable no-continue */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable no-cond-assign */ +/* eslint-disable no-shadow */ +import _ from 'lodash-es'; +import { + ExclusiveGatewayConfig, + InclusiveGatewayConfig, + ParallelGatewayConfig, + StartEventConfig, + EndEventConfig, + BoundaryEventConfig, + IntermediateEventConfig, + ServiceTaskConfig, + UserTaskConfig, + SubProcessConfig, +} from './constant'; +import { lfXml2Json } from './xml2json'; +import { lfJson2Xml, handleAttributes } from './json2xml'; + +type NodeConfig = { + id: string; + properties?: Record; + text?: { + x: number; + y: number; + value: string; + }; + type: string; + x: number; + y: number; + children?: string[]; +}; + +type Point = { + x: number; + y: number; +}; + +type EdgeConfig = { + id: string; + sourceNodeId: string; + targetNodeId: string; + type: string; + startPoint?: { + x: number; + y: number; + }; + endPoint?: { + x: number; + y: number; + }; + text?: { + x: number; + y: number; + value: string; + }; + pointsList?: Point[]; + properties: Record; +}; + +type TransformerType = { + [key: string]: { + in?: (key: string, data: any) => any; + out?: (data: any) => any; + }; +}; + +type MappingType = { + in?: { + [key: string]: string; + }; + out?: { + [key: string]: string; + }; +}; + +type excludeFieldsType = { + in?: Set; + out?: Set; +}; + +type ExtraPropsType = { + retainedAttrsFields?: string[]; + excludeFields?: excludeFieldsType; + transformer?: TransformerType; + mapping?: MappingType; +}; + +enum BpmnElements { + START = 'bpmn:startEvent', + END = 'bpmn:endEvent', + INTERMEDIATE_CATCH = 'bpmn:intermediateCatchEvent', + INTERMEDIATE_THROW = 'bpmn:intermediateThrowEvent', + BOUNDARY = 'bpmn:boundaryEvent', + PARALLEL_GATEWAY = 'bpmn:parallelGateway', + INCLUSIVE_GATEWAY = 'bpmn:inclusiveGateway', + EXCLUSIVE_GATEWAY = 'bpmn:exclusiveGateway', + USER = 'bpmn:userTask', + SYSTEM = 'bpmn:serviceTask', + FLOW = 'bpmn:sequenceFlow', + SUBPROCESS = 'bpmn:subProcess', +} + +const defaultAttrsForInput = [ + '-name', + '-id', + 'bpmn:incoming', + 'bpmn:outgoing', + '-sourceRef', + '-targetRef', + '-children', +]; + +const defaultRetainedProperties = [ + 'properties', + 'startPoint', + 'endPoint', + 'pointsList', +]; + +const defaultExcludeFields = { + in: [], + out: ['properties.panels'], +}; + +const mergeInNOutObject = (target: any, source: any): TransformerType => { + const sourceKeys = Object.keys(source || {}); + sourceKeys.forEach((key) => { + if (target[key]) { + const { in: fnIn, out: fnOut } = source[key]; + if (fnIn) { + target[key].in = fnIn; + } + if (fnOut) { + target[key].out = fnOut; + } + } else { + target[key] = source[key]; + } + }); + return target; +}; + +// @ts-ignore +let defaultTransformer: TransformerType = { + 'bpmn:startEvent': { + out(data: any) { + const { properties } = data; + return defaultTransformer[properties.definitionType]?.out?.(data) || {}; + }, + }, + // 'bpmn:endEvent': undefined, + 'bpmn:intermediateCatchEvent': { + out(data: any) { + const { properties } = data; + return defaultTransformer[properties.definitionType]?.out?.(data) || {}; + }, + }, + 'bpmn:intermediateThrowEvent': { + out(data: any) { + const { properties } = data; + return defaultTransformer[properties.definitionType]?.out?.(data) || {}; + }, + }, + 'bpmn:boundaryEvent': { + out(data: any) { + const { properties } = data; + return defaultTransformer[properties.definitionType]?.out?.(data) || {}; + }, + }, + // 'bpmn:userTask': undefined, + 'bpmn:sequenceFlow': { + out(data: any) { + const { + properties: { expressionType, condition }, + } = data; + if (condition) { + if (expressionType === 'cdata') { + return { + json: + ``, + }; + } + return { + json: `${condition}`, + }; + } + return { + json: '', + }; + + }, + }, + // 'bpmn:subProcess': undefined, + // 'bpmn:participant': undefined, + 'bpmn:timerEventDefinition': { + out(data: any) { + const { + properties: { timerType, timerValue, definitionId }, + } = data; + + const typeFunc = () => `${timerValue}`; + + return { + json: + `${typeFunc()}` + : '/>'}`, + }; + }, + in(key: string, data: any) { + const definitionType = key; + const definitionId = data['-id']; + let timerType = ''; + let timerValue = ''; + for (const key of Object.keys(data)) { + if (key.includes('bpmn:')) { + [, timerType] = key.split(':'); + timerValue = data[key]?.['#text']; + } + } + return { + '-definitionId': definitionId, + '-definitionType': definitionType, + '-timerType': timerType, + '-timerValue': timerValue, + }; + }, + }, + 'bpmn:conditionExpression': { + in(_key: string, data: any) { + let condition = ''; + let expressionType = ''; + if (data['#cdata-section']) { + expressionType = 'cdata'; + condition = /^\$\{(.*)\}$/g.exec(data['#cdata-section'])?.[1] || ''; + } else if (data['#text']) { + expressionType = 'normal'; + condition = data['#text']; + } + + return { + '-condition': condition, + '-expressionType': expressionType, + }; + }, + }, +}; + +/** + * 将普通json转换为xmlJson + * xmlJson中property会以“-”开头 + * 如果没有“-”表示为子节点 + * fix issue https://github.com/didi/LogicFlow/issues/718, contain the process of #text/#cdata and array + * @reference node type reference https://www.w3schools.com/xml/dom_nodetype.asp + * @param retainedAttrsFields retainedAttrsFields会和默认的defaultRetainedProperties: + * ["properties", "startPoint", "endPoint", "pointsList"]合并 + * 这意味着出现在这个数组里的字段当它的值是数组或是对象时不会被视为一个节点而是一个属性 + * @param excludeFields excludeFields会和默认的defaultExcludeFields合并,出现在这个数组中的字段在转换时会被忽略 + * @param transformer 对应节点或者边的子内容转换规则 + */ +function convertNormalToXml(other?: ExtraPropsType) { + const { retainedAttrsFields, excludeFields, transformer } = other ?? {}; + const retainedAttrsSet = new Set([ + ...defaultRetainedProperties, + ...(retainedAttrsFields || []), + ]); + const excludeFieldsSet = { + in: new Set([...defaultExcludeFields.in, ...(excludeFields?.in || [])]), + out: new Set([...defaultExcludeFields.out, ...(excludeFields?.out || [])]), + }; + defaultTransformer = mergeInNOutObject(defaultTransformer, transformer); + + return (object: { nodes: any; edges: any }) => { + const { nodes } = object; + const { edges } = object; + function ToXmlJson(obj: any, path: string): any { + if (obj?.flag === 1) { + return; + } + + let fn; + // @ts-ignore + if ((fn = defaultTransformer[obj.type]) && fn.out) { + const output = fn.out(obj); + const keys = Object.keys(output); + if (keys.length > 0) { + keys.forEach((key: string) => { + obj[key] = output[key]; + }); + } + } + + if (obj?.children) { + obj.children = obj.children.map((key: any) => { + const target = nodes.find((item: { id: any }) => item.id === key) + || edges.find((item: { id: any }) => item.id === key); + return target || {}; + }); + } + + const xmlJson: any = {}; + + if (typeof obj === 'string') { + return obj; + } + + if (Array.isArray(obj)) { + return obj + .map((item) => ToXmlJson(item, '')) + // eslint-disable-next-line eqeqeq + .filter((item) => item != undefined); + } + + for (const [key, value] of Object.entries(obj)) { + if ((value as any)?.['flag'] === 1) { + return; + } + const newPath = [path, key].filter((item) => item).join('.'); + + if (typeof value !== 'object') { + // node type reference https://www.w3schools.com/xml/dom_nodetype.asp + if ( + key.indexOf('-') === 0 + || ['#text', '#cdata-section', '#comment'].includes(key) + ) { + xmlJson[key] = value; + } else { + xmlJson[`-${key}`] = value; + } + } else if (excludeFieldsSet.out.has(newPath)) { + continue; + } else if (retainedAttrsSet.has(newPath)) { + xmlJson[`-${key}`] = ToXmlJson(value, newPath); + } else { + xmlJson[key] = ToXmlJson(value, newPath); + } + } + + return xmlJson; + } + return ToXmlJson(object, ''); + }; +} + +/** + * 将xmlJson转换为普通的json,在内部使用。 + */ +function convertXmlToNormal(xmlJson: any) { + const json: any = {}; + for (const [key, value] of Object.entries(xmlJson)) { + if (key.indexOf('-') === 0) { + json[key.substring(1)] = handleAttributes(value); + } else if (typeof value === 'string') { + json[key] = value; + } else if (Object.prototype.toString.call(value) === '[object Object]') { + json[key] = convertXmlToNormal(value); + } else if (Array.isArray(value)) { + // contain the process of array + json[key] = value.map((v) => convertXmlToNormal(v)); + } else { + json[key] = value; + } + } + return json; +} + +/** + * 设置bpmn process信息 + * 目标格式请参考examples/bpmn.json + * bpmn因为是默认基于xml格式的,其特点与json存在差异。 + * 1) 如果是xml的属性,json中属性用'-'开头 + * 2)如果只有一个子元素,json中表示为正常属性 + * 3)如果是多个子元素,json中使用数组存储 + */ +function convertLf2ProcessData( + bpmnData: any, + data: any, + other?: ExtraPropsType, +) { + const nodeIdMap = new Map(); + + const xmlJsonData = convertNormalToXml(other)(data); + + xmlJsonData.nodes.forEach((node: any) => { + const { + '-id': nodeId, + '-type': nodeType, + text, + children, + ...otherProps + } = node; + const processNode: any = { '-id': nodeId }; + + if (text?.['-value']) { + processNode['-name'] = text['-value']; + } + + if (otherProps['-json']) { + processNode['-json'] = otherProps['-json']; + } + + if (otherProps['-properties']) { + Object.assign(processNode, otherProps['-properties']); + } + + if (children) { + processNode.children = children; + } + + // (bpmnData[nodeType] ??= []).push(processNode); + + if (!bpmnData[nodeType]) { + bpmnData[nodeType] = []; + } + bpmnData[nodeType].push(processNode); + + nodeIdMap.set(nodeId, processNode); + }); + + const sequenceFlow = xmlJsonData.edges.map((edge: any) => { + const { + '-id': id, + '-type': type, + '-sourceNodeId': sourceNodeId, + '-targetNodeId': targetNodeId, + text, + ...otherProps + } = edge; + const targetNode = nodeIdMap.get(targetNodeId); + // (targetNode['bpmn:incoming'] ??= []).push(id); + + if (!targetNode['bpmn:incoming']) { + targetNode['bpmn:incoming'] = []; + } + targetNode['bpmn:incoming'].push(id); + + const edgeConfig: any = { + '-id': id, + '-sourceRef': sourceNodeId, + '-targetRef': targetNodeId, + }; + + if (text?.['-value']) { + edgeConfig['-name'] = text['-value']; + } + + if (otherProps['-json']) { + edgeConfig['-json'] = otherProps['-json']; + } + + if (otherProps['-properties']) { + Object.assign(edgeConfig, otherProps['-properties']); + } + + return edgeConfig; + }); + + // @see https://github.com/didi/LogicFlow/issues/325 + // 需要保证incoming在outgoing之前 + data.edges.forEach(({ sourceNodeId, id }: any) => { + const sourceNode = nodeIdMap.get(sourceNodeId); + // (sourceNode['bpmn:outgoing'] ??= []).push(id); + + if (!sourceNode['bpmn:outgoing']) { + sourceNode['bpmn:outgoing'] = []; + } + sourceNode['bpmn:outgoing'].push(id); + }); + + bpmnData['bpmn:subProcess']?.forEach((item: any) => { + const setMap: any = { + 'bpmn:incoming': new Set(), + 'bpmn:outgoing': new Set(), + }; + const edgesInSubProcess: any = []; + item.children.forEach((child: any) => { + const target = nodeIdMap.get(child['-id']); + ['bpmn:incoming', 'bpmn:outgoing'].forEach((key: string) => { + target[key] + && target[key].forEach((value: string) => { + setMap[key].add(value); + }); + }); + + const index = bpmnData[child['-type']]?.findIndex( + (_item: any) => _item['-id'] === child['-id'], + ); + if (index >= 0) { + bpmnData[child['-type']].splice(index, 1); + } + + nodeIdMap.delete(child['-id']); + + // (item[child['-type']] ??= []).push(target); + if (!item[child['-type']]) { + item[child['-type']] = []; + } + item[child['-type']].push(target); + }); + + const { 'bpmn:incoming': incomingSet, 'bpmn:outgoing': outgoingSet } = setMap; + + outgoingSet.forEach((value: string) => { + incomingSet.has(value) && edgesInSubProcess.push(value); + }); + + for (let i = 0; i < edgesInSubProcess.length;) { + const index = sequenceFlow.findIndex( + (item: any) => item['-id'] === edgesInSubProcess[i], + ); + if (index >= 0) { + // (item['bpmn:sequenceFlow'] ??= []).push(sequenceFlow[index]); + if (!item['bpmn:sequenceFlow']) { + item['bpmn:sequenceFlow'] = []; + } + item['bpmn:sequenceFlow'].push(sequenceFlow[index]); + sequenceFlow.splice(index, 1); + } else { + i++; + } + } + + delete item.children; + }); + + bpmnData[BpmnElements.FLOW] = sequenceFlow; + + return bpmnData; +} + +/** + * adapterOut 设置bpmn diagram信息 + */ +function convertLf2DiagramData(bpmnDiagramData: any, data: any) { + bpmnDiagramData['bpmndi:BPMNEdge'] = data.edges.map((edge: any) => { + const edgeId = edge.id; + const pointsList = edge.pointsList.map( + ({ x, y }: { x: number; y: number }) => ({ + '-x': x, + '-y': y, + }), + ); + const diagramData: any = { + '-id': `${edgeId}_di`, + '-bpmnElement': edgeId, + 'di:waypoint': pointsList, + }; + if (edge.text?.value) { + diagramData['bpmndi:BPMNLabel'] = { + 'dc:Bounds': { + '-x': edge.text.x - (edge.text.value.length * 10) / 2, + '-y': edge.text.y - 7, + '-width': edge.text.value.length * 10, + '-height': 14, + }, + }; + } + return diagramData; + }); + bpmnDiagramData['bpmndi:BPMNShape'] = data.nodes.map((node: any) => { + const nodeId = node.id; + let width = 100; + let height = 80; + let { x, y } = node; + // bpmn坐标是基于左上角,LogicFlow基于中心点,此处处理一下。 + const shapeConfig = BPMNBaseAdapter.shapeConfigMap.get(node.type); + if (shapeConfig) { + width = shapeConfig.width; + height = shapeConfig.height; + } + x -= width / 2; + y -= height / 2; + const diagramData: any = { + '-id': `${nodeId}_di`, + '-bpmnElement': nodeId, + 'dc:Bounds': { + '-x': x, + '-y': y, + '-width': width, + '-height': height, + }, + }; + if (node.text?.value) { + diagramData['bpmndi:BPMNLabel'] = { + 'dc:Bounds': { + '-x': node.text.x - (node.text.value.length * 10) / 2, + '-y': node.text.y - 7, + '-width': node.text.value.length * 10, + '-height': 14, + }, + }; + } + return diagramData; + }); +} + +const ignoreType = ['bpmn:incoming', 'bpmn:outgoing']; + +/** + * 将bpmn数据转换为LogicFlow内部能识别数据 + */ +function convertBpmn2LfData(bpmnData: any, other?: ExtraPropsType) { + let nodes: any[] = []; + let edges: any[] = []; + + const eleMap = new Map(); + + const { transformer, excludeFields } = other ?? {}; + + const excludeFieldsSet = { + in: new Set([...defaultExcludeFields.in, ...(excludeFields?.in || [])]), + out: new Set([...defaultExcludeFields.out, ...(excludeFields?.out || [])]), + }; + + defaultTransformer = mergeInNOutObject(defaultTransformer, transformer); + + const definitions = bpmnData['bpmn:definitions']; + if (definitions) { + const process = definitions['bpmn:process']; + (function (data, callbacks) { + callbacks.forEach((callback) => { + try { + Object.keys(data).forEach((key: string) => { + try { + callback(key); + } catch (error) { + console.error(error); + } + }); + } catch (error) { + console.error(error); + } + }); + }(process, [ + (key: string) => { + // 将bpmn:subProcess中的数据提升到process中 + function subProcessProcessing(data: any) { + // data['-children'] ??= []; + if (!data['-children']) { + data['-children'] = []; + } + Object.keys(data).forEach((key: string) => { + if (key.indexOf('bpmn:') === 0 && !ignoreType.includes(key)) { + // process[key] ??= []; + if (!process[key]) { + process[key] = []; + } + !Array.isArray(process[key]) && (process[key] = [process[key]]); + Array.isArray(data[key]) + ? process[key].push(...data[key]) + : process[key].push(data[key]); + if (Array.isArray(data[key])) { + data[key].forEach((item: any) => { + !key.includes('Flow') && data['-children'].push(item['-id']); + }); + } else { + !key.includes('Flow') + && data['-children'].push(data[key]['-id']); + } + delete data[key]; + } + }); + } + if (key === 'bpmn:subProcess') { + const data = process[key]; + if (Array.isArray(data)) { + data.forEach((item: any) => { + key === 'bpmn:subProcess' && subProcessProcessing(item); + }); + } else { + subProcessProcessing(data); + } + } + }, + (key: string) => { + // 处理被提升的节点、边, 主要是通过definitionTransformer处理出节点的属性 + const fn = (obj: any) => { + Object.keys(obj).forEach((key: string) => { + if (key.includes('bpmn:')) { + let props: any = {}; + if (defaultTransformer[key] && defaultTransformer[key].in) { + props = defaultTransformer[key].in?.(key, _.cloneDeep(obj[key])); + delete obj[key]; + } else { + func(obj[key]); + } + let keys: (string | symbol)[]; + if ((keys = Reflect.ownKeys(props)).length > 0) { + keys.forEach((key) => { + Reflect.set(obj, key, props[key]); + }); + } + } + }); + }; + function func(data: any) { + eleMap.set(data['-id'], data); + if (Array.isArray(data)) { + data.forEach((item) => { + func(item); + }); + } else if (typeof data === 'object') { + fn(data); + } + } + func(process[key]); + }, + (key: string) => { + if (key.indexOf('bpmn:') === 0) { + const value = process[key]; + if (key === 'bpmn:sequenceFlow') { + const bpmnEdges = definitions['bpmndi:BPMNDiagram']['bpmndi:BPMNPlane'][ + 'bpmndi:BPMNEdge' + ]; + edges = getLfEdges(value, bpmnEdges); + } else { + const shapes = definitions['bpmndi:BPMNDiagram']['bpmndi:BPMNPlane'][ + 'bpmndi:BPMNShape' + ]; + if (key === 'bpmn:boundaryEvent') { + const data = process[key]; + const fn = (item: any) => { + const { '-attachedToRef': attachedToRef } = item; + const attachedToNode = eleMap.get(attachedToRef); + + // attachedToNode['-boundaryEvents'] ??= []; + + if (!attachedToNode['-boundaryEvents']) { + attachedToNode['-boundaryEvents'] = []; + } + + attachedToNode['-boundaryEvents'].push(item['-id']); + }; + if (Array.isArray(data)) { + data.forEach((item) => { + fn(item); + }); + } else { + fn(data); + } + } + nodes = nodes.concat(getLfNodes(value, shapes, key)); + } + } + }, + ])); + } + + const ignoreFields = (obj: Object, filterSet: Set, path: string) => { + Object.keys(obj).forEach((key) => { + const tmpPath = path ? `${path}.${key}` : key; + if (filterSet.has(tmpPath)) { + delete obj[key]; + } else if (typeof obj[key] === 'object') { + ignoreFields(obj[key], filterSet, tmpPath); + } + }); + }; + + nodes.forEach((node) => { + if (other?.mapping?.in) { + const mapping = other?.mapping?.in; + const { type } = node; + if (mapping[type]) { + node.type = mapping[type]; + } + } + ignoreFields(node, excludeFieldsSet.in, ''); + // Object.keys(node.properties).forEach((key) => { + // excludeFieldsSet.in.has(key) && delete node.properties[key]; + // }); + }); + + edges.forEach((edge) => { + if (other?.mapping?.in) { + const mapping = other?.mapping?.in; + const { type } = edge; + if (mapping[type]) { + edge.type = mapping[type]; + } + } + ignoreFields(edge, excludeFieldsSet.in, ''); + // Object.keys(edge.properties).forEach((key) => { + // excludeFieldsSet.in.has(key) && delete edge.properties[key]; + // }); + }); + + return { + nodes, + edges, + }; +} + +function getLfNodes(value: any, shapes: any, key: any) { + const nodes: NodeConfig[] = []; + if (Array.isArray(value)) { + // 数组 + value.forEach((val) => { + let shapeValue: any; + if (Array.isArray(shapes)) { + shapeValue = shapes.find( + (shape) => shape['-bpmnElement'] === val['-id'], + ); + } else { + shapeValue = shapes; + } + const node = getNodeConfig(shapeValue, key, val); + nodes.push(node); + }); + } else { + let shapeValue; + if (Array.isArray(shapes)) { + shapeValue = shapes.find( + (shape) => shape['-bpmnElement'] === value['-id'], + ); + } else { + shapeValue = shapes; + } + const node = getNodeConfig(shapeValue, key, value); + nodes.push(node); + } + return nodes; +} + +function getNodeConfig(shapeValue: any, type: any, processValue: any) { + let x = Number(shapeValue['dc:Bounds']['-x']); + let y = Number(shapeValue['dc:Bounds']['-y']); + const { '-children': children } = processValue; + const name = processValue['-name']; + const shapeConfig = BPMNBaseAdapter.shapeConfigMap.get(type); + if (shapeConfig) { + x += shapeConfig.width / 2; + y += shapeConfig.height / 2; + } + let properties: any = {}; + // 判断是否存在额外的属性,将额外的属性放到properties中 + Object.entries(processValue).forEach(([key, value]) => { + if (!defaultAttrsForInput.includes(key)) { + properties[key] = value; + } + }); + properties = convertXmlToNormal(properties); + let text; + if (name) { + text = { + x, + y, + value: name, + }; + // 自定义文本位置 + if ( + shapeValue['bpmndi:BPMNLabel'] + && shapeValue['bpmndi:BPMNLabel']['dc:Bounds'] + ) { + const textBounds = shapeValue['bpmndi:BPMNLabel']['dc:Bounds']; + text.x = Number(textBounds['-x']) + Number(textBounds['-width']) / 2; + text.y = Number(textBounds['-y']) + Number(textBounds['-height']) / 2; + } + } + const nodeConfig: NodeConfig = { + id: shapeValue['-bpmnElement'], + type, + x, + y, + properties, + }; + children && (nodeConfig.children = children); + if (text) { + nodeConfig.text = text; + } + return nodeConfig; +} + +function getLfEdges(value: any, bpmnEdges: any) { + const edges: EdgeConfig[] = []; + if (Array.isArray(value)) { + value.forEach((val) => { + let edgeValue; + if (Array.isArray(bpmnEdges)) { + edgeValue = bpmnEdges.find( + (edge) => edge['-bpmnElement'] === val['-id'], + ); + } else { + edgeValue = bpmnEdges; + } + edges.push(getEdgeConfig(edgeValue, val)); + }); + } else { + let edgeValue; + if (Array.isArray(bpmnEdges)) { + edgeValue = bpmnEdges.find( + (edge) => edge['-bpmnElement'] === value['-id'], + ); + } else { + edgeValue = bpmnEdges; + } + edges.push(getEdgeConfig(edgeValue, value)); + } + return edges; +} + +function getEdgeConfig(edgeValue: any, processValue: any) { + let text; + const textVal = processValue['-name']; + if (textVal) { + const textBounds = edgeValue['bpmndi:BPMNLabel']['dc:Bounds']; + // 如果边文本换行,则其偏移量应该是最长一行的位置 + let textLength = 0; + textVal.split('\n').forEach((textSpan: string) => { + if (textLength < textSpan.length) { + textLength = textSpan.length; + } + }); + + text = { + value: textVal, + x: Number(textBounds['-x']) + (textLength * 10) / 2, + y: Number(textBounds['-y']) + 7, + }; + } + let properties: any = {}; + // 判断是否存在额外的属性,将额外的属性放到properties中 + Object.entries(processValue).forEach(([key, value]) => { + if (!defaultAttrsForInput.includes(key)) { + properties[key] = value; + } + }); + properties = convertXmlToNormal(properties); + const pointsList = edgeValue['di:waypoint'].map((point: any) => ({ + x: Number(point['-x']), + y: Number(point['-y']), + })); + const edge: EdgeConfig = { + id: processValue['-id'], + type: BpmnElements.FLOW, + pointsList, + sourceNodeId: processValue['-sourceRef'], + targetNodeId: processValue['-targetRef'], + properties, + }; + if (text) { + edge.text = text; + } + return edge; +} + +class BPMNBaseAdapter { + static pluginName = 'bpmn-adapter'; + static shapeConfigMap = new Map(); + processAttributes: { + ['-isExecutable']: string; + ['-id']: string; + }; + definitionAttributes: { + ['-id']: string; + ['-xmlns:xsi']: string; + ['-xmlns:bpmn']: string; + ['-xmlns:bpmndi']: string; + ['-xmlns:dc']: string; + ['-xmlns:di']: string; + ['-targetNamespace']: string; + ['-exporter']: string; + ['-exporterVersion']: string; + [key: string]: any; + }; + constructor({ lf }: any) { + lf.adapterIn = this.adapterIn; + lf.adapterOut = this.adapterOut; + this.processAttributes = { + '-isExecutable': 'true', + '-id': 'Process', + }; + this.definitionAttributes = { + '-id': 'Definitions', + '-xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '-xmlns:bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL', + '-xmlns:bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI', + '-xmlns:dc': 'http://www.omg.org/spec/DD/20100524/DC', + '-xmlns:di': 'http://www.omg.org/spec/DD/20100524/DI', + '-targetNamespace': 'http://logic-flow.org', + '-exporter': 'logicflow', + '-exporterVersion': '1.2.10', + }; + } + setCustomShape(key: string, val: any) { + BPMNBaseAdapter.shapeConfigMap.set(key, val); + } + /** + * @param retainedAttrsFields?: string[] (可选)属性保留字段,retainedField会和默认的defaultRetainedFields: + * ["properties", "startPoint", "endPoint", "pointsList"]合并, + * 这意味着出现在这个数组里的字段当它的值是数组或是对象时不会被视为一个节点而是一个属性。 + * @param excludeFields excludeFields会和默认的defaultExcludeFields合并,出现在这个数组中的字段在转换时会被忽略 + * @param transformer 对应节点或者边的内容转换规则 + */ + adapterOut = (data: any, other?: ExtraPropsType) => { + const bpmnProcessData = { ...this.processAttributes }; + convertLf2ProcessData(bpmnProcessData, data, other); + const bpmnDiagramData = { + '-id': 'BPMNPlane_1', + '-bpmnElement': bpmnProcessData['-id'], + }; + convertLf2DiagramData(bpmnDiagramData, data); + const definitions = this.definitionAttributes; + definitions['bpmn:process'] = bpmnProcessData; + definitions['bpmndi:BPMNDiagram'] = { + '-id': 'BPMNDiagram_1', + 'bpmndi:BPMNPlane': bpmnDiagramData, + }; + const bpmnData = { + 'bpmn:definitions': definitions, + }; + + if (other?.mapping?.out) { + const mapping = other?.mapping?.out; + + const nameMapping = (obj: Object | any[]) => { + if (Array.isArray(obj)) { + return obj.map(item => nameMapping(item)); + } + const keys = Object.keys(obj); + keys.forEach((key: string) => { + let mappingName: string; + if (mappingName = mapping[key]) { + obj[mappingName] = _.cloneDeep(obj[key]); + delete obj[key]; + } + }); + }; + nameMapping(bpmnData); + } + + return bpmnData; + }; + adapterIn = (bpmnData: any, other?: ExtraPropsType) => { + if (bpmnData) { + return convertBpmn2LfData(bpmnData, other); + } + }; +} + +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.START, { + width: StartEventConfig.width, + height: StartEventConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.END, { + width: EndEventConfig.width, + height: EndEventConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.INTERMEDIATE_CATCH, { + width: IntermediateEventConfig.width, + height: IntermediateEventConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.INTERMEDIATE_THROW, { + width: IntermediateEventConfig.width, + height: IntermediateEventConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.BOUNDARY, { + width: BoundaryEventConfig.width, + height: BoundaryEventConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.PARALLEL_GATEWAY, { + width: ParallelGatewayConfig.width, + height: ParallelGatewayConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.INCLUSIVE_GATEWAY, { + width: InclusiveGatewayConfig.width, + height: InclusiveGatewayConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.EXCLUSIVE_GATEWAY, { + width: ExclusiveGatewayConfig.width, + height: ExclusiveGatewayConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.SYSTEM, { + width: ServiceTaskConfig.width, + height: ServiceTaskConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.USER, { + width: UserTaskConfig.width, + height: UserTaskConfig.height, +}); +BPMNBaseAdapter.shapeConfigMap.set(BpmnElements.SUBPROCESS, { + width: SubProcessConfig.width, + height: SubProcessConfig.height, +}); + +class BPMNAdapter extends BPMNBaseAdapter { + static pluginName = 'bpmnXmlAdapterV2'; + constructor(data: any) { + super(data); + const { lf } = data; + lf.adapterIn = this.adapterXmlIn; + lf.adapterOut = this.adapterXmlOut; + } + adapterXmlIn = (bpmnData: any, other?: ExtraPropsType) => { + const json = lfXml2Json(bpmnData); + return this.adapterIn(json, other); + }; + adapterXmlOut = (data: any, other?: ExtraPropsType) => { + const outData = this.adapterOut(data, other); + return lfJson2Xml(outData); + }; +} + +export { BPMNBaseAdapter, BPMNAdapter, convertNormalToXml, convertXmlToNormal }; + +export default BPMNBaseAdapter; diff --git a/packages/extension/src/bpmn-elements-adapter/json2xml.ts b/packages/extension/src/bpmn-elements-adapter/json2xml.ts new file mode 100644 index 000000000..1a532473e --- /dev/null +++ b/packages/extension/src/bpmn-elements-adapter/json2xml.ts @@ -0,0 +1,91 @@ +/* eslint-disable guard-for-in */ +function type(obj: any) { + return Object.prototype.toString.call(obj); +} + +function addSpace(depth: number) { + return ' '.repeat(depth); +} + +function handleAttributes(obj: any): any { + if (type(obj) === '[object Object]') { + return Object.keys(obj).reduce((tmp: any, key: string) => { + let tmpKey = key; + if (key.charAt(0) === '-') { + tmpKey = key.substring(1); + } + tmp[tmpKey] = handleAttributes(obj[key]); + return tmp; + }, {}); + } if (Array.isArray(obj)) { + return obj.map((item) => handleAttributes(item)); + } + return obj; +} + +function getAttributes(obj: any) { + let tmp = obj; + try { + if (typeof tmp !== 'string') { + tmp = JSON.parse(obj); + } + } catch (error) { + tmp = JSON.stringify(handleAttributes(obj)).replace(/"/g, '\''); + } + return tmp; +} + +const tn = '\t\n'; + +// @see issue https://github.com/didi/LogicFlow/issues/718, refactoring of function toXml +function toXml(obj: any, name: string, depth: number) { + const frontSpace = addSpace(depth); + let str = ''; + const prefix = tn + frontSpace; + if (name === '-json') return ''; + if (name === '#text') { + return prefix + obj; + } if (name === '#cdata-section') { + return `${prefix}`; + } if (name === '#comment') { + return `${prefix}`; + } + if (`${name}`.charAt(0) === '-') { + return ` ${name.substring(1)}="${getAttributes(obj)}"`; + } + if (Array.isArray(obj)) { + str += obj.map((item) => toXml(item, name, depth + 1)).join(''); + } else if (type(obj) === '[object Object]') { + const keys = Object.keys(obj); + let attributes = ''; + let children = obj['-json'] + ? tn + addSpace(depth + 1) + obj['-json'] + : ''; + + str += `${depth === 0 ? '' : prefix}<${name}`; + + keys.forEach((k) => { + k.charAt(0) === '-' + ? (attributes += toXml(obj[k], k, depth + 1)) + : (children += toXml(obj[k], k, depth + 1)); + }); + + str + += attributes + + (children !== '' ? `>${children}${prefix}` : ' />'); + } else { + str += `${prefix}<${name}>${obj.toString()}`; + } + + return str; +} + +function lfJson2Xml(obj: any) { + let xmlStr = ' '; + for (const key in obj) { + xmlStr += toXml(obj[key], key, 0); + } + return xmlStr; +} + +export { lfJson2Xml, handleAttributes }; diff --git a/packages/extension/src/bpmn-elements-adapter/xml2json.ts b/packages/extension/src/bpmn-elements-adapter/xml2json.ts new file mode 100644 index 000000000..9a33a3fa4 --- /dev/null +++ b/packages/extension/src/bpmn-elements-adapter/xml2json.ts @@ -0,0 +1,549 @@ +/* eslint-disable no-continue */ +/* eslint-disable @typescript-eslint/no-this-alias */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable guard-for-in */ +/* eslint-disable func-names */ +// @ts-nocheck + +import _ from 'lodash-es'; +// ======================================================================== +// XML.ObjTree -- XML source code from/to JavaScript object like E4X +// ======================================================================== + +const XML = function () {}; + +// constructor +XML.ObjTree = function () { + // @ts-ignore + return this; +}; + +// class variables + +XML.ObjTree.VERSION = '0.23'; + +// object prototype + +XML.ObjTree.prototype.xmlDecl = '\n'; +XML.ObjTree.prototype.attr_prefix = '-'; + +// method: parseXML( xmlsource ) + +XML.ObjTree.prototype.parseXML = function (xml) { + let root; + if (window.DOMParser) { + const xmldom = new DOMParser(); + // xmldom.async = false; // DOMParser is always sync-mode + const dom = xmldom.parseFromString(xml, 'application/xml'); + if (!dom) return; + root = dom.documentElement; + } else if (window.ActiveXObject) { + xmldom = new ActiveXObject('Microsoft.XMLDOM'); + xmldom.async = false; + xmldom.loadXML(xml); + root = xmldom.documentElement; + } + if (!root) return; + const data = this.parseDOM(root); + return data; +}; + +// method: parseHTTP( url, options, callback ) + +XML.ObjTree.prototype.parseHTTP = function (url, options, callback) { + const myOpt = {}; + for (const key in options) { + myOpt[key] = options[key]; // copy object + } + if (!myOpt.method) { + if ( + typeof myOpt.postBody === 'undefined' + && typeof myOpt.postbody === 'undefined' + && typeof myOpt.parameters === 'undefined' + ) { + myOpt.method = 'get'; + } else { + myOpt.method = 'post'; + } + } + if (callback) { + myOpt.asynchronous = true; // async-mode + const __this = this; + const __func = callback; + const __save = myOpt.onComplete; + myOpt.onComplete = function (trans) { + let tree; + if (trans && trans.responseXML && trans.responseXML.documentElement) { + tree = __this.parseDOM(trans.responseXML.documentElement); + } + __func(tree, trans); + if (__save) __save(trans); + }; + } else { + myOpt.asynchronous = false; // sync-mode + } + let trans; + if (typeof HTTP !== 'undefined' && HTTP.Request) { + myOpt.uri = url; + const req = new HTTP.Request(myOpt); + if (req) trans = req.transport; + } else if (typeof Ajax !== 'undefined' && Ajax.Request) { + const req = new Ajax.Request(url, myOpt); + if (req) trans = req.transport; + } + if (callback) return trans; + if (trans && trans.responseXML && trans.responseXML.documentElement) { + return this.parseDOM(trans.responseXML.documentElement); + } +}; + +XML.ObjTree.prototype.parseDOM = function (root) { + if (!root) return; + + this.__force_array = {}; + if (this.force_array) { + for (let i = 0; i < this.force_array.length; i++) { + this.__force_array[this.force_array[i]] = 1; + } + } + + let json = this.parseElement(root); // parse root node + if (this.__force_array[root.nodeName]) { + json = [json]; + } + if (root.nodeType !== 11) { + // DOCUMENT_FRAGMENT_NODE + const tmp = {}; + tmp[root.nodeName] = json; // root nodeName + json = tmp; + } + return json; +}; + +// method: parseElement( element ) +/** + * @reference node type reference https://www.w3schools.com/xml/dom_nodetype.asp + */ +XML.ObjTree.prototype.parseElement = function (elem) { + // PROCESSING_INSTRUCTION_NODE + if (elem.nodeType === 7) { + return; + } + + // TEXT_NODE CDATA_SECTION_NODE COMMENT_NODE + if (elem.nodeType === 3 || elem.nodeType === 4 || elem.nodeType === 8) { + // eslint-disable-next-line no-control-regex + const bool = elem.nodeValue.match(/[^\x00-\x20]/); + if (bool == null) return; // ignore white spaces + return elem.nodeValue; + } + + let retVal = null; + const cnt = {}; + + if (elem.attributes && elem.attributes.length) { + retVal = {}; + for (let i = 0; i < elem.attributes.length; i++) { + let key = elem.attributes[i].nodeName; + if (typeof key !== 'string') continue; + let val = elem.attributes[i].nodeValue; + try { + val = JSON.parse(elem.attributes[i].nodeValue.replace(/'/g, '"')); + } catch (error) { + val = elem.attributes[i].nodeValue; + } + if (val === null || val === undefined) continue; + key = this.attr_prefix + key; + if (typeof cnt[key] === 'undefined') cnt[key] = 0; + cnt[key]++; + this.addNode(retVal, key, cnt[key], val); + } + } + + // parse child nodes (recursive) + if (elem.childNodes && elem.childNodes.length) { + let textOnly = true; + if (retVal) textOnly = false; // some attributes exists + for (let i = 0; i < elem.childNodes.length && textOnly; i++) { + const nType = elem.childNodes[i].nodeType; + if (nType === 3 || nType === 4 || nType === 8) continue; + textOnly = false; + } + if (textOnly) { + if (!retVal) retVal = ''; + for (let i = 0; i < elem.childNodes.length; i++) { + retVal += elem.childNodes[i].nodeValue; + } + } else { + if (!retVal) retVal = {}; + for (let i = 0; i < elem.childNodes.length; i++) { + const key = elem.childNodes[i].nodeName; + if (typeof key !== 'string') continue; + const val = this.parseElement(elem.childNodes[i]); + if (!val) continue; + if (typeof cnt[key] === 'undefined') cnt[key] = 0; + cnt[key]++; + this.addNode(retVal, key, cnt[key], val); + } + } + } else { + // @see issue https://github.com/didi/LogicFlow/issues/1068 + // if retVal is null, that means the elem doesn't have any attributes and children, + // the elem would be like: or , so set retVal to empty object {} + retVal === null && (retVal = {}); + } + return retVal; +}; + +// method: addNode( hash, key, count, value ) + +XML.ObjTree.prototype.addNode = function (hash, key, counts, val) { + if (this.__force_array[key]) { + if (counts === 1) hash[key] = []; + hash[key][hash[key].length] = val; // push + } else if (counts === 1) { + // 1st sibling + hash[key] = val; + } else if (counts === 2) { + // 2nd sibling + hash[key] = [hash[key], val]; + } else { + // 3rd sibling and more + hash[key][hash[key].length] = val; + } +}; + +// method: writeXML( tree ) + +XML.ObjTree.prototype.writeXML = function (tree) { + const xml = this.hash_to_xml(null, tree); + return this.xmlDecl + xml; +}; + +// method: hash_to_xml( tagName, tree ) + +XML.ObjTree.prototype.hash_to_xml = function (name, tree) { + const elem = []; + const attr = []; + for (const key in tree) { + if (!tree?.hasOwnProperty(key)) continue; + const val = tree[key]; + if (key.charAt(0) !== this.attr_prefix) { + if (typeof val === 'undefined' || val == null) { + elem[elem.length] = `<${key} />`; + } else if (typeof val === 'object' && val.constructor === Array) { + elem[elem.length] = this.array_to_xml(key, val); + } else if (typeof val === 'object') { + elem[elem.length] = this.hash_to_xml(key, val); + } else { + elem[elem.length] = this.scalar_to_xml(key, val); + } + } else { + attr[attr.length] = ` ${key.substring(1)}="${this.xml_escape(val)}"`; + } + } + const jattr = attr.join(''); + let jelem = elem.join(''); + if (typeof name === 'undefined' || name == null) { + // no tag + } else if (elem.length > 0) { + if (jelem.match(/\n/)) { + jelem = `<${name}${jattr}>\n${jelem}\n`; + } else { + jelem = `<${name}${jattr}>${jelem}\n`; + } + } else { + jelem = `<${name}${jattr} />\n`; + } + return jelem; +}; + +// method: array_to_xml( tagName, array ) + +XML.ObjTree.prototype.array_to_xml = function (name, array) { + const out = []; + for (let i = 0; i < array.length; i++) { + const val = array[i]; + if (typeof val === 'undefined' || val == null) { + out[out.length] = `<${name} />`; + } else if (typeof val === 'object' && val.constructor === Array) { + out[out.length] = this.array_to_xml(name, val); + } else if (typeof val === 'object') { + out[out.length] = this.hash_to_xml(name, val); + } else { + out[out.length] = this.scalar_to_xml(name, val); + } + } + return out.join(''); +}; + +// method: scalar_to_xml( tagName, text ) + +XML.ObjTree.prototype.scalar_to_xml = function (name, text) { + if (name === '#text') { + return this.xml_escape(text); + } + return `<${name}>${this.xml_escape(text)}\n`; + +}; + +// method: xml_escape( text ) + +XML.ObjTree.prototype.xml_escape = function (text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +}; + +/* +// ======================================================================== + +=head1 NAME + +XML.ObjTree -- XML source code from/to JavaScript object like E4X + +=head1 SYNOPSIS + +var xotree = new XML.ObjTree(); +var tree1 = { +root: { +node: "Hello, World!" +} +}; +var xml1 = xotree.writeXML( tree1 ); // object tree to XML source +alert( "xml1: "+xml1 ); + +var xml2 = '0'; +var tree2 = xotree.parseXML( xml2 ); // XML source to object tree +alert( "error: "+tree2.response.error ); + +=head1 DESCRIPTION + +XML.ObjTree class is a parser/generator between XML source code +and JavaScript object like E4X, ECMAScript for XML. +This is a JavaScript version of the XML::TreePP module for Perl. +This also works as a wrapper for XMLHTTPRequest and successor to JKL.ParseXML class +when this is used with prototype.js or JSAN's HTTP.Request class. + +=head2 JavaScript object tree format + +A sample XML source: + + + +Yasuhisa +Chizuko + +Shiori +Yusuke +Kairi + + + +Its JavaScript object tree like JSON/E4X: + +{ +'family': { +'-name': 'Kawasaki', +'father': 'Yasuhisa', +'mother': 'Chizuko', +'children': { +'girl': 'Shiori' +'boy': [ +'Yusuke', +'Kairi' +] +} +} +}; + +Each elements are parsed into objects: + +tree.family.father; # the father's given name. + +Prefix '-' is inserted before every attributes' name. + +tree.family["-name"]; # this family's family name + +A array is used because this family has two boys. + +tree.family.children.boy[0]; # first boy's name +tree.family.children.boy[1]; # second boy's name +tree.family.children.girl; # (girl has no other sisters) + +=head1 METHODS + +=head2 xotree = new XML.ObjTree() + +This constructor method returns a new XML.ObjTree object. + +=head2 xotree.force_array = [ "rdf:li", "item", "-xmlns" ]; + +This property allows you to specify a list of element names +which should always be forced into an array representation. +The default value is null, it means that context of the elements +will determine to make array or to keep it scalar. + +=head2 xotree.attr_prefix = '@'; + +This property allows you to specify a prefix character which is +inserted before each attribute names. +Instead of default prefix '-', E4X-style prefix '@' is also available. +The default character is '-'. +Or set '@' to access attribute values like E4X, ECMAScript for XML. +The length of attr_prefix must be just one character and not be empty. + +=head2 tree = xotree.parseXML( xmlSrc ); + +This method loads an XML document using the supplied string +and returns its JavaScript object converted. + +=head2 tree = xotree.parseDOM( domNode ); + +This method parses a DOM tree (ex. responseXML.documentElement) +and returns its JavaScript object converted. + +=head2 tree = xotree.parseHTTP( url, options ); + +This method loads a XML file from remote web server +and returns its JavaScript object converted. +XMLHTTPRequest's synchronous mode is always used. +This mode blocks the process until the response is completed. + +First argument is a XML file's URL +which must exist in the same domain as parent HTML file's. +Cross-domain loading is not available for security reasons. + +Second argument is options' object which can contains some parameters: +method, postBody, parameters, onLoading, etc. + +This method requires JSAN's L class or prototype.js's Ajax.Request class. + +=head2 xotree.parseHTTP( url, options, callback ); + +If a callback function is set as third argument, +XMLHTTPRequest's asynchronous mode is used. + +This mode calls a callback function with XML file's JavaScript object converted +after the response is completed. + +=head2 xmlSrc = xotree.writeXML( tree ); + +This method parses a JavaScript object tree +and returns its XML source generated. + +=head1 EXAMPLES + +=head2 Text node and attributes + +If a element has both of a text node and attributes +or both of a text node and other child nodes, +text node's value is moved to a special node named "#text". + +var xotree = new XML.ObjTree(); +var xmlSrc = 'Kawasaki Yusuke'; +var tree = xotree.parseXML( xmlSrc ); +var class = tree.span["-class"]; # attribute +var name = tree.span["#text"]; # text node + +=head2 parseHTTP() method with HTTP-GET and sync-mode + +HTTP/Request.js or prototype.js must be loaded before calling this method. + +var xotree = new XML.ObjTree(); +var url = "http://example.com/index.html"; +var tree = xotree.parseHTTP( url ); +xotree.attr_prefix = '@'; // E4X-style +alert( tree.html["@lang"] ); + +This code shows C attribute from a X-HTML source code. + +=head2 parseHTTP() method with HTTP-POST and async-mode + +Third argument is a callback function which is called on onComplete. + +var xotree = new XML.ObjTree(); +var url = "http://example.com/mt-tb.cgi"; +var opts = { +postBody: "title=...&excerpt=...&url=...&blog_name=..." +}; +var func = function ( tree ) { +alert( tree.response.error ); +}; +xotree.parseHTTP( url, opts, func ); + +This code send a track back ping and shows its response code. + +=head2 Simple RSS reader + +This is a RSS reader which loads RDF file and displays all items. + +var xotree = new XML.ObjTree(); +xotree.force_array = [ "rdf:li", "item" ]; +var url = "http://example.com/news-rdf.xml"; +var func = function( tree ) { +var elem = document.getElementById("rss_here"); +for( var i=0; i new XML.ObjTree().parseXML(xmlData); + +export { lfXml2Json }; diff --git a/packages/extension/src/bpmn-elements/README.md b/packages/extension/src/bpmn-elements/README.md new file mode 100644 index 000000000..3b6e73064 --- /dev/null +++ b/packages/extension/src/bpmn-elements/README.md @@ -0,0 +1,219 @@ +## 内置基础节点 + +### 事件 + +**开始事件 (bpmn:startEvent)** + +- 开始事件 +- 中断子流程事件 (与开始事件通过isInterrupting属性区分,即是否有isInterrupting属性, `isInterrupting = 'false'`) +- 非中断子流程事件 (与开始事件通过isInterrupting属性区分,即是否有isInterrupting属性, `isInterrupting = 'true'`) + +**边界事件 (bpmn:boundaryEvent)** + +- 中断边界事件 (属性`cancelActivity = 'false'`) +- 非中断边界事件 (属性`cancelActivity = 'true'`) + +**中间事件** + +- 捕捉事件 (bpmn:intermediateCatchEvent) + +- 抛出事件 (bpmn:intermediateThrowEvent) + +**结束事件 (bpmn:endEvent)** + +### 任务 + +- 服务任务 (bpmn:serviceTask) +- 用户任务 (bpmn:userTask) + +### 网关 + +- 并行网关 (bpmn:parallelGateway) +- 排他网关 (bpmn:exclusiveGateway) +- 包容网关 (bpmn:inclusiveGateway) + +### 子流程 + +- 嵌入子流程 (bpmn:subProcess) + +### 流(flow) + +- 顺序流 (bpmn:sequenceFlow) 可以通过`isDefaultFlow`(是否为缺省流)属性改变顺序流的样式 + +## 节点扩展 + +### 事件 + +在基础节点的基础上,我们需要通过定义definition属性来扩展事件节点。 + +``` ts +import { h } from '@logicflow/core' + +// 例如,我们想要扩展出时间开始事件,时间捕获事件,时间边界事件 +const [definition, setDefinition] = lf.useDefinition() +const customDefinition = [ + { + // 为startEvent、intermediateCatchEvent、boundaryEvent添加definition + nodes: ['startEvent', 'intermediateCatchEvent', 'boundaryEvent'], + definition: { + /** + * definition的type属性,对应XML数据中的节点名 + * 例如一个时间非中断边界事件的XML数据如下: + * + * + * + * P1D + * + * + * + */ + type: 'bpmn:timerEventDefinition', + // icon可以是svg的path路径m, 也可以是@logicflow/core 导出的h函数生成的svg, 这里是通过h函数生成的svg + icon: timerIcon, + /** + * 对应definition需要的属性,例如这里是timerType和timerValue + * timerType值可以"timeCycle", "timerDate", "timeDuration", 用于区分 + * timerValue是timerType对应的cron表达式 + * 最终会生成 `${timerValue}` + */ + properties: { + timerType: '', + timerValue: '' + } + } + } +] + +setDefinition(customDefinition) +``` + +
+ timerIcon的定义如下: +

+import { h } from '@logicflow/core'
+const timerIcon = [
+  h('circle', {
+    cx: 18,
+    cy: 18,
+    r: 11,
+    style:
+      'stroke-linecap: round;stroke-linejoin: round;stroke: rgb(34, 36, 42);stroke-width: 2px;fill: white',
+  }),
+  h('path', {
+    d: 'M 18,18 l 2.25,-7.5 m -2.25,7.5 l 5.25,1.5',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 2px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(0,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(30,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(60,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(90,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(120,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(150,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(180,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(210,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(240,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(270,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(300,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+  h('path', {
+    d: 'M 18,18 m 0,7.5 l -0,2.25',
+    transform: 'rotate(330,18,18)',
+    style:
+      'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;',
+  }),
+];
+
+ +### 任务 + +任务节点的扩展方式如下: + +```ts +import { TaskNodeFactory } from '@logicflow/extension' + +// 例如,扩展一个脚本任务 + +const scriptTaskIcon = 'M6.402,0.5H20.902C20.902,0.5,15.069,3.333,15.069,6.083S19.486,12.083,19.486,15.25S15.319,20.333,15.319,20.333H0.235C0.235,20.333,5.235,17.665999999999997,5.235,15.332999999999998S0.6520000000000001,8.582999999999998,0.6520000000000001,6.082999999999998S6.402,0.5,6.402,0.5ZM3.5,4.5L13.5,4.5M3.8,8.5L13.8,8.5M6.3,12.5L16.3,12.5M6.5,16.5L16.5,16.5'; + +// TaskNodeFactory的第一个参数是节点类型;第二个参数是节点图标(可以说svg path也可以是h函数生成的svg);第三个参数(可选的)需要给节点设置属性 +const receiveTask = TaskNodeFactory('bpmn:scriptTask', scriptTaskIcon) + +lf.register(receiveTask) +``` + +### 网关 + +网关节点的扩展方式如下: + +```ts + +import { GatewayNodeFactory } from '@logicflow/extension' + +// 例如,扩展一个复杂网关 +const complexIcon = 'm 23,13 0,7.116788321167883 -5.018248175182482,-5.018248175182482 -3.102189781021898,3.102189781021898 5.018248175182482,5.018248175182482 -7.116788321167883,0 0,4.37956204379562 7.116788321167883,0 -5.018248175182482,5.018248175182482 l 3.102189781021898,3.102189781021898 5.018248175182482,-5.018248175182482 0,7.116788321167883 4.37956204379562,0 0,-7.116788321167883 5.018248175182482,5.018248175182482 3.102189781021898,-3.102189781021898 -5.018248175182482,-5.018248175182482 7.116788321167883,0 0,-4.37956204379562 -7.116788321167883,0 5.018248175182482,-5.018248175182482 -3.102189781021898,-3.102189781021898 -5.018248175182482,5.018248175182482 0,-7.116788321167883 -4.37956204379562,0 z' + +const complexGateway = GatewayNodeFactory('bpmn:complexGateway', complexIcon) +``` + +### 子流程 + +*不支持扩展* + +### 流 + +流的扩展和自定义边的定义方法完全相同,参考 diff --git a/packages/extension/src/bpmn-elements/__tests__/definition.test.js b/packages/extension/src/bpmn-elements/__tests__/definition.test.js new file mode 100644 index 000000000..78978880b --- /dev/null +++ b/packages/extension/src/bpmn-elements/__tests__/definition.test.js @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable no-undef */ +import LogicFlow from '@logicflow/core'; +import { BPMNElements } from '../index'; + +const registerEventNodes = new (jest.fn())(); +const registerGatewayNodes = new (jest.fn())(); +const registerFlows = new (jest.fn())(); +const registerTaskNodes = new (jest.fn())(); + +/** */ +describe('Test bpmn elements: definitionConfig', () => { + LogicFlow.use(BPMNElements); + const div = document.createElement('div'); + document.body.appendChild(div); + const lf = new LogicFlow({ + container: div, + }); + const [definition, setDefinition] = lf.useDefinition(); + + // 默认definition配置 + // const definitionConfig: DefinitionConfigType[] = [ + // { + // nodes: ['startEvent', 'intermediateCatchEvent', 'boundaryEvent'], + // definition: [ + // { + // type: 'bpmn:timerEventDefinition', + // icon: timerIcon, + // properties: { + // definitionType: 'bpmn:timerEventDefinition', + // timerValue: '', + // timerType: '', + // }, + // }, + // ], + // }, + // ] + + test('nodes startEvent, intermediateCatchEvent, boundaryEvent, contain default definition: bpmn:timerEventDefinition', () => { + expect(Object.keys(definition)).toEqual([ + 'boundaryEvent', + 'intermediateCatchEvent', + 'startEvent', + ]); + expect(definition.startEvent.has('bpmn:timerEventDefinition')).toBe(true); + expect(definition.boundaryEvent.has('bpmn:timerEventDefinition')).toBe( + true, + ); + expect( + definition.intermediateCatchEvent.has('bpmn:timerEventDefinition'), + ).toBe(true); + }); + + test('after setting new definition by setDefinition for startEvent, startEvent should contain two definition: bpmn:timerEventDefinition, bpmn:messageEventDefinition', () => { + setDefinition([ + { + nodes: ['startEvent'], + definition: [ + { + type: 'bpmn:messageEventDefinition', + icon: messageIcon, + properties: { + panels: [], + definitionType: 'bpmn:messageEventDefinition', + }, + }, + ], + }, + ]); + + expect(Array.from(definition.startEvent.keys()).length).toBe(2); + expect(definition.startEvent.has('bpmn:messageEventDefinition')).toBe(true); + }); +}); diff --git a/packages/extension/src/bpmn-elements/index.d.ts b/packages/extension/src/bpmn-elements/index.d.ts new file mode 100644 index 000000000..925b26fb3 --- /dev/null +++ b/packages/extension/src/bpmn-elements/index.d.ts @@ -0,0 +1,26 @@ +type DefinitionConfigType = { + nodes: string[]; + definition: EventDefinitionType[] | TaskDefinitionType[]; +}; + +type DefinitionPropertiesType = { + definitionType: string; + timerType?: TimerType; + timerValue?: string; + [key: string]: any; +}; + +type EventDefinitionType = { + type: string; + icon: string | Object; + toJSON: Function; + properties: DefinitionPropertiesType; + [key: string]: any; +}; + +type TaskDefinitionType = { + type: string; + [key: string]: any; +}; + +type TimerType = 'timerCycle' | 'timerDate' | 'timerDuration'; diff --git a/packages/extension/src/bpmn-elements/index.ts b/packages/extension/src/bpmn-elements/index.ts new file mode 100644 index 000000000..6a451f51f --- /dev/null +++ b/packages/extension/src/bpmn-elements/index.ts @@ -0,0 +1,99 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { registerEventNodes } from './presets/Event'; +import { registerGatewayNodes } from './presets/Gateway'; +import { registerTaskNodes } from './presets/Task'; +// import { registerPoolNodes } from './presets/Pool'; +import { registerFlows } from './presets/Flow'; +import { timerIcon, messageIcon } from './presets/icons'; + +type DefinitionConfigType = { + nodes: string[]; + definition: EventDefinitionType[] | TaskDefinitionType[]; +}; + +type DefinitionPropertiesType = { + definitionType: string; + timerType?: TimerType; + timerValue?: string; + [key: string]: any; +}; + +type EventDefinitionType = { + type: string; + icon: string | Object; + toJSON: Function; + properties: DefinitionPropertiesType; + [key: string]: any; +}; + +type TaskDefinitionType = { + type: string; + [key: string]: any; +}; + +type TimerType = 'timerCycle' | 'timerDate' | 'timerDuration'; + +const definitionConfig: DefinitionConfigType[] = [ + { + nodes: ['startEvent', 'intermediateCatchEvent', 'boundaryEvent'], + definition: [ + { + type: 'bpmn:timerEventDefinition', + icon: timerIcon, + properties: { + definitionType: 'bpmn:timerEventDefinition', + timerValue: '', + timerType: '', + }, + }, + ], + }, +]; + +export function useDefinition(definition: any) { + function setDefinition(config: DefinitionConfigType[]) { + function set( + nodes: any[], + definitions: EventDefinitionType[] | TaskDefinitionType[], + ) { + nodes.forEach((name) => { + if (!definition?.[name]) { + definition[name] = new Map(); + } + const map = definition?.[name]; + definitions.forEach((define) => { + map.set(define.type, define); + }); + }); + return definition; + } + config.forEach((define: any) => { + set(define.nodes, define.definition); + }); + } + + return () => [definition, setDefinition]; +} + +export class BPMNElements { + static pluginName = 'BpmnElementsPlugin'; + constructor({ lf }: any) { + lf.definition = {}; + lf.useDefinition = useDefinition(lf.definition); + const [definition, setDefinition] = lf.useDefinition(); + setDefinition(definitionConfig); + + // event: startEvent, endEvent, intermediateCatchEvent, intermediateThrowEvent, boundaryEvent + + registerEventNodes(lf); + registerGatewayNodes(lf); + registerFlows(lf); + registerTaskNodes(lf); + // registerPoolNodes(lf); + + lf.setDefaultEdgeType('bpmn:sequenceFlow'); + } +} + +export * from './presets/Task/task'; +export * from './presets/Gateway/gateway'; diff --git a/packages/extension/src/bpmn-elements/presets/Event/EndEventFactory.ts b/packages/extension/src/bpmn-elements/presets/Event/EndEventFactory.ts new file mode 100644 index 000000000..fff5a0240 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Event/EndEventFactory.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + CircleNode, + CircleNodeModel, + GraphModel, + NodeConfig, + h, +} from '@logicflow/core'; +import { genBpmnId, groupRule } from '../../utils'; + +export function EndEventFactory(lf: any): { + type: string, + model: any, + view: any, +} { + const [definition] = lf.useDefinition(); + class view extends CircleNode { + getAnchorStyle() { + return { + visibility: 'hidden', + }; + } + getShape() { + const { model } = this.props; + const style = model.getNodeStyle(); + const { x, y, r, width, height, properties } = model; + const outCircle = super.getShape(); + const { definitionType } = properties; + const { icon } = definition.endEvent?.get(definitionType) || {}; + const i = Array.isArray(icon) + ? h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + ...icon, + ) + : h('path', { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + d: icon, + style: + 'fill: black; stroke-linecap: round; stroke-linejoin: round; stroke: white; stroke-width: 1px;', + }); + return h( + 'g', + {}, + outCircle, + h('circle', { + ...style, + strokeWidth: 2, + cx: x, + cy: y, + r: r - 2, + }), + i, + ); + } + } + class model extends CircleNodeModel { + constructor(data: NodeConfig, graphModel: GraphModel) { + if (!data.id) { + data.id = `Event_${genBpmnId()}`; + } + if (!data.text) { + data.text = ''; + } + if (data.text && typeof data.text === 'string') { + data.text = { + value: data.text, + x: data.x, + y: data.y + 40, + }; + } + const { properties } = definition.endEvent?.get(data.properties?.definitionType) || {}; + data.properties = { + ...properties, + ...data.properties, + }; + data.properties?.definitionType + && (data.properties!.definitionId = `${ + data.properties?.definitionType + }EventDefinition_${genBpmnId()}`); + super(data, graphModel); + groupRule.call(this); + } + setAttributes(): void { + this.r = 18; + } + getConnectedSourceRules() { + const rules = super.getConnectedSourceRules(); + const notAsSource = { + message: '结束节点不能作为边的起点', + validate: (source, target) => { + if (source === this) { + return false; + } + return true; + }, + }; + rules.push(notAsSource); + return rules; + } + } + + return { + type: 'bpmn:endEvent', + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/Event/IntermediateCatchEvent.ts b/packages/extension/src/bpmn-elements/presets/Event/IntermediateCatchEvent.ts new file mode 100644 index 000000000..ebe29c601 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Event/IntermediateCatchEvent.ts @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + CircleNode, + CircleNodeModel, + GraphModel, + NodeConfig, + h, +} from '@logicflow/core'; +import { genBpmnId, groupRule } from '../../utils'; + +export function IntermediateCatchEventFactory(lf: any): { + type: string, + model: any, + view: any, +} { + const [definition] = lf.useDefinition(); + class view extends CircleNode { + getAnchorStyle() { + return { + visibility: 'hidden', + }; + } + getShape() { + // @ts-ignore + const { model } = this.props; + const style = model.getNodeStyle(); + const { x, y, r, width, height, properties } = model; + const { definitionType } = properties; + const { icon } = definition.intermediateCatchEvent?.get(definitionType) || {}; + + const i = Array.isArray(icon) + ? h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + ...icon, + ) + : h('path', { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + d: icon, + }); + return h( + 'g', + {}, + h('circle', { + ...style, + cx: x, + cy: y, + r, + strokeWidth: 1.5, + }), + h('circle', { + ...style, + cx: x, + cy: y, + r: r - 3, + strokeWidth: 1.5, + }), + i, + ); + } + } + class model extends CircleNodeModel { + constructor(data: NodeConfig, graphModel: GraphModel) { + if (!data.id) { + data.id = `Event_${genBpmnId()}`; + } + if (!data.text) { + data.text = ''; + } + if (data.text && typeof data.text === 'string') { + data.text = { + value: data.text, + x: data.x, + y: data.y + 40, + }; + } + const { properties } = definition.intermediateCatchEvent?.get( + data.properties?.definitionType + ) || {}; + data.properties = { + ...properties, + ...data.properties, + }; + data.properties?.definitionType + && (data.properties!.definitionId = `${ + data.properties?.definitionType + }EventDefinition_${genBpmnId()}`); + super(data, graphModel); + groupRule.call(this); + } + setAttributes(): void { + this.r = 18; + } + } + + return { + type: 'bpmn:intermediateCatchEvent', + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/Event/IntermediateThrowEvent.ts b/packages/extension/src/bpmn-elements/presets/Event/IntermediateThrowEvent.ts new file mode 100644 index 000000000..75d054b48 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Event/IntermediateThrowEvent.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + CircleNode, + CircleNodeModel, + GraphModel, + NodeConfig, + h, +} from '@logicflow/core'; +import { genBpmnId, groupRule } from '../../utils'; + +export function IntermediateThrowEventFactory(lf: any): { + type: string, + model: any, + view: any, +} { + const [definition] = lf.useDefinition(); + class view extends CircleNode { + getAnchorStyle() { + return { + visibility: 'hidden', + }; + } + getShape() { + // @ts-ignore + const { model } = this.props; + const style = model.getNodeStyle(); + const { x, y, r, width, height, properties } = model; + const { definitionType } = properties; + const { icon } = definition.intermediateThrowEvent?.get(definitionType) || {}; + + const i = Array.isArray(icon) + ? h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + ...icon, + ) + : h('path', { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + d: icon, + style: 'fill: black', + }); + return h( + 'g', + {}, + h('circle', { + ...style, + cx: x, + cy: y, + r, + strokeWidth: 1.5, + }), + h('circle', { + ...style, + cx: x, + cy: y, + r: r - 3, + strokeWidth: 1.5, + }), + i, + ); + } + } + class model extends CircleNodeModel { + constructor(data: NodeConfig, graphModel: GraphModel) { + if (!data.id) { + data.id = `Event_${genBpmnId()}`; + } + if (!data.text) { + data.text = ''; + } + if (data.text && typeof data.text === 'string') { + data.text = { + value: data.text, + x: data.x, + y: data.y + 40, + }; + } + const { properties } = definition.intermediateThrowEvent?.get( + data.properties?.definitionType + ) || {}; + data.properties = { + ...properties, + ...data.properties, + }; + data.properties?.definitionType + && (data.properties!.definitionId = `${ + data.properties?.definitionType + }EventDefinition_${genBpmnId()}`); + super(data, graphModel); + groupRule.call(this); + } + setAttributes(): void { + this.r = 18; + } + } + + return { + type: 'bpmn:intermediateThrowEvent', + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/Event/StartEventFactory.ts b/packages/extension/src/bpmn-elements/presets/Event/StartEventFactory.ts new file mode 100644 index 000000000..95e6eb89a --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Event/StartEventFactory.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + CircleNode, + CircleNodeModel, + NodeConfig, + GraphModel, + h, +} from '@logicflow/core'; +import { genBpmnId } from '../../utils'; + +export function StartEventFactory(lf: any): { + type: string, + model: any, + view: any, +} { + const [definition] = lf.useDefinition(); + class view extends CircleNode { + getAnchorStyle() { + return { + visibility: 'hidden', + }; + } + getShape() { + // @ts-ignore + const { model } = this.props; + const style = model.getNodeStyle(); + const { x, y, r, width, height, properties } = model; + const { definitionType, isInterrupting } = properties; + const { icon } = definition.startEvent?.get(definitionType) || {}; + const i = Array.isArray(icon) + ? h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + ...icon, + ) + : h('path', { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + d: icon, + style: + 'fill: white; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }); + return h( + 'g', + {}, + h('circle', { + ...style, + cx: x, + cy: y, + r, + strokeDasharray: isInterrupting ? '5,5' : '', + strokeWidth: 2, + }), + i, + ); + } + } + + class model extends CircleNodeModel { + constructor(data: NodeConfig, graphModel: GraphModel) { + if (!data.id) { + data.id = `Event_${genBpmnId()}`; + } + if (!data.text) { + data.text = ''; + } + if (data.text && typeof data.text === 'string') { + data.text = { + value: data.text, + x: data.x, + y: data.y + 40, + }; + } + const { properties } = definition.startEvent?.get(data.properties?.definitionType) || {}; + data.properties = { + ...properties, + ...data.properties, + }; + data.properties?.definitionType + && (data.properties!.definitionId = `${ + data.properties?.definitionType + }EventDefinition_${genBpmnId()}`); + super(data, graphModel); + } + setAttributes(): void { + this.r = 18; + } + getConnectedTargetRules() { + const rules = super.getConnectedTargetRules(); + const notAsSource = { + message: '起始节点不能作为边的终点', + validate: (source, target) => { + if (target === this) { + return false; + } + return true; + }, + }; + rules.push(notAsSource); + return rules; + } + } + + return { + type: 'bpmn:startEvent', + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/Event/boundaryEventFactory.ts b/packages/extension/src/bpmn-elements/presets/Event/boundaryEventFactory.ts new file mode 100644 index 000000000..41683907f --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Event/boundaryEventFactory.ts @@ -0,0 +1,111 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + CircleNode, + CircleNodeModel, + GraphModel, + NodeConfig, + h, +} from '@logicflow/core'; +import _ from 'lodash-es'; +import { genBpmnId, groupRule } from '../../utils'; + +export function BoundaryEventFactory(lf: any): { + type: string, + model: any, + view: any, +} { + const [definition] = lf.useDefinition(); + class view extends CircleNode { + getAnchorStyle() { + return { + visibility: 'hidden', + }; + } + getShape() { + // @ts-ignore + const { model } = this.props; + const style = model.getNodeStyle(); + const { x, y, r, width, height, properties } = model; + const { definitionType, cancelActivity } = properties; + const { icon } = definition.boundaryEvent?.get(definitionType) || {}; + const i = Array.isArray(icon) + ? h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + ...icon, + ) + : h('path', { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + d: icon, + }); + return h( + 'g', + {}, + h('circle', { + ...style, + cx: x, + cy: y, + r, + strokeDasharray: cancelActivity ? '5,5' : '', + strokeWidth: 1.5, + }), + h('circle', { + ...style, + cx: x, + cy: y, + r: r - 3, + strokeDasharray: cancelActivity ? '5,5' : '', + strokeWidth: 1.5, + }), + i, + ); + } + } + class model extends CircleNodeModel { + constructor(data: NodeConfig, graphModel: GraphModel) { + if (!data.id) { + data.id = `Event_${genBpmnId()}`; + } + if (!data.text) { + data.text = ''; + } + if (data.text && typeof data.text === 'string') { + data.text = { + value: data.text, + x: data.x, + y: data.y + 40, + }; + } + const { properties } = definition.boundaryEvent?.get(data.properties?.definitionType) || {}; + + data.properties = { + attachedToRef: '', + cancelActivity: false, + ...properties, + ...data.properties, + }; + data.properties?.definitionType && (data.properties!.definitionId = `${ + data.properties?.definitionType + }EventDefinition_${genBpmnId()}`); + super(data, graphModel); + groupRule.call(this); + } + initNodeData(data: any) { + super.initNodeData(data); + this.r = 20; + this.autoToFront = false; // 不自动设置到最顶部,而是使用自己的zIndex + this.zIndex = 99999; // 保证边界事件节点用于在最上方 + } + setAttributes(): void { + this.r = 18; + } + } + + return { + type: 'bpmn:boundaryEvent', + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/Event/index.ts b/packages/extension/src/bpmn-elements/presets/Event/index.ts new file mode 100644 index 000000000..e17723cd3 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Event/index.ts @@ -0,0 +1,14 @@ +import LogicFlow from '@logicflow/core'; +import { EndEventFactory } from './EndEventFactory'; +import { IntermediateCatchEventFactory } from './IntermediateCatchEvent'; +import { StartEventFactory } from './StartEventFactory'; +import { BoundaryEventFactory } from './boundaryEventFactory'; +import { IntermediateThrowEventFactory } from './IntermediateThrowEvent'; + +export function registerEventNodes(lf: LogicFlow) { + lf.register(StartEventFactory(lf)); + lf.register(EndEventFactory(lf)); + lf.register(IntermediateCatchEventFactory(lf)); + lf.register(IntermediateThrowEventFactory(lf)); + lf.register(BoundaryEventFactory(lf)); +} diff --git a/packages/extension/src/bpmn-elements/presets/Flow/flow.d.ts b/packages/extension/src/bpmn-elements/presets/Flow/flow.d.ts new file mode 100644 index 000000000..9d244ccb1 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Flow/flow.d.ts @@ -0,0 +1,6 @@ +export type BBoxType = { + minX: number; + minY: number; + maxX: number; + maxY: number; +} diff --git a/packages/extension/src/bpmn-elements/presets/Flow/index.ts b/packages/extension/src/bpmn-elements/presets/Flow/index.ts new file mode 100644 index 000000000..65b812d23 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Flow/index.ts @@ -0,0 +1,8 @@ +import LogicFlow from '@logicflow/core'; +import { sequenceFlowFactory } from './sequenceFlow'; + +export const SequenceFlow = sequenceFlowFactory(); + +export function registerFlows(lf: LogicFlow) { + lf.register(SequenceFlow); +} diff --git a/packages/extension/src/bpmn-elements/presets/Flow/manhattan.ts b/packages/extension/src/bpmn-elements/presets/Flow/manhattan.ts new file mode 100644 index 000000000..721728485 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Flow/manhattan.ts @@ -0,0 +1,591 @@ +/* eslint-disable no-continue */ +import { Point } from '@logicflow/core'; + +class NodeBase { + x: any; + y: any; + G: number; + H: number; + isProcessed: boolean; + connection: any; + from: any; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + this.G = 0; + this.H = 0; + this.isProcessed = false; + this.connection = null; + this.from = null; + } + get F() { + return this.G + this.H; + } + setProcessed() { + this.isProcessed = true; + } + setConnection(connection: any) { + this.connection = connection; + } + setFrom(from: any) { + this.from = from; + } + setG(g: number) { + this.G = g; + } + setH(h: number) { + this.H = h; + } + getManhattanDistanceTo(point: { x: number; y: number; }) { + const { x: x1, y: y1 } = this; + const { x: x2, y: y2 } = point; + return Math.abs(x1 - x2) + Math.abs(y1 - y2); + } +} + +class PriorityQueue { + heap: any[]; + constructor() { + this.heap = []; + } + + enqueue(node: { x: never; y: never; }, priority: number) { + this.heap.push({ node, priority }); + this.bubbleUp(this.heap.length - 1); + } + + dequeue() { + const min = this.heap[0]; + const end = this.heap.pop(); + if (this.heap.length > 0) { + this.heap[0] = end; + this.sinkDown(0); + } + return min; + } + + bubbleUp(index: number) { + const node = this.heap[index]; + while (index > 0) { + const parentIndex = Math.floor((index - 1) / 2); + const parent = this.heap[parentIndex]; + if (node.priority >= parent.priority) break; + this.heap[parentIndex] = node; + this.heap[index] = parent; + index = parentIndex; + } + } + + sinkDown(index: number) { + const leftChildIndex = 2 * index + 1; + const rightChildIndex = 2 * index + 2; + let smallestChildIndex = index; + const { length } = this.heap; + + if ( + leftChildIndex < length + && this.heap[leftChildIndex].priority + < this.heap[smallestChildIndex].priority + ) { + smallestChildIndex = leftChildIndex; + } + + if ( + rightChildIndex < length + && this.heap[rightChildIndex].priority + < this.heap[smallestChildIndex].priority + ) { + smallestChildIndex = rightChildIndex; + } + + if (smallestChildIndex !== index) { + const swapNode = this.heap[smallestChildIndex]; + this.heap[smallestChildIndex] = this.heap[index]; + this.heap[index] = swapNode; + this.sinkDown(smallestChildIndex); + } + } + + isEmpty() { + return this.heap.length === 0; + } +} + +function expandBBox(bbox: BBoxType, offset: number) { + const { minX, minY, maxX, maxY } = bbox; + return { + minX: minX - offset, + minY: minY - offset, + maxX: maxX + offset, + maxY: maxY + offset, + }; +} + +function getPointsFromBBoxBorder(bbox: BBoxType) { + const { minX, minY, maxX, maxY } = bbox; + return [ + { x: minX, y: minY }, + { + x: minX + (maxX - minX) / 2, + y: minY, + }, + { x: maxX, y: minY }, + { + x: maxX, + y: minY + (maxY - minY) / 2, + }, + { x: maxX, y: maxY }, + { + x: minX + (maxX - minX) / 2, + y: maxY, + }, + { x: minX, y: maxY }, + { + x: minX, + y: minY + (maxY - minY) / 2, + }, + ]; +} + +function getHull(points: any[]) { + const xs = points.map((item: { x: any; }) => item.x); + const ys = points.map((item: { y: any; }) => item.y); + return { + minX: Math.min(...xs), + minY: Math.min(...ys), + maxX: Math.max(...xs), + maxY: Math.max(...ys), + }; +} + +function isPointInsideTheBoxes(point: NodeBase | Point, bboxes: BBoxType[]) { + let flag = false; + for (const bbox of bboxes) { + if (isBBoxContainThePoint(bbox, point)) { + flag = true; + break; + } + } + return flag; +} + +function isBBoxContainThePoint(bbox: BBoxType, p: NodeBase | Point) { + const { x, y } = p; + const { minX, minY, maxX, maxY } = bbox; + // ignore the point on the border + return x > minX && x < maxX && y > minY && y < maxY; +} + +function isSegmentsIntersected(seg1: any[], seg2: any[]) { + const [p0, p1] = seg1; + const [p2, p3] = seg2; + const s1x = p1.x - p0.x; + const s1y = p1.y - p0.y; + const s2x = p3.x - p2.x; + const s2y = p3.y - p2.y; + + const s = (-s1y * (p0.x - p2.x) + s1x * (p0.y - p2.y)) / (-s2x * s1y + s1x * s2y); + const t = (s2x * (p0.y - p2.y) - s2y * (p0.x - p2.x)) / (-s2x * s1y + s1x * s2y); + + return s >= 0 && s <= 1 && t >= 0 && t <= 1; +} + +function getVerticesFromBBox(bbox:BBoxType) { + const { minX, minY, maxX, maxY } = bbox; + return [ + { x: minX, y: minY }, + { x: maxX, y: minY }, + { x: maxX, y: maxY }, + { x: minX, y: maxY }, + ]; +} + +function isSegmentCrossingBBox(line: any, bbox: BBoxType) { + const [p1, p2] = line; + const { minX, minY, maxX, maxY } = bbox; + const width = Math.abs(maxX - minX); + const height = Math.abs(maxY - minY); + if (width === 0 && height === 0) { + return false; + } + const [pa, pb, pc, pd] = getVerticesFromBBox(bbox); + let count = 0; + if (isSegmentsIntersected([p1, p2], [pa, pb])) { + count++; + } + if (isSegmentsIntersected([p1, p2], [pa, pd])) { + count++; + } + if (isSegmentsIntersected([p1, p2], [pb, pc])) { + count++; + } + if (isSegmentsIntersected([p1, p2], [pc, pd])) { + count++; + } + return count !== 0; +} + +function aStarFindPathByGrid( + startNode: NodeBase, + endNode: NodeBase, + step: number, + bboxes: any[], + outside: BBoxType, +) { + let toSearch = [startNode]; + const searchSet = new Set(); + + while (toSearch.length) { + let current = toSearch[0]; + + for (const item of toSearch) { + if (item.F < current.F || (item.F === current.F && item.H < current.H)) { + current = item; + } + } + + if (`${current.x}/${current.y}` === `${endNode.x}/${endNode.y}`) { + const res = [{ x: current.x, y: current.y }]; + while (current.connection) { + const { connection } = current; + res.push({ + x: connection.x, + y: connection.y, + }); + current = current.connection; + } + return res.reverse(); + } + + const val = `${current.x}/${current.y}`; + + !searchSet.has(val) && searchSet.add(val); + + toSearch = toSearch.filter( + (item) => `${current.x}/${current.y}` !== `${item.x}/${item.y}`, + ); + + const neighborsRes = findNeighborsByGridStep( + current, + step, + bboxes, + outside, + ).filter((item) => { + const flag = !isPointInsideTheBoxes(item, bboxes); + return flag; + }); + + const tmpRes: NodeBase[] = []; + neighborsRes.forEach((item: NodeBase) => { + const key = `${item.x}/${item.y}`; + if (!searchSet.has(key)) { + tmpRes.push(item); + tmpRes.push(item); + } + }); + + for (const neighbor of tmpRes) { + if (neighbor.isProcessed) continue; + const inSearch = toSearch.includes(current); + const costToNeighbor = current.G + current.getManhattanDistanceTo(neighbor); + + if (!inSearch || costToNeighbor < neighbor.G) { + neighbor.setG(costToNeighbor); + neighbor.setConnection(current); + current.setFrom(neighbor); + + if (!inSearch) { + neighbor.setH(neighbor.getManhattanDistanceTo(endNode)); + toSearch.push(neighbor); + } + } + } + } + return null; +} + +function findNeighborsByGridStep(cur: NodeBase, step: number, bboxes: any, outside:BBoxType) { + const neighbors: NodeBase[] = []; + const { x, y } = cur; + const { minX, minY, maxX, maxY } = outside; + const x1 = x - step; + const x2 = x + step; + const y1 = y - step; + const y2 = y + step; + // eslint-disable-next-line no-shadow + function isValid(cur: NodeBase | Point, neighbor: NodeBase | Point, bboxes: BBoxType[]) { + let flag = !isPointInsideTheBoxes(neighbor, bboxes) + && !isPointInsideTheBoxes(cur, bboxes); + if (!flag) return false; + for (const bbox of bboxes) { + if ( + isSegmentCrossingBBox( + [ + { x: cur.x, y: cur.y }, + { x: neighbor.x, y: neighbor.y }, + ], + bbox, + ) + ) { + flag = false; + break; + } + } + return flag; + } + if (x1 >= minX) { + isValid(cur, { x: x1, y }, bboxes) && neighbors.push(new NodeBase(x1, y)); + } + if (x2 <= maxX) { + isValid(cur, { x: x2, y }, bboxes) && neighbors.push(new NodeBase(x2, y)); + } + if (y1 >= minY) { + isValid(cur, { x, y: y1 }, bboxes) && neighbors.push(new NodeBase(x, y1)); + } + if (y2 <= maxY) { + isValid(cur, { x, y: y2 }, bboxes) && neighbors.push(new NodeBase(x, y2)); + } + return neighbors; +} + +function getAnchorWithOffset({ bbox }: any, node: NodeBase, offset: number) { + const { minX, minY, maxX, maxY } = bbox; + const { x, y } = node; + if (x === minX) { + return { + x: x - offset, + y, + }; + } + if (x === maxX) { + return { + x: x + offset, + y, + }; + } + if (y === minY) { + return { + x, + y: y - offset, + }; + } + if (y === maxY) { + return { + x, + y: y + offset, + }; + } +} + +function perpendicularDistance(point: NodeBase, lineStart: NodeBase, lineEnd: NodeBase) { + const { x: x1, y: y1 } = lineStart; + const { x: x2, y: y2 } = lineEnd; + const { x, y } = point; + + if (x1 === x2) { + // 线段是垂直的 + return Math.abs(x - x1); + } + + if (y1 === y2) { + // 线段是水平的 + return Math.abs(y - y1); + } + + // 计算点到线段垂直点的坐标 + const px = x1 + + ((x2 - x1) * ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1))) + / ((x2 - x1) ** 2 + (y2 - y1) ** 2); + const py = y1 + + ((y2 - y1) * ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1))) + / ((x2 - x1) ** 2 + (y2 - y1) ** 2); + + // 计算曼哈顿距离 + return Math.abs(x - px) + Math.abs(y - py); +} + +function perpendicularToStraight(line: string | any[]) { + // Step 1: Convert perpendicular segments to straight lines + const straightLine = [line[0]]; + for (let i = 0; i < line.length - 2; i++) { + const point1 = line[i]; + const point2 = line[i + 1]; + const point3 = line[i + 2]; + + if ( + isVertical(point1, point2, point3) + || isHorizontal(point1, point2, point3) + ) { + // Remove point2 to make it a straight line + continue; + } + + straightLine.push(point2); + } + straightLine.push(line[line.length - 1]); + + // Step 2: Douglas-Peucker algorithm to remove redundant points + // return straightLine; + const epsilon = 1.0; // Adjust epsilon based on your requirements + return douglasPeucker(straightLine, epsilon); +} + +function isVertical(p1: { x: any; }, p2: { x: any; }, p3: { x: any; }) { + return p1.x === p2.x && p2.x === p3.x; +} + +function isHorizontal(p1: { y: any; }, p2: { y: any; }, p3: { y: any; }) { + return p1.y === p2.y && p2.y === p3.y; +} + +function douglasPeucker(points: string | any[], epsilon: number) { + let dmax = 0; + let index = 0; + + for (let i = 1; i < points.length - 1; i++) { + const d = perpendicularDistance( + points[i], + points[0], + points[points.length - 1], + ); + if (d > dmax) { + index = i; + dmax = d; + } + } + + if (dmax > epsilon) { + const left = douglasPeucker(points.slice(0, index + 1), epsilon); + const right = douglasPeucker(points.slice(index), epsilon); + + return left.slice(0, left.length - 1).concat(right); + } + return [points[0], points[points.length - 1]]; + +} + +// 每三个点如果其横坐标或者纵坐标都相同,则取其二 +function getSimplePath(path: string | any[]) { + // if (path.length < 5) return path; + path = circleDetection(path); + const res: NodeBase[] = []; + for (let i = 0; i < path.length;) { + const point1 = path[i]; + const point2 = path[i + 1]; + const point3 = path[i + 2]; + if (!point3) { + res.push(point1); + i++; + continue; + } + if ( + (point1.x === point2.x && point2.x === point3.x) + || (point1.y === point2.y && point2.y === point3.y) + ) { + res.push(point1); + res.push(point3); + i += 3; + } else { + res.push(point1); + i++; + } + } + return res; +} + +// 回环检测 & 处理 +function circleDetection(path: string | any[]) { + if (path.length < 6) return path; + + const res: Array = []; + for (let i = 0; i < path.length;) { + const point1 = path[i]; + const point2 = path[i + 1]; + const point4 = path[i + 3]; + const point5 = path[i + 4]; + if (!point5) { + res.push(point1); + i++; + continue; + } + if (isSegmentsIntersected([point1, point2], [point4, point5])) { + let x = 0; + let y = 0; + if (point1.x === point2.x) { + x = point1.x; + y = point4.y; + } else { + x = point4.x; + y = point1.y; + } + res.push({ x, y }); + res.push(point5); + i += 4; + continue; + } + res.push(point1); + i++; + } + return res; +} + +function getOrient(start: NodeBase, end: NodeBase) { + const { x: x1, y: y1 } = start; + const { x: x2, y: y2 } = end; + let prefix = ''; + let suffix = ''; + if (x1 >= x2) { + prefix = 'left'; + } else { + prefix = 'right'; + } + if (y1 >= y2) { + suffix = 'top'; + } else { + suffix = 'bottom'; + } + return `${prefix}:${suffix}`; +} + +export function ManhattanLayout( + startAnchor: any, + endAnchor: any, + startNode: { bbox: any; }, + endNode: { bbox: any; }, + // obstacles, + offset: any, +) { + // get expanded bbox + const { bbox: startBBox } = startNode; + const { bbox: endBBox } = endNode; + const startExpandBBox = expandBBox(startNode.bbox, offset); + const endExpandBBox = expandBBox(endNode.bbox, offset); + // get points from bbox border + const points1 = getPointsFromBBoxBorder(startExpandBBox); + const points2 = getPointsFromBBoxBorder(endExpandBBox); + // is bbox overlap + // const overlap = isBBoxOverlap(startBBox, endBBox); + const outsideBBox = getHull([...points1, ...points2]); + + const sNode = getAnchorWithOffset(startNode, startAnchor, offset); + const eNode = getAnchorWithOffset(endNode, endAnchor, offset); + + const sNodeBase = new NodeBase(sNode!.x, sNode!.y); + const eNodeBase = new NodeBase(eNode!.x, eNode!.y); + + const path = aStarFindPathByGrid( + eNodeBase, + sNodeBase, + 10, + // obstacles, + [startBBox, endBBox], + // [startExpandBBox, endExpandBBox], + outsideBBox, + ); + + if (path) { + const simplifiedPath = perpendicularToStraight(path); + + return getSimplePath([endAnchor, ...simplifiedPath, startAnchor].reverse()); + } +} diff --git a/packages/extension/src/bpmn-elements/presets/Flow/sequenceFlow.ts b/packages/extension/src/bpmn-elements/presets/Flow/sequenceFlow.ts new file mode 100644 index 000000000..2c5b08531 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Flow/sequenceFlow.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + EdgeConfig, + PolylineEdge, + PolylineEdgeModel, + GraphModel, + h, +} from '@logicflow/core'; +import { JSX } from 'preact'; +import { genBpmnId } from '../../utils'; + +type SequenceFlowType = { + panels: string[]; + [key: string]: any; +}; + +export function sequenceFlowFactory(props?: any): { + type: string, + model: any, + view: any } { + class model extends PolylineEdgeModel { + static extendKey = 'SequenceFlowModel'; + constructor(data: EdgeConfig, graphModel: GraphModel) { + if (!data.id) { + data.id = `Flow_${genBpmnId()}`; + } + const properties: SequenceFlowType = { + ...data.properties, + ...props, + // panels: ['condition'], + // isDefaultFlow: false, + }; + data.properties = properties; + + super(data, graphModel); + } + } + + class view extends PolylineEdge { + static extendKey = 'SequenceFlowEdge'; + getStartArrow(): JSX.Element | null { + // eslint-disable-next-line no-shadow + const { model } = this.props; + const { isDefaultFlow } = model.properties; + return isDefaultFlow + ? h('path', { + refX: 15, + stroke: '#000000', + strokeWidth: 2, + d: 'M 20 5 10 -5 z', + }) + : h('path', { + d: '', + }); + } + } + + return { + type: 'bpmn:sequenceFlow', + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/Gateway/gateway.ts b/packages/extension/src/bpmn-elements/presets/Gateway/gateway.ts new file mode 100644 index 000000000..f57abf18e --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Gateway/gateway.ts @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + GraphModel, + NodeConfig, + PolygonNode, + PolygonNodeModel, + h, +} from '@logicflow/core'; +import { genBpmnId, groupRule } from '../../utils'; + +const gateway = { + exclusive: 0, + inclusive: 1, + parallel: 2, +}; + +/** + * index 0 排他网关 + * index 1 包容网关 + * index 2 并行网关 + */ +export const gatewayComposable = [ + [1, 1, 0], + [0, 0, 1], + [0, 1, 1], +]; + +/** + * @param type 网关节点的type, 对应其XML定义中的节点名,如 其type为bpmn:inclusiveGateway + * @param icon 网关节点左上角的图标,可以是svg path,也可以是h函数生成的svg + * @param props (可选) 网关节点的属性 + * @returns { type: string, model: any, view: any } + */ +export function GatewayNodeFactory(type: string, icon: string | object, props?: any): { + type: string, + model: any, + view: any, +} { + class view extends PolygonNode { + getShape() { + // @ts-ignore + const { model } = this.props; + const { x, y, width, height, points } = model; + const style = model.getNodeStyle(); + return h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + h('polygon', { + ...style, + x, + y, + points, + }), + typeof icon === 'string' + ? h('path', { + d: icon, + ...style, + fill: 'rgb(34, 36, 42)', + strokeWidth: 1, + }) + : icon, + ); + } + } + + class model extends PolygonNodeModel { + constructor(data: NodeConfig, graphModel: GraphModel) { + if (!data.id) { + data.id = `Gateway_${genBpmnId()}`; + } + if (!data.text) { + data.text = ''; + } + if (data.text && typeof data.text === 'string') { + data.text = { + value: data.text, + x: data.x, + y: data.y + 40, + }; + } + data.properties = { + ...props, + ...data.properties, + }; + super(data, graphModel); + this.points = [ + [25, 0], + [50, 25], + [25, 50], + [0, 25], + ]; + groupRule.call(this); + } + } + + return { + type, + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/Gateway/index.ts b/packages/extension/src/bpmn-elements/presets/Gateway/index.ts new file mode 100644 index 000000000..eaca89d00 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Gateway/index.ts @@ -0,0 +1,14 @@ +import LogicFlow from '@logicflow/core'; +import { exclusiveIcon, parallelIcon, inclusiveIcon } from '../icons'; +import { GatewayNodeFactory } from './gateway'; + +export function registerGatewayNodes(lf: LogicFlow) { + const ExclusiveGateway = GatewayNodeFactory('bpmn:exclusiveGateway', exclusiveIcon); + + const ParallelGateway = GatewayNodeFactory('bpmn:parallelGateway', parallelIcon); + + const InclusiveGateway = GatewayNodeFactory('bpmn:inclusiveGateway', inclusiveIcon); + lf.register(ExclusiveGateway); + lf.register(InclusiveGateway); + lf.register(ParallelGateway); +} diff --git a/packages/extension/src/bpmn-elements/presets/Pool/Lane.ts b/packages/extension/src/bpmn-elements/presets/Pool/Lane.ts new file mode 100644 index 000000000..791d3de8f --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Pool/Lane.ts @@ -0,0 +1,189 @@ +import { h } from '@logicflow/core'; +import { GroupNode } from '@logicflow/extension'; +import { laneToJSON } from '.'; + +// 泳道 +class LaneModel extends GroupNode.model { + initNodeData(data: { + width: number; + height: number; + properties: Record; + }) { + data.properties = { + ...data.properties, + processRef: '', + panels: ['processRef'], + }; + super.initNodeData(data); + if (data.width) { + this.width = data.width; + } + if (data.height) { + this.height = data.height; + } + if (data.properties) { + this.properties = { + ...this.properties, + ...data.properties, + }; + } + this.draggable = false; + this.resizable = true; + this.zIndex = 1; + this.toJSON = laneToJSON; + } + + changeAttribute({ width, height, x, y }: any) { + if (width) this.width = width; + if (height) this.height = height; + if (x) this.x = x; + if (y) this.y = y; + } +} + +class LaneView extends GroupNode.view { + getOperateIcon() { + const { model } = this.props; + const { isSelected } = model; + if (!isSelected) { + return null; + } + return [this.addAboveIcon(), this.addBelowIcon(), this.deleteIcon()]; + } + + addAboveIcon() { + const { x, y, width, height, id } = this.props.model; + return h( + 'g', + { + cursor: 'pointer', + onClick: () => { + const groupId = this.props.graphModel.group.nodeGroupMap.get(id); + if (groupId) { + const groupModel = this.props.graphModel.getNodeModelById(groupId); + groupModel.addChildAbove({ x, y, width, height }); + } + }, + }, + [ + h('rect', { + height: 7, + width: 20, + strokeWidth: 1, + fill: '#fff', + stroke: '#000', + strokeDasharray: '3 3', + x: x + width / 2 + 15, + y: y - height / 2 + 3, + }), + h('rect', { + height: 10, + width: 20, + strokeWidth: 1, + fill: '#fff', + stroke: '#000', + x: x + width / 2 + 15, + y: y - height / 2 + 10, + }), + ], + ); + } + addBelowIcon() { + const { x, y, width, height, id } = this.props.model; + return h( + 'g', + { + cursor: 'pointer', + onClick: () => { + const groupId = this.props.graphModel.group.nodeGroupMap.get(id); + if (groupId) { + const groupModel = this.props.graphModel.getNodeModelById(groupId); + groupModel.addChildBelow({ x, y, width, height }); + } + }, + }, + [ + h('rect', { + height: 7, + width: 20, + strokeWidth: 1, + fill: '#fff', + stroke: '#000', + strokeDasharray: '3 3', + x: x + width / 2 + 15, + y: y - height / 2 + 32 + 5, + }), + h('rect', { + height: 10, + width: 20, + strokeWidth: 1, + fill: '#fff', + stroke: '#000', + x: x + width / 2 + 15, + y: y - height / 2 + 2.5 + 20 + 5, + }), + ], + ); + } + deleteIcon() { + const { x, y, width, height, id } = this.props.model; + return h( + 'g', + { + cursor: 'pointer', + onClick: () => { + const groupId = this.props.graphModel.group.nodeGroupMap.get(id); + if (groupId) { + const groupModel = this.props.graphModel.getNodeModelById(groupId); + groupModel.deleteChild(id); + } + }, + }, + [ + h('rect', { + height: 20, + width: 20, + rx: 2, + ry: 2, + strokeWidth: 1, + fill: 'transparent', + stroke: 'transparent', + x: x + width / 2 + 14, + y: y - height / 2 + 50, + }), + h( + 'svg', + { + transform: 'translate(1.000000, 1.000000)', + // fill: '#3C96FE', + x: x + width / 2 + 14, + y: y - height / 2 + 50, + width: 20, + height: 20, + }, + [ + h('path', { + 'pointer-events': 'none', + d: 'M15.3,1.4 L12.6,1.4 L12.6,0 L5.4,0 L5.4,1.4 L0,1.4 L0,2.8 L2,2.8 L2,17.3 C2,17.6865993 2.31340068,18 2.7,18 L15.3,18 C15.6865993,18 16,17.6865993 16,17.3 L16,2.8 L18,2.8 L18,1.4 L15.3,1.4 Z M14.6,16.6 L3.4,16.6 L3.4,2.8 L14.6,2.8 L14.6,16.6 Z', + }), + h('path', { + 'pointer-events': 'none', + d: 'M6,5.4 L7.4,5.4 L7.4,14.4 L6,14.4 L6,5.4 Z M10.6,5.4 L12,5.4 L12,14.4 L10.6,14.4 L10.6,5.4 Z', + }), + ], + ), + ], + ); + } + getResizeShape() { + return h('g', {}, [super.getResizeShape(), this.getOperateIcon()]); + } +} + +const LaneNode = { + type: 'lane', + view: LaneView, + model: LaneModel, +}; + +export default LaneNode; diff --git a/packages/extension/src/bpmn-elements/presets/Pool/Pool.ts b/packages/extension/src/bpmn-elements/presets/Pool/Pool.ts new file mode 100644 index 000000000..50376dfef --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Pool/Pool.ts @@ -0,0 +1,286 @@ +/* eslint-disable no-case-declarations */ +/** + * 泳道节点 + */ +import { h } from '@logicflow/core'; +import { GroupNode } from '@logicflow/extension'; +import { poolToJSON } from '.'; + +const laneMinSize = { + width: 312, + height: 72, +}; + +class HorizontalLaneModel extends GroupNode.model { + initNodeData(data: { + width: number; + height: number; + properties: Record; + }) { + super.initNodeData(data); + this.height = 260; + // this.foldable = true + this.foldedWidth = 42; + this.resizable = true; + this.zIndex = 1; + this.text.editable = true; + this.toJSON = poolToJSON; + } + + setAttributes() { + this.text = { + ...this.text, + value: this.text.value || '泳池示例', + x: this.x - this.width / 2 + 11, + y: this.y, + }; + } + + getTextStyle() { + const style = super.getTextStyle(); + style.textWidth = 16; + return style; + } + + foldGroup(isFolded: boolean) { + this.setProperty('isFolded', isFolded); + this.isFolded = isFolded; + // step 1 + if (isFolded) { + this.x = this.x - this.width / 2 + this.foldedWidth / 2; + this.unfoldedWidth = this.width; + this.unfoldedHight = this.height; + this.width = this.foldedWidth; + } else { + this.width = this.unfoldedWidth; + this.x = this.x + this.width / 2 - this.foldedWidth / 2; + } + // step 2 + let allEdges = this.incoming.edges.concat(this.outgoing.edges); + this.children.forEach((elementId) => { + const nodeModel: any = this.graphModel.getElement(elementId); + nodeModel.visible = !isFolded; + allEdges = allEdges.concat( + nodeModel.incoming.edges.concat(nodeModel.outgoing.edges), + ); + }); + // step 3 + // @ts-ignore + this.foldEdge(isFolded, allEdges); + } + // 感应泳道变化,调整宽高 + resize(resizeId?: string, newNodeSize?: { x: number; width: number }) { + if (!this.children.size) { + return; + } + let minX: number | null = null; + let maxX: number | null = null; + let minY: number | null = null; + let maxY: number | null = null; + let hasMaxX = false; + // 找到边界 + this.children.forEach((elementId) => { + const nodeModel: any = this.graphModel.getElement(elementId); + const { x, y, width, height, type, id } = nodeModel; + if (type !== 'lane') { + return; + } + if (id === resizeId && newNodeSize) { + minX = newNodeSize.x - newNodeSize.width / 2; + maxX = newNodeSize.x + newNodeSize.width / 2; + hasMaxX = true; + } + if (!hasMaxX && (!minX || x - width / 2 < minX)) { + minX = x - width / 2; + } + if (!hasMaxX && (!maxX || x + width / 2 > maxX)) { + maxX = x + width / 2; + } + if (!minY || y - height / 2 < minY) { + minY = y - height / 2; + } + if (!maxY || y + height / 2 > maxY) { + maxY = y + height / 2; + } + }); + if (minX && maxX && minY && maxY) { + this.width = maxX - minX + 30; + this.height = maxY - minY; + this.x = minX + (maxX - minX) / 2 - 15; + this.y = minY + (maxY - minY) / 2; + this.setAttributes(); + this.resizeChildren({}); + } + } + + resizeChildren({ resizeDir = '', deltaHeight = 0 }) { + const { x, y, width } = this; + const laneChildren: any[] = []; + this.children.forEach((elementId) => { + const nodeModel: any = this.graphModel.getElement(elementId); + const { type } = nodeModel; + if (type === 'lane') { + laneChildren.push(nodeModel); + } + }); + // 按照位置排序 + laneChildren.sort((a, b) => { + if (a.y < b.y) { + return -1; + } + return 1; + + }); + // 把泳池resize的高度加进来 + switch (resizeDir) { + case 'below': + // 高度加在最下面的泳道上 + const lastLane = laneChildren[laneChildren.length - 1]; + lastLane.height = lastLane.height + deltaHeight < laneMinSize.height + ? laneMinSize.height + : lastLane.height + deltaHeight; + laneChildren[laneChildren.length - 1] = lastLane; + break; + case 'above': + // 高度加在最上面的泳道上 + const firstLane = laneChildren[0]; + firstLane.height = firstLane.height + deltaHeight < laneMinSize.height + ? laneMinSize.height + : firstLane.height + deltaHeight; + laneChildren[0] = firstLane; + break; + default: + break; + } + const poolHeight = laneChildren.reduce((a, b) => a + b.height, 0); + let aboveNodeHeights = 0; + laneChildren.forEach((nodeModel, index) => { + const { height } = nodeModel; + nodeModel.changeAttribute({ + width: width - 30, + height, + x: x + 15, + y: y - poolHeight / 2 + aboveNodeHeights + height / 2, + }); + aboveNodeHeights += height; + }); + this.height = poolHeight; + } + + addChild(childId: any) { + super.addChild(childId); + this.graphModel.group.nodeGroupMap?.set(childId, this.id); + } + + addChildAbove({ x, y, width, height }: any) { + this.children.forEach((elementId) => { + const nodeModel: any = this.graphModel.getElement(elementId); + const { type, y: childY } = nodeModel; + if (type !== 'lane') { + return; + } + // 在被操作的泳道之上 + if (childY < y) { + nodeModel.changeAttribute({ y: childY - 120 }); + } + }); + const { id: laneId } = this.graphModel.addNode({ + type: 'lane', + properties: { + nodeSize: { + width, + height: 120, + }, + }, + x, + y: y - height / 2 - 60, + }); + this.addChild(laneId); + this.height = this.height + 120; + this.y = this.y - 60; + } + + addChildBelow({ x, y, width, height }: any) { + this.children.forEach((elementId) => { + const nodeModel: any = this.graphModel.getElement(elementId); + const { type, y: childY } = nodeModel; + if (type !== 'lane') { + return; + } + // 在被操作的泳道之下 + if (childY > y) { + nodeModel.changeAttribute({ y: childY + 120 }); + } + }); + const { id: laneId } = this.graphModel.addNode({ + type: 'lane', + properties: { + nodeSize: { + width, + height: 120, + }, + }, + x, + y: y + height / 2 + 60, + }); + this.addChild(laneId); + this.height = this.height + 120; + this.y = this.y + 60; + } + + deleteChild(childId: any) { + const laneChildren: any[] = []; + this.children.forEach((elementId) => { + const nodeModel: any = this.graphModel.getElement(elementId); + const { type } = nodeModel; + if (type === 'lane') { + laneChildren.push(nodeModel); + } + }); + if (laneChildren.length <= 1) { + return; + } + this.removeChild(childId); + this.graphModel.deleteNode(childId); + this.resize(); + } +} + +class HorizontalLaneView extends GroupNode.view { + getResizeShape() { + const { model } = this.props; + const { x, y, width, height } = model; + const style = model.getNodeStyle(); + // 标题区域 + const foldRectAttrs = { + ...style, + x: x - width / 2, + y: y - height / 2, + width: 30, + height, + }; + // 泳道区域 + const transRectAttrs = { + ...style, + x: x - width / 2 + 30, + y: y - height / 2, + width: width - 30, + height, + fill: 'transparent', + }; + return h('g', {}, [ + // this.getAddAbleShape(), + h('rect', { ...foldRectAttrs }), + h('rect', { ...transRectAttrs }), + this.getFoldIcon(), + ]); + } +} + +const PoolNode = { + type: 'pool', + view: HorizontalLaneView, + model: HorizontalLaneModel, +}; + +export default PoolNode; diff --git a/packages/extension/src/bpmn-elements/presets/Pool/index.ts b/packages/extension/src/bpmn-elements/presets/Pool/index.ts new file mode 100644 index 000000000..66959a874 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Pool/index.ts @@ -0,0 +1,86 @@ +import LogicFlow from '@logicflow/core'; +import PoolNode from './Pool'; +import LaneNode from './Lane'; + +export function laneToJSON(data: any) { + return ``; +} + +export function poolToJSON(data: any) { + return ` + ${data.children.map((child: any) => child.toJSON(child)).join('\t\n')} +`; +} + +function poolEvent(lf: any) { + const selectAll = () => { + const { nodes = [], edges = [] } = lf.getGraphData(); + nodes.forEach((node: any) => { + const { id } = node; + if (id) { + lf.selectElementById(id); + } + }); + edges.forEach((edge: any) => { + const { id } = edge; + if (id) { + lf.selectElementById(id); + } + }); + return false; + }; + + lf.keyboard.on('cmd + a', selectAll); + lf.keyboard.on('ctrl + a', selectAll); + lf.on('node:dnd-add, edge:add', ({ data }: any) => { + const { x, y, type, id } = data; + if (type === 'pool') { + lf.setProperties(data.id, {}); + const poolModel = lf.getNodeModelById(id); + const { width, height } = poolModel; + const { id: laneId } = lf.addNode({ + type: 'lane', + properties: { + nodeSize: { + width: width - 30, + height, + }, + }, + x: x + 15, + y, + }); + poolModel.addChild(laneId); + } + }); + lf.on('node:resize', ({ oldNodeSize, newNodeSize }: any) => { + const { id, type } = oldNodeSize; + const deltaHeight = newNodeSize.height - oldNodeSize.height; + // const resizeDir = newNodeSize.y - oldNodeSize.y > 0 ? 'below': 'above' + // 节点高度变高,y下移, 方向为below + // 节点高度变高, y上移, 方向为above + // 节点高度变小, y下移, 方向为above + // 节点高度变小, y上移,方向为below + let resizeDir = 'below'; + if (deltaHeight > 0 && newNodeSize.y - oldNodeSize.y < 0) { + resizeDir = 'above'; + } else if (deltaHeight < 0 && newNodeSize.y - oldNodeSize.y > 0) { + resizeDir = 'above'; + } + if (type === 'pool') { + // 泳池缩放,泳道一起调整 + lf.getNodeModelById(id).resizeChildren({ resizeDir, deltaHeight }); + } else if (type === 'lane') { + // 泳道缩放, 调整泳池 + const groupId = lf.extension.group.nodeGroupMap.get(id); + if (groupId) { + lf.getNodeModelById(groupId).resize(id, newNodeSize); + } + } + }); +} + +export const registerPoolNodes = (lf: LogicFlow) => { + lf.register(PoolNode); + lf.register(LaneNode); + poolEvent(lf); +}; diff --git a/packages/extension/src/bpmn-elements/presets/Task/index.ts b/packages/extension/src/bpmn-elements/presets/Task/index.ts new file mode 100644 index 000000000..94599a39b --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Task/index.ts @@ -0,0 +1,101 @@ +import LogicFlow from '@logicflow/core'; +import { serviceTaskIcon, userTaskIcon } from '../icons'; +import { TaskNodeFactory } from './task'; +import { SubProcessFactory } from './subProcess'; + +function boundaryEvent(lf: any) { + const nodeBoundaryMap: any = new Map(); + + lf.on('node:drag,node:dnd-drag', checkAppendBoundaryEvent); + lf.on('node:drop,node:dnd-add', appendBoundaryEvent); + lf.graphModel.addNodeMoveRules( + ( + model: { isTaskNode: any; boundaryEvents: any }, + deltaX: any, + deltaY: any, + ) => { + if (model.isTaskNode) { + // 如果移动的是分组,那么分组的子节点也跟着移动。 + const nodeIds = model.boundaryEvents; + lf.graphModel.moveNodes(nodeIds, deltaX, deltaY, true); + return true; + } + return true; + }, + ); + function appendBoundaryEvent(this: any, { data }: any) { + const preBoundaryNodeId = nodeBoundaryMap.get(data.id); + const closeNodeId = checkAppendBoundaryEvent({ data }); + if (closeNodeId) { + const taskNodeModel = lf.graphModel.getNodeModelById(closeNodeId); + taskNodeModel.setIsCloseToBoundary(false); + taskNodeModel.addBoundaryEvent(data.id); + const boundaryNodeModel = lf.graphModel.getNodeModelById(data.id); + boundaryNodeModel.setProperties({ + attachedToRef: closeNodeId, + }); + nodeBoundaryMap.set(data.id, closeNodeId); + } + if (preBoundaryNodeId !== closeNodeId) { + const preNodeModel = lf.graphModel.getNodeModelById(preBoundaryNodeId); + if (preNodeModel) { + preNodeModel.deleteBoundaryEvent(data.id); + } + } + } + // 判断此节点是否在某个节点的边界上 + // 如果在,且这个节点model存在属性isTaskNode,则调用这个方法 + function checkAppendBoundaryEvent(this: any, { data }: any) { + const { x, y, id, type } = data; + if (type !== 'bpmn:boundaryEvent') { + return; + } + const { nodes } = lf.graphModel; + let closeNodeId = ''; + for (let i = 0; i < nodes.length; i++) { + const nodeModel = nodes[i]; + if (nodeModel.isTaskNode && nodeModel.id !== id) { + if (isCloseNodeEdge(nodeModel, x, y) && !closeNodeId) { + // 同时只允许在一个节点的边界上 + nodeModel.setIsCloseToBoundary(true); + closeNodeId = nodeModel.id; + } else { + nodeModel.setIsCloseToBoundary(false); + } + } + } + return closeNodeId; + } + function isCloseNodeEdge( + nodeModel: { x: number; width: number; y: number; height: number }, + x: number, + y: number, + ) { + if ( + Math.abs(Math.abs(nodeModel.x - x) - nodeModel.width / 2) < 10 + && y >= nodeModel.y - nodeModel.height / 2 - 10 + && y <= nodeModel.y + nodeModel.height / 2 + 10 + ) { + return true; + } + if ( + Math.abs(Math.abs(nodeModel.y - y) - nodeModel.height / 2) < 10 + && x >= nodeModel.x - nodeModel.width / 2 - 10 + && x <= nodeModel.x + nodeModel.width / 2 + 10 + ) { + return true; + } + return false; + } +} + +export function registerTaskNodes(lf: LogicFlow) { + const ServiceTask = TaskNodeFactory('bpmn:serviceTask', serviceTaskIcon); + const UserTask = TaskNodeFactory('bpmn:userTask', userTaskIcon); + + lf.register(ServiceTask); + lf.register(UserTask); + lf.register(SubProcessFactory()); + + boundaryEvent(lf); +} diff --git a/packages/extension/src/bpmn-elements/presets/Task/subProcess.ts b/packages/extension/src/bpmn-elements/presets/Task/subProcess.ts new file mode 100644 index 000000000..fd6081a97 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Task/subProcess.ts @@ -0,0 +1,172 @@ +/* eslint-disable no-shadow */ +/* eslint-disable @typescript-eslint/naming-convention */ +import { h } from '@logicflow/core'; +import { GroupNode } from '@logicflow/extension'; + +export function SubProcessFactory(): { + type: string, + model: any, + view: any } { + class view extends GroupNode.view { + getFoldIcon() { + const { model } = this.props; + const { x, y, width, height, properties, foldable } = model; + const foldX = model.x - model.width / 2 + 5; + const foldY = model.y - model.height / 2 + 5; + if (!foldable) return null; + const iconIcon = h('path', { + fill: 'none', + stroke: '#818281', + strokeWidth: 2, + 'pointer-events': 'none', + d: properties.isFolded + ? `M ${foldX + 3},${foldY + 6} ${foldX + 11},${foldY + 6} M${ + foldX + 7 + },${foldY + 2} ${foldX + 7},${foldY + 10}` + : `M ${foldX + 3},${foldY + 6} ${foldX + 11},${foldY + 6} `, + }); + return h('g', {}, [ + h('rect', { + height: 12, + width: 14, + rx: 2, + ry: 2, + strokeWidth: 1, + fill: '#F4F5F6', + stroke: '#CECECE', + cursor: 'pointer', + x: x - width / 2 + 5, + y: y - height / 2 + 5, + onClick: (e: any) => { + e.stopPropagation(); + model.foldGroup(!properties.isFolded); + }, + }), + iconIcon, + ]); + } + getResizeShape() { + const { model } = this.props; + const { x, y, width, height } = model; + const style = model.getNodeStyle(); + const foldRectAttrs = { + ...style, + x: x - width / 2, + y: y - height / 2, + width, + height, + stroke: 'black', + strokeWidth: 2, + strokeDasharray: '0 0', + }; + return h('g', {}, [ + // this.getAddAbleShape(), + h('rect', { ...foldRectAttrs }), + this.getFoldIcon(), + ]); + } + } + + class model extends GroupNode.model { + initNodeData(data: { + width: number; + height: number; + properties: Record; + }) { + super.initNodeData(data); + this.foldable = true; + // this.isFolded = true; + this.resizable = true; + this.width = 400; + this.height = 200; + // 根据 properties中的配置重设 宽高 + this.resetWidthHeight(); + this.isTaskNode = true; // 标识此节点是任务节点,可以被附件边界事件 + this.boundaryEvents = []; // 记录自己附加的边界事件 + } + // 自定义根据properties.iniProp + resetWidthHeight() { + const width = this.properties.iniProp?.width; + const height = this.properties.iniProp?.height; + width && (this.width = width); + height && (this.height = height); + } + getNodeStyle() { + const style = super.getNodeStyle(); + style.stroke = '#989891'; + style.strokeWidth = 1; + style.strokeDasharray = '3 3'; + if (this.isSelected) { + style.stroke = 'rgb(124, 15, 255)'; + } + // isCloseToBoundary属性用于标识拖动边界节点是否靠近此节点 + // 如果靠近,则高亮提示 + const { isCloseToBoundary } = this.properties; + // style.fill = 'rgb(255, 230, 204)'; + if (isCloseToBoundary) { + style.stroke = '#00acff'; + style.strokeWidth = 2; + } + + return style; + } + addChild(id: string) { + const model = this.graphModel.getElement(id); + model.setProperties({ + parent: this.id, + }); + super.addChild(id); + } + // 隐藏锚点而不是设置锚点数为0 + // 因为分组内部节点与外部节点相连时, + // 如果折叠分组,需要分组代替内部节点与外部节点相连。 + getAnchorStyle() { + const style = super.getAnchorStyle({}); + style.stroke = '#000'; + style.fill = '#fff'; + style.hover.stroke = 'transparent'; + return style; + } + getOutlineStyle() { + const style = super.getOutlineStyle(); + style.stroke = 'transparent'; + !style.hover && (style.hover = {}); + style.hover.stroke = 'transparent'; + return style; + } + /** + * 提供方法给插件在判断此节点被拖动边界事件节点靠近时调用,从而触发高亮 + */ + setIsCloseToBoundary(flag: boolean) { + this.setProperty('isCloseToBoundary', flag); + } + /** + * 附加后记录被附加的边界事件节点Id + */ + addBoundaryEvent(nodeId: string) { + if (this.boundaryEvents.find((item: string) => item === nodeId)) { + return false; + } + const boundaryEvent = this.graphModel.getNodeModelById(nodeId); + boundaryEvent?.setProperties({ + attachedToRef: this.id, + }); + this.boundaryEvents.push(nodeId); + return true; + } + /** + * 被附加的边界事件节点被删除时,移除记录 + */ + deleteBoundaryEvent(nodeId: string) { + this.boundaryEvents = this.boundaryEvents.filter( + (item: string) => item !== nodeId, + ); + } + } + + return { + type: 'bpmn:subProcess', + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/Task/task.ts b/packages/extension/src/bpmn-elements/presets/Task/task.ts new file mode 100644 index 000000000..39350c6a2 --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/Task/task.ts @@ -0,0 +1,187 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + BaseEdge, + BaseEdgeModel, + BaseNode, + BaseNodeModel, + GraphModel, + NodeConfig, + RectNode, + RectNodeModel, + h, +} from '@logicflow/core'; +import { parallelMarker, sequentialMarker, loopMarker } from '../icons'; +import { genBpmnId, groupRule } from '../../utils'; + +export const multiInstanceIcon: any = { + parallel: parallelMarker, + sequential: sequentialMarker, + loop: loopMarker, +}; + +type TaskType = { + multiInstanceType: string; + [key: string]: any; +}; + +/** + * @param type 任务节点的type, 对应其XML定义中的节点名,如 其type为bpmn:userTask + * @param icon 任务节点左上角的图标,可以是svg path,也可以是h函数生成的svg + * @param props (可选) 任务节点的属性 + * @returns { type: string, model: any, view: any } + */ +export function TaskNodeFactory(type: string, icon: string | any[], props?: any): { + type: string, + model: any, + view: any, +} { + class view extends RectNode { + getLabelShape() { + // @ts-ignore + const { model } = this.props; + const { x, y, width, height } = model; + const style = model.getNodeStyle(); + const i = Array.isArray(icon) + ? h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + ...icon, + ) + : h('path', { + fill: style.stroke, + d: icon, + }); + return h( + 'svg', + { + x: x - width / 2 + 5, + y: y - height / 2 + 5, + width: 25, + height: 25, + viewBox: '0 0 1274 1024', + }, + i, + ); + } + getShape() { + // @ts-ignore + const { model } = this.props; + const { x, y, width, height, radius, properties } = model; + const style = model.getNodeStyle(); + return h('g', {}, [ + h('rect', { + ...style, + x: x - width / 2, + y: y - height / 2, + rx: radius, + ry: radius, + width, + height, + opacity: 0.95, + }), + this.getLabelShape(), + h( + 'g', + { + transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`, + }, + h('path', { + fill: 'white', + strokeLinecap: 'round', + strokeLinejoin: 'round', + stroke: 'rgb(34, 36, 42)', + strokeWidth: '2', + d: multiInstanceIcon[properties.multiInstanceType] || '', + }), + ), + ]); + } + } + + class model extends RectNodeModel { + constructor(data: NodeConfig, graphModel: GraphModel) { + if (!data.id) { + data.id = `Activity_${genBpmnId()}`; + } + const properties: TaskType = { + ...props, + ...data.properties, + // multiInstanceType: '', + // panels: ['multiInstance'], + }; + data.properties = properties; + super(data, graphModel); + properties?.boundaryEvents?.forEach((id: string) => { + this.addBoundaryEvent(id); + }); + this.deleteProperty('boundaryEvents'); + groupRule.call(this); + } + initNodeData(data: any) { + super.initNodeData(data); + // this.width = 100; + // this.height = 60; + this.isTaskNode = true; // 标识此节点是任务节点,可以被附件边界事件 + this.boundaryEvents = []; // 记录自己附加的边界事件 + } + getNodeStyle() { + const style = super.getNodeStyle(); + // isCloseToBoundary属性用于标识拖动边界节点是否靠近此节点 + // 如果靠近,则高亮提示 + const { isCloseToBoundary } = this.properties; + // style.fill = 'rgb(255, 230, 204)'; + if (isCloseToBoundary) { + style.stroke = '#00acff'; + style.strokeWidth = 2; + } + if (this.isSelected) { + style.strokeWidth = 2; + } + return style; + } + getOutlineStyle() { + const style = super.getOutlineStyle(); + style.stroke = 'transparent'; + !style.hover && (style.hover = {}); + style.hover.stroke = 'transparent'; + return style; + } + /** + * 提供方法给插件在判断此节点被拖动边界事件节点靠近时调用,从而触发高亮 + */ + setIsCloseToBoundary(flag: boolean) { + this.setProperty('isCloseToBoundary', flag); + } + /** + * 附加后记录被附加的边界事件节点Id + */ + addBoundaryEvent(nodeId: string) { + if (this.boundaryEvents.find((item: string) => item === nodeId)) { + return false; + } + const boundaryEvent = this.graphModel.getNodeModelById(nodeId); + boundaryEvent?.setProperties({ + attachedToRef: this.id, + }); + this.boundaryEvents.push(nodeId); + return true; + } + /** + * 被附加的边界事件节点被删除时,移除记录 + */ + deleteBoundaryEvent(nodeId: string) { + this.boundaryEvents = this.boundaryEvents.filter( + (item: string) => item !== nodeId, + ); + } + } + + // @ts-ignore + return { + type, + view, + model, + }; +} diff --git a/packages/extension/src/bpmn-elements/presets/icons.ts b/packages/extension/src/bpmn-elements/presets/icons.ts new file mode 100644 index 000000000..d9813565a --- /dev/null +++ b/packages/extension/src/bpmn-elements/presets/icons.ts @@ -0,0 +1,141 @@ +import { h } from '@logicflow/core'; + +// event +export const messageIcon = 'm 8.459999999999999,11.34 l 0,12.6 l 18.900000000000002,0 l 0,-12.6 z l 9.450000000000001,5.4 l 9.450000000000001,-5.4'; +export const timerIcon = [ + h('circle', { + cx: 18, + cy: 18, + r: 11, + style: + 'stroke-linecap: round;stroke-linejoin: round;stroke: rgb(34, 36, 42);stroke-width: 2px;fill: white', + }), + h('path', { + d: 'M 18,18 l 2.25,-7.5 m -2.25,7.5 l 5.25,1.5', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 2px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(0,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(30,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(60,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(90,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(120,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(150,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(180,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(210,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(240,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(270,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(300,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), + h('path', { + d: 'M 18,18 m 0,7.5 l -0,2.25', + transform: 'rotate(330,18,18)', + style: + 'fill: none; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + }), +]; +export const errorIcon = 'm 7.2,25.991999999999997 0.09350000000000001,-0.025300000000000003 7.3392,-9.610700000000001 7.667000000000001,8.9661 4.7003,-18.2204 -5.8707,11.6501 -7.299600000000001,-9.585400000000002 z'; +export const escalationIcon = 'M 18,7.2 l 8,20 l -8,-7 l -8,7 Z'; +export const compensationIcon = 'm 7.92,18 9,-6.5 0,13 z m 9.3,-0.4 8.7,-6.1 0,13 -8.7,-6.1 z'; +export const conditionalIcon = 'M 10.5,8.5 l 14.5,0 l 0,18 l -14.5,0 Z M 12.5,11.5 l 10.5,0 M 12.5,14.5 l 10.5,0 M 12.5,17.5 l 10.5,0 M 12.5,20.5 l 10.5,0 M 12.5,23.5 l 10.5,0 M 12.5,26.5 l 10.5,0 '; +export const linkIcon = 'm 20.52,9.468 0,4.4375 -13.5,0 0,6.75 13.5,0 0,4.4375 9.84375,-7.8125 -9.84375,-7.8125 z'; +export const signalIcon = 'M 18,7.2 l 9,16.2 l -18,0 Z'; +export const terminateIcon = [ + h('circle', { + cx: 18, + cy: 18, + r: 10, + style: + 'stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 4px; fill: rgb(34, 36, 42);', + }), +]; + +// gateway +export const exclusiveIcon = 'm 16,15 7.42857142857143,9.714285714285715 -7.42857142857143,9.714285714285715 3.428571428571429,0 5.714285714285715,-7.464228571428572 5.714285714285715,7.464228571428572 3.428571428571429,0 -7.42857142857143,-9.714285714285715 7.42857142857143,-9.714285714285715 -3.428571428571429,0 -5.714285714285715,7.464228571428572 -5.714285714285715,-7.464228571428572 -3.428571428571429,0 z'; +export const parallelIcon = 'm 23,10 0,12.5 -12.5,0 0,5 12.5,0 0,12.5 5,0 0,-12.5 12.5,0 0,-5 -12.5,0 0,-12.5 -5,0 z'; +export const inclusiveIcon = h('circle', { + cx: 25, + cy: 25, + r: 13, + style: + 'stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 2.5px; fill: white;', +}); + +// task +export const serviceTaskIcon = 'M882.527918 434.149934c-2.234901-5.303796-7.311523-8.853645-13.059434-9.138124l-61.390185-3.009544c-6.635117-20.973684-15.521508-41.175795-26.513864-60.282968l42.051745-47.743374c4.308119-4.889357 4.955872-12.004405 1.602498-17.59268-46.384423-77.30362-103.969956-101.422947-106.400309-102.410438-5.332449-2.170432-11.432377-1.090844-15.693424 2.77009L654.674467 240.664222c-17.004279-8.654101-35.092239-15.756869-53.995775-21.210068l-3.26537-66.490344c-0.280386-5.747911-3.833305-10.824533-9.134031-13.059434-1.683339-0.709151-30.193673-12.391215-76.866668-12.051477-46.672996-0.339738-75.18333 11.342326-76.866668 12.051477-5.300726 2.234901-8.853645 7.311523-9.134031 13.059434l-3.26537 66.490344c-18.903535 5.453199-36.991496 12.555967-53.995775 21.210068l-48.450479-43.922349c-4.261047-3.860934-10.360975-4.940522-15.693424-2.77009-2.430352 0.98749-60.015885 25.106818-106.400309 102.410438-3.353374 5.588275-2.705622 12.703323 1.602498 17.59268l42.051745 47.743374c-10.992355 19.107173-19.878746 39.309284-26.513864 60.282968l-61.390185 3.009544c-5.747911 0.284479-10.824533 3.834328-13.059434 9.138124-1.01512 2.415003-24.687262 60.190871-2.822278 147.651828 1.583055 6.324032 7.072069 10.893094 13.57518 11.308557 5.892197 0.37146 11.751648 0.523933 17.419741 0.667196 14.498202 0.372483 28.193109 0.723477 40.908712 4.63353 4.212952 1.294482 6.435573 8.270361 9.349949 18.763342 1.287319 4.640694 2.617617 9.43693 4.484128 14.010085 1.794879 4.393054 3.75758 8.570189 5.66093 12.607132 1.302669 2.765997 2.529613 5.380544 3.689019 8.018627 2.986007 6.803963 2.682086 9.773598 2.578732 10.349719-3.061732 3.672646-6.391571 7.238868-9.91379 11.015891-1.810229 1.943258-3.680832 3.949962-5.523807 5.980201l-22.560832 24.8909c-3.865028 4.261047-4.940522 10.365068-2.774183 15.693424 0.991584 2.426259 25.102724 60.011792 102.414531 106.400309 5.588275 3.353374 12.703323 2.701528 17.591657-1.603521l23.476691-20.682042c2.346441-2.061962 4.64888-4.336772 6.875594-6.534833 9.05319-8.93858 14.018272-12.95608 17.73185-11.576663 3.305279 1.222851 6.907317 3.166109 10.720156 5.228071 3.325745 1.794879 6.764054 3.650133 10.465352 5.288446 6.016017 2.662643 12.120039 4.688789 18.019399 6.65149 6.827499 2.266623 13.279445 4.409426 18.819624 7.275707 1.518586 0.782829 1.926886 0.994654 2.358721 7.830339 0.726547 11.496845 1.25048 23.276123 1.753947 34.672684 0.264013 5.900384 0.528026 11.803837 0.815575 17.700127 0.284479 5.743818 3.833305 10.82044 9.138124 13.05534 1.654686 0.698918 29.371958 12.063757 74.869175 12.063757 0.328481 0 3.65832 0 3.986801 0 45.497217 0 73.214489-11.364839 74.869175-12.063757 5.304819-2.234901 8.853645-7.311523 9.138124-13.05534 0.287549-5.89629 0.551562-11.799744 0.815575-17.700127 0.503467-11.396561 1.027399-23.175839 1.753947-34.672684 0.431835-6.835685 0.840134-7.04751 2.358721-7.830339 5.54018-2.866281 11.992125-5.009084 18.819624-7.275707 5.89936-1.962701 12.003382-3.988848 18.019399-6.65149 3.701299-1.638313 7.139607-3.493567 10.465352-5.288446 3.812839-2.061962 7.414877-4.00522 10.720156-5.228071 3.713578-1.379417 8.67866 2.638083 17.73185 11.576663 2.226714 2.198062 4.529153 4.472871 6.875594 6.534833l23.476691 20.682042c4.888334 4.305049 12.003382 4.956895 17.591657 1.603521 77.311807-46.388517 101.422947-103.97405 102.414531-106.400309 2.166339-5.328355 1.090844-11.432377-2.774183-15.693424l-22.560832-24.8909c-1.842974-2.030239-3.713578-4.036943-5.523807-5.980201-3.52222-3.777023-6.852058-7.343245-9.91379-11.015891-0.103354-0.576121-0.407276-3.545756 2.578732-10.349719 1.159406-2.638083 2.38635-5.252631 3.689019-8.018627 1.90335-4.036943 3.866051-8.214079 5.66093-12.607132 1.866511-4.573155 3.196809-9.369392 4.484128-14.010085 2.914376-10.492982 5.136997-17.46886 9.349949-18.763342 12.715603-3.910053 26.41051-4.261047 40.908712-4.63353 5.668093-0.143263 11.527544-0.295735 17.419741-0.667196 6.503111-0.415462 11.992125-4.984524 13.57518-11.308557C907.21518 494.340805 883.543038 436.564937 882.527918 434.149934zM643.49894 643.761929c-35.280528 35.280528-82.191954 54.711066-132.086317 54.711066s-96.806813-19.430538-132.086317-54.711066c-35.280528-35.279504-54.711066-82.191954-54.711066-132.086317 0-49.894364 19.430538-96.80272 54.711066-132.082224 35.283598-35.284621 82.191954-54.711066 132.086317-54.711066s96.80579 19.426445 132.086317 54.711066c35.279504 35.279504 54.711066 82.187861 54.711066 132.082224C698.210006 561.569976 678.782537 608.482425 643.49894 643.761929z'; + +export const userTaskIcon = 'M655.807326 287.35973m-223.989415 0a218.879 218.879 0 1 0 447.978829 0 218.879 218.879 0 1 0-447.978829 0ZM1039.955839 895.482975c-0.490184-212.177424-172.287821-384.030443-384.148513-384.030443-211.862739 0-383.660376 171.85302-384.15056 384.030443L1039.955839 895.482975z'; + +export const scriptTaskIcon = 'M6.402,0.5H20.902C20.902,0.5,15.069,3.333,15.069,6.083S19.486,12.083,19.486,15.25S15.319,20.333,15.319,20.333H0.235C0.235,20.333,5.235,17.665999999999997,5.235,15.332999999999998S0.6520000000000001,8.582999999999998,0.6520000000000001,6.082999999999998S6.402,0.5,6.402,0.5ZM3.5,4.5L13.5,4.5M3.8,8.5L13.8,8.5M6.3,12.5L16.3,12.5M6.5,16.5L16.5,16.5'; + +export const manualTaskIcon = 'M0.5,3.751L4.583,0.5009999999999999C4.583,0.5009999999999999,15.749,0.5839999999999999,16.666,0.5839999999999999S14.249,3.5009999999999994,15.166,3.5009999999999994S26.833,3.5009999999999994,27.75,3.5009999999999994C28.916,5.209,27.582,6.667999999999999,26.916,7.167999999999999S27.791,9.084999999999999,25.916,11.584999999999999C25.166,11.834999999999999,26.666,13.459999999999999,24.583000000000002,14.918C23.416,15.501,25.166,16.46,23.333000000000002,17.750999999999998C22.166,17.750999999999998,2.5000000000000036,17.833999999999996,2.5000000000000036,17.833999999999996L0.5000000000000036,16.500999999999998V3.751ZM13.5,7L27,7M13.5,11L26,11M14,14.5L25,14.5M8.2,3.1L15,3.1'; + +export const style = { + throw: + 'fill: rgb(34, 36, 42); stroke-linecap: round; stroke-linejoin: round; stroke: white; stroke-width: 1px;', + catch: + 'fill: white; stroke-linecap: round; stroke-linejoin: round; stroke: rgb(34, 36, 42); stroke-width: 1px;', + nonIntermediate: 'stroke-width: 1.5; stroke-dash-array: 6', + intermediate: 'stroke-width: 1.5', +}; + +// marker +export const parallelMarker = 'm44,60 m 3,2 l 0,10 m 3,-10 l 0,10 m 3,-10 l 0,10'; + +export const sequentialMarker = 'm47,61 m 0,3 l 10,0 m -10,3 l 10,0 m -10,3 l 10,0'; + +export const loopMarker = 'm 50,73 c 3.526979,0 6.386161,-2.829858 6.386161,-6.320661 0,-3.490806 -2.859182,-6.320661 -6.386161,-6.320661 -3.526978,0 -6.38616,2.829855 -6.38616,6.320661 0,1.745402 0.714797,3.325567 1.870463,4.469381 0.577834,0.571908 1.265885,1.034728 2.029916,1.35457 l -0.718163,-3.909793 m 0.718163,3.909793 -3.885211,0.802902'; diff --git a/packages/extension/src/bpmn-elements/utils.ts b/packages/extension/src/bpmn-elements/utils.ts new file mode 100644 index 000000000..3e994ebde --- /dev/null +++ b/packages/extension/src/bpmn-elements/utils.ts @@ -0,0 +1,26 @@ +import Ids from 'ids'; + +export function groupRule() { + const rule = { + message: '分组外的节点不允许连接分组内的', + validate: ( + _sourceNode: any, + _targetNode: any, + _sourceAnchor: any, + _targetAnchor: any, + ) => { + const isSourceNodeInsideTheGroup = !!_sourceNode.properties.parent; + const isTargetNodeInsideTheGroup = !!_targetNode.properties.parent; + + return !(!isSourceNodeInsideTheGroup && isTargetNodeInsideTheGroup); + }, + }; + this.targetRules.push(rule); +} + +// @ts-ignore +const ids = new Ids([32, 32, 1]); + +export function genBpmnId(): string { + return ids.next(); +} diff --git a/packages/extension/src/index.ts b/packages/extension/src/index.ts index d9b85fe43..480468319 100644 --- a/packages/extension/src/index.ts +++ b/packages/extension/src/index.ts @@ -1,7 +1,9 @@ export * from './bpmn'; +export * from './bpmn-adapter'; +export * from './bpmn-elements'; +export * from './bpmn-elements-adapter'; // export * from './~mindmap'; export * from './tools/snapshot'; -export * from './bpmn-adapter'; export * from './turbo-adapter'; export * from './insert-node-in-polyline'; export * from './components/control'; From f3aaf9169df7039527f28cfe2c9be5eb575e8933 Mon Sep 17 00:00:00 2001 From: wumail <1059037014@qq.com> Date: Thu, 3 Aug 2023 19:10:43 +0800 Subject: [PATCH 2/2] fix(extension): fix types error when run build:types --- packages/extension/src/bpmn-elements-adapter/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/src/bpmn-elements-adapter/index.ts b/packages/extension/src/bpmn-elements-adapter/index.ts index 855ff8565..29c4a9ea8 100644 --- a/packages/extension/src/bpmn-elements-adapter/index.ts +++ b/packages/extension/src/bpmn-elements-adapter/index.ts @@ -690,7 +690,7 @@ function convertBpmn2LfData(bpmnData: any, other?: ExtraPropsType) { } else { func(obj[key]); } - let keys: (string | symbol)[]; + let keys: any[]; if ((keys = Reflect.ownKeys(props)).length > 0) { keys.forEach((key) => { Reflect.set(obj, key, props[key]);