From 160f92a24fb7644f6c568430715815590f859747 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 18 Sep 2023 13:59:24 +0200 Subject: [PATCH] feat: add linked form Closes #949 Related to https://github.com/camunda/camunda-modeler/issues/3656 --- src/provider/zeebe/properties/FormProps.js | 404 +++++++++++---------- test/spec/provider/zeebe/Forms.bpmn | 29 +- test/spec/provider/zeebe/Forms.spec.js | 153 ++++++-- 3 files changed, 359 insertions(+), 227 deletions(-) diff --git a/src/provider/zeebe/properties/FormProps.js b/src/provider/zeebe/properties/FormProps.js index 04fb9a5e5..1babaaa24 100644 --- a/src/provider/zeebe/properties/FormProps.js +++ b/src/provider/zeebe/properties/FormProps.js @@ -1,3 +1,5 @@ +import { without } from 'min-dash'; + import { getBusinessObject, is @@ -14,18 +16,10 @@ import { import { createElement } from '../../../utils/ElementUtil'; -import { - isUndefined, - without -} from 'min-dash'; - -import { - useService -} from '../../../hooks'; +import { useService } from '../../../hooks'; import { createUserTaskFormId, - formKeyToUserTaskFormId, FORM_TYPES, getFormDefinition, getFormType, @@ -56,7 +50,12 @@ export function FormProps(props) { component: FormConfiguration, isEdited: isTextAreaEntryEdited }); - + } else if (formType === FORM_TYPES.CAMUNDA_FORM_LINKED) { + entries.push({ + id: 'formId', + component: FormId, + isEdited: isTextFieldEntryEdited + }); } else if (formType === FORM_TYPES.CUSTOM_FORM) { entries.push({ id: 'customFormKey', @@ -80,20 +79,23 @@ function FormType(props) { }; const setValue = (value) => { - removeUserTaskForm(injector, element); - 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) { - setFormDefinition(injector, element, ''); + setCustomFormKey(injector, element, ''); + } else { + removeFormDefinition(injector, element); } }; const getOptions = () => { return [ { value: '', label: translate('') }, - { value: FORM_TYPES.CAMUNDA_FORM_EMBEDDED, label: translate('Camunda forms') }, - { value: FORM_TYPES.CUSTOM_FORM, label: translate('Custom form key') }, + { 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') } ]; }; @@ -116,8 +118,7 @@ function FormConfiguration(props) { translate = useService('translate'); const getValue = () => { - const userTaskForm = getUserTaskForm(element); - return userTaskForm.get('body'); + return getUserTaskForm(element).get('body'); }; const setValue = (value) => { @@ -136,6 +137,32 @@ function FormConfiguration(props) { } +function FormId(props) { + const { element } = props; + + const debounce = useService('debounceInput'), + injector = useService('injector'), + translate = useService('translate'); + + const getValue = () => { + return getFormDefinition(element).get('formId'); + }; + + const setValue = (value) => { + setFormId(injector, element, value); + }; + + return TextFieldEntry({ + element, + id: 'formId', + label: translate('Form ID'), + getValue, + setValue, + debounce + }); +} + + function CustomFormKey(props) { const { element } = props; @@ -144,12 +171,11 @@ function CustomFormKey(props) { translate = useService('translate'); const getValue = () => { - const formDefinition = getFormDefinition(element); - return formDefinition.get('formKey'); + return getFormDefinition(element).get('formKey'); }; const setValue = (value) => { - setFormDefinition(injector, element, value); + setCustomFormKey(injector, element, value); }; return TextFieldEntry({ @@ -167,236 +193,220 @@ function CustomFormKey(props) { /** * @typedef { { cmd: string, context: Object } } Command * @typedef {Command[]} Commands + * + * @typedef {import('diagram-js/lib/model/Types').Element} Element + * @typedef {import('bpmn-js/lib/model/Types').ModdleElement} ModdleElement + * + * @param {import('didi').Injector} Injector */ /** - * @param {import('didi').Injector} injector - * @param {import('diagram-js/lib/model/Types').Element} element - * @param {Object} userTaskFormProperties + * @param {Injector} injector + * @param {Element} element * - * @returns {Commands} + * @returns { { + * commands: Commands, + * extensionElements: ModdleElement + * } } */ -function ensureUserTaskForm(injector, element, userTaskFormProperties) { - const bpmnFactory = injector.get('bpmnFactory'); +function getOrCreateExtensionElements(injector, element, moddleElement) { + const businessObject = moddleElement || getBusinessObject(element); - let commands = []; + let extensionElements = businessObject.get('extensionElements'); - const rootElement = getRootElement(element); + if (extensionElements) { + return { + commands: [], + extensionElements + }; + } - // (1) ensure root element extension elements - let rootExtensionElements = rootElement.get('extensionElements'); + const bpmnFactory = injector.get('bpmnFactory'); - if (!rootExtensionElements) { - rootExtensionElements = createElement( - 'bpmn:ExtensionElements', - { values: [] }, - rootElement, - bpmnFactory - ); + extensionElements = createElement('bpmn:ExtensionElements', { + values: [] + }, businessObject, bpmnFactory); - commands.push( - createUpdateModdlePropertiesCommand(element, rootElement, { - extensionElements: rootExtensionElements, + return { + commands: [ + createUpdateModdlePropertiesCommand(element, businessObject, { + extensionElements }) - ); - } + ], + extensionElements + }; +} - // (2) ensure user task form - let userTaskForm = getUserTaskForm(element); +/** + * @param {Injector} injector + * @param {Element} element + * + * @returns { { +* commands: Commands, +* formDefinition: ModdleElement +* } } +*/ +function getOrCreateFormDefintition(injector, element) { + let formDefinition = getFormDefinition(element); - // (2.1) create user task form if doesn't exist - if (!userTaskForm) { - userTaskForm = createUserTaskForm( - injector, - userTaskFormProperties, - rootExtensionElements - ); - - commands.push( - createUpdateModdlePropertiesCommand(element, rootExtensionElements,{ - values: [ ...rootExtensionElements.get('values'), userTaskForm ] - }) - ); + if (formDefinition) { + return { + commands: [], + formDefinition + }; } - commands.push(createUpdateModdlePropertiesCommand(element, userTaskForm, userTaskFormProperties)); + const { + extensionElements, + commands + } = getOrCreateExtensionElements(injector, element); + + formDefinition = createFormDefinition(injector, {}, extensionElements); - return commands; + return { + commands: [ + ...commands, + createUpdateModdlePropertiesCommand(element, extensionElements, { + values: [ + ...extensionElements.get('values'), + formDefinition + ] + }) + ], + formDefinition + }; } /** - * @param {import('didi').Injector} injector - * @param {import('diagram-js/lib/model/Types').Element} element - * @param {string} [customFormKey] + * @param {Injector} injector + * @param {Element} element * - * @returns { { formId: string, commands: Commands } } + * @returns { { + * commands: Commands, + * formDefinition: ModdleElement, + * userTaskForm: ModdleElement + * } } */ -function ensureFormDefinition(injector, element, customFormKey) { - const bpmnFactory = injector.get('bpmnFactory'); - - const businessObject = getBusinessObject(element); - - let commands = []; - - // (1) ensure extension elements - let extensionElements = businessObject.get('extensionElements'); - - if (isUndefined(extensionElements)) { - extensionElements = createElement( - 'bpmn:ExtensionElements', - { values: [] }, - businessObject, - bpmnFactory - ); +function getOrCreateUserTaskForm(injector, element) { + let userTaskForm = getUserTaskForm(element); - commands.push( - createUpdateModdlePropertiesCommand(element, businessObject, { - extensionElements: extensionElements, - }) - ); + if (userTaskForm) { + return { + commands: [], + formDefinition: getFormDefinition(element), + userTaskForm + }; } - // (2) ensure form definition - let formDefinition = getFormDefinition(element); + const rootElement = getRootElement(element); - // (2.1) create if doesn't exist - if (!formDefinition) { - let formKey = customFormKey; + const { + extensionElements, + commands: extensionElementsCommands + } = getOrCreateExtensionElements(injector, element, rootElement); - if (isUndefined(formKey)) { - const formId = createUserTaskFormId(); - formKey = userTaskFormIdToFormKey(formId); - } + const { + formDefinition, + commands: formDefinitionCommands + } = getOrCreateFormDefintition(injector, element); - const formDefinitionProperties = { - formKey - }; + const formId = createUserTaskFormId(); - formDefinition = createFormDefinition(injector, formDefinitionProperties, extensionElements); + userTaskForm = createUserTaskForm(injector, { + id: formId + }, extensionElements); - commands.push( + return { + commands: [ + ...extensionElementsCommands, + ...formDefinitionCommands, createUpdateModdlePropertiesCommand(element, extensionElements, { - values: [ ...extensionElements.get('values'), formDefinition ] - }) - ); - } - - // (2.2) update existing form definition with custom key - else if (customFormKey) { - commands.push( + values: [ + ...extensionElements.get('values'), + userTaskForm + ] + }), createUpdateModdlePropertiesCommand(element, formDefinition, { - formKey: customFormKey + formKey: userTaskFormIdToFormKey(formId) }) - ); - } - - return { - formId: formKeyToUserTaskFormId(formDefinition.get('formKey')), - commands + ], + formDefinition, + userTaskForm }; } -/** - * @param {import('didi').Injector} injector - * @param {import('diagram-js/lib/model/Types').Element} element - * @param {string} [customFormKey] - */ -function setFormDefinition(injector, element, customFormKey) { - const commandStack = injector.get('commandStack'); - - const { - commands - } = ensureFormDefinition(injector, element, customFormKey); - - commandStack.execute('properties-panel.multi-command-executor', commands); -} +function setFormId(injector, element, formId) { + let { + commands, + formDefinition + } = getOrCreateFormDefintition(injector, element); -/** - * @param {import('didi').Injector} injector - * @param {import('diagram-js/lib/model/Types').Element} element - * @param {string} body - */ -function setUserTaskForm(injector, element, body) { const commandStack = injector.get('commandStack'); - let { - formId, - commands - } = ensureFormDefinition(injector, element); - - commands = [ + commandStack.execute('properties-panel.multi-command-executor', [ ...commands, - ...ensureUserTaskForm(injector, element, { - id: formId, - body + createUpdateModdlePropertiesCommand(element, formDefinition, { + formId }) - ]; - - commandStack.execute('properties-panel.multi-command-executor', commands); + ]); } -/** - * @param {import('diagram-js/lib/model/Types').Element} element - * - * @returns {Commands} - */ -function removeFormDefinition(element) { - const businessObject = getBusinessObject(element), - extensionElements = businessObject.get('extensionElements'); - - let commands = []; +function setCustomFormKey(injector, element, formKey) { + let { + commands, + formDefinition + } = getOrCreateFormDefintition(injector, element); - const formDefinition = getFormDefinition(element); + const commandStack = injector.get('commandStack'); - if (!formDefinition) { - return commands; - } + commandStack.execute('properties-panel.multi-command-executor', [ + ...commands, + createUpdateModdlePropertiesCommand(element, formDefinition, { + formKey + }) + ]); +} - let values = without(extensionElements.get('values'), formDefinition); +function setUserTaskForm(injector, element, body) { + let { + commands, + userTaskForm + } = getOrCreateUserTaskForm(injector, element); - commands.push( - createUpdateModdlePropertiesCommand(element, extensionElements, { values }) - ); + const commandStack = injector.get('commandStack'); - return commands; + commandStack.execute('properties-panel.multi-command-executor', [ + ...commands, + createUpdateModdlePropertiesCommand(element, userTaskForm, { + body + }) + ]); } -/** - * @param {import('didi').Injector} injector - * @param {import('diagram-js/lib/model/Types').Element} element - */ -function removeUserTaskForm(injector, element) { - const commandStack = injector.get('commandStack'); - - const rootElement = getRootElement(element), - rootExtensionElements = rootElement.get('extensionElements'); +function removeFormDefinition(injector, element) { + const formDefinition = getFormDefinition(element); - // (1) remove form definition - const commands = removeFormDefinition(element); + /** + * @type {import('bpmn-js/lib/features/modeling/Modeling').default} + */ + const modeling = injector.get('modeling'); - // (2) remove referenced user task form - const userTaskForm = getUserTaskForm(element); + if (formDefinition) { + const businessObject = getBusinessObject(element), + extensionElements = businessObject.get('extensionElements'); - if (!userTaskForm) { - commandStack.execute('properties-panel.multi-command-executor', commands); - return; + modeling.updateModdleProperties(element, extensionElements, { + values: without(extensionElements.get('values'), formDefinition) + }); } - - const values = without(rootExtensionElements.get('values'), userTaskForm); - - commands.push( - createUpdateModdlePropertiesCommand(element, rootExtensionElements, { values }) - ); - - commandStack.execute('properties-panel.multi-command-executor', commands); } /** - * @param {import('didi').Injector} injector + * @param {Injector} injector * @param {Object} properties - * @param {import('bpmn-js/lib/model/Types').ModdleElement} parent + * @param {ModdleElement} parent * - * @returns {import('bpmn-js/lib/model/Types').ModdleElement} + * @returns {ModdleElement} */ function createFormDefinition(injector, properties, parent) { const bpmnFactory = injector.get('bpmnFactory'); @@ -410,11 +420,11 @@ function createFormDefinition(injector, properties, parent) { } /** - * @param {import('didi').Injector} injector + * @param {Injector} injector * @param {Object} properties - * @param {import('bpmn-js/lib/model/Types').ModdleElement} parent + * @param {ModdleElement} parent * - * @returns {import('bpmn-js/lib/model/Types').ModdleElement} + * @returns {ModdleElement} */ function createUserTaskForm(injector, properties, parent) { const bpmnFactory = injector.get('bpmnFactory'); @@ -428,8 +438,8 @@ function createUserTaskForm(injector, properties, parent) { } /** - * @param {import('diagram-js/lib/model/Types').Element} element - * @param {import('bpmn-js/lib/model/Types').ModdleElement} moddleElement + * @param {Element} element + * @param {ModdleElement} moddleElement * @param {Object} properties * * @returns {Command} diff --git a/test/spec/provider/zeebe/Forms.bpmn b/test/spec/provider/zeebe/Forms.bpmn index 4e1089c06..f5e929bcf 100644 --- a/test/spec/provider/zeebe/Forms.bpmn +++ b/test/spec/provider/zeebe/Forms.bpmn @@ -1,31 +1,42 @@ - + {} - + - + - + + + + + + - - + + + - + + + + + - - + + + diff --git a/test/spec/provider/zeebe/Forms.spec.js b/test/spec/provider/zeebe/Forms.spec.js index 6bbbd969b..461597859 100644 --- a/test/spec/provider/zeebe/Forms.spec.js +++ b/test/spec/provider/zeebe/Forms.spec.js @@ -31,6 +31,8 @@ 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 './Forms.bpmn'; @@ -43,7 +45,8 @@ describe('provider/zeebe - Forms', function() { CoreModule, ModelingModule, SelectionModule, - ZeebePropertiesProvider + ZeebePropertiesProvider, + BehaviorsModule ]; const moddleExtensions = { @@ -68,7 +71,7 @@ describe('provider/zeebe - Forms', function() { it('should display - embedded form', inject(async function(elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CAMUNDA_FORM'); + const userTask = elementRegistry.get('CAMUNDA_FORM_EMBEDDED'); // when await act(() => { @@ -84,10 +87,29 @@ describe('provider/zeebe - Forms', function() { })); + it('should display - linked form', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CAMUNDA_FORM_LINKED'); + + // when + await act(() => { + selection.select(userTask); + }); + + const formTypeSelect = getFormTypeSelect(container); + + // then + expect(formTypeSelect).to.exist; + + expect(formTypeSelect.value).to.equal(FORM_TYPES.CAMUNDA_FORM_LINKED); + })); + + it('should display - custom form', inject(async function(elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CUSTOM_KEY'); + const userTask = elementRegistry.get('CUSTOM_FORM'); // when await act(() => { @@ -125,7 +147,7 @@ describe('provider/zeebe - Forms', function() { it('should update - empty', inject(async function(elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CAMUNDA_FORM'); + const userTask = elementRegistry.get('CAMUNDA_FORM_EMBEDDED'); await act(() => { selection.select(userTask); @@ -138,14 +160,12 @@ describe('provider/zeebe - Forms', function() { // then - // expect user task form not to exist const formDefinition = getFormDefinition(userTask); expect(formDefinition).to.not.exist; - // expect form definnition not to exist const rootElement = getRootElement(userTask); - const forms = getExtensionElementsList(rootElement, 'zeebe:UserTaskForm'); - expect(forms).to.have.lengthOf(0); + const userTaskForms = getExtensionElementsList(rootElement, 'zeebe:UserTaskForm'); + expect(userTaskForms).to.have.lengthOf(0); })); @@ -168,6 +188,25 @@ describe('provider/zeebe - Forms', function() { })); + it('should update - linked form', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('NO_FORM'); + + await act(() => { + selection.select(userTask); + }); + + const formTypeSelect = getFormTypeSelect(container); + + // when + changeInput(formTypeSelect, FORM_TYPES.CAMUNDA_FORM_LINKED); + + // then + expectFormId(userTask, ''); + })); + + it('should update - custom form', inject(async function(elementRegistry, selection) { // given @@ -212,7 +251,6 @@ describe('provider/zeebe - Forms', function() { const formDefinition = getFormDefinition(userTask); expect(formDefinition).to.not.exist; - // expect form definnition not to exist const forms = getExtensionElementsList(rootElement, 'zeebe:UserTaskForm'); expect(forms).to.have.lengthOf(initialTaskForms); })); @@ -225,7 +263,7 @@ describe('provider/zeebe - Forms', function() { it('should display', inject(async function(elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CAMUNDA_FORM'); + const userTask = elementRegistry.get('CAMUNDA_FORM_EMBEDDED'); // when await act(() => { @@ -242,7 +280,7 @@ describe('provider/zeebe - Forms', function() { it('should update', inject(async function(elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CAMUNDA_FORM'); + const userTask = elementRegistry.get('CAMUNDA_FORM_EMBEDDED'); await act(() => { selection.select(userTask); @@ -261,7 +299,7 @@ describe('provider/zeebe - Forms', function() { it('should update on external change', inject(async function(commandStack, elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CAMUNDA_FORM'); + const userTask = elementRegistry.get('CAMUNDA_FORM_EMBEDDED'); await act(() => { selection.select(userTask); @@ -278,19 +316,82 @@ describe('provider/zeebe - Forms', function() { commandStack.undo(); }); - // expect form definnition not to exist expectUserTaskForm(userTask, initialConfig); })); }); + describe('linked form', function() { + + it('should display', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CAMUNDA_FORM_LINKED'); + + // when + await act(() => { + selection.select(userTask); + }); + + const formIdInput = getFormIdInput(container); + + // then + expect(formIdInput).to.exist; + })); + + + it('should update', inject(async function(elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CAMUNDA_FORM_LINKED'); + + await act(() => { + selection.select(userTask); + }); + + const formIdInput = getFormIdInput(container); + + // when + changeInput(formIdInput, 'bar'); + + // then + expectFormId(userTask, 'bar'); + })); + + + it('should update on external change', inject(async function(commandStack, elementRegistry, selection) { + + // given + const userTask = elementRegistry.get('CAMUNDA_FORM_LINKED'); + + await act(() => { + selection.select(userTask); + }); + + const formIdInput = getFormIdInput(container); + const initialFormId = formIdInput.value; + + changeInput(formIdInput, 'bar'); + expectFormId(userTask, 'bar'); + + // when + await act(() => { + commandStack.undo(); + }); + + expectFormId(userTask, initialFormId); + })); + + }); + + describe('custom form', function() { it('should display', inject(async function(elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CUSTOM_KEY'); + const userTask = elementRegistry.get('CUSTOM_FORM'); // when await act(() => { @@ -307,7 +408,7 @@ describe('provider/zeebe - Forms', function() { it('should update', inject(async function(elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CUSTOM_KEY'); + const userTask = elementRegistry.get('CUSTOM_FORM'); await act(() => { selection.select(userTask); @@ -316,17 +417,17 @@ describe('provider/zeebe - Forms', function() { const customFormKeyInput = getCustomFormKeyInput(container); // when - changeInput(customFormKeyInput, 'customKey'); + changeInput(customFormKeyInput, 'foo'); // then - expectFormKey(userTask, 'customKey'); + expectFormKey(userTask, 'foo'); })); it('should update on external change', inject(async function(commandStack, elementRegistry, selection) { // given - const userTask = elementRegistry.get('WITH_CUSTOM_KEY'); + const userTask = elementRegistry.get('CUSTOM_FORM'); await act(() => { selection.select(userTask); @@ -335,15 +436,14 @@ describe('provider/zeebe - Forms', function() { const customFormKeyInput = getCustomFormKeyInput(container); const initialFormKey = customFormKeyInput.value; - changeInput(customFormKeyInput, ''); - expectFormKey(userTask, 'customKey'); + changeInput(customFormKeyInput, 'bar'); + expectFormKey(userTask, 'bar'); // when await act(() => { commandStack.undo(); }); - // expect form definnition not to exist expectFormKey(userTask, initialFormKey); })); @@ -362,10 +462,21 @@ function getFormConfigurationTextarea(container) { return domQuery('textarea[name=formConfiguration]', container); } +function getFormIdInput(container) { + return domQuery('input[name=formId]', container); +} + function getFormTypeSelect(container) { return domQuery('select[name=formType]', container); } +function expectFormId(element, expected) { + const formDefinition = getFormDefinition(element); + + expect(formDefinition).to.exist; + expect(formDefinition.get('formId')).to.eql(expected); +} + function expectFormKey(element, expected) { const formDefinition = getFormDefinition(element);