diff --git a/CHANGELOG.md b/CHANGELOG.md index e63958471..e8e70b646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to [bpmn-js](https://github.com/bpmn-io/bpmn-js) are documen ___Note:__ Yet to be released changes appear here._ +* `FIX`: pasting compensation activity without boundary event ([#2070](https://github.com/bpmn-io/bpmn-js/issues/2070)) + ## 17.9.2 * `FIX`: keep direction when collapsing pools ([#2208](https://github.com/bpmn-io/bpmn-js/issues/2208)) diff --git a/lib/features/modeling/behavior/SetCompensationActivityAfterPasteBehavior.js b/lib/features/modeling/behavior/SetCompensationActivityAfterPasteBehavior.js new file mode 100644 index 000000000..62b6afd08 --- /dev/null +++ b/lib/features/modeling/behavior/SetCompensationActivityAfterPasteBehavior.js @@ -0,0 +1,67 @@ +import inherits from 'inherits-browser'; + +import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; + +import { getBusinessObject, is } from '../../../util/ModelUtil'; + +import { hasEventDefinition } from '../../../util/DiUtil'; + +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('../../rules/BpmnRules').default} BpmnRules + * @typedef {import('../Modeling').default} Modeling + */ + + +/** + * A behavior that sets the property of Compensation Activity after paste operation + * + * @param {EventBus} eventBus + * @param {Modeling} modeling + */ +export default function SetCompensationActivityAfterPasteBehavior(eventBus, modeling) { + + CommandInterceptor.call(this, eventBus); + + this.postExecuted('elements.create', function(event) { + const context = event.context, + elements = context.elements; + + // check if compensation activity is connected to compensation boundary event + for (const element of elements) { + if (isForCompensation(element) && !isConnectedToCompensationBoundaryEvent(element)) { + modeling.updateProperties(element, { isForCompensation: undefined }); + } + } + }); +} + +inherits(SetCompensationActivityAfterPasteBehavior, CommandInterceptor); + +SetCompensationActivityAfterPasteBehavior.$inject = [ + 'eventBus', + 'modeling' +]; + + +// helpers ////////////////////// + +function isForCompensation(element) { + const bo = getBusinessObject(element); + return bo && bo.isForCompensation; +} + +function isCompensationBoundaryEvent(element) { + return element && is(element, 'bpmn:BoundaryEvent') && + hasEventDefinition(element, 'bpmn:CompensateEventDefinition'); +} + +function isConnectedToCompensationBoundaryEvent(element) { + const compensationAssociations = element.incoming.filter( + connection => isCompensationBoundaryEvent(connection.source) + ); + if (compensationAssociations.length > 0) { + return true; + } + return false; +} \ No newline at end of file diff --git a/lib/features/modeling/behavior/index.js b/lib/features/modeling/behavior/index.js index 5e34c1a81..a7e0d93d1 100644 --- a/lib/features/modeling/behavior/index.js +++ b/lib/features/modeling/behavior/index.js @@ -38,6 +38,7 @@ import ToggleElementCollapseBehaviour from './ToggleElementCollapseBehaviour'; import UnclaimIdBehavior from './UnclaimIdBehavior'; import UnsetDefaultFlowBehavior from './UnsetDefaultFlowBehavior'; import UpdateFlowNodeRefsBehavior from './UpdateFlowNodeRefsBehavior'; +import SetCompensationActivityAfterPasteBehavior from './SetCompensationActivityAfterPasteBehavior'; /** * @type { import('didi').ModuleDeclaration } @@ -83,7 +84,8 @@ export default { 'toggleElementCollapseBehaviour', 'unclaimIdBehavior', 'updateFlowNodeRefsBehavior', - 'unsetDefaultFlowBehavior' + 'unsetDefaultFlowBehavior', + 'setCompensationActivityAfterPasteBehavior' ], adaptiveLabelPositioningBehavior: [ 'type', AdaptiveLabelPositioningBehavior ], appendBehavior: [ 'type', AppendBehavior ], @@ -124,5 +126,6 @@ export default { toggleElementCollapseBehaviour : [ 'type', ToggleElementCollapseBehaviour ], unclaimIdBehavior: [ 'type', UnclaimIdBehavior ], unsetDefaultFlowBehavior: [ 'type', UnsetDefaultFlowBehavior ], - updateFlowNodeRefsBehavior: [ 'type', UpdateFlowNodeRefsBehavior ] + updateFlowNodeRefsBehavior: [ 'type', UpdateFlowNodeRefsBehavior ], + setCompensationActivityAfterPasteBehavior: [ 'type', SetCompensationActivityAfterPasteBehavior ] }; diff --git a/test/spec/features/modeling/behavior/SetCompensationActivityAfterPasteBehaviorSpec.bpmn b/test/spec/features/modeling/behavior/SetCompensationActivityAfterPasteBehaviorSpec.bpmn new file mode 100644 index 000000000..34d317117 --- /dev/null +++ b/test/spec/features/modeling/behavior/SetCompensationActivityAfterPasteBehaviorSpec.bpmn @@ -0,0 +1,42 @@ + + + + + Flow_1ank1yi + + + Flow_1ank1yi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/features/modeling/behavior/SetCompensationActivityAfterPasteBehaviorSpec.js b/test/spec/features/modeling/behavior/SetCompensationActivityAfterPasteBehaviorSpec.js new file mode 100644 index 000000000..fbd6ba768 --- /dev/null +++ b/test/spec/features/modeling/behavior/SetCompensationActivityAfterPasteBehaviorSpec.js @@ -0,0 +1,80 @@ +import { + bootstrapModeler, + inject +} from 'test/TestHelper'; + +import modelingModule from 'lib/features/modeling'; +import coreModule from 'lib/core'; +import { is } from 'lib/util/ModelUtil'; +import copyPasteModule from 'lib/features/copy-paste'; + +import diagramXML from './SetCompensationActivityAfterPasteBehaviorSpec.bpmn'; + + +describe('features/modeling/behavior - compensation activity after paste', function() { + + const testModules = [ + copyPasteModule, + coreModule, + modelingModule + ]; + + beforeEach(bootstrapModeler(diagramXML, { modules: testModules })); + + + describe('copy/paste compensation activity', function() { + + it('without boundary event', inject(function(canvas, elementRegistry, copyPaste) { + + // given + copyPaste.copy([ elementRegistry.get('Compensation_Activity') ]); + + // when + var copiedElements = copyPaste.paste({ + element: canvas.getRootElement(), + point: { + x: 100, + y: 100 + } + }); + + // then + expect(copiedElements).to.have.lengthOf(1); + const taskElement = copiedElements.find(element => is(element, 'bpmn:Task')); + expect(taskElement.businessObject.isForCompensation).to.be.false; + })); + + + it('with boundary event', inject(function(canvas, elementRegistry, copyPaste) { + + // given + copyPaste.copy([ + elementRegistry.get('Compensation_Boundary_Task'), + elementRegistry.get('Compensation_Activity') ]); + + // when + var copiedElements = copyPaste.paste({ + element: canvas.getRootElement(), + point: { + x: 100, + y: 100 + } + }); + + // then + expect(copiedElements).to.have.lengthOf(4); + expect(copiedElements.filter(element => is(element, 'bpmn:Association'))).to.have.length(1); + expect(copiedElements.filter(element => is(element, 'bpmn:BoundaryEvent'))).to.have.length(1); + expect(copiedElements.filter(element => is(element, 'bpmn:Task'))).to.have.length(2); + + // verify that for every Task element, if businessObject.isForCompensation exists, it should be true + copiedElements.filter(element => is(element, 'bpmn:Task')).forEach(taskElement => { + if (Object.prototype.hasOwnProperty.call(taskElement.businessObject, 'isForCompensation')) { + expect(taskElement.businessObject.isForCompensation).to.be.true; + } + }); + })); + + }); + +}); \ No newline at end of file