@bgroup/wise-form 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/form/view/index.tsx
CHANGED
|
@@ -2,10 +2,20 @@ import React from 'react';
|
|
|
2
2
|
import { useModel } from './hooks/use-model';
|
|
3
3
|
import { WiseFormContext } from './context';
|
|
4
4
|
import { useTypes } from './hooks/use-types';
|
|
5
|
+
import type { FormModel } from '@bgroup/wise-form/models';
|
|
5
6
|
|
|
6
7
|
import { IWiseFormSpecs } from '../interfaces/wise-form-specs';
|
|
7
8
|
import { Containers } from './components/containers';
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Interfaz extendida para FormModel que puede tener el método onSubmit
|
|
12
|
+
* Esto permite que las implementaciones extendidas de FormModel (como en frontend)
|
|
13
|
+
* puedan usar el método onSubmit sin errores de tipado
|
|
14
|
+
*/
|
|
15
|
+
interface IFormModelWithSubmit extends FormModel {
|
|
16
|
+
onSubmit?: (event: Event | React.FormEvent) => Promise<{ status: boolean; error?: Error }> | void;
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export function WiseForm({ children, settings, types, model }: IWiseFormSpecs): JSX.Element {
|
|
10
20
|
const { ready, model: instance, type, styles, items } = useModel(settings, model);
|
|
11
21
|
const formTypes = useTypes(types);
|
|
@@ -30,10 +40,14 @@ export function WiseForm({ children, settings, types, model }: IWiseFormSpecs):
|
|
|
30
40
|
|
|
31
41
|
const onSubmit = (event: React.FormEvent) => {
|
|
32
42
|
event.preventDefault();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
|
|
44
|
+
// Verificar si la instancia tiene el método onSubmit (implementación extendida)
|
|
45
|
+
const modelWithSubmit = instance as IFormModelWithSubmit;
|
|
46
|
+
if (typeof modelWithSubmit.onSubmit === 'function') {
|
|
47
|
+
modelWithSubmit.onSubmit(event);
|
|
48
|
+
return;
|
|
36
49
|
}
|
|
50
|
+
|
|
37
51
|
};
|
|
38
52
|
|
|
39
53
|
const value = {
|
|
@@ -97,7 +97,12 @@ export class FormulaArray {
|
|
|
97
97
|
|
|
98
98
|
async calculate() {
|
|
99
99
|
const formulaField = this.#plugin.form.getField(this.name);
|
|
100
|
-
if (!formulaField)
|
|
100
|
+
if (!formulaField) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`[WiseForm.FormulaArray] Formula field "${this.name}" not found. Check that the field exists in the form configuration.`
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
101
106
|
const value = formulaField[this.#specs.propertyValue || 'entries']
|
|
102
107
|
if (!value || !Array.isArray(value) || !value.length) {
|
|
103
108
|
formulaField.set({ [this.#specs.propertyValue]: [] });
|
package/src/models/base.ts
CHANGED
|
@@ -100,28 +100,54 @@ export class BaseWiseModel extends ReactiveModel<IBaseWiseModel> {
|
|
|
100
100
|
/**
|
|
101
101
|
* Retrieves a field or nested wrapper by name. Supports dot notation for accessing deeply nested fields.
|
|
102
102
|
* @param {string} name - The name of the field or nested wrapper to retrieve.
|
|
103
|
+
* @param {Set<BaseWiseModel | WrappedFormModel>} visited - Set of already visited models to prevent infinite recursion.
|
|
103
104
|
* @returns {FormField | WrappedFormModel | undefined} The requested instance, or undefined if not found.
|
|
104
105
|
*/
|
|
105
|
-
getField(name: string) {
|
|
106
|
-
if (!name)
|
|
106
|
+
getField(name: string, visited: Set<BaseWiseModel | WrappedFormModel> = new Set()) {
|
|
107
|
+
if (!name) {
|
|
108
|
+
console.warn('[WiseForm.getField] Empty field name provided in form', this.#settings.name);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Protección contra recursión circular
|
|
113
|
+
if (visited.has(this)) {
|
|
114
|
+
console.error(
|
|
115
|
+
`[WiseForm.getField] Circular reference detected in form "${this.#settings.name}" while searching for field "${name}". This usually indicates a configuration issue with nested wrappers.`
|
|
116
|
+
);
|
|
117
|
+
console.log(
|
|
118
|
+
`[WiseForm.getField] PROTECTION ACTIVE: Circular reference prevented. Form: "${this.#settings.name}", Field: "${name}", Visited models count: ${visited.size}`
|
|
119
|
+
);
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
visited.add(this);
|
|
107
123
|
|
|
108
124
|
if (!name.includes('.')) {
|
|
109
125
|
let field = this.#fields.get(name);
|
|
110
126
|
|
|
111
127
|
if (!field) {
|
|
112
128
|
this.#wrappers.forEach(item => {
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
if (!visited.has(item)) {
|
|
130
|
+
const foundField = item.getField(name, visited);
|
|
131
|
+
if (foundField) field = foundField;
|
|
132
|
+
}
|
|
115
133
|
});
|
|
116
134
|
}
|
|
117
135
|
return field;
|
|
118
136
|
}
|
|
119
137
|
|
|
138
|
+
// Dot notation path
|
|
120
139
|
const [wrapperName, ...others] = name.split('.');
|
|
121
140
|
const currentWrapper = this.#wrappers.get(wrapperName);
|
|
122
141
|
|
|
142
|
+
if (!currentWrapper) {
|
|
143
|
+
console.warn(
|
|
144
|
+
`[WiseForm.getField] Wrapper "${wrapperName}" not found in form "${this.#settings.name}" while searching for "${name}". Available wrappers: ${Array.from(this.#wrappers.keys()).join(', ') || 'none'}`
|
|
145
|
+
);
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
123
149
|
const otherWrapper = others.join('.');
|
|
124
|
-
return currentWrapper.getField(otherWrapper);
|
|
150
|
+
return currentWrapper.getField(otherWrapper, visited);
|
|
125
151
|
}
|
|
126
152
|
|
|
127
153
|
/**
|
|
@@ -2,101 +2,179 @@ import type { FormField } from './field';
|
|
|
2
2
|
import type { FormModel } from './model';
|
|
3
3
|
import { CallbackFunction, ICallbackProps } from './types/callbacks';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Maximum number of recursive executions allowed for a callback before preventing further execution.
|
|
7
|
+
* This can be overridden if specific use cases require a higher limit.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_CALLBACK_RECURSION_DEPTH = 3;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map to track callback execution depth per unique execution context.
|
|
13
|
+
* Key format: "callbackName:fieldName:dependencyName" or "callbackName:fieldName" if no dependency
|
|
14
|
+
*/
|
|
15
|
+
const callbackExecutionDepth = new Map<string, number>();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set to track callbacks currently executing (prevents concurrent executions of the same callback)
|
|
19
|
+
*/
|
|
20
|
+
const currentlyExecutingCallbacks = new Set<string>();
|
|
21
|
+
|
|
5
22
|
export class CallbackManager {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
#field: FormField;
|
|
24
|
+
#model: FormModel;
|
|
25
|
+
#callbacks: CallbackFunction[] = [];
|
|
26
|
+
#listeners = [];
|
|
27
|
+
constructor(model, field) {
|
|
28
|
+
this.#field = field;
|
|
29
|
+
this.#model = model;
|
|
30
|
+
this.#callbacks = model.callbacks;
|
|
31
|
+
this.initialize();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
initialize() {
|
|
35
|
+
const instance = this.#field;
|
|
36
|
+
const checkField = async settings => {
|
|
37
|
+
const dependency = this.#model.getField(this.#model.getFieldName(settings.field));
|
|
38
|
+
await dependency.isReady;
|
|
39
|
+
const required = ['field', 'callback'];
|
|
40
|
+
required.forEach(prop => {
|
|
41
|
+
if (!settings[prop]) throw new Error(`${settings?.field} is missing ${prop}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!dependency) throw new Error(`${settings?.field} is not a registered field`);
|
|
45
|
+
|
|
46
|
+
if (!this.#callbacks[settings.callback]) {
|
|
47
|
+
throw new Error(`${settings.callback} is not a registered callback ${settings.name}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// saved in listener array to be able to remove the listener if is required.
|
|
51
|
+
const event = settings.event || 'value.change';
|
|
52
|
+
const caller = () => this.executeCallback(settings);
|
|
53
|
+
this.#listeners.push(caller);
|
|
54
|
+
dependency.on(event, caller);
|
|
55
|
+
|
|
56
|
+
//callback({ dependency, settings, field: instance, form: this });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
instance?.specs?.dependentOn.forEach(checkField);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generates a unique execution key for tracking callback recursion.
|
|
64
|
+
* Format: "callbackName:fieldName:dependencyName"
|
|
65
|
+
*/
|
|
66
|
+
#getExecutionKey(callbackName: string, fieldName: string, dependencyName?: string): string {
|
|
67
|
+
const fieldNameStr = fieldName || 'unknown';
|
|
68
|
+
const dependencyNameStr = dependencyName || 'unknown';
|
|
69
|
+
return `${callbackName}:${fieldNameStr}:${dependencyNameStr}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
executeCallback = async settings => {
|
|
73
|
+
if (!settings) {
|
|
74
|
+
console.warn('the field does not have dependentOn settings');
|
|
75
|
+
return;
|
|
15
76
|
}
|
|
16
77
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.#model.getFieldName(settings.field)
|
|
22
|
-
);
|
|
23
|
-
await dependency.isReady;
|
|
24
|
-
const required = ['field', 'callback'];
|
|
25
|
-
required.forEach((prop) => {
|
|
26
|
-
if (!settings[prop])
|
|
27
|
-
throw new Error(`${settings?.field} is missing ${prop}`);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (!dependency)
|
|
31
|
-
throw new Error(`${settings?.field} is not a registered field`);
|
|
32
|
-
|
|
33
|
-
if (!this.#callbacks[settings.callback]) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
`${settings.callback} is not a registered callback ${settings.name}`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// saved in listener array to be able to remove the listener if is required.
|
|
40
|
-
const event = settings.event || 'value.change';
|
|
41
|
-
const caller = () => this.executeCallback(settings);
|
|
42
|
-
this.#listeners.push(caller);
|
|
43
|
-
dependency.on(event, caller);
|
|
44
|
-
|
|
45
|
-
//callback({ dependency, settings, field: instance, form: this });
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
instance?.specs?.dependentOn.forEach(checkField);
|
|
78
|
+
const callbackName = settings.callback;
|
|
79
|
+
if (!callbackName) {
|
|
80
|
+
console.warn('[CallbackManager] executeCallback called without callback name in settings');
|
|
81
|
+
return;
|
|
49
82
|
}
|
|
50
83
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
const fieldName = (this.#field as { name?: string })?.name || 'unknown';
|
|
85
|
+
const dependencyFieldName = typeof settings.field === 'string' ? settings.field : settings.field?.field || 'unknown';
|
|
86
|
+
|
|
87
|
+
const executionKey = this.#getExecutionKey(callbackName, fieldName, dependencyFieldName);
|
|
88
|
+
|
|
89
|
+
// Check if already executing (prevent concurrent executions)
|
|
90
|
+
if (currentlyExecutingCallbacks.has(executionKey)) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`[CallbackManager] Callback "${callbackName}" is already executing for field "${fieldName}" with dependency "${dependencyFieldName}". Skipping concurrent execution.`
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check recursion depth
|
|
98
|
+
const currentDepth = callbackExecutionDepth.get(executionKey) || 0;
|
|
99
|
+
const maxDepth = settings.maxRecursionDepth !== undefined ? settings.maxRecursionDepth : MAX_CALLBACK_RECURSION_DEPTH;
|
|
100
|
+
|
|
101
|
+
if (currentDepth >= maxDepth) {
|
|
102
|
+
console.error(
|
|
103
|
+
`[CallbackManager] RECURSION DETECTED: Callback "${callbackName}" exceeded maximum recursion depth of ${maxDepth}`,
|
|
104
|
+
`\n Field: ${fieldName}`,
|
|
105
|
+
`\n Dependency: ${dependencyFieldName}`,
|
|
106
|
+
`\n Current depth: ${currentDepth + 1}`,
|
|
107
|
+
`\n Execution key: ${executionKey}`,
|
|
108
|
+
`\n This indicates a circular dependency in callbacks. Please review the form configuration.`
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Mark as currently executing and increment depth
|
|
114
|
+
currentlyExecutingCallbacks.add(executionKey);
|
|
115
|
+
callbackExecutionDepth.set(executionKey, currentDepth + 1);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const params: ICallbackProps = {
|
|
119
|
+
...settings,
|
|
120
|
+
form: this.#model,
|
|
121
|
+
field: this.#field,
|
|
122
|
+
settings,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const callback: CallbackFunction = this.#callbacks[callbackName];
|
|
126
|
+
|
|
127
|
+
if (!callback) {
|
|
128
|
+
console.error(`[CallbackManager] Callback "${callbackName}" not found in registered callbacks`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const dependency = this.#model.getField(this.#model.getFieldName(settings.field));
|
|
133
|
+
await dependency.isReady;
|
|
134
|
+
const fields = { [dependency.name]: dependency };
|
|
135
|
+
if (settings.hasOwnProperty('fields')) {
|
|
136
|
+
for (const field of settings.fields) {
|
|
137
|
+
const instance = this.#model.getField(this.#model.getFieldName(field));
|
|
138
|
+
if (instance) await instance.isReady;
|
|
139
|
+
const propName = typeof field === 'string' ? field : field.alias;
|
|
140
|
+
fields[propName] = instance;
|
|
98
141
|
}
|
|
99
|
-
callback(params);
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
142
|
|
|
143
|
+
params.fields = fields;
|
|
144
|
+
}
|
|
145
|
+
params.dependency = dependency;
|
|
146
|
+
|
|
147
|
+
//global wiseForm params
|
|
148
|
+
if (settings.hasOwnProperty('params')) {
|
|
149
|
+
const specs = {};
|
|
150
|
+
settings.params.forEach(param => {
|
|
151
|
+
if (!this.#model?.getParams(param)) {
|
|
152
|
+
console.warn(`param ${param} is not registered in the form`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
specs[param] = this.#model.getParams(param);
|
|
157
|
+
});
|
|
158
|
+
params.specs = specs;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Execute the callback (handle both sync and async callbacks)
|
|
162
|
+
const result = callback(params) as any;
|
|
163
|
+
if (result && typeof result.then === 'function') {
|
|
164
|
+
await result;
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`[CallbackManager] Error executing callback "${callbackName}":`, error);
|
|
168
|
+
} finally {
|
|
169
|
+
// Clean up execution tracking
|
|
170
|
+
currentlyExecutingCallbacks.delete(executionKey);
|
|
171
|
+
|
|
172
|
+
const finalDepth = callbackExecutionDepth.get(executionKey) || 0;
|
|
173
|
+
if (finalDepth <= 1) {
|
|
174
|
+
callbackExecutionDepth.delete(executionKey);
|
|
175
|
+
} else {
|
|
176
|
+
callbackExecutionDepth.set(executionKey, finalDepth - 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
package/src/models/wrapper.ts
CHANGED
|
@@ -131,32 +131,57 @@ class WrappedFormModel extends BaseWiseModel {
|
|
|
131
131
|
/**
|
|
132
132
|
* Retrieves a field or nested wrapper by name. Supports dot notation for accessing deeply nested fields.
|
|
133
133
|
* @param {string} name - The name of the field or nested wrapper to retrieve.
|
|
134
|
+
* @param {Set<BaseWiseModel | WrappedFormModel>} visited - Set of already visited wrappers to prevent infinite recursion.
|
|
134
135
|
* @returns {FormField | WrappedFormModel | undefined} The requested instance, or undefined if not found.
|
|
135
136
|
*/
|
|
136
|
-
getField(name: string) {
|
|
137
|
-
if (!name)
|
|
138
|
-
|
|
139
|
-
'
|
|
137
|
+
getField(name: string, visited: Set<BaseWiseModel | WrappedFormModel> = new Set()) {
|
|
138
|
+
if (!name) {
|
|
139
|
+
console.warn(
|
|
140
|
+
'[WiseForm.getField] Empty field name provided in wrapper',
|
|
140
141
|
this.settings.name
|
|
141
142
|
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Protección contra recursión circular
|
|
147
|
+
if (visited.has(this)) {
|
|
148
|
+
console.error(
|
|
149
|
+
`[WiseForm.getField] Circular reference detected in wrapper "${this.settings.name}" while searching for field "${name}". This usually indicates a configuration issue with nested wrappers.`
|
|
150
|
+
);
|
|
151
|
+
console.log(
|
|
152
|
+
`[WiseForm.getField] PROTECTION ACTIVE: Circular reference prevented. Wrapper: "${this.settings.name}", Field: "${name}", Visited wrappers count: ${visited.size}`
|
|
153
|
+
);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
visited.add(this);
|
|
142
157
|
|
|
143
158
|
if (!name.includes('.')) {
|
|
144
159
|
let field = this.fields.get(name);
|
|
145
160
|
|
|
146
161
|
if (!field) {
|
|
147
162
|
this.wrappers.forEach((item) => {
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
if (!visited.has(item)) {
|
|
164
|
+
const foundField = item.getField(name, visited);
|
|
165
|
+
if (foundField) field = foundField;
|
|
166
|
+
}
|
|
150
167
|
});
|
|
151
168
|
}
|
|
152
169
|
return field;
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
// Dot notation path
|
|
155
173
|
const [wrapperName, ...others] = name.split('.');
|
|
156
174
|
const currentWrapper = this.wrappers.get(wrapperName);
|
|
157
175
|
|
|
176
|
+
if (!currentWrapper) {
|
|
177
|
+
console.warn(
|
|
178
|
+
`[WiseForm.getField] Wrapper "${wrapperName}" not found in "${this.settings.name}" while searching for "${name}". Available wrappers: ${Array.from(this.wrappers.keys()).join(', ') || 'none'}`
|
|
179
|
+
);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
const otherWrapper = others.join('.');
|
|
159
|
-
return currentWrapper.getField(otherWrapper);
|
|
184
|
+
return currentWrapper.getField(otherWrapper, visited);
|
|
160
185
|
}
|
|
161
186
|
|
|
162
187
|
/**
|