diff --git a/app/app.js b/app/app.js index 2bdc7af..e9308dd 100644 --- a/app/app.js +++ b/app/app.js @@ -41,8 +41,33 @@ try { throw error; } +/* The default importer drops the loop data inputs and outputs on multi instance tasks. + * They don't reference anything in our diagrams (which is a problem we should tackle, but not essential right now). + * This is probably a terrible solution, but given that the modifications happen every time a diagram is parsed, + * ia a locally defined function inside the parser, I don't see any other way of dealing with it than to just + * intercept the parsed results and patch them up again. + */ + +function importWithUnresolvedRefs(bpmnModeler, xml) { + bpmnModeler._moddle.fromXML(xml).then((result) => { + const refs = result.references.filter(r => r.property === 'bpmn:loopDataInputRef' || r.property === 'bpmn:loopDataOutputRef'); + const desc = bpmnModeler._moddle.registry.getEffectiveDescriptor('bpmn:ItemAwareElement'); + refs.forEach(ref => { + const props = { + id: ref.id, + name: ref.id ? typeof(ref.name) === 'undefined': ref.name, + }; + let elem = bpmnModeler._moddle.create(desc, props); + elem.$parent = ref.element; + ref.element.set(ref.property, elem); + }); + bpmnModeler.importDefinitions(result.rootElement); + bpmnModeler.open(); + }); +}; + // import XML -bpmnModeler.importXML(diagramXML).then(() => {}); +importWithUnresolvedRefs(bpmnModeler, diagramXML); /** * It is possible to populate certain components using API calls to @@ -195,4 +220,4 @@ bpmnModeler.on('spiff.callactivity.search', (event) => { // This handles the download and upload buttons - it isn't specific to // the BPMN modeler or these extensions, just a quick way to allow you to // create and save files, so keeping it outside the example. -setupFileOperations(bpmnModeler); +setupFileOperations(bpmnModeler, importWithUnresolvedRefs); diff --git a/app/fileOperations.js b/app/fileOperations.js index 170a75c..df0ef89 100644 --- a/app/fileOperations.js +++ b/app/fileOperations.js @@ -7,11 +7,12 @@ import FileSaver from 'file-saver'; * easily from the editor for testing purposes. * ----------------------------------------- */ -export default function setupFileOperations(bpmnModeler) { +export default function setupFileOperations(bpmnModeler, importWithUnresolvedRefs) { /** * Just a quick bit of code so we can save the XML that is output. * Helps for debugging against other libraries (like SpiffWorkflow) */ + const btn = document.getElementById('downloadButton'); btn.addEventListener('click', (_event) => { saveXML(); @@ -29,12 +30,8 @@ export default function setupFileOperations(bpmnModeler) { */ const uploadBtn = document.getElementById('uploadButton'); uploadBtn.addEventListener('click', (_event) => { - openFile(displayFile); + openFile(bpmnModeler, importWithUnresolvedRefs); }); - - function displayFile(contents) { - bpmnModeler.importXML(contents).then(() => {}); - } } function clickElem(elem) { @@ -59,7 +56,7 @@ function clickElem(elem) { elem.dispatchEvent(eventMouse); } -export function openFile(func) { +export function openFile(bpmnModeler, importWithUnresolvedRefs) { const readFile = function readFileCallback(e) { const file = e.target.files[0]; if (!file) { @@ -68,7 +65,7 @@ export function openFile(func) { const reader = new FileReader(); reader.onload = function onloadCallback(onloadEvent) { const contents = onloadEvent.target.result; - fileInput.func(contents); + importWithUnresolvedRefs(bpmnModeler, contents); document.body.removeChild(fileInput); }; reader.readAsText(file); @@ -77,7 +74,6 @@ export function openFile(func) { fileInput.type = 'file'; fileInput.style.display = 'none'; fileInput.onchange = readFile; - fileInput.func = func; document.body.appendChild(fileInput); clickElem(fileInput); } 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..9c9be94 --- /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') || is(element, 'bpmn:CallActivity')) { + 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..68581bc --- /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') || is(element, 'bpmn:CallActivity')) && + 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 + }); +}