diff --git a/package-lock.json b/package-lock.json index f406e62f..69666bd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.13.2", "license": "MIT", "dependencies": { - "@bpmn-io/element-templates-validator": "^1.7.0", + "@bpmn-io/element-templates-validator": "^2.0.0", "@bpmn-io/extract-process-variables": "^0.8.0", "bpmnlint": "^10.0.0", "classnames": "^2.3.1", @@ -521,12 +521,12 @@ } }, "node_modules/@bpmn-io/element-templates-validator": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-1.7.0.tgz", - "integrity": "sha512-IBBUyb045OzXJUMN4Xs8FEL6wwykzqYRcAgoC3Krb2gb4d6mbnpe1b8LUutMpz3PYlomVgRXdTkL+zdGwbO7qQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.0.0.tgz", + "integrity": "sha512-L2PjOme42dVL8W178XTjiSJ5SOTGrHrn/2Xd6KF4/dfRguJkTn62MBgqnYEkXNZ/nj63TTl9wj5ye+M60PzSsA==", "dependencies": { - "@camunda/element-templates-json-schema": "^0.16.0", - "@camunda/zeebe-element-templates-json-schema": "^0.17.0", + "@camunda/element-templates-json-schema": "^0.17.1", + "@camunda/zeebe-element-templates-json-schema": "^0.19.1", "json-source-map": "^0.6.1", "min-dash": "^4.1.1" } @@ -615,9 +615,9 @@ } }, "node_modules/@camunda/element-templates-json-schema": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.16.0.tgz", - "integrity": "sha512-QXlizHS2wh05JZJcVoJHd974SI/bWyGJxrgA8STmqMSUBnslDpCkaCTuxtW9YGsqImOkXKaSYI2843AbMOWBXQ==" + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.17.1.tgz", + "integrity": "sha512-lUiAprJuFaZ3p/7ybmhUBuKT+BlI54WiWei38e9vhg6Dujv37PIQA4wAy6DOv1TqvVSa0WgQpjJop16rfW/Lvw==" }, "node_modules/@camunda/linting": { "version": "3.12.0", @@ -681,9 +681,9 @@ } }, "node_modules/@camunda/zeebe-element-templates-json-schema": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.17.0.tgz", - "integrity": "sha512-iUGC1NdD/w9exO3Eap1d69EcH+uoff+YX1mswUJDqk6OqeDAfnHayClNdHR24VDctdHuwNrqOdGN+/1E/WsMow==" + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.19.1.tgz", + "integrity": "sha512-WjRdQyWwM8zahMEBI6PAo+zZrXbi6ECpfi+GKmkCHG4nojH9kp1xv7t2E0CdqOjTioNwqx/7j1N16lhaa3mBXw==" }, "node_modules/@codemirror/autocomplete": { "version": "6.11.1", @@ -10051,12 +10051,12 @@ } }, "@bpmn-io/element-templates-validator": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-1.7.0.tgz", - "integrity": "sha512-IBBUyb045OzXJUMN4Xs8FEL6wwykzqYRcAgoC3Krb2gb4d6mbnpe1b8LUutMpz3PYlomVgRXdTkL+zdGwbO7qQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.0.0.tgz", + "integrity": "sha512-L2PjOme42dVL8W178XTjiSJ5SOTGrHrn/2Xd6KF4/dfRguJkTn62MBgqnYEkXNZ/nj63TTl9wj5ye+M60PzSsA==", "requires": { - "@camunda/element-templates-json-schema": "^0.16.0", - "@camunda/zeebe-element-templates-json-schema": "^0.17.0", + "@camunda/element-templates-json-schema": "^0.17.1", + "@camunda/zeebe-element-templates-json-schema": "^0.19.1", "json-source-map": "^0.6.1", "min-dash": "^4.1.1" } @@ -10133,9 +10133,9 @@ } }, "@camunda/element-templates-json-schema": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.16.0.tgz", - "integrity": "sha512-QXlizHS2wh05JZJcVoJHd974SI/bWyGJxrgA8STmqMSUBnslDpCkaCTuxtW9YGsqImOkXKaSYI2843AbMOWBXQ==" + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.17.1.tgz", + "integrity": "sha512-lUiAprJuFaZ3p/7ybmhUBuKT+BlI54WiWei38e9vhg6Dujv37PIQA4wAy6DOv1TqvVSa0WgQpjJop16rfW/Lvw==" }, "@camunda/linting": { "version": "3.12.0", @@ -10184,9 +10184,9 @@ } }, "@camunda/zeebe-element-templates-json-schema": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.17.0.tgz", - "integrity": "sha512-iUGC1NdD/w9exO3Eap1d69EcH+uoff+YX1mswUJDqk6OqeDAfnHayClNdHR24VDctdHuwNrqOdGN+/1E/WsMow==" + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.19.1.tgz", + "integrity": "sha512-WjRdQyWwM8zahMEBI6PAo+zZrXbi6ECpfi+GKmkCHG4nojH9kp1xv7t2E0CdqOjTioNwqx/7j1N16lhaa3mBXw==" }, "@codemirror/autocomplete": { "version": "6.11.1", diff --git a/package.json b/package.json index 2e973c60..fd35b249 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ ], "license": "MIT", "dependencies": { - "@bpmn-io/element-templates-validator": "^1.7.0", + "@bpmn-io/element-templates-validator": "^2.0.0", "@bpmn-io/extract-process-variables": "^0.8.0", "bpmnlint": "^10.0.0", "classnames": "^2.3.1", diff --git a/src/cloud-element-templates/properties-panel/properties/CustomProperties.js b/src/cloud-element-templates/properties-panel/properties/CustomProperties.js deleted file mode 100644 index 557f9cef..00000000 --- a/src/cloud-element-templates/properties-panel/properties/CustomProperties.js +++ /dev/null @@ -1,475 +0,0 @@ -import { - find, - forEach, - groupBy -} from 'min-dash'; - -import { useService } from 'bpmn-js-properties-panel'; - -import { PropertyDescription } from '../../../components/PropertyDescription'; -import { PropertyTooltip } from '../components/PropertyTooltip'; - -import { getPropertyValue, setPropertyValue, validateProperty } from '../../util/propertyUtil'; - -import { - Group, - SelectEntry, isSelectEntryEdited, - CheckboxEntry, isCheckboxEntryEdited, - TextAreaEntry, isTextAreaEntryEdited, - TextFieldEntry, isTextFieldEntryEdited, - isFeelEntryEdited -} from '@bpmn-io/properties-panel'; - -import { - PROPERTY_TYPE, - ZEEBE_TASK_DEFINITION_TYPE_TYPE, - ZEEBE_TASK_DEFINITION, - ZEBBE_INPUT_TYPE, - ZEEBE_OUTPUT_TYPE, - ZEEBE_PROPERTY_TYPE, - ZEEBE_TASK_HEADER_TYPE -} from '../../util/bindingTypes'; - -import { - FeelEntryWithVariableContext, - FeelTextAreaEntryWithVariableContext, - FeelEntry, - FeelTextAreaEntry -} from '../../../entries/FeelEntryWithContext'; - - -const DEFAULT_CUSTOM_GROUP = { - id: 'ElementTemplates__CustomProperties', - label: 'Custom properties' -}; - - -export function CustomProperties(props) { - const { - element, - elementTemplate - } = props; - - const groups = []; - - const { - id, - properties, - groups: propertyGroups - } = elementTemplate; - - // (1) group properties by group id - const groupedProperties = groupByGroupId(properties); - const defaultProps = []; - - forEach(groupedProperties, (properties, groupId) => { - - const group = findCustomGroup(propertyGroups, groupId); - - if (!group) { - return defaultProps.push(...properties); - } - - addCustomGroup(groups, { - element, - id: `ElementTemplates__CustomProperties-${groupId}`, - label: group.label, - openByDefault: group.openByDefault, - properties: properties, - templateId: `${id}-${groupId}`, - tooltip: PropertyTooltip({ tooltip: group.tooltip }) - }); - }); - - // (2) add default custom props - if (defaultProps.length) { - addCustomGroup(groups, { - ...DEFAULT_CUSTOM_GROUP, - element, - properties: defaultProps, - templateId: id - }); - } - - return groups; -} - -function addCustomGroup(groups, props) { - - const { - element, - id, - label, - openByDefault = true, - properties, - templateId, - tooltip - } = props; - - const customPropertiesGroup = { - id, - label, - component: Group, - entries: [], - shouldOpen: openByDefault, - tooltip - }; - - properties.forEach((property, index) => { - const entry = createCustomEntry(`custom-entry-${ templateId }-${ index }`, element, property); - - if (entry) { - customPropertiesGroup.entries.push(entry); - } - }); - - if (customPropertiesGroup.entries.length) { - groups.push(customPropertiesGroup); - } -} - -function createCustomEntry(id, element, property) { - let { type, feel } = property; - - if (!type) { - type = getDefaultType(property); - } - - if (type === 'Boolean') { - return { - id, - component: BooleanProperty, - isEdited: isCheckboxEntryEdited, - property - }; - } - - if (type === 'Dropdown') { - return { - id, - component: DropdownProperty, - isEdited: isSelectEntryEdited, - property - }; - } - - if (type === 'String') { - if (feel) { - return { - id, - component: FeelProperty, - isEdited: isFeelEntryEdited, - property - }; - } - return { - id, - component: StringProperty, - isEdited: isTextFieldEntryEdited, - property - }; - } - - if (type === 'Text') { - if (feel) { - return { - id, - component: FeelTextAreaProperty, - isEdited: isFeelEntryEdited, - property - }; - } - return { - id, - component: TextAreaProperty, - isEdited: isTextAreaEntryEdited, - property - }; - } -} - -function getDefaultType(property) { - const { binding } = property; - - const { type } = binding; - - if ([ - PROPERTY_TYPE, - ZEEBE_TASK_DEFINITION_TYPE_TYPE, - ZEEBE_TASK_DEFINITION, - ZEBBE_INPUT_TYPE, - ZEEBE_OUTPUT_TYPE, - ZEEBE_PROPERTY_TYPE, - ZEEBE_TASK_HEADER_TYPE - ].includes(type)) { - return 'String'; - } -} - -function BooleanProperty(props) { - const { - element, - id, - property - } = props; - - const { - description, - editable, - label, - tooltip - } = property; - - const bpmnFactory = useService('bpmnFactory'), - commandStack = useService('commandStack'); - - return CheckboxEntry({ - element, - getValue: propertyGetter(element, property), - id, - label, - description: PropertyDescription({ description }), - setValue: propertySetter(bpmnFactory, commandStack, element, property), - disabled: editable === false, - tooltip: PropertyTooltip({ tooltip }) - }); -} - -function DropdownProperty(props) { - const { - element, - id, - property - } = props; - - const { - description, - editable, - label, - tooltip - } = property; - - const bpmnFactory = useService('bpmnFactory'), - commandStack = useService('commandStack'), - translate = useService('translate'); - - const getOptions = () => { - const { choices, optional } = property; - let dropdownOptions = []; - - dropdownOptions = choices.map(({ name, value }) => { - return { - label: name, - value - }; - }); - - if (optional) { - dropdownOptions = [ { label: '', value: undefined }, ...dropdownOptions ]; - } - - return dropdownOptions; - }; - - return SelectEntry({ - element, - id, - label, - getOptions, - description: PropertyDescription({ description }), - getValue: propertyGetter(element, property), - setValue: propertySetter(bpmnFactory, commandStack, element, property), - validate: propertyValidator(translate, property), - disabled: editable === false, - tooltip: PropertyTooltip({ tooltip }) - }); -} - -function FeelTextAreaProperty(props) { - const { - element, - id, - property - } = props; - - const { - description, - editable, - label, - feel, - tooltip - } = property; - - const bpmnFactory = useService('bpmnFactory'), - commandStack = useService('commandStack'), - debounce = useService('debounceInput'), - translate = useService('translate'); - - const TextAreaComponent = - !isExternalProperty(property) - ? FeelTextAreaEntryWithVariableContext - : FeelTextAreaEntry; - - return TextAreaComponent({ - debounce, - element, - getValue: propertyGetter(element, property), - id, - label, - feel, - description: PropertyDescription({ description }), - setValue: propertySetter(bpmnFactory, commandStack, element, property), - validate: propertyValidator(translate, property), - disabled: editable === false, - tooltip: PropertyTooltip({ tooltip }) - }); -} - -function FeelProperty(props) { - const { - element, - id, - property - } = props; - - const { - description, - editable, - label, - feel, - tooltip - } = property; - - const bpmnFactory = useService('bpmnFactory'), - commandStack = useService('commandStack'), - debounce = useService('debounceInput'), - translate = useService('translate'); - - const TextFieldComponent = - !isExternalProperty(property) - ? FeelEntryWithVariableContext - : FeelEntry; - - return TextFieldComponent({ - debounce, - element, - getValue: propertyGetter(element, property), - id, - label, - feel, - description: PropertyDescription({ description }), - setValue: propertySetter(bpmnFactory, commandStack, element, property), - validate: propertyValidator(translate, property), - disabled: editable === false, - tooltip: PropertyTooltip({ tooltip }) - }); -} - -function StringProperty(props) { - const { - element, - id, - property - } = props; - - const { - description, - editable, - label, - feel, - tooltip - } = property; - - const bpmnFactory = useService('bpmnFactory'), - commandStack = useService('commandStack'), - debounce = useService('debounceInput'), - translate = useService('translate'); - - return TextFieldEntry({ - debounce, - element, - getValue: propertyGetter(element, property), - id, - label, - feel, - description: PropertyDescription({ description }), - setValue: propertySetter(bpmnFactory, commandStack, element, property), - validate: propertyValidator(translate, property), - disabled: editable === false, - tooltip: PropertyTooltip({ tooltip }) - }); -} - -function TextAreaProperty(props) { - const { - element, - id, - property - } = props; - - const { - description, - editable, - label, - feel, - language, - tooltip - } = property; - - const bpmnFactory = useService('bpmnFactory'), - commandStack = useService('commandStack'), - debounce = useService('debounceInput'), - translate = useService('translate'); - - return TextAreaEntry({ - debounce, - element, - id, - label, - feel, - monospace: !!language, - autoResize: true, - description: PropertyDescription({ description }), - getValue: propertyGetter(element, property), - setValue: propertySetter(bpmnFactory, commandStack, element, property), - validate: propertyValidator(translate, property), - disabled: editable === false, - tooltip: PropertyTooltip({ tooltip }) - }); -} - -function propertyGetter(element, property) { - return function getValue() { - return getPropertyValue(element, property); - }; -} - -function propertySetter(bpmnFactory, commandStack, element, property) { - return function setValue(value) { - return setPropertyValue(bpmnFactory, commandStack, element, property, value); - }; -} - -function propertyValidator(translate, property) { - return value => validateProperty(value, property, translate); -} - - - -function groupByGroupId(properties) { - return groupBy(properties, 'group'); -} - -function findCustomGroup(groups, id) { - return find(groups, g => g.id === id); -} - -/** - * Is the given property executed by the engine? - * - * @param { { binding: { type: string } } } property - * @return {boolean} - */ -function isExternalProperty(property) { - return [ 'zeebe:property', 'zeebe:taskHeader' ].includes(property.binding.type); -} \ No newline at end of file diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/BooleanProperty.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/BooleanProperty.js new file mode 100644 index 00000000..0817fd44 --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/BooleanProperty.js @@ -0,0 +1,43 @@ +import { useService } from 'bpmn-js-properties-panel'; +import { PropertyDescription } from '../../../../components/PropertyDescription'; +import { PropertyTooltip } from '../../components/PropertyTooltip'; +import { CheckboxEntry, FeelCheckboxEntry } from '@bpmn-io/properties-panel'; +import { usePropertyAccessors } from './util'; + +export function BooleanProperty(props) { + const { + element, + id, + property + } = props; + + const { + description, + editable, + label, + tooltip, + feel + } = property; + + const bpmnFactory = useService('bpmnFactory'), + commandStack = useService('commandStack'), + debounce = useService('debounceInput'), + translate = useService('translate'); + + const Component = feel === 'optional' ? FeelCheckboxEntry : CheckboxEntry; + + const [ getValue, setValue ] = usePropertyAccessors(bpmnFactory, commandStack, element, property); + + return Component({ + element, + debounce, + translate, + getValue, + id, + label, + description: PropertyDescription({ description }), + setValue, + disabled: editable === false, + tooltip: PropertyTooltip({ tooltip }) + }); +} diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/DropdownProperty.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/DropdownProperty.js new file mode 100644 index 00000000..15c10707 --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/DropdownProperty.js @@ -0,0 +1,55 @@ +import { useService } from 'bpmn-js-properties-panel'; +import { PropertyDescription } from '../../../../components/PropertyDescription'; +import { PropertyTooltip } from '../../components/PropertyTooltip'; +import { SelectEntry } from '@bpmn-io/properties-panel'; +import { propertyGetter, propertySetter, propertyValidator } from './util'; + +export function DropdownProperty(props) { + const { + element, + id, + property + } = props; + + const { + description, + editable, + label, + tooltip + } = property; + + const bpmnFactory = useService('bpmnFactory'), + commandStack = useService('commandStack'), + translate = useService('translate'); + + const getOptions = () => { + const { choices, optional } = property; + let dropdownOptions = []; + + dropdownOptions = choices.map(({ name, value }) => { + return { + label: name, + value + }; + }); + + if (optional) { + dropdownOptions = [ { label: '', value: undefined }, ...dropdownOptions ]; + } + + return dropdownOptions; + }; + + return SelectEntry({ + element, + id, + label, + getOptions, + description: PropertyDescription({ description }), + getValue: propertyGetter(element, property), + setValue: propertySetter(bpmnFactory, commandStack, element, property), + validate: propertyValidator(translate, property), + disabled: editable === false, + tooltip: PropertyTooltip({ tooltip }) + }); +} diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/FeelProperty.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/FeelProperty.js new file mode 100644 index 00000000..558fe5e6 --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/FeelProperty.js @@ -0,0 +1,44 @@ +import { useService } from 'bpmn-js-properties-panel'; +import { PropertyDescription } from '../../../../components/PropertyDescription'; +import { PropertyTooltip } from '../../components/PropertyTooltip'; +import { FeelEntryWithVariableContext, FeelEntry } from '../../../../entries/FeelEntryWithContext'; +import { propertyGetter, propertySetter, propertyValidator, isExternalProperty } from './util'; + +export function FeelProperty(props) { + const { + element, + id, + property + } = props; + + const { + description, + editable, + label, + feel, + tooltip + } = property; + + const bpmnFactory = useService('bpmnFactory'), + commandStack = useService('commandStack'), + debounce = useService('debounceInput'), + translate = useService('translate'); + + const TextFieldComponent = !isExternalProperty(property) + ? FeelEntryWithVariableContext + : FeelEntry; + + return TextFieldComponent({ + debounce, + element, + getValue: propertyGetter(element, property), + id, + label, + feel, + description: PropertyDescription({ description }), + setValue: propertySetter(bpmnFactory, commandStack, element, property), + validate: propertyValidator(translate, property), + disabled: editable === false, + tooltip: PropertyTooltip({ tooltip }) + }); +} diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/FeelTextAreaProperty.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/FeelTextAreaProperty.js new file mode 100644 index 00000000..b3f72577 --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/FeelTextAreaProperty.js @@ -0,0 +1,47 @@ +import { useService } from 'bpmn-js-properties-panel'; +import { PropertyDescription } from '../../../../components/PropertyDescription'; +import { PropertyTooltip } from '../../components/PropertyTooltip'; +import { + FeelTextAreaEntryWithVariableContext, + FeelTextAreaEntry +} from '../../../../entries/FeelEntryWithContext'; +import { propertyGetter, propertySetter, propertyValidator, isExternalProperty } from './util'; + +export function FeelTextAreaProperty(props) { + const { + element, + id, + property + } = props; + + const { + description, + editable, + label, + feel, + tooltip + } = property; + + const bpmnFactory = useService('bpmnFactory'), + commandStack = useService('commandStack'), + debounce = useService('debounceInput'), + translate = useService('translate'); + + const TextAreaComponent = !isExternalProperty(property) + ? FeelTextAreaEntryWithVariableContext + : FeelTextAreaEntry; + + return TextAreaComponent({ + debounce, + element, + getValue: propertyGetter(element, property), + id, + label, + feel, + description: PropertyDescription({ description }), + setValue: propertySetter(bpmnFactory, commandStack, element, property), + validate: propertyValidator(translate, property), + disabled: editable === false, + tooltip: PropertyTooltip({ tooltip }) + }); +} diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/NumberProperty.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/NumberProperty.js new file mode 100644 index 00000000..3f83ef1d --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/NumberProperty.js @@ -0,0 +1,54 @@ +import { FeelNumberEntry, NumberFieldEntry } from '@bpmn-io/properties-panel'; +import { useService } from 'bpmn-js-properties-panel'; +import { isSpecialFeelProperty, propertyValidator, usePropertyAccessors } from './util'; +import { PropertyDescription } from '../../../../components/PropertyDescription'; +import { PropertyTooltip } from '../../components/PropertyTooltip'; +import { useCallback } from '@bpmn-io/properties-panel/preact/hooks'; +import { isNumber } from 'min-dash'; + +export function NumberProperty(props) { + const { + element, + id, + property + } = props; + + const { + description, + editable, + label, + feel, + tooltip + } = property; + + const Component = feel === 'optional' ? FeelNumberEntry : NumberFieldEntry; + + const bpmnFactory = useService('bpmnFactory'), + commandStack = useService('commandStack'), + debounce = useService('debounceInput'), + translate = useService('translate'); + + const [ getValue, setValue ] = usePropertyAccessors(bpmnFactory, commandStack, element, property); + + const validate = useCallback((value) => { + if (isSpecialFeelProperty(property) && isNumber(value) && value.toString().includes('e')) { + return translate('Scientific notation is disallowed in FEEL.'); + } + + const defaultValidator = propertyValidator(translate, property); + return defaultValidator(value); + }, [ translate, property ]); + + return Component({ + debounce, + element, + getValue, + id, + label, + description: PropertyDescription({ description }), + setValue, + validate: validate, + disabled: editable === false, + tooltip: PropertyTooltip({ tooltip }) + }); +} diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/StringProperty.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/StringProperty.js new file mode 100644 index 00000000..a46e3389 --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/StringProperty.js @@ -0,0 +1,40 @@ +import { useService } from 'bpmn-js-properties-panel'; +import { PropertyDescription } from '../../../../components/PropertyDescription'; +import { PropertyTooltip } from '../../components/PropertyTooltip'; +import { TextFieldEntry } from '@bpmn-io/properties-panel'; +import { propertyGetter, propertySetter, propertyValidator } from './util'; + +export function StringProperty(props) { + const { + element, + id, + property + } = props; + + const { + description, + editable, + label, + feel, + tooltip + } = property; + + const bpmnFactory = useService('bpmnFactory'), + commandStack = useService('commandStack'), + debounce = useService('debounceInput'), + translate = useService('translate'); + + return TextFieldEntry({ + debounce, + element, + getValue: propertyGetter(element, property), + id, + label, + feel, + description: PropertyDescription({ description }), + setValue: propertySetter(bpmnFactory, commandStack, element, property), + validate: propertyValidator(translate, property), + disabled: editable === false, + tooltip: PropertyTooltip({ tooltip }) + }); +} diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/TextAreaProperty.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/TextAreaProperty.js new file mode 100644 index 00000000..13eb500a --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/TextAreaProperty.js @@ -0,0 +1,47 @@ +import { useService } from 'bpmn-js-properties-panel'; +import { PropertyDescription } from '../../../../components/PropertyDescription'; +import { PropertyTooltip } from '../../components/PropertyTooltip'; +import { TextAreaEntry } from '@bpmn-io/properties-panel'; +import { + propertyGetter, + propertySetter, + propertyValidator +} from './util'; + +export function TextAreaProperty(props) { + const { + element, + id, + property + } = props; + + const { + description, + editable, + label, + feel, + language, + tooltip + } = property; + + const bpmnFactory = useService('bpmnFactory'), + commandStack = useService('commandStack'), + debounce = useService('debounceInput'), + translate = useService('translate'); + + return TextAreaEntry({ + debounce, + element, + id, + label, + feel, + monospace: !!language, + autoResize: true, + description: PropertyDescription({ description }), + getValue: propertyGetter(element, property), + setValue: propertySetter(bpmnFactory, commandStack, element, property), + validate: propertyValidator(translate, property), + disabled: editable === false, + tooltip: PropertyTooltip({ tooltip }) + }); +} diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/index.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/index.js new file mode 100644 index 00000000..cbbe6003 --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/index.js @@ -0,0 +1,222 @@ +import { forEach } from 'min-dash'; + + +import { PropertyTooltip } from '../../components/PropertyTooltip'; + +import { + Group, + isSelectEntryEdited, + isCheckboxEntryEdited, + isTextAreaEntryEdited, + isTextFieldEntryEdited, + isFeelEntryEdited, + isNumberFieldEntryEdited +} from '@bpmn-io/properties-panel'; + +import { + PROPERTY_TYPE, + ZEEBE_TASK_DEFINITION_TYPE_TYPE, + ZEEBE_TASK_DEFINITION, + ZEBBE_INPUT_TYPE, + ZEEBE_OUTPUT_TYPE, + ZEEBE_PROPERTY_TYPE, + ZEEBE_TASK_HEADER_TYPE +} from '../../../util/bindingTypes'; + +import { groupByGroupId, findCustomGroup } from './util'; +import { TextAreaProperty } from './TextAreaProperty'; +import { StringProperty } from './StringProperty'; +import { FeelProperty } from './FeelProperty'; +import { FeelTextAreaProperty } from './FeelTextAreaProperty'; +import { DropdownProperty } from './DropdownProperty'; +import { BooleanProperty } from './BooleanProperty'; +import { NumberProperty } from './NumberProperty'; + + +const DEFAULT_CUSTOM_GROUP = { + id: 'ElementTemplates__CustomProperties', + label: 'Custom properties' +}; + + +export function CustomProperties(props) { + const { + element, + elementTemplate + } = props; + + const groups = []; + + const { + id, + properties, + groups: propertyGroups + } = elementTemplate; + + // (1) group properties by group id + const groupedProperties = groupByGroupId(properties); + const defaultProps = []; + + forEach(groupedProperties, (properties, groupId) => { + + const group = findCustomGroup(propertyGroups, groupId); + + if (!group) { + return defaultProps.push(...properties); + } + + addCustomGroup(groups, { + element, + id: `ElementTemplates__CustomProperties-${groupId}`, + label: group.label, + openByDefault: group.openByDefault, + properties: properties, + templateId: `${id}-${groupId}`, + tooltip: PropertyTooltip({ tooltip: group.tooltip }) + }); + }); + + // (2) add default custom props + if (defaultProps.length) { + addCustomGroup(groups, { + ...DEFAULT_CUSTOM_GROUP, + element, + properties: defaultProps, + templateId: id + }); + } + + return groups; +} + +function addCustomGroup(groups, props) { + + const { + element, + id, + label, + openByDefault = true, + properties, + templateId, + tooltip + } = props; + + const customPropertiesGroup = { + id, + label, + component: Group, + entries: [], + shouldOpen: openByDefault, + tooltip + }; + + properties.forEach((property, index) => { + const entry = createCustomEntry(`custom-entry-${ templateId }-${ index }`, element, property); + + if (entry) { + customPropertiesGroup.entries.push(entry); + } + }); + + if (customPropertiesGroup.entries.length) { + groups.push(customPropertiesGroup); + } +} + +function createCustomEntry(id, element, property) { + let { type, feel } = property; + + if (!type) { + type = getDefaultType(property); + } + + if (feel === 'required') { + return { + id, + component: FeelProperty, + isEdited: isFeelEntryEdited, + property + }; + } + + if (type === 'Number') { + return { + id, + component: NumberProperty, + isEdited: isNumberFieldEntryEdited, + property + }; + } + + if (type === 'Boolean') { + return { + id, + component: BooleanProperty, + isEdited: isCheckboxEntryEdited, + property + }; + } + + if (type === 'Dropdown') { + return { + id, + component: DropdownProperty, + isEdited: isSelectEntryEdited, + property + }; + } + + if (type === 'String') { + if (feel) { + return { + id, + component: FeelProperty, + isEdited: isFeelEntryEdited, + property + }; + } + return { + id, + component: StringProperty, + isEdited: isTextFieldEntryEdited, + property + }; + } + + if (type === 'Text') { + if (feel) { + return { + id, + component: FeelTextAreaProperty, + isEdited: isFeelEntryEdited, + property + }; + } + return { + id, + component: TextAreaProperty, + isEdited: isTextAreaEntryEdited, + property + }; + } +} + +function getDefaultType(property) { + const { binding } = property; + + const { type } = binding; + + if ([ + PROPERTY_TYPE, + ZEEBE_TASK_DEFINITION_TYPE_TYPE, + ZEEBE_TASK_DEFINITION, + ZEBBE_INPUT_TYPE, + ZEEBE_OUTPUT_TYPE, + ZEEBE_PROPERTY_TYPE, + ZEEBE_TASK_HEADER_TYPE + ].includes(type)) { + return 'String'; + } +} + + diff --git a/src/cloud-element-templates/properties-panel/properties/custom-properties/util.js b/src/cloud-element-templates/properties-panel/properties/custom-properties/util.js new file mode 100644 index 00000000..da618037 --- /dev/null +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/util.js @@ -0,0 +1,131 @@ +import { find, groupBy } from 'min-dash'; +import { getPropertyValue, setPropertyValue, validateProperty } from '../../../util/propertyUtil'; +import { useCallback, useState } from '@bpmn-io/properties-panel/preact/hooks'; + +export function usePropertyAccessors(bpmnFactory, commandStack, element, property) { + const directSet = useCallback(propertySetter(bpmnFactory, commandStack, element, property), [ bpmnFactory, commandStack, element, property ]); + const directGet = useCallback(propertyGetter(element, property), [ element, property ]); + + const [ isFeelEnabled, setIsFeelEnabled ] = useState(feelEnabled(property, directGet())); + + const handleFeelToggle = useCallback((value) => { + if (!isFeelEnabled && typeof value === 'string' && value.startsWith('=')) { + setIsFeelEnabled(true); + } + + if (isFeelEnabled && (typeof value !== 'string' || !value.startsWith('='))) { + setIsFeelEnabled(false); + } + }, [ isFeelEnabled ]); + + const set = useCallback((value, error) => { + handleFeelToggle(value); + directSet(toFeelExpression(value, property.type)); + }, [ directSet, property, handleFeelToggle ]); + + const get = useCallback(() => { + if (isFeelEnabled) { + return directGet(); + } + + return fromFeelExpression(directGet(), property.type); + }, [ directGet, property, isFeelEnabled ]); + + if (!isSpecialFeelProperty(property)) { + return [ directGet, directSet ]; + } + + return [ get, set ]; +} + +export const isSpecialFeelProperty = (property) => { + return [ 'optional', 'static' ].includes(property.feel) && [ 'Boolean', 'Number' ].includes(property.type); +}; + +const toFeelExpression = (value, type) => { + if (typeof value === 'string' && value.startsWith('=')) { + return value; + } + + if (type === 'Boolean') { + value = value === 'false' ? false : value; + return '=' + !!value; + } + + if (typeof value === 'undefined') { + return value; + } + + return '=' + value.toString(); +}; + +const fromFeelExpression = (value, type) => { + if (typeof value === 'undefined') { + return value; + } + + if (typeof value === 'string' && value.startsWith('=')) { + value = value.slice(1); + } + + if (type === 'Number') { + return Number(value); + } + + if (type === 'Boolean') { + return value !== 'false'; + } + + return value; +}; + +const feelEnabled = (property, value) => { + if (!isSpecialFeelProperty(property)) { + return true; + } + + if (property.type === 'Boolean') { + return !(value === '=true' || value === '=false'); + } + + if (property.type === 'Number') { + return isNaN(fromFeelExpression(value, property.type)); + } + + return true; +}; + + +export function propertyGetter(element, property) { + return function getValue() { + return getPropertyValue(element, property); + }; +} + +export function propertySetter(bpmnFactory, commandStack, element, property) { + return function setValue(value) { + return setPropertyValue(bpmnFactory, commandStack, element, property, value); + }; +} + +export function propertyValidator(translate, property) { + return value => validateProperty(value, property, translate); +} + +export function groupByGroupId(properties) { + return groupBy(properties, 'group'); +} + +export function findCustomGroup(groups, id) { + return find(groups, g => g.id === id); +} + +/** + * Is the given property executed by the engine? + * + * @param { { binding: { type: string } } } property + * @return {boolean} + */ +export function isExternalProperty(property) { + return [ 'zeebe:property', 'zeebe:taskHeader' ].includes(property.binding.type); +} diff --git a/src/cloud-element-templates/properties-panel/properties/index.js b/src/cloud-element-templates/properties-panel/properties/index.js index e8dcfbba..8f5c595f 100644 --- a/src/cloud-element-templates/properties-panel/properties/index.js +++ b/src/cloud-element-templates/properties-panel/properties/index.js @@ -1,2 +1,2 @@ -export { CustomProperties } from './CustomProperties'; +export { CustomProperties } from './custom-properties'; export { MessageProps } from './MessageProps'; \ No newline at end of file diff --git a/src/element-templates/Validator.js b/src/element-templates/Validator.js index a0123152..5f31e7b7 100644 --- a/src/element-templates/Validator.js +++ b/src/element-templates/Validator.js @@ -266,7 +266,7 @@ export function getSchemaVersion(schemaUri) { export function filteredSchemaErrors(schemaErrors) { return filter(schemaErrors, (err) => { const { - dataPath, + instancePath, keyword } = err; @@ -277,7 +277,7 @@ export function filteredSchemaErrors(schemaErrors) { // (2) data type errors // ignore type errors nested in scopes - if (keyword === 'type' && dataPath && !dataPath.startsWith('/scopes/')) { + if (keyword === 'type' && instancePath && !instancePath.startsWith('/scopes/')) { return true; } diff --git a/test/spec/cloud-element-templates/Validator.spec.js b/test/spec/cloud-element-templates/Validator.spec.js index 1191853a..c63f4be6 100644 --- a/test/spec/cloud-element-templates/Validator.spec.js +++ b/test/spec/cloud-element-templates/Validator.spec.js @@ -428,7 +428,7 @@ describe('provider/cloud-element-templates - Validator', function() { templates.addAll(templateDescriptor); // then - expect(errors(templates)).to.contain('template(id: , name: ): feel is only supported for "String" and "Text" type'); + expect(errors(templates)).to.contain('template(id: , name: ): feel is only supported for "String", "Text", "Number" and "Boolean" type'); expect(valid(templates)).to.be.empty; }); diff --git a/test/spec/cloud-element-templates/fixtures/complex.json b/test/spec/cloud-element-templates/fixtures/complex.json index df37c46f..4bfc2771 100644 --- a/test/spec/cloud-element-templates/fixtures/complex.json +++ b/test/spec/cloud-element-templates/fixtures/complex.json @@ -1615,5 +1615,46 @@ } } ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "Boolean and Number", + "id": "com.zeebe.example.boolean.and.number", + "description": "Shows a Boolean and number field as optional FEEL fields", + "appliesTo": [ + "bpmn:Task" + ], + "properties": [ + { + "label": "Cardholder", + "value": "Jon Doe", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "name" + } + }, + { + "label": "Amount", + "value": "=10", + "type": "Number", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "Amount" + } + }, + { + "label": "Capture", + "value": "=false", + "type": "Boolean", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "capture" + } + } + ] } ] \ No newline at end of file diff --git a/test/spec/cloud-element-templates/properties/BooleanProperty.bpmn b/test/spec/cloud-element-templates/properties/BooleanProperty.bpmn new file mode 100644 index 00000000..cf50833e --- /dev/null +++ b/test/spec/cloud-element-templates/properties/BooleanProperty.bpmn @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/cloud-element-templates/properties/BooleanProperty.json b/test/spec/cloud-element-templates/properties/BooleanProperty.json new file mode 100644 index 00000000..6a25e288 --- /dev/null +++ b/test/spec/cloud-element-templates/properties/BooleanProperty.json @@ -0,0 +1,77 @@ +[ + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL disabled", + "id": "booleanField.feel.disabled", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "BooleanProperty", + "type": "Boolean", + "binding": { + "type": "zeebe:property", + "name": "BooleanProperty" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL required", + "id": "booleanField.feel.required", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "BooleanProperty", + "type": "Boolean", + "feel": "required", + "binding": { + "type": "zeebe:property", + "name": "BooleanProperty" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL optional", + "id": "booleanField.feel.optional", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "BooleanProperty", + "type": "Boolean", + "feel": "optional", + "binding": { + "type": "zeebe:property", + "name": "BooleanProperty" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL static", + "id": "booleanField.feel.static", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "BooleanProperty", + "type": "Boolean", + "feel": "static", + "binding": { + "type": "zeebe:property", + "name": "BooleanProperty" + } + } + ] + } +] diff --git a/test/spec/cloud-element-templates/properties/BooleanProperty.spec.js b/test/spec/cloud-element-templates/properties/BooleanProperty.spec.js new file mode 100644 index 00000000..6f7031ad --- /dev/null +++ b/test/spec/cloud-element-templates/properties/BooleanProperty.spec.js @@ -0,0 +1,277 @@ +import TestContainer from 'mocha-test-container-support'; + +import { + bootstrapPropertiesPanel, + getBpmnJS +} from 'test/TestHelper'; + +import { + act, + fireEvent +} from '@testing-library/preact'; + + +import { + query as domQuery +} from 'min-dom'; + +import { + findExtension, + findZeebeProperty +} from 'src/cloud-element-templates/Helper'; + +import coreModule from 'bpmn-js/lib/core'; +import modelingModule from 'bpmn-js/lib/features/modeling'; +import zeebeModdlePackage from 'zeebe-bpmn-moddle/resources/zeebe'; + +import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; + +import { BpmnPropertiesPanelModule as BpmnPropertiesPanel } from 'bpmn-js-properties-panel';import elementTemplatesModule from 'src/cloud-element-templates'; + +import diagramXML from './BooleanProperty.bpmn'; +import templates from './BooleanProperty.json'; + + +describe('provider/cloud-element-templates - BooleanProperty', function() { + + let container; + + beforeEach(function() { + container = TestContainer.get(this); + }); + + beforeEach(bootstrapPropertiesPanel(diagramXML, { + container, + debounceInput: false, + elementTemplates: templates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + describe('feel disabled', function() { + + let entry, input; + + beforeEach(async function() { + await expectSelected('disabled'); + entry = findEntry('custom-entry-booleanField.feel.disabled-0', container); + input = findInput('checkbox', entry); + }); + + it('should render boolean field', async function() { + + // then + expect(input).to.exist; + }); + + + it('should be editable', async function() { + + // when + await input.click(); + + // then + expectZeebeProperty('disabled', 'BooleanProperty', true); + }); + + }); + + + describe('feel required', function() { + + let entry, input; + + beforeEach(async function() { + await expectSelected('required'); + entry = findEntry('custom-entry-booleanField.feel.required-0', container); + input = domQuery('.bio-properties-panel-feel-editor-container', entry); + }); + + it('should render as FEEL field', async function() { + + // then + expect(input).to.exist; + + }); + + }); + + + describe('feel static', function() { + + let entry, input; + + beforeEach(async function() { + await expectSelected('static'); + entry = findEntry('custom-entry-booleanField.feel.static-0', container); + input = findInput('checkbox', entry); + }); + + it('should render boolean field', async function() { + + // then + expect(input).to.exist; + }); + + + it('should cast to FEEL expression', async function() { + + // when + await input.click(); + + // then + expectZeebeProperty('static', 'BooleanProperty', '=true'); + }); + + }); + + + describe('feel optional', function() { + + let entry, input, toggle; + + beforeEach(async function() { + await expectSelected('optional'); + entry = findEntry('custom-entry-booleanField.feel.optional-0', container); + input = findInput('checkbox', entry); + toggle = domQuery('button.bio-properties-panel-feel-icon.optional', entry); + }); + + + describe('feel disabled', function() { + + it('should render boolean field', async function() { + + // then + expect(input).to.exist; + expect(toggle).to.exist; + }); + + + it('should cast to FEEL expression', async function() { + + // when + await input.click(); + + // then + expectZeebeProperty('optional', 'BooleanProperty', '=true'); + }); + + }); + + + describe('feel enabled', function() { + + it('should toggle to FEEL field', async function() { + + // when + await act(() => { + toggle.click(); + }); + + // then + expect(domQuery('.bio-properties-panel-feel-entry', container)).to.exist; + }); + + + it('should revert to boolean field on re-select', async function() { + + // given + await act(() => { + fireEvent.click(toggle); + }); + + // assume + expect(domQuery('.bio-properties-panel-feel-entry', container)).to.exist; + + // when + await expectSelected('required'); + await expectSelected('optional'); + + // then + entry = findEntry('custom-entry-booleanField.feel.optional-0', container); + expect(findInput('checkbox', entry)).to.exist; + + }); + + + it('should stay expression re-select', async function() { + + // given + await act(() => { + fireEvent.click(toggle); + }); + + const input = domQuery('[role="textbox"]', entry); + + // assume + expect(domQuery('.bio-properties-panel-feel-editor-container', entry)).to.exist; + + // when + input.textContent = 'foo'; + + await expectSelected('required'); + await expectSelected('optional'); + + // then + entry = findEntry('custom-entry-booleanField.feel.optional-0', container); + expect(findInput('checkbox', entry)).not.to.exist; + }); + + }); + + + }); + +}); + + +// helpers ////////// + + +function expectZeebeProperty(id, name, value) { + return getBpmnJS().invoke(function(elementRegistry) { + const element = elementRegistry.get(id); + + const bo = getBusinessObject(element); + + const zeebeProperties = findExtension(bo, 'zeebe:Properties'), + zeebeProperty = findZeebeProperty(zeebeProperties, { name }); + + expect(zeebeProperty).to.exist; + expect(zeebeProperty.value).to.eql(value); + }); +} + +function expectSelected(id) { + return getBpmnJS().invoke(async function(elementRegistry, selection) { + const element = elementRegistry.get(id); + + await act(() => { + selection.select(element); + }); + + return element; + }); +} + + +function findEntry(id, container) { + expect(container).to.not.be.null; + + return domQuery(`[data-entry-id='${ id }']`, container); +} + +function findInput(type, container) { + expect(container).to.not.be.null; + + return domQuery(`input[type='${ type }']`, container); +} diff --git a/test/spec/cloud-element-templates/properties/CustomProperties.spec copy.js b/test/spec/cloud-element-templates/properties/CustomProperties.spec copy.js new file mode 100644 index 00000000..1efd063f --- /dev/null +++ b/test/spec/cloud-element-templates/properties/CustomProperties.spec copy.js @@ -0,0 +1,2247 @@ +import TestContainer from 'mocha-test-container-support'; + +import { + bootstrapPropertiesPanel, + changeInput, + getBpmnJS, + withPropertiesPanel, + inject +} from 'test/TestHelper'; + +import { + act, + cleanup, + fireEvent +} from '@testing-library/preact'; + +import { + map +} from 'min-dash'; + +import { + classes as domClasses, + query as domQuery, + queryAll as domQueryAll +} from 'min-dom'; + +import { + findExtension, + findInputParameter, + findMessage, + findOutputParameter, + findTaskHeader, + findZeebeProperty, + findZeebeSubscription +} from 'src/cloud-element-templates/Helper'; + +import coreModule from 'bpmn-js/lib/core'; +import modelingModule from 'bpmn-js/lib/features/modeling'; +import zeebeModdlePackage from 'zeebe-bpmn-moddle/resources/zeebe'; + +import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; + +import { BpmnPropertiesPanelModule as BpmnPropertiesPanel } from 'bpmn-js-properties-panel';import elementTemplatesModule from 'src/cloud-element-templates'; + +import diagramXML from './CustomProperties.bpmn'; +import templates from './CustomProperties.json'; + +import descriptionDiagramXML from './CustomProperties.description.bpmn'; +import descriptionElementTemplates from './CustomProperties.description.json'; + +import tooltipDiagramXML from './CustomProperties.tooltip.bpmn'; +import tooltipElementTemplates from './CustomProperties.tooltip.json'; + +import editableDiagramXML from './CustomProperties.editable.bpmn'; +import editableElementTemplates from './CustomProperties.editable.json'; + +import feelDiagramXML from './CustomProperties.feel.bpmn'; +import feelElementTemplates from './CustomProperties.feel.json'; + +import defaultTypesDiagramXML from './CustomProperties.default-types.bpmn'; +import defaultTypesElementTemplates from './CustomProperties.default-types.json'; + +import defaultValuesDiagramXML from './CustomProperties.default-values.bpmn'; +import defaultValuesElementTemplates from './CustomProperties.default-values.json'; + +import groupsDiagramXML from './CustomProperties.groups.bpmn'; +import groupsElementTemplates from './CustomProperties.groups.json'; + +import textLanguageDiagramXML from './CustomProperties.text-language.bpmn'; +import textLanguageElementTemplates from './CustomProperties.text-language.json'; + + +describe('provider/cloud-element-templates - CustomProperties', function() { + + let container; + + beforeEach(function() { + container = TestContainer.get(this); + }); + + beforeEach(bootstrapPropertiesPanel(diagramXML, { + container, + debounceInput: false, + elementTemplates: templates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + describe('property', function() { + + it('should display', async function() { + + // when + await expectSelected('Task_1'); + + // then + const entry = findEntry('custom-entry-my.example.template-0', container); + + expect(entry).to.exist; + + const input = findInput('text', entry); + + expect(input).to.exist; + expect(input.value).to.equal('My task'); + }); + + + it('should change', async function() { + + // given + const task = await expectSelected('Task_1'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-my.example.template-0', container), + input = findInput('text', entry); + + changeInput(input, 'foo'); + + // then + expect(input.value).to.equal('foo'); + expect(businessObject.get('name')).to.equal('foo'); + }); + + + it('should change String property to empty string when erased', async function() { + + // given + const task = await expectSelected('Task_1'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-my.example.template-0', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + expect(input.value).to.eql(''); + expect(businessObject.get('name')).to.be.eql(''); + }); + + }); + + + describe('zeebe:taskDefinition:type', function() { + + it('should display', async function() { + + // when + await expectSelected('RestTask'); + + // then + const entry = findEntry('custom-entry-com.example.rest-0', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('task-type'); + }); + + + it('should NOT display (type=hidden)', async function() { + + // when + await expectSelected('RestTask_hidden'); + + // then + const entry = findEntry('custom-entry-com.example.rest-hidden-0', container); + + expect(entry).to.not.exist; + }); + + + it('should change, setting zeebe:TaskDefinition#type (plain)', inject(async function() { + + // given + const task = await expectSelected('RestTask'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-0', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const taskDefinition = findExtension(businessObject, 'zeebe:TaskDefinition'); + + expect(taskDefinition).to.exist; + expect(taskDefinition).to.jsonEqual({ + $type: 'zeebe:TaskDefinition', + type: 'foo@bar' + }); + })); + + + it('should change, creating zeebe:TaskDefinition if non-existing', async function() { + + // given + const task = await expectSelected('RestTask_noData'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-0', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const taskDefinition = findExtension(businessObject, 'zeebe:TaskDefinition'); + + // then + expect(taskDefinition).to.exist; + expect(taskDefinition).to.jsonEqual({ + $type: 'zeebe:TaskDefinition', + type: 'foo@bar' + }); + }); + + }); + + + describe('zeebe:taskDefinition', function() { + + it('should display', async function() { + + // when + await expectSelected('TaskDefinition'); + + // then + const entry = findEntry('custom-entry-taskDefinitionTemplate-0', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('http'); + }); + + + it('should change value', async function() { + + // given + const task = await expectSelected('TaskDefinition'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-taskDefinitionTemplate-0', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const taskDefinition = findExtension(businessObject, 'zeebe:TaskDefinition'); + + // then + expect(taskDefinition).to.exist; + expect(taskDefinition).to.jsonEqual({ + $type: 'zeebe:TaskDefinition', + type: 'foo@bar', + retries: '5' + }); + }); + + }); + + + describe('zeebe:input', function() { + + it('should display', async function() { + + // when + await expectSelected('RestTask'); + + // then + const entry = findEntry('custom-entry-com.example.rest-3', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('input-1-source'); + }); + + + it('should display empty (optional)', async function() { + + // when + await expectSelected('RestTask_optional'); + + // then + const entry = findEntry('custom-entry-com.example.rest-optional-2', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal(''); + }); + + + it('should change, setting zeebe:Input (plain)', inject(async function() { + + // given + const task = await expectSelected('RestTask'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-3', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const ioMapping = findExtension(businessObject, 'zeebe:IoMapping'), + inputParameter = findInputParameter(ioMapping, { name: 'input-1-target' }); + + expect(inputParameter).to.exist; + expect(inputParameter).to.jsonEqual({ + $type: 'zeebe:Input', + source: 'foo@bar', + target: 'input-1-target' + }); + })); + + + it('should change, creating zeebe:Input if non-existing', async function() { + + // given + const task = await expectSelected('RestTask_noData'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-3', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const ioMapping = findExtension(businessObject, 'zeebe:IoMapping'), + inputParameter = findInputParameter(ioMapping, { name: 'input-1-target' }); + + // then + expect(inputParameter).to.exist; + expect(inputParameter).to.jsonEqual({ + $type: 'zeebe:Input', + source: 'foo@bar', + target: 'input-1-target' + }); + }); + + + it('should keep input (non optional)', inject(async function() { + + // given + const task = await expectSelected('RestTask_optional'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-optional-0', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const ioMapping = findExtension(businessObject, 'zeebe:IoMapping'), + inputParameter = findInputParameter(ioMapping, { name: 'input-1-target' }); + + expect(inputParameter).to.exist; + expect(inputParameter).to.jsonEqual({ + $type: 'zeebe:Input', + source: undefined, + target: 'input-1-target' + }); + })); + + + it('should not keep input (optional)', inject(async function() { + + // given + const task = await expectSelected('RestTask_optional'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-optional-1', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const ioMapping = findExtension(businessObject, 'zeebe:IoMapping'), + inputParameter = findInputParameter(ioMapping, { name: 'input-2-target' }); + + expect(inputParameter).to.not.exist; + })); + + }); + + + describe('zeebe:output', function() { + + it('should display', async function() { + + // when + await expectSelected('RestTask'); + + // then + const entry = findEntry('custom-entry-com.example.rest-5', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('output-1-target'); + }); + + + it('should display empty (optional)', async function() { + + // when + await expectSelected('RestTask_optional'); + + // then + const entry = findEntry('custom-entry-com.example.rest-optional-5', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal(''); + }); + + + it('should change, setting zeebe:Output (plain)', inject(async function() { + + // given + const task = await expectSelected('RestTask'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-5', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const ioMapping = findExtension(businessObject, 'zeebe:IoMapping'), + outputParameter = findOutputParameter(ioMapping, { source: 'output-1-source' }); + + expect(outputParameter).to.exist; + expect(outputParameter).to.jsonEqual({ + $type: 'zeebe:Output', + source: 'output-1-source', + target: 'foo@bar' + }); + })); + + + it('should change, creating zeebe:Output if non-existing', async function() { + + // given + const task = await expectSelected('RestTask_noData'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-5', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const ioMapping = findExtension(businessObject, 'zeebe:IoMapping'), + outputParameter = findOutputParameter(ioMapping, { source: 'output-1-source' }); + + // then + expect(outputParameter).to.exist; + expect(outputParameter).to.jsonEqual({ + $type: 'zeebe:Output', + source: 'output-1-source', + target: 'foo@bar' + }); + }); + + + it('should keep output (non optional)', inject(async function() { + + // given + const task = await expectSelected('RestTask_optional'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-optional-3', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const ioMapping = findExtension(businessObject, 'zeebe:IoMapping'), + outputParameter = findOutputParameter(ioMapping, { source: 'output-1-source' }); + + expect(outputParameter).to.exist; + expect(outputParameter).to.jsonEqual({ + $type: 'zeebe:Output', + source: 'output-1-source', + target: undefined + }); + })); + + + it('should NOT keep output (optional)', inject(async function() { + + // given + const task = await expectSelected('RestTask_optional'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-optional-4', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const ioMapping = findExtension(businessObject, 'zeebe:IoMapping'), + outputParameter = findOutputParameter(ioMapping, { source: 'output-2-source' }); + + expect(outputParameter).to.not.exist; + })); + + }); + + + describe('zeebe:taskHeader', function() { + + it('should display', async function() { + + // when + await expectSelected('RestTask'); + + // then + const entry = findEntry('custom-entry-com.example.rest-1', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('header-1-value'); + }); + + + it('should change, setting zeebe:Header (plain)', inject(async function() { + + // given + const task = await expectSelected('RestTask'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-1', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const taskHeaders = findExtension(businessObject, 'zeebe:TaskHeaders'), + header = findTaskHeader(taskHeaders, { key: 'header-1-key' }); + + expect(header).to.exist; + expect(header).to.jsonEqual({ + $type: 'zeebe:Header', + key: 'header-1-key', + value: 'foo@bar' + }); + })); + + + it('should change, creating zeebe:Header if non-existing', async function() { + + // given + const task = await expectSelected('RestTask_noData'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-1', container), + input = findInput('text', entry); + + changeInput(input, 'foo@bar'); + + // then + const taskHeaders = findExtension(businessObject, 'zeebe:TaskHeaders'), + header = findTaskHeader(taskHeaders, { key: 'header-1-key' }); + + // then + expect(header).to.exist; + expect(header).to.jsonEqual({ + $type: 'zeebe:Header', + key: 'header-1-key', + value: 'foo@bar' + }); + }); + + + it('should remove if empty value', inject(async function() { + + // given + const task = await expectSelected('RestTask'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-1', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const taskHeaders = findExtension(businessObject, 'zeebe:TaskHeaders'), + header = findTaskHeader(taskHeaders, { key: 'header-1-key' }); + + expect(header).not.to.exist; + })); + + }); + + + describe('zeebe:property', function() { + + it('should display', async function() { + + // when + await expectSelected('RestTask'); + + // then + const entry = findEntry('custom-entry-com.example.rest-7', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('property-1-value'); + }); + + + it('should change, setting zeebe:Property (plain)', inject(async function() { + + // given + const task = await expectSelected('RestTask'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-7', container), + input = findInput('text', entry); + + changeInput(input, 'property-1-changed-value'); + + // then + const zeebeProperties = findExtension(businessObject, 'zeebe:Properties'), + zeebeProperty = findZeebeProperty(zeebeProperties, { name: 'property-1-name' }); + + expect(zeebeProperty).to.exist; + expect(zeebeProperty).to.jsonEqual({ + $type: 'zeebe:Property', + name: 'property-1-name', + value: 'property-1-changed-value' + }); + })); + + + it('should change, creating zeebe:Property if non-existing', async function() { + + // given + const task = await expectSelected('RestTask_noData'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-7', container), + input = findInput('text', entry); + + changeInput(input, 'property-1-changed-value'); + + // then + const zeebeProperties = findExtension(businessObject, 'zeebe:Properties'), + zeebeProperty = findZeebeProperty(zeebeProperties, { name: 'property-1-name' }); + + // then + expect(zeebeProperty).to.exist; + expect(zeebeProperty).to.jsonEqual({ + $type: 'zeebe:Property', + name: 'property-1-name', + value: 'property-1-changed-value' + }); + }); + + + it('should keep property (non optional)', inject(async function() { + + // given + const task = await expectSelected('RestTask'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-7', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const zeebeProperties = findExtension(businessObject, 'zeebe:Properties'), + zeebeProperty = findZeebeProperty(zeebeProperties, { name: 'property-1-name' }); + + expect(zeebeProperty).to.exist; + expect(zeebeProperty).to.jsonEqual({ + $type: 'zeebe:Property', + name: 'property-1-name', + value: '' + }); + })); + + + it('should not keep property (optional)', inject(async function() { + + // given + const task = await expectSelected('RestTask_optional'), + businessObject = getBusinessObject(task); + + // when + const entry = findEntry('custom-entry-com.example.rest-optional-8', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const zeebeProperties = findExtension(businessObject, 'zeebe:Properties'), + zeebeProperty = findZeebeProperty(zeebeProperties, { name: 'property-3-name' }); + + // then + expect(zeebeProperty).not.to.exist; + })); + + }); + + + describe('bpmn:Message#property', function() { + + + it('should display', async function() { + + // when + await expectSelected('MessageEvent'); + + // then + const entry = findEntry('custom-entry-messageEventTemplate-0', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('name'); + }); + + + it('should NOT display (type=hidden)', async function() { + + // when + await expectSelected('MessageEvent_hidden'); + + // then + const entry = findEntry('custom-entry-messageEventTemplate_hidden-0', container); + + expect(entry).to.not.exist; + }); + + + it('should change, setting bpmn:Message#property (plain)', async function() { + + // given + const event = await expectSelected('MessageEvent'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-0', container), + input = findInput('text', entry); + + changeInput(input, 'meaningfulMessageName'); + + // then + const message = findMessage(businessObject); + + expect(message).to.exist; + expect(message).to.have.property('name', 'meaningfulMessageName'); + }); + + + it('should change, creating bpmn:Message if non-existing', async function() { + + // given + const event = await expectSelected('MessageEvent_noData'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-0', container), + input = findInput('text', entry); + + changeInput(input, 'meaningfulMessageName'); + + // then + const message = findMessage(businessObject); + + // then + expect(message).to.exist; + expect(message).to.have.property('name', 'meaningfulMessageName'); + }); + + + it('should NOT remove bpmn:Message when changed to empty value', inject(async function() { + + // given + const event = await expectSelected('MessageEvent'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-0', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const message = findMessage(businessObject); + + expect(message).to.exist; + expect(message).to.have.property('name', ''); + })); + + }); + + + describe('bpmn:Message#zeebe:subscription#property', function() { + + + it('should display', async function() { + + // when + await expectSelected('MessageEvent'); + + // then + const entry = findEntry('custom-entry-messageEventTemplate-1', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('correlationKey'); + }); + + + it('should NOT display (type=hidden)', async function() { + + // when + await expectSelected('MessageEvent_hidden'); + + // then + const entry = findEntry('custom-entry-messageEventTemplate_hidden-1', container); + + expect(entry).to.not.exist; + }); + + + it('should change, setting zeebe:subscription#property (plain)', async function() { + + // given + const event = await expectSelected('MessageEvent'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-1', container), + input = findInput('text', entry); + + changeInput(input, 'meaningfulCorrelationKey'); + + // then + const message = findMessage(businessObject); + const subscription = findZeebeSubscription(message); + + expect(subscription).to.exist; + expect(subscription).to.have.property('correlationKey', 'meaningfulCorrelationKey'); + }); + + + it('should change, creating bpmn:Message if non-existing', async function() { + + // given + const event = await expectSelected('MessageEvent_noData'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-1', container), + input = findInput('text', entry); + + changeInput(input, 'meaningfulCorrelationKey'); + + // then + const message = findMessage(businessObject); + const subscription = findZeebeSubscription(message); + + // then + expect(subscription).to.exist; + expect(subscription).to.have.property('correlationKey', 'meaningfulCorrelationKey'); + }); + + + it('should NOT remove zeebe:subscription when changed to empty value', inject(async function() { + + // given + const event = await expectSelected('MessageEvent'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-messageEventTemplate-1', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const message = findMessage(businessObject); + const subscription = findZeebeSubscription(message); + + expect(subscription).to.exist; + expect(subscription).to.have.property('correlationKey', ''); + })); + + }); + + + describe('zeebe:calledElement', function() { + + + it('should display', async function() { + + // when + await expectSelected('CalledElement'); + + // then + const entry = findEntry('custom-entry-calledElement-0', container), + input = findInput('text', entry); + + expect(entry).to.exist; + expect(input).to.exist; + expect(input.value).to.equal('paymentProcess'); + }); + + + it('should change, setting zeebe:calledElement', async function() { + + // given + const element = await expectSelected('CalledElement'), + businessObject = getBusinessObject(element); + + // when + const entry = findEntry('custom-entry-calledElement-0', container), + input = findInput('text', entry); + + changeInput(input, 'anotherProcessId'); + + // then + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.have.property('processId', 'anotherProcessId'); + }); + + + it('should change, creating zeebe:calledElement if non-existing', async function() { + + // given + const element = await expectSelected('CalledElement_empty'), + businessObject = getBusinessObject(element); + + // when + const entry = findEntry('custom-entry-calledElement-0', container), + input = findInput('text', entry); + + changeInput(input, 'Called Element'); + + // then + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.have.property('processId', 'paymentProcess'); + }); + + + it('should NOT remove zeebe:calledElement when changed to empty value', inject(async function() { + + // given + const event = await expectSelected('CalledElement'), + businessObject = getBusinessObject(event); + + // when + const entry = findEntry('custom-entry-calledElement-0', container), + input = findInput('text', entry); + + changeInput(input, ''); + + // then + const calledElement = findExtension(businessObject, 'zeebe:CalledElement'); + + expect(calledElement).to.exist; + expect(calledElement).to.have.property('processId', ''); + })); + }); + + + describe('types', function() { + + describe('Dropdown', function() { + + beforeEach(bootstrapPropertiesPanel(diagramXML, { + container, + debounceInput: false, + elementTemplates: templates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + it('should display options', async function() { + + // when + await expectSelected('DropdownTask'); + + // then + const entry = findEntry('custom-entry-my.example.dropdown-0', container), + options = domQueryAll('select option', entry); + + expect(Array.from(options).map(({ selected, value }) => { + return { + selected, + value + }; + })).to.eql([ + { value: 'low', selected: true }, + { value: 'medium', selected: false }, + { value: 'high', selected: false } + ]); + }); + + + it('should display options - optional', async function() { + + // when + await expectSelected('OptionalDropdownTask'); + + // then + const entry = findEntry('custom-entry-my.example.dropdown-2-0', container), + options = domQueryAll('select option', entry); + + expect(Array.from(options).map(({ selected, value }) => { + return { + selected, + value + }; + })).to.eql([ + { value: '', selected: false }, + { value: 'low', selected: true }, + { value: 'medium', selected: false }, + { value: 'high', selected: false } + ]); + }); + + + it('should display options (no visual selection)', async function() { + + // when + await expectSelected('DropdownNoSelection'); + + // then + const entry = findEntry('custom-entry-my.example.dropdown-1-0', container), + options = domQueryAll('select option', entry); + + expect(Array.from(options).map(({ selected, value }) => { + return { + selected, + value + }; + })).to.eql([ + { value: 'low', selected: false }, + { value: 'medium', selected: false }, + { value: 'high', selected: false } + ]); + }); + + + it('should change, updating binding', async function() { + + // given + const task = await expectSelected('DropdownTask'), + businessObject = getBusinessObject(task); + + const entry = findEntry('custom-entry-my.example.dropdown-0', container), + select = findSelect(entry); + + // when + changeInput(select, 'medium'); + + // then + expect(businessObject.get('name')).to.equal('medium'); + }); + + }); + + + describe('Text', function() { + + beforeEach(bootstrapPropertiesPanel(textLanguageDiagramXML, { + container, + debounceInput: false, + elementTemplates: textLanguageElementTemplates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + it('should display annotated with monospace font', async function() { + + // when + await expectSelected('textTask'); + + // then + const entry = findEntry('custom-entry-my.example.custom-language-text-0', container); + const input = findTextarea(entry); + + expect(input.className).to.include('bio-properties-panel-input-monospace'); + }); + + + withPropertiesPanel('>=1.3.0')('should be auto-resizable', async function() { + + // when + await expectSelected('textTask'); + + // then + const entry = findEntry('custom-entry-my.example.custom-language-text-0', container); + const input = findTextarea(entry); + + expect(input.className).to.include('auto-resize'); + }); + + }); + + }); + + + describe('description', function() { + + beforeEach(bootstrapPropertiesPanel(descriptionDiagramXML, { + container, + debounceInput: false, + elementTemplates: descriptionElementTemplates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + it('should display description for string property', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.description-0', container); + + expect(entry.textContent).to.contain('STRING_DESCRIPTION'); + }); + + + it('should display description for textarea property', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.description-1', container); + + expect(entry.textContent).to.contain('TEXT_DESCRIPTION'); + }); + + + it('should display description for boolean property', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.description-2', container); + + expect(entry.textContent).to.contain('BOOLEAN_DESCRIPTION'); + }); + + + it('should display description for dropdown property', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.description-3', container); + + expect(entry.textContent).to.contain('DROPDOWN_DESCRIPTION'); + }); + + + it('should display HTML descriptions', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.description-4', container); + const description = domQuery('.bio-properties-panel-description', entry); + + expect(description).to.exist; + expect(description.innerHTML).to.eql( + '
' + + '
' + + 'By the way, you can use ' + + 'freemarker templates ' + + 'here' + + '
' + + '
' + ); + }); + + + it('should NOT display empty descriptions', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.description-5', container); + const description = domQuery('.bio-properties-panel-description', entry); + + expect(description).to.not.exist; + }); + }); + + + describe('tooltip', function() { + + let clock; + + function openTooltip(element) { + return act(() => { + fireEvent.mouseEnter(element); + clock.tick(200); + }); + } + + beforeEach(function() { + clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + cleanup(); + clock.restore(); + }); + + beforeEach(bootstrapPropertiesPanel(tooltipDiagramXML, { + container, + debounceInput: false, + elementTemplates: tooltipElementTemplates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + it('should display tooltip for string property', async function() { + + // given + await expectSelected('Task'); + + const entry = findEntry('custom-entry-com.zeebe.example.tooltip-group-0', container); + const tooltipWrapper = domQuery('.bio-properties-panel-tooltip-wrapper', entry); + + // when + await openTooltip(tooltipWrapper); + const tooltip = domQuery('.bio-properties-panel-tooltip', entry); + + // then + expect(tooltip).to.exist; + expect(tooltip.textContent).to.contain('STRING_TOOLTIP'); + }); + + + it('should display tooltip for textarea property', async function() { + + // when + await expectSelected('Task'); + + const entry = findEntry('custom-entry-com.zeebe.example.tooltip-group-1', container); + const tooltipWrapper = domQuery('.bio-properties-panel-tooltip-wrapper', entry); + + // then + await openTooltip(tooltipWrapper); + const tooltip = domQuery('.bio-properties-panel-tooltip', entry); + + // then + expect(tooltip).to.exist; + expect(tooltip.textContent).to.contain('TEXT_TOOLTIP'); + }); + + + it('should display tooltip for boolean property', async function() { + + // given + await expectSelected('Task'); + + const entry = findEntry('custom-entry-com.zeebe.example.tooltip-group-2', container); + const tooltipWrapper = domQuery('.bio-properties-panel-tooltip-wrapper', entry); + + // when + await openTooltip(tooltipWrapper); + const tooltip = domQuery('.bio-properties-panel-tooltip', entry); + + // then + expect(tooltip).to.exist; + expect(tooltip.textContent).to.contain('BOOLEAN_TOOLTIP'); + }); + + + it('should display tooltip for dropdown property', async function() { + + // given + await expectSelected('Task'); + + const entry = findEntry('custom-entry-com.zeebe.example.tooltip-group-3', container); + const tooltipWrapper = domQuery('.bio-properties-panel-tooltip-wrapper', entry); + + // when + await openTooltip(tooltipWrapper); + const tooltip = domQuery('.bio-properties-panel-tooltip', entry); + + // then + expect(tooltip).to.exist; + expect(tooltip.textContent).to.contain('DROPDOWN_TOOLTIP'); + }); + + + it('should display tooltip for groups', async function() { + + // given + await expectSelected('Task'); + + const group = domQuery('.bio-properties-panel-group-header-title[title="Custom group"]', container); + const tooltipWrapper = domQuery('.bio-properties-panel-tooltip-wrapper', group); + + // when + await openTooltip(tooltipWrapper); + const tooltip = domQuery('.bio-properties-panel-tooltip'); + + // then + expect(tooltip).to.exist; + expect(tooltip.textContent).to.contain('GROUP_TOOLTIP'); + }); + + + it('should display HTML tooltips', async function() { + + // given + await expectSelected('Task'); + + const entry = findEntry('custom-entry-com.zeebe.example.tooltip-group-4', container); + const tooltipWrapper = domQuery('.bio-properties-panel-tooltip-wrapper', entry); + + // when + await openTooltip(tooltipWrapper); + const tooltip = domQuery('.bio-properties-panel-tooltip-content', entry); + + // then + expect(tooltip).to.exist; + expect(tooltip.innerHTML).to.eql( + '
' + + '
' + + 'By the way, you can use ' + + 'freemarker templates ' + + 'here' + + '
' + + '
' + ); + }); + + + it('should NOT display empty descriptions', async function() { + + // given + await expectSelected('Task'); + + const entry = findEntry('custom-entry-com.zeebe.example.tooltip-group-5', container); + const tooltipWrapper = domQuery('.bio-properties-panel-tooltip-wrapper', entry); + + // then + expect(tooltipWrapper).to.not.exist; + }); + }); + + + describe('editable', function() { + + beforeEach(bootstrapPropertiesPanel(editableDiagramXML, { + container, + debounceInput: false, + elementTemplates: editableElementTemplates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + it('should NOT disable input when editable is NOT set', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.editable-4', container), + input = findInput('text', entry); + + expect(input).not.to.have.property('disabled', true); + }); + + + it('should NOT disable input when editable=true', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.editable-5', container), + input = findInput('text', entry); + + expect(input).not.to.have.property('disabled', true); + }); + + + it('should disable string input when editable=false', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.editable-0', container), + input = findInput('text', entry); + + expect(input).to.have.property('disabled', true); + }); + + + it('should disable textarea input when editable=false', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.editable-1', container), + input = findTextarea(entry); + + expect(input).to.have.property('disabled', true); + }); + + + it('should disable boolean input when editable=false', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.editable-2', container), + input = findInput('checkbox', entry); + + expect(input).to.have.property('disabled', true); + }); + + + it('should disable dropdown input when editable=false', async function() { + + // when + await expectSelected('Task'); + + // then + const entry = findEntry('custom-entry-com.zeebe.example.editable-3', container), + input = findSelect(entry); + + expect(input).to.have.property('disabled', true); + }); + }); + + + describe('validation', function() { + + [ + [ 'String', 'input' ], + [ 'Select', 'select' ], + [ 'TextArea', 'textarea' ] + ].forEach(function([ name, selector ]) { + + describe(name, function() { + + it('should validate nonEmpty', async function() { + + // given + await expectSelected('ValidateTask'); + + const entry = findEntry(`custom-entry-com.validated-inputs.Task-${selector}-0`, container), + input = domQuery(selector, entry); + + // assume + expectError(entry, `${name} - NotEmpty must not be empty.`); + + // when + changeInput(input, 'FOO'); + + // then + expectValid(entry); + }); + + + it('should validate minLength', async function() { + + // given + await expectSelected('ValidateTask'); + + const entry = findEntry(`custom-entry-com.validated-inputs.Task-${selector}-1`, container), + input = domQuery(selector, entry); + + // assume + expectError(entry, `${name} - MinLength must have min length 5.`); + + // when + changeInput(input, 'FOOOOOOO'); + + // then + expectValid(entry); + }); + + + it('should validate maxLength', async function() { + + // given + await expectSelected('ValidateTask'); + + const entry = findEntry(`custom-entry-com.validated-inputs.Task-${selector}-2`, container), + input = domQuery(selector, entry); + + // assume + expectValid(entry); + + // when + changeInput(input, 'FOOOOOOO'); + + // then + expectError(entry, `${name} - MaxLength must have max length 5.`); + }); + + + it('should validate pattern (String)', async function() { + + // given + await expectSelected('ValidateTask'); + + const entry = findEntry(`custom-entry-com.validated-inputs.Task-${selector}-3`, container), + input = domQuery(selector, entry); + + // assume + expectError(entry, `${name} - Pattern (String) must match pattern A+B.`); + + // when + changeInput(input, 'AAAB'); + + // then + expectValid(entry); + }); + + + it('should validate pattern (String + Message)', async function() { + + // given + await expectSelected('ValidateTask'); + + const entry = findEntry(`custom-entry-com.validated-inputs.Task-${selector}-4`, container), + input = domQuery(selector, entry); + + // assume + expectError(entry, `${name} - Pattern (String + Message) Must start with https://`); + + // when + changeInput(input, 'https://'); + + // then + expectValid(entry); + }); + + + it('should validate pattern (Integer)', async function() { + + // given + await expectSelected('ValidateTask'); + + const entry = findEntry(`custom-entry-com.validated-inputs.Task-${selector}-5`, container), + input = domQuery(selector, entry); + + // assume + expectError(entry, `${name} - Pattern (Integer) Must be positive integer`); + + // when + changeInput(input, '20'); + + // then + expectValid(entry); + }); + + }); + + }); + + + it('should work with conditional properties', inject( + async function(elementTemplates, elementRegistry) { + + // given + await expectSelected('ValidatedConditionalTask'); + const task = elementRegistry.get('ValidatedConditionalTask'); + const template = templates.find(t => t.id === 'com.validated-inputs-conditional.Task'); + + // when + await act(() => { + elementTemplates.applyTemplate(task, template); + }); + + // then + const entry = findEntry('custom-entry-com.validated-inputs-conditional.Task-authentication-1', container), + input = domQuery('input', entry); + expectError(entry, 'Bearer token must not be empty.'); + + // and when + changeInput(input, '123456'); + + // then + expectValid(entry); + }) + ); + }); + + + describe('default-types', function() { + + beforeEach(bootstrapPropertiesPanel(defaultTypesDiagramXML, { + container, + debounceInput: false, + elementTemplates: defaultTypesElementTemplates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + it('should display String as default - property', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-0', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + }); + + + it('should display String as default - zeebe:taskDefinition:type', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-1', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + }); + + + it('should display String as default - zeebe:taskHeader', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-2', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + }); + + + it('should display String as default - zeebe:input', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-3', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + }); + + + it('should display String as default - zeebe:output', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-4', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + }); + + + it('should display String as default - zeebe:property', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-5', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + }); + + }); + + + describe('default values', function() { + + beforeEach(bootstrapPropertiesPanel(defaultValuesDiagramXML, { + container, + debounceInput: false, + elementTemplates: defaultValuesElementTemplates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + it('should display empty String - property', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-0', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + expect(input.value).to.eql(''); + }); + + + it('should display String as default - zeebe:taskDefinition:type', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-1', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + expect(input.value).to.eql(''); + }); + + + it('should display String as default - zeebe:taskHeader', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-2', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + expect(input.value).to.eql(''); + }); + + + it('should display String as default - zeebe:input', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-3', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + expect(input.value).to.eql(''); + }); + + + it('should display String as default - zeebe:output', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-4', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + expect(input.value).to.eql(''); + }); + + + it('should display String as default - zeebe:property', async function() { + + // given + await expectSelected('RestTask'); + + const entry = findEntry('custom-entry-com.example.default-types-5', container), + input = findInput('text', entry); + + // then + expect(input).to.exist; + expect(input.value).to.eql(''); + }); + + }); + + + describe('grouping', function() { + + beforeEach(bootstrapPropertiesPanel(groupsDiagramXML, { + container, + debounceInput: false, + elementTemplates: groupsElementTemplates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + it('should create defined groups', async function() { + + // given + await expectSelected('ServiceTask_1'); + + // when + const groups = getGroupIds(container); + + // then + expect(groups).to.contain('ElementTemplates__CustomProperties-headers'); + expect(groups).to.contain('ElementTemplates__CustomProperties-payload'); + expect(groups).to.contain('ElementTemplates__CustomProperties-mapping'); + expect(groups).to.contain('ElementTemplates__CustomProperties'); + }); + + + it('should open custom groups by default', async function() { + + // given + await expectSelected('ServiceTask_groupsCollapsed'); + + // when + var customGroups = [ + [ getGroupById('ElementTemplates__CustomProperties-collapsed', container), false ], + [ getGroupById('ElementTemplates__CustomProperties-open', container), true ], + [ getGroupById('ElementTemplates__CustomProperties-unspecified', container), true ], + [ getGroupById('ElementTemplates__CustomProperties', container),true ] + ]; + + // then + customGroups.forEach(function([ group, open ]) { + expectGroupOpen(group, open); + }); + + }); + + + it('should display in defined properties order', async function() { + + // given + await expectSelected('ServiceTask_1'); + + // when + const groups = getGroupIds(container); + + // then + expect(groups).to.eql([ + 'ElementTemplates__Template', + 'ElementTemplates__CustomProperties-headers', + 'ElementTemplates__CustomProperties-payload', + 'ElementTemplates__CustomProperties-mapping', + 'ElementTemplates__CustomProperties', + ]); + }); + + + it('should not create defined group (no entries)', async function() { + + // given + await expectSelected('ServiceTask_noEntries'); + + // when + const groups = getGroupIds(container); + + // then + expect(groups).to.not.contain('ElementTemplates__CustomProperties-headers'); + }); + + + it('should only create default group', async function() { + + // given + await expectSelected('ServiceTask_noGroups'); + + // when + const groups = getGroupIds(container); + + // then + expect(groups).to.eql([ + 'ElementTemplates__Template', + 'ElementTemplates__CustomProperties' + ]); + }); + + + it('should open default group', async function() { + + // given + await expectSelected('ServiceTask_noGroups'); + + // when + var tempalteGroup = getGroupById('ElementTemplates__Template', container); + var customPropertiesGroup = getGroupById('ElementTemplates__CustomProperties', container); + + + // then + expectGroupOpen(tempalteGroup, false); + expectGroupOpen(customPropertiesGroup, true); + }); + + + it('should not create default group', async function() { + + // given + await expectSelected('ServiceTask_noDefault'); + + // when + const groups = getGroupIds(container); + + // then + expect(groups).to.not.contain('ElementTemplates__CustomProperties'); + }); + + + it('should position into defined groups', async function() { + + // given + await expectSelected('ServiceTask_1'); + + // when + const entry1 = findEntry('custom-entry-example.com.grouping-headers-0', container); + const entry2 = findEntry('custom-entry-example.com.grouping-payload-0', container); + const entry3 = findEntry('custom-entry-example.com.grouping-mapping-0', container); + + // then + expect(getGroup(entry1)).to.equal('ElementTemplates__CustomProperties-headers'); + expect(getGroup(entry2)).to.equal('ElementTemplates__CustomProperties-payload'); + expect(getGroup(entry3)).to.equal('ElementTemplates__CustomProperties-mapping'); + }); + + + it('should position into default group (empty group id)', async function() { + + // given + await expectSelected('ServiceTask_1'); + + // when + const entry = findEntry('custom-entry-example.com.grouping-0', container); + + // then + expect(getGroup(entry)).to.equal('ElementTemplates__CustomProperties'); + }); + + + it('should position into default group (non existing group)', async function() { + + // given + await expectSelected('ServiceTask_nonExisting'); + + // when + const entry = findEntry('custom-entry-example.com.grouping-nonExisting-0', container); + + // then + expect(getGroup(entry)).to.equal('ElementTemplates__CustomProperties'); + }); + + }); + + + describe('feel', function() { + + beforeEach(bootstrapPropertiesPanel(feelDiagramXML, { + container, + debounceInput: false, + elementTemplates: feelElementTemplates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + describe('TextField', function() { + + it('should not display icon by default', async function() { + + // when + await expectSelected('stringTask'); + + // then + const entry = findEntry('custom-entry-my.custom.FeelTask.String-2', container); + + const feelIcon = domQuery('.bio-properties-panel-feel-icon', entry); + + expect(feelIcon).not.to.exist; + + }); + + + it('should display icons', async function() { + + // when + await expectSelected('stringTask'); + + // then + const requiredEntry = findEntry('custom-entry-my.custom.FeelTask.String-0', container); + const optionalEntry = findEntry('custom-entry-my.custom.FeelTask.String-1', container); + + const requiredIcon = domQuery('.bio-properties-panel-feel-icon', requiredEntry); + const optionalIcon = domQuery('.bio-properties-panel-feel-icon', optionalEntry); + + expect(requiredIcon).to.exist; + expect(optionalIcon).to.exist; + }); + + }); + + + describe('TextArea', function() { + + it('should not display icon by default', async function() { + + // when + await expectSelected('textTask'); + + // then + const entry = findEntry('custom-entry-my.custom.FeelTask.Text-2', container); + + const feelIcon = domQuery('.bio-properties-panel-feel-icon', entry); + + expect(feelIcon).not.to.exist; + + }); + + + it('should display icons on TextArea', async function() { + + // when + await expectSelected('textTask'); + + // then + const requiredEntry = findEntry('custom-entry-my.custom.FeelTask.Text-0', container); + const optionalEntry = findEntry('custom-entry-my.custom.FeelTask.Text-1', container); + + const requiredIcon = domQuery('.bio-properties-panel-feel-icon', requiredEntry); + const optionalIcon = domQuery('.bio-properties-panel-feel-icon', optionalEntry); + + expect(requiredIcon).to.exist; + expect(optionalIcon).to.exist; + }); + + }); + + }); + +}); + + +// helpers ////////// + +function expectSelected(id) { + return getBpmnJS().invoke(async function(elementRegistry, selection) { + const element = elementRegistry.get(id); + + await act(() => { + selection.select(element); + }); + + return element; + }); +} + +function expectError(entry, message) { + expect(entry).to.not.be.null; + + const errorMessage = domQuery('.bio-properties-panel-error', entry); + + const error = errorMessage && errorMessage.textContent; + + expect(error).to.equal(message); +} + +function expectGroupOpen(group, open) { + expect(group).to.not.be.null; + + const entries = domQuery('.bio-properties-panel-group-entries', group); + + expect(domClasses(entries).contains('open')).to.eql(open); +} + +function expectValid(entry) { + expectError(entry, null); +} + +function getGroupIds(container) { + expect(container).to.not.be.null; + + const groups = domQueryAll('[data-group-id]', container); + + const groupIds = map(groups, group => withoutPrefix(group.dataset.groupId)); + + return groupIds; +} + +function getGroup(entry) { + const parent = entry.closest('[data-group-id]'); + + return parent && withoutPrefix(parent.dataset.groupId); +} + +function getGroupById(id, container) { + expect(container).to.not.be.null; + + const group = domQuery( + `[data-group-id=group-${id}]`, + container + ); + + return group; +} + +function withoutPrefix(groupId) { + return groupId.slice(6); +} + +function findEntry(id, container) { + expect(container).to.not.be.null; + + return domQuery(`[data-entry-id='${ id }']`, container); +} + +function findInput(type, container) { + expect(container).to.not.be.null; + + return domQuery(`input[type='${ type }']`, container); +} + +function findSelect(container) { + expect(container).to.not.be.null; + + return domQuery('select', container); +} + +function findTextarea(container) { + expect(container).to.not.be.null; + + return domQuery('textarea', container); +} \ No newline at end of file diff --git a/test/spec/cloud-element-templates/properties/NumberProperty.bpmn b/test/spec/cloud-element-templates/properties/NumberProperty.bpmn new file mode 100644 index 00000000..ac56ac70 --- /dev/null +++ b/test/spec/cloud-element-templates/properties/NumberProperty.bpmn @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/spec/cloud-element-templates/properties/NumberProperty.json b/test/spec/cloud-element-templates/properties/NumberProperty.json new file mode 100644 index 00000000..2cbbd511 --- /dev/null +++ b/test/spec/cloud-element-templates/properties/NumberProperty.json @@ -0,0 +1,77 @@ +[ + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL disabled", + "id": "numberField.feel.disabled", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "NumberProperty", + "type": "Number", + "binding": { + "type": "zeebe:property", + "name": "NumberProperty" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL required", + "id": "numberField.feel.required", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "NumberProperty", + "type": "Number", + "feel": "required", + "binding": { + "type": "zeebe:property", + "name": "NumberProperty" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL optional", + "id": "numberField.feel.optional", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "NumberProperty", + "type": "Number", + "feel": "optional", + "binding": { + "type": "zeebe:property", + "name": "NumberProperty" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL static", + "id": "numberField.feel.static", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "NumberProperty", + "type": "Number", + "feel": "static", + "binding": { + "type": "zeebe:property", + "name": "NumberProperty" + } + } + ] + } +] diff --git a/test/spec/cloud-element-templates/properties/NumberProperty.spec.js b/test/spec/cloud-element-templates/properties/NumberProperty.spec.js new file mode 100644 index 00000000..ef48c5aa --- /dev/null +++ b/test/spec/cloud-element-templates/properties/NumberProperty.spec.js @@ -0,0 +1,318 @@ +import TestContainer from 'mocha-test-container-support'; + +import { + bootstrapPropertiesPanel, + changeInput, + getBpmnJS +} from 'test/TestHelper'; + +import { + act, + fireEvent +} from '@testing-library/preact'; + + +import { + query as domQuery +} from 'min-dom'; + +import { + findExtension, + findZeebeProperty +} from 'src/cloud-element-templates/Helper'; + +import coreModule from 'bpmn-js/lib/core'; +import modelingModule from 'bpmn-js/lib/features/modeling'; +import zeebeModdlePackage from 'zeebe-bpmn-moddle/resources/zeebe'; + +import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; + +import { BpmnPropertiesPanelModule as BpmnPropertiesPanel } from 'bpmn-js-properties-panel';import elementTemplatesModule from 'src/cloud-element-templates'; + +import diagramXML from './NumberProperty.bpmn'; +import templates from './NumberProperty.json'; + + +describe('provider/cloud-element-templates - NumberProperty', function() { + + let container; + + beforeEach(function() { + container = TestContainer.get(this); + }); + + beforeEach(bootstrapPropertiesPanel(diagramXML, { + container, + debounceInput: false, + elementTemplates: templates, + moddleExtensions: { + zeebe: zeebeModdlePackage + }, + modules: [ + BpmnPropertiesPanel, + coreModule, + elementTemplatesModule, + modelingModule + ] + })); + + + describe('feel disabled', function() { + + let entry, input; + + beforeEach(async function() { + await expectSelected('disabled'); + entry = findEntry('custom-entry-numberField.feel.disabled-0', container); + input = findInput('number', entry); + }); + + it('should render number field', async function() { + + // then + expect(input).to.exist; + }); + + + it('should be editable', async function() { + + // when + await changeInput(input, '123'); + + // then + expectZeebeProperty('disabled', 'NumberProperty', 123); + }); + + + it('should accept scientific notation', async function() { + + // when + await changeInput(input, '1.23e100'); + + // then + const errorMessage = domQuery('.bio-properties-panel-error', entry); + expect(errorMessage).not.to.exist; + }); + + }); + + + describe('feel required', function() { + + let entry, input; + + beforeEach(async function() { + await expectSelected('required'); + entry = findEntry('custom-entry-numberField.feel.required-0', container); + input = domQuery('.bio-properties-panel-feel-editor-container', entry); + }); + + it('should render as FEEL field', async function() { + + // then + expect(input).to.exist; + + }); + + }); + + + describe('feel static', function() { + + let entry, input; + + beforeEach(async function() { + await expectSelected('static'); + entry = findEntry('custom-entry-numberField.feel.static-0', container); + input = findInput('number', entry); + }); + + it('should render number field', async function() { + + // then + expect(input).to.exist; + }); + + + it('should cast to FEEL expression', async function() { + + // when + await changeInput(input, '123'); + + // then + expectZeebeProperty('static', 'NumberProperty', '=123'); + }); + + + it('should not accept scientific notation', async function() { + + // when + await changeInput(input, '1.23e100'); + + // then + expectError(entry, 'Scientific notation is disallowed in FEEL.'); + }); + + }); + + + describe('feel optional', function() { + + let entry, input, toggle; + + beforeEach(async function() { + await expectSelected('optional'); + entry = findEntry('custom-entry-numberField.feel.optional-0', container); + input = findInput('number', entry); + toggle = domQuery('button.bio-properties-panel-feel-icon.optional', entry); + }); + + + describe('feel disabled', function() { + + it('should render number field', async function() { + + // then + expect(input).to.exist; + expect(toggle).to.exist; + }); + + + it('should cast to FEEL expression', async function() { + + // when + await changeInput(input, '123'); + + // then + expectZeebeProperty('optional', 'NumberProperty', '=123'); + }); + + + it('should not accept scientific notation', async function() { + + // when + await changeInput(input, '1.23e100'); + + // then + expectError(entry, 'Scientific notation is disallowed in FEEL.'); + }); + + }); + + + describe('feel enabled', function() { + + it('should toggle to FEEL field', async function() { + + // when + await act(() => { + fireEvent.click(toggle); + }); + + // then + expect(domQuery('.bio-properties-panel-feel-editor-container', entry)).to.exist; + }); + + + it('should revert to number field on re-select', async function() { + + // given + await act(() => { + fireEvent.click(toggle); + }); + + // assume + expect(domQuery('.bio-properties-panel-feel-editor-container', entry)).to.exist; + + // when + await expectSelected('required'); + await expectSelected('optional'); + + // then + entry = findEntry('custom-entry-numberField.feel.optional-0', container); + expect(findInput('number', entry)).to.exist; + + }); + + + it('should stay expression re-select', async function() { + + // given + await act(() => { + fireEvent.click(toggle); + }); + + const input = domQuery('[role="textbox"]', entry); + + // assume + expect(domQuery('.bio-properties-panel-feel-editor-container', entry)).to.exist; + + // when + input.textContent = 'foo'; + + await expectSelected('required'); + await expectSelected('optional'); + + // then + entry = findEntry('custom-entry-numberField.feel.optional-0', container); + expect(findInput('number', entry)).not.to.exist; + }); + + }); + + + }); + +}); + + +// helpers ////////// + + +function expectZeebeProperty(id, name, value) { + return getBpmnJS().invoke(function(elementRegistry) { + const element = elementRegistry.get(id); + + const bo = getBusinessObject(element); + + const zeebeProperties = findExtension(bo, 'zeebe:Properties'), + zeebeProperty = findZeebeProperty(zeebeProperties, { name }); + + expect(zeebeProperty).to.exist; + expect(zeebeProperty.value).to.eql(value); + }); +} + +function expectSelected(id) { + return getBpmnJS().invoke(async function(elementRegistry, selection) { + const element = elementRegistry.get(id); + + await act(() => { + selection.select(element); + }); + + return element; + }); +} + +function expectError(entry, message) { + expect(entry).to.not.be.null; + + const errorMessage = domQuery('.bio-properties-panel-error', entry); + + const error = errorMessage && errorMessage.textContent; + + expect(error).to.equal(message); +} + +function findEntry(id, container) { + expect(container).to.not.be.null; + + return domQuery(`[data-entry-id='${ id }']`, container); +} + +function findInput(type, container) { + expect(container).to.not.be.null; + + return domQuery(`input[type='${ type }']`, container); +} diff --git a/test/spec/element-templates/Validator.spec.js b/test/spec/element-templates/Validator.spec.js index 71a3df1b..a8a5726a 100644 --- a/test/spec/element-templates/Validator.spec.js +++ b/test/spec/element-templates/Validator.spec.js @@ -570,7 +570,7 @@ describe('provider/element-templates - Validator', function() { templates.addAll(templateDescriptor); // then - expect(errors(templates)).to.contain('template(id: , name: ): should be array'); + expect(errors(templates)).to.contain('template(id: , name: ): must be array'); expect(valid(templates)).to.be.empty; });