From 116214ccde906a391e1a5cd317fa6c71e7e14bf8 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 8 May 2024 08:24:50 +0200 Subject: [PATCH] :bug: [#4269] Fixed DMN integration for real-world decision definitions Backport-of: #4282 --- package-lock.json | 100 ++++++++++++ package.json | 1 + requirements/base.txt | 2 +- requirements/ci.txt | 2 +- requirements/dev.txt | 2 +- requirements/extensions.txt | 2 +- .../dmn/contrib/camunda/tests/test_plugin.py | 24 +-- src/openforms/js/compiled-lang/en.json | 42 +++++ src/openforms/js/compiled-lang/nl.json | 42 +++++ .../actions/dmn/DMNActionConfig.stories.js | 79 +++++++-- .../logic/actions/dmn/DMNParametersForm.js | 150 ++++++++++++++---- .../logic/actions/dmn/InputsOverview.js | 71 +++++++++ .../logic/actions/dmn/VariableMapping.js | 20 ++- .../form_design/logic/actions/dmn/utils.js | 49 ++++++ .../zgw/ZGWOptionsFormFields.stories.js | 2 +- .../admin/forms/VariableSelection.js | 13 +- src/openforms/js/lang/en.json | 35 ++++ src/openforms/js/lang/nl.json | 36 +++++ .../scss/components/admin/_ReactModal.scss | 7 + .../scss/components/admin/_index.scss | 2 +- .../scss/components/admin/_logic-dmn.scss | 22 +++ .../scss/components/admin/_mappings.scss | 10 -- 22 files changed, 637 insertions(+), 76 deletions(-) create mode 100644 src/openforms/js/components/admin/form_design/logic/actions/dmn/InputsOverview.js create mode 100644 src/openforms/js/components/admin/form_design/logic/actions/dmn/utils.js create mode 100644 src/openforms/scss/components/admin/_logic-dmn.scss delete mode 100644 src/openforms/scss/components/admin/_mappings.scss diff --git a/package-lock.json b/package-lock.json index 4f2703510c..1b1e0acd03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "classnames": "^2.3.1", "copy-to-clipboard": "^3.3.1", "design-token-editor": "^0.6.0", + "feelin": "^3.1.0", "flatpickr": "^4.6.9", "formik": "^2.2.9", "formiojs": "~4.13.0", @@ -4410,6 +4411,27 @@ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", "dev": true }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz", + "integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@mdx-js/react": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", @@ -16203,6 +16225,19 @@ "pend": "~1.2.0" } }, + "node_modules/feelin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/feelin/-/feelin-3.1.0.tgz", + "integrity": "sha512-ITPATtpwDWeLr7FKEAai7mJPlIH0td+D58f61+ZFDOs6Gg+8mFIo1LlhltQOeLkmZlOdvC/RsovbZ7SqxUfoyQ==", + "dependencies": { + "@lezer/lr": "^1.3.9", + "lezer-feel": "^1.2.8", + "luxon": "^3.4.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/fetch-ponyfill": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz", @@ -20209,6 +20244,18 @@ "node": ">=6" } }, + "node_modules/lezer-feel": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/lezer-feel/-/lezer-feel-1.2.8.tgz", + "integrity": "sha512-CO5JEpwNhH1p8mmRRcqMjJrYxO3vNx0nEsF9Ak4OPa1pNHEqvJ2rwYwM9LjZ7jh/Sl5FxbTJT/teF9a+zWmflg==", + "dependencies": { + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.4.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/lilconfig": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", @@ -20443,6 +20490,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -29384,6 +29439,27 @@ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", "dev": true }, + "@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz", + "integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, "@mdx-js/react": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", @@ -37999,6 +38075,16 @@ "pend": "~1.2.0" } }, + "feelin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/feelin/-/feelin-3.1.0.tgz", + "integrity": "sha512-ITPATtpwDWeLr7FKEAai7mJPlIH0td+D58f61+ZFDOs6Gg+8mFIo1LlhltQOeLkmZlOdvC/RsovbZ7SqxUfoyQ==", + "requires": { + "@lezer/lr": "^1.3.9", + "lezer-feel": "^1.2.8", + "luxon": "^3.4.4" + } + }, "fetch-ponyfill": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz", @@ -40968,6 +41054,15 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, + "lezer-feel": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/lezer-feel/-/lezer-feel-1.2.8.tgz", + "integrity": "sha512-CO5JEpwNhH1p8mmRRcqMjJrYxO3vNx0nEsF9Ak4OPa1pNHEqvJ2rwYwM9LjZ7jh/Sl5FxbTJT/teF9a+zWmflg==", + "requires": { + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.4.0" + } + }, "lilconfig": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", @@ -41161,6 +41256,11 @@ "yallist": "^3.0.2" } }, + "luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + }, "lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index 19dd0f8255..c41db16121 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "classnames": "^2.3.1", "copy-to-clipboard": "^3.3.1", "design-token-editor": "^0.6.0", + "feelin": "^3.1.0", "flatpickr": "^4.6.9", "formik": "^2.2.9", "formiojs": "~4.13.0", diff --git a/requirements/base.txt b/requirements/base.txt index 3b5ef973a7..b1b4875204 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -142,7 +142,7 @@ django-autoslug==1.9.9 # via -r requirements/base.in django-axes[ipware]==6.0.5 # via -r requirements/base.in -django-camunda==0.14.0 +django-camunda==0.15.0 # via -r requirements/base.in django-capture-tag==1.0 # via -r requirements/base.in diff --git a/requirements/ci.txt b/requirements/ci.txt index 51bf74ed9d..88216fef53 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -231,7 +231,7 @@ django-axes[ipware]==6.0.5 # -c requirements/base.txt # -r requirements/base.txt # django-axes -django-camunda==0.14.0 +django-camunda==0.15.0 # via # -c requirements/base.txt # -r requirements/base.txt diff --git a/requirements/dev.txt b/requirements/dev.txt index 3dff538031..5e5abd6f51 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -258,7 +258,7 @@ django-axes[ipware]==6.0.5 # -c requirements/ci.txt # -r requirements/ci.txt # django-axes -django-camunda==0.14.0 +django-camunda==0.15.0 # via # -c requirements/ci.txt # -r requirements/ci.txt diff --git a/requirements/extensions.txt b/requirements/extensions.txt index 1783c3ebcb..9a3d1f1c27 100644 --- a/requirements/extensions.txt +++ b/requirements/extensions.txt @@ -196,7 +196,7 @@ django-axes[ipware]==6.0.5 # -c requirements/base.in # -r requirements/base.txt # django-axes -django-camunda==0.14.0 +django-camunda==0.15.0 # via # -c requirements/base.in # -r requirements/base.txt diff --git a/src/openforms/dmn/contrib/camunda/tests/test_plugin.py b/src/openforms/dmn/contrib/camunda/tests/test_plugin.py index f056f133d7..182c72857f 100644 --- a/src/openforms/dmn/contrib/camunda/tests/test_plugin.py +++ b/src/openforms/dmn/contrib/camunda/tests/test_plugin.py @@ -160,14 +160,14 @@ def test_get_inputs_outputs(self): outputs = params.outputs self.assertEqual(len(inputs), 2) - self.assertEqual(inputs[0]["label"], "Invoice Amount") - self.assertEqual(inputs[0]["expression"], "amount") - self.assertEqual(inputs[1]["label"], "Invoice Category") - self.assertEqual(inputs[1]["expression"], "invoiceCategory") + self.assertEqual(inputs[0].label, "Invoice Amount") + self.assertEqual(inputs[0].expression, "amount") + self.assertEqual(inputs[1].label, "Invoice Category") + self.assertEqual(inputs[1].expression, "invoiceCategory") self.assertEqual(len(outputs), 1) - self.assertEqual(outputs[0]["label"], "Classification") - self.assertEqual(outputs[0]["name"], "invoiceClassification") + self.assertEqual(outputs[0].label, "Classification") + self.assertEqual(outputs[0].name, "invoiceClassification") def test_get_inputs_outputs_table_with_dependency(self): # This decision ID depends on the invoiceClassification table @@ -179,11 +179,11 @@ def test_get_inputs_outputs_table_with_dependency(self): outputs = params.outputs self.assertEqual(len(inputs), 2) - self.assertEqual(inputs[0]["label"], "Invoice Amount") - self.assertEqual(inputs[0]["expression"], "amount") - self.assertEqual(inputs[1]["label"], "Invoice Category") - self.assertEqual(inputs[1]["expression"], "invoiceCategory") + self.assertEqual(inputs[0].label, "Invoice Amount") + self.assertEqual(inputs[0].expression, "amount") + self.assertEqual(inputs[1].label, "Invoice Category") + self.assertEqual(inputs[1].expression, "invoiceCategory") self.assertEqual(len(outputs), 1) - self.assertEqual(outputs[0]["label"], "Approver Group") - self.assertEqual(outputs[0]["name"], "result") + self.assertEqual(outputs[0].label, "Approver Group") + self.assertEqual(outputs[0].name, "result") diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index 33fbb93339..6afec79048 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -469,6 +469,12 @@ "value": "Birth Date Component" } ], + "4sFGgA": [ + { + "type": 0, + "value": "The expressions here are extracted from the selected decision definition. It's possible certain inputs are displayed here that are already provided by a dependency of the selected decision, due to the complexity of the input expression." + } + ], "5/5LjA": [ { "type": 0, @@ -669,6 +675,12 @@ "value": "Stay logged in" } ], + "6jv6nd": [ + { + "type": 0, + "value": "Data type" + } + ], "6k14SS": [ { "type": 0, @@ -795,6 +807,12 @@ "value": "Years" } ], + "7cfkp6": [ + { + "type": 0, + "value": "Expected input expressions" + } + ], "7di6Fm": [ { "type": 0, @@ -2453,6 +2471,12 @@ "value": "Confidentiality" } ], + "R8/zGm": [ + { + "type": 0, + "value": "Form variable" + } + ], "RN628y": [ { "type": 0, @@ -4005,6 +4029,12 @@ "value": "Tab name" } ], + "i7Vmcf": [ + { + "type": 0, + "value": "Label" + } + ], "iHpAYZ": [ { "type": 0, @@ -4071,6 +4101,12 @@ "value": "years" } ], + "ipGvSb": [ + { + "type": 0, + "value": "Expression" + } + ], "iq0ppL": [ { "type": 0, @@ -4129,6 +4165,12 @@ "value": "House number component" } ], + "jLv6k8": [ + { + "type": 0, + "value": "DMN variable" + } + ], "jU/t8O": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index 8fb874ce8e..c28a2cacb1 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -469,6 +469,12 @@ "value": "Geboortedatum veld" } ], + "4sFGgA": [ + { + "type": 0, + "value": "De opgelijste expressies komen uit de beslisdefinities-XML. Het kan gebeuren dat bepaalde inputs al berekend zijn via een beslistabel die als input dient voor de geselecteerde beslistabel, maar door de complexiteit van de expressie is deze er niet uitgefilterd." + } + ], "5/5LjA": [ { "type": 1, @@ -669,6 +675,12 @@ "value": "Ingelogd blijven" } ], + "6jv6nd": [ + { + "type": 0, + "value": "Datatype" + } + ], "6k14SS": [ { "type": 0, @@ -799,6 +811,12 @@ "value": "Jaren" } ], + "7cfkp6": [ + { + "type": 0, + "value": "Verwachtte input-expressies" + } + ], "7di6Fm": [ { "type": 0, @@ -2453,6 +2471,12 @@ "value": "Vertrouwelijkheidaanduiding" } ], + "R8/zGm": [ + { + "type": 0, + "value": "Formuliervariable" + } + ], "RN628y": [ { "type": 0, @@ -4010,6 +4034,12 @@ "value": "Tabnaam" } ], + "i7Vmcf": [ + { + "type": 0, + "value": "Label" + } + ], "iHpAYZ": [ { "type": 0, @@ -4076,6 +4106,12 @@ "value": "jaren" } ], + "ipGvSb": [ + { + "type": 0, + "value": "Expressie" + } + ], "iq0ppL": [ { "type": 0, @@ -4134,6 +4170,12 @@ "value": "Huisnummercomponent" } ], + "jLv6k8": [ + { + "type": 0, + "value": "DMN-variabele" + } + ], "jU/t8O": [ { "type": 0, diff --git a/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js b/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js index 8889b365a8..01d891e7b6 100644 --- a/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js +++ b/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js @@ -25,6 +25,7 @@ export default { {type: 'textfield', key: 'surname', name: 'Surname'}, {type: 'number', key: 'income', name: 'Income'}, {type: 'checkbox', key: 'canApply', name: 'Can apply?'}, + {type: 'postcode', key: 'postcode', name: 'Postcode'}, ], }, parameters: { @@ -40,6 +41,10 @@ export default { id: 'invoiceClassification', label: 'Invoice Classification', }, + { + id: 'withComplexExpressions', + label: 'Complex expression in inputs', + }, ], 'some-other-engine': [{id: 'some-definition-id', label: 'Some definition id'}], }), @@ -54,19 +59,19 @@ export default { { label: 'Direction', id: 'Input_1', - type_ref: 'string', + typeRef: 'string', expression: 'direction', }, { label: 'Port number', id: 'InputClause_1cn8gp3', - type_ref: 'integer', + typeRef: 'integer', expression: 'port', }, { label: 'Camunda variable', id: 'InputClause_1f09wt8', - type_ref: 'string', + typeRef: 'string', expression: 'camundaVar', }, ], @@ -74,13 +79,13 @@ export default { { id: 'Output_1', label: 'Policy', - type_ref: 'string', + typeRef: 'string', name: 'policy', }, { id: 'OutputClause_0lzmnio', label: 'Reason', - type_ref: 'string', + typeRef: 'string', name: 'reason', }, ], @@ -91,13 +96,13 @@ export default { id: 'clause1', label: 'Invoice Amount', expression: 'amount', - type_ref: 'double', + typeRef: 'double', }, { id: 'InputClause_15qmk0v', label: 'Invoice Category', expression: 'invoiceCategory', - type_ref: 'string', + typeRef: 'string', }, ], outputs: [ @@ -105,13 +110,49 @@ export default { id: 'clause3', label: 'Classification', name: 'invoiceClassification', - type_ref: 'string', + typeRef: 'string', }, { id: 'OutputClause_1cthd0w', label: 'Approver Group', name: 'result', - type_ref: 'string', + typeRef: 'string', + }, + ], + }, + withComplexExpressions: { + inputs: [ + { + label: 'Simple variable', + id: '', + typeRef: 'string', + expression: 'foo', + }, + { + label: 'Sum of a and b', + id: '', + typeRef: 'integer', + expression: 'a + b', + }, + { + label: 'Numeric part postcode', + id: '', + typeRef: 'integer', + expression: 'number(substring(postcode, 1, 4))', + }, + { + label: 'Weird but valid syntax', + id: '', + typeRef: 'integer', + expression: 'a+b', + }, + ], + outputs: [ + { + id: 'OutputClause_1cthd0w', + label: 'Sole output', + typeRef: 'string', + name: 'result', }, ], }, @@ -160,7 +201,7 @@ export const Empty = { await waitFor(async () => { const renderedOptions = within(decisionDefDropdown).getAllByRole('option'); - await expect(renderedOptions.length).toBe(3); + await expect(renderedOptions.length).toBe(4); }); await userEvent.selectOptions(decisionDefDropdown, 'Approve payment'); @@ -189,7 +230,7 @@ export const Empty = { const [formVarsDropdowns, dmnVarsDropdown] = dropdowns; - await userEvent.selectOptions(formVarsDropdowns, 'Name'); + await userEvent.selectOptions(formVarsDropdowns, 'Name (name)'); await userEvent.selectOptions(dmnVarsDropdown, 'camundaVar'); await expect(formVarsDropdowns.value).toBe('name'); @@ -319,3 +360,19 @@ export const OnePluginAvailable = { await expect(pluginDropdown.value).toBe('camunda7'); }, }; + +export const ComplexInputExpressions = { + args: { + initialValues: { + pluginId: 'camunda7', + decisionDefinitionId: 'withComplexExpressions', + decisionDefinitionVersion: '1', + inputMapping: [ + {formVariable: 'postcode', dmnVariable: 'postcode'}, + {formVariable: 'income', dmnVariable: 'a'}, + {formVariable: 'income', dmnVariable: 'b'}, + ], + outputMapping: [], + }, + }, +}; diff --git a/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNParametersForm.js b/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNParametersForm.js index 04bf2ab9b3..265f76c4c9 100644 --- a/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNParametersForm.js +++ b/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNParametersForm.js @@ -1,21 +1,108 @@ +import {parseExpression} from 'feelin'; import {useFormikContext} from 'formik'; -import React, {useContext, useState} from 'react'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; import {useAsync} from 'react-use'; -import {FormContext} from 'components/admin/form_design/Context'; import {DMN_DECISION_DEFINITIONS_PARAMS_LIST} from 'components/admin/form_design/constants'; import {get} from 'utils/fetch'; +import InputsOverview from './InputsOverview'; import VariableMapping from './VariableMapping'; +import {namePattern} from './utils'; -const EMPTY_DMN_PARAMS = {inputs: [], outputs: []}; +const EMPTY_DMN_PARAMS = { + inputClauses: [], + inputs: [], + outputs: [], +}; + +/** + * @typedef InputClause + * @type {object} + * @property {string} expression - the FEEL expression of the input clause + * @property {string} label - the human-readable input clause label + * + * @typedef {string} OptionValue - The value of a dropdown option. + * @typedef {string} OptionLabel - The label of a dropdown option. + * @typedef {[OptionValue, OptionLabel]} Option - A dropdown option. + */ + +/** + * Process the input parameters and their expressions. + * + * Each input parameter has a FEEL expression, which itself can be a 'complex' + * expression requiring individual variables, e.g. `a + b`. + * + * Note that expressions are ambiguous without context, `a+b` could mean that input + * variables `a` and `b` are required, but you could also provide a single input with + * the name `"a+b"` (this is valid FEEL!). + * + * @param {InputClause[]} params The input clauses extracted from the decision definition. + * @return {Option[]} An array of two-tuples (value, label). + */ +const processInputParams = params => { + const variableExpressionsWithLabels = params + // check each expression if it can possibly be a valid identifier itself. These + // should be the most common cases. + .filter(param => namePattern.test(param.expression)) + .map(param => [param.expression, param.label]); + + // for (simple) expressions, we can grab the explicit label from the input parameter + // if it's defined. These variables will come up again when we process each expression + // as a FEEL expression. + const expressionLabels = Object.fromEntries(variableExpressionsWithLabels); + + // process each expression individually and add the extract variables to the + // possible variables. This includes the most simple e + const extractedVariables = []; + for (const {expression} of params) { + // docs: https://lezer.codemirror.net/docs/ref/#common.Tree.iterate + const tree = parseExpression(expression); + tree.iterate({ + enter({name, from, to, node: {parent}}) { + if (name !== 'Identifier') return; + if (parent.name !== 'VariableName') return; + + // check if this var identifier is a function, we need to ignore those. + const isFunction = parent?.parent.name === 'FunctionInvocation'; + if (isFunction) return; + const varName = expression.substring(from, to); + if (extractedVariables.includes(varName)) return; + extractedVariables.push(varName); + }, + }); + } + + // We classify the input parameters in two buckets - explicit labels and parsed + // 'labels' (the same as the variable name, really). Explicit simple input vars with + // labels (and thus simple expressions) are favoured. + // + // It's possible an expression like `a+b` is in variableExpressionsWithLabels without + // being in extractedVariables - therefore we add those after all the common variable + // patterns are processed. + const labeledOptions = []; + const unlabeledOptions = []; + for (const varName of extractedVariables) { + const label = expressionLabels[varName]; + const target = label !== undefined ? labeledOptions : unlabeledOptions; + target.push([varName, label || varName]); + } + const possibleVariables = [...labeledOptions, ...unlabeledOptions]; + + // add weird-but-valid labeled expressions that were not picked up by the expression + // parsing (this parsing is context dependent and cases like `a+b` are ambiguous). + const weirdCases = variableExpressionsWithLabels.filter( + ([varName]) => !extractedVariables.includes(varName) + ); + + return possibleVariables.concat(weirdCases); +}; const DMNParametersForm = () => { const { values: {pluginId, decisionDefinitionId, decisionDefinitionVersion}, } = useFormikContext(); - const {formVariables} = useContext(FormContext); const {loading, value: dmnParams = EMPTY_DMN_PARAMS} = useAsync(async () => { if (!pluginId || !decisionDefinitionId) { @@ -33,38 +120,43 @@ const DMNParametersForm = () => { const response = await get(DMN_DECISION_DEFINITIONS_PARAMS_LIST, queryParams); + const inputs = processInputParams(response.data.inputs); + return { - inputs: response.data.inputs.map(inputParam => [inputParam.expression, inputParam.label]), + inputClauses: response.data.inputs, + inputs: inputs, outputs: response.data.outputs.map(outputParam => [outputParam.name, outputParam.label]), }; }, [pluginId, decisionDefinitionId, decisionDefinitionVersion]); - const variablesChoices = formVariables.map(variable => [variable.key, variable.name]); - return ( -
-
-

- -

- -
-
-

- -

- +
+
+
+

+ +

+ +
+ +
+

+ +

+ +
+ +
); }; diff --git a/src/openforms/js/components/admin/form_design/logic/actions/dmn/InputsOverview.js b/src/openforms/js/components/admin/form_design/logic/actions/dmn/InputsOverview.js new file mode 100644 index 0000000000..c2e14486d2 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/logic/actions/dmn/InputsOverview.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +const InputsOverview = ({inputClauses}) => ( +
+

+ +

+

+ +

+ + + + + + + + + + + {inputClauses.map((inputClause, index) => ( + + + + + + ))} + +
+ + + + + +
{inputClause.label || '-'} + {inputClause.expression} + + {inputClause.typeRef || '-'} +
+
+); + +InputsOverview.propTypes = { + inputClauses: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + expression: PropTypes.string.isRequired, + typeRef: PropTypes.string, + }) + ), +}; + +export default InputsOverview; diff --git a/src/openforms/js/components/admin/form_design/logic/actions/dmn/VariableMapping.js b/src/openforms/js/components/admin/form_design/logic/actions/dmn/VariableMapping.js index 789f1f7d66..54d8927057 100644 --- a/src/openforms/js/components/admin/form_design/logic/actions/dmn/VariableMapping.js +++ b/src/openforms/js/components/admin/form_design/logic/actions/dmn/VariableMapping.js @@ -7,8 +7,9 @@ import DeleteIcon from 'components/admin/DeleteIcon'; import ButtonContainer from 'components/admin/forms/ButtonContainer'; import Field from 'components/admin/forms/Field'; import Select from 'components/admin/forms/Select'; +import VariableSelection from 'components/admin/forms/VariableSelection'; -const VariableMapping = ({loading, mappingName, formVariables, dmnVariables}) => { +const VariableMapping = ({loading, mappingName, dmnVariables, includeStaticVariables = false}) => { const intl = useIntl(); const {getFieldProps, values} = useFormikContext(); @@ -21,7 +22,7 @@ const VariableMapping = ({loading, mappingName, formVariables, dmnVariables}) => ( -
+
@@ -48,11 +49,14 @@ const VariableMapping = ({loading, mappingName, formVariables, dmnVariables}) => name={`${mappingName}.${index}.formVariable`} htmlFor={`${mappingName}.${index}.formVariable`} > - +