From 72dc8324784d462fe708e7561d83a99a0675796d Mon Sep 17 00:00:00 2001 From: Jon Herron Date: Mon, 23 Oct 2023 12:51:34 -0400 Subject: [PATCH] Copy and rename files --- .../DataStores/DataStoreHelpers.js | 70 ++++++++ .../DataStores/DataStoreInterceptor.js | 143 +++++++++++++++++ .../DataStores/DataStoreRenderer.js | 44 ++++++ .../DataStores/DataStoreRules.js | 39 +++++ app/spiffworkflow/DataStores/index.js | 20 +++ .../propertiesPanel/DataReferenceGroup.js | 30 ++++ .../propertiesPanel/DataStoreArray.js | 149 ++++++++++++++++++ .../DataStorePropertiesProvider.js | 100 ++++++++++++ .../propertiesPanel/DataStoreSelect.js | 77 +++++++++ 9 files changed, 672 insertions(+) create mode 100644 app/spiffworkflow/DataStores/DataStoreHelpers.js create mode 100644 app/spiffworkflow/DataStores/DataStoreInterceptor.js create mode 100644 app/spiffworkflow/DataStores/DataStoreRenderer.js create mode 100644 app/spiffworkflow/DataStores/DataStoreRules.js create mode 100644 app/spiffworkflow/DataStores/index.js create mode 100644 app/spiffworkflow/DataStores/propertiesPanel/DataReferenceGroup.js create mode 100644 app/spiffworkflow/DataStores/propertiesPanel/DataStoreArray.js create mode 100644 app/spiffworkflow/DataStores/propertiesPanel/DataStorePropertiesProvider.js create mode 100644 app/spiffworkflow/DataStores/propertiesPanel/DataStoreSelect.js diff --git a/app/spiffworkflow/DataStores/DataStoreHelpers.js b/app/spiffworkflow/DataStores/DataStoreHelpers.js new file mode 100644 index 0000000..dd34378 --- /dev/null +++ b/app/spiffworkflow/DataStores/DataStoreHelpers.js @@ -0,0 +1,70 @@ +/** + * Returns the moddelElement if it is a process, otherwise, returns the + * + * @param container + */ + +export function findDataObjects(parent, dataObjects) { + if (typeof(dataObjects) === 'undefined') + dataObjects = []; + let process; + if (!parent) { + return []; + } + if (parent.processRef) { + process = parent.processRef; + } else { + process = parent; + if (process.$type === 'bpmn:SubProcess') + findDataObjects(process.$parent, dataObjects); + } + if (typeof(process.flowElements) !== 'undefined') { + for (const element of process.flowElements) { + if (element.$type === 'bpmn:DataObject') + dataObjects.push(element); + } + } + return dataObjects; +} + +export function findDataObject(process, id) { + for (const dataObj of findDataObjects(process)) { + if (dataObj.id === id) { + return dataObj; + } + } +} + +export function findDataObjectReferences(children, dataObjectId) { + if (children == null) { + return []; + } + 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) { + const words = id.match(/[A-Za-z][a-z]*|[0-9]+/g) || [id]; + return words.map(capitalize).join(' '); + + function capitalize(word) { + return word.charAt(0).toUpperCase() + word.substring(1); + } +} diff --git a/app/spiffworkflow/DataStores/DataStoreInterceptor.js b/app/spiffworkflow/DataStores/DataStoreInterceptor.js new file mode 100644 index 0000000..a8eb3f7 --- /dev/null +++ b/app/spiffworkflow/DataStores/DataStoreInterceptor.js @@ -0,0 +1,143 @@ +import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; +import { getDi, is } from 'bpmn-js/lib/util/ModelUtil'; +import { remove as collectionRemove } from 'diagram-js/lib/util/Collections'; +import { + findDataObjects, + 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. + * + * This handles some special cases we want to handle for DataObjects and DataObjectReferences, + * for instance: + * 1) Use existing data objects if possible when creating a new reference (don't create new objects each time) + * 2) Don't automatically delete a data object when you delete the reference - unless all references are removed. + * 3) Update the name of the DataObjectReference to match the id of the DataObject. + * 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, 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'); + const existingElement = flowElements.find(i => i.id === 1); + if (!existingElement) { + 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(bpmnUpdater, businessObject, parentBusinessObject); + } + }; + + /** + * For DataObjectReferences only ... + * Prevent this from calling the CreateDataObjectBehavior in BPMN-js, as it will + * attempt to crete a dataObject immediately. We can't create the dataObject until + * we know where it is placed - as we want to reuse data objects of the parent when + * possible */ + this.preExecute(['shape.create'], HIGH_PRIORITY, function (event) { + const { context } = event; + const { shape } = context; + if (is(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') { + event.stopPropagation(); + } + }); + + /** + * Don't just create a new data object, use the first existing one if it already exists + */ + this.executed(['shape.create'], HIGH_PRIORITY, function (event) { + const { context } = event; + const { shape } = context; + if (is(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') { + const process = shape.parent.businessObject; + const existingDataObjects = findDataObjects(process); + let dataObject; + if (existingDataObjects.length > 0) { + dataObject = existingDataObjects[0]; + } else { + dataObject = bpmnFactory.create('bpmn:DataObject'); + } + // set the reference to the DataObject + shape.businessObject.dataObjectRef = dataObject; + shape.businessObject.$parent = process; + } + }); + + /** + * In order for the label to display correctly, we need to update it in POST step. + */ + this.postExecuted(['shape.create'], HIGH_PRIORITY, function (event) { + const { context } = event; + const { shape } = context; + // set the reference to the DataObject + // Update the name of the reference to match the data object's id. + if (is(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') { + commandStack.execute('element.updateProperties', { + element: shape, + moddleElement: shape.businessObject, + properties: { + name: idToHumanReadableName(shape.businessObject.dataObjectRef.id), + }, + }); + } + }); + + /** + * Don't remove the associated DataObject, unless all references to that data object + * Difficult to do given placement of this logic in the BPMN Updater, so we have + * to manually handle the removal. + */ + this.executed(['shape.delete'], HIGH_PRIORITY, function (event) { + const { context } = event; + const { shape } = context; + if (is(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') { + const dataObject = shape.businessObject.dataObjectRef; + let parent = shape.businessObject.$parent; + if (parent.processRef) { + // Our immediate parent may be a pool, so we need to get the process + parent = parent.processRef; + } + const flowElements = parent.get('flowElements'); + collectionRemove(flowElements, shape.businessObject); + const references = findDataObjectReferences(flowElements, dataObject.id); + if (references.length === 0) { + const dataFlowElements = dataObject.$parent.get('flowElements'); + collectionRemove(dataFlowElements, dataObject); + } + } + }); + } +} + +DataObjectInterceptor.$inject = ['eventBus', 'bpmnFactory', 'commandStack', 'bpmnUpdater']; diff --git a/app/spiffworkflow/DataStores/DataStoreRenderer.js b/app/spiffworkflow/DataStores/DataStoreRenderer.js new file mode 100644 index 0000000..d66fc7a --- /dev/null +++ b/app/spiffworkflow/DataStores/DataStoreRenderer.js @@ -0,0 +1,44 @@ +import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; + +import { + attr as svgAttr +} from 'tiny-svg'; + +import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil'; +import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; +import { findDataObject } from './DataObjectHelpers'; + +const HIGH_PRIORITY = 1500; + +/** + * Work in progress -- render data object references in red if they are + * not valid. + */ +export default class DataObjectRenderer extends BaseRenderer { + constructor(eventBus, bpmnRenderer) { + super(eventBus, HIGH_PRIORITY); + this.bpmnRenderer = bpmnRenderer; + } + + canRender(element) { + return isAny(element, [ 'bpmn:DataObjectReference' ]) && !element.labelTarget; + } + + drawShape(parentNode, element) { + const shape = this.bpmnRenderer.drawShape(parentNode, element); + if (is(element, 'bpmn:DataObjectReference')) { + let businessObject = getBusinessObject(element); + let dataObject = businessObject.dataObjectRef; + if (dataObject && dataObject.id) { + let parentObject = businessObject.$parent; + dataObject = findDataObject(parentObject, dataObject.id); + } + if (!dataObject) { + svgAttr(shape, 'stroke', 'red'); + } + return shape; + } + } +} + +DataObjectRenderer.$inject = [ 'eventBus', 'bpmnRenderer' ]; diff --git a/app/spiffworkflow/DataStores/DataStoreRules.js b/app/spiffworkflow/DataStores/DataStoreRules.js new file mode 100644 index 0000000..146962b --- /dev/null +++ b/app/spiffworkflow/DataStores/DataStoreRules.js @@ -0,0 +1,39 @@ +/** + * Custom Rules for the DataObject - Rules allow you to prevent an + * action from happening in the diagram, such as dropping an element + * where it doesn't belong. + * + * Here we don't allow people to move a data object Reference + * from one parent to another, as we can't move the data objects + * from one parent to another. + * + */ +import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; +import inherits from 'inherits-browser'; +import { is } from 'bpmn-js/lib/util/ModelUtil'; + +export default function DataObjectRules(eventBus) { + RuleProvider.call(this, eventBus); +} +inherits(DataObjectRules, RuleProvider); +const HIGH_PRIORITY = 1500; + +DataObjectRules.prototype.init = function() { + this.addRule('elements.move', HIGH_PRIORITY,function(context) { + let elements = context.shapes; + let target = context.target; + return canDrop(elements, target); + }); +}; + +function canDrop(elements, target) { + for (let element of elements) { + if (is(element, 'bpmn:DataObjectReference') && element.parent && target) { + return target === element.parent; + } + // Intentionally returning null here to allow other rules to fire. + } +} + +DataObjectRules.prototype.canDrop = canDrop; +DataObjectRules.$inject = [ 'eventBus' ]; diff --git a/app/spiffworkflow/DataStores/index.js b/app/spiffworkflow/DataStores/index.js new file mode 100644 index 0000000..8489aec --- /dev/null +++ b/app/spiffworkflow/DataStores/index.js @@ -0,0 +1,20 @@ +import DataObjectInterceptor from './DataObjectInterceptor'; +import DataObjectRules from './DataObjectRules'; +import RulesModule from 'diagram-js/lib/features/rules'; +import DataObjectRenderer from './DataObjectRenderer'; +import DataObjectPropertiesProvider from './propertiesPanel/DataObjectPropertiesProvider'; + + +export default { + __depends__: [ + RulesModule + ], + __init__: [ 'dataInterceptor', 'dataObjectRules', 'dataObjectRenderer', 'dataObjectPropertiesProvider' ], + dataInterceptor: [ 'type', DataObjectInterceptor ], + dataObjectRules: [ 'type', DataObjectRules ], + dataObjectRenderer: [ 'type', DataObjectRenderer ], + dataObjectPropertiesProvider: [ 'type', DataObjectPropertiesProvider ] +}; + + + diff --git a/app/spiffworkflow/DataStores/propertiesPanel/DataReferenceGroup.js b/app/spiffworkflow/DataStores/propertiesPanel/DataReferenceGroup.js new file mode 100644 index 0000000..8f36c5c --- /dev/null +++ b/app/spiffworkflow/DataStores/propertiesPanel/DataReferenceGroup.js @@ -0,0 +1,30 @@ +import { ListGroup } from '@bpmn-io/properties-panel'; +import { DataObjectArray } from './DataObjectArray'; + +/** + * Also allows you to select which Data Objects are available + * in the process element. + * @param element The selected process + * @param moddle For updating the underlying xml object + * @returns {[{component: (function(*)), isEdited: *, id: string, element},{component: + * (function(*)), isEdited: *, id: string, element}]} + */ +export default function(element, moddle) { + + const groupSections = []; + const dataObjectArray = { + id: 'editDataObjects', + element, + label: 'Available Data Objects', + component: ListGroup, + ...DataObjectArray({ element, moddle }) + }; + + if (dataObjectArray.items) { + groupSections.push(dataObjectArray); + } + + return groupSections; +} + + diff --git a/app/spiffworkflow/DataStores/propertiesPanel/DataStoreArray.js b/app/spiffworkflow/DataStores/propertiesPanel/DataStoreArray.js new file mode 100644 index 0000000..70e5f41 --- /dev/null +++ b/app/spiffworkflow/DataStores/propertiesPanel/DataStoreArray.js @@ -0,0 +1,149 @@ +import { useService } from 'bpmn-js-properties-panel'; +import { + isTextFieldEntryEdited, + TextFieldEntry, +} from '@bpmn-io/properties-panel'; +import { without } from 'min-dash'; +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { + findDataObjects, + findDataObjectReferenceShapes, + idToHumanReadableName, +} from '../DataObjectHelpers'; + +/** + * Provides a list of data objects, and allows you to add / remove data objects, and change their ids. + * @param props + * @constructor + */ +export function DataObjectArray(props) { + const { moddle } = props; + const { element } = props; + const { commandStack } = props; + const { elementRegistry } = props; + let process; + + // This element might be a process, or something that will reference a process. + if (is(element.businessObject, 'bpmn:Process') || is(element.businessObject, 'bpmn:SubProcess')) { + process = element.businessObject; + } else if (element.businessObject.processRef) { + process = element.businessObject.processRef; + } + + const dataObjects = findDataObjects(process); + const items = dataObjects.map((dataObject, index) => { + const id = `${process.id}-dataObj-${index}`; + return { + id, + label: dataObject.id, + entries: DataObjectGroup({ + idPrefix: id, + element, + dataObject, + }), + autoFocusEntry: `${id}-dataObject`, + remove: removeFactory({ + element, + dataObject, + process, + commandStack, + elementRegistry, + }), + }; + }); + + function add(event) { + event.stopPropagation(); + 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, + moddleElement: process, + properties: { + flowElements: newElements, + }, + }); + } + + return { items, add }; +} + +function removeFactory(props) { + const { element, dataObject, process, commandStack } = props; + + return function (event) { + event.stopPropagation(); + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: process, + properties: { + flowElements: without(process.get('flowElements'), dataObject), + }, + }); + // When a data object is removed, remove all references as well. + const references = findDataObjectReferenceShapes(element.children, dataObject.id); + for (const ref of references) { + commandStack.execute('shape.delete', { shape: ref }); + } + }; +} + +function DataObjectGroup(props) { + const { idPrefix, dataObject } = props; + + return [ + { + id: `${idPrefix}-dataObject`, + component: DataObjectTextField, + isEdited: isTextFieldEntryEdited, + idPrefix, + dataObject, + }, + ]; +} + +function DataObjectTextField(props) { + const { idPrefix, element, parameter, dataObject } = props; + + const commandStack = useService('commandStack'); + const debounce = useService('debounceInput'); + + const setValue = (value) => { + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: dataObject, + properties: { + id: value, + }, + }); + + // Also update the label of all the references + const references = findDataObjectReferenceShapes(element.children, dataObject.id); + for (const ref of references) { + commandStack.execute('element.updateProperties', { + element: ref, + moddleElement: ref.businessObject, + properties: { + name: idToHumanReadableName(value), + }, + changed: [ref], // everything is already marked as changed, don't recalculate. + }); + } + }; + + const getValue = () => { + return dataObject.id; + }; + + return TextFieldEntry({ + element: parameter, + id: `${idPrefix}-id`, + label: 'Data Object Id', + getValue, + setValue, + debounce, + }); +} diff --git a/app/spiffworkflow/DataStores/propertiesPanel/DataStorePropertiesProvider.js b/app/spiffworkflow/DataStores/propertiesPanel/DataStorePropertiesProvider.js new file mode 100644 index 0000000..6af1426 --- /dev/null +++ b/app/spiffworkflow/DataStores/propertiesPanel/DataStorePropertiesProvider.js @@ -0,0 +1,100 @@ +import { is, isAny } from 'bpmn-js/lib/util/ModelUtil'; +import { ListGroup, isTextFieldEntryEdited } from '@bpmn-io/properties-panel'; +import { DataObjectSelect } from './DataObjectSelect'; +import { DataObjectArray } from './DataObjectArray'; + +const LOW_PRIORITY = 500; + +export default function DataObjectPropertiesProvider( + propertiesPanel, + translate, + moddle, + commandStack, + elementRegistry +) { + this.getGroups = function (element) { + return function (groups) { + if (is(element, 'bpmn:DataObjectReference')) { + groups.push( + createDataObjectSelector(element, translate, moddle, commandStack) + ); + } + if ( + isAny(element, ['bpmn:Process', 'bpmn:Participant']) || + (is(element, 'bpmn:SubProcess') && !element.collapsed) + ) { + groups.push( + createDataObjectEditor( + element, + translate, + moddle, + commandStack, + elementRegistry + ) + ); + } + return groups; + }; + }; + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +DataObjectPropertiesProvider.$inject = [ + 'propertiesPanel', + 'translate', + 'moddle', + 'commandStack', + 'elementRegistry', +]; + +/** + * Create a group on the main panel with a select box (for choosing the Data Object to connect) + * @param element + * @param translate + * @param moddle + * @returns entries + */ +function createDataObjectSelector(element, translate, moddle, commandStack) { + return { + id: 'data_object_properties', + label: translate('Data Object Properties'), + entries: [ + { + id: 'selectDataObject', + element, + component: DataObjectSelect, + isEdited: isTextFieldEntryEdited, + moddle, + commandStack, + }, + ], + }; +} + +/** + * Create a group on the main panel with a select box (for choosing the Data Object to connect) AND a + * full Data Object Array for modifying all the data objects. + * @param element + * @param translate + * @param moddle + * @returns entries + */ +function createDataObjectEditor( + element, + translate, + moddle, + commandStack, + elementRegistry +) { + const dataObjectArray = { + id: 'editDataObjects', + element, + label: 'Data Objects', + component: ListGroup, + ...DataObjectArray({ element, moddle, commandStack, elementRegistry }), + }; + + if (dataObjectArray.items) { + return dataObjectArray; + } +} diff --git a/app/spiffworkflow/DataStores/propertiesPanel/DataStoreSelect.js b/app/spiffworkflow/DataStores/propertiesPanel/DataStoreSelect.js new file mode 100644 index 0000000..2804355 --- /dev/null +++ b/app/spiffworkflow/DataStores/propertiesPanel/DataStoreSelect.js @@ -0,0 +1,77 @@ +import {useService } from 'bpmn-js-properties-panel'; +import { SelectEntry } from '@bpmn-io/properties-panel'; +import {findDataObjects, idToHumanReadableName} from '../DataObjectHelpers'; + +/** + * Finds the value of the given type within the extensionElements + * given a type of "spiff:preScript", would find it in this, and return + * the object. + * + * + + + me = "100% awesome" + + + ... + + * + * @returns {string|null|*} + */ +export function DataObjectSelect(props) { + const element = props.element; + const commandStack = props.commandStack; + const debounce = useService('debounceInput'); + + + const getValue = () => { + return element.businessObject.dataObjectRef.id + } + + const setValue = value => { + const businessObject = element.businessObject; + const dataObjects = findDataObjects(businessObject.$parent) + for (const flowElem of dataObjects) { + if (flowElem.$type === 'bpmn:DataObject' && flowElem.id === value) { + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: businessObject, + properties: { + dataObjectRef: flowElem + } + }); + commandStack.execute('element.updateProperties', { + element, + moddleElement: businessObject, + properties: { + 'name': idToHumanReadableName(flowElem.id) + } + }); + } + } + } + + const getOptions = value => { + const businessObject = element.businessObject; + const parent = businessObject.$parent; + let dataObjects = findDataObjects(parent); + let options = []; + dataObjects.forEach(dataObj => { + options.push({label: dataObj.id, value: dataObj.id}) + }); + return options; + } + + return ; + +}