@bgroup/wise-form 1.0.5 → 1.0.8
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]: [] });
|
|
@@ -1,208 +1,161 @@
|
|
|
1
1
|
import type { FormulaManager } from '..';
|
|
2
2
|
import { conditionsTypes } from '../helpers/condition-types';
|
|
3
3
|
import { EvaluationsManager } from '../helpers/evaluations';
|
|
4
|
-
import {
|
|
5
|
-
EvaluatedFormula,
|
|
6
|
-
FormulaObserver,
|
|
7
|
-
IComplexCondition,
|
|
8
|
-
IConditionalField,
|
|
9
|
-
} from '../types/formulas';
|
|
4
|
+
import { EvaluatedFormula, FormulaObserver, IComplexCondition, IConditionalField } from '../types/formulas';
|
|
10
5
|
import { parse } from 'mathjs';
|
|
11
6
|
|
|
12
7
|
export class FormulaConditional {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
8
|
+
#plugin: any;
|
|
9
|
+
#specs: FormulaObserver;
|
|
10
|
+
#emptyValue: undefined;
|
|
11
|
+
get formula() {
|
|
12
|
+
return this.#specs.formula;
|
|
13
|
+
}
|
|
14
|
+
get base() {
|
|
15
|
+
const formula = <IComplexCondition>this.#specs.formula;
|
|
16
|
+
return formula.base;
|
|
17
|
+
}
|
|
18
|
+
#value: string | number | undefined | 0;
|
|
19
|
+
get value() {
|
|
20
|
+
return this.#value;
|
|
21
|
+
}
|
|
22
|
+
get name() {
|
|
23
|
+
return this.#specs.name;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Represents the fields defined in the plugin settings
|
|
27
|
+
*/
|
|
28
|
+
get fields() {
|
|
29
|
+
const formula = <IComplexCondition>this.formula;
|
|
30
|
+
return typeof formula?.fields === 'string' ? [formula?.fields] : formula?.fields;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get conditions() {
|
|
34
|
+
if (typeof this.#specs.formula === 'string') return;
|
|
35
|
+
const formula = this.#specs.formula as IComplexCondition;
|
|
36
|
+
return formula.conditions;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* FormField type
|
|
41
|
+
*/
|
|
42
|
+
#fields: any;
|
|
43
|
+
|
|
44
|
+
#parent: FormulaManager;
|
|
45
|
+
#ceil: boolean;
|
|
46
|
+
#round: boolean;
|
|
47
|
+
#isNotListenToChanges = false;
|
|
48
|
+
constructor(parent, plugin, specs) {
|
|
49
|
+
this.#parent = parent;
|
|
50
|
+
this.#plugin = plugin;
|
|
51
|
+
this.#specs = specs;
|
|
52
|
+
this.#round = specs.round;
|
|
53
|
+
this.#ceil = specs.ceil;
|
|
54
|
+
this.#emptyValue = specs.emptyValue;
|
|
55
|
+
|
|
56
|
+
if (specs.isNotListenToChanges) this.#isNotListenToChanges = specs.isNotListenToChanges;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
initialize() {
|
|
60
|
+
try {
|
|
61
|
+
const { form } = this.#plugin;
|
|
62
|
+
|
|
63
|
+
if (!this.fields) {
|
|
64
|
+
throw new Error(`Fields not found in formula ${this.name}`);
|
|
65
|
+
}
|
|
66
|
+
const fields = this.fields.map(name => {
|
|
67
|
+
const formula = this.#plugin.formulas.get(name);
|
|
68
|
+
if (formula) return formula;
|
|
69
|
+
const field = form.getField(name);
|
|
70
|
+
return field;
|
|
71
|
+
});
|
|
72
|
+
this.#fields = fields;
|
|
73
|
+
if (this.name === 'costoTotalGrafico') {
|
|
74
|
+
console.log('fields', fields);
|
|
75
|
+
}
|
|
76
|
+
if (!this.#isNotListenToChanges)
|
|
77
|
+
fields.forEach(field => {
|
|
78
|
+
if (!field) {
|
|
79
|
+
throw new Error(`Field ${this.name} not found in form ${form.name}`);
|
|
80
|
+
}
|
|
81
|
+
field.on('change', this.calculate.bind(this));
|
|
82
|
+
});
|
|
83
|
+
} catch (e) {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
evaluate() {
|
|
87
|
+
const formula = <IComplexCondition>this.#specs.formula;
|
|
88
|
+
let evaluatedFormula: any = { formula: formula.base }; // Use the base formula by default
|
|
89
|
+
if (formula.conditions) {
|
|
90
|
+
const conditionsArray = Array.isArray(formula.conditions) ? formula.conditions : [formula.conditions];
|
|
91
|
+
for (const condition of conditionsArray) {
|
|
92
|
+
let conditionMet = false;
|
|
93
|
+
if (condition.conditions) {
|
|
94
|
+
// If there are nested conditions, all must be met
|
|
95
|
+
conditionMet = condition.conditions.every(subCondition => {
|
|
96
|
+
const fieldValues = subCondition.fields.map(fieldName => {
|
|
97
|
+
const field = this.#fields.find(f => f.name === fieldName);
|
|
98
|
+
return field ? field.value : this.#emptyValue;
|
|
99
|
+
});
|
|
100
|
+
return EvaluationsManager.validateAll(subCondition.condition, fieldValues, subCondition.value);
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
const fieldValues = condition.fields.map(fieldName => {
|
|
104
|
+
const field = this.#fields.find(f => f.name === fieldName);
|
|
105
|
+
return field ? field.value : this.#emptyValue;
|
|
106
|
+
});
|
|
107
|
+
const conditionType = !!condition.type && conditionsTypes[condition.type] ? conditionsTypes[condition.type] : conditionsTypes.some;
|
|
108
|
+
// Check if any of the specified fields meet the condition
|
|
109
|
+
conditionMet = EvaluationsManager[conditionType](condition.condition, fieldValues, condition.value);
|
|
110
|
+
}
|
|
39
111
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
112
|
+
if (conditionMet) {
|
|
113
|
+
evaluatedFormula.formula = condition.formula;
|
|
114
|
+
evaluatedFormula.fi = condition;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
44
118
|
}
|
|
119
|
+
return evaluatedFormula;
|
|
120
|
+
}
|
|
45
121
|
|
|
122
|
+
async calculate() {
|
|
46
123
|
/**
|
|
47
|
-
*
|
|
124
|
+
* the formula is taken from the evaluate method since the conditions are evaluated there and
|
|
125
|
+
* can change the formula to be applied
|
|
48
126
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.#plugin = plugin;
|
|
58
|
-
this.#specs = specs;
|
|
59
|
-
this.#round = specs.round;
|
|
60
|
-
this.#ceil = specs.ceil;
|
|
61
|
-
this.#emptyValue = specs.emptyValue;
|
|
62
|
-
|
|
63
|
-
if (specs.isNotListenToChanges)
|
|
64
|
-
this.#isNotListenToChanges = specs.isNotListenToChanges;
|
|
127
|
+
const model = this.#plugin.form.getField(this.name);
|
|
128
|
+
const formula = this.evaluate();
|
|
129
|
+
|
|
130
|
+
if (!formula.formula) {
|
|
131
|
+
this.#value = this.#emptyValue !== undefined ? this.#emptyValue : '';
|
|
132
|
+
model && model.set({ value: this.#value });
|
|
133
|
+
this.#parent.trigger('change');
|
|
134
|
+
return this.#value;
|
|
65
135
|
}
|
|
136
|
+
// todo: Review if this section can be replaced by formulaManager.variables property.
|
|
137
|
+
const { tokens } = this.#parent.getParser(formula);
|
|
138
|
+
const variables = tokens.filter(token => token.type === 'variable').map(item => item.value);
|
|
66
139
|
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const { form } = this.#plugin;
|
|
70
|
-
|
|
71
|
-
if (!this.fields) {
|
|
72
|
-
throw new Error(`Fields not found in formula ${this.name}`);
|
|
73
|
-
}
|
|
74
|
-
const fields = this.fields.map((name) => {
|
|
75
|
-
const formula = this.#plugin.formulas.get(name);
|
|
76
|
-
if (formula) return formula;
|
|
77
|
-
const field = form.getField(name);
|
|
78
|
-
return field;
|
|
79
|
-
});
|
|
80
|
-
this.#fields = fields;
|
|
81
|
-
if (this.name === 'costoTotalGrafico') {
|
|
82
|
-
console.log('fields', fields);
|
|
83
|
-
}
|
|
84
|
-
if (!this.#isNotListenToChanges)
|
|
85
|
-
fields.forEach((field) => {
|
|
86
|
-
if (!field) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Field ${this.name} not found in form ${form.name}`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
field.on('change', this.calculate.bind(this));
|
|
92
|
-
});
|
|
93
|
-
} catch (e) {}
|
|
94
|
-
}
|
|
140
|
+
const params = await this.#parent.getParams(variables);
|
|
95
141
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (formula.conditions) {
|
|
100
|
-
const conditionsArray = Array.isArray(formula.conditions) ? formula.conditions : [formula.conditions];
|
|
101
|
-
for (const condition of conditionsArray) {
|
|
102
|
-
let conditionMet = false;
|
|
103
|
-
if (condition.conditions) {
|
|
104
|
-
// If there are nested conditions, all must be met
|
|
105
|
-
conditionMet = condition.conditions.every(
|
|
106
|
-
(subCondition) => {
|
|
107
|
-
const fieldValues = subCondition.fields.map(
|
|
108
|
-
(fieldName) => {
|
|
109
|
-
const field = this.#fields.find(
|
|
110
|
-
(f) => f.name === fieldName
|
|
111
|
-
);
|
|
112
|
-
return field
|
|
113
|
-
? field.value
|
|
114
|
-
: this.#emptyValue;
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
return EvaluationsManager.validateAll(
|
|
118
|
-
subCondition.condition,
|
|
119
|
-
fieldValues,
|
|
120
|
-
subCondition.value
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
);
|
|
124
|
-
} else {
|
|
125
|
-
const fieldValues = condition.fields.map((fieldName) => {
|
|
126
|
-
const field = this.#fields.find(
|
|
127
|
-
(f) => f.name === fieldName
|
|
128
|
-
);
|
|
129
|
-
return field ? field.value : this.#emptyValue;
|
|
130
|
-
});
|
|
131
|
-
const conditionType =
|
|
132
|
-
!!condition.type && conditionsTypes[condition.type]
|
|
133
|
-
? conditionsTypes[condition.type]
|
|
134
|
-
: conditionsTypes.some;
|
|
135
|
-
// Check if any of the specified fields meet the condition
|
|
136
|
-
conditionMet = EvaluationsManager[conditionType](
|
|
137
|
-
condition.condition,
|
|
138
|
-
fieldValues,
|
|
139
|
-
condition.value
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (conditionMet) {
|
|
144
|
-
evaluatedFormula.formula = condition.formula;
|
|
145
|
-
evaluatedFormula.fi = condition;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return evaluatedFormula;
|
|
151
|
-
}
|
|
142
|
+
try {
|
|
143
|
+
const keys = Object.keys(params);
|
|
144
|
+
let result = keys.length === 1 ? params[keys[0]] : parse(formula.formula as string).evaluate(params);
|
|
152
145
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return this.#value;
|
|
167
|
-
}
|
|
168
|
-
// todo: Review if this section can be replaced by formulaManager.variables property.
|
|
169
|
-
const { tokens } = this.#parent.getParser(formula);
|
|
170
|
-
const variables = tokens
|
|
171
|
-
.filter((token) => token.type === 'variable')
|
|
172
|
-
.map((item) => item.value);
|
|
173
|
-
|
|
174
|
-
const params = await this.#parent.getParams(variables);
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const keys = Object.keys(params);
|
|
178
|
-
let result =
|
|
179
|
-
keys.length === 1
|
|
180
|
-
? params[keys[0]]
|
|
181
|
-
: parse(formula.formula as string).evaluate(params);
|
|
182
|
-
|
|
183
|
-
const isInvalidResult = [
|
|
184
|
-
-Infinity,
|
|
185
|
-
Infinity,
|
|
186
|
-
undefined,
|
|
187
|
-
null,
|
|
188
|
-
NaN,
|
|
189
|
-
].includes(result);
|
|
190
|
-
if (this.#round && !isInvalidResult) result = Math.round(result);
|
|
191
|
-
if (this.#ceil && !isInvalidResult) result = Math.ceil(result);
|
|
192
|
-
this.#value =
|
|
193
|
-
isInvalidResult || typeof result === 'object'
|
|
194
|
-
? this.#emptyValue
|
|
195
|
-
: Number(result.toFixed(2));
|
|
196
|
-
|
|
197
|
-
this.#parent.trigger('change');
|
|
198
|
-
|
|
199
|
-
model && model.set({ value: this.#value });
|
|
200
|
-
return this.#value;
|
|
201
|
-
} catch (e) {
|
|
202
|
-
console.log('formula', this.name, formula.formula, params);
|
|
203
|
-
console.error(e);
|
|
204
|
-
throw new Error('Error calculating the formula');
|
|
205
|
-
}
|
|
146
|
+
const isInvalidResult = [-Infinity, Infinity, undefined, null, NaN].includes(result);
|
|
147
|
+
if (this.#round && !isInvalidResult) result = Math.round(result);
|
|
148
|
+
if (this.#ceil && !isInvalidResult) result = Math.ceil(result);
|
|
149
|
+
this.#value = isInvalidResult || typeof result === 'object' ? this.#emptyValue : Number(Number(result).toFixed(2));
|
|
150
|
+
|
|
151
|
+
this.#parent.trigger('change');
|
|
152
|
+
|
|
153
|
+
model && model.set({ value: this.#value });
|
|
154
|
+
return this.#value;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
console.log('formula', this.name, formula.formula, params);
|
|
157
|
+
console.error(e);
|
|
158
|
+
throw new Error('Error calculating the formula');
|
|
206
159
|
}
|
|
160
|
+
}
|
|
207
161
|
}
|
|
208
|
-
|
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,192 @@ 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, index) => {
|
|
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
|
+
// Asignar un ID único a cada configuración de callback para tracking individual
|
|
51
|
+
if (!settings.__callbackId) {
|
|
52
|
+
const instanceName = (instance as any).name || 'unknown';
|
|
53
|
+
settings.__callbackId = `${instanceName}_${settings.callback}_${settings.field}_${index}_${Date.now()}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// saved in listener array to be able to remove the listener if is required.
|
|
57
|
+
const event = settings.event || 'value.change';
|
|
58
|
+
const caller = () => this.executeCallback(settings);
|
|
59
|
+
this.#listeners.push(caller);
|
|
60
|
+
dependency.on(event, caller);
|
|
61
|
+
|
|
62
|
+
//callback({ dependency, settings, field: instance, form: this });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
instance?.specs?.dependentOn.forEach(checkField);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generates a unique execution key for tracking callback recursion.
|
|
70
|
+
* Uses the unique __callbackId from settings if available, otherwise falls back to name-based key.
|
|
71
|
+
*/
|
|
72
|
+
#getExecutionKey(settings: any, fieldName: string): string {
|
|
73
|
+
// Si el settings tiene un __callbackId único, usarlo para permitir múltiples callbacks del mismo tipo
|
|
74
|
+
if (settings?.__callbackId) {
|
|
75
|
+
return settings.__callbackId;
|
|
15
76
|
}
|
|
77
|
+
// Fallback para compatibilidad con código legacy
|
|
78
|
+
const callbackName = settings?.callback || 'unknown';
|
|
79
|
+
const fieldNameStr = fieldName || 'unknown';
|
|
80
|
+
const dependencyFieldName = typeof settings?.field === 'string' ? settings.field : settings?.field?.field || 'unknown';
|
|
81
|
+
return `${callbackName}:${fieldNameStr}:${dependencyFieldName}`;
|
|
82
|
+
}
|
|
16
83
|
|
|
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);
|
|
84
|
+
executeCallback = async settings => {
|
|
85
|
+
if (!settings) {
|
|
86
|
+
console.warn('the field does not have dependentOn settings');
|
|
87
|
+
return;
|
|
49
88
|
}
|
|
50
89
|
|
|
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
|
-
|
|
90
|
+
const callbackName = settings.callback;
|
|
91
|
+
if (!callbackName) {
|
|
92
|
+
console.warn('[CallbackManager] executeCallback called without callback name in settings');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fieldName = (this.#field as { name?: string })?.name || 'unknown';
|
|
97
|
+
const dependencyFieldName = typeof settings.field === 'string' ? settings.field : settings.field?.field || 'unknown';
|
|
98
|
+
|
|
99
|
+
const executionKey = this.#getExecutionKey(settings, fieldName);
|
|
100
|
+
|
|
101
|
+
// Check if already executing (prevent concurrent executions)
|
|
102
|
+
if (currentlyExecutingCallbacks.has(executionKey)) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`[CallbackManager] Callback "${callbackName}" is already executing for field "${fieldName}" with dependency "${dependencyFieldName}". Skipping concurrent execution.`
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check recursion depth
|
|
110
|
+
const currentDepth = callbackExecutionDepth.get(executionKey) || 0;
|
|
111
|
+
const maxDepth = settings.maxRecursionDepth !== undefined ? settings.maxRecursionDepth : MAX_CALLBACK_RECURSION_DEPTH;
|
|
112
|
+
|
|
113
|
+
if (currentDepth >= maxDepth) {
|
|
114
|
+
console.error(
|
|
115
|
+
`[CallbackManager] RECURSION DETECTED: Callback "${callbackName}" exceeded maximum recursion depth of ${maxDepth}`,
|
|
116
|
+
`\n Field: ${fieldName}`,
|
|
117
|
+
`\n Dependency: ${dependencyFieldName}`,
|
|
118
|
+
`\n Current depth: ${currentDepth + 1}`,
|
|
119
|
+
`\n Execution key: ${executionKey}`,
|
|
120
|
+
`\n Callback ID: ${settings?.__callbackId || 'N/A'}`,
|
|
121
|
+
`\n This indicates a circular dependency in callbacks. Please review the form configuration.`
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Mark as currently executing and increment depth
|
|
127
|
+
currentlyExecutingCallbacks.add(executionKey);
|
|
128
|
+
callbackExecutionDepth.set(executionKey, currentDepth + 1);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const params: ICallbackProps = {
|
|
132
|
+
...settings,
|
|
133
|
+
form: this.#model,
|
|
134
|
+
field: this.#field,
|
|
135
|
+
settings,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const callback: CallbackFunction = this.#callbacks[callbackName];
|
|
139
|
+
|
|
140
|
+
if (!callback) {
|
|
141
|
+
console.error(`[CallbackManager] Callback "${callbackName}" not found in registered callbacks`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const dependency = this.#model.getField(this.#model.getFieldName(settings.field));
|
|
146
|
+
await dependency.isReady;
|
|
147
|
+
const fields = { [dependency.name]: dependency };
|
|
148
|
+
if (settings.hasOwnProperty('fields')) {
|
|
149
|
+
for (const field of settings.fields) {
|
|
150
|
+
const instance = this.#model.getField(this.#model.getFieldName(field));
|
|
151
|
+
if (instance) await instance.isReady;
|
|
152
|
+
const propName = typeof field === 'string' ? field : field.alias;
|
|
153
|
+
fields[propName] = instance;
|
|
98
154
|
}
|
|
99
|
-
callback(params);
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
155
|
|
|
156
|
+
params.fields = fields;
|
|
157
|
+
}
|
|
158
|
+
params.dependency = dependency;
|
|
159
|
+
|
|
160
|
+
//global wiseForm params
|
|
161
|
+
if (settings.hasOwnProperty('params')) {
|
|
162
|
+
const specs = {};
|
|
163
|
+
settings.params.forEach(param => {
|
|
164
|
+
if (!this.#model?.getParams(param)) {
|
|
165
|
+
console.warn(`param ${param} is not registered in the form`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
specs[param] = this.#model.getParams(param);
|
|
170
|
+
});
|
|
171
|
+
params.specs = specs;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Execute the callback (handle both sync and async callbacks)
|
|
175
|
+
const result = callback(params) as any;
|
|
176
|
+
if (result && typeof result.then === 'function') {
|
|
177
|
+
await result;
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(`[CallbackManager] Error executing callback "${callbackName}":`, error);
|
|
181
|
+
} finally {
|
|
182
|
+
// Clean up execution tracking
|
|
183
|
+
currentlyExecutingCallbacks.delete(executionKey);
|
|
184
|
+
|
|
185
|
+
const finalDepth = callbackExecutionDepth.get(executionKey) || 0;
|
|
186
|
+
if (finalDepth <= 1) {
|
|
187
|
+
callbackExecutionDepth.delete(executionKey);
|
|
188
|
+
} else {
|
|
189
|
+
callbackExecutionDepth.set(executionKey, finalDepth - 1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
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
|
/**
|