From 095da31a942ab80f42dc2376ae20f4f9a6214c8f Mon Sep 17 00:00:00 2001 From: Maciej Barelkowski Date: Wed, 8 May 2024 15:28:54 +0200 Subject: [PATCH] feat: add execution listeners for Zeebe --- src/provider/zeebe/ZeebePropertiesProvider.js | 18 ++ .../zeebe/properties/ExecutionListener.js | 140 ++++++++++++++ .../properties/ExecutionListenersProps.js | 178 ++++++++++++++++++ src/provider/zeebe/properties/index.js | 1 + 4 files changed, 337 insertions(+) create mode 100644 src/provider/zeebe/properties/ExecutionListener.js create mode 100644 src/provider/zeebe/properties/ExecutionListenersProps.js diff --git a/src/provider/zeebe/ZeebePropertiesProvider.js b/src/provider/zeebe/ZeebePropertiesProvider.js index c9faac1c5..c552c81d4 100644 --- a/src/provider/zeebe/ZeebePropertiesProvider.js +++ b/src/provider/zeebe/ZeebePropertiesProvider.js @@ -7,6 +7,7 @@ import { ConditionProps, ErrorProps, EscalationProps, + ExecutionListenersProps, FormProps, HeaderProps, InputPropagationProps, @@ -51,6 +52,7 @@ const ZEEBE_GROUPS = [ OutputPropagationGroup, OutputGroup, HeaderGroup, + ExecutionListenersGroup, ExtensionPropertiesGroup ]; @@ -299,6 +301,22 @@ function AssignmentDefinitionGroup(element, injector) { return group.entries.length ? group : null; } +function ExecutionListenersGroup(element, injector) { + const translate = injector.get('translate'); + const group = { + label: translate('Execution listeners'), + id: 'Zeebe__ExecutionListeners', + component: ListGroup, + ...ExecutionListenersProps({ element, injector }) + }; + + if (group.items) { + return group; + } + + return null; +} + function ExtensionPropertiesGroup(element, injector) { const translate = injector.get('translate'); const group = { diff --git a/src/provider/zeebe/properties/ExecutionListener.js b/src/provider/zeebe/properties/ExecutionListener.js new file mode 100644 index 000000000..74a5ee7ac --- /dev/null +++ b/src/provider/zeebe/properties/ExecutionListener.js @@ -0,0 +1,140 @@ +import { SelectEntry } from '@bpmn-io/properties-panel'; + +import { + useService +} from '../../../hooks'; + +import { FeelEntryWithVariableContext } from '../../../entries/FeelEntryWithContext'; + + +export default function ExecutionListener(props) { + + const { + idPrefix, + listener + } = props; + + const entries = [ + { + id: idPrefix + '-eventType', + component: EventType, + idPrefix, + listener + }, + { + id: idPrefix + '-listenerType', + component: ListenerType, + idPrefix, + listener + }, + { + id: idPrefix + '-retries', + component: Retries, + idPrefix, + listener + } + ]; + + return entries; +} + +function EventType(props) { + const { + idPrefix, + element, + listener + } = props; + + const modeling = useService('modeling'); + const translate = useService('translate'); + + const getOptions = () => { + return [ + { value: 'start', label: translate('Start') }, + { value: 'end', label: translate('End') } + ]; + }; + + const setValue = (value) => { + modeling.updateModdleProperties(element, listener, { + eventType: value + }); + }; + + const getValue = () => { + return listener.get('eventType'); + }; + + return SelectEntry({ + element: listener, + id: idPrefix + '-eventType', + label: translate('Event type'), + getValue, + setValue, + getOptions + }); +} + +function ListenerType(props) { + const { + idPrefix, + element, + listener + } = props; + + const modeling = useService('modeling'); + const translate = useService('translate'); + const debounce = useService('debounceInput'); + + const setValue = (value) => { + modeling.updateModdleProperties(element, listener, { + type: value + }); + }; + + const getValue = () => { + return listener.get('type'); + }; + + return FeelEntryWithVariableContext({ + element: listener, + id: idPrefix + '-listenerType', + label: translate('Listener type'), + getValue, + setValue, + debounce, + feel: 'optional' + }); +} + +function Retries(props) { + const { + idPrefix, + element, + listener + } = props; + + const modeling = useService('modeling'); + const translate = useService('translate'); + const debounce = useService('debounceInput'); + + const setValue = (value) => { + modeling.updateModdleProperties(element, listener, { + retries: value + }); + }; + + const getValue = () => { + return listener.get('retries'); + }; + + return FeelEntryWithVariableContext({ + element: listener, + id: idPrefix + '-retries', + label: translate('Retries'), + getValue, + setValue, + debounce, + feel: 'optional' + }); +} \ No newline at end of file diff --git a/src/provider/zeebe/properties/ExecutionListenersProps.js b/src/provider/zeebe/properties/ExecutionListenersProps.js new file mode 100644 index 000000000..00ab319c1 --- /dev/null +++ b/src/provider/zeebe/properties/ExecutionListenersProps.js @@ -0,0 +1,178 @@ +import { + getBusinessObject, + is +} from 'bpmn-js/lib/util/ModelUtil'; + +import ExecutionListenerProperty from './ExecutionListener'; + +import { + createElement +} from '../../../utils/ElementUtil'; + +import { + getExtensionElementsList +} from '../../../utils/ExtensionElementsUtil'; + +import { without } from 'min-dash'; + +const EVENT_TO_LABEL = { + 'start': 'Start', + 'end': 'End' +}; + +const DEFAULT_LISTENER_PROPS = { + eventType: 'start' +}; + + +export function ExecutionListenersProps({ element, injector }) { + let businessObject = getRelevantBusinessObject(element); + + // do not offer for empty pools + if (!businessObject) { + return; + } + + const listeners = getListenersList(businessObject) || []; + + const bpmnFactory = injector.get('bpmnFactory'), + commandStack = injector.get('commandStack'), + modeling = injector.get('modeling'), + translate = injector.get('translate'); + + const items = listeners.map((listener, index) => { + const id = element.id + '-executionListener-' + index; + const type = listener.get('type') || ''; + + return { + id, + label: translate(`${EVENT_TO_LABEL[listener.get('eventType')]}: {type}`, { type }), + entries: ExecutionListenerProperty({ + idPrefix: id, + element, + listener + }), + autoFocusEntry: id + '-eventType', + remove: removeFactory({ modeling, element, listener }) + }; + }); + + return { + items, + add: addFactory({ bpmnFactory, commandStack, element }), + shouldSort: false + }; +} + +function removeFactory({ modeling, element, listener }) { + return function(event) { + event.stopPropagation(); + + const businessObject = getRelevantBusinessObject(element); + const container = getExecutionListenersContainer(businessObject); + + if (!container) { + return; + } + + const listeners = without(container.get('listeners'), listener); + + modeling.updateModdleProperties(element, container, { listeners }); + }; +} + +function addFactory({ bpmnFactory, commandStack, element }) { + return function(event) { + event.stopPropagation(); + + let commands = []; + + const businessObject = getRelevantBusinessObject(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 zeebe:ExecutionListeners + let executionListeners = getExecutionListenersContainer(businessObject); + + if (!executionListeners) { + const parent = extensionElements; + + executionListeners = createElement('zeebe:ExecutionListeners', { + listeners: [] + }, parent, bpmnFactory); + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: extensionElements, + properties: { + values: [ ...extensionElements.get('values'), executionListeners ] + } + } + }); + } + + // (3) create zeebe:ExecutionListener + const executionListener = createElement('zeebe:ExecutionListener', DEFAULT_LISTENER_PROPS, executionListeners, bpmnFactory); + + // (4) add executionListener to list + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: executionListeners, + properties: { + listeners: [ ...executionListeners.get('listeners'), executionListener ] + } + } + }); + + // (5) commit all updates + commandStack.execute('properties-panel.multi-command-executor', commands); + }; +} + + +// helper ////////////////// + +export function getRelevantBusinessObject(element) { + let businessObject = getBusinessObject(element); + + if (is(element, 'bpmn:Participant')) { + return businessObject.get('processRef'); + } + + return businessObject; +} + +export function getExecutionListenersContainer(element) { + const executionListeners = getExtensionElementsList(element, 'zeebe:ExecutionListeners'); + + return executionListeners && executionListeners[0]; +} + +export function getListenersList(element) { + const executionListeners = getExecutionListenersContainer(element); + + return executionListeners && executionListeners.get('listeners'); +} \ No newline at end of file diff --git a/src/provider/zeebe/properties/index.js b/src/provider/zeebe/properties/index.js index c8dbc34c4..625213352 100644 --- a/src/provider/zeebe/properties/index.js +++ b/src/provider/zeebe/properties/index.js @@ -4,6 +4,7 @@ export { CalledDecisionProps } from './CalledDecisionProps'; export { ConditionProps } from './ConditionProps'; export { ErrorProps } from './ErrorProps'; export { EscalationProps } from './EscalationProps'; +export { ExecutionListenersProps } from './ExecutionListenersProps'; export { FormProps } from './FormProps'; export { HeaderProps } from './HeaderProps'; export { InputPropagationProps } from './InputPropagationProps';