This is an automated email from the ASF dual-hosted git repository.
yixia pushed a commit to branch refactor_designer
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/refactor_designer by this push:
new bfad822cda feature: full support for states in the refactored state
machine designer (#6169)
bfad822cda is described below
commit bfad822cda8636b4d48a962504e197cefea4e391
Author: Xiangkun Yin <[email protected]>
AuthorDate: Fri Jan 5 21:46:56 2024 +0800
feature: full support for states in the refactored state machine designer
(#6169)
---
.../seata-saga-statemachine-designer/src/Editor.js | 2 +
.../src/layout/behavior/AttachCatchBehavior.js | 42 ++++
.../src/layout/behavior/index.js | 3 +
.../src/modeling/SagaExporter.js | 34 ++-
.../src/modeling/SagaFactory.js | 20 ++
.../src/modeling/SagaImporter.js | 35 ++-
.../src/modeling/SagaRules.js | 78 +++++-
.../provider/PropertiesProvider.js | 4 +-
.../provider/properties/StateProps.js | 2 +
.../src/providers/PaletteProvider.js | 12 +-
.../src/render/PathMap.js | 42 ++++
.../src/render/Renderer.js | 276 +++++++++++++++++++--
.../src/spec/{TaskState.js => Catch.js} | 21 +-
.../src/spec/{TaskState.js => Choice.js} | 31 ++-
.../src/spec/{ServiceTask.js => ChoiceEntry.js} | 23 +-
.../src/spec/{TaskState.js => Compensation.js} | 16 +-
.../spec/{TaskState.js => CompensationTrigger.js} | 19 +-
.../src/spec/{ServiceTask.js => ExceptionMatch.js} | 27 +-
.../src/spec/{TaskState.js => Fail.js} | 17 +-
.../src/spec/{TaskState.js => ScriptTask.js} | 14 +-
.../src/spec/ServiceTask.js | 5 -
.../src/spec/{TaskState.js => SubStateMachine.js} | 13 +-
.../src/spec/{TaskState.js => Succeed.js} | 18 +-
.../src/spec/TaskState.js | 25 ++
.../src/utils/index.js | 6 +-
25 files changed, 664 insertions(+), 121 deletions(-)
diff --git a/saga/seata-saga-statemachine-designer/src/Editor.js
b/saga/seata-saga-statemachine-designer/src/Editor.js
index ab282ee6e9..fc03ffc486 100644
--- a/saga/seata-saga-statemachine-designer/src/Editor.js
+++ b/saga/seata-saga-statemachine-designer/src/Editor.js
@@ -20,6 +20,7 @@ import { innerSVG } from 'tiny-svg';
import Diagram from 'diagram-js';
import AlignElementsModule from 'diagram-js/lib/features/align-elements';
+import AttachSupport from 'diagram-js/lib/features/attach-support';
import AutoScrollModule from 'diagram-js/lib/features/auto-scroll';
import BendpointsModule from 'diagram-js/lib/features/bendpoints';
import ConnectModule from 'diagram-js/lib/features/connect';
@@ -79,6 +80,7 @@ Editor.prototype.modules = [
// Built-in modules
AlignElementsModule,
+ AttachSupport,
AutoScrollModule,
BendpointsModule,
ConnectModule,
diff --git
a/saga/seata-saga-statemachine-designer/src/layout/behavior/AttachCatchBehavior.js
b/saga/seata-saga-statemachine-designer/src/layout/behavior/AttachCatchBehavior.js
new file mode 100644
index 0000000000..d9b9b1d8a7
--- /dev/null
+++
b/saga/seata-saga-statemachine-designer/src/layout/behavior/AttachCatchBehavior.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright 1999-2019 Seata.io Group.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import inherits from 'inherits-browser';
+
+import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
+import { is } from '../../utils';
+
+const LOW_PRIORITY = 500;
+
+function shouldUpdate(shape, host) {
+ return is(shape, 'Catch') && host;
+}
+
+export default function AttachEventBehavior(injector) {
+ injector.invoke(CommandInterceptor, this);
+ this.postExecuted('element.updateAttachment', LOW_PRIORITY, ({ context }) =>
{
+ const { shape, oldHost, newHost } = context;
+
+ if (shouldUpdate(shape, newHost)) {
+ delete oldHost?.businessObject.Catch;
+ newHost.businessObject.Catch = shape.businessObject;
+ }
+ });
+}
+
+inherits(AttachEventBehavior, CommandInterceptor);
+
+AttachEventBehavior.$inject = ['injector'];
diff --git a/saga/seata-saga-statemachine-designer/src/layout/behavior/index.js
b/saga/seata-saga-statemachine-designer/src/layout/behavior/index.js
index fad1764dab..e0f716184f 100644
--- a/saga/seata-saga-statemachine-designer/src/layout/behavior/index.js
+++ b/saga/seata-saga-statemachine-designer/src/layout/behavior/index.js
@@ -14,16 +14,19 @@
* limitations under the License.
*/
+import AttachCatchBehavior from './AttachCatchBehavior';
import LayoutConnectionBehavior from './LayoutConnectionBehavior';
import ReplaceConnectionBehavior from './ReplaceConnectionBehavior';
import LayoutUpdateBehavior from './LayoutUpdateBehavior';
export default {
__init__: [
+ 'attachCatchBehavior',
'layoutConnectionBehavior',
'replaceConnectionBehavior',
'layoutUpdateBehavior',
],
+ attachCatchBehavior: ['type', AttachCatchBehavior],
layoutConnectionBehavior: ['type', LayoutConnectionBehavior],
replaceConnectionBehavior: ['type', ReplaceConnectionBehavior],
layoutUpdateBehavior: ['type', LayoutUpdateBehavior],
diff --git a/saga/seata-saga-statemachine-designer/src/modeling/SagaExporter.js
b/saga/seata-saga-statemachine-designer/src/modeling/SagaExporter.js
index 67fc1b6a6a..92112c6dd2 100644
--- a/saga/seata-saga-statemachine-designer/src/modeling/SagaExporter.js
+++ b/saga/seata-saga-statemachine-designer/src/modeling/SagaExporter.js
@@ -54,12 +54,36 @@ SagaExporter.prototype.parseEdge = function (definitions,
edge) {
}
} else {
const stateRef = definitions.States[source];
- stateRef.Next = target;
-
- if (stateRef.edge === undefined) {
- stateRef.edge = {};
+ switch (businessObject.Type) {
+ case 'ChoiceEntry':
+ if (!stateRef.Choices) {
+ stateRef.Choices = [];
+ }
+ stateRef.Choices.push({
+ Expression: businessObject.Expression,
+ Next: target,
+ });
+ if (businessObject.Default) {
+ stateRef.Default = target;
+ }
+ stateRef.edge = assign(stateRef.edge || {}, { [target]: elementJson });
+ break;
+ case 'ExceptionMatch':
+ stateRef.Catch.push({
+ Exceptions: businessObject.Exceptions,
+ Next: target,
+ });
+ stateRef.catch = assign(stateRef.catch || {}, { edge: { [target]:
elementJson } });
+ break;
+ case 'Compensation':
+ stateRef.CompensateState = target;
+ stateRef.edge = assign(stateRef.edge || {}, { [target]: elementJson });
+ break;
+ case 'Transition':
+ default:
+ stateRef.Next = target;
+ stateRef.edge = assign(stateRef.edge || {}, { [target]: elementJson });
}
- assign(stateRef.edge, { [target]: elementJson });
}
};
diff --git a/saga/seata-saga-statemachine-designer/src/modeling/SagaFactory.js
b/saga/seata-saga-statemachine-designer/src/modeling/SagaFactory.js
index 9e7adb73a9..7456bc54df 100644
--- a/saga/seata-saga-statemachine-designer/src/modeling/SagaFactory.js
+++ b/saga/seata-saga-statemachine-designer/src/modeling/SagaFactory.js
@@ -18,6 +18,16 @@ import Transition from '../spec/Transition';
import StateMachine from '../spec/StateMachine';
import ServiceTask from '../spec/ServiceTask';
import StartState from '../spec/StartState';
+import ScriptTask from '../spec/ScriptTask';
+import Choice from '../spec/Choice';
+import ChoiceEntry from '../spec/ChoiceEntry';
+import Succeed from '../spec/Succeed';
+import Fail from '../spec/Fail';
+import Catch from '../spec/Catch';
+import ExceptionMatch from '../spec/ExceptionMatch';
+import CompensationTrigger from '../spec/CompensationTrigger';
+import SubStateMachine from '../spec/SubStateMachine';
+import Compensation from '../spec/Compensation';
export default function SagaFactory() {
const typeToSpec = new Map();
@@ -25,6 +35,16 @@ export default function SagaFactory() {
typeToSpec.set('StartState', StartState);
typeToSpec.set('StateMachine', StateMachine);
typeToSpec.set('ServiceTask', ServiceTask);
+ typeToSpec.set('ScriptTask', ScriptTask);
+ typeToSpec.set('Choice', Choice);
+ typeToSpec.set('ChoiceEntry', ChoiceEntry);
+ typeToSpec.set('Succeed', Succeed);
+ typeToSpec.set('Fail', Fail);
+ typeToSpec.set('Catch', Catch);
+ typeToSpec.set('ExceptionMatch', ExceptionMatch);
+ typeToSpec.set('CompensationTrigger', CompensationTrigger);
+ typeToSpec.set('SubStateMachine', SubStateMachine);
+ typeToSpec.set('Compensation', Compensation);
this.typeToSpec = typeToSpec;
}
diff --git a/saga/seata-saga-statemachine-designer/src/modeling/SagaImporter.js
b/saga/seata-saga-statemachine-designer/src/modeling/SagaImporter.js
index e0fb570348..6cd3708adc 100644
--- a/saga/seata-saga-statemachine-designer/src/modeling/SagaImporter.js
+++ b/saga/seata-saga-statemachine-designer/src/modeling/SagaImporter.js
@@ -46,12 +46,14 @@ export default function SagaImporter(
canvas,
elementFactory,
elementRegistry,
+ modeling,
) {
this.sagaFactory = sagaFactory;
this.eventBus = eventBus;
this.canvas = canvas;
this.elementRegistry = elementRegistry;
this.elementFactory = elementFactory;
+ this.modeling = modeling;
}
SagaImporter.$inject = [
@@ -60,6 +62,7 @@ SagaImporter.$inject = [
'canvas',
'elementFactory',
'elementRegistry',
+ 'modeling',
];
SagaImporter.prototype.import = function (definitions) {
@@ -74,18 +77,33 @@ SagaImporter.prototype.import = function (definitions) {
this.root(root);
// Add start state
- const start = this.sagaFactory.create('StartState');
+ let start = this.sagaFactory.create('StartState');
start.importJson(definitions);
- this.add(start);
+ start = this.add(start);
const edges = [];
+ const catches = [];
forEach(definitions.States, (semantic) => {
const state = this.sagaFactory.create(semantic.Type);
state.importJson(semantic);
- this.add(state);
+ const host = this.add(state);
if (semantic.edge) {
edges.push(...Object.values(semantic.edge));
}
+ if (semantic.catch) {
+ const node = this.sagaFactory.create('Catch');
+ node.importJson(semantic.catch);
+ const source = this.add(node);
+ if (semantic.catch.edge) {
+ semantic.Catch.forEach((exceptionMatch) => {
+ if (semantic.catch.edge[exceptionMatch.Next]) {
+ semantic.catch.edge[exceptionMatch.Next].Exceptions =
exceptionMatch.Exceptions;
+ }
+ });
+ }
+ this.modeling.updateAttachment(source, host);
+ catches.push({ source, edges: Object.values(semantic.catch.edge) });
+ }
});
// Add start edge
@@ -100,6 +118,15 @@ SagaImporter.prototype.import = function (definitions) {
transition.importJson(semantic);
this.add(transition);
});
+
+ forEach(catches, (oneCatch) => {
+ const { source, edges: exceptionMatches } = oneCatch;
+ forEach(exceptionMatches, (semantic) => {
+ const exceptionMatch = this.sagaFactory.create(semantic.Type);
+ exceptionMatch.importJson(semantic);
+ this.add(exceptionMatch, { source });
+ });
+ });
} catch (e) {
error = e;
console.error(error);
@@ -142,7 +169,7 @@ SagaImporter.prototype.add = function (semantic, attrs =
{}) {
} else if (style.Type === 'Edge') {
waypoints = collectWaypoints(style);
- source = this.getSource(semantic) || attrs.source;
+ source = attrs.source || this.getSource(semantic);
target = this.getTarget(semantic);
semantic.style.source = source;
semantic.style.target = target;
diff --git a/saga/seata-saga-statemachine-designer/src/modeling/SagaRules.js
b/saga/seata-saga-statemachine-designer/src/modeling/SagaRules.js
index 8abfba154f..5a90516342 100644
--- a/saga/seata-saga-statemachine-designer/src/modeling/SagaRules.js
+++ b/saga/seata-saga-statemachine-designer/src/modeling/SagaRules.js
@@ -17,6 +17,8 @@
import inherits from 'inherits-browser';
import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider';
+import { getOrientation } from 'diagram-js/lib/layout/LayoutUtil';
+import { is } from '../utils';
export default function SagaRules(injector) {
injector.invoke(RuleProvider, this);
@@ -34,17 +36,89 @@ function canConnect(source, target) {
if (target.parent !== source.parent || source === target) {
return false;
}
+
+ if (is(source, 'Task') && is(target, 'Task') &&
target.businessObject.IsForCompensation) {
+ return { type: 'Compensation' };
+ }
+
+ if (is(source, 'Choice')) {
+ return { type: 'ChoiceEntry' };
+ }
+
+ if (is(source, 'Catch')) {
+ return { type: 'ExceptionMatch' };
+ }
+
return { type: 'Transition' };
}
-SagaRules.prototype.canConnect = canConnect;
+function canCreate(shapes, target) {
+ let shapeList = shapes;
+ if (!Array.isArray(shapes)) {
+ shapeList = [shapes];
+ }
+
+ const invalid = shapeList.map((shape) => {
+ if (is(shape, 'Catch')) {
+ return false;
+ }
+
+ if (!target) {
+ return true;
+ }
+
+ return target.parent === shape.target;
+ }).filter((valid) => !valid).length;
+
+ return !invalid;
+}
+
+function canAttach(shapes, target, position) {
+ if (Array.isArray(shapes)) {
+ if (shapes.length > 1) {
+ return false;
+ }
+ }
+ const shape = shapes[0] || shapes;
+
+ if (is(shape, 'Catch')) {
+ if (position && getOrientation(position, target, -15) === 'intersect') {
+ return false;
+ }
+
+ if (is(target, 'Task')) {
+ return 'attach';
+ }
+ }
+
+ return false;
+}
+
+function canMove(shapes, target, position) {
+ const shapeSet = new Set(shapes);
+ // Exclude all catches with parents included
+ const filtered = shapes.filter((shape) => !(is(shape, 'Catch') &&
shapeSet.has(shape.parent)));
+ return !target || canAttach(filtered, target, position) ||
canCreate(filtered, target);
+}
SagaRules.prototype.init = function () {
this.addRule('shape.create', (context) => {
const { target } = context;
const { shape } = context;
- return target.parent === shape.target;
+ return canCreate(shape, target);
+ });
+
+ this.addRule('shape.attach', (context) => {
+ const { shape, target, position } = context;
+
+ return canAttach(shape, target, position);
+ });
+
+ this.addRule('elements.move', (context) => {
+ const { shapes, target, position } = context;
+
+ return canMove(shapes, target, position);
});
this.addRule('connection.create', (context) => {
diff --git
a/saga/seata-saga-statemachine-designer/src/properties-panel/provider/PropertiesProvider.js
b/saga/seata-saga-statemachine-designer/src/properties-panel/provider/PropertiesProvider.js
index e40f634d8b..3aaf2a22c0 100644
---
a/saga/seata-saga-statemachine-designer/src/properties-panel/provider/PropertiesProvider.js
+++
b/saga/seata-saga-statemachine-designer/src/properties-panel/provider/PropertiesProvider.js
@@ -33,7 +33,7 @@ function GeneralGroup(element) {
entries.push(...VersionProps({ element }));
}
- if (is(element, 'Connection') || is(element, 'StartState')) {
+ if (is(element, 'Connection') || is(element, 'StartState') || is(element,
'Catch')) {
return null;
}
@@ -51,7 +51,7 @@ function JsonGroup(element) {
...StyleProps({ element }),
];
- if (is(element, 'Connection') || is(element, 'StartState')) {
+ if (is(element, 'Transition') || is(element, 'Compensation') || is(element,
'StartState') || is(element, 'Catch')) {
entries.splice(0, 1);
}
diff --git
a/saga/seata-saga-statemachine-designer/src/properties-panel/provider/properties/StateProps.js
b/saga/seata-saga-statemachine-designer/src/properties-panel/provider/properties/StateProps.js
index 44bdcd4c53..57c3e7f9eb 100644
---
a/saga/seata-saga-statemachine-designer/src/properties-panel/provider/properties/StateProps.js
+++
b/saga/seata-saga-statemachine-designer/src/properties-panel/provider/properties/StateProps.js
@@ -39,6 +39,8 @@ function State(props) {
const value = assign({}, e.businessObject);
// Exclude style
delete value.style;
+ // Exclude Catch for Task
+ delete value.Catch;
return JSON.stringify(value, null, 2);
},
validate: (value) => {
diff --git
a/saga/seata-saga-statemachine-designer/src/providers/PaletteProvider.js
b/saga/seata-saga-statemachine-designer/src/providers/PaletteProvider.js
index 071946746e..e673c6f312 100644
--- a/saga/seata-saga-statemachine-designer/src/providers/PaletteProvider.js
+++ b/saga/seata-saga-statemachine-designer/src/providers/PaletteProvider.js
@@ -17,6 +17,16 @@
import { assign } from 'min-dash';
import ServiceTask from '../spec/ServiceTask';
import StartState from '../spec/StartState';
+import ScriptTask from '../spec/ScriptTask';
+import Choice from '../spec/Choice';
+import Succeed from '../spec/Succeed';
+import Fail from '../spec/Fail';
+import Catch from '../spec/Catch';
+import CompensationTrigger from '../spec/CompensationTrigger';
+import SubStateMachine from '../spec/SubStateMachine';
+
+const SPEC_LIST = [StartState, ServiceTask, ScriptTask, SubStateMachine,
Choice, Succeed, Fail,
+ Catch, CompensationTrigger];
/**
* A palette provider.
@@ -75,7 +85,7 @@ PaletteProvider.prototype.getPaletteEntries = function () {
separator: true,
},
};
- [StartState, ServiceTask].forEach((Spec) => {
+ SPEC_LIST.forEach((Spec) => {
const type = Spec.prototype.Type;
entries[`create-${type}`] = createAction(type, 'state',
Spec.prototype.THUMBNAIL_CLASS, `Create ${type}`);
});
diff --git a/saga/seata-saga-statemachine-designer/src/render/PathMap.js
b/saga/seata-saga-statemachine-designer/src/render/PathMap.js
index 281de6c29d..a74013b95f 100644
--- a/saga/seata-saga-statemachine-designer/src/render/PathMap.js
+++ b/saga/seata-saga-statemachine-designer/src/render/PathMap.js
@@ -127,6 +127,48 @@ export default function PathMap() {
heightElements: [],
widthElements: [],
},
+ MARKER_SUB_PROCESS: {
+ d: 'm{mx},{my} m 7,2 l 0,10 m -5,-5 l 10,0',
+ height: 10,
+ width: 10,
+ heightElements: [],
+ widthElements: [],
+ },
+ TASK_TYPE_SCRIPT: {
+ d: 'm {mx},{my} c 9.966553,-6.27276 -8.000926,-7.91932 2.968968,-14.938
l -8.802728,0 '
+ + 'c -10.969894,7.01868 6.997585,8.66524 -2.968967,14.938 z '
+ + 'm -7,-12 l 5,0 '
+ + 'm -4.5,3 l 4.5,0 '
+ + 'm -3,3 l 5,0'
+ + 'm -4,3 l 5,0',
+ height: 15,
+ width: 12.6,
+ heightElements: [6, 14],
+ widthElements: [10.5, 21],
+ },
+ GATEWAY_EXCLUSIVE: {
+ d: 'm {mx},{my} {e.x0},{e.y0} {e.x1},{e.y0} {e.x2},0 {e.x4},{e.y2} '
+ + '{e.x4},{e.y1} {e.x2},0 {e.x1},{e.y3} {e.x0},{e.y3} '
+ + '{e.x3},0 {e.x5},{e.y1} {e.x5},{e.y2} {e.x3},0 z',
+ height: 17.5,
+ width: 17.5,
+ heightElements: [8.5, 6.5312, -6.5312, -8.5],
+ widthElements: [6.5, -6.5, 3, -3, 5, -5],
+ },
+ EVENT_ERROR: {
+ d: 'm {mx},{my} {e.x0},-{e.y0} {e.x1},-{e.y1} {e.x2},{e.y2}
{e.x3},-{e.y3} -{e.x4},{e.y4} -{e.x5},-{e.y5} z',
+ height: 36,
+ width: 36,
+ heightElements: [0.023, 8.737, 8.151, 16.564, 10.591, 8.714],
+ widthElements: [0.085, 6.672, 6.97, 4.273, 5.337, 6.636],
+ },
+ EVENT_COMPENSATION: {
+ d: 'm {mx},{my} {e.x0},-{e.y0} 0,{e.y1} z m {e.x1},-{e.y2}
{e.x2},-{e.y3} 0,{e.y1} -{e.x2},-{e.y3} z',
+ height: 36,
+ width: 36,
+ heightElements: [6.5, 13, 0.4, 6.1],
+ widthElements: [9, 9.3, 8.7],
+ },
};
this.getRawPath = function getRawPath(pathId) {
diff --git a/saga/seata-saga-statemachine-designer/src/render/Renderer.js
b/saga/seata-saga-statemachine-designer/src/render/Renderer.js
index 6fb53d7e15..c5d0d9bc21 100644
--- a/saga/seata-saga-statemachine-designer/src/render/Renderer.js
+++ b/saga/seata-saga-statemachine-designer/src/render/Renderer.js
@@ -25,6 +25,7 @@ import { append as svgAppend, attr as svgAttr, create as
svgCreate } from 'tiny-
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';
import { createLine } from 'diagram-js/lib/util/RenderUtil';
+import { translate } from 'diagram-js/lib/util/SvgTransformUtil';
const BLACK = 'hsl(225, 10%, 15%)';
const TASK_BORDER_RADIUS = 10;
@@ -64,6 +65,16 @@ export default function Renderer(config, eventBus, pathMap,
styles, textRenderer
const defaultStrokeColor = (config && config.defaultStrokeColor) || BLACK;
const defaultLabelColor = (config && config.defaultLabelColor);
+ function shapeStyle(attrs) {
+ return styles.computeStyle(attrs, {
+ strokeLinecap: 'round',
+ strokeLinejoin: 'round',
+ stroke: BLACK,
+ strokeWidth: 2,
+ fill: 'white',
+ });
+ }
+
function addMarker(id, options) {
const attrs = assign({
strokeWidth: 1,
@@ -116,15 +127,32 @@ export default function Renderer(config, eventBus,
pathMap, styles, textRenderer
const end = svgCreate('path');
svgAttr(end, { d: 'M 1 5 L 11 10 L 1 15 Z' });
- addMarker(id, {
- element: end,
- attrs: {
- fill: stroke,
- stroke: 'none',
- },
- ref: { x: 11, y: 10 },
- scale: 1,
- });
+ if (type === 'connection-end') {
+ addMarker(id, {
+ element: end,
+ attrs: {
+ fill: stroke,
+ stroke: 'none',
+ },
+ ref: { x: 11, y: 10 },
+ scale: 1,
+ });
+ }
+
+ if (type === 'default-choice-marker') {
+ const defaultChoiceMarker = svgCreate('path', {
+ d: 'M 6 4 L 10 16',
+ ...shapeStyle({
+ stroke,
+ }),
+ });
+
+ addMarker(id, {
+ element: defaultChoiceMarker,
+ ref: { x: 0, y: 10 },
+ scale: 1,
+ });
+ }
}
function marker(type, fill, stroke) {
@@ -146,13 +174,7 @@ export default function Renderer(config, eventBus,
pathMap, styles, textRenderer
offset = offset || 0;
- attrs = computeStyle(attrs, {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- stroke: BLACK,
- strokeWidth: 2,
- fill: 'white',
- });
+ attrs = shapeStyle(attrs);
if (attrs.fill === 'none') {
delete attrs.fillOpacity;
@@ -243,6 +265,33 @@ export default function Renderer(config, eventBus,
pathMap, styles, textRenderer
return path;
}
+ function drawDiamond(parentGfx, width, height, attrs) {
+ const x2 = width / 2;
+ const y2 = height / 2;
+
+ const points = [
+ { x: x2, y: 0 },
+ { x: width, y: y2 },
+ { x: x2, y: height },
+ { x: 0, y: y2 },
+ ];
+
+ const pointsString = points.map((point) => {
+ return `${point.x},${point.y}`;
+ }).join(' ');
+
+ attrs = shapeStyle(attrs);
+
+ const polygon = svgCreate('polygon', {
+ ...attrs,
+ points: pointsString,
+ });
+
+ svgAppend(parentGfx, polygon);
+
+ return polygon;
+ }
+
function drawLine(p, waypoints, attrs) {
attrs = computeStyle(attrs, ['no-fill'], {
stroke: BLACK,
@@ -270,7 +319,7 @@ export default function Renderer(config, eventBus, pathMap,
styles, textRenderer
function attachTaskMarkers(p, element, taskMarkers) {
const obj = getSemantic(element);
- const sub = taskMarkers && taskMarkers.indexOf('SubStateMachine') !== -1;
+ const sub = taskMarkers && taskMarkers.indexOf('SubStateMachineMarker')
!== -1;
let position;
if (sub) {
@@ -318,13 +367,49 @@ export default function Renderer(config, eventBus,
pathMap, styles, textRenderer
return drawLine(p, element.waypoints, attrs);
},
+ ChoiceEntry(p, element) {
+ const fill = getFillColor(element, defaultFillColor);
+ const stroke = getStrokeColor(element, defaultStrokeColor);
+ const attrs = {
+ stroke,
+ strokeWidth: 1,
+ strokeLinecap: 'round',
+ strokeLinejoin: 'round',
+ markerEnd: marker('connection-end', fill, stroke),
+ };
+
+ const path = drawLine(p, element.waypoints, attrs);
+
+ if (getSemantic(element).Default) {
+ svgAttr(path, {
+ markerStart: marker('default-choice-marker', fill, stroke),
+ });
+ }
+
+ return path;
+ },
+ ExceptionMatch(p, element) {
+ return renderer('Transition')(p, element);
+ },
+ Compensation(p, element) {
+ const stroke = getStrokeColor(element, defaultStrokeColor);
+ const attrs = {
+ stroke,
+ strokeWidth: 1,
+ strokeLinecap: 'round',
+ strokeLinejoin: 'round',
+ strokeDasharray: '10, 11',
+ };
+
+ return drawLine(p, element.waypoints, attrs);
+ },
StartState(parentGfx, element) {
return drawCircle(parentGfx, element.width, element.height, {
fill: getFillColor(element, defaultFillColor),
stroke: getStrokeColor(element, defaultStrokeColor),
});
},
- Task(parentGfx, element) {
+ Task(parentGfx, element, additionalMarkers) {
const attrs = {
fill: getFillColor(element, defaultFillColor),
stroke: getStrokeColor(element, defaultStrokeColor),
@@ -334,7 +419,7 @@ export default function Renderer(config, eventBus, pathMap,
styles, textRenderer
const rect = drawRect(parentGfx, element.width, element.height,
TASK_BORDER_RADIUS, attrs);
renderEmbeddedLabel(parentGfx, element, 'center-middle');
- attachTaskMarkers(parentGfx, element);
+ attachTaskMarkers(parentGfx, element, additionalMarkers);
return rect;
},
@@ -380,6 +465,50 @@ export default function Renderer(config, eventBus,
pathMap, styles, textRenderer
return task;
},
+ ScriptTask(parentGfx, element) {
+ const task = renderer('Task')(parentGfx, element);
+ const pathData = pathMap.getScaledPath('TASK_TYPE_SCRIPT', {
+ abspos: {
+ x: 15,
+ y: 20,
+ },
+ });
+
+ /* script path */ drawPath(parentGfx, pathData, {
+ strokeWidth: 1,
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ });
+
+ return task;
+ },
+ SubStateMachine(parentGfx, element) {
+ return renderer('Task')(parentGfx, element, ['SubStateMachineMarker']);
+ },
+ SubStateMachineMarker(parentGfx, element) {
+ const markerRect = drawRect(parentGfx, 14, 14, 0, {
+ strokeWidth: 1,
+ fill: getFillColor(element, defaultFillColor),
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ });
+
+ translate(markerRect, element.width / 2 - 7.5, element.height - 20);
+
+ const markerPath = pathMap.getScaledPath('MARKER_SUB_PROCESS', {
+ xScaleFactor: 1.5,
+ yScaleFactor: 1.5,
+ containerWidth: element.width,
+ containerHeight: element.height,
+ position: {
+ mx: (element.width / 2 - 7.5) / element.width,
+ my: (element.height - 20) / element.height,
+ },
+ });
+
+ drawMarker('sub-process', parentGfx, markerPath, {
+ fill: getFillColor(element, defaultFillColor),
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ });
+ },
LoopMarker(parentGfx, element, position) {
const markerPath = pathMap.getScaledPath('MARKER_LOOP', {
xScaleFactor: 1,
@@ -417,6 +546,115 @@ export default function Renderer(config, eventBus,
pathMap, styles, textRenderer
stroke: getStrokeColor(element, defaultStrokeColor),
});
},
+ Gateway(parentGfx, element) {
+ return drawDiamond(parentGfx, element.width, element.height, {
+ fill: getFillColor(element, defaultFillColor),
+ fillOpacity: DEFAULT_FILL_OPACITY,
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ });
+ },
+ Choice(parentGfx, element) {
+ const diamond = renderer('Gateway')(parentGfx, element);
+
+ const pathData = pathMap.getScaledPath('GATEWAY_EXCLUSIVE', {
+ xScaleFactor: 0.4,
+ yScaleFactor: 0.4,
+ containerWidth: element.width,
+ containerHeight: element.height,
+ position: {
+ mx: 0.32,
+ my: 0.3,
+ },
+ });
+
+ drawPath(parentGfx, pathData, {
+ strokeWidth: 1,
+ fill: getStrokeColor(element, defaultStrokeColor),
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ });
+
+ return diamond;
+ },
+ Succeed(parentGfx, element) {
+ return drawCircle(parentGfx, element.width, element.height, {
+ strokeWidth: 4,
+ fill: getFillColor(element, defaultFillColor),
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ });
+ },
+ Error(parentGfx, element, fill) {
+ const pathData = pathMap.getScaledPath('EVENT_ERROR', {
+ xScaleFactor: 1.1,
+ yScaleFactor: 1.1,
+ containerWidth: element.width,
+ containerHeight: element.height,
+ position: {
+ mx: 0.2,
+ my: 0.722,
+ },
+ });
+ return drawPath(parentGfx, pathData, {
+ strokeWidth: 1,
+ fill: fill ? getStrokeColor(element, defaultStrokeColor) : 'none',
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ });
+ },
+ Fail(parentGfx, element) {
+ const circle = handlers.Succeed(parentGfx, element);
+ renderer('Error')(parentGfx, element, true);
+ return circle;
+ },
+ Event(parentGfx, element) {
+ const attrs = {
+ strokeWidth: 1.5,
+ fill: getFillColor(element, defaultFillColor),
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ };
+
+ // apply fillOpacity
+ const outerAttrs = {
+ ...attrs,
+ fillOpacity: 1,
+ };
+
+ // apply no-fill
+ const innerAttrs = {
+ ...attrs,
+ fill: 'none',
+ };
+
+ const outer = drawCircle(parentGfx, element.width, element.height,
outerAttrs);
+ drawCircle(parentGfx, element.width, element.height, 3, innerAttrs);
+ return outer;
+ },
+ Catch(parentGfx, element) {
+ const outer = renderer('Event')(parentGfx, element);
+ renderer('Error')(parentGfx, element);
+
+ return outer;
+ },
+ CompensationTrigger(parentGfx, element) {
+ const outer = renderer('Event')(parentGfx, element);
+ const pathData = pathMap.getScaledPath('EVENT_COMPENSATION', {
+ xScaleFactor: 1,
+ yScaleFactor: 1,
+ containerWidth: element.width,
+ containerHeight: element.height,
+ position: {
+ mx: 0.22,
+ my: 0.5,
+ },
+ });
+
+ const fill = 'none';
+
+ drawPath(parentGfx, pathData, {
+ strokeWidth: 1,
+ fill,
+ stroke: getStrokeColor(element, defaultStrokeColor),
+ });
+ return outer;
+ },
};
function drawShape(parent, element) {
const h = handlers[element.type];
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/Catch.js
similarity index 71%
copy from saga/seata-saga-statemachine-designer/src/spec/TaskState.js
copy to saga/seata-saga-statemachine-designer/src/spec/Catch.js
index 47ab79b3c8..e32432b950 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/Catch.js
@@ -14,14 +14,17 @@
* limitations under the License.
*/
-import State from './State';
+import Node from './style/Node';
+
+export default class Catch extends Node {
-export default class TaskState extends State {
- constructor() {
- super();
- this.Input = [{}];
- this.Output = {};
- this.Status = {};
- this.Retry = [];
- }
}
+
+Catch.prototype.Type = 'Catch';
+
+Catch.prototype.THUMBNAIL_CLASS = 'bpmn-icon-intermediate-event-catch-error';
+
+Catch.prototype.DEFAULT_SIZE = {
+ width: 36,
+ height: 36,
+};
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/Choice.js
similarity index 52%
copy from saga/seata-saga-statemachine-designer/src/spec/TaskState.js
copy to saga/seata-saga-statemachine-designer/src/spec/Choice.js
index 47ab79b3c8..810c3b1d90 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/Choice.js
@@ -16,12 +16,29 @@
import State from './State';
-export default class TaskState extends State {
- constructor() {
- super();
- this.Input = [{}];
- this.Output = {};
- this.Status = {};
- this.Retry = [];
+export default class Choice extends State {
+ importJson(json) {
+ super.importJson(json);
+ if (json.edge) {
+ this.Choices.forEach((choice) => {
+ if (json.edge[choice.Next]) {
+ json.edge[choice.Next].Expression = choice.Expression;
+ }
+ });
+ if (json.edge[this.Default]) {
+ json.edge[this.Default].Default = true;
+ }
+ }
+ delete this.Choices;
+ delete this.Default;
}
}
+
+Choice.prototype.Type = 'Choice';
+
+Choice.prototype.THUMBNAIL_CLASS = 'bpmn-icon-gateway-xor';
+
+Choice.prototype.DEFAULT_SIZE = {
+ width: 50,
+ height: 50,
+};
diff --git a/saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
b/saga/seata-saga-statemachine-designer/src/spec/ChoiceEntry.js
similarity index 63%
copy from saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
copy to saga/seata-saga-statemachine-designer/src/spec/ChoiceEntry.js
index 9ac9a2d447..5440988ada 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/ChoiceEntry.js
@@ -14,21 +14,22 @@
* limitations under the License.
*/
-import TaskState from './TaskState';
+import Transition from './Transition';
-export default class ServiceTask extends TaskState {
+export default class ChoiceEntry extends Transition {
constructor() {
super();
- this.ServiceName = '';
- this.ServiceMethod = '';
+ this.Expression = '';
+ this.Default = false;
}
-}
-ServiceTask.prototype.Type = 'ServiceTask';
+ importJson(json) {
+ super.importJson(json);
+ this.Expression = json.Expression;
+ this.Default = json.Default;
+ }
+}
-ServiceTask.prototype.THUMBNAIL_CLASS = 'bpmn-icon-service-task';
+ChoiceEntry.prototype.Type = 'ChoiceEntry';
-ServiceTask.prototype.DEFAULT_SIZE = {
- width: 100,
- height: 80,
-};
+ChoiceEntry.prototype.THUMBNAIL_CLASS = 'bpmn-icon-connection';
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/Compensation.js
similarity index 74%
copy from saga/seata-saga-statemachine-designer/src/spec/TaskState.js
copy to saga/seata-saga-statemachine-designer/src/spec/Compensation.js
index 47ab79b3c8..c496c18249 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/Compensation.js
@@ -14,14 +14,12 @@
* limitations under the License.
*/
-import State from './State';
+import Transition from './Transition';
+
+export default class Compensation extends Transition {
-export default class TaskState extends State {
- constructor() {
- super();
- this.Input = [{}];
- this.Output = {};
- this.Status = {};
- this.Retry = [];
- }
}
+
+Compensation.prototype.Type = 'Compensation';
+
+Compensation.prototype.THUMBNAIL_CLASS = 'bpmn-icon-connection-multi';
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/CompensationTrigger.js
similarity index 68%
copy from saga/seata-saga-statemachine-designer/src/spec/TaskState.js
copy to saga/seata-saga-statemachine-designer/src/spec/CompensationTrigger.js
index 47ab79b3c8..4a023564fd 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/CompensationTrigger.js
@@ -16,12 +16,15 @@
import State from './State';
-export default class TaskState extends State {
- constructor() {
- super();
- this.Input = [{}];
- this.Output = {};
- this.Status = {};
- this.Retry = [];
- }
+export default class CompensationTrigger extends State {
+
}
+
+CompensationTrigger.prototype.Type = 'CompensationTrigger';
+
+CompensationTrigger.prototype.THUMBNAIL_CLASS =
'bpmn-icon-intermediate-event-catch-compensation';
+
+CompensationTrigger.prototype.DEFAULT_SIZE = {
+ width: 36,
+ height: 36,
+};
diff --git a/saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
b/saga/seata-saga-statemachine-designer/src/spec/ExceptionMatch.js
similarity index 58%
copy from saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
copy to saga/seata-saga-statemachine-designer/src/spec/ExceptionMatch.js
index 9ac9a2d447..72f7097b58 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/ExceptionMatch.js
@@ -14,21 +14,26 @@
* limitations under the License.
*/
-import TaskState from './TaskState';
+import Transition from './Transition';
-export default class ServiceTask extends TaskState {
+export default class ExceptionMatch extends Transition {
constructor() {
super();
- this.ServiceName = '';
- this.ServiceMethod = '';
+ this.Exceptions = [];
+ }
+
+ importJson(json) {
+ super.importJson(json);
+ this.Exceptions = json.Exceptions;
}
-}
-ServiceTask.prototype.Type = 'ServiceTask';
+ exportJson() {
+ const json = super.exportJson();
+ json.style.source = this.style.source.host.businessObject.Name;
+ return json;
+ }
+}
-ServiceTask.prototype.THUMBNAIL_CLASS = 'bpmn-icon-service-task';
+ExceptionMatch.prototype.Type = 'ExceptionMatch';
-ServiceTask.prototype.DEFAULT_SIZE = {
- width: 100,
- height: 80,
-};
+ExceptionMatch.prototype.THUMBNAIL_CLASS = 'bpmn-icon-connection';
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/Fail.js
similarity index 73%
copy from saga/seata-saga-statemachine-designer/src/spec/TaskState.js
copy to saga/seata-saga-statemachine-designer/src/spec/Fail.js
index 47ab79b3c8..d10fa518ab 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/Fail.js
@@ -16,12 +16,19 @@
import State from './State';
-export default class TaskState extends State {
+export default class Fail extends State {
constructor() {
super();
- this.Input = [{}];
- this.Output = {};
- this.Status = {};
- this.Retry = [];
+ this.ErrorCode = '';
+ this.Message = '';
}
}
+
+Fail.prototype.Type = 'Fail';
+
+Fail.prototype.THUMBNAIL_CLASS = 'bpmn-icon-end-event-error';
+
+Fail.prototype.DEFAULT_SIZE = {
+ width: 36,
+ height: 36,
+};
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/ScriptTask.js
similarity index 71%
copy from saga/seata-saga-statemachine-designer/src/spec/TaskState.js
copy to saga/seata-saga-statemachine-designer/src/spec/ScriptTask.js
index 47ab79b3c8..094f749c9c 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/ScriptTask.js
@@ -14,14 +14,16 @@
* limitations under the License.
*/
-import State from './State';
+import TaskState from './TaskState';
-export default class TaskState extends State {
+export default class ScriptTask extends TaskState {
constructor() {
super();
- this.Input = [{}];
- this.Output = {};
- this.Status = {};
- this.Retry = [];
+ this.ScriptType = 'groovy';
+ this.ScriptContent = '';
}
}
+
+ScriptTask.prototype.Type = 'ScriptTask';
+
+ScriptTask.prototype.THUMBNAIL_CLASS = 'bpmn-icon-script-task';
diff --git a/saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
b/saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
index 9ac9a2d447..000b1b3feb 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/ServiceTask.js
@@ -27,8 +27,3 @@ export default class ServiceTask extends TaskState {
ServiceTask.prototype.Type = 'ServiceTask';
ServiceTask.prototype.THUMBNAIL_CLASS = 'bpmn-icon-service-task';
-
-ServiceTask.prototype.DEFAULT_SIZE = {
- width: 100,
- height: 80,
-};
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/SubStateMachine.js
similarity index 71%
copy from saga/seata-saga-statemachine-designer/src/spec/TaskState.js
copy to saga/seata-saga-statemachine-designer/src/spec/SubStateMachine.js
index 47ab79b3c8..6dcd611487 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/SubStateMachine.js
@@ -14,14 +14,15 @@
* limitations under the License.
*/
-import State from './State';
+import TaskState from './TaskState';
-export default class TaskState extends State {
+export default class SubStateMachine extends TaskState {
constructor() {
super();
- this.Input = [{}];
- this.Output = {};
- this.Status = {};
- this.Retry = [];
+ this.StateMachineName = '';
}
}
+
+SubStateMachine.prototype.Type = 'SubStateMachine';
+
+SubStateMachine.prototype.THUMBNAIL_CLASS = 'bpmn-icon-subprocess-collapsed';
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/Succeed.js
similarity index 75%
copy from saga/seata-saga-statemachine-designer/src/spec/TaskState.js
copy to saga/seata-saga-statemachine-designer/src/spec/Succeed.js
index 47ab79b3c8..9ae46f34cd 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/Succeed.js
@@ -16,12 +16,14 @@
import State from './State';
-export default class TaskState extends State {
- constructor() {
- super();
- this.Input = [{}];
- this.Output = {};
- this.Status = {};
- this.Retry = [];
- }
+export default class Succeed extends State {
}
+
+Succeed.prototype.Type = 'Succeed';
+
+Succeed.prototype.THUMBNAIL_CLASS = 'bpmn-icon-end-event-none';
+
+Succeed.prototype.DEFAULT_SIZE = {
+ width: 36,
+ height: 36,
+};
diff --git a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
b/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
index 47ab79b3c8..70b1f45638 100644
--- a/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
+++ b/saga/seata-saga-statemachine-designer/src/spec/TaskState.js
@@ -19,9 +19,34 @@ import State from './State';
export default class TaskState extends State {
constructor() {
super();
+ this.IsForCompensation = false;
this.Input = [{}];
this.Output = {};
this.Status = {};
this.Retry = [];
}
+
+ importJson(json) {
+ super.importJson(json);
+ delete this.catch;
+ }
+
+ exportJson() {
+ const json = super.exportJson();
+ const { Catch } = json;
+ if (Catch) {
+ json.catch = json.Catch.exportJson();
+ json.Catch = [];
+ }
+
+ if (this.CompensateState) {
+ json.CompensateState = this.CompensateState.Name;
+ }
+ return json;
+ }
}
+
+TaskState.prototype.DEFAULT_SIZE = {
+ width: 100,
+ height: 80,
+};
diff --git a/saga/seata-saga-statemachine-designer/src/utils/index.js
b/saga/seata-saga-statemachine-designer/src/utils/index.js
index 91b12d590b..83007488b5 100644
--- a/saga/seata-saga-statemachine-designer/src/utils/index.js
+++ b/saga/seata-saga-statemachine-designer/src/utils/index.js
@@ -60,12 +60,12 @@ export function setProperties(businessObject, properties,
override) {
export function is(element, target) {
const type = element?.businessObject?.Type || element?.Type || element;
- if (target === 'State') {
- return type === 'ServiceTask';
+ if (target === 'Task') {
+ return type === 'ServiceTask' || type === 'ScriptTask' || type ===
'SubStateMachine';
}
if (target === 'Connection') {
- return type === 'Transition';
+ return type === 'Transition' || type === 'ChoiceEntry' || type ===
'ExceptionMatch' || type === 'Compensation';
}
return type === target;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]