@@ -2,7 +2,7 @@ import EventEmitter from 'events';
22
33import { 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
77export 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}
0 commit comments