diff --git a/package-lock.json b/package-lock.json index 88641a2476..b527db4ad3 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", @@ -4398,6 +4399,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", @@ -16156,6 +16178,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", @@ -20162,6 +20197,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", @@ -20396,6 +20443,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", @@ -29412,6 +29467,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", @@ -38007,6 +38083,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", @@ -40976,6 +41062,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", @@ -41169,6 +41264,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 dde2b6de0c..e15ff028c3 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 3c83f17d92..72c838b07d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -146,7 +146,7 @@ django-autoslug==1.9.9 # via -r requirements/base.in django-axes[ipware]==6.4.0 # 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 6ca34bdf9f..d78a765c99 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -235,7 +235,7 @@ django-axes[ipware]==6.4.0 # -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 c5422130d8..b963d07310 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -259,7 +259,7 @@ django-axes[ipware]==6.4.0 # -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 634690d1ae..e98cb33c73 100644 --- a/requirements/extensions.txt +++ b/requirements/extensions.txt @@ -201,7 +201,7 @@ django-axes[ipware]==6.4.0 # -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/conf/locale/nl/LC_MESSAGES/django.po b/src/openforms/conf/locale/nl/LC_MESSAGES/django.po index ed59f7ac2f..62319bed3f 100644 --- a/src/openforms/conf/locale/nl/LC_MESSAGES/django.po +++ b/src/openforms/conf/locale/nl/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Open Forms\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-05-08 18:50+0200\n" +"POT-Creation-Date: 2024-05-08 18:51+0200\n" "PO-Revision-Date: 2024-05-06 12:23+0200\n" "Last-Translator: Sergei Maertens \n" "Language-Team: Dutch \n" 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 b0eb34b176..7f76ed270e 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, @@ -675,6 +681,12 @@ "value": "Stay logged in" } ], + "6jv6nd": [ + { + "type": 0, + "value": "Data type" + } + ], "6k14SS": [ { "type": 0, @@ -801,6 +813,12 @@ "value": "Years" } ], + "7cfkp6": [ + { + "type": 0, + "value": "Expected input expressions" + } + ], "7di6Fm": [ { "type": 0, @@ -2477,6 +2495,12 @@ "value": "Confidentiality" } ], + "R8/zGm": [ + { + "type": 0, + "value": "Form variable" + } + ], "RN628y": [ { "type": 0, @@ -4047,6 +4071,12 @@ "value": "Tab name" } ], + "i7Vmcf": [ + { + "type": 0, + "value": "Label" + } + ], "iHpAYZ": [ { "type": 0, @@ -4113,6 +4143,12 @@ "value": "years" } ], + "ipGvSb": [ + { + "type": 0, + "value": "Expression" + } + ], "iq0ppL": [ { "type": 0, @@ -4171,6 +4207,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 e33b6a1ccf..64ca781076 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": "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": 1, @@ -675,6 +681,12 @@ "value": "Ingelogd blijven" } ], + "6jv6nd": [ + { + "type": 0, + "value": "Data type" + } + ], "6k14SS": [ { "type": 0, @@ -805,6 +817,12 @@ "value": "Jaren" } ], + "7cfkp6": [ + { + "type": 0, + "value": "Expected input expressions" + } + ], "7di6Fm": [ { "type": 0, @@ -2477,6 +2495,12 @@ "value": "Vertrouwelijkheidaanduiding" } ], + "R8/zGm": [ + { + "type": 0, + "value": "Form variable" + } + ], "RN628y": [ { "type": 0, @@ -4052,6 +4076,12 @@ "value": "Tabnaam" } ], + "i7Vmcf": [ + { + "type": 0, + "value": "Label" + } + ], "iHpAYZ": [ { "type": 0, @@ -4118,6 +4148,12 @@ "value": "jaren" } ], + "ipGvSb": [ + { + "type": 0, + "value": "Expression" + } + ], "iq0ppL": [ { "type": 0, @@ -4176,6 +4212,12 @@ "value": "Huisnummercomponent" } ], + "jLv6k8": [ + { + "type": 0, + "value": "DMN variable" + } + ], "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`} > - +