From e504af9bb041c40bb8ecd0de3a42e0c109aa91c6 Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Thu, 6 Apr 2023 10:44:06 -0400 Subject: [PATCH] add multi instance configuration panel --- app/spiffworkflow/index.js | 6 + .../loops/propertiesPanel/LoopProperty.js | 29 +++ .../MultiInstancePropertiesProvider.js | 227 ++++++++++++++++++ .../StandardLoopPropertiesProvider.js | 133 ++++++++++ 4 files changed, 395 insertions(+) create mode 100644 app/spiffworkflow/loops/propertiesPanel/LoopProperty.js create mode 100644 app/spiffworkflow/loops/propertiesPanel/MultiInstancePropertiesProvider.js create mode 100644 app/spiffworkflow/loops/propertiesPanel/StandardLoopPropertiesProvider.js diff --git a/app/spiffworkflow/index.js b/app/spiffworkflow/index.js index 5327773..829cb75 100644 --- a/app/spiffworkflow/index.js +++ b/app/spiffworkflow/index.js @@ -10,6 +10,8 @@ import ConditionsPropertiesProvider from './conditions/propertiesPanel/Condition import ExtensionsPropertiesProvider from './extensions/propertiesPanel/ExtensionsPropertiesProvider'; import MessagesPropertiesProvider from './messages/propertiesPanel/MessagesPropertiesProvider'; import CallActivityPropertiesProvider from './callActivity/propertiesPanel/CallActivityPropertiesProvider'; +import StandardLoopPropertiesProvider from './loops/propertiesPanel/StandardLoopPropertiesProvider'; +import MultiInstancePropertiesProvider from './loops/propertiesPanel/MultiInstancePropertiesProvider'; export default { __depends__: [RulesModule], @@ -25,6 +27,8 @@ export default { 'ioRules', 'ioInterceptor', 'dataObjectRenderer', + 'multiInstancePropertiesProvider', + 'standardLoopPropertiesProvider', ], dataObjectInterceptor: ['type', DataObjectInterceptor], dataObjectRules: ['type', DataObjectRules], @@ -37,4 +41,6 @@ export default { ioPalette: ['type', IoPalette], ioRules: ['type', IoRules], ioInterceptor: ['type', IoInterceptor], + multiInstancePropertiesProvider: ['type', MultiInstancePropertiesProvider], + standardLoopPropertiesProvider: ['type', StandardLoopPropertiesProvider], }; diff --git a/app/spiffworkflow/loops/propertiesPanel/LoopProperty.js b/app/spiffworkflow/loops/propertiesPanel/LoopProperty.js new file mode 100644 index 0000000..5a8ac39 --- /dev/null +++ b/app/spiffworkflow/loops/propertiesPanel/LoopProperty.js @@ -0,0 +1,29 @@ +export function getLoopProperty(element, propertyName) { + + const loopCharacteristics = element.businessObject.loopCharacteristics; + const prop = loopCharacteristics.get(propertyName); + + let value = ''; + if (typeof(prop) !== 'object') { + value = prop; + } else if (typeof(prop) !== 'undefined') { + if (prop.$type === 'bpmn:FormalExpression') + value = prop.get('body'); + else + value = prop.get('id'); + } + return value; +} + +export function setLoopProperty(element, propertyName, value, commandStack) { + const loopCharacteristics = element.businessObject.loopCharacteristics; + if (typeof(value) === 'object') + value.$parent = loopCharacteristics; + commandStack.execute('element.updateModdleProperties', { + element, + moddleElement: loopCharacteristics, + properties: { + [propertyName]: value + } + }); +} diff --git a/app/spiffworkflow/loops/propertiesPanel/MultiInstancePropertiesProvider.js b/app/spiffworkflow/loops/propertiesPanel/MultiInstancePropertiesProvider.js new file mode 100644 index 0000000..f0f4ce9 --- /dev/null +++ b/app/spiffworkflow/loops/propertiesPanel/MultiInstancePropertiesProvider.js @@ -0,0 +1,227 @@ +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { useService } from 'bpmn-js-properties-panel'; +import { TextFieldEntry, isTextFieldEntryEdited } from '@bpmn-io/properties-panel'; +import { getLoopProperty, setLoopProperty } from './LoopProperty'; + +const LOW_PRIORITY = 500; + +export default function MultiInstancePropertiesProvider(propertiesPanel) { + this.getGroups = function getGroupsCallback(element) { + return function pushGroup(groups) { + if (is(element, 'bpmn:Task')) { + let group = groups.filter(g => g.id == 'multiInstance'); + if (group.length == 1) + updateMultiInstanceGroup(element, group[0]); + } + return groups; + }; + }; + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +MultiInstancePropertiesProvider.$inject = ['propertiesPanel']; + +function updateMultiInstanceGroup(element, group) { + group.entries = MultiInstanceProps({element}); + group.shouldOpen = true; +} + +function MultiInstanceProps(props) { + const { element } = props; + + const entries = [{ + id: 'loopCardinality', + component: LoopCardinality, + isEdited: isTextFieldEntryEdited + }, { + id: 'loopDataInputRef', + component: InputCollection, + isEdited: isTextFieldEntryEdited + }, { + id: 'dataInputItem', + component: InputItem, + isEdited: isTextFieldEntryEdited + }, { + id: 'loopDataOutputRef', + component: OutputCollection, + isEdited: isTextFieldEntryEdited + }, { + id: 'dataOutputItem', + component: OutputItem, + isEdited: isTextFieldEntryEdited + }, { + id: 'completionCondition', + component: CompletionCondition, + isEdited: isTextFieldEntryEdited + }]; + return entries; +} + +function LoopCardinality(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopCardinality'); + }; + + const setValue = value => { + const loopCardinality = bpmnFactory.create('bpmn:FormalExpression', {body: value}) + setLoopProperty(element, 'loopCardinality', loopCardinality, commandStack); + let inputCollection = getLoopProperty(element, 'loopDataInputRef'); + if (typeof(value) !== 'undefined' && typeof(inputCollection) !== 'undefined') + setLoopProperty(element, 'loopDataInputRef', undefined, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopCardinality', + label: translate('Loop Cardinality'), + getValue, + setValue, + debounce, + description: 'Explicitly set the number of instances' + }); +} + +function InputCollection(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopDataInputRef'); + }; + + const setValue = value => { + const collection = bpmnFactory.create('bpmn:ItemAwareElement', {id: value}); + setLoopProperty(element, 'loopDataInputRef', collection, commandStack); + let cardinality = getLoopProperty(element, 'loopCardinality'); + if (typeof(value) !== 'undefined' && typeof(cardinality) !== 'undefined') + setLoopProperty(element, 'loopCardinality', undefined, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopDataInputRef', + label: translate('Input Collection'), + getValue, + setValue, + debounce, + description: 'Create an instance for each item in this collection' + }); +} + +function InputItem(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'inputDataItem'); + }; + + const setValue = value => { + const item = bpmnFactory.create('bpmn:DataInput', {id: value, name: value}); + setLoopProperty(element, 'inputDataItem', item, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'inputDataItem', + label: translate('Input Element'), + getValue, + setValue, + debounce, + description: 'Each item in the collection will be copied to this variable' + }); +} + +function OutputCollection(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopDataOutputRef'); + }; + + const setValue = value => { + const collection = bpmnFactory.create('bpmn:ItemAwareElement', {id: value}); + setLoopProperty(element, 'loopDataOutputRef', collection, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopDataOutputRef', + label: translate('Output Collection'), + getValue, + setValue, + debounce, + description: 'Create or update this collection with the instance results' + }); +} + +function OutputItem(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'outputDataItem'); + }; + + const setValue = value => { + const item = bpmnFactory.create('bpmn:DataOutput', {id: value, name: value}); + setLoopProperty(element, 'outputDataItem', item, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'outputDataItem', + label: translate('Output Element'), + getValue, + setValue, + debounce, + description: 'The value of this variable will be added to the output collection' + }); +} + +function CompletionCondition(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'completionCondition'); + }; + + const setValue = value => { + const completionCondition = bpmnFactory.create('bpmn:FormalExpression', {body: value}) + setLoopProperty(element, 'completionCondition', completionCondition, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'completionCondition', + label: translate('Completion Condition'), + getValue, + setValue, + debounce, + description: 'Stop executing this task when this condition is met' + }); +} + diff --git a/app/spiffworkflow/loops/propertiesPanel/StandardLoopPropertiesProvider.js b/app/spiffworkflow/loops/propertiesPanel/StandardLoopPropertiesProvider.js new file mode 100644 index 0000000..b18c0e8 --- /dev/null +++ b/app/spiffworkflow/loops/propertiesPanel/StandardLoopPropertiesProvider.js @@ -0,0 +1,133 @@ +import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { useService } from 'bpmn-js-properties-panel'; +import { + Group, + TextFieldEntry, + isTextFieldEntryEdited, + CheckboxEntry, + isCheckboxEntryEdited, +} from '@bpmn-io/properties-panel'; + +import { getLoopProperty, setLoopProperty } from './LoopProperty'; + +const LOW_PRIORITY = 500; + +export default function StandardLoopPropertiesProvider(propertiesPanel) { + this.getGroups = function getGroupsCallback(element) { + return function pushGroup(groups) { + if ( + is(element, 'bpmn:Task') && + typeof(element.businessObject.loopCharacteristics) !== 'undefined' && + element.businessObject.loopCharacteristics.$type === 'bpmn:StandardLoopCharacteristics' + ) { + const group = { + id: 'standardLoopCharacteristics', + component: Group, + label: 'Standard Loop', + entries: StandardLoopProps(element), + shouldOpen: true, + }; + if (groups.length < 3) + groups.push(group); + else + groups.splice(2, 0, group); + } + return groups; + }; + }; + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +StandardLoopPropertiesProvider.$inject = ['propertiesPanel']; + +function StandardLoopProps(props) { + const { element } = props; + return [{ + id: 'loopMaximum', + component: LoopMaximum, + isEdited: isTextFieldEntryEdited + }, { + id: 'loopCondition', + component: LoopCondition, + isEdited: isTextFieldEntryEdited + }, { + id: 'testBefore', + component: TestBefore, + isEdited: isCheckboxEntryEdited + }]; +} + +function LoopMaximum(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopMaximum'); + }; + + const setValue = value => { + setLoopProperty(element, 'loopMaximum', value, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopMaximum', + label: translate('Loop Maximum'), + getValue, + setValue, + debounce + }); +} + +function TestBefore(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'testBefore'); + }; + + const setValue = value => { + setLoopProperty(element, 'testBefore', value, commandStack); + }; + + return CheckboxEntry({ + element, + id: 'testBefore', + label: translate('Test Before'), + getValue, + setValue, + }); +} + +function LoopCondition(props) { + const { element } = props; + const debounce = useService('debounceInput'); + const translate = useService('translate'); + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + + const getValue = () => { + return getLoopProperty(element, 'loopCondition'); + }; + + const setValue = value => { + const loopCondition = bpmnFactory.create('bpmn:FormalExpression', {body: value}) + setLoopProperty(element, 'loopCondition', loopCondition, commandStack); + }; + + return TextFieldEntry({ + element, + id: 'loopCondition', + label: translate('Loop Condition'), + getValue, + setValue, + debounce + }); +}