diff --git a/app/app.js b/app/app.js index dc8188e..db5e688 100644 --- a/app/app.js +++ b/app/app.js @@ -3,7 +3,7 @@ import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule, } from 'bpmn-js-properties-panel'; -import diagramXML from '../test/spec/bpmn/basic_message.bpmn'; +import diagramXML from '../test/spec/bpmn/empty_diagram.bpmn'; import spiffworkflow from './spiffworkflow'; import setupFileOperations from './fileOperations'; diff --git a/app/spiffworkflow/DataObject/DataObjectHelpers.js b/app/spiffworkflow/DataObject/DataObjectHelpers.js index de081e4..1c62efc 100644 --- a/app/spiffworkflow/DataObject/DataObjectHelpers.js +++ b/app/spiffworkflow/DataObject/DataObjectHelpers.js @@ -4,9 +4,9 @@ * @param container */ - -export function findDataObjects(parent) { - let dataObjects = []; +export function findDataObjects(parent, dataObjects) { + if (typeof(dataObjects) === 'undefined') + dataObjects = []; let process; if (!parent) { return []; @@ -15,16 +15,13 @@ export function findDataObjects(parent) { process = parent.processRef; } else { process = parent; + if (process.$type === 'bpmn:SubProcess') + findDataObjects(process.$parent, dataObjects); } - if (!process.flowElements) { - return []; - } - for (const element of process.flowElements) { - if ( - element.$type === 'bpmn:DataObject' && - dataObjects.indexOf(element) < 0 - ) { - dataObjects.push(element); + if (typeof(process.flowElements) !== 'undefined') { + for (const element of process.flowElements) { + if (element.$type === 'bpmn:DataObject') + dataObjects.push(element); } } return dataObjects; @@ -38,16 +35,26 @@ export function findDataObject(process, id) { } } -export function findDataReferenceShapes(processShape, id) { - let refs = []; - for (const shape of processShape.children) { - if (shape.type === 'bpmn:DataObjectReference') { - if (shape.businessObject.dataObjectRef && shape.businessObject.dataObjectRef.id === id) { - refs.push(shape); - } - } - } - return refs; +export function findDataObjectReferences(children, dataObjectId) { + return children.flatMap((child) => { + if (child.$type == 'bpmn:DataObjectReference' && child.dataObjectRef.id == dataObjectId) + return [child]; + else if (child.$type == 'bpmn:SubProcess') + return findDataObjectReferences(child.get('flowElements'), dataObjectId); + else + return []; + }); +} + +export function findDataObjectReferenceShapes(children, dataObjectId) { + return children.flatMap((child) => { + if (child.type == 'bpmn:DataObjectReference' && child.businessObject.dataObjectRef.id == dataObjectId) + return [child]; + else if (child.type == 'bpmn:SubProcess') + return findDataObjectReferenceShapes(child.children, dataObjectId); + else + return []; + }); } export function idToHumanReadableName(id) { diff --git a/app/spiffworkflow/DataObject/DataObjectInterceptor.js b/app/spiffworkflow/DataObject/DataObjectInterceptor.js index c008830..1dc6538 100644 --- a/app/spiffworkflow/DataObject/DataObjectInterceptor.js +++ b/app/spiffworkflow/DataObject/DataObjectInterceptor.js @@ -3,11 +3,12 @@ import { getDi, is } from 'bpmn-js/lib/util/ModelUtil'; import { remove as collectionRemove } from 'diagram-js/lib/util/Collections'; import { findDataObjects, - findDataReferenceShapes, + findDataObjectReferences, idToHumanReadableName, } from './DataObjectHelpers'; const HIGH_PRIORITY = 1500; + /** * This Command Interceptor functions like the BpmnUpdator in BPMN.js - It hooks into events * from Diagram.js and updates the underlying BPMN model accordingly. @@ -20,9 +21,40 @@ const HIGH_PRIORITY = 1500; * 4) Don't allow someone to move a DataObjectReference from one process to another process. */ export default class DataObjectInterceptor extends CommandInterceptor { - constructor(eventBus, bpmnFactory, commandStack) { + + constructor(eventBus, bpmnFactory, commandStack, bpmnUpdater) { super(eventBus); + /* The default behavior is to move the data object into whatever object the reference is being created in. + * If a data object already has a parent, don't change it. + */ + bpmnUpdater.updateSemanticParent = (businessObject, parentBusinessObject) => { + // Special case for participant - which is a valid place to drop a data object, but it needs to be added + // to the particpant's Process (which isn't directly accessible in BPMN.io + let realParent = parentBusinessObject; + if (is(realParent, 'bpmn:Participant')) { + realParent = realParent.processRef; + } + + if (is(businessObject, 'bpmn:DataObjectReference')) { + // For data object references, always update the flowElements when a parent is provided + // The parent could be null if it's being deleted, and I could probably handle that here instead of + // when the shape is deleted, but not interested in refactoring at the moment. + if (realParent != null) { + const flowElements = realParent.get('flowElements'); + flowElements.push(businessObject); + } + } else if (is(businessObject, 'bpmn:DataObject')) { + // For data objects, only update the flowElements for new data objects, and set the parent so it doesn't get moved. + if (typeof(businessObject.$parent) === 'undefined') { + const flowElements = realParent.get('flowElements'); + flowElements.push(businessObject); + businessObject.$parent = realParent; + } + } else + bpmnUpdater.__proto__.updateSemanticParent.call(this, businessObject, parentBusinessObject); + }; + /** * For DataObjectReferences only ... * Prevent this from calling the CreateDataObjectBehavior in BPMN-js, as it will @@ -52,9 +84,9 @@ export default class DataObjectInterceptor extends CommandInterceptor { } else { dataObject = bpmnFactory.create('bpmn:DataObject'); } - // set the reference to the DataObject shape.businessObject.dataObjectRef = dataObject; + shape.businessObject.$parent = process; } }); @@ -84,38 +116,19 @@ export default class DataObjectInterceptor extends CommandInterceptor { */ this.executed(['shape.delete'], HIGH_PRIORITY, function (event) { const { context } = event; - const { shape, oldParent } = context; + const { shape } = context; if (is(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') { - const references = findDataReferenceShapes( - oldParent, - shape.businessObject.dataObjectRef.id - ); + const dataObject = shape.businessObject.dataObjectRef; + let flowElements = shape.businessObject.$parent.get('flowElements'); + collectionRemove(flowElements, shape.businessObject); + let references = findDataObjectReferences(flowElements, dataObject.id); if (references.length === 0) { - return; // Use the default bahavior and delete the data object. + let flowElements = dataObject.$parent.get('flowElements'); + collectionRemove(flowElements, dataObject); } - // Remove the business Object - let containment = ''; - const { businessObject } = shape; - if (is(businessObject, 'bpmn:DataOutputAssociation')) { - containment = 'dataOutputAssociations'; - } - if (is(businessObject, 'bpmn:DataInputAssociation')) { - containment = 'dataInputAssociations'; - } - const children = businessObject.$parent.get(containment); - collectionRemove(children, businessObject); - - // Remove the visible element. - const di = getDi(shape); - const planeElements = di.$parent.get('planeElement'); - collectionRemove(planeElements, di); - di.$parent = null; - - // Stop the propogation. - event.stopPropagation(); } }); } } -DataObjectInterceptor.$inject = ['eventBus', 'bpmnFactory', 'commandStack']; +DataObjectInterceptor.$inject = ['eventBus', 'bpmnFactory', 'commandStack', 'bpmnUpdater']; diff --git a/app/spiffworkflow/DataObject/propertiesPanel/DataObjectArray.js b/app/spiffworkflow/DataObject/propertiesPanel/DataObjectArray.js index 49b9487..70e5f41 100644 --- a/app/spiffworkflow/DataObject/propertiesPanel/DataObjectArray.js +++ b/app/spiffworkflow/DataObject/propertiesPanel/DataObjectArray.js @@ -5,7 +5,11 @@ import { } from '@bpmn-io/properties-panel'; import { without } from 'min-dash'; import { is } from 'bpmn-js/lib/util/ModelUtil'; -import {findDataObjects, findDataReferenceShapes, idToHumanReadableName} from '../DataObjectHelpers'; +import { + findDataObjects, + findDataObjectReferenceShapes, + idToHumanReadableName, +} from '../DataObjectHelpers'; /** * Provides a list of data objects, and allows you to add / remove data objects, and change their ids. @@ -20,7 +24,7 @@ export function DataObjectArray(props) { let process; // This element might be a process, or something that will reference a process. - if (is(element.businessObject, 'bpmn:Process')) { + if (is(element.businessObject, 'bpmn:Process') || is(element.businessObject, 'bpmn:SubProcess')) { process = element.businessObject; } else if (element.businessObject.processRef) { process = element.businessObject.processRef; @@ -53,6 +57,7 @@ export function DataObjectArray(props) { const newDataObject = moddle.create('bpmn:DataObject'); const newElements = process.get('flowElements'); newDataObject.id = moddle.ids.nextPrefixed('DataObject_'); + newDataObject.$parent = process; newElements.push(newDataObject); commandStack.execute('element.updateModdleProperties', { element, @@ -79,7 +84,7 @@ function removeFactory(props) { }, }); // When a data object is removed, remove all references as well. - const references = findDataReferenceShapes(element, dataObject.id); + const references = findDataObjectReferenceShapes(element.children, dataObject.id); for (const ref of references) { commandStack.execute('shape.delete', { shape: ref }); } @@ -116,7 +121,7 @@ function DataObjectTextField(props) { }); // Also update the label of all the references - const references = findDataReferenceShapes(element, dataObject.id); + const references = findDataObjectReferenceShapes(element.children, dataObject.id); for (const ref of references) { commandStack.execute('element.updateProperties', { element: ref, diff --git a/app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider.js b/app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider.js index f6be933..6af1426 100644 --- a/app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider.js +++ b/app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider.js @@ -20,7 +20,8 @@ export default function DataObjectPropertiesProvider( ); } if ( - isAny(element, ['bpmn:Process', 'bpmn:SubProcess', 'bpmn:Participant']) + isAny(element, ['bpmn:Process', 'bpmn:Participant']) || + (is(element, 'bpmn:SubProcess') && !element.collapsed) ) { groups.push( createDataObjectEditor( diff --git a/test/spec/DataObjectInterceptorSpec.js b/test/spec/DataObjectInterceptorSpec.js index 1fa8324..c109f62 100644 --- a/test/spec/DataObjectInterceptorSpec.js +++ b/test/spec/DataObjectInterceptorSpec.js @@ -4,7 +4,11 @@ import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js import { inject, } from 'bpmn-js/test/helper'; -import {findDataObjects, idToHumanReadableName} from '../../app/spiffworkflow/DataObject/DataObjectHelpers'; +import { + findDataObjects, + findDataObjectReferenceShapes, + idToHumanReadableName, +} from '../../app/spiffworkflow/DataObject/DataObjectHelpers'; describe('DataObject Interceptor', function() { @@ -113,4 +117,52 @@ describe('DataObject Interceptor', function() { expect(dataObjects.length).to.equal(1); })); + it('Data objects in a process should be visible in a subprocess', inject(function(canvas, modeling, elementRegistry) { + + let subProcessShape = elementRegistry.get('my_subprocess'); + let subProcess = subProcessShape.businessObject; + let dataObjects = findDataObjects(subProcess); + expect(dataObjects.length).to.equal(0); + + let rootShape = canvas.getRootElement(); + const dataObjectRefShape = modeling.createShape({ type: 'bpmn:DataObjectReference' }, + { x: 220, y: 220 }, rootShape); + + dataObjects = findDataObjects(subProcess); + expect(dataObjects.length).to.equal(1); + })); + + it('Data objects in a subprocess should not be visible in a process', inject(function(canvas, modeling, elementRegistry) { + + let subProcessShape = elementRegistry.get('my_subprocess'); + let subProcess = subProcessShape.businessObject; + const dataObjectRefShape = modeling.createShape({ type: 'bpmn:DataObjectReference' }, + { x: 220, y: 220 }, subProcessShape); + + let dataObjects = findDataObjects(subProcess); + expect(dataObjects.length).to.equal(1); + + let rootShape = canvas.getRootElement(); + dataObjects = findDataObjects(rootShape); + expect(dataObjects.length).to.equal(0); + })); + + it('References inside subprocesses should be visible in a process', inject(function(canvas, modeling, elementRegistry) { + + let rootShape = canvas.getRootElement(); + const refOne = modeling.createShape({ type: 'bpmn:DataObjectReference' }, + { x: 220, y: 220 }, rootShape); + + let subProcessShape = elementRegistry.get('my_subprocess'); + let subProcess = subProcessShape.businessObject; + const refTwo = modeling.createShape({ type: 'bpmn:DataObjectReference' }, + { x: 320, y: 220 }, subProcessShape); + + let dataObjects = findDataObjects(subProcess); + expect(dataObjects.length).to.equal(1); + let references = findDataObjectReferenceShapes(rootShape.children, dataObjects[0].id); + expect(references.length).to.equal(2); + + })); + });