From 3aa29584e15a86617e67c8e2d7584fd0153383b7 Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Wed, 10 Jul 2024 12:27:40 +0200 Subject: [PATCH] feat(zeebe): add _Version tag_ field to processes Closes #1062 Related to https://github.com/camunda/camunda-modeler/issues/4453 --- src/contextProvider/zeebe/TooltipProvider.js | 12 + src/provider/zeebe/ZeebePropertiesProvider.js | 22 +- .../zeebe/properties/VersionTagProps.js | 139 +++++++++ src/provider/zeebe/properties/index.js | 3 +- .../zeebe/VersionTagProps-collaboration.bpmn | 25 ++ .../zeebe/VersionTagProps-process.bpmn | 19 ++ .../provider/zeebe/VersionTagProps.spec.js | 272 ++++++++++++++++++ 7 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 src/provider/zeebe/properties/VersionTagProps.js create mode 100644 test/spec/provider/zeebe/VersionTagProps-collaboration.bpmn create mode 100644 test/spec/provider/zeebe/VersionTagProps-process.bpmn create mode 100644 test/spec/provider/zeebe/VersionTagProps.spec.js diff --git a/src/contextProvider/zeebe/TooltipProvider.js b/src/contextProvider/zeebe/TooltipProvider.js index b3bbff6d3..f77c9403e 100644 --- a/src/contextProvider/zeebe/TooltipProvider.js +++ b/src/contextProvider/zeebe/TooltipProvider.js @@ -296,6 +296,18 @@ const TooltipProvider = { ); }, + 'versionTag': (element) => { + + const translate = useService('translate'); + + return ( +
+

+ { translate('Specifying a version tag will allow you to reference this process in another process.') } +

+
+ ); + }, 'priorityDefinitionPriority': (element) => { const translate = useService('translate'); diff --git a/src/provider/zeebe/ZeebePropertiesProvider.js b/src/provider/zeebe/ZeebePropertiesProvider.js index 80fec3db8..cfdc3e48d 100644 --- a/src/provider/zeebe/ZeebePropertiesProvider.js +++ b/src/provider/zeebe/ZeebePropertiesProvider.js @@ -1,5 +1,7 @@ import { Group, ListGroup } from '@bpmn-io/properties-panel'; +import { findIndex } from 'min-dash'; + import { AssignmentDefinitionProps, BusinessRuleImplementationProps, @@ -24,7 +26,8 @@ import { TaskDefinitionProps, TaskScheduleProps, TimerProps, - UserTaskImplementationProps + UserTaskImplementationProps, + VersionTagProps } from './properties'; import { ExtensionPropertiesProps } from '../shared/ExtensionPropertiesProps'; @@ -72,6 +75,7 @@ export default class ZeebePropertiesProvider { groups = groups.concat(this._getGroups(element)); // (2) update existing groups with zeebe specific properties + updateGeneralGroup(groups, element); updateErrorGroup(groups, element); updateEscalationGroup(groups, element); updateMessageGroup(groups, element); @@ -335,6 +339,22 @@ function ExtensionPropertiesGroup(element, injector) { return null; } +function updateGeneralGroup(groups, element) { + + const generalGroup = findGroup(groups, 'general'); + + if (!generalGroup) { + return; + } + + const { entries } = generalGroup; + + const executableEntry = findIndex(entries, (entry) => entry.id === 'isExecutable'); + const insertIndex = executableEntry >= 0 ? executableEntry : entries.length; + + entries.splice(insertIndex, 0, ...VersionTagProps({ element })); +} + function updateErrorGroup(groups, element) { const errorGroup = findGroup(groups, 'error'); diff --git a/src/provider/zeebe/properties/VersionTagProps.js b/src/provider/zeebe/properties/VersionTagProps.js new file mode 100644 index 000000000..cc76b3918 --- /dev/null +++ b/src/provider/zeebe/properties/VersionTagProps.js @@ -0,0 +1,139 @@ +import { + getBusinessObject, + is +} from 'bpmn-js/lib/util/ModelUtil'; + +import { TextFieldEntry, isTextFieldEntryEdited } from '@bpmn-io/properties-panel'; + +import { + useService +} from '../../../hooks'; + +import { createElement } from '../../../utils/ElementUtil'; + +import { getExtensionElementsList } from '../../../utils/ExtensionElementsUtil'; + + +export function VersionTagProps(props) { + const { + element + } = props; + + const businessObject = getBusinessObject(element); + + if (!is(element, 'bpmn:Process') && + !(is(element, 'bpmn:Participant') && businessObject.get('processRef'))) { + return []; + } + + return [ + { + id: 'versionTag', + component: VersionTag, + isEdited: isTextFieldEntryEdited + }, + ]; +} + +function VersionTag(props) { + const { element } = props; + + const bpmnFactory = useService('bpmnFactory'); + const commandStack = useService('commandStack'); + const debounce = useService('debounceInput'); + const translate = useService('translate'); + + const getValue = () => { + const versionTag = getVersionTag(element); + + if (versionTag) { + return versionTag.get('value'); + } + }; + + const setValue = (value) => { + let commands = []; + + const businessObject = getProcess(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 version tag + let versionTag = getVersionTag(element); + + if (!versionTag) { + versionTag = createElement( + 'zeebe:VersionTag', + {}, + extensionElements, + bpmnFactory + ); + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: extensionElements, + properties: { + values: [ ...extensionElements.get('values'), versionTag ] + } + } + }); + } + + // (3) update version tag value + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: versionTag, + properties: { value } + } + }); + + commandStack.execute('properties-panel.multi-command-executor', commands); + }; + + return TextFieldEntry({ + element, + id: 'versionTag', + label: translate('Version tag'), + getValue, + setValue, + debounce + }); +} + + +// helper ////////////////// + +function getProcess(element) { + return is(element, 'bpmn:Process') ? + getBusinessObject(element) : + getBusinessObject(element).get('processRef'); +} + +function getVersionTag(element) { + const businessObject = getProcess(element); + + return getExtensionElementsList(businessObject, 'zeebe:VersionTag')[ 0 ]; +} \ No newline at end of file diff --git a/src/provider/zeebe/properties/index.js b/src/provider/zeebe/properties/index.js index 8282f3c42..d824f4f14 100644 --- a/src/provider/zeebe/properties/index.js +++ b/src/provider/zeebe/properties/index.js @@ -21,4 +21,5 @@ export { TargetProps } from './TargetProps'; export { TaskDefinitionProps } from './TaskDefinitionProps'; export { TaskScheduleProps } from './TaskScheduleProps'; export { TimerProps } from './TimerProps'; -export { UserTaskImplementationProps } from './UserTaskImplementationProps'; \ No newline at end of file +export { UserTaskImplementationProps } from './UserTaskImplementationProps'; +export { VersionTagProps } from './VersionTagProps'; \ No newline at end of file diff --git a/test/spec/provider/zeebe/VersionTagProps-collaboration.bpmn b/test/spec/provider/zeebe/VersionTagProps-collaboration.bpmn new file mode 100644 index 000000000..855750419 --- /dev/null +++ b/test/spec/provider/zeebe/VersionTagProps-collaboration.bpmn @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/provider/zeebe/VersionTagProps-process.bpmn b/test/spec/provider/zeebe/VersionTagProps-process.bpmn new file mode 100644 index 000000000..c137c16ff --- /dev/null +++ b/test/spec/provider/zeebe/VersionTagProps-process.bpmn @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/spec/provider/zeebe/VersionTagProps.spec.js b/test/spec/provider/zeebe/VersionTagProps.spec.js new file mode 100644 index 000000000..4bd5c204a --- /dev/null +++ b/test/spec/provider/zeebe/VersionTagProps.spec.js @@ -0,0 +1,272 @@ +import TestContainer from 'mocha-test-container-support'; + +import { + act +} from '@testing-library/preact'; + +import { + bootstrapPropertiesPanel, + changeInput, + inject, + mouseEnter +} from 'test/TestHelper'; + +import { + query as domQuery +} from 'min-dom'; + +import { + getBusinessObject +} from 'bpmn-js/lib/util/ModelUtil'; + +import CoreModule from 'bpmn-js/lib/core'; +import SelectionModule from 'diagram-js/lib/features/selection'; +import ModelingModule from 'bpmn-js/lib/features/modeling'; + +import { is } from 'bpmn-js/lib/util/ModelUtil'; + +import BpmnPropertiesPanel from 'src/render'; + +import BpmnPropertiesProvider from 'src/provider/bpmn'; +import ZeebePropertiesProvider from 'src/provider/zeebe'; + +import zeebeModdleExtensions from 'zeebe-bpmn-moddle/resources/zeebe'; + +import TooltipProvider from 'src/contextProvider/zeebe/TooltipProvider'; + +import { + getExtensionElementsList +} from 'src/utils/ExtensionElementsUtil'; + +import processDiagramXML from './VersionTagProps-process.bpmn'; +import collaborationDiagramXML from './VersionTagProps-collaboration.bpmn'; + + +describe('provider/zeebe - VersionTagProps', function() { + + const testModules = [ + CoreModule, SelectionModule, ModelingModule, + BpmnPropertiesPanel, + BpmnPropertiesProvider, + ZeebePropertiesProvider + ]; + + const moddleExtensions = { + zeebe: zeebeModdleExtensions + }; + + let container, clock; + + beforeEach(function() { + container = TestContainer.get(this); + clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + clock.restore(); + }); + + function openTooltip() { + return act(() => { + const wrapper = domQuery('.bio-properties-panel-tooltip-wrapper', container); + mouseEnter(wrapper); + clock.tick(200); + }); + } + + [ + [ 'process', 'Process_1', processDiagramXML ], + [ 'collaboration', 'Participant_1', collaborationDiagramXML ] + ].forEach(([ title, elementId, diagramXML ]) => { + + describe(title, function() { + + beforeEach(bootstrapPropertiesPanel(diagramXML, { + modules: testModules, + moddleExtensions, + propertiesPanel: { + tooltip: TooltipProvider + }, + debounceInput: false + })); + + + it('should NOT display for task', inject(async function(elementRegistry, selection) { + + // given + const task = elementRegistry.get('Task_1'); + + await act(() => { + selection.select(task); + }); + + // when + const versionTagInput = domQuery('input[name=versionTag]', container); + + // then + expect(versionTagInput).to.not.exist; + })); + + + it('should display for process', inject(async function(elementRegistry, selection) { + + // given + const element = elementRegistry.get(elementId); + + await act(() => { + selection.select(element); + }); + + // when + const versionTagInput = domQuery('input[name=versionTag]', container); + + // then + expect(versionTagInput).to.exist; + expect(versionTagInput.value).to.eql(getVersionTag(element).get('value')); + })); + + + it('should update', inject(async function(elementRegistry, selection) { + + // given + const element = elementRegistry.get(elementId); + + await act(() => { + selection.select(element); + }); + + // when + const versionTagInput = domQuery('input[name=versionTag]', container); + + changeInput(versionTagInput, 'v2.0.0'); + + // then + expect(getVersionTag(element).get('value')).to.eql('v2.0.0'); + })); + + + it('should update on external change', + inject(async function(elementRegistry, selection, commandStack) { + + // given + const element = elementRegistry.get(elementId); + + const originalValue = getVersionTag(element).get('value'); + + await act(() => { + selection.select(element); + }); + + const versionTagInput = domQuery('input[name=versionTag]', container); + + changeInput(versionTagInput, 'v2.0.0'); + + // when + await act(() => { + commandStack.undo(); + }); + + // then + expect(versionTagInput.value).to.eql(originalValue); + }) + ); + + + it('should create non existing extension elements', + inject(async function(elementRegistry, modeling, selection) { + + // given + const element = elementRegistry.get(elementId); + + modeling.updateModdleProperties(element, getProcess(element), { extensionElements: undefined }); + + // assume + expect(getProcess(element).get('extensionElements')).to.not.exist; + + await act(() => { + selection.select(element); + }); + + // when + const versionTagInput = domQuery('input[name=versionTag]', container); + + changeInput(versionTagInput, 'v1.0.0'); + + // then + expect(getProcess(element).get('extensionElements')).to.exist; + expect(getVersionTag(element).get('value')).to.eql('v1.0.0'); + }) + ); + + + it('should create non existing version tag', + inject(async function(elementRegistry, modeling, selection) { + + // given + const element = elementRegistry.get(elementId); + + modeling.updateModdleProperties( + element, + getProcess(element).get('extensionElements'), + { values: [] } + ); + + // assume + expect(getProcess(element).get('extensionElements')).to.exist; + expect(getVersionTag(element)).not.to.exist; + + await act(() => { + selection.select(element); + }); + + // when + const versionTagInput = domQuery('input[name=versionTag]', container); + + changeInput(versionTagInput, 'v1.0.0'); + + // then + expect(getVersionTag(element)).to.exist; + expect(getVersionTag(element).get('value')).to.eql('v1.0.0'); + }) + ); + + + it('should display correct documentation', inject(async function(elementRegistry, selection) { + + // given + const element = elementRegistry.get(elementId); + + await act(() => { + selection.select(element); + }); + + // when + await openTooltip(); + + const documentationLinkGroup = domQuery('.bio-properties-panel-tooltip-content p', container); + + // then + expect(documentationLinkGroup).to.exist; + expect(documentationLinkGroup.textContent).to.equal('Version tag by which this process can be referenced.'); + })); + + }); + + }); + +}); + + +// helper ////////////////// + +function getProcess(element) { + return is(element, 'bpmn:Process') ? + getBusinessObject(element) : + getBusinessObject(element).get('processRef'); +} + +function getVersionTag(element) { + const businessObject = getProcess(element); + + return getExtensionElementsList(businessObject, 'zeebe:VersionTag')[ 0 ]; +} \ No newline at end of file