Skip to content

Commit 700c97d

Browse files
committed
wip
1 parent 5123f38 commit 700c97d

File tree

11 files changed

+553
-254
lines changed

11 files changed

+553
-254
lines changed

demo/fixtures/diagram.bpmn

Lines changed: 22 additions & 197 deletions
Large diffs are not rendered by default.

lib/ElementConfig.js

Lines changed: 230 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import EventEmitter from 'events';
22

33
import { isAny } from 'bpmn-js/lib/util/ModelUtil';
44

5-
import { isString, omit } from 'min-dash';
5+
import { has, isObject, isString, omit } from 'min-dash';
66

77
export const DEFAULT_CONFIG = {
88
input: {},
@@ -112,6 +112,113 @@ export class ElementConfig extends EventEmitter {
112112
return this._config.input[element.id];
113113
}
114114

115+
/**
116+
* Returns a prefilled input config for the given element based on
117+
* the input requirements extracted from its expressions.
118+
* If the element has no stored config, the returned JSON will contain
119+
* stub entries for all variables referenced in input mappings / scripts.
120+
*
121+
* @param {Object} element
122+
* @returns {Promise<string>} JSON string
123+
*/
124+
async getPrefilledInputConfigForElement(element) {
125+
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
126+
throw new Error(`Unsupported element type: ${element.type}`);
127+
}
128+
129+
// If user already has a stored config, return it as-is
130+
if (isString(this._config.input[element.id])) {
131+
return this._config.input[element.id];
132+
}
133+
134+
return this._computePrefilledInput(element);
135+
}
136+
137+
/**
138+
* Always computes a fresh prefilled input config from the element's
139+
* input requirements, ignoring any stored user config.
140+
*
141+
* @param {Object} element
142+
* @returns {Promise<string>} JSON string
143+
*/
144+
async getAutoPrefilledInputForElement(element) {
145+
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
146+
throw new Error(`Unsupported element type: ${element.type}`);
147+
}
148+
149+
return this._computePrefilledInput(element);
150+
}
151+
152+
/**
153+
* @param {Object} element
154+
* @returns {Promise<string>} JSON string
155+
*/
156+
async _computePrefilledInput(element) {
157+
const requirements = await this._elementVariables
158+
.getInputRequirementsForElement(element);
159+
160+
if (!requirements || requirements.length === 0) {
161+
return this._getDefaultInputConfig();
162+
}
163+
164+
const prefill = {};
165+
166+
for (const variable of requirements) {
167+
prefill[variable.name] = variableToStub(variable);
168+
}
169+
170+
return JSON.stringify(prefill, null, 2);
171+
}
172+
173+
/**
174+
* Merges current user input with fresh input requirements from the element.
175+
* Removes null values (unfilled stubs) from user input, then adds new
176+
* requirement stubs for any variables not yet present.
177+
*
178+
* Returns `null` when the current input is invalid JSON, signalling that
179+
* no merge was possible and the caller should skip overwriting the config.
180+
*
181+
* @param {Object} element
182+
* @returns {Promise<string|null>} merged JSON string, or null if current input is unparseable
183+
*/
184+
async getMergedInputConfigForElement(element) {
185+
if (!isAny(element, SUPPORTED_ELEMENT_TYPES)) {
186+
throw new Error(`Unsupported element type: ${element.type}`);
187+
}
188+
189+
const requirements = await this._elementVariables
190+
.getInputRequirementsForElement(element);
191+
192+
// Build the requirements stub
193+
const requirementsStub = {};
194+
195+
if (requirements && requirements.length > 0) {
196+
for (const variable of requirements) {
197+
requirementsStub[variable.name] = variableToStub(variable);
198+
}
199+
}
200+
201+
// Parse current user input
202+
const currentConfigString = isString(this._config.input[element.id])
203+
? this._config.input[element.id]
204+
: '{}';
205+
206+
let currentConfig;
207+
try {
208+
currentConfig = JSON.parse(currentConfigString);
209+
} catch (e) {
210+
211+
// If user input is invalid JSON, signal that no merge is possible
212+
return null;
213+
}
214+
215+
// Remove null values from user input, then merge with requirements
216+
const cleaned = removeNullValues(currentConfig);
217+
const merged = mergeObjects(requirementsStub, cleaned);
218+
219+
return JSON.stringify(merged, null, 2);
220+
}
221+
115222
/**
116223
* @param {import('./types').Element} element
117224
* @returns {import('./types').ElementOutput}
@@ -131,4 +238,126 @@ export class ElementConfig extends EventEmitter {
131238
_getDefaultInputConfig() {
132239
return '{}';
133240
}
241+
}
242+
243+
244+
// helpers //////////////////////
245+
246+
/**
247+
* Convert a variable with entries (nested context) into a JSON stub value.
248+
* Uses `info` (example/computed value) and `type` to produce a typed value
249+
* instead of null when available.
250+
*
251+
* @param {Object} variable
252+
* @returns {*} stub value
253+
*/
254+
function variableToStub(variable) {
255+
if (variable.entries && variable.entries.length > 0) {
256+
const result = {};
257+
258+
for (const entry of variable.entries) {
259+
result[entry.name] = variableToStub(entry);
260+
}
261+
262+
return result;
263+
}
264+
265+
// Use example/computed value from variable intelligence when available
266+
if (variable.info) {
267+
return infoToValue(variable.info, variable.type || variable.detail);
268+
}
269+
270+
return null;
271+
}
272+
273+
/**
274+
* Convert a variable's info string to a typed JSON value based on its type.
275+
*
276+
* @param {string} info - string representation of the value
277+
* @param {string} [type] - type hint (e.g. "Number", "Boolean", "String")
278+
* @returns {*} typed value
279+
*/
280+
function infoToValue(info, type) {
281+
switch (type) {
282+
case 'Number': {
283+
const num = Number(info);
284+
return isNaN(num) ? info : num;
285+
}
286+
case 'Boolean':
287+
return info === 'true';
288+
case 'String':
289+
return info;
290+
default: {
291+
292+
// Try to parse as JSON for objects/arrays
293+
try {
294+
return JSON.parse(info);
295+
} catch (e) {
296+
return info;
297+
}
298+
}
299+
}
300+
}
301+
302+
/**
303+
* Recursively remove all null values from an object.
304+
* Removes keys whose value is null, and recurses into nested objects.
305+
* If all keys are removed, returns an empty object.
306+
*
307+
* @param {Object} obj
308+
* @returns {Object}
309+
*/
310+
function removeNullValues(obj) {
311+
if (!isObject(obj)) {
312+
return obj;
313+
}
314+
315+
const result = {};
316+
317+
for (const key in obj) {
318+
if (!has(obj, key)) continue;
319+
320+
const value = obj[key];
321+
322+
if (value === null) continue;
323+
324+
if (isObject(value)) {
325+
const cleaned = removeNullValues(value);
326+
327+
if (Object.keys(cleaned).length > 0) {
328+
result[key] = cleaned;
329+
}
330+
} else {
331+
result[key] = value;
332+
}
333+
}
334+
335+
return result;
336+
}
337+
338+
/**
339+
* Merge two objects: base provides the structure (stubs), override
340+
* provides user values. User values take precedence.
341+
*
342+
* @param {Object} base - requirements stub (may contain null values)
343+
* @param {Object} override - user input (cleaned of nulls)
344+
* @returns {Object}
345+
*/
346+
function mergeObjects(base, override) {
347+
const result = { ...base };
348+
349+
for (const key in override) {
350+
if (!has(override, key)) continue;
351+
352+
const overrideValue = override[key];
353+
const baseValue = result[key];
354+
355+
if (isObject(overrideValue) && isObject(baseValue)) {
356+
result[key] = mergeObjects(baseValue, overrideValue);
357+
} else {
358+
result[key] = overrideValue;
359+
}
360+
}
361+
362+
return result;
134363
}

lib/ElementVariables.js

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

3636
return variablesWithoutLocal;
3737
}
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+
}
3849
}

lib/components/Input/Input.jsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useRef } from 'react';
22

33
import { Link } from '@carbon/react';
44
import { Launch } from '@carbon/icons-react';
@@ -13,8 +13,11 @@ export default function Input({
1313
onSetInput,
1414
variablesForElement
1515
}) {
16-
const handleResetInput = () => {
17-
onResetInput();
16+
const editorRef = useRef(null);
17+
18+
const handleResetInput = async () => {
19+
const prefilledValue = await onResetInput();
20+
editorRef.current?.replaceContent(prefilledValue);
1821
};
1922

2023
return (
@@ -29,9 +32,10 @@ export default function Input({
2932
<Link
3033
className="input__header--button-reset"
3134
onClick={ handleResetInput }
32-
role="button">Clear</Link>
35+
role="button">Reset</Link>
3336
</div>
3437
<InputEditor
38+
ref={ editorRef }
3539
allOutputs={ allOutputs }
3640
value={ input }
3741
onChange={ onSetInput }

0 commit comments

Comments
 (0)