From 846b0f0135d2887dca24c51927f3c10fda8d12c2 Mon Sep 17 00:00:00 2001 From: Maciej Barelkowski Date: Wed, 29 Jan 2025 10:33:42 +0100 Subject: [PATCH] fix: make `feel` default to `static` for inputs and outputs deps: update to `@bpmn-io/element-templates-validator@2.3.3` --- CHANGELOG.md | 9 +++ package-lock.json | 30 ++++---- package.json | 2 +- src/cloud-element-templates/Helper.js | 4 +- .../custom-properties/NumberProperty.js | 4 +- .../properties/custom-properties/util.js | 6 +- src/cloud-element-templates/util/FeelUtil.js | 26 ++++++- .../properties/BooleanProperty.bpmn | 5 ++ .../properties/BooleanProperty.json | 26 +++++++ .../properties/BooleanProperty.spec.js | 73 +++++++++++++++++++ .../properties/NumberProperty.bpmn | 5 ++ .../properties/NumberProperty.json | 26 +++++++ .../properties/NumberProperty.spec.js | 73 +++++++++++++++++++ 13 files changed, 264 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b7f2d1..5d3e19c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ All notable changes to [bpmn-js-element-templates](https://github.com/bpmn-io/bp ___Note:__ Yet to be released changes appear here._ +## 2.5.2 + +* `FIX`: make `feel` default value `static` for inputs and outputs +* `DEPS`: update to `@bpmn-io/element-templates-validator@2.3.3` + +### Note + +This release reverts the breaking changes introduced via [camunda/element-templates-json-schema#156](https://github.com/camunda/element-templates-json-schema/pull/156). Any `feel` value out of the supported enum is allowed, but `static` is used if the property is missing. + ## 2.5.1 * `FIX`: require `feel` to be `optional` or `static` for `Boolean` and `Number` inputs and outputs ([camunda/element-templates-json-schema#156](https://github.com/camunda/element-templates-json-schema/pull/156)) diff --git a/package-lock.json b/package-lock.json index ac00f2c6..02ec9d7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.5.1", "license": "MIT", "dependencies": { - "@bpmn-io/element-templates-validator": "^2.3.2", + "@bpmn-io/element-templates-validator": "^2.3.3", "@bpmn-io/extract-process-variables": "^1.0.0", "bpmnlint": "^11.0.0", "classnames": "^2.3.1", @@ -484,13 +484,13 @@ } }, "node_modules/@bpmn-io/element-templates-validator": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.3.2.tgz", - "integrity": "sha512-Gr6pNSoFif2Sgd01+NdPVVLGrz4R/AICHbK29zBcF849RDH+iLq7qAUhbT8MwwUKOs9WHztXoDTEiKbF85lx5g==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.3.3.tgz", + "integrity": "sha512-Q39lxuUA1T7pCkKiy40EAncr7MLVITq/P1MRDPHpFUz2S1Yvt5aZ4IdyKBpATlffcF5s6UfnRY7bq8qE+e2byw==", "license": "MIT", "dependencies": { "@camunda/element-templates-json-schema": "^0.18.1", - "@camunda/zeebe-element-templates-json-schema": "^0.22.2", + "@camunda/zeebe-element-templates-json-schema": "^0.22.3", "json-source-map": "^0.6.1", "min-dash": "^4.1.1" } @@ -670,9 +670,9 @@ } }, "node_modules/@camunda/zeebe-element-templates-json-schema": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.22.2.tgz", - "integrity": "sha512-qKUa64twO5Ewh6rN+z0n1cdTweuKYuwPCZH6VL7knsdfSYe4PBLnx8FwTXS6Hc5LZCP60rp+XXgQ5puQZfqlNQ==", + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.22.3.tgz", + "integrity": "sha512-22D9ZAkBounIpQ0DKQ5lmkGGE7LZM7WCwLBez8AT0hGTHVve5egUCrh3f7QIm5DkdIHxyWdibD0zRIWUhDBrYg==", "license": "MIT" }, "node_modules/@codemirror/autocomplete": { @@ -10596,12 +10596,12 @@ } }, "@bpmn-io/element-templates-validator": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.3.2.tgz", - "integrity": "sha512-Gr6pNSoFif2Sgd01+NdPVVLGrz4R/AICHbK29zBcF849RDH+iLq7qAUhbT8MwwUKOs9WHztXoDTEiKbF85lx5g==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@bpmn-io/element-templates-validator/-/element-templates-validator-2.3.3.tgz", + "integrity": "sha512-Q39lxuUA1T7pCkKiy40EAncr7MLVITq/P1MRDPHpFUz2S1Yvt5aZ4IdyKBpATlffcF5s6UfnRY7bq8qE+e2byw==", "requires": { "@camunda/element-templates-json-schema": "^0.18.1", - "@camunda/zeebe-element-templates-json-schema": "^0.22.2", + "@camunda/zeebe-element-templates-json-schema": "^0.22.3", "json-source-map": "^0.6.1", "min-dash": "^4.1.1" } @@ -10752,9 +10752,9 @@ } }, "@camunda/zeebe-element-templates-json-schema": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.22.2.tgz", - "integrity": "sha512-qKUa64twO5Ewh6rN+z0n1cdTweuKYuwPCZH6VL7knsdfSYe4PBLnx8FwTXS6Hc5LZCP60rp+XXgQ5puQZfqlNQ==" + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@camunda/zeebe-element-templates-json-schema/-/zeebe-element-templates-json-schema-0.22.3.tgz", + "integrity": "sha512-22D9ZAkBounIpQ0DKQ5lmkGGE7LZM7WCwLBez8AT0hGTHVve5egUCrh3f7QIm5DkdIHxyWdibD0zRIWUhDBrYg==" }, "@codemirror/autocomplete": { "version": "6.17.0", diff --git a/package.json b/package.json index 169fa2ec..5a58b2ba 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ ], "license": "MIT", "dependencies": { - "@bpmn-io/element-templates-validator": "^2.3.2", + "@bpmn-io/element-templates-validator": "^2.3.3", "@bpmn-io/extract-process-variables": "^1.0.0", "bpmnlint": "^11.0.0", "classnames": "^2.3.1", diff --git a/src/cloud-element-templates/Helper.js b/src/cloud-element-templates/Helper.js index c8857154..58907aea 100644 --- a/src/cloud-element-templates/Helper.js +++ b/src/cloud-element-templates/Helper.js @@ -3,7 +3,7 @@ import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; import { is, isAny } from 'bpmn-js/lib/util/ModelUtil'; import { v4 as uuid } from 'uuid'; -import { isSpecialFeelProperty, toFeelExpression } from './util/FeelUtil'; +import { shouldCastToFeel, toFeelExpression } from './util/FeelUtil'; /** * The BPMN 2.0 extension attribute name under @@ -179,7 +179,7 @@ export function findZeebeSubscription(message) { export function getDefaultValue(property) { if ( - isSpecialFeelProperty(property) + shouldCastToFeel(property) ) { return toFeelExpression(property.value, property.type); } 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 index 13270d9e..394d7dee 100644 --- a/src/cloud-element-templates/properties-panel/properties/custom-properties/NumberProperty.js +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/NumberProperty.js @@ -1,7 +1,7 @@ import { FeelNumberEntry, NumberFieldEntry } from '@bpmn-io/properties-panel'; import { useService } from 'bpmn-js-properties-panel'; import { propertyValidator, usePropertyAccessors } from './util'; -import { isSpecialFeelProperty } from '../../../util/FeelUtil'; +import { shouldCastToFeel } from '../../../util/FeelUtil'; import { PropertyDescription } from '../../../../components/PropertyDescription'; import { PropertyTooltip } from '../../components/PropertyTooltip'; import { useCallback } from '@bpmn-io/properties-panel/preact/hooks'; @@ -32,7 +32,7 @@ export function NumberProperty(props) { const [ getValue, setValue ] = usePropertyAccessors(bpmnFactory, commandStack, element, property); const validate = useCallback((value) => { - if (isSpecialFeelProperty(property) && isNumber(value) && value.toString().includes('e')) { + if (shouldCastToFeel(property) && isNumber(value) && value.toString().includes('e')) { return translate('Scientific notation is disallowed in FEEL.'); } 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 index 5ab9c801..2ba4a89a 100644 --- a/src/cloud-element-templates/properties-panel/properties/custom-properties/util.js +++ b/src/cloud-element-templates/properties-panel/properties/custom-properties/util.js @@ -1,6 +1,6 @@ import { find, groupBy } from 'min-dash'; import { getPropertyValue, setPropertyValue, validateProperty } from '../../../util/propertyUtil'; -import { isSpecialFeelProperty, toFeelExpression } from '../../../util/FeelUtil'; +import { shouldCastToFeel, toFeelExpression } from '../../../util/FeelUtil'; import { useCallback, useState } from '@bpmn-io/properties-panel/preact/hooks'; @@ -33,7 +33,7 @@ export function usePropertyAccessors(bpmnFactory, commandStack, element, propert return fromFeelExpression(directGet(), property.type); }, [ directGet, property, isFeelEnabled ]); - if (!isSpecialFeelProperty(property)) { + if (!shouldCastToFeel(property)) { return [ directGet, directSet ]; } @@ -62,7 +62,7 @@ const fromFeelExpression = (value, type) => { }; const feelEnabled = (property, value) => { - if (!isSpecialFeelProperty(property)) { + if (!shouldCastToFeel(property)) { return true; } diff --git a/src/cloud-element-templates/util/FeelUtil.js b/src/cloud-element-templates/util/FeelUtil.js index 12f22c18..d88230b8 100644 --- a/src/cloud-element-templates/util/FeelUtil.js +++ b/src/cloud-element-templates/util/FeelUtil.js @@ -1,7 +1,29 @@ -export const isSpecialFeelProperty = (property) => { - return [ 'optional', 'static' ].includes(property.feel) && [ 'Boolean', 'Number' ].includes(property.type); +/** + * Check if the property is cast to FEEL expression: + * - Boolean and Number properties with feel set to 'optional' or 'static' + * - Boolean and Number input/output parameters have default feel=static + * + * @returns {boolean} + */ +export const shouldCastToFeel = (property) => { + const feel = getFeelValue(property); + + return [ 'optional', 'static' ].includes(feel) && [ 'Boolean', 'Number' ].includes(property.type); }; +const ALWAYS_CAST_TO_FEEL = [ + 'zeebe:input', + 'zeebe:output' +]; + +function getFeelValue(property) { + if (ALWAYS_CAST_TO_FEEL.includes(property.binding.type)) { + return property.feel || 'static'; + } + + return property.feel; +} + export const toFeelExpression = (value, type) => { if (typeof value === 'string' && value.startsWith('=')) { return value; diff --git a/test/spec/cloud-element-templates/properties/BooleanProperty.bpmn b/test/spec/cloud-element-templates/properties/BooleanProperty.bpmn index cf50833e..a359cbd1 100644 --- a/test/spec/cloud-element-templates/properties/BooleanProperty.bpmn +++ b/test/spec/cloud-element-templates/properties/BooleanProperty.bpmn @@ -5,6 +5,7 @@ + @@ -24,6 +25,10 @@ + + + + diff --git a/test/spec/cloud-element-templates/properties/BooleanProperty.json b/test/spec/cloud-element-templates/properties/BooleanProperty.json index 6a25e288..d9284408 100644 --- a/test/spec/cloud-element-templates/properties/BooleanProperty.json +++ b/test/spec/cloud-element-templates/properties/BooleanProperty.json @@ -73,5 +73,31 @@ } } ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL missing - input output", + "id": "booleanField.feel.no-feel", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "BooleanProperty", + "type": "Boolean", + "binding": { + "type": "zeebe:input", + "name": "BooleanProperty" + } + }, + { + "label": "BooleanProperty", + "type": "Boolean", + "binding": { + "type": "zeebe:output", + "source": "BooleanProperty" + } + } + ] } ] diff --git a/test/spec/cloud-element-templates/properties/BooleanProperty.spec.js b/test/spec/cloud-element-templates/properties/BooleanProperty.spec.js index 6f7031ad..0ea947ff 100644 --- a/test/spec/cloud-element-templates/properties/BooleanProperty.spec.js +++ b/test/spec/cloud-element-templates/properties/BooleanProperty.spec.js @@ -17,6 +17,8 @@ import { import { findExtension, + findInputParameter, + findOutputParameter, findZeebeProperty } from 'src/cloud-element-templates/Helper'; @@ -85,6 +87,45 @@ describe('provider/cloud-element-templates - BooleanProperty', function() { }); + describe('no feel property - static for zeebe:input and zeebe:output', function() { + + let inputEntry, outputEntry; + + beforeEach(async function() { + await expectSelected('no-feel'); + inputEntry = findEntry('custom-entry-booleanField.feel.no-feel-0', container); + outputEntry = findEntry('custom-entry-booleanField.feel.no-feel-1', container); + }); + + it('should render boolean field', async function() { + + // then + expect(findCheckbox(inputEntry)).to.exist; + expect(findCheckbox(outputEntry)).to.exist; + }); + + + it('should cast to FEEL expression as in static (zeebe:input)', async function() { + + // when + await findCheckbox(inputEntry).click(); + + // then + expectZeebeInput('no-feel', 'BooleanProperty', '=true'); + }); + + + it('should cast to FEEL expression as in static (zeebe:output)', async function() { + + // when + await findCheckbox(outputEntry).click(); + + // then + expectZeebeOutput('no-feel', 'BooleanProperty', '=true'); + }); + }); + + describe('feel required', function() { let entry, input; @@ -251,6 +292,34 @@ function expectZeebeProperty(id, name, value) { }); } +function expectZeebeInput(id, name, value) { + return getBpmnJS().invoke(function(elementRegistry) { + const element = elementRegistry.get(id); + + const bo = getBusinessObject(element); + + const ioMapping = findExtension(bo, 'zeebe:IoMapping'), + input = findInputParameter(ioMapping, { name }); + + expect(input).to.exist; + expect(input.source).to.eql(value); + }); +} + +function expectZeebeOutput(id, source, value) { + return getBpmnJS().invoke(function(elementRegistry) { + const element = elementRegistry.get(id); + + const bo = getBusinessObject(element); + + const ioMapping = findExtension(bo, 'zeebe:IoMapping'), + output = findOutputParameter(ioMapping, { source }); + + expect(output).to.exist; + expect(output.target).to.eql(value); + }); +} + function expectSelected(id) { return getBpmnJS().invoke(async function(elementRegistry, selection) { const element = elementRegistry.get(id); @@ -275,3 +344,7 @@ function findInput(type, container) { return domQuery(`input[type='${ type }']`, container); } + +function findCheckbox(container) { + return findInput('checkbox', container); +} diff --git a/test/spec/cloud-element-templates/properties/NumberProperty.bpmn b/test/spec/cloud-element-templates/properties/NumberProperty.bpmn index ac56ac70..dc1c2203 100644 --- a/test/spec/cloud-element-templates/properties/NumberProperty.bpmn +++ b/test/spec/cloud-element-templates/properties/NumberProperty.bpmn @@ -5,6 +5,7 @@ + @@ -24,6 +25,10 @@ + + + + diff --git a/test/spec/cloud-element-templates/properties/NumberProperty.json b/test/spec/cloud-element-templates/properties/NumberProperty.json index 2cbbd511..ed69f715 100644 --- a/test/spec/cloud-element-templates/properties/NumberProperty.json +++ b/test/spec/cloud-element-templates/properties/NumberProperty.json @@ -73,5 +73,31 @@ } } ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "FEEL missing - input output", + "id": "numberField.feel.no-feel", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "properties": [ + { + "label": "NumberProperty", + "type": "Number", + "binding": { + "type": "zeebe:input", + "name": "NumberProperty" + } + }, + { + "label": "NumberProperty", + "type": "Number", + "binding": { + "type": "zeebe:output", + "source": "NumberProperty" + } + } + ] } ] diff --git a/test/spec/cloud-element-templates/properties/NumberProperty.spec.js b/test/spec/cloud-element-templates/properties/NumberProperty.spec.js index ef48c5aa..00668921 100644 --- a/test/spec/cloud-element-templates/properties/NumberProperty.spec.js +++ b/test/spec/cloud-element-templates/properties/NumberProperty.spec.js @@ -18,6 +18,8 @@ import { import { findExtension, + findInputParameter, + findOutputParameter, findZeebeProperty } from 'src/cloud-element-templates/Helper'; @@ -97,6 +99,45 @@ describe('provider/cloud-element-templates - NumberProperty', function() { }); + describe('no feel property - static for input and output', function() { + + let inputEntry, outputEntry; + + beforeEach(async function() { + await expectSelected('no-feel'); + inputEntry = findEntry('custom-entry-numberField.feel.no-feel-0', container); + outputEntry = findEntry('custom-entry-numberField.feel.no-feel-1', container); + }); + + it('should render number field', async function() { + + // then + expect(findNumberInput(inputEntry)).to.exist; + expect(findNumberInput(outputEntry)).to.exist; + }); + + + it('should cast to FEEL expression (zeebe:input)', async function() { + + // when + await changeInput(findNumberInput(inputEntry), '123'); + + // then + expectZeebeInput('no-feel', 'NumberProperty', '=123'); + }); + + + it('should cast to FEEL expression (zeebe:output)', async function() { + + // when + await changeInput(findNumberInput(outputEntry), '123'); + + // then + expectZeebeOutput('no-feel', 'NumberProperty', '=123'); + }); + }); + + describe('feel required', function() { let entry, input; @@ -283,6 +324,34 @@ function expectZeebeProperty(id, name, value) { }); } +function expectZeebeInput(id, name, value) { + return getBpmnJS().invoke(function(elementRegistry) { + const element = elementRegistry.get(id); + + const bo = getBusinessObject(element); + + const ioMapping = findExtension(bo, 'zeebe:IoMapping'), + input = findInputParameter(ioMapping, { name }); + + expect(input).to.exist; + expect(input.source).to.eql(value); + }); +} + +function expectZeebeOutput(id, source, value) { + return getBpmnJS().invoke(function(elementRegistry) { + const element = elementRegistry.get(id); + + const bo = getBusinessObject(element); + + const ioMapping = findExtension(bo, 'zeebe:IoMapping'), + output = findOutputParameter(ioMapping, { source }); + + expect(output).to.exist; + expect(output.target).to.eql(value); + }); +} + function expectSelected(id) { return getBpmnJS().invoke(async function(elementRegistry, selection) { const element = elementRegistry.get(id); @@ -311,6 +380,10 @@ function findEntry(id, container) { return domQuery(`[data-entry-id='${ id }']`, container); } +function findNumberInput(container) { + return findInput('number', container); +} + function findInput(type, container) { expect(container).to.not.be.null;