In and Out Variables for Tasks (#84)

* INIT

* Input Group

* Output group and CSS Alignments

* Add unit tests

* fix typing issue
This commit is contained in:
Ayoub Ait Lachgar 2024-06-26 15:42:20 +01:00 committed by GitHub
parent c39627b959
commit 020de78f82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 629 additions and 4 deletions

View File

@ -74,9 +74,22 @@ adjust CSS props so that padding won't add to dimensions.
.bio-properties-panel-group-entries > .bio-properties-panel-description {
padding-inline: 15px;
padding-block: 8px;
padding-block: 5px;
}
.bio-properties-panel-group-entries.open > .bio-properties-panel-group {
margin-inline: 15px;
border: 1px solid #ccc;
border-radius: 8px;
/* box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: border-color 0.3s, box-shadow 0.3s; */
margin-bottom: 5px;
}
/* .bio-properties-panel-group + .bio-properties-panel-group {
margin-top: 10px;
} */
/* Darker background on mouse-over */
.bpmn-js-spiffworkflow-btn:hover {
background-color: RoyalBlue;

View File

@ -0,0 +1,64 @@
export function createSpecification(bpmnFactory, businessObject, type, newElement) {
let ioSpecification = businessObject.ioSpecification;
if (!ioSpecification) {
ioSpecification = bpmnFactory.create('bpmn:InputOutputSpecification', {
dataInputs: [],
dataOutputs: [],
inputSets: [],
outputSets: [],
});
businessObject.ioSpecification = ioSpecification;
}
if (type === 'input') {
ioSpecification.dataInputs.push(newElement);
if (!ioSpecification.inputSets.length) {
ioSpecification.inputSets.push(bpmnFactory.create('bpmn:InputSet', { dataInputRefs: [newElement] }));
} else {
ioSpecification.inputSets[0].dataInputRefs.push(newElement);
}
} else if (type === 'output') {
ioSpecification.dataOutputs.push(newElement);
if (!ioSpecification.outputSets.length) {
ioSpecification.outputSets.push(bpmnFactory.create('bpmn:OutputSet', { dataOutputRefs: [newElement] }));
} else {
ioSpecification.outputSets[0].dataOutputRefs.push(newElement);
}
}
return ioSpecification;
}
export function removeElementFromSpecification(element, entry, type) {
const ioSpecification = element.businessObject.ioSpecification;
if (!ioSpecification) {
console.error('No ioSpecification found for this element.');
return;
}
const collection = type === 'input' ? ioSpecification.dataInputs : ioSpecification.dataOutputs;
const setCollection = type === 'input' ? ioSpecification.inputSets : ioSpecification.outputSets;
const index = collection.findIndex(item => item.id === entry.id);
if (index > -1) {
const [removedElement] = collection.splice(index, 1);
setCollection.forEach(set => {
const refIndex = set[type === 'input' ? 'dataInputRefs' : 'dataOutputRefs'].indexOf(removedElement);
if (refIndex > -1) {
set[type === 'input' ? 'dataInputRefs' : 'dataOutputRefs'].splice(refIndex, 1);
}
});
} else {
console.error(`No ${type === 'input' ? 'DataInput' : 'DataOutput'} found for id ${entry.id}`);
}
}
export function updateElementProperties(commandStack, element) {
commandStack.execute('element.updateProperties', {
element: element,
moddleElement: element.businessObject,
properties: {}
});
}

View File

@ -1,11 +1,13 @@
import IoPalette from './IoPalette';
import IoRules from './IoRules';
import IoInterceptor from './IoInterceptor';
import IoPropertiesProvider from './propertiesProvider/IoPropertiesProvider';
export default {
__init__: [ 'IoPalette', 'IoRules', 'IoInterceptor' ],
__init__: [ 'IoPalette', 'IoRules', 'IoInterceptor', 'IoPropertiesProvider' ],
IoPalette: [ 'type', IoPalette ],
IoRules: [ 'type', IoRules ],
IoInterceptor: [ 'type', IoInterceptor ]
IoInterceptor: [ 'type', IoInterceptor ],
IoPropertiesProvider: [ 'type', IoPropertiesProvider ]
};

View File

@ -0,0 +1,134 @@
import { useService } from 'bpmn-js-properties-panel';
import {
isTextFieldEntryEdited,
TextFieldEntry,
} from '@bpmn-io/properties-panel';
import { createSpecification, removeElementFromSpecification, updateElementProperties } from '../helpers';
export function InputParametersArray(props) {
const { element, moddle, translate, commandStack, bpmnFactory } = props;
const { businessObject } = element;
const ioSpecification = businessObject.ioSpecification;
const inputsEntries = (ioSpecification) ? ioSpecification.dataInputs : [];
const items = (inputsEntries) ? inputsEntries.map((inputEntry, index) => {
const id = `inputEntry-${index}`;
return {
id,
label: translate(inputEntry.name),
entries: InputParamGroup({
element,
commandStack,
moddle,
translate,
bpmnFactory,
inputEntry
}),
autoFocusEntry: `input-focus-entry`,
remove: removeFactory({
element, moddle, commandStack, inputEntry
}),
};
}) : [];
function add(event) {
const { businessObject } = element;
const newInputID = moddle.ids.nextPrefixed('DataInput_');
// Create a new DataInput
const newInput = bpmnFactory.create('bpmn:DataInput', { id: newInputID, name: newInputID });
// Check if ioSpecification already exists
createSpecification(bpmnFactory, businessObject, 'input', newInput)
// Update the element
updateElementProperties(commandStack, element);
event.stopPropagation();
}
return { items, add };
}
function removeFactory(props) {
const { element, bpmnFactory, commandStack, inputEntry } = props;
return function (event) {
event.stopPropagation();
removeElementFromSpecification(element, inputEntry, 'input');
updateElementProperties(commandStack, element);
};
}
function InputParamGroup(props) {
const { id, inputEntry, element, moddle, commandStack, translate, bpmnFactory } = props;
return [
{
id,
inputEntry,
component: InputParamTextField,
isEdited: isTextFieldEntryEdited,
element,
moddle,
commandStack,
translate,
bpmnFactory
}
];
}
function InputParamTextField(props) {
const { id, element, inputEntry, moddle, commandStack, translate, bpmnFactory } = props;
const debounce = useService('debounceInput');
const setValue = (value) => {
try {
const ioSpecification = element.businessObject.ioSpecification;
if (!value || value == '') {
console.error('No value provided for this input.');
return;
}
if (!ioSpecification) {
console.error('No ioSpecification found for this element.');
return;
}
let existingInput = ioSpecification.dataInputs.find(input => input.id === inputEntry.name || input.name === inputEntry.name);
if (existingInput) {
existingInput.name = value;
existingInput.id = value;
} else {
console.error(`No DataInput found :> ${inputEntry.name}`);
return;
}
updateElementProperties(commandStack, element);
} catch (error) {
console.log('Setting Value Error : ', error);
}
};
const getValue = () => {
return inputEntry.name;
};
return TextFieldEntry({
element,
id: `${id}-input`,
label: translate('Input Name'),
getValue,
setValue,
debounce,
});
}

View File

@ -0,0 +1,59 @@
import { ListGroup, DescriptionEntry } from '@bpmn-io/properties-panel';
import { InputParametersArray } from './InputParametersArray.js';
import { OutputParametersArray } from './OutputParametersArray.js';
export function createIoGroup(
element,
translate,
moddle,
commandStack,
bpmnFactory
) {
const group = {
label: translate('Input/Output Management'),
id: 'ioProperties',
entries: [],
};
// add description input
group.entries.push({
id: `infos-textField`,
component: DescriptionEntry,
value:
' When no specific inputs/outputs are defined, all process variables are accessible.',
element,
translate,
commandStack,
});
// add input list component
group.entries.push({
id: 'inputParameters',
label: translate('Inputs'),
component: ListGroup,
...InputParametersArray({
element,
moddle,
translate,
commandStack,
bpmnFactory
}),
});
// add output list component
group.entries.push({
id: 'outputParameters',
label: translate('Outputs'),
component: ListGroup,
...OutputParametersArray({
element,
moddle,
translate,
commandStack,
bpmnFactory
})
});
return group;
}

View File

@ -0,0 +1,55 @@
import { is } from 'bpmn-js/lib/util/ModelUtil';
import { createIoGroup } from './IoGroup.js';
const LOW_PRIORITY = 500;
export default function IoPropertiesProvider(
propertiesPanel,
translate,
moddle,
commandStack,
elementRegistry,
bpmnFactory
) {
this.getGroups = function getGroupsCallback(element) {
return function pushGroup(groups) {
if (isBpmnTask(element)) {
groups.push(
createIoGroup(
element,
translate,
moddle,
commandStack,
bpmnFactory
)
);
}
return groups;
};
};
propertiesPanel.registerProvider(LOW_PRIORITY, this);
}
IoPropertiesProvider.$inject = [
'propertiesPanel',
'translate',
'moddle',
'commandStack',
'elementRegistry',
'bpmnFactory',
];
function isBpmnTask(element) {
if (!element) {
return false;
}
return (
is(element, 'bpmn:UserTask') ||
is(element, 'bpmn:ScriptTask') ||
is(element, 'bpmn:ServiceTask') ||
is(element, 'bpmn:SendTask') ||
is(element, 'bpmn:ReceiveTask') ||
is(element, 'bpmn:ManualTask')
);
}

View File

@ -0,0 +1,134 @@
import { useService } from 'bpmn-js-properties-panel';
import {
isTextFieldEntryEdited,
TextFieldEntry,
} from '@bpmn-io/properties-panel';
import { createSpecification, removeElementFromSpecification, updateElementProperties } from '../helpers';
export function OutputParametersArray(props) {
const { element, moddle, translate, commandStack, bpmnFactory } = props;
const { businessObject } = element;
const ioSpecification = businessObject.ioSpecification;
const outputsEntries = (ioSpecification) ? ioSpecification.dataOutputs : [];
const items = (outputsEntries) ? outputsEntries.map((outputEntry, index) => {
const id = `outputEntry-${index}`;
return {
id,
label: translate(outputEntry.name),
entries: OutputParamGroup({
element,
commandStack,
moddle,
translate,
bpmnFactory,
outputEntry
}),
autoFocusEntry: `output-focus-entry`,
remove: removeFactory({
element, moddle, commandStack, outputEntry
}),
};
}) : [];
function add(event) {
const { businessObject } = element;
const newOutputID = moddle.ids.nextPrefixed('DataOutput_');
// Create a new DataOutput
const newOutput = bpmnFactory.create('bpmn:DataOutput', { id: newOutputID, name: newOutputID });
// Check if ioSpecification already exists
createSpecification(bpmnFactory, businessObject, 'output', newOutput)
// Update the element
updateElementProperties(commandStack, element);
event.stopPropagation();
}
return { items, add };
}
function removeFactory(props) {
const { element, bpmnFactory, commandStack, outputEntry } = props;
return function (event) {
event.stopPropagation();
removeElementFromSpecification(element, outputEntry, 'output');
updateElementProperties(commandStack, element);
};
}
function OutputParamGroup(props) {
const { id, outputEntry, element, moddle, commandStack, translate, bpmnFactory } = props;
return [
{
id,
outputEntry,
component: OutputParamTextField,
isEdited: isTextFieldEntryEdited,
element,
moddle,
commandStack,
translate,
bpmnFactory
}
];
}
function OutputParamTextField(props) {
const { id, element, outputEntry, moddle, commandStack, translate, bpmnFactory } = props;
const debounce = useService('debounceInput');
const setValue = (value) => {
try {
const ioSpecification = element.businessObject.ioSpecification;
if (!value || value == '') {
console.error('No value provided for this input.');
return;
}
if (!ioSpecification) {
console.error('No ioSpecification found for this element.');
return;
}
let existingInput = ioSpecification.dataOutputs.find(input => input.id === outputEntry.name || input.name === outputEntry.name);
if (existingInput) {
existingInput.name = value;
existingInput.id = value;
} else {
console.error(`No DataOutput found :> ${outputEntry.name}`);
return;
}
updateElementProperties(commandStack, element);
} catch (error) {
console.log('Setting Value Error : ', error);
}
};
const getValue = () => {
return outputEntry.name;
};
return TextFieldEntry({
element,
id: `${id}-output`,
label: translate('Output Name'),
getValue,
setValue,
debounce,
});
}

View File

@ -16,6 +16,7 @@ import SignalPropertiesProvider from './signals/propertiesPanel/SignalProperties
import ErrorPropertiesProvider from './errors/propertiesPanel/ErrorPropertiesProvider';
import EscalationPropertiesProvider from './escalations/propertiesPanel/EscalationPropertiesProvider';
import CallActivityPropertiesProvider from './callActivity/propertiesPanel/CallActivityPropertiesProvider';
import IoPropertiesProvider from './InputOutput/propertiesProvider/IoPropertiesProvider';
import StandardLoopPropertiesProvider from './loops/StandardLoopPropertiesProvider';
import MultiInstancePropertiesProvider from './loops/MultiInstancePropertiesProvider';
import CallActivityInterceptor from './callActivity/CallActivityInterceptor';
@ -44,6 +45,7 @@ export default {
'dataObjectRenderer',
'multiInstancePropertiesProvider',
'standardLoopPropertiesProvider',
'IoPropertiesProvider',
'callActivityInterceptor'
],
dataObjectInterceptor: ['type', DataObjectInterceptor],
@ -66,5 +68,6 @@ export default {
ioInterceptor: ['type', IoInterceptor],
multiInstancePropertiesProvider: ['type', MultiInstancePropertiesProvider],
standardLoopPropertiesProvider: ['type', StandardLoopPropertiesProvider],
callActivityInterceptor: [ 'type', CallActivityInterceptor ],
IoPropertiesProvider: ['type', IoPropertiesProvider],
callActivityInterceptor: [ 'type', CallActivityInterceptor ]
};

View File

@ -0,0 +1,91 @@
import {
query as domQuery
} from 'min-dom';
import { bootstrapPropertiesPanel, CONTAINER, expectSelected, findGroupEntry } from './helpers';
import inputOutput from '../../app/spiffworkflow/InputOutput';
import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel';
import { fireEvent } from '@testing-library/preact';
describe('BPMN Input / Output Variables', function () {
let xml = require('./bpmn/io_variables.bpmn').default;
beforeEach(bootstrapPropertiesPanel(xml, {
debounceInput: false,
additionalModules: [
inputOutput,
BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule
]
}));
it('should be able to add a new input to the user task', async function () {
// We Select a userTask element
const shapeElement = await expectSelected('Activity_1hmit5k');
expect(shapeElement, "I can't find User Task element").to.exist;
// Expect shapeElement.businessObject.ioSpecification to be undefined
expect(shapeElement.businessObject.ioSpecification).to.be.undefined;
// Add new dataInput
const entry = findGroupEntry('inputParameters', CONTAINER);
let addButton = domQuery('.bio-properties-panel-add-entry', entry);
fireEvent.click(addButton);
expect(shapeElement.businessObject.ioSpecification).not.to.be.undefined;
expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(1);
});
it('should be able to add a new output to the user task', async function () {
// We Select a userTask element
const shapeElement = await expectSelected('Activity_1hmit5k');
expect(shapeElement, "I can't find User Task element").to.exist;
// Expect shapeElement.businessObject.ioSpecification to be undefined
expect(shapeElement.businessObject.ioSpecification).to.be.undefined;
// Add new dataOutput
const entry = findGroupEntry('outputParameters', CONTAINER);
let addButton = domQuery('.bio-properties-panel-add-entry', entry);
fireEvent.click(addButton);
expect(shapeElement.businessObject.ioSpecification).not.to.be.undefined;
expect(shapeElement.businessObject.ioSpecification.dataOutputs.length).to.equal(1);
});
it('should be able to delete an existing input variable from script task', async function () {
// We Select a scriptTask element
const shapeElement = await expectSelected('Activity_1dkj93x');
expect(shapeElement, "I can't find Script Task element").to.exist;
expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(1);
const entry = findGroupEntry('inputParameters', CONTAINER);
let removeButton = domQuery('.bio-properties-panel-remove-entry', entry);
fireEvent.click(removeButton);
expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(0);
expect(shapeElement.businessObject.ioSpecification.dataOutputs.length).to.equal(1);
})
it('should be able to delete an existing output variable from script task', async function () {
// We Select a scriptTask element
const shapeElement = await expectSelected('Activity_1dkj93x');
expect(shapeElement, "I can't find Script Task element").to.exist;
expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(1);
const entry = findGroupEntry('outputParameters', CONTAINER);
let removeButton = domQuery('.bio-properties-panel-remove-entry', entry);
fireEvent.click(removeButton);
expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(1);
expect(shapeElement.businessObject.ioSpecification.dataOutputs.length).to.equal(0);
})
});

View File

@ -0,0 +1,70 @@
<?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:di="http://www.omg.org/spec/DD/20100524/DI"
xmlns:camunda="http://camunda.org/schema/1.0/bpmn"
xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1qnx3d3"
targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0"
modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.17.0">
<bpmn:process id="Process_16xfaqc" isExecutable="true" camunda:versionTag="1">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0vt1twq</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:endEvent id="Event_0yxpeto">
<bpmn:incoming>Flow_1oukz5y</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0vt1twq" sourceRef="StartEvent_1" targetRef="Activity_1hmit5k" />
<bpmn:sequenceFlow id="Flow_1oukz5y" sourceRef="Activity_1dkj93x" targetRef="Event_0yxpeto" />
<bpmn:scriptTask id="Activity_1dkj93x" name="Script Task">
<bpmn:incoming>Flow_05w3wu8</bpmn:incoming>
<bpmn:outgoing>Flow_1oukz5y</bpmn:outgoing>
<bpmn:ioSpecification>
<bpmn:dataInput id="DataInput_0ab29sz" name="DataInput_0ab29sz" />
<bpmn:dataOutput id="DataOutput_1n1fg4r" name="DataOutput_1n1fg4r" />
<bpmn:inputSet>
<bpmn:dataInputRefs>DataInput_0ab29sz</bpmn:dataInputRefs>
</bpmn:inputSet>
<bpmn:outputSet>
<bpmn:dataOutputRefs>DataOutput_1n1fg4r</bpmn:dataOutputRefs>
</bpmn:outputSet>
</bpmn:ioSpecification>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_05w3wu8" sourceRef="Activity_1hmit5k"
targetRef="Activity_1dkj93x" />
<bpmn:userTask id="Activity_1hmit5k" name="User task">
<bpmn:incoming>Flow_0vt1twq</bpmn:incoming>
<bpmn:outgoing>Flow_05w3wu8</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_16xfaqc">
<bpmndi:BPMNShape id="Event_0yxpeto_di" bpmnElement="Event_0yxpeto">
<dc:Bounds x="422" y="82" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="12" y="82" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1viyuct_di" bpmnElement="Activity_1hmit5k">
<dc:Bounds x="110" y="60" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1ywbcwu_di" bpmnElement="Activity_1dkj93x">
<dc:Bounds x="270" y="60" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0vt1twq_di" bpmnElement="Flow_0vt1twq">
<di:waypoint x="48" y="100" />
<di:waypoint x="110" y="100" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1oukz5y_di" bpmnElement="Flow_1oukz5y">
<di:waypoint x="370" y="100" />
<di:waypoint x="422" y="100" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_05w3wu8_di" bpmnElement="Flow_05w3wu8">
<di:waypoint x="210" y="100" />
<di:waypoint x="270" y="100" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>