Skip to content

Commit

Permalink
feature: full support for states in the refactored state machine desi…
Browse files Browse the repository at this point in the history
…gner (#6169)
  • Loading branch information
ptyin authored Jan 5, 2024
1 parent c029314 commit bfad822
Show file tree
Hide file tree
Showing 25 changed files with 868 additions and 41 deletions.
2 changes: 2 additions & 0 deletions saga/seata-saga-statemachine-designer/src/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,6 +80,7 @@ Editor.prototype.modules = [

// Built-in modules
AlignElementsModule,
AttachSupport,
AutoScrollModule,
BendpointsModule,
ConnectModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'];
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
34 changes: 29 additions & 5 deletions saga/seata-saga-statemachine-designer/src/modeling/SagaExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
};

Expand Down
20 changes: 20 additions & 0 deletions saga/seata-saga-statemachine-designer/src/modeling/SagaFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,33 @@ 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();
typeToSpec.set('Transition', Transition);
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;
}

Expand Down
35 changes: 31 additions & 4 deletions saga/seata-saga-statemachine-designer/src/modeling/SagaImporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -60,6 +62,7 @@ SagaImporter.$inject = [
'canvas',
'elementFactory',
'elementRegistry',
'modeling',
];

SagaImporter.prototype.import = function (definitions) {
Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
78 changes: 76 additions & 2 deletions saga/seata-saga-statemachine-designer/src/modeling/SagaRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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}`);
});
Expand Down
Loading

0 comments on commit bfad822

Please sign in to comment.