diff --git a/src/provider/zeebe/ZeebePropertiesProvider.js b/src/provider/zeebe/ZeebePropertiesProvider.js index cfdc3e48d..f559bba65 100644 --- a/src/provider/zeebe/ZeebePropertiesProvider.js +++ b/src/provider/zeebe/ZeebePropertiesProvider.js @@ -24,6 +24,7 @@ import { SignalProps, TargetProps, TaskDefinitionProps, + TaskListenersProps, TaskScheduleProps, TimerProps, UserTaskImplementationProps, @@ -56,6 +57,7 @@ const ZEEBE_GROUPS = [ OutputPropagationGroup, OutputGroup, HeaderGroup, + TaskListenersGroup, ExecutionListenersGroup, ExtensionPropertiesGroup ]; @@ -323,6 +325,22 @@ function ExecutionListenersGroup(element, injector) { return null; } +function TaskListenersGroup(element, injector) { + const translate = injector.get('translate'); + const group = { + label: translate('Task listeners'), + id: 'Zeebe__TaskListeners', + component: ListGroup, + ...TaskListenersProps({ 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 index 5d275a81d..ad3667bb0 100644 --- a/src/provider/zeebe/properties/ExecutionListener.js +++ b/src/provider/zeebe/properties/ExecutionListener.js @@ -13,7 +13,7 @@ import { getErrorEventDefinition } from '../../../utils/EventDefinitionUtil'; -import { FeelEntryWithVariableContext } from '../../../entries/FeelEntryWithContext'; +import { ListenerType, Retries } from './shared/Listener'; export const EVENT_TO_LABEL = { @@ -95,70 +95,6 @@ function EventType(props) { }); } -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, - 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, - id: idPrefix + '-retries', - label: translate('Retries'), - getValue, - setValue, - debounce, - feel: 'optional' - }); -} - export function getEventTypes(element) { if (isAny(element, [ 'bpmn:BoundaryEvent', 'bpmn:StartEvent' ])) { return [ 'end' ]; diff --git a/src/provider/zeebe/properties/TaskListener.js b/src/provider/zeebe/properties/TaskListener.js new file mode 100644 index 000000000..f6242e451 --- /dev/null +++ b/src/provider/zeebe/properties/TaskListener.js @@ -0,0 +1,84 @@ +import { SelectEntry } from '@bpmn-io/properties-panel'; + +import { + useService +} from '../../../hooks'; + +import { ListenerType, Retries } from './shared/Listener'; + +export const EVENT_TYPE = [ 'complete', 'assignment' ]; + +export const EVENT_TO_LABEL = { + complete: 'Complete', + assignment: 'Assignment' +}; + +export function TaskListenerEntries(props) { + + const { + idPrefix, + listener + } = props; + + return [ + { + id: idPrefix + '-eventType', + component: EventType, + idPrefix, + listener, + eventTypes: EVENT_TYPE + }, + { + id: idPrefix + '-listenerType', + component: ListenerType, + idPrefix, + listener + }, + { + id: idPrefix + '-retries', + component: Retries, + idPrefix, + listener + } + ]; +} + +function EventType(props) { + const { + idPrefix, + element, + listener, + eventTypes + } = props; + + const modeling = useService('modeling'); + const translate = useService('translate'); + + const getOptions = () => { + return eventTypes.map(eventType => ({ + value: eventType, + label: translate(EVENT_TO_LABEL[eventType]) + })); + }; + + const setValue = (value) => { + modeling.updateModdleProperties(element, listener, { + eventType: value + }); + }; + + const getValue = () => { + return listener.get('eventType'); + }; + + return SelectEntry({ + element, + id: idPrefix + '-eventType', + label: translate('Event type'), + getValue, + setValue, + getOptions + }); +} + + diff --git a/src/provider/zeebe/properties/TaskListenersProps.js b/src/provider/zeebe/properties/TaskListenersProps.js new file mode 100644 index 000000000..07f143dd3 --- /dev/null +++ b/src/provider/zeebe/properties/TaskListenersProps.js @@ -0,0 +1,197 @@ +import { + getBusinessObject, + is, + isAny +} from 'bpmn-js/lib/util/ModelUtil'; + +import { without } from 'min-dash'; + +import { TaskListenerEntries, EVENT_TYPE, EVENT_TO_LABEL } from './TaskListener'; + +import { + createElement +} from '../../../utils/ElementUtil'; + +import { + getExtensionElementsList +} from '../../../utils/ExtensionElementsUtil'; + +import { isZeebeUserTask } from '../utils/FormUtil'; + + +export function TaskListenersProps({ element, injector }) { + let businessObject = getRelevantBusinessObject(element); + + // not allowed in empty pools + if (!businessObject) { + return; + } + + if (!isZeebeUserTask(element)) { + return; + } + + const moddle = injector.get('moddle'); + if (!canHaveTaskListeners(businessObject, moddle)) { + 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 + '-TaskListener-' + index; + const type = listener.get('type') || ''; + + return { + id, + label: translate(`${EVENT_TO_LABEL[listener.get('eventType')]}: {type}`, { type }), + entries: TaskListenerEntries({ + idPrefix: id, + listener + }), + autoFocusEntry: id + '-eventType', + remove: removeFactory({ modeling, element, listener }) + }; + }); + + return { + items, + add: addFactory({ bpmnFactory, commandStack, element }) + }; +} + +function removeFactory({ modeling, element, listener }) { + return function(event) { + event.stopPropagation(); + + const businessObject = getRelevantBusinessObject(element); + const container = getTaskListenersContainer(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:TaskListeners + let taskListeners = getTaskListenersContainer(businessObject); + + if (!taskListeners) { + const parent = extensionElements; + + taskListeners = createElement('zeebe:TaskListeners', { + listeners: [] + }, parent, bpmnFactory); + + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: extensionElements, + properties: { + values: [ ...extensionElements.get('values'), taskListeners ] + } + } + }); + } + + // (3) create zeebe:TaskListener + const TaskListener = createElement( + 'zeebe:TaskListener', getDefaultListenerProps(), taskListeners, bpmnFactory + ); + + // (4) add TaskListener to list + commands.push({ + cmd: 'element.updateModdleProperties', + context: { + element, + moddleElement: taskListeners, + properties: { + listeners: [ ...taskListeners.get('listeners'), TaskListener ] + } + } + }); + + // (5) commit all updates + commandStack.execute('properties-panel.multi-command-executor', commands); + }; +} + + +// helper ////////////////// + +function getRelevantBusinessObject(element) { + let businessObject = getBusinessObject(element); + + if (is(element, 'bpmn:Participant')) { + return businessObject.get('processRef'); + } + + return businessObject; +} + +function getTaskListenersContainer(element) { + const TaskListeners = getExtensionElementsList(element, 'zeebe:TaskListeners'); + + return TaskListeners && TaskListeners[0]; +} + +function getListenersList(element) { + const TaskListeners = getTaskListenersContainer(element); + + return TaskListeners && TaskListeners.get('listeners'); +} + +function canHaveTaskListeners(bo, moddle) { + const TaskListenersDescriptor = moddle.getTypeDescriptor('zeebe:TaskListeners'); + + if (!isAny(bo, TaskListenersDescriptor.meta.allowedIn)) { + return false; + } + + return true; +} + +function getDefaultListenerProps() { + return { + eventType: EVENT_TYPE[0] + }; +} \ No newline at end of file diff --git a/src/provider/zeebe/properties/index.js b/src/provider/zeebe/properties/index.js index d824f4f14..200cbff7d 100644 --- a/src/provider/zeebe/properties/index.js +++ b/src/provider/zeebe/properties/index.js @@ -19,6 +19,7 @@ export { ScriptProps } from './ScriptProps'; export { SignalProps } from './SignalProps'; export { TargetProps } from './TargetProps'; export { TaskDefinitionProps } from './TaskDefinitionProps'; +export { TaskListenersProps } from './TaskListenersProps'; export { TaskScheduleProps } from './TaskScheduleProps'; export { TimerProps } from './TimerProps'; export { UserTaskImplementationProps } from './UserTaskImplementationProps'; diff --git a/src/provider/zeebe/properties/shared/Listener.js b/src/provider/zeebe/properties/shared/Listener.js new file mode 100644 index 000000000..90b78b55c --- /dev/null +++ b/src/provider/zeebe/properties/shared/Listener.js @@ -0,0 +1,67 @@ +import { FeelEntryWithVariableContext } from '../../../../entries/FeelEntryWithContext'; + +import { useService } from '../../../../hooks'; + +export 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, + id: idPrefix + '-listenerType', + label: translate('Listener type'), + getValue, + setValue, + debounce, + feel: 'optional' + }); +} + +export 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, + id: idPrefix + '-retries', + label: translate('Retries'), + getValue, + setValue, + debounce, + feel: 'optional' + }); +} \ No newline at end of file