Skip to content

Commit e88295e

Browse files
committed
feat: prefill context input with needed feel variables
Closes camunda/camunda-modeler#5640
1 parent fb82ba6 commit e88295e

File tree

8 files changed

+595
-90
lines changed

8 files changed

+595
-90
lines changed

lib/ElementConfig.js

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import { isAny } from 'bpmn-js/lib/util/ModelUtil';
44

55
import { isString, omit } from 'min-dash';
66

7+
import {
8+
DEFAULT_INPUT_CONFIG,
9+
computeDefaultInput,
10+
computeMergedInput
11+
} from './utils/prefill';
12+
13+
export { DEFAULT_INPUT_CONFIG };
14+
715
export const DEFAULT_CONFIG = {
816
input: {},
917
output: {}
@@ -28,8 +36,6 @@ export class ElementConfig extends EventEmitter {
2836
...config
2937
};
3038

31-
this._selectedElement = null;
32-
this._variablesForElements = new Map();
3339
}
3440

3541
setConfig(newConfig) {
@@ -43,9 +49,7 @@ export class ElementConfig extends EventEmitter {
4349
}
4450

4551
setInputConfigForElement(element, newConfig) {
46-
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
47-
throw new Error(`Unsupported element type: ${element.type}`);
48-
}
52+
this._assertSupportedElement(element);
4953

5054
this._config = {
5155
...this._config,
@@ -59,9 +63,7 @@ export class ElementConfig extends EventEmitter {
5963
}
6064

6165
resetInputConfigForElement(element) {
62-
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
63-
throw new Error(`Unsupported element type: ${element.type}`);
64-
}
66+
this._assertSupportedElement(element);
6567

6668
this._config = {
6769
...this._config,
@@ -72,9 +74,7 @@ export class ElementConfig extends EventEmitter {
7274
}
7375

7476
setOutputConfigForElement(element, newConfig) {
75-
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
76-
throw new Error(`Unsupported element type: ${element.type}`);
77-
}
77+
this._assertSupportedElement(element);
7878

7979
this._config = {
8080
...this._config,
@@ -88,9 +88,7 @@ export class ElementConfig extends EventEmitter {
8888
}
8989

9090
resetOutputConfigForElement(element) {
91-
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
92-
throw new Error(`Unsupported element type: ${element.type}`);
93-
}
91+
this._assertSupportedElement(element);
9492

9593
this._config = {
9694
...this._config,
@@ -101,25 +99,55 @@ export class ElementConfig extends EventEmitter {
10199
}
102100

103101
getInputConfigForElement(element) {
104-
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
105-
throw new Error(`Unsupported element type: ${element.type}`);
106-
}
102+
this._assertSupportedElement(element);
107103

108104
if (!isString(this._config.input[element.id])) {
109-
return this._getDefaultInputConfig();
105+
return DEFAULT_INPUT_CONFIG;
110106
}
111107

112108
return this._config.input[element.id];
113109
}
114110

111+
/**
112+
* Computes a fresh input config from the element's input requirements,
113+
* ignoring any stored user config. Used for resetting input to defaults.
114+
*
115+
* @param {Object} element
116+
* @returns {Promise<string>} JSON string
117+
*/
118+
async getDefaultInputForElement(element) {
119+
this._assertSupportedElement(element);
120+
121+
return computeDefaultInput(this._elementVariables, element);
122+
}
123+
124+
/**
125+
* Merges current user input with fresh input requirements from the element.
126+
* Removes null values (unfilled stubs) from user input, then adds new
127+
* requirement stubs for any variables not yet present.
128+
*
129+
* Returns `null` when the current input is invalid JSON, signalling that
130+
* no merge was possible and the caller should skip overwriting the config.
131+
*
132+
* @param {Object} element
133+
* @returns {Promise<string|null>} merged JSON string, or null if current input is unparseable
134+
*/
135+
async getMergedInputConfigForElement(element) {
136+
this._assertSupportedElement(element);
137+
138+
return computeMergedInput(
139+
this._config.input[element.id],
140+
this._elementVariables,
141+
element
142+
);
143+
}
144+
115145
/**
116146
* @param {import('./types').Element} element
117147
* @returns {import('./types').ElementOutput}
118148
*/
119149
getOutputConfigForElement(element) {
120-
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
121-
throw new Error(`Unsupported element type: ${element.type}`);
122-
}
150+
this._assertSupportedElement(element);
123151

124152
if (!this._config.output[element.id]) {
125153
return DEFAULT_OUTPUT;
@@ -128,7 +156,13 @@ export class ElementConfig extends EventEmitter {
128156
return this._config.output[element.id];
129157
}
130158

131-
_getDefaultInputConfig() {
132-
return '{}';
159+
/**
160+
* @param {Object} element
161+
* @throws {Error} if the element type is not supported
162+
*/
163+
_assertSupportedElement(element) {
164+
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
165+
throw new Error(`Unsupported element type: ${element.type}`);
166+
}
133167
}
134-
}
168+
}

lib/ElementVariables.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,18 @@ export class ElementVariables extends EventEmitter {
3535

3636
return variablesWithoutLocal;
3737
}
38-
}
38+
39+
/**
40+
* Returns input requirement variables for an element — variables
41+
* the element needs as input for its expressions and mappings.
42+
*
43+
* @param {Object} element
44+
* @returns {Promise<Array>}
45+
*/
46+
async getInputRequirementsForElement(element) {
47+
return this._variableResolver.getInputRequirementsForElement(element)
48+
.catch(() => {
49+
return [];
50+
});
51+
}
52+
}

lib/components/TaskTesting/TaskTesting.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ export default function TaskTesting({
159159
const variables = await elementVariablesRef.current.getVariablesForElement(element);
160160

161161
setVariablesForElement(variables);
162+
163+
// Merge updated input requirements into the current input
164+
if (elementConfigRef.current) {
165+
const mergedInput = await elementConfigRef.current.getMergedInputConfigForElement(element);
166+
167+
// Skip update when merge was not possible (e.g. invalid JSON)
168+
if (mergedInput !== null) {
169+
elementConfigRef.current.setInputConfigForElement(element, mergedInput);
170+
}
171+
}
162172
};
163173

164174
elementVariablesRef.current.on('variables.changed', handleVariablesChanged);
@@ -272,7 +282,13 @@ export default function TaskTesting({
272282
return;
273283
}
274284

275-
setInput(elementConfigRef?.current?.getInputConfigForElement(element));
285+
elementConfigRef?.current?.getMergedInputConfigForElement(element).then(
286+
merged => {
287+
if (merged !== null) {
288+
setInput(merged);
289+
}
290+
}
291+
);
276292
setOutput(elementConfigRef?.current?.getOutputConfigForElement(element));
277293
}, [ element ]);
278294

@@ -284,9 +300,10 @@ export default function TaskTesting({
284300
}
285301
}, [ element ]);
286302

287-
const handleResetInput = useCallback(() => {
303+
const handleResetInput = useCallback(async () => {
288304
if (element && elementConfigRef.current) {
289-
elementConfigRef.current.resetInputConfigForElement(element);
305+
const prefilled = await elementConfigRef.current.getDefaultInputForElement(element);
306+
elementConfigRef.current.setInputConfigForElement(element, prefilled);
290307
}
291308
}, [ element ]);
292309

lib/utils/prefill.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { has, isObject, isString } from 'min-dash';
2+
3+
export const DEFAULT_INPUT_CONFIG = '{}';
4+
5+
6+
/**
7+
* Compute a default input config from the element's input requirements,
8+
* producing a JSON string with `null` stubs for each required variable.
9+
*
10+
* @param {Object} elementVariables
11+
* @param {Object} element
12+
* @returns {Promise<string>} JSON string
13+
*/
14+
export async function computeDefaultInput(elementVariables, element) {
15+
const stub = await buildRequirementsStub(elementVariables, element);
16+
17+
if (Object.keys(stub).length === 0) {
18+
return DEFAULT_INPUT_CONFIG;
19+
}
20+
21+
return JSON.stringify(stub, null, 2);
22+
}
23+
24+
25+
/**
26+
* Merge current user input with fresh input requirements from the element.
27+
* Removes null values (unfilled stubs) from user input, then adds new
28+
* requirement stubs for any variables not yet present.
29+
*
30+
* Returns `null` when the current input is invalid JSON, signalling that
31+
* no merge was possible and the caller should skip overwriting the config.
32+
*
33+
* @param {string} currentInputString - stored JSON string (or undefined)
34+
* @param {Object} elementVariables
35+
* @param {Object} element
36+
* @returns {Promise<string|null>} merged JSON string, or null if unparseable
37+
*/
38+
export async function computeMergedInput(currentInputString, elementVariables, element) {
39+
const requirementsStub = await buildRequirementsStub(elementVariables, element);
40+
41+
const inputString = isString(currentInputString)
42+
? currentInputString
43+
: DEFAULT_INPUT_CONFIG;
44+
45+
let currentConfig;
46+
try {
47+
currentConfig = JSON.parse(inputString);
48+
} catch (e) {
49+
50+
// If user input is invalid JSON, signal that no merge is possible
51+
return null;
52+
}
53+
54+
// Remove null values from user input, then merge with requirements
55+
const cleaned = removeNullValues(currentConfig);
56+
const merged = mergeObjects(requirementsStub, cleaned);
57+
58+
if (Object.keys(merged).length === 0) {
59+
return DEFAULT_INPUT_CONFIG;
60+
}
61+
62+
return JSON.stringify(merged, null, 2);
63+
}
64+
65+
66+
// helpers //////////////////////
67+
68+
/**
69+
* Build a stub object from the element's input requirements.
70+
* Each requirement variable becomes a key with `null` (or a nested
71+
* object for context variables).
72+
*
73+
* @param {Object} elementVariables
74+
* @param {Object} element
75+
* @returns {Promise<Object>} requirements stub
76+
*/
77+
async function buildRequirementsStub(elementVariables, element) {
78+
const requirements = await elementVariables
79+
.getInputRequirementsForElement(element);
80+
81+
if (!requirements || requirements.length === 0) {
82+
return {};
83+
}
84+
85+
const stub = {};
86+
87+
for (const variable of requirements) {
88+
stub[variable.name] = variableToStub(variable);
89+
}
90+
91+
return stub;
92+
}
93+
94+
/**
95+
* Convert a variable with entries (nested context) into a JSON stub value.
96+
* Produces nested objects for context variables, `null` for leaves.
97+
*
98+
* @param {Object} variable
99+
* @returns {*} stub value
100+
*/
101+
function variableToStub(variable) {
102+
if (variable.entries && variable.entries.length > 0) {
103+
const result = {};
104+
105+
for (const entry of variable.entries) {
106+
result[entry.name] = variableToStub(entry);
107+
}
108+
109+
return result;
110+
}
111+
112+
return null;
113+
}
114+
115+
/**
116+
* Recursively remove all null values from an object.
117+
* Removes keys whose value is null, and recurses into nested objects.
118+
* If all keys are removed, returns an empty object.
119+
*
120+
* @param {Object} obj
121+
* @returns {Object}
122+
*/
123+
function removeNullValues(obj) {
124+
if (!isObject(obj)) {
125+
return obj;
126+
}
127+
128+
const result = {};
129+
130+
for (const key in obj) {
131+
if (!has(obj, key)) continue;
132+
133+
const value = obj[key];
134+
135+
if (value === null) continue;
136+
137+
if (isObject(value)) {
138+
const cleaned = removeNullValues(value);
139+
140+
if (Object.keys(cleaned).length > 0) {
141+
result[key] = cleaned;
142+
}
143+
} else {
144+
result[key] = value;
145+
}
146+
}
147+
148+
return result;
149+
}
150+
151+
/**
152+
* Merge two objects: base provides the structure (stubs), override
153+
* provides user values. User values take precedence.
154+
*
155+
* @param {Object} base - requirements stub (may contain null values)
156+
* @param {Object} override - user input (cleaned of nulls)
157+
* @returns {Object}
158+
*/
159+
function mergeObjects(base, override) {
160+
const result = { ...base };
161+
162+
for (const key in override) {
163+
if (!has(override, key)) continue;
164+
165+
const overrideValue = override[key];
166+
const baseValue = result[key];
167+
168+
if (isObject(overrideValue) && isObject(baseValue)) {
169+
result[key] = mergeObjects(baseValue, overrideValue);
170+
} else {
171+
result[key] = overrideValue;
172+
}
173+
}
174+
175+
return result;
176+
}

0 commit comments

Comments
 (0)