Merge commit '7db152f01049e0b788ccbfd3ce2358987df67cdc'

This commit is contained in:
burnettk 2023-08-09 16:14:32 -04:00
commit b9fd9279e0
No known key found for this signature in database
29 changed files with 4190 additions and 28 deletions

View File

@ -15,7 +15,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.4.0
uses: dependabot/fetch-metadata@v1.6.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs

View File

@ -20,7 +20,7 @@ jobs:
steps:
- uses: actions/checkout@v3 #Checkout Repo
- uses: actions/setup-node@v3 #Setup Node
- uses: nanasess/setup-chromedriver@v1
- uses: nanasess/setup-chromedriver@v2 #Setup ChromeDriver
with:
node-version: '18'
- name: Run Karma Tests

View File

@ -20,6 +20,7 @@ let bpmnModeler;
try {
bpmnModeler = new BpmnModeler({
container: modelerEl,
keyboard: { bindTo: document },
propertiesPanel: {
parent: panelEl,
},
@ -41,9 +42,6 @@ try {
throw error;
}
// import XML
bpmnModeler.importXML(diagramXML).then(() => {});
/**
* It is possible to populate certain components using API calls to
* a backend. Here we mock out the API call, but this gives you
@ -191,6 +189,23 @@ bpmnModeler.on('spiff.callactivity.search', (event) => {
});
});
/* This restores unresolved references that camunda removes */
bpmnModeler.on('import.parse.complete', event => {
const refs = event.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.importXML(diagramXML).then(() => {});
// 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

View File

@ -12,6 +12,7 @@ export default function setupFileOperations(bpmnModeler) {
* 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);
});
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) {
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);
bpmnModeler.importXML(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);
}

View File

@ -36,6 +36,9 @@ export function findDataObject(process, id) {
}
export function findDataObjectReferences(children, dataObjectId) {
if (children == null) {
return [];
}
return children.flatMap((child) => {
if (child.$type == 'bpmn:DataObjectReference' && child.dataObjectRef.id == dataObjectId)
return [child];
@ -58,7 +61,7 @@ export function findDataObjectReferenceShapes(children, dataObjectId) {
}
export function idToHumanReadableName(id) {
const words = id.match(/[A-Za-z][a-z]*/g) || [id];
const words = id.match(/[A-Za-z][a-z]*|[0-9]+/g) || [id];
return words.map(capitalize).join(' ');
function capitalize(word) {

View File

@ -42,7 +42,10 @@ export default class DataObjectInterceptor extends CommandInterceptor {
// when the shape is deleted, but not interested in refactoring at the moment.
if (realParent != null) {
const flowElements = realParent.get('flowElements');
flowElements.push(businessObject);
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.
@ -120,12 +123,17 @@ export default class DataObjectInterceptor extends CommandInterceptor {
const { shape } = context;
if (is(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') {
const dataObject = shape.businessObject.dataObjectRef;
let flowElements = shape.businessObject.$parent.get('flowElements');
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);
let references = findDataObjectReferences(flowElements, dataObject.id);
const references = findDataObjectReferences(flowElements, dataObject.id);
if (references.length === 0) {
let flowElements = dataObject.$parent.get('flowElements');
collectionRemove(flowElements, dataObject);
const dataFlowElements = dataObject.$parent.get('flowElements');
collectionRemove(dataFlowElements, dataObject);
}
}
});

View File

@ -31,7 +31,8 @@ export function DataObjectSelect(props) {
const setValue = value => {
const businessObject = element.businessObject;
for (const flowElem of businessObject.$parent.flowElements) {
const dataObjects = findDataObjects(businessObject.$parent)
for (const flowElem of dataObjects) {
if (flowElem.$type === 'bpmn:DataObject' && flowElem.id === value) {
commandStack.execute('element.updateModdleProperties', {
element,

View File

@ -0,0 +1,6 @@
import ErrorPropertiesProvider from './propertiesPanel/ErrorPropertiesProvider';
export default {
__init__: ['errorPropertiesProvider'],
errorPropertiesProvider: ['type', ErrorPropertiesProvider],
}

View File

@ -0,0 +1,49 @@
import { is } from 'bpmn-js/lib/util/ModelUtil';
import { getRoot } from '../../helpers';
import { getArrayForType, getListGroupForType } from '../../eventList.js';
import { hasEventType,
replaceGroup,
getSelectorForType,
getConfigureGroupForType
} from '../../eventSelect.js';
const LOW_PRIORITY = 500;
const eventDetails = {
'eventType': 'bpmn:Error',
'eventDefType': 'bpmn:ErrorEventDefinition',
'referenceType': 'errorRef',
'idPrefix': 'error',
};
export default function ErrorPropertiesProvider(
propertiesPanel,
translate,
moddle,
commandStack,
) {
this.getGroups = function (element) {
return function (groups) {
if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) {
const getErrorArray = getArrayForType('bpmn:Error', 'errorRef', 'Error');
const errorGroup = getListGroupForType('errors', 'Errors', getErrorArray);
groups.push(errorGroup({ element, translate, moddle, commandStack }));
} else if (hasEventType(element, 'bpmn:ErrorEventDefinition')) {
const getErrorSelector = getSelectorForType(eventDetails);
const errorGroup = getConfigureGroupForType(eventDetails, 'Error', true, getErrorSelector);
const group = errorGroup({ element, translate, moddle, commandStack });
replaceGroup('error', groups, group);
}
return groups;
};
};
propertiesPanel.registerProvider(LOW_PRIORITY, this);
}
ErrorPropertiesProvider.$inject = [
'propertiesPanel',
'translate',
'moddle',
'commandStack',
];

View File

@ -0,0 +1,6 @@
import EscalationPropertiesProvider from './propertiesPanel/EscalationPropertiesProvider';
export default {
__init__: ['escalationPropertiesProvider'],
escalationrrorPropertiesProvider: ['type', EscalationPropertiesProvider],
}

View File

@ -0,0 +1,49 @@
import { is } from 'bpmn-js/lib/util/ModelUtil';
import { getRoot } from '../../helpers';
import { getArrayForType, getListGroupForType } from '../../eventList.js';
import { hasEventType,
replaceGroup,
getSelectorForType,
getConfigureGroupForType
} from '../../eventSelect.js';
const LOW_PRIORITY = 500;
const eventDetails = {
'eventType': 'bpmn:Escalation',
'eventDefType': 'bpmn:EscalationEventDefinition',
'referenceType': 'escalationRef',
'idPrefix': 'escalation',
};
export default function EscalationPropertiesProvider(
propertiesPanel,
translate,
moddle,
commandStack,
) {
this.getGroups = function (element) {
return function (groups) {
if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) {
const getEscalationArray = getArrayForType('bpmn:Escalation', 'escalationRef', 'Escalation');
const escalationGroup = getListGroupForType('escalations', 'Escalations', getEscalationArray);
groups.push(escalationGroup({ element, translate, moddle, commandStack }));
} else if (hasEventType(element, 'bpmn:EscalationEventDefinition')) {
const getEscalationSelector = getSelectorForType(eventDetails);
const escalationGroup = getConfigureGroupForType(eventDetails, 'Escalation', true, getEscalationSelector);
const group = escalationGroup({ element, translate, moddle, commandStack });
replaceGroup('escalation', groups, group);
}
return groups;
};
};
propertiesPanel.registerProvider(LOW_PRIORITY, this);
}
EscalationPropertiesProvider.$inject = [
'propertiesPanel',
'translate',
'moddle',
'commandStack',
];

View File

@ -0,0 +1,163 @@
import { is } from 'bpmn-js/lib/util/ModelUtil';
import { useService } from 'bpmn-js-properties-panel';
import {
ListGroup,
TextFieldEntry,
isTextFieldEntryEdited
} from '@bpmn-io/properties-panel';
import { getRoot } from './helpers';
/* This function creates a list of a particular event type at the process level using the item list
* and add function provided by `getArray`.
*
* Usage:
* const getArray = getArrayForType('bpmn:Signal', 'signalRef', 'Signal');
* const signalGroup = createGroupForType('signals', 'Signals', getArray);
*/
function getListGroupForType(groupId, label, getArray) {
return function (props) {
const { element, translate, moddle, commandStack } = props;
const eventArray = {
id: groupId,
element,
label: label,
component: ListGroup,
...getArray({ element, moddle, commandStack, translate }),
};
if (eventArray.items) {
return eventArray;
}
}
}
function getArrayForType(itemType, referenceType, prefix) {
return function (props) {
const { element, moddle, commandStack, translate } = props;
const root = getRoot(element.businessObject);
const matching = root.rootElements ? root.rootElements.filter(elem => elem.$type === itemType) : [];
function removeModelReferences(flowElements, match) {
flowElements.map(elem => {
if (elem.eventDefinitions)
elem.eventDefinitions = elem.eventDefinitions.filter(def => def.get(referenceType) != match);
else if (elem.flowElements)
removeModelReferences(elem.flowElements, match);
});
}
function removeElementReferences(children, match) {
children.map(child => {
if (child.businessObject.eventDefinitions) {
const bo = child.businessObject;
bo.eventDefinitions = bo.eventDefinitions.filter(def => def.get(referenceType) != match);
commandStack.execute('element.updateProperties', {
element: child,
moddleElement: bo,
properties: {}
});
}
if (child.children)
removeElementReferences(child.children, match);
});
}
function removeFactory(item) {
return function (event) {
event.stopPropagation();
if (root.rootElements) {
root.rootElements = root.rootElements.filter(elem => elem != item);
// This updates visible elements
removeElementReferences(element.children, item);
// This handles everything else (eg collapsed subprocesses) but does not update the shapes
// I can't figure out how to do that
root.rootElements.filter(elem => elem.$type === 'bpmn:Process').map(
process => removeModelReferences(process.flowElements, item)
);
commandStack.execute('element.updateProperties', {
element,
properties: {},
});
}
}
}
const items = matching.map((item, idx) => {
const itemId = `${prefix}-${idx}`;
return {
id: itemId,
label: item.name,
entries: getItemEditor({
itemId,
element,
item,
commandStack,
translate,
}),
autoFocusEntry: itemId,
remove: removeFactory(item),
};
});
function add(event) {
event.stopPropagation();
const item = moddle.create(itemType);
item.id = moddle.ids.nextPrefixed(`${prefix}_`);
item.name = item.id;
if (root.rootElements)
root.rootElements.push(item);
commandStack.execute('element.updateProperties', {
element,
properties: {},
});
};
return { items, add };
}
}
function getItemEditor(props) {
const { itemId, element, item, commandStack, translate } = props;
return [
{
id: `${itemId}-name`,
component: ItemTextField,
item,
commandStack,
translate,
},
];
}
function ItemTextField(props) {
const { itemId, element, item, commandStack, translate } = props;
const debounce = useService('debounceInput');
const setValue = (value) => {
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: item,
properties: {
id: value,
name: value,
},
});
};
const getValue = () => { return item.id; }
return TextFieldEntry({
element,
id: `${itemId}-id-textField`,
label: translate('ID'),
getValue,
setValue,
debounce,
});
}
export { getArrayForType, getListGroupForType };

View File

@ -0,0 +1,251 @@
import { is, isAny } from 'bpmn-js/lib/util/ModelUtil';
import { useService } from 'bpmn-js-properties-panel';
import {
ListGroup,
TextFieldEntry,
TextAreaEntry,
SelectEntry,
isTextFieldEntryEdited
} from '@bpmn-io/properties-panel';
import { getRoot } from './helpers';
function hasEventType(element, eventType) {
const events = element.businessObject.eventDefinitions;
return events && events.filter(item => is(item, eventType)).length > 0;
}
function replaceGroup(groupId, groups, group) {
const idx = groups.map(g => g.id).indexOf(groupId);
if (idx > -1)
groups.splice(idx, 1, group);
else
groups.push(group);
group.shouldOpen = true;
}
function isCatchingEvent(element) {
return isAny(element, ['bpmn:StartEvent', 'bpmn:IntermediateCatchEvent', 'bpmn:BoundaryEvent']);
}
function isThrowingEvent(element) {
return isAny(element, ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent']);
}
function getConfigureGroupForType(eventDetails, label, includeCode, getSelect) {
const { eventType, eventDefType, referenceType, idPrefix } = eventDetails;
return function (props) {
const { element, translate, moddle, commandStack } = props;
const variableName = getTextFieldForExtension(eventDetails, 'Variable Name', 'The name of the variable to store the payload in', true);
const payloadDefinition = getTextFieldForExtension(eventDetails, 'Payload', 'The expression to create the payload with', false);
const entries = [
{
id: `${idPrefix}-select`,
element,
component: getSelect,
isEdited: isTextFieldEntryEdited,
moddle,
commandStack,
},
];
if (includeCode) {
const codeField = getCodeTextField(eventDetails, `${label} Code`);
entries.push({
id: `${idPrefix}-code`,
element,
component: codeField,
isEdited: isTextFieldEntryEdited,
moddle,
commandStack,
});
}
if (isCatchingEvent(element)) {
entries.push({
id: `${idPrefix}-variable`,
element,
component: variableName,
isEdited: isTextFieldEntryEdited,
moddle,
commandStack,
});
} else if (isThrowingEvent(element)) {
entries.push({
id: `${idPrefix}-payload`,
element,
component: payloadDefinition,
isEdited: isTextFieldEntryEdited,
moddle,
commandStack,
});
};
return {
id: `${idPrefix}-group`,
label: label,
entries,
}
}
}
function getSelectorForType(eventDetails) {
const { eventType, eventDefType, referenceType, idPrefix } = eventDetails;
return function (props) {
const { element, translate, moddle, commandStack } = props;
const debounce = useService('debounceInput');
const root = getRoot(element.businessObject);
const getValue = () => {
const eventDef = element.businessObject.eventDefinitions.find(v => v.$type == eventDefType);
return (eventDef && eventDef.get(referenceType)) ? eventDef.get(referenceType).id : '';
};
const setValue = (value) => {
const bpmnEvent = root.rootElements.find(e => e.id == value);
// not sure how to handle multiple event definitions
const eventDef = element.businessObject.eventDefinitions.find(v => v.$type == eventDefType);
// really not sure what to do here if one of these can't be found either
if (bpmnEvent && eventDef)
eventDef.set(referenceType, bpmnEvent);
commandStack.execute('element.updateProperties', {
element,
moddleElement: element.businessObject,
properties: {},
});
};
const getOptions = (val) => {
const matching = root.rootElements ? root.rootElements.filter(elem => elem.$type === eventType) : [];
const options = [];
matching.map(option => options.push({label: option.name, value: option.id}));
return options;
}
return SelectEntry({
id: `${idPrefix}-select`,
element,
description: 'Select item',
getValue,
setValue,
getOptions,
debounce,
});
}
}
function getTextFieldForExtension(eventDetails, label, description, catching) {
const { eventType, eventDefType, referenceType, idPrefix } = eventDetails;
return function (props) {
const { element, moddle, commandStack } = props;
const debounce = useService('debounceInput');
const translate = useService('translate');
const root = getRoot(element.businessObject);
const extensionName = (catching) ? 'spiffworkflow:variableName' : 'spiffworkflow:payloadExpression';
const getEvent = () => {
const eventDef = element.businessObject.eventDefinitions.find(v => v.$type == eventDefType);
const bpmnEvent = eventDef.get(referenceType);
return bpmnEvent;
};
const getValue = () => {
// I've put the variable name (and payload) on the event for consistency with messages.
// However, when I think about this, I wonder if it shouldn't be on the event definition.
// I think that's something we should address in the future.
// Creating a payload and defining access to it are both process-specific, and that's an argument for leaving
// it in the event definition
const bpmnEvent = getEvent();
if (bpmnEvent && bpmnEvent.extensionElements) {
const extension = bpmnEvent.extensionElements.get('values').find(ext => ext.$instanceOf(extensionName));
return (extension) ? extension.value : null;
}
}
const setValue = (value) => {
const bpmnEvent = getEvent();
if (bpmnEvent) {
if (!bpmnEvent.extensionElements)
bpmnEvent.extensionElements = moddle.create('bpmn:ExtensionElements');
const extensions = bpmnEvent.extensionElements.get('values');
const extension = extensions.find(ext => ext.$instanceOf(extensionName));
if (!extension) {
const newExt = moddle.create(extensionName);
newExt.value = value;
extensions.push(newExt);
} else
extension.value = value;
} // not sure what to do if the event hasn't been set
};
if (catching) {
return TextFieldEntry({
element,
id: `${idPrefix}-variable-name`,
description: description,
label: translate(label),
getValue,
setValue,
debounce,
});
} else {
return TextAreaEntry({
element,
id: `${idPrefix}-payload-expression`,
description: description,
label: translate(label),
getValue,
setValue,
debounce,
});
}
}
}
function getCodeTextField(eventDetails, label) {
const { eventType, eventDefType, referenceType, idPrefix } = eventDetails;
return function (props) {
const { element, moddle, commandStack } = props;
const translate = useService('translate');
const debounce = useService('debounceInput');
const attrName = `${idPrefix}Code`;
const getEvent = () => {
const eventDef = element.businessObject.eventDefinitions.find(v => v.$type == eventDefType);
const bpmnEvent = eventDef.get(referenceType);
return bpmnEvent;
};
const getValue = () => {
const bpmnEvent = getEvent();
return (bpmnEvent) ? bpmnEvent.get(attrName) : null;
};
const setValue = (value) => {
const bpmnEvent = getEvent();
if (bpmnEvent)
bpmnEvent.set(attrName, value);
};
return TextFieldEntry({
element,
id: `${idPrefix}-code-value`,
label: translate(label),
getValue,
setValue,
debounce,
});
}
}
export { hasEventType, getSelectorForType, getConfigureGroupForType, replaceGroup };

View File

@ -28,7 +28,6 @@ export function SpiffExtensionLaunchButton(props) {
const { commandStack, moddle } = props;
// Listen for a response, to update the script.
eventBus.once(listenEvent, (response) => {
console.log("Calling Update!")
setExtensionValue(element, name, response.value, moddle, commandStack);
});
}

View File

@ -40,7 +40,6 @@ export function SpiffExtensionSelect(props) {
};
const setValue = (value) => {
console.log(`Set Value called with ${ value}`);
setExtensionValue(element, name, value, moddle, commandStack);
};

View File

@ -12,3 +12,27 @@ export function removeExtensionElementsIfEmpty(moddleElement) {
moddleElement.extensionElements = null;
}
}
/**
* loops up until it can find the root.
* @param element
*/
export function getRoot(businessObject, moddle) {
// HACK: get the root element. need a more formal way to do this
if (moddle) {
for (const elementId in moddle.ids._seed.hats) {
if (elementId.startsWith('Definitions_')) {
return moddle.ids._seed.hats[elementId];
}
}
} else {
// todo: Do we want businessObject to be a shape or moddle object?
if (businessObject.$type === 'bpmn:Definitions') {
return businessObject;
}
if (typeof businessObject.$parent !== 'undefined') {
return getRoot(businessObject.$parent);
}
}
return businessObject;
}

View File

@ -9,7 +9,12 @@ import DataObjectPropertiesProvider from './DataObject/propertiesPanel/DataObjec
import ConditionsPropertiesProvider from './conditions/propertiesPanel/ConditionsPropertiesProvider';
import ExtensionsPropertiesProvider from './extensions/propertiesPanel/ExtensionsPropertiesProvider';
import MessagesPropertiesProvider from './messages/propertiesPanel/MessagesPropertiesProvider';
import SignalPropertiesProvider from './signals/propertiesPanel/SignalPropertiesProvider';
import ErrorPropertiesProvider from './errors/propertiesPanel/ErrorPropertiesProvider';
import EscalationPropertiesProvider from './escalations/propertiesPanel/EscalationPropertiesProvider';
import CallActivityPropertiesProvider from './callActivity/propertiesPanel/CallActivityPropertiesProvider';
import StandardLoopPropertiesProvider from './loops/propertiesPanel/StandardLoopPropertiesProvider';
import MultiInstancePropertiesProvider from './loops/propertiesPanel/MultiInstancePropertiesProvider';
export default {
__depends__: [RulesModule],
@ -20,11 +25,16 @@ export default {
'conditionsPropertiesProvider',
'extensionsPropertiesProvider',
'messagesPropertiesProvider',
'signalPropertiesProvider',
'errorPropertiesProvider',
'escalationPropertiesProvider',
'callActivityPropertiesProvider',
'ioPalette',
'ioRules',
'ioInterceptor',
'dataObjectRenderer',
'multiInstancePropertiesProvider',
'standardLoopPropertiesProvider',
],
dataObjectInterceptor: ['type', DataObjectInterceptor],
dataObjectRules: ['type', DataObjectRules],
@ -32,9 +42,14 @@ export default {
dataObjectPropertiesProvider: ['type', DataObjectPropertiesProvider],
conditionsPropertiesProvider: ['type', ConditionsPropertiesProvider],
extensionsPropertiesProvider: ['type', ExtensionsPropertiesProvider],
signalPropertiesProvider: ['type', SignalPropertiesProvider],
errorPropertiesProvider: ['type', ErrorPropertiesProvider],
escalationPropertiesProvider: ['type', EscalationPropertiesProvider],
messagesPropertiesProvider: ['type', MessagesPropertiesProvider],
callActivityPropertiesProvider: ['type', CallActivityPropertiesProvider],
ioPalette: ['type', IoPalette],
ioRules: ['type', IoRules],
ioInterceptor: ['type', IoInterceptor],
multiInstancePropertiesProvider: ['type', MultiInstancePropertiesProvider],
standardLoopPropertiesProvider: ['type', StandardLoopPropertiesProvider],
};

View File

@ -0,0 +1,30 @@
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;
let properties = { [propertyName]: value };
if (propertyName === 'loopCardinality') properties['loopDataInputRef'] = undefined;
if (propertyName === 'loopDataInputRef') properties['loopCardinality'] = undefined;
commandStack.execute('element.updateModdleProperties', {
element,
moddleElement: loopCharacteristics,
properties: properties,
});
}

View File

@ -0,0 +1,221 @@
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') || is(element, 'bpmn:SubProcess')) {
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);
};
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);
};
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 = (typeof(value) !== 'undefined') ? bpmnFactory.create('bpmn:DataInput', {id: value, name: value}) : undefined;
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 = (typeof(value) !== 'undefined') ? bpmnFactory.create('bpmn:DataOutput', {id: value, name: value}) : undefined;
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'
});
}

View File

@ -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') || is(element, 'bpmn:SubProcess')) &&
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
});
}

View File

@ -148,14 +148,24 @@ function getRetrievalExpressionFromCorrelationProperty(
export function findCorrelationProperties(businessObject, moddle) {
const root = getRoot(businessObject, moddle);
const correlationProperties = [];
for (const rootElement of root.rootElements) {
if (rootElement.$type === 'bpmn:CorrelationProperty') {
correlationProperties.push(rootElement);
if (isIterable(root.rootElements)) {
for (const rootElement of root.rootElements) {
if (rootElement.$type === 'bpmn:CorrelationProperty') {
correlationProperties.push(rootElement);
}
}
}
return correlationProperties;
}
function isIterable(obj) {
// checks for null and undefined
if (obj == null) {
return false;
}
return typeof obj[Symbol.iterator] === 'function';
}
export function findCorrelationKeys(businessObject, moddle) {
const root = getRoot(businessObject, moddle);
const correlationKeys = [];

View File

@ -35,6 +35,9 @@ export function MessageSelect(props) {
commandStack.execute('element.updateModdleProperties', {
element: shapeElement,
moddleElement: businessObject,
properties: {
messageRef: message,
},
});
} else if (
businessObject.$type === 'bpmn:ReceiveTask' ||

View File

@ -219,6 +219,28 @@
"type": "string"
}
]
},
{
"name": "payloadExpression",
"superClass": [ "Element" ],
"properties": [
{
"name": "value",
"isBody": true,
"type": "String"
}
]
},
{
"name": "variableName",
"superClass": [ "Element" ],
"properties": [
{
"name": "value",
"isBody": true,
"type": "String"
}
]
}
]
}

View File

@ -0,0 +1,6 @@
import SignalPropertiesProvider from './propertiesPanel/SignalPropertiesProvider';
export default {
__init__: ['signalPropertiesProvider'],
signalPropertiesProvider: ['type', SignalPropertiesProvider],
}

View File

@ -0,0 +1,49 @@
import { is } from 'bpmn-js/lib/util/ModelUtil';
import { getRoot } from '../../helpers';
import { getArrayForType, getListGroupForType } from '../../eventList.js';
import { hasEventType,
replaceGroup,
getSelectorForType,
getConfigureGroupForType
} from '../../eventSelect.js';
const LOW_PRIORITY = 500;
const eventDetails = {
'eventType': 'bpmn:Signal',
'eventDefType': 'bpmn:SignalEventDefinition',
'referenceType': 'signalRef',
'idPrefix': 'signal',
};
export default function SignalPropertiesProvider(
propertiesPanel,
translate,
moddle,
commandStack,
) {
this.getGroups = function (element) {
return function (groups) {
if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) {
const getSignalArray = getArrayForType('bpmn:Signal', 'signalRef', 'Signal');
const signalGroup = getListGroupForType('signals', 'Signals', getSignalArray);
groups.push(signalGroup({ element, translate, moddle, commandStack }));
} else if (hasEventType(element, 'bpmn:SignalEventDefinition')) {
const getSignalSelector = getSelectorForType(eventDetails);
const signalGroup = getConfigureGroupForType(eventDetails, 'Signal', false, getSignalSelector);
const group = signalGroup({ element, translate, moddle, commandStack });
replaceGroup('signal', groups, group);
}
return groups;
};
};
propertiesPanel.registerProvider(LOW_PRIORITY, this);
}
SignalPropertiesProvider.$inject = [
'propertiesPanel',
'translate',
'moddle',
'commandStack',
];

View File

@ -0,0 +1,66 @@
import {
BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule,
} from 'bpmn-js-properties-panel';
import TestContainer from 'mocha-test-container-support';
import {
bootstrapPropertiesPanel,
changeInput,
expectSelected,
findEntry,
findSelect,
} from './helpers';
import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json';
import DataObject from '../../app/spiffworkflow/DataObject';
describe('Properties Panel for Data Objects', function () {
const xml = require('./bpmn/data_objects_in_pools.bpmn').default;
let container;
beforeEach(function () {
container = TestContainer.get(this);
});
beforeEach(
bootstrapPropertiesPanel(xml, {
container,
debounceInput: false,
additionalModules: [
DataObject,
BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule,
],
moddleExtensions: {
spiffworkflow: spiffModdleExtension,
},
})
);
it('should allow you to select other data objects within the same participant', async function () {
// IF - a data object reference is selected
const doREF = await expectSelected('pool1Do1_REF');
expect(doREF).to.exist;
// THEN - a select Data Object section should appear in the properties panel
const entry = findEntry('selectDataObject', container);
const selector = findSelect(entry);
changeInput(selector, 'pool1Do2');
// then this data reference object now references that data object.
const { businessObject } = doREF;
expect(businessObject.get('dataObjectRef').id).to.equal('pool1Do2');
});
it('should NOT allow you to select data objects within other participants', async function () {
// IF - a data object reference is selected
const doREF = await expectSelected('pool1Do1_REF');
expect(doREF).to.exist;
// THEN - a select Data Object section should appear in the properties panel but pool2Do1 should not be an option
const entry = findEntry('selectDataObject', container);
const selector = findSelect(entry);
expect(selector.length).to.equal(2);
expect(selector[0].value === 'pool1Do2');
expect(selector[1].value === 'pool1Do1');
});
});

View File

@ -6,9 +6,6 @@ import {
import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel';
import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json';
import TestContainer from 'mocha-test-container-support';
import DataObjectPropertiesProvider
from '../../app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider';
import spiffworkflow from '../../app/spiffworkflow';
import DataObject from '../../app/spiffworkflow/DataObject';
describe('Properties Panel for Data Objects', function() {
@ -78,4 +75,17 @@ describe('Properties Panel for Data Objects', function() {
expect(my_data_ref_1.businessObject.name).to.equal('My Nifty New Name');
});
it('renaming a data object creates a lable without losing the numbers', async function() {
// IF - a process is selected, and the name of a data object is changed.
let entry = findEntry('ProcessTest-dataObj-2-id', container);
let textInput = findInput('text', entry);
changeInput(textInput, 'MyObject1');
let my_data_ref_1 = await expectSelected('my_data_ref_1');
// THEN - both the data object itself, and the label of any references are updated.
expect(my_data_ref_1.businessObject.dataObjectRef.id).to.equal('MyObject1');
expect(my_data_ref_1.businessObject.name).to.equal('My Object 1');
});
});

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_19o7vxg" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.17.0">
<bpmn:collaboration id="Collaboration_0jmlu5k">
<bpmn:participant id="Participant_11j98s8" processRef="ProcessTest" />
<bpmn:participant id="Participant_1d4d2vr" processRef="Process_1y9rx5q" />
</bpmn:collaboration>
<bpmn:process id="ProcessTest" name="Process Test" isExecutable="true">
<bpmn:dataObjectReference id="pool1Do2_REF" name="Pool 1 Do 2" dataObjectRef="pool1Do2" />
<bpmn:dataObject id="pool1Do2" />
<bpmn:dataObject id="pool1Do1" />
<bpmn:dataObjectReference id="pool1Do2_REF" name="Pool 1 Do 2" dataObjectRef="pool1Do2" />
<bpmn:dataObjectReference id="pool1Do1_REF" name="Pool 1 Do 1" dataObjectRef="pool1Do1" />
</bpmn:process>
<bpmn:process id="Process_1y9rx5q">
<bpmn:dataObject id="Pool2do" />
<bpmn:dataObjectReference id="pool2Do2" name="Pool 2 Do" dataObjectRef="Pool2do" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0jmlu5k">
<bpmndi:BPMNShape id="Participant_11j98s8_di" bpmnElement="Participant_11j98s8" isHorizontal="true">
<dc:Bounds x="290" y="270" width="300" height="140" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="pool1Do2_REF_di" bpmnElement="pool1Do2_REF">
<dc:Bounds x="462" y="295" width="36" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="453" y="352" width="58" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="pool1Do1_REF_di" bpmnElement="pool1Do1_REF">
<dc:Bounds x="372" y="295" width="36" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="365" y="352" width="58" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Participant_1d4d2vr_di" bpmnElement="Participant_1d4d2vr" isHorizontal="true">
<dc:Bounds x="290" y="430" width="390" height="130" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="DataObjectReference_18xfjvh_di" bpmnElement="DataObjectReference_18xfjvh">
<dc:Bounds x="372" y="465" width="36" height="50" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

File diff suppressed because it is too large Load Diff