From 67d81c346c3ba6ca9f0b778cfd9553e7aa619aa6 Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Wed, 11 Mar 2015 16:17:19 +0100 Subject: [PATCH] fix(replace): pre-build BPMN elements This simplifies the ways we work with replacements. Instead of monkey-patching the original replace functionality we * create the business object / target object * pass it to replace A simple way that works well with our existing infrastructure. Other changes: * fixes redo / undo issues. * splits context-pad + replace Closes #180 --- lib/Modeler.js | 3 - .../context-pad/ContextPadProvider.js | 33 +- .../context-pad/ReplaceMenuEntries.js | 395 ----------------- .../context-pad/ReplaceMenuFactory.js | 95 ---- lib/features/context-pad/index.js | 3 +- lib/features/replace/BpmnReplace.js | 205 ++++++--- lib/features/replace/ReplaceOptions.js | 407 ++++++++++++++++++ lib/features/replace/index.js | 9 +- .../{bpmn-replace => replace}/01_replace.bpmn | 0 test/spec/features/replace/BpmnReplaceSpec.js | 75 ++-- .../replace/ReplaceOptionsChooserSpec.js | 66 +++ 11 files changed, 679 insertions(+), 612 deletions(-) delete mode 100644 lib/features/context-pad/ReplaceMenuEntries.js delete mode 100644 lib/features/context-pad/ReplaceMenuFactory.js create mode 100644 lib/features/replace/ReplaceOptions.js rename test/fixtures/bpmn/features/{bpmn-replace => replace}/01_replace.bpmn (100%) create mode 100644 test/spec/features/replace/ReplaceOptionsChooserSpec.js diff --git a/lib/Modeler.js b/lib/Modeler.js index f11c9060..c5aecf9a 100644 --- a/lib/Modeler.js +++ b/lib/Modeler.js @@ -67,9 +67,6 @@ Modeler.prototype._modelingModules = [ require('diagram-js/lib/features/bendpoints'), require('diagram-js/lib/features/resize'), require('diagram-js/lib/features/lasso-tool'), - require('diagram-js/lib/features/replace'), - require('diagram-js/lib/features/popup-menu'), - require('./features/replace'), require('./features/modeling'), require('./features/context-pad'), require('./features/palette') diff --git a/lib/features/context-pad/ContextPadProvider.js b/lib/features/context-pad/ContextPadProvider.js index 1b40401c..b2f4239b 100644 --- a/lib/features/context-pad/ContextPadProvider.js +++ b/lib/features/context-pad/ContextPadProvider.js @@ -4,15 +4,13 @@ var assign = require('lodash/object/assign'), forEach = require('lodash/collection/forEach'); -var getReplacementMenuEntries = require('./ReplaceMenuFactory').getReplacementMenuEntries; - /** * A provider for BPMN 2.0 elements context pad */ function ContextPadProvider(contextPad, modeling, elementFactory, - connect, create, popupMenu, - replace, canvas) { + connect, create, bpmnReplace, + canvas) { contextPad.registerProvider(this); @@ -23,8 +21,7 @@ function ContextPadProvider(contextPad, modeling, elementFactory, this._elementFactory = elementFactory; this._connect = connect; this._create = create; - this._popupMenu = popupMenu; - this._replace = replace; + this._bpmnReplace = bpmnReplace; this._canvas = canvas; } @@ -34,8 +31,7 @@ ContextPadProvider.$inject = [ 'elementFactory', 'connect', 'create', - 'popupMenu', - 'replace', + 'bpmnReplace', 'canvas' ]; @@ -47,8 +43,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { elementFactory = this._elementFactory, connect = this._connect, create = this._create, - popupMenu = this._popupMenu, - replace = this._replace, + bpmnReplace = this._bpmnReplace, canvas = this._canvas; var actions = {}; @@ -71,11 +66,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { } } - function replaceElement(element, newType, options) { - replace.replaceElement(element, {type: newType}, options); - } - - function getPosition(element) { + function getReplaceMenuPosition(element) { var Y_OFFSET = 5; @@ -96,7 +87,6 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { } - function appendAction(type, className, options) { function appendListener(event, element) { @@ -177,17 +167,8 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { group: 'edit', className: 'icon-screw-wrench', action: { - click: function(event, element) { - var pos = getPosition(element); - - var entries = getReplacementMenuEntries(element, replaceElement); - - popupMenu.open( - 'replace-menu', - pos, - entries - ); + bpmnReplace.openChooser(getReplaceMenuPosition(element), element); } } } diff --git a/lib/features/context-pad/ReplaceMenuEntries.js b/lib/features/context-pad/ReplaceMenuEntries.js deleted file mode 100644 index 06747de6..00000000 --- a/lib/features/context-pad/ReplaceMenuEntries.js +++ /dev/null @@ -1,395 +0,0 @@ -'use strict'; - -var startEventReplace = [ - { - options: { - eventDefinition: '' - }, - label: 'Start Event', - actionName: 'replace-with-none-start', - className: 'icon-start-event-none', - targetType: 'bpmn:StartEvent' - }, - { - options: { - eventDefinition: '' - }, - label: 'Intermediate Throw Event', - actionName: 'replace-with-intermediate-throwing', - className: 'icon-intermediate-event-none', - targetType: 'bpmn:IntermediateThrowEvent' - }, - { - options: { - eventDefinition: '' - }, - label: 'End Event', - actionName: 'replace-with-message-end', - className: 'icon-end-event-none', - targetType: 'bpmn:EndEvent' - }, - { - label: 'Message Start Event', - actionName: 'replace-with-message-start', - className: 'icon-start-event-message', - targetType: 'bpmn:StartEvent', - options: { - eventDefinition: 'bpmn:MessageEventDefinition' - } - }, - { - label: 'Timer Start Event', - actionName: 'replace-with-timer-start', - className: 'icon-start-event-timer', - targetType: 'bpmn:StartEvent', - options: { - eventDefinition: 'bpmn:TimerEventDefinition' - } - }, - { - label: 'Conditional Start Event', - actionName: 'replace-with-conditional-start', - className: 'icon-start-event-condition', - targetType: 'bpmn:StartEvent', - options:{ - eventDefinition: 'bpmn:ConditionalEventDefinition' - } - }, - { - label: 'Signal Start Event', - actionName: 'replace-with-signal-start', - className: 'icon-start-event-signal', - targetType: 'bpmn:StartEvent', - options: { - eventDefinition: 'bpmn:SignalEventDefinition' - } - } -]; - - var intermediateEventReplace = [ - { - label: 'Start Event', - actionName: 'replace-with-none-start', - className: 'icon-start-event-none', - targetType: 'bpmn:StartEvent', - options: { - eventDefinition: '' - }, - }, - { - label: 'Intermediate Throw Event', - actionName: 'replace-with-message-intermediate-throw', - className: 'icon-intermediate-event-none', - targetType: 'bpmn:IntermediateThrowEvent', - options: { - eventDefinition: '' - } - }, - { - label: 'End Event', - actionName: 'replace-with-message-end', - className: 'icon-end-event-none', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: '' - } - }, - { - label: 'Message Intermediate Catch Event', - actionName: 'replace-with-intermediate-catch', - className: 'icon-intermediate-event-catch-message', - targetType: 'bpmn:IntermediateCatchEvent', - options: { - eventDefinition: 'bpmn:MessageEventDefinition' - } - }, - { - label: 'Message Intermediate Throw Event', - actionName: 'replace-with-intermediate-throw', - className: 'icon-intermediate-event-throw-message', - targetType: 'bpmn:IntermediateThrowEvent', - options: { - eventDefinition: 'bpmn:MessageEventDefinition' - } - }, - { - label: 'Timer Intermediate Catch Event', - actionName: 'replace-with-timer-intermediate-catch', - className: 'icon-intermediate-event-catch-timer', - targetType: 'bpmn:IntermediateCatchEvent', - options: { - eventDefinition: 'bpmn:TimerEventDefinition' - } - }, - { - label: 'Escalation Intermediate Catch Event', - actionName: 'replace-with-escalation-catch', - className: 'icon-intermediate-event-catch-escalation', - targetType: 'bpmn:IntermediateThrowEvent', - options: { - eventDefinition: 'bpmn:EscalationEventDefinition' - } - }, - { - label: 'Conditional Intermediate Catch Event', - actionName: 'replace-with-conditional-intermediate-catch', - className: 'icon-intermediate-event-catch-condition', - targetType: 'bpmn:IntermediateCatchEvent', - options: { - eventDefinition: 'bpmn:ConditionalEventDefinition' - } - }, - { - label: 'Link Intermediate Catch Event', - actionName: 'replace-with-link-intermediate-catch', - className: 'icon-intermediate-event-catch-link', - targetType: 'bpmn:IntermediateCatchEvent', - options: { - eventDefinition: 'bpmn:LinkEventDefinition' - } - }, - { - label: 'Link Intermediate Throw Event', - actionName: 'replace-with-link-intermediate-throw', - className: 'icon-intermediate-event-throw-link', - targetType: 'bpmn:IntermediateThrowEvent', - options: { - eventDefinition: 'bpmn:LinkEventDefinition' - } - }, - { - label: 'Compensation Intermediate Throw Event', - actionName: 'replace-with-compensation-intermediate-throw', - className: 'icon-intermediate-event-throw-compensation', - targetType: 'bpmn:IntermediateThrowEvent', - options: { - eventDefinition: 'bpmn:CompensateEventDefinition' - } - }, - { - label: 'Signal Throw Catch Event', - actionName: 'replace-with-throw-intermediate-catch', - className: 'icon-intermediate-event-catch-signal', - targetType: 'bpmn:IntermediateCatchEvent', - options: { - eventDefinition: 'bpmn:SignalEventDefinition' - } - }, - { - label: 'Signal Intermediate Throw Event', - actionName: 'replace-with-signal-intermediate-throw', - className: 'icon-intermediate-event-throw-signal', - targetType: 'bpmn:IntermediateThrowEvent', - options: { - eventDefinition: 'bpmn:SignalEventDefinition' - } - } - ]; - - - -var endEventReplace = [ - { - label: 'Start Event', - actionName: 'replace-with-none-start', - className: 'icon-start-event-none', - targetType: 'bpmn:StartEvent', - options: { - eventDefinition: '' - } - }, - { - label: 'Intermediate Throw Event', - actionName: 'replace-with-message-intermediate-throw', - className: 'icon-intermediate-event-none', - targetType: 'bpmn:IntermediateThrowEvent', - options: { - eventDefinition: '' - }, - }, - { - label: 'End Event', - actionName: 'replace-with-none-end', - className: 'icon-end-event-none', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: '' - }, - }, - { - label: 'Message End Event', - actionName: 'replace-with-message-end', - className: 'icon-end-event-message', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: 'bpmn:MessageEventDefinition' - } - }, - { - label: 'Escalation End Event', - actionName: 'replace-with-escalation-end', - className: 'icon-end-event-escalation', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: 'bpmn:EscalationEventDefinition' - } - }, - { - label: 'Error End Event', - actionName: 'replace-with-error-end', - className: 'icon-end-event-error', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: 'bpmn:ErrorEventDefinition' - } - }, - { - label: 'Cancel End Event', - actionName: 'replace-with-cancel-end', - className: 'icon-end-event-cancel', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: 'bpmn:CancelEventDefinition' - } - }, - { - label: 'Compensation End Event', - actionName: 'replace-with-compensation-end', - className: 'icon-end-event-compensation', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: 'bpmn:CompensateEventDefinition' - } - }, - { - label: 'Signal End Event', - actionName: 'replace-with-signal-end', - className: 'icon-end-event-signal', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: 'bpmn:SignalEventDefinition' - } - }, - { - label: 'Terminate End Event', - actionName: 'replace-with-terminate-end', - className: 'icon-end-event-terminate', - targetType: 'bpmn:EndEvent', - options: { - eventDefinition: 'bpmn:TerminateEventDefinition' - } - } -]; - -var gatewayReplace = [ - { - label: 'Exclusive Gateway', - actionName: 'replace-with-exclusive-gateway', - className: 'icon-gateway-xor', - targetType: 'bpmn:ExclusiveGateway' - }, - { - label: 'Parallel Gateway', - actionName: 'replace-with-parallel-gateway', - className: 'icon-gateway-parallel', - targetType: 'bpmn:ParallelGateway' - }, - { - label: 'Inclusive Gateway', - actionName: 'replace-with-inclusive-gateway', - className: 'icon-gateway-or', - targetType: 'bpmn:InclusiveGateway' - }, - { - label: 'Complex Gateway', - actionName: 'replace-with-complex-gateway', - className: 'icon-gateway-complex', - targetType: 'bpmn:ComplexGateway' - }, - { - label: 'Event based Gateway', - actionName: 'replace-with-event-based-gateway', - className: 'icon-gateway-eventbased', - targetType: 'bpmn:EventBasedGateway', - options: { - newBusinessAtt: { instantiate: false, eventGatewayType: 'Exclusive' } - } - } - // Gateways deactivated until https://github.com/bpmn-io/bpmn-js/issues/194 - // { - // label: 'Event based instantiating Gateway', - // actionName: 'replace-with-exclusive-event-based-gateway', - // className: 'icon-exclusive-event-based', - // targetType: 'bpmn:EventBasedGateway', - // options: { - // newBusinessAtt: { instantiate: true, eventGatewayType: 'Exclusive' } - // } - // }, - // { - // label: 'Parallel Event based instantiating Gateway', - // actionName: 'replace-with-parallel-event-based-instantiate-gateway', - // className: 'icon-parallel-event-based-instantiate-gateway', - // targetType: 'bpmn:EventBasedGateway', - // options: { - // newBusinessAtt: { instantiate: true, eventGatewayType: 'Parallel' } - // } - // } -]; - -var taskReplace = [ - { - label: 'Task', - actionName: 'replace-with-task', - className: 'icon-task', - targetType: 'bpmn:Task' - }, - { - label: 'Send Task', - actionName: 'replace-with-send-task', - className: 'icon-send', - targetType: 'bpmn:SendTask' - }, - { - label: 'Receive Task', - actionName: 'replace-with-receive-task', - className: 'icon-receive', - targetType: 'bpmn:ReceiveTask' - }, - { - label: 'User Task', - actionName: 'replace-with-user-task', - className: 'icon-user', - targetType: 'bpmn:UserTask' - }, - { - label: 'Manual Task', - actionName: 'replace-with-manual-task', - className: 'icon-manual', - targetType: 'bpmn:ManualTask' - }, - { - label: 'Business Rule Task', - actionName: 'replace-with-rule-task', - className: 'icon-business-rule', - targetType: 'bpmn:BusinessRuleTask' - }, - { - label: 'Service Task', - actionName: 'replace-with-service-task', - className: 'icon-service', - targetType: 'bpmn:ServiceTask' - }, - { - label: 'Script Task', - actionName: 'replace-with-script-task', - className: 'icon-script', - targetType: 'bpmn:ScriptTask' - } -]; - - -module.exports.startEventReplace = startEventReplace; -module.exports.intermediateEventReplace = intermediateEventReplace; -module.exports.endEventReplace = endEventReplace; -module.exports.gatewayReplace = gatewayReplace; -module.exports.taskReplace = taskReplace; diff --git a/lib/features/context-pad/ReplaceMenuFactory.js b/lib/features/context-pad/ReplaceMenuFactory.js deleted file mode 100644 index c0884a76..00000000 --- a/lib/features/context-pad/ReplaceMenuFactory.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -var forEach = require('lodash/collection/forEach'), - filter = require('lodash/collection/filter'); - -var ReplaceMenuEntries = require ('./ReplaceMenuEntries'); - -var startEventReplace = ReplaceMenuEntries.startEventReplace, - intermediateEventReplace = ReplaceMenuEntries.intermediateEventReplace, - endEventReplace = ReplaceMenuEntries.endEventReplace, - gatewayReplace = ReplaceMenuEntries.gatewayReplace, - taskReplace = ReplaceMenuEntries.taskReplace; - - - -function getReplacementMenuEntries(element, replaceElement) { - - var menuEntries = []; - var bo = element.businessObject; - - - if (bo.$instanceOf('bpmn:StartEvent')) { - - addEntries(startEventReplace, filterEvents); - } else if (bo.$instanceOf('bpmn:IntermediateCatchEvent') || - bo.$instanceOf('bpmn:IntermediateThrowEvent')) { - - addEntries(intermediateEventReplace, filterEvents); - } else if (bo.$instanceOf('bpmn:EndEvent')) { - - addEntries(endEventReplace, filterEvents); - } else if (bo.$instanceOf('bpmn:Gateway')) { - - addEntries(gatewayReplace, function(entry) { - - return entry.targetType !== bo.$type; - }); - } else if (bo.$instanceOf('bpmn:FlowNode')) { - - addEntries(taskReplace, function(entry) { - - return entry.targetType !== bo.$type; - }); - } - - function filterEvents(entry) { - - var eventDefinition = bo.eventDefinitions ? bo.eventDefinitions[0].$type : ''; - var isEventDefinitionEqual = entry.options.eventDefinition === eventDefinition; - var isEventTypeEqual = bo.$type === entry.targetType; - - return ((!isEventDefinitionEqual && isEventTypeEqual) || - !isEventTypeEqual) || - !(isEventDefinitionEqual && isEventTypeEqual); - } - - function addEntries(entries, filterFun) { - // Filter selected type from the array - var filteredEntries = filter(entries, filterFun); - - // Add entries to replace menu - forEach(filteredEntries, function(definition) { - - var entry = addMenuEntry(definition); - menuEntries.push(entry); - }); - } - - function addMenuEntry(definition) { - - var label = definition.label, - newType = definition.targetType, - options = definition.options, - actionName = definition.actionName, - className = definition.className; - - function appendListener() { - replaceElement(element, newType, options); - } - - return { - label: label, - className: className, - action: { - name: actionName, - handler: appendListener - } - }; - } - - return menuEntries; -} - - -module.exports.getReplacementMenuEntries = getReplacementMenuEntries; diff --git a/lib/features/context-pad/index.js b/lib/features/context-pad/index.js index 4d52bb3e..cb9bfdb4 100644 --- a/lib/features/context-pad/index.js +++ b/lib/features/context-pad/index.js @@ -5,7 +5,8 @@ module.exports = { require('diagram-js/lib/features/selection'), require('diagram-js/lib/features/connect'), require('diagram-js/lib/features/create'), - require('../modeling') + require('../modeling'), + require('../replace') ], __init__: [ 'contextPadProvider' ], contextPadProvider: [ 'type', require('./ContextPadProvider') ] diff --git a/lib/features/replace/BpmnReplace.js b/lib/features/replace/BpmnReplace.js index c7c95fd0..353a274a 100644 --- a/lib/features/replace/BpmnReplace.js +++ b/lib/features/replace/BpmnReplace.js @@ -1,71 +1,170 @@ 'use strict'; -var _ = require('lodash'); +var forEach = require('lodash/collection/forEach'), + filter = require('lodash/collection/filter'); -var LabelUtil = require('../label-editing/LabelUtil'), - BaseReplace = require('diagram-js/lib/features/replace/Replace'); +var REPLACE_OPTIONS = require ('./ReplaceOptions'); -function BpmnReplace(modeling, eventBus, moddle) { +var startEventReplace = REPLACE_OPTIONS.START_EVENT, + intermediateEventReplace = REPLACE_OPTIONS.INTERMEDIATE_EVENT, + endEventReplace = REPLACE_OPTIONS.END_EVENT, + gatewayReplace = REPLACE_OPTIONS.GATEWAY, + taskReplace = REPLACE_OPTIONS.TASK; - BaseReplace.call(this, modeling); - this._originalReplaceElement = BaseReplace.prototype.replaceElement; +/** + * A replace menu provider that gives users the controls to choose + * and replace BPMN elements with each other. + * + * @param {BpmnFactory} bpmnFactory + * @param {Moddle} moddle + * @param {PopupMenu} popupMenu + * @param {Replace} replace + */ +function BpmnReplace(bpmnFactory, moddle, popupMenu, replace) { - this._modeling = modeling; + /** + * Prepares a new business object for the replacement element + * and triggers the replace operation. + * + * @param {djs.model.Base} element + * @param {Object} target + * @return {djs.model.Base} the newly created element + */ + function replaceElement(element, target) { - eventBus.on([ - 'commandStack.shape.replace.execute' - ], function(event) { + var type = target.type, + oldBusinessObject = element.businessObject, + businessObject = bpmnFactory.create(type); - var context = event.context, - oldShape = context.oldShape, - newShape = context.newShape, - newBusinessAtt = context.options ? context.options.newBusinessAtt || {} : {}, - eventDefinitionType = context.options ? context.options.eventDefinition : null; + var newElement = { + type: type, + businessObject: businessObject + }; - // Attach eventDefinitions - if (eventDefinitionType) { - var eventDefinitions = newShape.businessObject.get('eventDefinitions'), - newEventDefinition = moddle.create(eventDefinitionType); + // initialize custom BPMN extensions - eventDefinitions.push(newEventDefinition); + if (target.eventDefinition) { + var eventDefinitions = businessObject.get('eventDefinitions'), + eventDefinition = moddle.create(target.eventDefinition); + + eventDefinitions.push(eventDefinition); } - // extend newBusinessAtt - _.assign(newShape.businessObject, newBusinessAtt); + if (target.instantiate !== undefined) { + businessObject.instantiate = target.instantiate; + } - // set label - modeling.updateLabel(newShape, LabelUtil.getLabel(oldShape)); - }); + if (target.eventGatewayType !== undefined) { + businessObject.eventGatewayType = target.eventGatewayType; + } - // TODO(nre): update bpmn specific properties based on our meta-model - // i.e. we should not only keep the name, but also - // other properties that exist in BOTH the old and new shape + // copy size (for activities only) + if (oldBusinessObject.$instanceOf('bpmn:Activity')) { + + // TODO: need also to respect min/max Size + + newElement.width = element.width; + newElement.height = element.height; + } + + // TODO: copy other elligable properties from old business object + businessObject.name = oldBusinessObject.name; + + return replace.replaceElement(element, newElement); + } + + + function getReplaceOptions(element) { + + var menuEntries = []; + var businessObject = element.businessObject; + + if (businessObject.$instanceOf('bpmn:StartEvent')) { + addEntries(startEventReplace, filterEvents); + } else + + if (businessObject.$instanceOf('bpmn:IntermediateCatchEvent') || + businessObject.$instanceOf('bpmn:IntermediateThrowEvent')) { + + addEntries(intermediateEventReplace, filterEvents); + } else + + if (businessObject.$instanceOf('bpmn:EndEvent')) { + + addEntries(endEventReplace, filterEvents); + } else + + if (businessObject.$instanceOf('bpmn:Gateway')) { + + addEntries(gatewayReplace, function(entry) { + + return entry.target.type !== businessObject.$type; + }); + } else + + if (businessObject.$instanceOf('bpmn:FlowNode')) { + addEntries(taskReplace, function(entry) { + return entry.target.type !== businessObject.$type; + }); + } + + function filterEvents(entry) { + + var target = entry.target; + + var eventDefinition = businessObject.eventDefinitions && businessObject.eventDefinitions[0].$type; + var isEventDefinitionEqual = target.eventDefinition == eventDefinition; + var isEventTypeEqual = businessObject.$type == target.type; + + return ((!isEventDefinitionEqual && isEventTypeEqual) || + !isEventTypeEqual) || + !(isEventDefinitionEqual && isEventTypeEqual); + } + + function addEntries(entries, filterFun) { + // Filter selected type from the array + var filteredEntries = filter(entries, filterFun); + + // Add entries to replace menu + forEach(filteredEntries, function(definition) { + + var entry = addMenuEntry(definition); + menuEntries.push(entry); + }); + } + + function addMenuEntry(definition) { + + return { + label: definition.label, + className: definition.className, + action: { + name: definition.actionName, + handler: function() { + replaceElement(element, definition.target); + } + } + }; + } + + return menuEntries; + } + + + // API + + this.openChooser = function(position, element) { + var entries = this.getReplaceOptions(element); + + popupMenu.open('replace-menu', position, entries); + }; + + this.getReplaceOptions = getReplaceOptions; + + this.replaceElement = replaceElement; } -module.exports = BpmnReplace; +BpmnReplace.$inject = [ 'bpmnFactory', 'moddle', 'popupMenu', 'replace' ]; -BpmnReplace.$inject = [ 'modeling', 'eventBus', 'moddle' ]; - - - -BpmnReplace.prototype = Object.create(BaseReplace.prototype); - -BpmnReplace.prototype.replaceElement = function(oldElement, newElementData, options) { - - if (oldElement.waypoints) { - throw new Error('connections cannot be replaced (yet)'); - } - - // use old elements size for activities - // TODO(nre): may also specify do this during the replace suggestions - // already (i.e. replace suggestions = { type, label, newElementData }) (?) - if (oldElement.businessObject.$instanceOf('bpmn:Activity')) { - - // TODO need also to respect min/max Size - newElementData.width = oldElement.width; - newElementData.height = oldElement.height; - } - - return this._originalReplaceElement(oldElement, newElementData, options || {}); -}; +module.exports = BpmnReplace; \ No newline at end of file diff --git a/lib/features/replace/ReplaceOptions.js b/lib/features/replace/ReplaceOptions.js new file mode 100644 index 00000000..8e948fd2 --- /dev/null +++ b/lib/features/replace/ReplaceOptions.js @@ -0,0 +1,407 @@ +'use strict'; + +module.exports.START_EVENT = [ + { + label: 'Start Event', + actionName: 'replace-with-none-start', + className: 'icon-start-event-none', + target: { + type: 'bpmn:StartEvent' + } + }, + { + label: 'Intermediate Throw Event', + actionName: 'replace-with-intermediate-throwing', + className: 'icon-intermediate-event-none', + target: { + type: 'bpmn:IntermediateThrowEvent' + } + }, + { + label: 'End Event', + actionName: 'replace-with-message-end', + className: 'icon-end-event-none', + target: { + type: 'bpmn:EndEvent' + } + }, + { + label: 'Message Start Event', + actionName: 'replace-with-message-start', + className: 'icon-start-event-message', + target: { + type: 'bpmn:StartEvent', + eventDefinition: 'bpmn:MessageEventDefinition' + } + }, + { + label: 'Timer Start Event', + actionName: 'replace-with-timer-start', + className: 'icon-start-event-timer', + target: { + type: 'bpmn:StartEvent', + eventDefinition: 'bpmn:TimerEventDefinition' + } + }, + { + label: 'Conditional Start Event', + actionName: 'replace-with-conditional-start', + className: 'icon-start-event-condition', + target: { + type: 'bpmn:StartEvent', + eventDefinition: 'bpmn:ConditionalEventDefinition' + } + }, + { + label: 'Signal Start Event', + actionName: 'replace-with-signal-start', + className: 'icon-start-event-signal', + target: { + type: 'bpmn:StartEvent', + eventDefinition: 'bpmn:SignalEventDefinition' + } + } +]; + +module.exports.INTERMEDIATE_EVENT = [ + { + label: 'Start Event', + actionName: 'replace-with-none-start', + className: 'icon-start-event-none', + target: { + type: 'bpmn:StartEvent' + } + }, + { + label: 'Intermediate Throw Event', + actionName: 'replace-with-message-intermediate-throw', + className: 'icon-intermediate-event-none', + target: { + type: 'bpmn:IntermediateThrowEvent' + } + }, + { + label: 'End Event', + actionName: 'replace-with-message-end', + className: 'icon-end-event-none', + target: { + type: 'bpmn:EndEvent' + } + }, + { + label: 'Message Intermediate Catch Event', + actionName: 'replace-with-intermediate-catch', + className: 'icon-intermediate-event-catch-message', + target: { + type: 'bpmn:IntermediateCatchEvent', + eventDefinition: 'bpmn:MessageEventDefinition' + } + }, + { + label: 'Message Intermediate Throw Event', + actionName: 'replace-with-intermediate-throw', + className: 'icon-intermediate-event-throw-message', + target: { + type: 'bpmn:IntermediateThrowEvent', + eventDefinition: 'bpmn:MessageEventDefinition' + } + }, + { + label: 'Timer Intermediate Catch Event', + actionName: 'replace-with-timer-intermediate-catch', + className: 'icon-intermediate-event-catch-timer', + target: { + type: 'bpmn:IntermediateCatchEvent', + eventDefinition: 'bpmn:TimerEventDefinition' + } + }, + { + label: 'Escalation Intermediate Catch Event', + actionName: 'replace-with-escalation-catch', + className: 'icon-intermediate-event-catch-escalation', + target: { + type: 'bpmn:IntermediateCatchEvent', + eventDefinition: 'bpmn:EscalationEventDefinition' + } + }, + { + label: 'Conditional Intermediate Catch Event', + actionName: 'replace-with-conditional-intermediate-catch', + className: 'icon-intermediate-event-catch-condition', + target: { + type: 'bpmn:IntermediateCatchEvent', + eventDefinition: 'bpmn:ConditionalEventDefinition' + } + }, + { + label: 'Link Intermediate Catch Event', + actionName: 'replace-with-link-intermediate-catch', + className: 'icon-intermediate-event-catch-link', + target: { + type: 'bpmn:IntermediateCatchEvent', + eventDefinition: 'bpmn:LinkEventDefinition' + } + }, + { + label: 'Link Intermediate Throw Event', + actionName: 'replace-with-link-intermediate-throw', + className: 'icon-intermediate-event-throw-link', + target: { + type: 'bpmn:IntermediateThrowEvent', + eventDefinition: 'bpmn:LinkEventDefinition' + } + }, + { + label: 'Compensation Intermediate Throw Event', + actionName: 'replace-with-compensation-intermediate-throw', + className: 'icon-intermediate-event-throw-compensation', + target: { + type: 'bpmn:IntermediateThrowEvent', + eventDefinition: 'bpmn:CompensateEventDefinition' + } + }, + { + label: 'Signal Throw Catch Event', + actionName: 'replace-with-throw-intermediate-catch', + className: 'icon-intermediate-event-catch-signal', + target: { + type: 'bpmn:IntermediateCatchEvent', + eventDefinition: 'bpmn:SignalEventDefinition' + } + }, + { + label: 'Signal Intermediate Throw Event', + actionName: 'replace-with-signal-intermediate-throw', + className: 'icon-intermediate-event-throw-signal', + target: { + type: 'bpmn:IntermediateThrowEvent', + eventDefinition: 'bpmn:SignalEventDefinition' + } + } +]; + +module.exports.END_EVENT = [ + { + label: 'Start Event', + actionName: 'replace-with-none-start', + className: 'icon-start-event-none', + target: { + type: 'bpmn:StartEvent' + } + }, + { + label: 'Intermediate Throw Event', + actionName: 'replace-with-message-intermediate-throw', + className: 'icon-intermediate-event-none', + target: { + type: 'bpmn:IntermediateThrowEvent' + } + }, + { + label: 'End Event', + actionName: 'replace-with-none-end', + className: 'icon-end-event-none', + target: { + type: 'bpmn:EndEvent' + } + }, + { + label: 'Message End Event', + actionName: 'replace-with-message-end', + className: 'icon-end-event-message', + target: { + type: 'bpmn:EndEvent', + eventDefinition: 'bpmn:MessageEventDefinition' + } + }, + { + label: 'Escalation End Event', + actionName: 'replace-with-escalation-end', + className: 'icon-end-event-escalation', + target: { + type: 'bpmn:EndEvent', + eventDefinition: 'bpmn:EscalationEventDefinition' + } + }, + { + label: 'Error End Event', + actionName: 'replace-with-error-end', + className: 'icon-end-event-error', + target: { + type: 'bpmn:EndEvent', + eventDefinition: 'bpmn:ErrorEventDefinition' + } + }, + { + label: 'Cancel End Event', + actionName: 'replace-with-cancel-end', + className: 'icon-end-event-cancel', + target: { + type: 'bpmn:EndEvent', + eventDefinition: 'bpmn:CancelEventDefinition' + } + }, + { + label: 'Compensation End Event', + actionName: 'replace-with-compensation-end', + className: 'icon-end-event-compensation', + target: { + type: 'bpmn:EndEvent', + eventDefinition: 'bpmn:CompensateEventDefinition' + } + }, + { + label: 'Signal End Event', + actionName: 'replace-with-signal-end', + className: 'icon-end-event-signal', + target: { + type: 'bpmn:EndEvent', + eventDefinition: 'bpmn:SignalEventDefinition' + } + }, + { + label: 'Terminate End Event', + actionName: 'replace-with-terminate-end', + className: 'icon-end-event-terminate', + target: { + type: 'bpmn:EndEvent', + eventDefinition: 'bpmn:TerminateEventDefinition' + } + } +]; + +module.exports.GATEWAY = [ + { + label: 'Exclusive Gateway', + actionName: 'replace-with-exclusive-gateway', + className: 'icon-gateway-xor', + target: { + type: 'bpmn:ExclusiveGateway' + } + }, + { + label: 'Parallel Gateway', + actionName: 'replace-with-parallel-gateway', + className: 'icon-gateway-parallel', + target: { + type: 'bpmn:ParallelGateway' + } + }, + { + label: 'Inclusive Gateway', + actionName: 'replace-with-inclusive-gateway', + className: 'icon-gateway-or', + target: { + type: 'bpmn:InclusiveGateway' + } + }, + { + label: 'Complex Gateway', + actionName: 'replace-with-complex-gateway', + className: 'icon-gateway-complex', + target: { + type: 'bpmn:ComplexGateway' + } + }, + { + label: 'Event based Gateway', + actionName: 'replace-with-event-based-gateway', + className: 'icon-gateway-eventbased', + target: { + type: 'bpmn:EventBasedGateway', + instantiate: false, + eventGatewayType: 'Exclusive' + } + } + // Gateways deactivated until https://github.com/bpmn-io/bpmn-js/issues/194 + // { + // label: 'Event based instantiating Gateway', + // actionName: 'replace-with-exclusive-event-based-gateway', + // className: 'icon-exclusive-event-based', + // target: { + // type: 'bpmn:EventBasedGateway' + // }, + // options: { + // businessObject: { instantiate: true, eventGatewayType: 'Exclusive' } + // } + // }, + // { + // label: 'Parallel Event based instantiating Gateway', + // actionName: 'replace-with-parallel-event-based-instantiate-gateway', + // className: 'icon-parallel-event-based-instantiate-gateway', + // target: { + // type: 'bpmn:EventBasedGateway' + // }, + // options: { + // businessObject: { instantiate: true, eventGatewayType: 'Parallel' } + // } + // } +]; + + +module.exports.TASK = [ + { + label: 'Task', + actionName: 'replace-with-task', + className: 'icon-task', + target: { + type: 'bpmn:Task' + } + }, + { + label: 'Send Task', + actionName: 'replace-with-send-task', + className: 'icon-send', + target: { + type: 'bpmn:SendTask' + } + }, + { + label: 'Receive Task', + actionName: 'replace-with-receive-task', + className: 'icon-receive', + target: { + type: 'bpmn:ReceiveTask' + } + }, + { + label: 'User Task', + actionName: 'replace-with-user-task', + className: 'icon-user', + target: { + type: 'bpmn:UserTask' + } + }, + { + label: 'Manual Task', + actionName: 'replace-with-manual-task', + className: 'icon-manual', + target: { + type: 'bpmn:ManualTask' + } + }, + { + label: 'Business Rule Task', + actionName: 'replace-with-rule-task', + className: 'icon-business-rule', + target: { + type: 'bpmn:BusinessRuleTask' + } + }, + { + label: 'Service Task', + actionName: 'replace-with-service-task', + className: 'icon-service', + target: { + type: 'bpmn:ServiceTask' + } + }, + { + label: 'Script Task', + actionName: 'replace-with-script-task', + className: 'icon-script', + target: { + type: 'bpmn:ScriptTask' + } + } +]; \ No newline at end of file diff --git a/lib/features/replace/index.js b/lib/features/replace/index.js index af745520..a5548d6a 100644 --- a/lib/features/replace/index.js +++ b/lib/features/replace/index.js @@ -1,7 +1,8 @@ module.exports = { __depends__: [ - require('diagram-js/lib/features/rules') + require('diagram-js/lib/features/popup-menu'), + require('diagram-js/lib/features/replace'), + require('../modeling') ], - __init__: [ 'replace' ], - replace: [ 'type', require('./BpmnReplace') ], -}; + bpmnReplace: [ 'type', require('./BpmnReplace') ] +}; \ No newline at end of file diff --git a/test/fixtures/bpmn/features/bpmn-replace/01_replace.bpmn b/test/fixtures/bpmn/features/replace/01_replace.bpmn similarity index 100% rename from test/fixtures/bpmn/features/bpmn-replace/01_replace.bpmn rename to test/fixtures/bpmn/features/replace/01_replace.bpmn diff --git a/test/spec/features/replace/BpmnReplaceSpec.js b/test/spec/features/replace/BpmnReplaceSpec.js index aac9ea12..32185e2a 100644 --- a/test/spec/features/replace/BpmnReplaceSpec.js +++ b/test/spec/features/replace/BpmnReplaceSpec.js @@ -4,8 +4,6 @@ var TestHelper = require('../../../TestHelper'); /* global bootstrapModeler, inject */ -var _ = require('lodash'); - var fs = require('fs'); var modelingModule = require('../../../../lib/features/modeling'), @@ -16,7 +14,7 @@ var modelingModule = require('../../../../lib/features/modeling'), describe('features/replace', function() { - var diagramXML = fs.readFileSync('test/fixtures/bpmn/features/bpmn-replace/01_replace.bpmn', 'utf8'); + var diagramXML = fs.readFileSync('test/fixtures/bpmn/features/replace/01_replace.bpmn', 'utf8'); var testModules = [ coreModule, modelingModule, replaceModule ]; @@ -25,7 +23,7 @@ describe('features/replace', function() { describe('should replace', function() { - it('task', inject(function(elementRegistry, modeling, replace) { + it('task', inject(function(elementRegistry, modeling, bpmnReplace) { // given var task = elementRegistry.get('Task_1'); @@ -34,7 +32,7 @@ describe('features/replace', function() { }; // when - var newElement = replace.replaceElement(task, newElementData); + var newElement = bpmnReplace.replaceElement(task, newElementData); // then var businessObject = newElement.businessObject; @@ -44,7 +42,7 @@ describe('features/replace', function() { })); - it('gateway', inject(function(elementRegistry, modeling, replace) { + it('gateway', inject(function(elementRegistry, modeling, bpmnReplace) { // given var gateway = elementRegistry.get('ExclusiveGateway_1'); @@ -53,7 +51,7 @@ describe('features/replace', function() { }; // when - var newElement = replace.replaceElement(gateway, newElementData); + var newElement = bpmnReplace.replaceElement(gateway, newElementData); // then @@ -68,7 +66,7 @@ describe('features/replace', function() { describe('position and size', function() { - it('should keep position', inject(function(elementRegistry, replace) { + it('should keep position', inject(function(elementRegistry, bpmnReplace) { // given var task = elementRegistry.get('Task_1'); @@ -77,7 +75,7 @@ describe('features/replace', function() { }; // when - var newElement = replace.replaceElement(task, newElementData); + var newElement = bpmnReplace.replaceElement(task, newElementData); // then expect(newElement.x).toBe(346); @@ -89,7 +87,7 @@ describe('features/replace', function() { describe('label', function() { - it('should keep copy label', inject(function(elementRegistry, replace) { + it('should keep copy label', inject(function(elementRegistry, bpmnReplace) { // given var task = elementRegistry.get('Task_1'); @@ -99,7 +97,7 @@ describe('features/replace', function() { }; // when - var newElement = replace.replaceElement(task, newElementData); + var newElement = bpmnReplace.replaceElement(task, newElementData); // then expect(newElement.businessObject.name).toBe('Task Caption'); @@ -107,9 +105,10 @@ describe('features/replace', function() { }); + describe('undo support', function() { - it('should undo replace', inject(function(elementRegistry, modeling, replace, commandStack) { + it('should undo replace', inject(function(elementRegistry, modeling, bpmnReplace, commandStack) { // given var task = elementRegistry.get('Task_1'); @@ -117,7 +116,7 @@ describe('features/replace', function() { type: 'bpmn:UserTask' }; - replace.replaceElement(task, newElementData); + bpmnReplace.replaceElement(task, newElementData); // when commandStack.undo(); @@ -131,7 +130,7 @@ describe('features/replace', function() { })); - it('should redo replace', inject(function(elementRegistry, modeling, replace, commandStack, eventBus) { + it('should redo replace', inject(function(elementRegistry, modeling, bpmnReplace, commandStack, eventBus) { // given var task = elementRegistry.get('Task_1'); @@ -142,8 +141,8 @@ describe('features/replace', function() { type: 'bpmn:ServiceTask' }; - var usertask = replace.replaceElement(task, newElementData); - var servicetask = replace.replaceElement(usertask, newElementData2); + var usertask = bpmnReplace.replaceElement(task, newElementData); + var servicetask = bpmnReplace.replaceElement(usertask, newElementData2); commandStack.undo(); commandStack.undo(); @@ -164,7 +163,7 @@ describe('features/replace', function() { describe('connection handling', function() { - it('should reconnect valid connections', inject(function(elementRegistry, modeling, replace) { + it('should reconnect valid connections', inject(function(elementRegistry, modeling, bpmnReplace) { // given var task = elementRegistry.get('Task_1'); @@ -173,7 +172,7 @@ describe('features/replace', function() { }; // when - var newElement = replace.replaceElement(task, newElementData); + var newElement = bpmnReplace.replaceElement(task, newElementData); // then var incoming = newElement.incoming[0], @@ -189,7 +188,7 @@ describe('features/replace', function() { })); - it('should remove invalid incomming connections', inject(function(elementRegistry, modeling, replace) { + it('should remove invalid incomming connections', inject(function(elementRegistry, modeling, bpmnReplace) { // given var task = elementRegistry.get('StartEvent_1'); @@ -198,7 +197,7 @@ describe('features/replace', function() { }; // when - var newElement = replace.replaceElement(task, newElementData); + var newElement = bpmnReplace.replaceElement(task, newElementData); // then var incoming = newElement.incoming[0], @@ -209,7 +208,8 @@ describe('features/replace', function() { expect(outgoing).toBeUndefined(); })); - it('should remove invalid outgoing connections', inject(function(elementRegistry, modeling, replace) { + + it('should remove invalid outgoing connections', inject(function(elementRegistry, modeling, bpmnReplace) { // given var task = elementRegistry.get('EndEvent_1'); @@ -218,7 +218,7 @@ describe('features/replace', function() { }; // when - var newElement = replace.replaceElement(task, newElementData); + var newElement = bpmnReplace.replaceElement(task, newElementData); // then var incoming = newElement.incoming[0], @@ -232,14 +232,15 @@ describe('features/replace', function() { describe('undo support', function() { - it('should reconnect valid connections', inject(function(elementRegistry, modeling, replace, commandStack) { + it('should reconnect valid connections', inject(function(elementRegistry, modeling, bpmnReplace, commandStack) { // given var task = elementRegistry.get('Task_1'); var newElementData = { type: 'bpmn:UserTask' }; - var newElement = replace.replaceElement(task, newElementData); + + bpmnReplace.replaceElement(task, newElementData); // when commandStack.undo(); @@ -258,15 +259,17 @@ describe('features/replace', function() { expect(target).toBe(elementRegistry.get('ExclusiveGateway_1')); })); + it('should remove invalid incoming connections', inject(function(elementRegistry, - modeling, replace, commandStack) { + modeling, bpmnReplace, commandStack) { // given var startEvent = elementRegistry.get('StartEvent_1'); var newElementData = { type: 'bpmn:EndEvent' }; - var newElement = replace.replaceElement(startEvent, newElementData); + + bpmnReplace.replaceElement(startEvent, newElementData); // when commandStack.undo(); @@ -285,14 +288,15 @@ describe('features/replace', function() { it('should remove invalid outgoing connections', inject(function(elementRegistry, - modeling, replace, commandStack) { + modeling, bpmnReplace, commandStack) { // given var endEvent = elementRegistry.get('EndEvent_1'); var newElementData = { type: 'bpmn:StartEvent' }; - var newElement = replace.replaceElement(endEvent, newElementData); + + bpmnReplace.replaceElement(endEvent, newElementData); // when commandStack.undo(); @@ -311,17 +315,17 @@ describe('features/replace', function() { }); + describe('redo support', function() { - - it('should reconnect valid connections', inject(function(elementRegistry, modeling, replace, commandStack) { + it('should reconnect valid connections', inject(function(elementRegistry, modeling, bpmnReplace, commandStack) { // given var task = elementRegistry.get('Task_1'); var newElementData = { type: 'bpmn:UserTask' }; - var newElement = replace.replaceElement(task, newElementData); + var newElement = bpmnReplace.replaceElement(task, newElementData); // when commandStack.undo(); @@ -342,14 +346,14 @@ describe('features/replace', function() { it('should remove invalid incoming connections', inject(function(elementRegistry, - modeling, replace, commandStack) { + modeling, bpmnReplace, commandStack) { // given var startEvent = elementRegistry.get('StartEvent_1'); var newElementData = { type: 'bpmn:EndEvent' }; - var newElement = replace.replaceElement(startEvent, newElementData); + var newElement = bpmnReplace.replaceElement(startEvent, newElementData); // when commandStack.undo(); @@ -366,14 +370,14 @@ describe('features/replace', function() { it('should remove invalid outgoing connections', inject(function(elementRegistry, - modeling, replace, commandStack) { + modeling, bpmnReplace, commandStack) { // given var endEvent = elementRegistry.get('EndEvent_1'); var newElementData = { type: 'bpmn:StartEvent' }; - var newElement = replace.replaceElement(endEvent, newElementData); + var newElement = bpmnReplace.replaceElement(endEvent, newElementData); // when commandStack.undo(); @@ -387,6 +391,7 @@ describe('features/replace', function() { expect(incoming).toBeUndefined(); expect(outgoing).toBeUndefined(); })); + }); }); diff --git a/test/spec/features/replace/ReplaceOptionsChooserSpec.js b/test/spec/features/replace/ReplaceOptionsChooserSpec.js new file mode 100644 index 00000000..496838a9 --- /dev/null +++ b/test/spec/features/replace/ReplaceOptionsChooserSpec.js @@ -0,0 +1,66 @@ +'use strict'; + +var TestHelper = require('../../../TestHelper'); + +/* global bootstrapModeler, inject */ + +var fs = require('fs'); + +var modelingModule = require('../../../../lib/features/modeling'), + replaceModule = require('../../../../lib/features/replace'), + coreModule = require('../../../../lib/core'); + + + +describe('features/replace - chooser', function() { + + var diagramXML = fs.readFileSync('test/fixtures/bpmn/features/replace/01_replace.bpmn', 'utf8'); + + var testModules = [ coreModule, modelingModule, replaceModule ]; + + beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); + + + describe('should show chooser', function() { + + it('for task', inject(function(elementRegistry, modeling, bpmnReplace) { + + // given + var element = elementRegistry.get('Task_1'); + + // when + bpmnReplace.openChooser({ x: 100, y: 100 }, element); + + // then + expect(null).toBeDefined(); + })); + + + it('for event event', inject(function(elementRegistry, modeling, bpmnReplace) { + + // given + var element = elementRegistry.get('StartEvent_1'); + + // when + bpmnReplace.openChooser({ x: 100, y: 100 }, element); + + // then + expect(null).toBeDefined(); + })); + + + it('for gateway event', inject(function(elementRegistry, modeling, bpmnReplace) { + + // given + var element = elementRegistry.get('ExclusiveGateway_1'); + + // when + bpmnReplace.openChooser({ x: 100, y: 100 }, element); + + // then + expect(null).toBeDefined(); + })); + + }); + +});