diff --git a/src/provider/zeebe/ZeebePropertiesProvider.js b/src/provider/zeebe/ZeebePropertiesProvider.js index c552c81d..80fec3db 100644 --- a/src/provider/zeebe/ZeebePropertiesProvider.js +++ b/src/provider/zeebe/ZeebePropertiesProvider.js @@ -16,6 +16,7 @@ import { MultiInstanceProps, OutputPropagationProps, OutputProps, + PriorityDefinitionProps, ScriptImplementationProps, ScriptProps, SignalProps, @@ -293,7 +294,8 @@ function AssignmentDefinitionGroup(element, injector) { label: translate('Assignment'), entries: [ ...AssignmentDefinitionProps({ element }), - ...TaskScheduleProps({ element }) + ...TaskScheduleProps({ element }), + ...PriorityDefinitionProps({ element }) ], component: Group }; diff --git a/src/provider/zeebe/properties/PriorityDefinitionProps.js b/src/provider/zeebe/properties/PriorityDefinitionProps.js new file mode 100644 index 00000000..8d26f340 --- /dev/null +++ b/src/provider/zeebe/properties/PriorityDefinitionProps.js @@ -0,0 +1,163 @@ +import { + getBusinessObject, + is +} from 'bpmn-js/lib/util/ModelUtil'; + +import { isFeelEntryEdited } from '@bpmn-io/properties-panel'; + +import { + getExtensionElementsList +} from '../../../utils/ExtensionElementsUtil'; + +import { + createElement +} from '../../../utils/ElementUtil'; + +import { + useService +} from '../../../hooks'; + +import { FeelEntryWithVariableContext } from '../../../entries/FeelEntryWithContext'; + + +export function PriorityDefinitionProps(props) { + const { + element + } = props; + + if (!is(element, 'bpmn:UserTask')) { + return []; + } + + return [ + { + id: 'priorityDefinitionPriority', + component: Priority, + isEdited: isFeelEntryEdited + } + ]; +} + +function Priority(props) { + const { + element + } = props; + + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + const translate = useService('translate'); + const debounce = useService('debounceInput'); + + const getValue = () => { + return (getPriorityDefinition(element) || {}).priority || '50'; + }; + + const setValue = (value) => { + const commands = []; + + const businessObject = getBusinessObject(element); + + let extensionElements = businessObject.get('extensionElements'); + + // (1) ensure extension elements + if (!extensionElements) { + extensionElements = createElement( + 'bpmn:ExtensionElements', + { values: [] }, + businessObject, + bpmnFactory + ); + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: businessObject, + properties: { extensionElements } + } + }); + } + + // (2) ensure PriorityDefinition + let priorityDefinition = getPriorityDefinition(element); + const isNullValue = value === null || value === '' || value === undefined; + + if (priorityDefinition && isNullValue) { + + // (3a) remove priority definition if it exists and priority is set to null + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: extensionElements, + properties: { + values: extensionElements.get('values').filter(v => v !== priorityDefinition) + } + } + }); + + } else if (priorityDefinition && !isNullValue) { + + // (3b) update priority definition if it already exists + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: priorityDefinition, + properties: { priority: value } + } + }); + + } else if (!priorityDefinition && !isNullValue) { + + // (3c) create priority definition if it does not exist + priorityDefinition = createElement( + 'zeebe:PriorityDefinition', + { priority: value }, + extensionElements, + bpmnFactory + ); + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: extensionElements, + properties: { + values: [ ...extensionElements.get('values'), priorityDefinition ] + } + } + }); + } + + // (4) commit all updates + commandStack.execute('properties-panel.multi-command-executor', commands); + }; + + const tooltip = `
+

An integer value that defines the priority of a User Task. It can be set as an expression.

+

The priority can range from 0 to 100, with a default value of 50.

+

A higher value indicates a higher priority.

+

Read more about it here.

+
`; + + return FeelEntryWithVariableContext({ + element, + id: 'priorityDefinitionPriority', + label: translate('Priority'), + feel: 'optional', + tooltip, + getValue, + setValue, + debounce + }); +} + + +// helper /////////////////////// + +export function getPriorityDefinition(element) { + const businessObject = getBusinessObject(element); + + return getExtensionElementsList(businessObject, 'zeebe:PriorityDefinition')[0]; +} diff --git a/src/provider/zeebe/properties/index.js b/src/provider/zeebe/properties/index.js index 62521335..8282f3c4 100644 --- a/src/provider/zeebe/properties/index.js +++ b/src/provider/zeebe/properties/index.js @@ -13,6 +13,7 @@ export { MessageProps } from './MessageProps'; export { MultiInstanceProps } from './MultiInstanceProps'; export { OutputPropagationProps } from './OutputPropagationProps'; export { OutputProps } from './OutputProps'; +export { PriorityDefinitionProps } from './PriorityDefinitionProps'; export { ScriptImplementationProps } from './ScriptImplementationProps'; export { ScriptProps } from './ScriptProps'; export { SignalProps } from './SignalProps'; diff --git a/test/spec/provider/zeebe/PriorityDefinitionProps.bpmn b/test/spec/provider/zeebe/PriorityDefinitionProps.bpmn new file mode 100644 index 00000000..dd08f4b8 --- /dev/null +++ b/test/spec/provider/zeebe/PriorityDefinitionProps.bpmn @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/provider/zeebe/PriorityDefinitionProps.spec.js b/test/spec/provider/zeebe/PriorityDefinitionProps.spec.js new file mode 100644 index 00000000..a34824e8 --- /dev/null +++ b/test/spec/provider/zeebe/PriorityDefinitionProps.spec.js @@ -0,0 +1,249 @@ +import TestContainer from 'mocha-test-container-support'; + +import { + act +} from '@testing-library/preact'; + +import { + bootstrapPropertiesPanel, + changeInput, + inject +} from 'test/TestHelper'; + +import { + query as domQuery +} from 'min-dom'; + +import { + getBusinessObject +} from 'bpmn-js/lib/util/ModelUtil'; + +import { getPriorityDefinition } from 'src/provider/zeebe/properties/PriorityDefinitionProps'; + +import CoreModule from 'bpmn-js/lib/core'; +import SelectionModule from 'diagram-js/lib/features/selection'; +import ModelingModule from 'bpmn-js/lib/features/modeling'; + +import BpmnPropertiesPanel from 'src/render'; + +import ZeebePropertiesProvider from 'src/provider/zeebe'; + +import BehaviorsModule from 'camunda-bpmn-js-behaviors/lib/camunda-cloud'; + +import zeebeModdleExtensions from 'zeebe-bpmn-moddle/resources/zeebe'; + +import diagramXML from './PriorityDefinitionProps.bpmn'; +import { setEditorValue } from '../../../TestHelper'; + + +describe('provider/zeebe - PriorityDefinitionProps', function() { + + const testModules = [ + CoreModule, + SelectionModule, + ModelingModule, + BpmnPropertiesPanel, + ZeebePropertiesProvider, + BehaviorsModule + ]; + + const moddleExtensions = { + zeebe: zeebeModdleExtensions + }; + + let container; + + beforeEach(function() { + container = TestContainer.get(this); + }); + + beforeEach(bootstrapPropertiesPanel(diagramXML, { + modules: testModules, + moddleExtensions, + debounceInput: false + })); + + + describe('zeebe:priorityDefinition', function() { + + it('should NOT display for service task', inject(async function(elementRegistry, selection) { + + // given + const serviceTask = elementRegistry.get('ServiceTask_1'); + + await act(() => { + selection.select(serviceTask); + }); + + // when + const priorityInput = domQuery('input[name=priorityDefinitionPriority]', container); + + // then + expect(priorityInput).to.not.exist; + })); + + + it('should display for user task', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('UserTask_1'); + + await act(() => { + selection.select(userTask); + }); + + // when + const entry = domQuery('[data-entry-id="priorityDefinitionPriority"]', container); + + // then + expect(entry).to.exist; + + // is FEEL input + const input = domQuery('[role="textbox"]', entry); + expect(input).to.exist; + + const priorityDefinition = getPriorityDefinition(userTask); + const feelExpression = priorityDefinition.get('priority').substring(1); + + expect(input.textContent).to.equal(feelExpression); + })); + + + it('should display value 50 if undefined', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('UserTask_2'); + + await act(() => { + selection.select(userTask); + }); + + // when + const priorityInput = domQuery('input[name=priorityDefinitionPriority]', container); + + // then + expect(priorityInput.value).to.equal('50'); + })); + + + it('should update', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('UserTask_1'); + + await act(() => { + selection.select(userTask); + }); + + // when + const priorityInput = domQuery('[data-entry-id="priorityDefinitionPriority"] [role="textbox"]', container); + + await setEditorValue(priorityInput, 'newValue'); + + // then + // keep FEEL configuration + expect(getPriorityDefinition(userTask).get('priority')).to.eql('=newValue'); + })); + + + it('should update on external change', + inject(async function(elementRegistry, selection, commandStack) { + + // given + const userTask = elementRegistry.get('UserTask_1'); + const originalValue = getPriorityDefinition(userTask).get('priority'); + + await act(() => { + selection.select(userTask); + }); + const priorityInput = domQuery('[data-entry-id="priorityDefinitionPriority"] [role="textbox"]', container); + await setEditorValue(priorityInput, 'newValue'); + + // when + await act(() => { + commandStack.undo(); + }); + + // then + expect('=' + priorityInput.textContent).to.eql(originalValue); + }) + ); + + + it('should create non-existing extension elements and priority definition', + inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('UserTask_2'); + + // assume + expect(getBusinessObject(userTask).get('extensionElements')).to.not.exist; + + await act(() => { + selection.select(userTask); + }); + + // when + const priorityInput = domQuery('input[name=priorityDefinitionPriority]', container); + changeInput(priorityInput, 'newValue'); + + // then + expect(getBusinessObject(userTask).get('extensionElements')).to.exist; + }) + ); + + + it('should re-use existing extension elements, creating new priority definition', + inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('UserTask_3'); + + // assume + expect(getBusinessObject(userTask).get('extensionElements')).to.exist; + expect(getPriorityDefinition(userTask)).not.to.exist; + + await act(() => { + selection.select(userTask); + }); + + // when + const priorityInput = domQuery('input[name=priorityDefinitionPriority]', container); + changeInput(priorityInput, 'newValue'); + + // then + const extensionElements = getBusinessObject(userTask).get('extensionElements'); + expect(getPriorityDefinition(userTask).get('priority')).to.eql('newValue'); + expect(extensionElements.values).to.have.length(2); + }) + ); + + }); + + describe('integration', function() { + + describe('removing priority definition when empty', function() { + + it('removing zeebe:priority', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('UserTask_4'); + + await act(() => { + selection.select(userTask); + }); + + // when + const priorityInput = domQuery('[data-entry-id="priorityDefinitionPriority"] [role="textbox"]', container); + + await setEditorValue(priorityInput, ''); + + // then + expect(getPriorityDefinition(userTask)).not.to.exist; + })); + + }); + + }); + +});