From e284b4184700381d998ac9ea057dbd3bd2bea61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Steinr=C3=BCcken?= Date: Thu, 19 Feb 2026 14:47:16 +0100 Subject: [PATCH 1/6] feat: prefill context input with needed feel variables Closes https://github.com/camunda/camunda-modeler/issues/5640 --- lib/ElementConfig.js | 82 +++++++--- lib/ElementVariables.js | 16 +- lib/components/TaskTesting/TaskTesting.js | 25 ++- lib/utils/prefill.js | 176 ++++++++++++++++++++ package-lock.json | 189 +++++++++++++++------- package.json | 2 +- test/Prefill.spec.js | 160 ++++++++++++++++++ test/fixtures/prefill.bpmn | 37 +++++ 8 files changed, 597 insertions(+), 90 deletions(-) create mode 100644 lib/utils/prefill.js create mode 100644 test/Prefill.spec.js create mode 100644 test/fixtures/prefill.bpmn diff --git a/lib/ElementConfig.js b/lib/ElementConfig.js index 2944564..e115ae6 100644 --- a/lib/ElementConfig.js +++ b/lib/ElementConfig.js @@ -4,6 +4,14 @@ import { isAny } from 'bpmn-js/lib/util/ModelUtil'; import { isString, omit } from 'min-dash'; +import { + DEFAULT_INPUT_CONFIG, + computeDefaultInput, + computeMergedInput +} from './utils/prefill'; + +export { DEFAULT_INPUT_CONFIG }; + export const DEFAULT_CONFIG = { input: {}, output: {} @@ -28,8 +36,6 @@ export class ElementConfig extends EventEmitter { ...config }; - this._selectedElement = null; - this._variablesForElements = new Map(); } setConfig(newConfig) { @@ -43,9 +49,7 @@ export class ElementConfig extends EventEmitter { } setInputConfigForElement(element, newConfig) { - if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) { - throw new Error(`Unsupported element type: ${element.type}`); - } + this._assertSupportedElement(element); this._config = { ...this._config, @@ -59,9 +63,7 @@ export class ElementConfig extends EventEmitter { } resetInputConfigForElement(element) { - if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) { - throw new Error(`Unsupported element type: ${element.type}`); - } + this._assertSupportedElement(element); this._config = { ...this._config, @@ -72,9 +74,7 @@ export class ElementConfig extends EventEmitter { } setOutputConfigForElement(element, newConfig) { - if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) { - throw new Error(`Unsupported element type: ${element.type}`); - } + this._assertSupportedElement(element); this._config = { ...this._config, @@ -88,9 +88,7 @@ export class ElementConfig extends EventEmitter { } resetOutputConfigForElement(element) { - if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) { - throw new Error(`Unsupported element type: ${element.type}`); - } + this._assertSupportedElement(element); this._config = { ...this._config, @@ -101,25 +99,55 @@ export class ElementConfig extends EventEmitter { } getInputConfigForElement(element) { - if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) { - throw new Error(`Unsupported element type: ${element.type}`); - } + this._assertSupportedElement(element); if (!isString(this._config.input[element.id])) { - return this._getDefaultInputConfig(); + return DEFAULT_INPUT_CONFIG; } return this._config.input[element.id]; } + /** + * Computes a fresh input config from the element's input requirements, + * ignoring any stored user config. Used for resetting input to defaults. + * + * @param {Object} element + * @returns {Promise} JSON string + */ + async getDefaultInputForElement(element) { + this._assertSupportedElement(element); + + return computeDefaultInput(this._elementVariables, element); + } + + /** + * Merges current user input with fresh input requirements from the element. + * Removes null values (unfilled stubs) from user input, then adds new + * requirement stubs for any variables not yet present. + * + * Returns `null` when the current input is invalid JSON, signalling that + * no merge was possible and the caller should skip overwriting the config. + * + * @param {Object} element + * @returns {Promise} merged JSON string, or null if current input is unparseable + */ + async getMergedInputConfigForElement(element) { + this._assertSupportedElement(element); + + return computeMergedInput( + this._config.input[element.id], + this._elementVariables, + element + ); + } + /** * @param {import('./types').Element} element * @returns {import('./types').ElementOutput} */ getOutputConfigForElement(element) { - if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) { - throw new Error(`Unsupported element type: ${element.type}`); - } + this._assertSupportedElement(element); if (!this._config.output[element.id]) { return DEFAULT_OUTPUT; @@ -128,7 +156,13 @@ export class ElementConfig extends EventEmitter { return this._config.output[element.id]; } - _getDefaultInputConfig() { - return '{}'; + /** + * @param {Object} element + * @throws {Error} if the element type is not supported + */ + _assertSupportedElement(element) { + if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) { + throw new Error(`Unsupported element type: ${element.type}`); + } } -} \ No newline at end of file +} diff --git a/lib/ElementVariables.js b/lib/ElementVariables.js index 2e7ba09..5283a44 100644 --- a/lib/ElementVariables.js +++ b/lib/ElementVariables.js @@ -35,4 +35,18 @@ export class ElementVariables extends EventEmitter { return variablesWithoutLocal; } -} \ No newline at end of file + + /** + * Returns input requirement variables for an element — variables + * the element needs as input for its expressions and mappings. + * + * @param {Object} element + * @returns {Promise} + */ + async getInputRequirementsForElement(element) { + return this._variableResolver.getInputRequirementsForElement(element) + .catch(() => { + return []; + }); + } +} diff --git a/lib/components/TaskTesting/TaskTesting.js b/lib/components/TaskTesting/TaskTesting.js index 0a16fba..52eb18a 100644 --- a/lib/components/TaskTesting/TaskTesting.js +++ b/lib/components/TaskTesting/TaskTesting.js @@ -159,6 +159,16 @@ export default function TaskTesting({ const variables = await elementVariablesRef.current.getVariablesForElement(element); setVariablesForElement(variables); + + // Merge updated input requirements into the current input + if (elementConfigRef.current) { + const mergedInput = await elementConfigRef.current.getMergedInputConfigForElement(element); + + // Skip update when merge was not possible (e.g. invalid JSON) + if (mergedInput !== null) { + elementConfigRef.current.setInputConfigForElement(element, mergedInput); + } + } }; elementVariablesRef.current.on('variables.changed', handleVariablesChanged); @@ -272,7 +282,15 @@ export default function TaskTesting({ return; } - setInput(elementConfigRef?.current?.getInputConfigForElement(element)); + elementConfigRef?.current?.getMergedInputConfigForElement(element).then( + merged => { + if (merged !== null) { + setInput(merged); + } else { + setInput(elementConfigRef?.current?.getInputConfigForElement(element)); + } + } + ); setOutput(elementConfigRef?.current?.getOutputConfigForElement(element)); }, [ element ]); @@ -284,9 +302,10 @@ export default function TaskTesting({ } }, [ element ]); - const handleResetInput = useCallback(() => { + const handleResetInput = useCallback(async () => { if (element && elementConfigRef.current) { - elementConfigRef.current.resetInputConfigForElement(element); + const prefilled = await elementConfigRef.current.getDefaultInputForElement(element); + elementConfigRef.current.setInputConfigForElement(element, prefilled); } }, [ element ]); diff --git a/lib/utils/prefill.js b/lib/utils/prefill.js new file mode 100644 index 0000000..048ec1f --- /dev/null +++ b/lib/utils/prefill.js @@ -0,0 +1,176 @@ +import { has, isObject, isString } from 'min-dash'; + +export const DEFAULT_INPUT_CONFIG = '{}'; + + +/** + * Compute a default input config from the element's input requirements, + * producing a JSON string with `null` stubs for each required variable. + * + * @param {Object} elementVariables + * @param {Object} element + * @returns {Promise} JSON string + */ +export async function computeDefaultInput(elementVariables, element) { + const stub = await buildRequirementsStub(elementVariables, element); + + if (Object.keys(stub).length === 0) { + return DEFAULT_INPUT_CONFIG; + } + + return JSON.stringify(stub, null, 2); +} + + +/** + * Merge current user input with fresh input requirements from the element. + * Removes null values (unfilled stubs) from user input, then adds new + * requirement stubs for any variables not yet present. + * + * Returns `null` when the current input is invalid JSON, signalling that + * no merge was possible and the caller should skip overwriting the config. + * + * @param {string} currentInputString - stored JSON string (or undefined) + * @param {Object} elementVariables + * @param {Object} element + * @returns {Promise} merged JSON string, or null if unparseable + */ +export async function computeMergedInput(currentInputString, elementVariables, element) { + const requirementsStub = await buildRequirementsStub(elementVariables, element); + + const inputString = isString(currentInputString) + ? currentInputString + : DEFAULT_INPUT_CONFIG; + + let currentConfig; + try { + currentConfig = JSON.parse(inputString); + } catch (e) { + + // If user input is invalid JSON, signal that no merge is possible + return null; + } + + // Remove null values from user input, then merge with requirements + const cleaned = removeNullValues(currentConfig); + const merged = mergeObjects(requirementsStub, cleaned); + + if (Object.keys(merged).length === 0) { + return DEFAULT_INPUT_CONFIG; + } + + return JSON.stringify(merged, null, 2); +} + + +// helpers ////////////////////// + +/** + * Build a stub object from the element's input requirements. + * Each requirement variable becomes a key with `null` (or a nested + * object for context variables). + * + * @param {Object} elementVariables + * @param {Object} element + * @returns {Promise} requirements stub + */ +async function buildRequirementsStub(elementVariables, element) { + const requirements = await elementVariables + .getInputRequirementsForElement(element); + + if (!requirements || requirements.length === 0) { + return {}; + } + + const stub = {}; + + for (const variable of requirements) { + stub[variable.name] = variableToStub(variable); + } + + return stub; +} + +/** + * Convert a variable with entries (nested context) into a JSON stub value. + * Produces nested objects for context variables, `null` for leaves. + * + * @param {Object} variable + * @returns {*} stub value + */ +function variableToStub(variable) { + if (variable.entries && variable.entries.length > 0) { + const result = {}; + + for (const entry of variable.entries) { + result[entry.name] = variableToStub(entry); + } + + return result; + } + + return null; +} + +/** + * Recursively remove all null values from an object. + * Removes keys whose value is null, and recurses into nested objects. + * If all keys are removed, returns an empty object. + * + * @param {Object} obj + * @returns {Object} + */ +function removeNullValues(obj) { + if (!isObject(obj)) { + return obj; + } + + const result = {}; + + for (const key in obj) { + if (!has(obj, key)) continue; + + const value = obj[key]; + + if (value === null) continue; + + if (isObject(value)) { + const cleaned = removeNullValues(value); + + if (Object.keys(cleaned).length > 0) { + result[key] = cleaned; + } + } else { + result[key] = value; + } + } + + return result; +} + +/** + * Merge two objects: base provides the structure (stubs), override + * provides user values. User values take precedence. + * + * @param {Object} base - requirements stub (may contain null values) + * @param {Object} override - user input (cleaned of nulls) + * @returns {Object} + */ +function mergeObjects(base, override) { + const result = { ...base }; + + for (const key in override) { + if (!has(override, key)) continue; + + const overrideValue = override[key]; + const baseValue = result[key]; + + if (isObject(overrideValue) && isObject(baseValue)) { + result[key] = mergeObjects(baseValue, overrideValue); + } else { + result[key] = overrideValue; + } + } + + return result; +} diff --git a/package-lock.json b/package-lock.json index 9c52764..5fda141 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@babel/core": "^7.28.0", "@babel/preset-env": "^7.28.0", "@babel/preset-react": "^7.27.1", - "@bpmn-io/variable-resolver": "^1.3.6", + "@bpmn-io/variable-resolver": "github:bpmn-io/variable-resolver#5639_variable-input-requirements", "@camunda8/orchestration-cluster-api": "^8.8.4", "@camunda8/sdk": "^8.8.3", "@carbon/icons-react": "^11.62.0", @@ -123,7 +123,6 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1764,6 +1763,7 @@ "integrity": "sha512-2frw1gBl5I3XGrIDg4CBy6bpJiOuslKUOg9T91Fke6bIttFkF0zxlTKh4E4zU8g7gAo4ze0HnKMZDgHxea+Itw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "contra": "^1.9.4" } @@ -1817,6 +1817,41 @@ "min-dash": "^4.0.0" } }, + "node_modules/@bpmn-io/feel-analyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/feel-analyzer/-/feel-analyzer-0.1.0.tgz", + "integrity": "sha512-lQp3EZY6WbBI0y0PwnlXIk+fJO9yZpotFbd2a1UaIrule9i+wk9au99J+Md1RjT38GiqcBzk2mMftRkRb8Iezg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bpmn-io/lezer-feel": "^2.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@bpmn-io/feel-analyzer/node_modules/@bpmn-io/lezer-feel": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/lezer-feel/-/lezer-feel-2.3.0.tgz", + "integrity": "sha512-GU6UwiBZP2imXkDOXhNWdlBWCjZjDj0QcR+zEWRDK5jDHDFdN1wUZjOvYpZIKFpNJd3JRHY6uz3Ynqu/THn3gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.8", + "min-dash": "^5.0.0" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@bpmn-io/feel-analyzer/node_modules/min-dash": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz", + "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==", + "dev": true, + "license": "MIT" + }, "node_modules/@bpmn-io/feel-editor": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@bpmn-io/feel-editor/-/feel-editor-1.12.1.tgz", @@ -1887,7 +1922,8 @@ "resolved": "https://registry.npmjs.org/@bpmn-io/form-js-carbon-styles/-/form-js-carbon-styles-1.15.3.tgz", "integrity": "sha512-hel8qkcRPTwiTxsba/T+sjAOaJlR7GIU4VSEmsdMgoAjS+L1L9hO10f48aWz7umGKzuwr9+20YrwB6Il7eFeyA==", "dev": true, - "license": "SEE LICENSE IN LICENSE" + "license": "SEE LICENSE IN LICENSE", + "peer": true }, "node_modules/@bpmn-io/form-js-editor": { "version": "1.15.3", @@ -1895,6 +1931,7 @@ "integrity": "sha512-byFF6RbE7+UMsMsQFL6G011uQeKXIt9lysy2RS7ILUALeWfAW7eWzdhDHpPS7B36AwuyUBEOPFb90tTcibGdcw==", "dev": true, "license": "SEE LICENSE IN LICENSE", + "peer": true, "dependencies": { "@bpmn-io/draggle": "^4.1.1", "@bpmn-io/form-js-viewer": "^1.15.3", @@ -1913,6 +1950,7 @@ "integrity": "sha512-jrlq3E0Plz2SozZHP0mCqLkqzQMuKpeb+FN5/DVrcDWZmdUeZq32wx7KZTcHB4rlYn6YBvlCRAVf5XlkCHS2cw==", "dev": true, "license": "SEE LICENSE IN LICENSE", + "peer": true, "dependencies": { "@bpmn-io/form-js-editor": "^1.15.3", "@bpmn-io/form-js-viewer": "^1.15.3", @@ -1937,6 +1975,7 @@ "integrity": "sha512-YDiAlUap9M2KzJxE8sDSax8a52NrGZ0JdThUuzi2HFguGP9BSMeFz+mbjwsJd8ooIpQsGdd3PIYTs2EUiWCG+Q==", "dev": true, "license": "SEE LICENSE IN LICENSE", + "peer": true, "dependencies": { "@carbon/grid": "^11.32.2", "big.js": "^6.2.2", @@ -2013,7 +2052,6 @@ "integrity": "sha512-BkY3JYVDtmuUox4U/B5Mxis0UzW+uY9cZvKEUMKQeW+4IyOZ6yF07/fsabS1koaQV19YxF8im3PX5fibSNxphg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bpmn-io/feel-editor": "^1.12.0", "@carbon/icons": "^11.69.0", @@ -2029,22 +2067,61 @@ } }, "node_modules/@bpmn-io/variable-resolver": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@bpmn-io/variable-resolver/-/variable-resolver-1.3.6.tgz", - "integrity": "sha512-u3dzPB0DYNJld9sHu9GsqwqyV3XNfAQJ0qzXlxPGLYnL10Q7iOlsNHeCvGQ4TehfwpCT4YUarHfB5Ggkhptojg==", + "version": "1.5.0", + "resolved": "git+ssh://git@github.com/bpmn-io/variable-resolver.git#98bf692e60654f4572a4d5711d868f6155cf7eaa", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@bpmn-io/extract-process-variables": "^1.0.1", - "@lezer/common": "^1.2.3", - "lezer-feel": "^1.4.0", - "min-dash": "^4.2.3" + "@bpmn-io/extract-process-variables": "^2.1.0", + "@bpmn-io/feel-analyzer": "^0.1.0", + "@bpmn-io/lezer-feel": "^2.3.0", + "@camunda/feel-builtins": "^1.0.0", + "@lezer/common": "^1.5.1", + "min-dash": "^5.0.0" }, "peerDependencies": { "bpmn-js": "*" } }, + "node_modules/@bpmn-io/variable-resolver/node_modules/@bpmn-io/extract-process-variables": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/extract-process-variables/-/extract-process-variables-2.1.0.tgz", + "integrity": "sha512-Z5iwYHvo58GnEC2QAg53dymOb4o1r6x5Q4PvWk7du74z/HeS3x5VUDnozc4CDIugYMCe2j+RJV7CUBMbn48HJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-dash": "^5.0.0" + } + }, + "node_modules/@bpmn-io/variable-resolver/node_modules/@bpmn-io/lezer-feel": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/lezer-feel/-/lezer-feel-2.3.0.tgz", + "integrity": "sha512-GU6UwiBZP2imXkDOXhNWdlBWCjZjDj0QcR+zEWRDK5jDHDFdN1wUZjOvYpZIKFpNJd3JRHY6uz3Ynqu/THn3gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.8", + "min-dash": "^5.0.0" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@bpmn-io/variable-resolver/node_modules/@camunda/feel-builtins": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@camunda/feel-builtins/-/feel-builtins-1.0.0.tgz", + "integrity": "sha512-/0w86UVmNcNqlZB9n/4Ro5wqHt+JuBKwD8kt+O7ASxSqs80D1y4BMfgu+We+O7cA8MWbt9eNKUjwRgGf4f75TQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bpmn-io/variable-resolver/node_modules/min-dash": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz", + "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==", + "dev": true, + "license": "MIT" + }, "node_modules/@camunda/element-templates-json-schema": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@camunda/element-templates-json-schema/-/element-templates-json-schema-0.19.0.tgz", @@ -3237,18 +3314,18 @@ "license": "MIT" }, "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", "license": "MIT" }, "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/json": { @@ -3263,9 +3340,9 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -3850,7 +3927,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/body-parser": { "version": "1.19.6", @@ -4087,7 +4165,6 @@ "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4175,7 +4252,8 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -4442,7 +4520,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4479,7 +4556,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4619,6 +4695,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -4820,7 +4897,8 @@ "resolved": "https://registry.npmjs.org/atoa/-/atoa-1.0.0.tgz", "integrity": "sha512-VVE1H6cc4ai+ZXo/CRWoJiHXrA1qfA31DPnx6D20+kSI547hQN5Greh51LQ1baMRMfxO5K5M4ImMtZbZt2DODQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -4948,6 +5026,7 @@ "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "*" }, @@ -5035,7 +5114,6 @@ "integrity": "sha512-w8ld7ainSZ9OEYDS0Tm2rrUjll2ubDFYp8RDml6/ggkpxQDnYXCJR7yfATdv1TSBWE/mvnEjpsW6ttihgp8wKw==", "dev": true, "license": "SEE LICENSE IN LICENSE", - "peer": true, "dependencies": { "bpmn-moddle": "^9.0.4", "diagram-js": "^15.4.0", @@ -5162,7 +5240,6 @@ "integrity": "sha512-WHoebcYaLJkufS7dO1TbLCF0NYAnzbEoRPz4IjJlrtXXuZgVUgADf+uep7Il4d3ZUY10xzfFkRg7qleT3JyQww==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bpmn-io/extract-process-variables": "^1.0.1", "array-move": "^4.0.0", @@ -5302,7 +5379,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -5526,7 +5602,6 @@ "integrity": "sha512-NI0BhaEWePZgAemPOII8r88UQ+56uhOh07YokfOQTyAhv3PiBsdqDMEYuexB380qphYAOP4irH6extxQE2DBLw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ids": "^1.0.0", "min-dash": "^4.2.3" @@ -5542,8 +5617,7 @@ "resolved": "https://registry.npmjs.org/camunda-bpmn-moddle/-/camunda-bpmn-moddle-7.0.1.tgz", "integrity": "sha512-Br8Diu6roMpziHdpl66Dhnm0DTnCFMrSD9zwLV08LpD52QA0UsXxU87XfHf08HjuB7ly0Hd1bvajZRpf9hbmYQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/caniuse-lite": { "version": "1.0.30001727", @@ -5572,7 +5646,6 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -6060,6 +6133,7 @@ "integrity": "sha512-N9ArHAqwR/lhPq4OdIAwH4e1btn6EIZMAz4TazjnzCiVECcWUPTma+dRAM38ERImEJBh8NiCCpjoQruSZ+agYg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "atoa": "1.0.0", "ticky": "1.0.1" @@ -6569,6 +6643,7 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -6618,7 +6693,6 @@ "integrity": "sha512-rpwDLA/w55wPfiZgp/z2T6cFdCXU3bARGdjhUpEOoh673K18OJ4ruEu3+/94ALRIIyEvJ0XHUk9sgZrzzE79ng==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bpmn-io/diagram-js-ui": "^0.2.3", "clsx": "^2.1.0", @@ -6738,7 +6812,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-converter": { "version": "0.2.0", @@ -6823,6 +6898,7 @@ "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", "dev": true, "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -6871,7 +6947,8 @@ "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/downshift": { "version": "9.0.9", @@ -7310,7 +7387,6 @@ "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8083,6 +8159,7 @@ "integrity": "sha512-Uv3YNCrmXSXgkCJSfa2jR7SL/acjPhfEHKNsaL///fRJxD7epUvUJyF1WXbh8oh9Myv+z3WVClGb+rIuT7LDow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@lezer/lr": "^1.4.2", "lezer-feel": "^1.6.0", @@ -8098,6 +8175,7 @@ "integrity": "sha512-ZaENKwVySae4RhEGjh1gEE1wMnIIPG6XqtOwHNQYSl7RNwUHoRGVVspe+BrW7cUFseHNIit3Oy9Z/HPIEU5XWA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "min-dom": "^4.0.3" } @@ -10440,6 +10518,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -10464,6 +10543,7 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -10686,7 +10766,8 @@ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/mkdirp": { "version": "0.5.6", @@ -10870,7 +10951,6 @@ "integrity": "sha512-x1+JREThy7JBOBR3g2hbOnOfrlC/YAWXX9RzrSZS5HhqeuBly9H/PCtOBtcQs+Y2sjRAXF+WTNSgHvn8Uq+6Yw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "min-dash": "^4.2.1" } @@ -11764,7 +11844,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11864,7 +11943,6 @@ "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -11907,6 +11985,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11922,6 +12001,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -12165,7 +12245,6 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12179,7 +12258,6 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12200,8 +12278,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/read-package-json-fast": { "version": "4.0.0", @@ -12723,7 +12800,6 @@ "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -13275,7 +13351,6 @@ "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -13295,7 +13370,6 @@ "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", "dev": true, "license": "(BSD-2-Clause OR WTFPL)", - "peer": true, "peerDependencies": { "chai": "^4.0.0", "sinon": ">=4.0.0" @@ -13948,7 +14022,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14047,7 +14120,8 @@ "resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz", "integrity": "sha512-RX35iq/D+lrsqhcPWIazM9ELkjOe30MSeoBHQHSsRwd1YuhJO5ui1K1/R0r7N3mFvbLBs33idw+eR6j+w6i/DA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tiny-glob": { "version": "0.2.9", @@ -14157,8 +14231,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -14607,7 +14680,6 @@ "integrity": "sha512-YJB/ESPUe2Locd0NKXmw72Dx8fZQk1gTzI6rc9TAT4+Sypbnhl8jd8RywB1bDsDF9Dy1RUR7gn3q/ZJTd0OZZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -14657,7 +14729,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -14756,7 +14827,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14872,7 +14942,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15196,7 +15265,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15685,8 +15753,7 @@ "resolved": "https://registry.npmjs.org/zeebe-bpmn-moddle/-/zeebe-bpmn-moddle-1.11.0.tgz", "integrity": "sha512-v2PkIAjyZEnzuFHrm9ZhpbEGMgNjYZkUw+H17JxkA7Da+dcbPHbD7fWuBWSbPzNSCOyYYmrH+PL6wp9407ptMg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/zod": { "version": "4.1.12", diff --git a/package.json b/package.json index 0151ea9..0910767 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@babel/core": "^7.28.0", "@babel/preset-env": "^7.28.0", "@babel/preset-react": "^7.27.1", - "@bpmn-io/variable-resolver": "^1.3.6", + "@bpmn-io/variable-resolver": "github:bpmn-io/variable-resolver#5639_variable-input-requirements", "@camunda8/orchestration-cluster-api": "^8.8.4", "@camunda8/sdk": "^8.8.3", "@carbon/icons-react": "^11.62.0", diff --git a/test/Prefill.spec.js b/test/Prefill.spec.js new file mode 100644 index 0000000..78c4800 --- /dev/null +++ b/test/Prefill.spec.js @@ -0,0 +1,160 @@ +import { bootstrapModeler, inject } from './util/Util'; + +import { ElementConfig } from '../lib/ElementConfig'; +import { ElementVariables } from '../lib/ElementVariables'; + +import diagramXML from './fixtures/prefill.bpmn'; + +describe('Prefill', function() { + + beforeEach(bootstrapModeler(diagramXML)); + + let elementConfig; + let elementVariables; + + beforeEach(inject(function(injector) { + elementVariables = new ElementVariables(injector); + elementConfig = new ElementConfig(injector, elementVariables); + })); + + + describe('#getDefaultInputForElement', function() { + + it('should compute input stubs from expression requirements', + inject(async function(elementRegistry) { + + // given + const element = elementRegistry.get('firstTask'); + + // when + const result = await elementConfig.getDefaultInputForElement(element); + const parsed = JSON.parse(result); + + // then + expect(parsed).to.have.property('a', null); + expect(parsed).to.have.property('b', null); + expect(parsed).to.not.have.property('firstResult'); + }) + ); + + + it('should only include requirements for the given element', + inject(async function(elementRegistry) { + + // given + const element = elementRegistry.get('secondTask'); + + // when + const result = await elementConfig.getDefaultInputForElement(element); + const parsed = JSON.parse(result); + + // then + expect(parsed).to.have.property('d', null); + expect(parsed).to.have.property('f', null); + expect(parsed).to.not.have.property('a'); + expect(parsed).to.not.have.property('firstResult'); + expect(parsed).to.not.have.property('secondResult'); + }) + ); + + + it('should always compute fresh result ignoring stored config', + inject(async function(elementRegistry) { + + // given + const element = elementRegistry.get('firstTask'); + + elementConfig.setInputConfigForElement(element, '{"custom": 42}'); + + // when + const result = await elementConfig.getDefaultInputForElement(element); + const parsed = JSON.parse(result); + + // then — stored config is ignored, requirements are used + expect(parsed).to.have.property('a', null); + expect(parsed).to.have.property('b', null); + expect(parsed).to.not.have.property('custom'); + }) + ); + + + it('should throw for unsupported element types', + inject(async function(elementRegistry) { + + // given + const element = elementRegistry.get('Process_prefill'); + + // when + try { + await elementConfig.getDefaultInputForElement(element); + expect.fail('should have thrown'); + } catch (error) { + + // then + expect(error.message).to.match(/Unsupported element type/); + } + }) + ); + + }); + + + describe('#getMergedInputConfigForElement', function() { + + it('should merge user values with fresh requirements', + inject(async function(elementRegistry) { + + // given + const element = elementRegistry.get('firstTask'); + + elementConfig.setInputConfigForElement(element, '{"a": 42}'); + + // when + const merged = await elementConfig.getMergedInputConfigForElement(element); + const parsed = JSON.parse(merged); + + // then — user value preserved, missing requirement added as null + expect(parsed).to.have.property('a', 42); + expect(parsed).to.have.property('b', null); + }) + ); + + + it('should strip unfilled null stubs from user input before merging', + inject(async function(elementRegistry) { + + // given + const element = elementRegistry.get('firstTask'); + + elementConfig.setInputConfigForElement(element, '{"a": null, "b": 99}'); + + // when + const merged = await elementConfig.getMergedInputConfigForElement(element); + const parsed = JSON.parse(merged); + + // then — null stub for "a" was cleaned, fresh stub re-added + expect(parsed).to.have.property('a', null); + expect(parsed).to.have.property('b', 99); + }) + ); + + + it('should return null when current input is invalid JSON', + inject(async function(elementRegistry) { + + // given + const element = elementRegistry.get('firstTask'); + + elementConfig.setInputConfigForElement(element, '{ invalid json }'); + + // when + const merged = await elementConfig.getMergedInputConfigForElement(element); + + // then + expect(merged).to.be.null; + }) + ); + + }); + +}); diff --git a/test/fixtures/prefill.bpmn b/test/fixtures/prefill.bpmn new file mode 100644 index 0000000..4bfbdc8 --- /dev/null +++ b/test/fixtures/prefill.bpmn @@ -0,0 +1,37 @@ + + + + + + + + + + + + + Flow_1 + + + + + + + Flow_1 + + + + + + + + + + + + + + + + + From 10348cbb455dad28f69923e0a7e1b58e605e2bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Steinr=C3=BCcken?= Date: Thu, 19 Feb 2026 14:48:05 +0100 Subject: [PATCH 2/6] feat: adjust clear button to undoable reset to default prefill Closes https://github.com/camunda/task-testing/issues/76 --- lib/components/Input/Input.jsx | 17 ++- lib/components/Input/InputEditor.jsx | 9 +- lib/utils/prefill.js | 2 +- test/ElementConfig.spec.js | 4 +- test/components/Input/Input.spec.js | 10 +- test/components/Input/InputEditor.spec.js | 131 ++++++++++++++++++++++ 6 files changed, 158 insertions(+), 15 deletions(-) diff --git a/lib/components/Input/Input.jsx b/lib/components/Input/Input.jsx index 3940fd5..6d623e9 100644 --- a/lib/components/Input/Input.jsx +++ b/lib/components/Input/Input.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { Link } from '@carbon/react'; import { Launch } from '@carbon/icons-react'; @@ -13,12 +13,19 @@ export default function Input({ onSetInput, variablesForElement }) { - const handleResetInput = () => { + + const containerRef = /** @type {import('react').RefObject} */ (useRef(null)); + + const handleReset = () => { onResetInput(); + const cmContent = /** @type {HTMLElement | undefined} */ ( + containerRef.current?.querySelector('.cm-content') + ); + cmContent?.focus(); }; return ( -
+
Input process variables @@ -28,8 +35,8 @@ export default function Input({ Clear + onClick={ handleReset } + role="button">Reset
{}, - reset = () => {}, + onSetInput = () => {}, + onResetInput = () => {}, + onErrorChange = () => {}, variablesForElement, output, onRunTask = () => {} @@ -49,8 +50,9 @@ function renderWithProps(props) { }z{/Meta}' : '{Control>}z{/Control}'; + const redoKeys = isMac ? '{Meta>}{Shift>}z{/Shift}{/Meta}' : '{Control>}y{/Control}'; + + + it('should undo typing', async function() { + + // given + const onChangeSpy = sinon.spy(); + + const { getByRole } = renderWithProps({ + value: '{}', + onChange: onChangeSpy + }); + + const textbox = getByRole('textbox'); + await user.click(textbox); + + // when - type something + await user.keyboard('{ArrowRight}{Enter}"a": 1'); + + // assume - typing happened + await waitFor(() => { + expect(onChangeSpy).to.have.been.called; + }); + + const valueAfterTyping = onChangeSpy.lastCall.args[0]; + expect(valueAfterTyping).to.contain('"a": 1'); + + onChangeSpy.resetHistory(); + + // when - undo + await user.keyboard(undoKeys); + + // then - onChange should fire with reverted content + await waitFor(() => { + expect(onChangeSpy).to.have.been.called; + }); + }); + + + it('should undo reset', async function() { + + // given + const onChangeSpy = sinon.spy(); + const originalValue = '{\n "foo": "bar"\n}'; + const resetValue = '{}'; + + const { container, getByRole, rerender } = renderWithProps({ + value: originalValue, + onChange: onChangeSpy + }); + + // assume - editor shows original value + expect(container.textContent).to.contain('"foo": "bar"'); + + // when - simulate reset by changing value prop + rerender( + {} } + /> + ); + + await waitFor(() => { + expect(container.textContent).to.not.contain('"foo": "bar"'); + }); + + // when - undo the reset + const textbox = getByRole('textbox'); + await user.click(textbox); + await user.keyboard(undoKeys); + + // then - editor should revert to original value + await waitFor(() => { + expect(onChangeSpy).to.have.been.calledWith(originalValue); + }); + }); + + + it('should redo after undo', async function() { + + // given + const onChangeSpy = sinon.spy(); + const originalValue = '{\n "foo": "bar"\n}'; + const resetValue = '{}'; + + const { container, getByRole, rerender } = renderWithProps({ + value: originalValue, + onChange: onChangeSpy + }); + + // reset + rerender( + {} } + /> + ); + + await waitFor(() => { + expect(container.textContent).to.not.contain('"foo": "bar"'); + }); + + // undo + const textbox = getByRole('textbox'); + await user.click(textbox); + await user.keyboard(undoKeys); + + await waitFor(() => { + expect(onChangeSpy).to.have.been.calledWith(originalValue); + }); + + onChangeSpy.resetHistory(); + + // when - redo + await user.keyboard(redoKeys); + + // then - should go back to reset value + await waitFor(() => { + expect(onChangeSpy).to.have.been.calledWith(resetValue); + }); + }); + + }); + }); function renderWithProps(props = {}) { From 604d7f8f6528a7a715c8d8ab42a034002e108672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Steinr=C3=BCcken?= Date: Thu, 19 Feb 2026 16:22:49 +0100 Subject: [PATCH 3/6] chore: update for filtered vars --- package-lock.json | 2 +- test/Prefill.spec.js | 19 +++++++++++++++++++ test/fixtures/prefill.bpmn | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 5fda141..c462760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2068,7 +2068,7 @@ }, "node_modules/@bpmn-io/variable-resolver": { "version": "1.5.0", - "resolved": "git+ssh://git@github.com/bpmn-io/variable-resolver.git#98bf692e60654f4572a4d5711d868f6155cf7eaa", + "resolved": "git+ssh://git@github.com/bpmn-io/variable-resolver.git#94b7b8e0c8a51dbb5cdb8a0586ef6be09fe13bad", "dev": true, "license": "MIT", "dependencies": { diff --git a/test/Prefill.spec.js b/test/Prefill.spec.js index 78c4800..f33dc10 100644 --- a/test/Prefill.spec.js +++ b/test/Prefill.spec.js @@ -96,6 +96,25 @@ describe('Prefill', function() { }) ); + + it('should only prefill process variable from input mapping, not locally mapped script variable', + inject(async function(elementRegistry) { + + // given - taskWithInputMapping has input mapping fooInput=foo and script =fooInput + const element = elementRegistry.get('taskWithInputMapping'); + + // when + const result = await elementConfig.getDefaultInputForElement(element); + const parsed = JSON.parse(result); + + // then - only `foo` (from input mapping source) should be prefilled; + // `fooInput` is provided by the input mapping, not an external requirement + expect(parsed).to.have.property('foo', null); + expect(parsed).to.not.have.property('fooInput'); + expect(parsed).to.not.have.property('mappedResult'); + }) + ); + }); diff --git a/test/fixtures/prefill.bpmn b/test/fixtures/prefill.bpmn index 4bfbdc8..98ecee9 100644 --- a/test/fixtures/prefill.bpmn +++ b/test/fixtures/prefill.bpmn @@ -18,6 +18,17 @@ Flow_1 + Flow_2 + + + + + + + + + + Flow_2 @@ -28,10 +39,17 @@ + + + + + + + From b2fbf3e387aff3d43b08bea0c893221066ec7950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Steinr=C3=BCcken?= Date: Thu, 19 Feb 2026 16:39:15 +0100 Subject: [PATCH 4/6] fix: prevent undoing initial fill --- lib/components/Input/InputEditor.jsx | 19 +++++- test/components/Input/InputEditor.spec.js | 77 +++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/lib/components/Input/InputEditor.jsx b/lib/components/Input/InputEditor.jsx index 4bbd75a..e9da237 100644 --- a/lib/components/Input/InputEditor.jsx +++ b/lib/components/Input/InputEditor.jsx @@ -4,7 +4,7 @@ import { renderToStaticMarkup } from 'react-dom/server'; import { autocompletion, closeBrackets } from '@codemirror/autocomplete'; import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; import { bracketMatching, indentOnInput } from '@codemirror/language'; -import { Compartment, EditorState, Annotation } from '@codemirror/state'; +import { Compartment, EditorState, Annotation, Transaction } from '@codemirror/state'; import { EditorView, keymap, placeholder } from '@codemirror/view'; import { linter } from '@codemirror/lint'; import { json, jsonParseLinter } from '@codemirror/lang-json'; @@ -66,6 +66,9 @@ export default function InputEditor({ const ref = useRef(null); + /** @type {import('react').MutableRefObject} */ + const initializedViewRef = useRef(null); + /** * @type {ReturnType>} */ @@ -137,6 +140,10 @@ export default function InputEditor({ setEditorView(view); + if (value) { + initializedViewRef.current = view; + } + return () => { view.destroy(); }; @@ -158,13 +165,21 @@ export default function InputEditor({ const editorValue = editorView.state.doc.toString(); if (value !== editorValue) { + const isInitialFill = initializedViewRef.current !== editorView; + if (value) { + initializedViewRef.current = editorView; + } + editorView.dispatch({ changes: { from: 0, to: editorValue.length, insert: value }, - annotations: fromPropAnnotation.of(true) + annotations: [ + fromPropAnnotation.of(true), + ...isInitialFill ? [ Transaction.addToHistory.of(false) ] : [] + ] }); } }, [ editorView, value ]); diff --git a/test/components/Input/InputEditor.spec.js b/test/components/Input/InputEditor.spec.js index 99a721b..a46d3e8 100644 --- a/test/components/Input/InputEditor.spec.js +++ b/test/components/Input/InputEditor.spec.js @@ -441,6 +441,83 @@ describe('InputEditor', function() { }); }); + + it('should not undo the initial fill', async function() { + + // given - editor starts without value, then receives async prefill + const onChangeSpy = sinon.spy(); + const prefillValue = '{\n "foo": "bar"\n}'; + + const { container, getByRole, rerender } = renderWithProps({ + onChange: onChangeSpy + }); + + // simulate async prefill arriving + rerender( + {} } + /> + ); + + await waitFor(() => { + expect(container.textContent).to.contain('"foo": "bar"'); + }); + + // when - try to undo the prefill + const textbox = getByRole('textbox'); + await user.click(textbox); + await user.keyboard(undoKeys); + + // then - prefill should remain (not undoable) + await new Promise(resolve => setTimeout(resolve, 100)); + expect(onChangeSpy).to.not.have.been.called; + expect(container.textContent).to.contain('"foo": "bar"'); + }); + + + it('should not undo past the initial value', async function() { + + // given - editor starts with value, user types something + const onChangeSpy = sinon.spy(); + const initialValue = '{\n "foo": "bar"\n}'; + + const { container, getByRole } = renderWithProps({ + value: initialValue, + onChange: onChangeSpy + }); + + const textbox = getByRole('textbox'); + await user.click(textbox); + + // type something + await user.keyboard('{ArrowRight}{Enter}"a": 1'); + + await waitFor(() => { + expect(onChangeSpy).to.have.been.called; + }); + + onChangeSpy.resetHistory(); + + // when - undo typing + await user.keyboard(undoKeys); + + await waitFor(() => { + expect(onChangeSpy).to.have.been.calledWith(initialValue); + }); + + onChangeSpy.resetHistory(); + + // when - undo again (should not go to empty) + await user.keyboard(undoKeys); + + // then - should stay at initial value + await new Promise(resolve => setTimeout(resolve, 100)); + expect(onChangeSpy).to.not.have.been.called; + expect(container.textContent).to.contain('"foo": "bar"'); + }); + }); }); From 9feb05762e6012297357b6662319d0a7a7286f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Steinr=C3=BCcken?= Date: Thu, 26 Feb 2026 13:09:24 +0100 Subject: [PATCH 5/6] chore: update for changed interface --- lib/ElementVariables.js | 4 ++-- lib/utils/prefill.js | 2 +- package-lock.json | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ElementVariables.js b/lib/ElementVariables.js index 5283a44..22c8bd7 100644 --- a/lib/ElementVariables.js +++ b/lib/ElementVariables.js @@ -43,8 +43,8 @@ export class ElementVariables extends EventEmitter { * @param {Object} element * @returns {Promise} */ - async getInputRequirementsForElement(element) { - return this._variableResolver.getInputRequirementsForElement(element) + async getConsumedVariablesForElement(element) { + return this._variableResolver.getConsumedVariablesForElement(element) .catch(() => { return []; }); diff --git a/lib/utils/prefill.js b/lib/utils/prefill.js index ad8b1a0..52e537d 100644 --- a/lib/utils/prefill.js +++ b/lib/utils/prefill.js @@ -76,7 +76,7 @@ export async function computeMergedInput(currentInputString, elementVariables, e */ async function buildRequirementsStub(elementVariables, element) { const requirements = await elementVariables - .getInputRequirementsForElement(element); + .getConsumedVariablesForElement(element); if (!requirements || requirements.length === 0) { return {}; diff --git a/package-lock.json b/package-lock.json index c462760..c2dddf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2067,12 +2067,12 @@ } }, "node_modules/@bpmn-io/variable-resolver": { - "version": "1.5.0", - "resolved": "git+ssh://git@github.com/bpmn-io/variable-resolver.git#94b7b8e0c8a51dbb5cdb8a0586ef6be09fe13bad", + "version": "1.6.2", + "resolved": "git+ssh://git@github.com/bpmn-io/variable-resolver.git#9e90ca4e0d37896132a1ba04cdfc7216d1ee942b", "dev": true, "license": "MIT", "dependencies": { - "@bpmn-io/extract-process-variables": "^2.1.0", + "@bpmn-io/extract-process-variables": "^2.2.0", "@bpmn-io/feel-analyzer": "^0.1.0", "@bpmn-io/lezer-feel": "^2.3.0", "@camunda/feel-builtins": "^1.0.0", @@ -2084,9 +2084,9 @@ } }, "node_modules/@bpmn-io/variable-resolver/node_modules/@bpmn-io/extract-process-variables": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@bpmn-io/extract-process-variables/-/extract-process-variables-2.1.0.tgz", - "integrity": "sha512-Z5iwYHvo58GnEC2QAg53dymOb4o1r6x5Q4PvWk7du74z/HeS3x5VUDnozc4CDIugYMCe2j+RJV7CUBMbn48HJg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@bpmn-io/extract-process-variables/-/extract-process-variables-2.2.0.tgz", + "integrity": "sha512-I3yboi1uBhZo+3+hjAqoogPuBHayp8mGyGJ8/sNxf530DegBICqsI8eRdj6HnN94Y/DtWpcV8XJGBoxX6BtWcg==", "dev": true, "license": "MIT", "dependencies": { From 19531fa2018b7eed8cd32242353fd6ddded3755f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Steinr=C3=BCcken?= Date: Mon, 2 Mar 2026 14:01:12 +0100 Subject: [PATCH 6/6] debug: allow linking major version change TODO remove before merging --- package-lock.json | 12 ++++++------ package.json | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2dddf3..6a845fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2067,12 +2067,12 @@ } }, "node_modules/@bpmn-io/variable-resolver": { - "version": "1.6.2", - "resolved": "git+ssh://git@github.com/bpmn-io/variable-resolver.git#9e90ca4e0d37896132a1ba04cdfc7216d1ee942b", + "version": "2.0.0", + "resolved": "git+ssh://git@github.com/bpmn-io/variable-resolver.git#f4de80707c8d02e2ad21c26297bd8d852bb52197", "dev": true, "license": "MIT", "dependencies": { - "@bpmn-io/extract-process-variables": "^2.2.0", + "@bpmn-io/extract-process-variables": "^2.2.1", "@bpmn-io/feel-analyzer": "^0.1.0", "@bpmn-io/lezer-feel": "^2.3.0", "@camunda/feel-builtins": "^1.0.0", @@ -2084,9 +2084,9 @@ } }, "node_modules/@bpmn-io/variable-resolver/node_modules/@bpmn-io/extract-process-variables": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@bpmn-io/extract-process-variables/-/extract-process-variables-2.2.0.tgz", - "integrity": "sha512-I3yboi1uBhZo+3+hjAqoogPuBHayp8mGyGJ8/sNxf530DegBICqsI8eRdj6HnN94Y/DtWpcV8XJGBoxX6BtWcg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@bpmn-io/extract-process-variables/-/extract-process-variables-2.2.1.tgz", + "integrity": "sha512-1E5ydNzTqgx513NxA1nxk2CqDb9OkusfU9Tf6WejD1BiY+5u130XnZOFE/FQX0JB6ZmPOeFMx4IDEuHjrGhOzw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 0910767..7907080 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "react-dom": "*" }, "overrides": { - "react-is": "^18.0.0" + "react-is": "^18.0.0", + "@bpmn-io/variable-resolver": "$@bpmn-io/variable-resolver" } }