diff --git a/src/provider/zeebe/ZeebePropertiesProvider.js b/src/provider/zeebe/ZeebePropertiesProvider.js index 320a1ef19..af9828e3f 100644 --- a/src/provider/zeebe/ZeebePropertiesProvider.js +++ b/src/provider/zeebe/ZeebePropertiesProvider.js @@ -21,7 +21,8 @@ import { TargetProps, TaskDefinitionProps, TaskScheduleProps, - TimerProps + TimerProps, + UserTaskImplementationProps } from './properties'; import { ExtensionPropertiesProps } from '../shared/ExtensionPropertiesProps'; @@ -39,6 +40,7 @@ const ZEEBE_GROUPS = [ CalledDecisionGroup, ScriptImplementationGroup, ScriptGroup, + UserTaskImplementationGroup, TaskDefinitionGroup, AssignmentDefinitionGroup, FormGroup, @@ -255,6 +257,19 @@ function ScriptImplementationGroup(element) { return group.entries.length ? group : null; } +function UserTaskImplementationGroup(element) { + const group = { + id: 'userTaskImplementation', + label: 'Implementation', + entries: [ + ...UserTaskImplementationProps({ element }) + ], + component: Group + }; + + return group.entries.length ? group : null; +} + function AssignmentDefinitionGroup(element) { const group = { id: 'assignmentDefinition', diff --git a/src/provider/zeebe/properties/FormProps.js b/src/provider/zeebe/properties/FormProps.js index 25533fb11..62ce48137 100644 --- a/src/provider/zeebe/properties/FormProps.js +++ b/src/provider/zeebe/properties/FormProps.js @@ -12,7 +12,6 @@ import { SelectEntry, TextFieldEntry, TextAreaEntry, - isSelectEntryEdited, isTextFieldEntryEdited, isTextAreaEntryEdited } from '@bpmn-io/properties-panel'; @@ -28,9 +27,12 @@ import { getFormType, getRootElement, getUserTaskForm, + isZeebeUserTask, userTaskFormIdToFormKey } from '../utils/FormUtil'; +const NONE_VALUE = 'none'; + export function FormProps(props) { const { element } = props; @@ -42,7 +44,7 @@ export function FormProps(props) { const entries = [ { id: 'formType', component: FormType, - isEdited: isSelectEntryEdited + isEdited: node => node.value !== NONE_VALUE } ]; const formType = getFormType(element); @@ -62,7 +64,13 @@ export function FormProps(props) { } else if (formType === FORM_TYPES.CUSTOM_FORM) { entries.push({ id: 'customFormKey', - component: CustomFormKey, + component: CustomForm, + isEdited: isTextFieldEntryEdited + }); + } else if (formType === FORM_TYPES.EXTERNAL_REFERENCE) { + entries.push({ + id: 'externalReference', + component: ExternalReference, isEdited: isTextFieldEntryEdited }); } @@ -78,28 +86,15 @@ function FormType(props) { translate = useService('translate'); const getValue = () => { - return getFormType(element) || ''; + return getFormType(element) || NONE_VALUE; }; const setValue = (value) => { - if (value === FORM_TYPES.CAMUNDA_FORM_EMBEDDED) { - setUserTaskForm(injector, element, ''); - } else if (value === FORM_TYPES.CAMUNDA_FORM_LINKED) { - setFormId(injector, element, ''); - } else if (value === FORM_TYPES.CUSTOM_FORM) { - setCustomFormKey(injector, element, ''); - } else { - removeFormDefinition(injector, element); - } + setFormType(injector, element, value); }; const getOptions = () => { - return [ - { value: '', label: translate('') }, - { value: FORM_TYPES.CAMUNDA_FORM_LINKED, label: translate('Camunda Form (linked)') }, - { value: FORM_TYPES.CAMUNDA_FORM_EMBEDDED, label: translate('Camunda Form (embedded)') }, - { value: FORM_TYPES.CUSTOM_FORM, label: translate('Custom form key') } - ]; + return getFormTypeOptions(translate, element); }; return SelectEntry({ @@ -112,6 +107,37 @@ function FormType(props) { }); } +function setFormType(injector, element, value) { + if (value === FORM_TYPES.CAMUNDA_FORM_EMBEDDED) { + setUserTaskForm(injector, element, ''); + } else if (value === FORM_TYPES.CAMUNDA_FORM_LINKED) { + setFormId(injector, element, ''); + } else if (value === FORM_TYPES.CUSTOM_FORM) { + setCustomFormKey(injector, element, ''); + } else if (value === FORM_TYPES.EXTERNAL_REFERENCE) { + setExternalReference(injector, element, ''); + } else { + removeFormDefinition(injector, element); + } +} + +function getFormTypeOptions(translate, element) { + if (isZeebeUserTask(element)) { + return [ + { value: NONE_VALUE, label: translate('') }, + { value: FORM_TYPES.CAMUNDA_FORM_LINKED, label: translate('Camunda Form') }, + { value: FORM_TYPES.EXTERNAL_REFERENCE, label: translate('External form reference') } + ]; + } + + return [ + { value: NONE_VALUE, label: translate('') }, + { value: FORM_TYPES.CAMUNDA_FORM_LINKED, label: translate('Camunda Form (linked)') }, + { value: FORM_TYPES.CAMUNDA_FORM_EMBEDDED, label: translate('Camunda Form (embedded)') }, + { value: FORM_TYPES.CUSTOM_FORM, label: translate('Custom form key') } + ]; +} + function FormConfiguration(props) { const { element } = props; @@ -165,8 +191,7 @@ function FormId(props) { }); } - -function CustomFormKey(props) { +function CustomForm(props) { const { element } = props; const debounce = useService('debounceInput'), @@ -174,7 +199,8 @@ function CustomFormKey(props) { translate = useService('translate'); const getValue = () => { - return getFormDefinition(element).get('formKey'); + const formDefinition = getFormDefinition(element); + return formDefinition.get('formKey'); }; const setValue = (value) => { @@ -184,7 +210,33 @@ function CustomFormKey(props) { return TextFieldEntry({ element, id: 'customFormKey', - label: translate('Form key'), + label: translate('Custom form key'), + getValue, + setValue, + debounce + }); +} + +function ExternalReference(props) { + const { element } = props; + + const debounce = useService('debounceInput'), + injector = useService('injector'), + translate = useService('translate'); + + const getValue = () => { + const formDefinition = getFormDefinition(element); + return formDefinition.get('externalReference'); + }; + + const setValue = (value) => { + setExternalReference(injector, element, isUndefined(value) ? '' : value); + }; + + return TextFieldEntry({ + element, + id: 'externalReference', + label: translate('External form reference'), getValue, setValue, debounce @@ -370,6 +422,22 @@ function setCustomFormKey(injector, element, formKey) { ]); } +function setExternalReference(injector, element, externalReference) { + let { + commands, + formDefinition + } = getOrCreateFormDefintition(injector, element); + + const commandStack = injector.get('commandStack'); + + commandStack.execute('properties-panel.multi-command-executor', [ + ...commands, + createUpdateModdlePropertiesCommand(element, formDefinition, { + externalReference + }) + ]); +} + function setUserTaskForm(injector, element, body) { let { commands, diff --git a/src/provider/zeebe/properties/UserTaskImplementationProps.js b/src/provider/zeebe/properties/UserTaskImplementationProps.js new file mode 100644 index 000000000..52940eb5a --- /dev/null +++ b/src/provider/zeebe/properties/UserTaskImplementationProps.js @@ -0,0 +1,125 @@ +import { + getBusinessObject +} from 'bpmn-js/lib/util/ModelUtil'; + +import { + is +} from 'bpmn-js/lib/util/ModelUtil'; + +import { SelectEntry } from '@bpmn-io/properties-panel'; + +import { + addExtensionElements, + getExtensionElementsList, + removeExtensionElements +} from '../../../utils/ExtensionElementsUtil'; + +import { + createElement +} from '../../../utils/ElementUtil'; + +import { useService } from '../../../hooks'; + +export const ZEEBE_USER_TASK_IMPLEMENTATION_OPTION = 'zeebeUserTask', + JOB_WORKER_IMPLEMENTATION_OPTION = 'jobWorker'; + + +export function UserTaskImplementationProps(props) { + const { + element + } = props; + + if (!is(element, 'bpmn:UserTask')) { + return []; + } + + return [ + { + id: 'userTaskImplementation', + component: UserTaskImplementation, + isEdited: () => isUserTaskImplementationEdited(element) + } + ]; +} + +function UserTaskImplementation(props) { + const { + element, + id + } = props; + + const commandStack = useService('commandStack'); + const bpmnFactory = useService('bpmnFactory'); + const translate = useService('translate'); + + const getValue = () => { + if (getZeebeUserTask(element)) { + return ZEEBE_USER_TASK_IMPLEMENTATION_OPTION; + } + + return JOB_WORKER_IMPLEMENTATION_OPTION; + }; + + /** + * Set value by either creating or removing zeebe:userTask extension element. + * Note that they must not exist both at the same time, however this + * will be ensured by a camunda-bpmn-js behavior (and not by the propPanel). + */ + const setValue = (value) => { + if (value === ZEEBE_USER_TASK_IMPLEMENTATION_OPTION) { + createZeebeUserTask(element, bpmnFactory, commandStack); + } else if (value === JOB_WORKER_IMPLEMENTATION_OPTION) { + removeZeebeUserTask(element, commandStack); + } + }; + + const getOptions = () => { + + const options = [ + { value: ZEEBE_USER_TASK_IMPLEMENTATION_OPTION, label: translate('Zeebe user task') }, + { value: JOB_WORKER_IMPLEMENTATION_OPTION, label: translate('Job worker') } + ]; + + return options; + }; + + return SelectEntry({ + element, + id, + label: translate('Implementation'), + getValue, + setValue, + getOptions + }); +} + + +// helper /////////////////////// +function createZeebeUserTask(element, bpmnFactory, commandStack) { + const businessObject = getBusinessObject(element); + + const zeebeUserTask = createElement( + 'zeebe:UserTask', + {}, + businessObject, + bpmnFactory + ); + + addExtensionElements(element, businessObject, zeebeUserTask, bpmnFactory, commandStack); +} + +function removeZeebeUserTask(element, commandStack) { + const zeebeUserTask = getZeebeUserTask(element); + + removeExtensionElements(element, getBusinessObject(element), zeebeUserTask, commandStack); +} + +function isUserTaskImplementationEdited(element) { + return getZeebeUserTask(element); +} + +function getZeebeUserTask(element) { + const businessObject = getBusinessObject(element); + + return getExtensionElementsList(businessObject, 'zeebe:UserTask')[0]; +} diff --git a/src/provider/zeebe/properties/index.js b/src/provider/zeebe/properties/index.js index cc5b454a0..c8dbc34c4 100644 --- a/src/provider/zeebe/properties/index.js +++ b/src/provider/zeebe/properties/index.js @@ -19,3 +19,4 @@ 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 diff --git a/src/provider/zeebe/utils/FormUtil.js b/src/provider/zeebe/utils/FormUtil.js index d457c206a..50ab7d11d 100644 --- a/src/provider/zeebe/utils/FormUtil.js +++ b/src/provider/zeebe/utils/FormUtil.js @@ -15,7 +15,8 @@ const FORM_KEY_PREFIX = 'camunda-forms:bpmn:', export const FORM_TYPES = { CAMUNDA_FORM_EMBEDDED: 'camunda-form-embedded', CAMUNDA_FORM_LINKED: 'camunda-form-linked', - CUSTOM_FORM: 'custom-form' + CUSTOM_FORM: 'custom-form', + EXTERNAL_REFERENCE: 'external-reference' }; export const DEFAULT_FORM_TYPE = FORM_TYPES.CAMUNDA_FORM_LINKED; @@ -78,12 +79,17 @@ export function getFormType(element) { } const formId = formDefinition.get('formId'), - formKey = formDefinition.get('formKey'); + formKey = formDefinition.get('formKey'), + externalReference = formDefinition.get('externalReference'); if (isDefined(formId)) { return FORM_TYPES.CAMUNDA_FORM_LINKED; } + if (isDefined(externalReference)) { + return FORM_TYPES.EXTERNAL_REFERENCE; + } + if (isDefined(formKey)) { if (getUserTaskForm(element)) { @@ -92,4 +98,10 @@ export function getFormType(element) { return FORM_TYPES.CUSTOM_FORM; } -} \ No newline at end of file +} + +export function isZeebeUserTask(element) { + const bo = getBusinessObject(element); + + return getExtensionElementsList(bo, 'zeebe:UserTask').length > 0; +} diff --git a/test/spec/provider/zeebe/Forms.bpmn b/test/spec/provider/zeebe/Forms.bpmn index f5e929bcf..87717bbec 100644 --- a/test/spec/provider/zeebe/Forms.bpmn +++ b/test/spec/provider/zeebe/Forms.bpmn @@ -10,16 +10,33 @@ + + + + + + + + + + + + + + + + + @@ -29,15 +46,28 @@ + + + + + + + + + + + + + diff --git a/test/spec/provider/zeebe/Forms.spec.js b/test/spec/provider/zeebe/Forms.spec.js index c10f365d0..207f40a70 100644 --- a/test/spec/provider/zeebe/Forms.spec.js +++ b/test/spec/provider/zeebe/Forms.spec.js @@ -125,6 +125,25 @@ describe('provider/zeebe - Forms', function() { })); + it('should display - external reference', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CUSTOM_FORM_ZEEBE_USER_TASK'); + + // when + await act(() => { + selection.select(userTask); + }); + + const formTypeSelect = getFormTypeSelect(container); + + // then + expect(formTypeSelect).to.exist; + + expect(formTypeSelect.value).to.equal(FORM_TYPES.EXTERNAL_REFERENCE); + })); + + it('should display - empty', inject(async function(elementRegistry, selection) { // given @@ -140,7 +159,7 @@ describe('provider/zeebe - Forms', function() { // then expect(formTypeSelect).to.exist; - expect(formTypeSelect.value).to.equal(''); + expect(formTypeSelect.value).to.equal('none'); })); @@ -226,6 +245,25 @@ describe('provider/zeebe - Forms', function() { })); + it('should update - external reference', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('NO_FORM_ZEEBE_USER_TASK'); + + await act(() => { + selection.select(userTask); + }); + + const formTypeSelect = getFormTypeSelect(container); + + // when + changeInput(formTypeSelect, FORM_TYPES.EXTERNAL_REFERENCE); + + // then + expectExternalReference(userTask, ''); + })); + + it('should update on external change', inject(async function(commandStack, elementRegistry, selection) { // given @@ -360,6 +398,23 @@ describe('provider/zeebe - Forms', function() { })); + it('should display - Zeebe User Task', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CAMUNDA_FORM_LINKED_ZEEBE_USER_TASK'); + + // when + await act(() => { + selection.select(userTask); + }); + + const formIdInput = getFormIdInput(container); + + // then + expect(formIdInput).to.exist; + })); + + it('should update', inject(async function(elementRegistry, selection) { // given @@ -443,6 +498,23 @@ describe('provider/zeebe - Forms', function() { })); + it('should NOT display', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CUSTOM_FORM_ZEEBE_USER_TASK'); + + // when + await act(() => { + selection.select(userTask); + }); + + const customFormKeyInput = getCustomFormKeyInput(container); + + // then + expect(customFormKeyInput).not.to.exist; + })); + + it('should update', inject(async function(elementRegistry, selection) { // given @@ -503,9 +575,106 @@ describe('provider/zeebe - Forms', function() { expectFormKey(userTask, initialFormKey); })); - }); + + describe('external reference', function() { + + it('should display', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CUSTOM_FORM_ZEEBE_USER_TASK'); + + // when + await act(() => { + selection.select(userTask); + }); + + const externalReferenceInput = getExternalReferenceInput(container); + + // then + expect(externalReferenceInput).to.exist; + })); + + + it('should NOT display', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CUSTOM_FORM'); + + // when + await act(() => { + selection.select(userTask); + }); + + const externalReferenceInput = getExternalReferenceInput(container); + + // then + expect(externalReferenceInput).not.to.exist; + })); + + + it('should update', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CUSTOM_FORM_ZEEBE_USER_TASK'); + + await act(() => { + selection.select(userTask); + }); + + const externalReferenceInput = getExternalReferenceInput(container); + + // when + changeInput(externalReferenceInput, 'foo'); + + // then + expectExternalReference(userTask, 'foo'); + })); + + + it('should not delete if empty', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CUSTOM_FORM_ZEEBE_USER_TASK'); + + await act(() => { + selection.select(userTask); + }); + + const externalReferenceInput = getExternalReferenceInput(container); + + // when + changeInput(externalReferenceInput, ''); + + // then + expectExternalReference(userTask, ''); + })); + + + it('should update on external change', inject(async function(commandStack, elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CUSTOM_FORM_ZEEBE_USER_TASK'); + + await act(() => { + selection.select(userTask); + }); + + const externalReferenceInput = getExternalReferenceInput(container); + const initialFormKey = externalReferenceInput.value; + + changeInput(externalReferenceInput, 'bar'); + expectExternalReference(userTask, 'bar'); + + // when + await act(() => { + commandStack.undo(); + }); + + expectExternalReference(userTask, initialFormKey); + })); + }); }); @@ -515,6 +684,10 @@ function getCustomFormKeyInput(container) { return domQuery('input[name=customFormKey]', container); } +function getExternalReferenceInput(container) { + return domQuery('input[name=externalReference]', container); +} + function getFormConfigurationTextarea(container) { return domQuery('textarea[name=formConfiguration]', container); } @@ -546,4 +719,11 @@ function expectUserTaskForm(element, expected) { expect(userTaskForm).to.exist; expect(userTaskForm.get('body')).to.eql(expected); -} \ No newline at end of file +} + +function expectExternalReference(element, expected) { + const formDefinition = getFormDefinition(element); + + expect(formDefinition).to.exist; + expect(formDefinition.get('externalReference')).to.eql(expected); +} diff --git a/test/spec/provider/zeebe/UserTaskImplementationProps.bpmn b/test/spec/provider/zeebe/UserTaskImplementationProps.bpmn new file mode 100644 index 000000000..ec2d1863c --- /dev/null +++ b/test/spec/provider/zeebe/UserTaskImplementationProps.bpmn @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/provider/zeebe/UserTaskImplementationProps.spec.js b/test/spec/provider/zeebe/UserTaskImplementationProps.spec.js new file mode 100644 index 000000000..8d9496a02 --- /dev/null +++ b/test/spec/provider/zeebe/UserTaskImplementationProps.spec.js @@ -0,0 +1,233 @@ +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 { + getExtensionElementsList +} from 'src/utils/ExtensionElementsUtil.js'; + +import BpmnPropertiesPanel from 'src/render'; +import CoreModule from 'bpmn-js/lib/core'; +import ModelingModule from 'bpmn-js/lib/features/modeling'; +import SelectionModule from 'diagram-js/lib/features/selection'; +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 './UserTaskImplementationProps.bpmn'; + + +describe('provider/zeebe - UserTaskImplementationProps', function() { + + const testModules = [ + BpmnPropertiesPanel, + CoreModule, + ModelingModule, + SelectionModule, + ZeebePropertiesProvider, + BehaviorsModule + ]; + + const moddleExtensions = { + zeebe: zeebeModdleExtensions + }; + + let container; + + beforeEach(function() { + container = TestContainer.get(this); + }); + + beforeEach(bootstrapPropertiesPanel(diagramXML, { + modules: testModules, + moddleExtensions, + debounceInput: false + })); + + + describe('bpmn:UserTask#implementation', function() { + + it('should display', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('JobWorker'); + + // when + await act(() => { + selection.select(userTask); + }); + + // then + const implementation = getImplementationSelect(container); + expect(implementation).to.exist; + })); + + + it('should not display', inject(async function(elementRegistry, selection) { + + // given + const serviceTask = elementRegistry.get('ServiceTask'); + + // when + await act(() => { + selection.select(serviceTask); + }); + + // then + const implementation = getImplementationSelect(container); + expect(implementation).to.not.exist; + })); + + + it('should display zeebe user task', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('ZeebeUserTask'); + + // when + await act(() => { + selection.select(userTask); + }); + + // then + const implementation = getImplementationSelect(container); + expect(implementation.value).to.equal('zeebeUserTask'); + })); + + + it('should display jobWorker', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('JobWorker'); + + // when + await act(() => { + selection.select(userTask); + }); + + // then + const implementation = getImplementationSelect(container); + expect(implementation.value).to.equal('jobWorker'); + })); + + + it('should create zeebe:UserTask', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('JobWorker'); + + await act(() => { + selection.select(userTask); + }); + + const implementation = getImplementationSelect(container); + + // when + changeInput(implementation, 'zeebeUserTask'); + + // then + const zeebeUserTask = getZeebeUserTask(userTask); + expect(zeebeUserTask).to.exist; + })); + + + it('should remove zeebe:UserTask when set to jobWorker', inject( + async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('ZeebeUserTask'); + + await act(() => { + selection.select(userTask); + }); + + // when + const implementationSelect = getImplementationSelect(container); + changeInput(implementationSelect, 'jobWorker'); + + // then + const zeebeUserTask = getZeebeUserTask(userTask); + expect(zeebeUserTask).to.not.exist; + } + )); + + + it('should re-use extension elements', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('JobWorkerWithFormDefinition'); + const businessObject = getBusinessObject(userTask); + + await act(() => { + selection.select(userTask); + }); + + const implementation = getImplementationSelect(container); + + // assume + expect(getExtensionElementsList(businessObject)).to.have.length(1); + + // when + changeInput(implementation, 'zeebeUserTask'); + + // then + expect(getExtensionElementsList(businessObject)).to.have.length(2); + })); + + + it('should undo', inject(async function(elementRegistry, selection, commandStack) { + + // given + const userTask = elementRegistry.get('JobWorker'); + + await act(() => { + selection.select(userTask); + }); + + const implementation = getImplementationSelect(container); + + // when + changeInput(implementation, 'zeebeUserTask'); + changeInput(implementation, 'jobWorker'); + + await act(() => { + commandStack.undo(); + }); + + // then + expect(implementation.value).to.eql('zeebeUserTask'); + })); + }); + +}); + + +// helper ///////////////// + +function getImplementationSelect(container) { + return domQuery('select[name=userTaskImplementation]', container); +} + +function getZeebeUserTask(element) { + const businessObject = getBusinessObject(element); + + return getExtensionElementsList(businessObject, 'zeebe:UserTask')[0]; +}