hs-uix 1.0.0 → 1.0.1
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/README.md +22 -17
- package/dist/form.js +794 -152
- package/dist/form.mjs +794 -152
- package/dist/index.js +794 -152
- package/dist/index.mjs +794 -152
- package/package.json +1 -1
package/dist/form.js
CHANGED
|
@@ -72,7 +72,132 @@ var isValueEmpty = (value, field) => {
|
|
|
72
72
|
if ((field.type === "toggle" || field.type === "checkbox") && value === false) return true;
|
|
73
73
|
return false;
|
|
74
74
|
};
|
|
75
|
-
var
|
|
75
|
+
var isPromise = (value) => value && typeof value.then === "function";
|
|
76
|
+
var isAsyncFunction = (fn) => fn && fn.constructor && fn.constructor.name === "AsyncFunction";
|
|
77
|
+
var normalizeValidatorResult = (result) => {
|
|
78
|
+
if (result === true || result === void 0 || result === null || result === false) return null;
|
|
79
|
+
return String(result);
|
|
80
|
+
};
|
|
81
|
+
var isDateValueObject = (value) => isPlainObject(value) && Number.isInteger(value.year) && Number.isInteger(value.month) && Number.isInteger(value.date);
|
|
82
|
+
var isTimeValueObject = (value) => isPlainObject(value) && Number.isInteger(value.hours) && Number.isInteger(value.minutes);
|
|
83
|
+
var compareDateValues = (a, b) => {
|
|
84
|
+
if (a.year !== b.year) return a.year - b.year;
|
|
85
|
+
if (a.month !== b.month) return a.month - b.month;
|
|
86
|
+
return a.date - b.date;
|
|
87
|
+
};
|
|
88
|
+
var compareTimeValues = (a, b) => {
|
|
89
|
+
if (a.hours !== b.hours) return a.hours - b.hours;
|
|
90
|
+
return a.minutes - b.minutes;
|
|
91
|
+
};
|
|
92
|
+
var runDefaultFieldValidator = (value, field, allValues) => {
|
|
93
|
+
const errorPrefix = field.label || field.name;
|
|
94
|
+
switch (field.type) {
|
|
95
|
+
case "text":
|
|
96
|
+
case "password":
|
|
97
|
+
case "textarea":
|
|
98
|
+
if (typeof value !== "string") return `${errorPrefix} must be text`;
|
|
99
|
+
break;
|
|
100
|
+
case "number":
|
|
101
|
+
case "stepper":
|
|
102
|
+
case "currency":
|
|
103
|
+
if (typeof value !== "number" || Number.isNaN(value)) return `${errorPrefix} must be a number`;
|
|
104
|
+
break;
|
|
105
|
+
case "toggle":
|
|
106
|
+
case "checkbox":
|
|
107
|
+
if (typeof value !== "boolean") return `${errorPrefix} must be true or false`;
|
|
108
|
+
break;
|
|
109
|
+
case "select":
|
|
110
|
+
case "radioGroup": {
|
|
111
|
+
const options = resolveOptions(field, allValues);
|
|
112
|
+
if (options.length > 0 && !options.some((o) => Object.is(o.value, value))) {
|
|
113
|
+
return `${errorPrefix} has an invalid selection`;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "multiselect":
|
|
118
|
+
case "checkboxGroup": {
|
|
119
|
+
if (!Array.isArray(value)) return `${errorPrefix} must be a list`;
|
|
120
|
+
const options = resolveOptions(field, allValues);
|
|
121
|
+
if (options.length > 0) {
|
|
122
|
+
const validValues = new Set(options.map((o) => o.value));
|
|
123
|
+
const hasInvalid = value.some((item) => !validValues.has(item));
|
|
124
|
+
if (hasInvalid) return `${errorPrefix} has an invalid selection`;
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "date":
|
|
129
|
+
if (!isDateValueObject(value)) return `${errorPrefix} has an invalid date`;
|
|
130
|
+
break;
|
|
131
|
+
case "time":
|
|
132
|
+
if (!isTimeValueObject(value)) return `${errorPrefix} has an invalid time`;
|
|
133
|
+
break;
|
|
134
|
+
case "datetime": {
|
|
135
|
+
if (isDateValueObject(value)) break;
|
|
136
|
+
if (!isPlainObject(value)) return `${errorPrefix} has an invalid date/time`;
|
|
137
|
+
const hasDate = value.date !== void 0;
|
|
138
|
+
const hasTime = value.time !== void 0;
|
|
139
|
+
if (!hasDate && !hasTime) return `${errorPrefix} has an invalid date/time`;
|
|
140
|
+
if (hasDate && !isDateValueObject(value.date)) return `${errorPrefix} has an invalid date`;
|
|
141
|
+
if (hasTime && !isTimeValueObject(value.time)) return `${errorPrefix} has an invalid time`;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "repeater":
|
|
145
|
+
if (!Array.isArray(value)) return `${errorPrefix} has invalid rows`;
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
var runCustomSyncValidators = (value, field, allValues) => {
|
|
153
|
+
const validators = Array.isArray(field.validators) ? field.validators : [];
|
|
154
|
+
for (const validator of validators) {
|
|
155
|
+
if (isAsyncFunction(validator)) continue;
|
|
156
|
+
try {
|
|
157
|
+
const result = validator(value, allValues);
|
|
158
|
+
if (isPromise(result)) continue;
|
|
159
|
+
const err = normalizeValidatorResult(result);
|
|
160
|
+
if (err) return err;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return (err == null ? void 0 : err.message) || "Validation failed";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (field.validate && !isAsyncFunction(field.validate)) {
|
|
166
|
+
try {
|
|
167
|
+
const result = field.validate(value, allValues);
|
|
168
|
+
if (!isPromise(result)) {
|
|
169
|
+
const err = normalizeValidatorResult(result);
|
|
170
|
+
if (err) return err;
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return (err == null ? void 0 : err.message) || "Validation failed";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
};
|
|
178
|
+
var collectAsyncValidatorPromises = (value, field, allValues, context) => {
|
|
179
|
+
const promises = [];
|
|
180
|
+
const validators = Array.isArray(field.validators) ? field.validators : [];
|
|
181
|
+
for (const validator of validators) {
|
|
182
|
+
try {
|
|
183
|
+
const result = validator(value, allValues, context);
|
|
184
|
+
if (isPromise(result)) promises.push(result);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
promises.push(Promise.reject(err));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (field.validate) {
|
|
190
|
+
try {
|
|
191
|
+
const result = field.validate(value, allValues, context);
|
|
192
|
+
if (isPromise(result)) promises.push(result);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
promises.push(Promise.reject(err));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return promises;
|
|
198
|
+
};
|
|
199
|
+
var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
|
|
200
|
+
const includeCustomValidators = options.includeCustomValidators !== false;
|
|
76
201
|
if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList") return null;
|
|
77
202
|
const isRequired = resolveRequired(field, allValues);
|
|
78
203
|
const plugin = fieldTypes && fieldTypes[field.type];
|
|
@@ -81,6 +206,10 @@ var runValidators = (value, field, allValues, fieldTypes) => {
|
|
|
81
206
|
return `${field.label} is required`;
|
|
82
207
|
}
|
|
83
208
|
if (empty) return null;
|
|
209
|
+
if (field.useDefaultValidators !== false) {
|
|
210
|
+
const typeError = runDefaultFieldValidator(value, field, allValues);
|
|
211
|
+
if (typeError) return typeError;
|
|
212
|
+
}
|
|
84
213
|
if (field.pattern && typeof value === "string") {
|
|
85
214
|
if (!field.pattern.test(value)) {
|
|
86
215
|
return field.patternMessage || "Invalid format";
|
|
@@ -102,10 +231,25 @@ var runValidators = (value, field, allValues, fieldTypes) => {
|
|
|
102
231
|
return `Must be no more than ${field.max}`;
|
|
103
232
|
}
|
|
104
233
|
}
|
|
105
|
-
if (field.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
234
|
+
if (field.type === "date" && isDateValueObject(value)) {
|
|
235
|
+
if (field.min && isDateValueObject(field.min) && compareDateValues(value, field.min) < 0) {
|
|
236
|
+
return field.minValidationMessage || "Date is too early";
|
|
237
|
+
}
|
|
238
|
+
if (field.max && isDateValueObject(field.max) && compareDateValues(value, field.max) > 0) {
|
|
239
|
+
return field.maxValidationMessage || "Date is too late";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (field.type === "time" && isTimeValueObject(value)) {
|
|
243
|
+
if (field.min && isTimeValueObject(field.min) && compareTimeValues(value, field.min) < 0) {
|
|
244
|
+
return field.minValidationMessage || "Time is too early";
|
|
245
|
+
}
|
|
246
|
+
if (field.max && isTimeValueObject(field.max) && compareTimeValues(value, field.max) > 0) {
|
|
247
|
+
return field.maxValidationMessage || "Time is too late";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (includeCustomValidators) {
|
|
251
|
+
const customError = runCustomSyncValidators(value, field, allValues);
|
|
252
|
+
if (customError) return customError;
|
|
109
253
|
}
|
|
110
254
|
return null;
|
|
111
255
|
};
|
|
@@ -117,6 +261,58 @@ var resolveOptions = (field, allValues) => {
|
|
|
117
261
|
if (typeof field.options === "function") return field.options(allValues);
|
|
118
262
|
return field.options || [];
|
|
119
263
|
};
|
|
264
|
+
var getDependsOnName = (field) => field.dependsOnConfig && field.dependsOnConfig.field;
|
|
265
|
+
var getDependsOnDisplay = (field) => field.dependsOnConfig && field.dependsOnConfig.display || "grouped";
|
|
266
|
+
var getDependsOnLabel = (field) => field.dependsOnConfig && field.dependsOnConfig.label;
|
|
267
|
+
var getDependsOnMessage = (field) => field.dependsOnConfig && field.dependsOnConfig.message;
|
|
268
|
+
var getRepeaterErrorKey = (fieldName, rowIdx, subFieldName) => `${fieldName}[${rowIdx}].${subFieldName}`;
|
|
269
|
+
var isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
|
|
270
|
+
var deepEqual = (a, b) => {
|
|
271
|
+
if (Object.is(a, b)) return true;
|
|
272
|
+
if (typeof a !== typeof b) return false;
|
|
273
|
+
if (a == null || b == null) return false;
|
|
274
|
+
if (Array.isArray(a)) {
|
|
275
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
276
|
+
for (let i = 0; i < a.length; i++) {
|
|
277
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (a instanceof Date && b instanceof Date) {
|
|
282
|
+
return a.getTime() === b.getTime();
|
|
283
|
+
}
|
|
284
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
285
|
+
const aKeys = Object.keys(a);
|
|
286
|
+
const bKeys = Object.keys(b);
|
|
287
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
288
|
+
for (const key of aKeys) {
|
|
289
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
290
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
291
|
+
}
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
};
|
|
296
|
+
var fieldSetHasErrors = (errors, fields) => {
|
|
297
|
+
if (!errors || !fields || fields.length === 0) return false;
|
|
298
|
+
const names = new Set(fields.map((field) => field.name));
|
|
299
|
+
return Object.keys(errors).some((errorKey) => {
|
|
300
|
+
const base = errorKey.split("[")[0];
|
|
301
|
+
return names.has(base);
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
var deepClone = (value) => {
|
|
305
|
+
if (Array.isArray(value)) return value.map(deepClone);
|
|
306
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
307
|
+
if (isPlainObject(value)) {
|
|
308
|
+
const next = {};
|
|
309
|
+
for (const key of Object.keys(value)) {
|
|
310
|
+
next[key] = deepClone(value[key]);
|
|
311
|
+
}
|
|
312
|
+
return next;
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
};
|
|
120
316
|
var useFormPrefill = (properties, mapping) => {
|
|
121
317
|
return (0, import_react.useMemo)(() => {
|
|
122
318
|
if (!properties) return {};
|
|
@@ -186,28 +382,30 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
186
382
|
// validate current step fields before Next
|
|
187
383
|
} = props;
|
|
188
384
|
const {
|
|
189
|
-
|
|
190
|
-
// submit
|
|
385
|
+
labels,
|
|
386
|
+
// { submit, cancel, back, next } — i18n label object
|
|
191
387
|
submitVariant = "primary",
|
|
192
388
|
// submit button variant
|
|
193
389
|
showCancel = false,
|
|
194
390
|
// show cancel button
|
|
195
|
-
cancelLabel = "Cancel",
|
|
196
|
-
// cancel button text
|
|
197
391
|
onCancel,
|
|
198
392
|
// () => void
|
|
199
393
|
submitPosition = "bottom",
|
|
200
394
|
// "bottom" | "none"
|
|
201
395
|
loading: controlledLoading,
|
|
202
396
|
// controlled loading state
|
|
203
|
-
disabled = false
|
|
397
|
+
disabled = false,
|
|
204
398
|
// disable entire form
|
|
399
|
+
renderButtons: renderButtonsProp
|
|
400
|
+
// custom action row renderer
|
|
205
401
|
} = props;
|
|
206
402
|
const {
|
|
207
403
|
columns = 1,
|
|
208
404
|
// number of grid columns (1 = full-width stack)
|
|
209
405
|
columnWidth,
|
|
210
406
|
// AutoGrid columnWidth — responsive layout (overrides columns)
|
|
407
|
+
maxColumns,
|
|
408
|
+
// cap number of columns per row in AutoGrid mode
|
|
211
409
|
layout,
|
|
212
410
|
// explicit row layout array (overrides columns + columnWidth)
|
|
213
411
|
sections,
|
|
@@ -218,6 +416,10 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
218
416
|
// show * on required fields
|
|
219
417
|
noFormWrapper = false,
|
|
220
418
|
// skip HubSpot <Form> wrapper
|
|
419
|
+
autoComplete,
|
|
420
|
+
// form autoComplete attribute
|
|
421
|
+
formProps,
|
|
422
|
+
// pass-through props for Form wrapper
|
|
221
423
|
fieldTypes
|
|
222
424
|
// Record<string, FieldTypePlugin> — custom field type registry
|
|
223
425
|
} = props;
|
|
@@ -228,8 +430,12 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
228
430
|
// string — form-level success alert
|
|
229
431
|
readOnly: formReadOnly = false,
|
|
230
432
|
// boolean — lock all fields
|
|
231
|
-
readOnlyMessage
|
|
433
|
+
readOnlyMessage,
|
|
232
434
|
// string — warning alert when readOnly
|
|
435
|
+
alerts,
|
|
436
|
+
// { addAlert, readOnlyTitle, errorTitle, successTitle }
|
|
437
|
+
errors: controlledErrors
|
|
438
|
+
// controlled validation errors
|
|
233
439
|
} = props;
|
|
234
440
|
const {
|
|
235
441
|
onDirtyChange,
|
|
@@ -237,6 +443,38 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
237
443
|
autoSave
|
|
238
444
|
// { debounce: number, onAutoSave: (values) => void }
|
|
239
445
|
} = props;
|
|
446
|
+
const submitButtonLabel = (labels == null ? void 0 : labels.submit) || "Submit";
|
|
447
|
+
const cancelButtonLabel = (labels == null ? void 0 : labels.cancel) || "Cancel";
|
|
448
|
+
const backButtonLabel = (labels == null ? void 0 : labels.back) || "Back";
|
|
449
|
+
const nextButtonLabel = (labels == null ? void 0 : labels.next) || "Next";
|
|
450
|
+
const addAlert = alerts == null ? void 0 : alerts.addAlert;
|
|
451
|
+
const readOnlyTitle = (alerts == null ? void 0 : alerts.readOnlyTitle) || "Read Only";
|
|
452
|
+
const errorTitle = (alerts == null ? void 0 : alerts.errorTitle) || "Error";
|
|
453
|
+
const successTitle = (alerts == null ? void 0 : alerts.successTitle) || "Success";
|
|
454
|
+
const prevErrorRef = (0, import_react.useRef)(formError);
|
|
455
|
+
const prevSuccessRef = (0, import_react.useRef)(formSuccess);
|
|
456
|
+
(0, import_react.useEffect)(() => {
|
|
457
|
+
if (!addAlert) return;
|
|
458
|
+
if (formError && formError !== prevErrorRef.current) {
|
|
459
|
+
addAlert({
|
|
460
|
+
type: "danger",
|
|
461
|
+
title: errorTitle,
|
|
462
|
+
message: typeof formError === "string" ? formError : void 0
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
prevErrorRef.current = formError;
|
|
466
|
+
}, [addAlert, formError, errorTitle]);
|
|
467
|
+
(0, import_react.useEffect)(() => {
|
|
468
|
+
if (!addAlert) return;
|
|
469
|
+
if (formSuccess && formSuccess !== prevSuccessRef.current) {
|
|
470
|
+
addAlert({
|
|
471
|
+
type: "success",
|
|
472
|
+
title: successTitle,
|
|
473
|
+
message: formSuccess
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
prevSuccessRef.current = formSuccess;
|
|
477
|
+
}, [addAlert, formSuccess, successTitle]);
|
|
240
478
|
const computeInitialValues = () => {
|
|
241
479
|
const vals = {};
|
|
242
480
|
for (const field of fields) {
|
|
@@ -252,25 +490,99 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
252
490
|
const [internalErrors, setInternalErrors] = (0, import_react.useState)({});
|
|
253
491
|
const [internalStep, setInternalStep] = (0, import_react.useState)(0);
|
|
254
492
|
const [internalLoading, setInternalLoading] = (0, import_react.useState)(false);
|
|
255
|
-
const [touchedFields, setTouchedFields] = (0, import_react.useState)({});
|
|
256
493
|
const [validatingFields, setValidatingFields] = (0, import_react.useState)({});
|
|
257
494
|
const asyncValidationRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
495
|
+
const asyncAbortRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
496
|
+
const asyncValidationVersionRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
258
497
|
const debounceTimersRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
498
|
+
const inputDebounceRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
499
|
+
const rowKeyRef = (0, import_react.useRef)(/* @__PURE__ */ new WeakMap());
|
|
500
|
+
const rowKeyCounterRef = (0, import_react.useRef)(0);
|
|
259
501
|
const initialSnapshot = (0, import_react.useRef)(null);
|
|
260
502
|
if (initialSnapshot.current === null) {
|
|
261
|
-
initialSnapshot.current =
|
|
503
|
+
initialSnapshot.current = deepClone(computeInitialValues());
|
|
262
504
|
}
|
|
263
505
|
const formValues = values != null ? values : internalValues;
|
|
506
|
+
const formErrors = controlledErrors != null ? controlledErrors : internalErrors;
|
|
264
507
|
const currentStep = controlledStep != null ? controlledStep : internalStep;
|
|
265
508
|
const isLoading = controlledLoading != null ? controlledLoading : internalLoading;
|
|
266
509
|
const isMultiStep = Array.isArray(steps) && steps.length > 0;
|
|
510
|
+
const formValuesRef = (0, import_react.useRef)(formValues);
|
|
511
|
+
const formErrorsRef = (0, import_react.useRef)(formErrors);
|
|
512
|
+
const draftValuesRef = (0, import_react.useRef)(null);
|
|
513
|
+
formValuesRef.current = formValues;
|
|
514
|
+
formErrorsRef.current = formErrors;
|
|
515
|
+
const fieldByName = (0, import_react.useMemo)(() => {
|
|
516
|
+
const map = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const field of fields) map.set(field.name, field);
|
|
518
|
+
return map;
|
|
519
|
+
}, [fields]);
|
|
520
|
+
const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
|
|
521
|
+
const configWarningsRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
522
|
+
const warnConfig = (0, import_react.useCallback)((message) => {
|
|
523
|
+
if (!isDev) return;
|
|
524
|
+
if (configWarningsRef.current.has(message)) return;
|
|
525
|
+
configWarningsRef.current.add(message);
|
|
526
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
527
|
+
console.warn(`[FormBuilder] ${message}`);
|
|
528
|
+
}
|
|
529
|
+
}, [isDev]);
|
|
530
|
+
const replaceErrors = (0, import_react.useCallback)(
|
|
531
|
+
(nextErrors) => {
|
|
532
|
+
if (controlledErrors == null) setInternalErrors(nextErrors);
|
|
533
|
+
if (onValidationChange) onValidationChange(nextErrors);
|
|
534
|
+
},
|
|
535
|
+
[controlledErrors, onValidationChange]
|
|
536
|
+
);
|
|
537
|
+
const updateErrors = (0, import_react.useCallback)(
|
|
538
|
+
(newErrors) => {
|
|
539
|
+
const mergeErrors = (base) => {
|
|
540
|
+
const merged = { ...base, ...newErrors };
|
|
541
|
+
for (const key of Object.keys(newErrors)) {
|
|
542
|
+
if (newErrors[key] === null || newErrors[key] === void 0) {
|
|
543
|
+
delete merged[key];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return merged;
|
|
547
|
+
};
|
|
548
|
+
if (controlledErrors != null) {
|
|
549
|
+
const merged = mergeErrors(formErrorsRef.current || {});
|
|
550
|
+
if (onValidationChange) onValidationChange(merged);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
setInternalErrors((prev) => {
|
|
554
|
+
const merged = mergeErrors(prev);
|
|
555
|
+
if (onValidationChange) onValidationChange(merged);
|
|
556
|
+
return merged;
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
[controlledErrors, onValidationChange]
|
|
560
|
+
);
|
|
561
|
+
const getFieldEmptyValue = (0, import_react.useCallback)(
|
|
562
|
+
(field) => {
|
|
563
|
+
const plugin = fieldTypes && fieldTypes[field.type];
|
|
564
|
+
return plugin && plugin.getEmptyValue ? plugin.getEmptyValue() : getEmptyValue(field);
|
|
565
|
+
},
|
|
566
|
+
[fieldTypes]
|
|
567
|
+
);
|
|
568
|
+
const getRowKey = (0, import_react.useCallback)((fieldName, row, index) => {
|
|
569
|
+
if (!row || typeof row !== "object") return `${fieldName}-idx-${index}`;
|
|
570
|
+
if (!rowKeyRef.current.has(row)) {
|
|
571
|
+
rowKeyCounterRef.current += 1;
|
|
572
|
+
rowKeyRef.current.set(row, `${fieldName}-row-${rowKeyCounterRef.current}`);
|
|
573
|
+
}
|
|
574
|
+
return rowKeyRef.current.get(row);
|
|
575
|
+
}, []);
|
|
267
576
|
(0, import_react.useEffect)(() => {
|
|
268
577
|
return () => {
|
|
269
578
|
for (const timer of debounceTimersRef.current.values()) clearTimeout(timer);
|
|
579
|
+
for (const timer of inputDebounceRef.current.values()) clearTimeout(timer);
|
|
580
|
+
for (const controller of asyncAbortRef.current.values()) controller.abort();
|
|
581
|
+
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
|
270
582
|
};
|
|
271
583
|
}, []);
|
|
272
584
|
const isDirty = (0, import_react.useMemo)(() => {
|
|
273
|
-
return
|
|
585
|
+
return !deepEqual(formValues, initialSnapshot.current);
|
|
274
586
|
}, [formValues]);
|
|
275
587
|
const prevDirtyRef = (0, import_react.useRef)(false);
|
|
276
588
|
(0, import_react.useEffect)(() => {
|
|
@@ -280,36 +592,129 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
280
592
|
}
|
|
281
593
|
}, [isDirty, onDirtyChange]);
|
|
282
594
|
const autoSaveTimerRef = (0, import_react.useRef)(null);
|
|
595
|
+
const autoSaveRef = (0, import_react.useRef)(autoSave);
|
|
596
|
+
autoSaveRef.current = autoSave;
|
|
597
|
+
const prevAutoSaveValues = (0, import_react.useRef)(deepClone(formValues));
|
|
283
598
|
(0, import_react.useEffect)(() => {
|
|
284
|
-
|
|
599
|
+
const cfg = autoSaveRef.current;
|
|
600
|
+
if (!cfg || !cfg.onAutoSave || !isDirty) return;
|
|
601
|
+
if (deepEqual(prevAutoSaveValues.current, formValues)) return;
|
|
602
|
+
prevAutoSaveValues.current = deepClone(formValues);
|
|
285
603
|
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
|
286
604
|
autoSaveTimerRef.current = setTimeout(() => {
|
|
287
605
|
autoSaveTimerRef.current = null;
|
|
288
|
-
|
|
289
|
-
},
|
|
606
|
+
autoSaveRef.current.onAutoSave(formValues);
|
|
607
|
+
}, cfg.debounce || 1e3);
|
|
290
608
|
return () => {
|
|
291
609
|
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
|
292
610
|
};
|
|
293
|
-
}, [formValues, isDirty
|
|
294
|
-
const
|
|
295
|
-
|
|
611
|
+
}, [formValues, isDirty]);
|
|
612
|
+
const allVisibleFields = (0, import_react.useMemo)(() => {
|
|
613
|
+
return fields.filter((f) => {
|
|
296
614
|
if (f.visible && !f.visible(formValues)) return false;
|
|
297
615
|
return true;
|
|
298
616
|
});
|
|
617
|
+
}, [fields, formValues]);
|
|
618
|
+
const visibleFields = (0, import_react.useMemo)(() => {
|
|
619
|
+
let filtered = allVisibleFields;
|
|
299
620
|
if (isMultiStep && steps[currentStep] && steps[currentStep].fields) {
|
|
300
621
|
const stepFieldNames = new Set(steps[currentStep].fields);
|
|
301
622
|
filtered = filtered.filter((f) => stepFieldNames.has(f.name));
|
|
302
623
|
}
|
|
303
624
|
return filtered;
|
|
304
|
-
}, [
|
|
625
|
+
}, [allVisibleFields, isMultiStep, steps, currentStep]);
|
|
626
|
+
(0, import_react.useEffect)(() => {
|
|
627
|
+
const nameSet = new Set(fields.map((f) => f.name));
|
|
628
|
+
if (nameSet.size !== fields.length) {
|
|
629
|
+
warnConfig("Duplicate field names detected. Field names must be unique.");
|
|
630
|
+
}
|
|
631
|
+
for (const field of fields) {
|
|
632
|
+
const parentName = getDependsOnName(field);
|
|
633
|
+
if (parentName && !nameSet.has(parentName)) {
|
|
634
|
+
warnConfig(`Field "${field.name}" depends on missing field "${parentName}".`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (steps) {
|
|
638
|
+
for (let i = 0; i < steps.length; i++) {
|
|
639
|
+
const step = steps[i];
|
|
640
|
+
if (!step.fields) continue;
|
|
641
|
+
for (const fieldName of step.fields) {
|
|
642
|
+
if (!nameSet.has(fieldName)) {
|
|
643
|
+
warnConfig(`Step ${i + 1} references missing field "${fieldName}".`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (layout) {
|
|
649
|
+
for (const row of layout) {
|
|
650
|
+
for (const entry of row) {
|
|
651
|
+
const fieldName = typeof entry === "string" ? entry : entry.field;
|
|
652
|
+
if (!nameSet.has(fieldName)) {
|
|
653
|
+
warnConfig(`Layout references missing field "${fieldName}".`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (sections) {
|
|
659
|
+
for (const section of sections) {
|
|
660
|
+
for (const fieldName of section.fields || []) {
|
|
661
|
+
if (!nameSet.has(fieldName)) {
|
|
662
|
+
warnConfig(`Section "${section.id}" references missing field "${fieldName}".`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}, [fields, steps, layout, sections, warnConfig]);
|
|
668
|
+
const validateRepeaterField = (0, import_react.useCallback)(
|
|
669
|
+
(field, value, allValues) => {
|
|
670
|
+
const errors = {};
|
|
671
|
+
const rows = Array.isArray(value) ? value : [];
|
|
672
|
+
const subFields = field.fields || [];
|
|
673
|
+
let firstSubError = null;
|
|
674
|
+
if (resolveRequired(field, allValues) && rows.length === 0) {
|
|
675
|
+
const requiredError = `${field.label} is required`;
|
|
676
|
+
errors[field.name] = requiredError;
|
|
677
|
+
return { errors, hasErrors: true };
|
|
678
|
+
}
|
|
679
|
+
if (typeof field.min === "number" && rows.length < field.min) {
|
|
680
|
+
errors[field.name] = `Must have at least ${field.min} ${field.min === 1 ? "row" : "rows"}`;
|
|
681
|
+
} else if (typeof field.max === "number" && rows.length > field.max) {
|
|
682
|
+
errors[field.name] = `Must have no more than ${field.max} ${field.max === 1 ? "row" : "rows"}`;
|
|
683
|
+
}
|
|
684
|
+
rows.forEach((row, rowIdx) => {
|
|
685
|
+
const rowValues = { ...allValues, [field.name]: rows };
|
|
686
|
+
subFields.forEach((subField) => {
|
|
687
|
+
if (subField.visible && !subField.visible(rowValues)) return;
|
|
688
|
+
const err = runValidators(row == null ? void 0 : row[subField.name], subField, rowValues, fieldTypes);
|
|
689
|
+
if (!err) return;
|
|
690
|
+
const key = getRepeaterErrorKey(field.name, rowIdx, subField.name);
|
|
691
|
+
errors[key] = err;
|
|
692
|
+
if (!firstSubError) firstSubError = { row: rowIdx + 1, message: err };
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
if (!errors[field.name] && firstSubError) {
|
|
696
|
+
errors[field.name] = `Row ${firstSubError.row}: ${firstSubError.message}`;
|
|
697
|
+
}
|
|
698
|
+
return { errors, hasErrors: Object.keys(errors).length > 0 };
|
|
699
|
+
},
|
|
700
|
+
[fieldTypes]
|
|
701
|
+
);
|
|
305
702
|
const validateField = (0, import_react.useCallback)(
|
|
306
703
|
(name, value) => {
|
|
307
|
-
const field =
|
|
704
|
+
const field = fieldByName.get(name);
|
|
308
705
|
if (!field) return null;
|
|
309
706
|
if (field.visible && !field.visible(formValues)) return null;
|
|
707
|
+
if (field.type === "repeater") {
|
|
708
|
+
const repeaterResult = validateRepeaterField(
|
|
709
|
+
field,
|
|
710
|
+
value != null ? value : formValues[name],
|
|
711
|
+
formValues
|
|
712
|
+
);
|
|
713
|
+
return repeaterResult.errors[name] || null;
|
|
714
|
+
}
|
|
310
715
|
return runValidators(value != null ? value : formValues[name], field, formValues, fieldTypes);
|
|
311
716
|
},
|
|
312
|
-
[
|
|
717
|
+
[fieldByName, formValues, validateRepeaterField, fieldTypes]
|
|
313
718
|
);
|
|
314
719
|
const validateVisibleFields = (0, import_react.useCallback)(
|
|
315
720
|
(fieldSubset) => {
|
|
@@ -317,6 +722,14 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
317
722
|
const errors = {};
|
|
318
723
|
let hasErrors = false;
|
|
319
724
|
for (const field of toValidate) {
|
|
725
|
+
if (field.type === "repeater") {
|
|
726
|
+
const repeaterResult = validateRepeaterField(field, formValues[field.name], formValues);
|
|
727
|
+
if (repeaterResult.hasErrors) {
|
|
728
|
+
Object.assign(errors, repeaterResult.errors);
|
|
729
|
+
hasErrors = true;
|
|
730
|
+
}
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
320
733
|
const err = runValidators(formValues[field.name], field, formValues, fieldTypes);
|
|
321
734
|
if (err) {
|
|
322
735
|
errors[field.name] = err;
|
|
@@ -325,64 +738,87 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
325
738
|
}
|
|
326
739
|
return { errors, hasErrors };
|
|
327
740
|
},
|
|
328
|
-
[visibleFields, formValues]
|
|
329
|
-
);
|
|
330
|
-
const updateErrors = (0, import_react.useCallback)(
|
|
331
|
-
(newErrors) => {
|
|
332
|
-
setInternalErrors((prev) => {
|
|
333
|
-
const merged = { ...prev, ...newErrors };
|
|
334
|
-
for (const key of Object.keys(merged)) {
|
|
335
|
-
if (newErrors[key] === null || newErrors[key] === void 0) {
|
|
336
|
-
delete merged[key];
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (onValidationChange) onValidationChange(merged);
|
|
340
|
-
return merged;
|
|
341
|
-
});
|
|
342
|
-
},
|
|
343
|
-
[onValidationChange]
|
|
741
|
+
[visibleFields, formValues, validateRepeaterField, fieldTypes]
|
|
344
742
|
);
|
|
345
743
|
const runAsyncValidation = (0, import_react.useCallback)(
|
|
346
744
|
(name, value) => {
|
|
347
|
-
const field =
|
|
348
|
-
if (!field ||
|
|
745
|
+
const field = fieldByName.get(name);
|
|
746
|
+
if (!field || field.type === "repeater") return null;
|
|
349
747
|
const val = value != null ? value : formValues[name];
|
|
350
|
-
const syncError = runValidators(val, field, formValues, fieldTypes);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
(
|
|
356
|
-
|
|
748
|
+
const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false });
|
|
749
|
+
const prevController = asyncAbortRef.current.get(name);
|
|
750
|
+
if (prevController) prevController.abort();
|
|
751
|
+
asyncAbortRef.current.delete(name);
|
|
752
|
+
setValidatingFields((prev) => {
|
|
753
|
+
if (!prev[name]) return prev;
|
|
754
|
+
const next = { ...prev };
|
|
755
|
+
delete next[name];
|
|
756
|
+
return next;
|
|
757
|
+
});
|
|
758
|
+
if (syncError) return null;
|
|
759
|
+
const version = (asyncValidationVersionRef.current.get(name) || 0) + 1;
|
|
760
|
+
asyncValidationVersionRef.current.set(name, version);
|
|
761
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
762
|
+
if (controller) asyncAbortRef.current.set(name, controller);
|
|
763
|
+
let asyncPromises;
|
|
764
|
+
try {
|
|
765
|
+
asyncPromises = collectAsyncValidatorPromises(
|
|
766
|
+
val,
|
|
767
|
+
field,
|
|
768
|
+
formValues,
|
|
769
|
+
controller ? { signal: controller.signal } : void 0
|
|
770
|
+
);
|
|
771
|
+
} catch (err) {
|
|
772
|
+
updateErrors({ [name]: (err == null ? void 0 : err.message) || "Validation failed" });
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
if (asyncPromises.length === 0) {
|
|
776
|
+
asyncAbortRef.current.delete(name);
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
const validationPromise = Promise.all(asyncPromises).then(
|
|
780
|
+
(results) => {
|
|
781
|
+
if (asyncValidationVersionRef.current.get(name) !== version) return;
|
|
357
782
|
asyncValidationRef.current.delete(name);
|
|
783
|
+
asyncAbortRef.current.delete(name);
|
|
358
784
|
setValidatingFields((prev) => {
|
|
359
785
|
const next = { ...prev };
|
|
360
786
|
delete next[name];
|
|
361
787
|
return next;
|
|
362
788
|
});
|
|
363
|
-
|
|
789
|
+
let err = null;
|
|
790
|
+
for (const result of results) {
|
|
791
|
+
const normalized = normalizeValidatorResult(result);
|
|
792
|
+
if (normalized) {
|
|
793
|
+
err = normalized;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
364
797
|
updateErrors({ [name]: err });
|
|
365
798
|
},
|
|
366
799
|
(rejection) => {
|
|
367
|
-
if (
|
|
800
|
+
if (asyncValidationVersionRef.current.get(name) !== version) return;
|
|
368
801
|
asyncValidationRef.current.delete(name);
|
|
802
|
+
asyncAbortRef.current.delete(name);
|
|
369
803
|
setValidatingFields((prev) => {
|
|
370
804
|
const next = { ...prev };
|
|
371
805
|
delete next[name];
|
|
372
806
|
return next;
|
|
373
807
|
});
|
|
808
|
+
if (rejection && rejection.name === "AbortError") return;
|
|
374
809
|
updateErrors({ [name]: (rejection == null ? void 0 : rejection.message) || "Validation failed" });
|
|
375
810
|
}
|
|
376
811
|
);
|
|
377
812
|
asyncValidationRef.current.set(name, validationPromise);
|
|
378
813
|
setValidatingFields((prev) => ({ ...prev, [name]: true }));
|
|
814
|
+
return validationPromise;
|
|
379
815
|
},
|
|
380
|
-
[
|
|
816
|
+
[fieldByName, formValues, fieldTypes, updateErrors]
|
|
381
817
|
);
|
|
382
818
|
const triggerAsyncValidation = (0, import_react.useCallback)(
|
|
383
819
|
(name, value) => {
|
|
384
|
-
const field =
|
|
385
|
-
if (!field ||
|
|
820
|
+
const field = fieldByName.get(name);
|
|
821
|
+
if (!field || field.type === "repeater") return;
|
|
386
822
|
const debounceMs = field.validateDebounce;
|
|
387
823
|
if (debounceMs && debounceMs > 0) {
|
|
388
824
|
const existing = debounceTimersRef.current.get(name);
|
|
@@ -396,44 +832,93 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
396
832
|
runAsyncValidation(name, value);
|
|
397
833
|
}
|
|
398
834
|
},
|
|
399
|
-
[
|
|
835
|
+
[fieldByName, runAsyncValidation]
|
|
400
836
|
);
|
|
401
|
-
const
|
|
402
|
-
(
|
|
837
|
+
const commitValues = (0, import_react.useCallback)(
|
|
838
|
+
(nextValues) => {
|
|
839
|
+
formValuesRef.current = nextValues;
|
|
403
840
|
if (values != null) {
|
|
404
|
-
if (onChange) onChange(
|
|
841
|
+
if (onChange) onChange(nextValues);
|
|
405
842
|
} else {
|
|
406
|
-
setInternalValues(
|
|
843
|
+
setInternalValues(nextValues);
|
|
407
844
|
}
|
|
408
845
|
},
|
|
409
|
-
[values, onChange
|
|
846
|
+
[values, onChange]
|
|
847
|
+
);
|
|
848
|
+
const setFieldValueSilent = (0, import_react.useCallback)(
|
|
849
|
+
(name, value) => {
|
|
850
|
+
const base = draftValuesRef.current || formValuesRef.current || {};
|
|
851
|
+
const nextValues = { ...base, [name]: value };
|
|
852
|
+
draftValuesRef.current = nextValues;
|
|
853
|
+
commitValues(nextValues);
|
|
854
|
+
},
|
|
855
|
+
[commitValues]
|
|
410
856
|
);
|
|
411
857
|
const handleFieldChange = (0, import_react.useCallback)(
|
|
412
858
|
(name, value) => {
|
|
413
|
-
const newValues = { ...
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
859
|
+
const newValues = { ...formValuesRef.current, [name]: value };
|
|
860
|
+
const queue = [name];
|
|
861
|
+
const visited = /* @__PURE__ */ new Set();
|
|
862
|
+
const clearedErrors = {};
|
|
863
|
+
while (queue.length > 0) {
|
|
864
|
+
const current = queue.shift();
|
|
865
|
+
if (!current || visited.has(current)) continue;
|
|
866
|
+
visited.add(current);
|
|
867
|
+
fields.forEach((dep) => {
|
|
868
|
+
const parentName = getDependsOnName(dep);
|
|
869
|
+
if (parentName !== current || dep.name === current) return;
|
|
870
|
+
if (!dep.options) return;
|
|
871
|
+
const depOptions = resolveOptions(dep, newValues);
|
|
872
|
+
const depValue = newValues[dep.name];
|
|
873
|
+
if (depValue == null || depValue === "") return;
|
|
874
|
+
const validValues = new Set(depOptions.map((o) => o.value));
|
|
875
|
+
let nextDepValue = depValue;
|
|
876
|
+
let changed = false;
|
|
877
|
+
if (Array.isArray(depValue)) {
|
|
878
|
+
const filtered = depValue.filter((v) => validValues.has(v));
|
|
879
|
+
if (filtered.length !== depValue.length) {
|
|
880
|
+
nextDepValue = filtered;
|
|
881
|
+
changed = true;
|
|
882
|
+
}
|
|
883
|
+
} else if (!validValues.has(depValue)) {
|
|
884
|
+
nextDepValue = getFieldEmptyValue(dep);
|
|
885
|
+
changed = true;
|
|
886
|
+
}
|
|
887
|
+
if (changed) {
|
|
888
|
+
newValues[dep.name] = nextDepValue;
|
|
889
|
+
queue.push(dep.name);
|
|
890
|
+
if (formErrorsRef.current[dep.name] != null) {
|
|
891
|
+
clearedErrors[dep.name] = null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
});
|
|
418
895
|
}
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
896
|
+
if (formErrorsRef.current[name] != null) {
|
|
897
|
+
clearedErrors[name] = null;
|
|
898
|
+
}
|
|
899
|
+
for (const key of Object.keys(formErrorsRef.current)) {
|
|
900
|
+
if (key.startsWith(`${name}[`)) {
|
|
901
|
+
clearedErrors[key] = null;
|
|
902
|
+
}
|
|
422
903
|
}
|
|
423
|
-
|
|
904
|
+
draftValuesRef.current = newValues;
|
|
905
|
+
commitValues(newValues);
|
|
906
|
+
if (onFieldChange) onFieldChange(name, value, newValues);
|
|
907
|
+
if (Object.keys(clearedErrors).length > 0) updateErrors(clearedErrors);
|
|
908
|
+
const field = fieldByName.get(name);
|
|
424
909
|
if (field && field.onFieldChange) {
|
|
425
910
|
field.onFieldChange(value, newValues, {
|
|
426
911
|
setFieldValue: setFieldValueSilent,
|
|
427
912
|
setFieldError: (fieldName, message) => updateErrors({ [fieldName]: message })
|
|
428
913
|
});
|
|
429
914
|
}
|
|
915
|
+
draftValuesRef.current = null;
|
|
430
916
|
},
|
|
431
|
-
[
|
|
917
|
+
[fields, getFieldEmptyValue, commitValues, onFieldChange, updateErrors, fieldByName, setFieldValueSilent]
|
|
432
918
|
);
|
|
433
|
-
const inputDebounceRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
434
919
|
const handleDebouncedFieldChange = (0, import_react.useCallback)(
|
|
435
920
|
(name, value) => {
|
|
436
|
-
const field =
|
|
921
|
+
const field = fieldByName.get(name);
|
|
437
922
|
const debounceMs = field && field.debounce;
|
|
438
923
|
if (debounceMs && debounceMs > 0) {
|
|
439
924
|
const existing = inputDebounceRef.current.get(name);
|
|
@@ -447,26 +932,24 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
447
932
|
handleFieldChange(name, value);
|
|
448
933
|
}
|
|
449
934
|
},
|
|
450
|
-
[
|
|
935
|
+
[fieldByName, handleFieldChange]
|
|
451
936
|
);
|
|
452
937
|
const handleFieldInput = (0, import_react.useCallback)(
|
|
453
938
|
(name, value) => {
|
|
454
|
-
if (validateOnChange)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
939
|
+
if (!validateOnChange) return;
|
|
940
|
+
const err = validateField(name, value);
|
|
941
|
+
updateErrors({ [name]: err });
|
|
458
942
|
},
|
|
459
943
|
[validateOnChange, validateField, updateErrors]
|
|
460
944
|
);
|
|
461
945
|
const handleFieldBlur = (0, import_react.useCallback)(
|
|
462
946
|
(name, value) => {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
947
|
+
if (!validateOnBlur) return;
|
|
948
|
+
const resolvedValue = value != null ? value : formValues[name];
|
|
949
|
+
const err = validateField(name, resolvedValue);
|
|
950
|
+
updateErrors({ [name]: err });
|
|
951
|
+
if (!err) {
|
|
952
|
+
triggerAsyncValidation(name, resolvedValue);
|
|
470
953
|
}
|
|
471
954
|
},
|
|
472
955
|
[validateOnBlur, validateField, updateErrors, formValues, triggerAsyncValidation]
|
|
@@ -475,30 +958,33 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
475
958
|
async (e) => {
|
|
476
959
|
if (e && e.preventDefault) e.preventDefault();
|
|
477
960
|
if (validateOnSubmit) {
|
|
478
|
-
const
|
|
479
|
-
const { errors, hasErrors } = validateVisibleFields(allVisible);
|
|
961
|
+
const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
|
|
480
962
|
if (hasErrors) {
|
|
481
|
-
|
|
482
|
-
if (onValidationChange) onValidationChange(errors);
|
|
963
|
+
replaceErrors(errors);
|
|
483
964
|
return;
|
|
484
965
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
966
|
+
const asyncSubmitValidations = allVisibleFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
|
|
967
|
+
if (asyncSubmitValidations.length > 0 || asyncValidationRef.current.size > 0) {
|
|
968
|
+
const pendingValidations = [
|
|
969
|
+
.../* @__PURE__ */ new Set([
|
|
970
|
+
...asyncSubmitValidations,
|
|
971
|
+
...Array.from(asyncValidationRef.current.values())
|
|
972
|
+
])
|
|
973
|
+
];
|
|
974
|
+
await Promise.all(pendingValidations);
|
|
975
|
+
if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
|
|
490
976
|
}
|
|
491
977
|
}
|
|
492
978
|
const reset = () => {
|
|
493
979
|
const fresh = computeInitialValues();
|
|
494
980
|
if (values == null) setInternalValues(fresh);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
981
|
+
replaceErrors({});
|
|
982
|
+
initialSnapshot.current = deepClone(fresh);
|
|
983
|
+
prevAutoSaveValues.current = deepClone(fresh);
|
|
498
984
|
};
|
|
499
985
|
const rawValues = {};
|
|
500
986
|
for (const key of Object.keys(formValues)) {
|
|
501
|
-
const f =
|
|
987
|
+
const f = fieldByName.get(key);
|
|
502
988
|
if (f && (f.type === "display" || f.type === "crmPropertyList" || f.type === "crmAssociationPropertyList")) continue;
|
|
503
989
|
rawValues[key] = formValues[key];
|
|
504
990
|
}
|
|
@@ -522,25 +1008,34 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
522
1008
|
if (controlledLoading == null) setInternalLoading(false);
|
|
523
1009
|
}
|
|
524
1010
|
},
|
|
525
|
-
[validateOnSubmit,
|
|
1011
|
+
[validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, runAsyncValidation]
|
|
526
1012
|
);
|
|
527
|
-
const handleNext = (0, import_react.useCallback)(() => {
|
|
1013
|
+
const handleNext = (0, import_react.useCallback)(async () => {
|
|
528
1014
|
if (!isMultiStep) return;
|
|
529
1015
|
if (validateStepOnNext && steps[currentStep] && steps[currentStep].fields) {
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
);
|
|
1016
|
+
const stepFieldNames = new Set(steps[currentStep].fields);
|
|
1017
|
+
const stepFields = allVisibleFields.filter((f) => stepFieldNames.has(f.name));
|
|
533
1018
|
const { errors, hasErrors } = validateVisibleFields(stepFields);
|
|
534
1019
|
if (hasErrors) {
|
|
535
|
-
|
|
536
|
-
if (onValidationChange) onValidationChange({ ...internalErrors, ...errors });
|
|
1020
|
+
replaceErrors({ ...formErrorsRef.current, ...errors });
|
|
537
1021
|
return;
|
|
538
1022
|
}
|
|
1023
|
+
const asyncStepValidations = stepFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
|
|
1024
|
+
if (asyncStepValidations.length > 0 || asyncValidationRef.current.size > 0) {
|
|
1025
|
+
const pendingValidations = [
|
|
1026
|
+
.../* @__PURE__ */ new Set([
|
|
1027
|
+
...asyncStepValidations,
|
|
1028
|
+
...Array.from(asyncValidationRef.current.values())
|
|
1029
|
+
])
|
|
1030
|
+
];
|
|
1031
|
+
await Promise.all(pendingValidations);
|
|
1032
|
+
if (fieldSetHasErrors(formErrorsRef.current, stepFields)) return;
|
|
1033
|
+
}
|
|
539
1034
|
}
|
|
540
1035
|
if (steps[currentStep] && steps[currentStep].validate) {
|
|
541
1036
|
const result = steps[currentStep].validate(formValues);
|
|
542
1037
|
if (result !== true && result) {
|
|
543
|
-
|
|
1038
|
+
replaceErrors({ ...formErrorsRef.current, ...result });
|
|
544
1039
|
return;
|
|
545
1040
|
}
|
|
546
1041
|
}
|
|
@@ -550,7 +1045,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
550
1045
|
} else {
|
|
551
1046
|
setInternalStep(nextStep);
|
|
552
1047
|
}
|
|
553
|
-
}, [isMultiStep, validateStepOnNext, steps, currentStep,
|
|
1048
|
+
}, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, runAsyncValidation]);
|
|
554
1049
|
const handleBack = (0, import_react.useCallback)(() => {
|
|
555
1050
|
if (!isMultiStep) return;
|
|
556
1051
|
const prevStep = Math.max(currentStep - 1, 0);
|
|
@@ -575,33 +1070,56 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
575
1070
|
(0, import_react.useImperativeHandle)(ref, () => ({
|
|
576
1071
|
submit: handleSubmit,
|
|
577
1072
|
validate: () => {
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
setInternalErrors(errors);
|
|
1073
|
+
const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
|
|
1074
|
+
replaceErrors(errors);
|
|
581
1075
|
return { valid: !hasErrors, errors };
|
|
582
1076
|
},
|
|
583
1077
|
reset: () => {
|
|
584
1078
|
const fresh = computeInitialValues();
|
|
585
1079
|
if (values == null) setInternalValues(fresh);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1080
|
+
replaceErrors({});
|
|
1081
|
+
initialSnapshot.current = deepClone(fresh);
|
|
1082
|
+
prevAutoSaveValues.current = deepClone(fresh);
|
|
589
1083
|
},
|
|
590
1084
|
getValues: () => formValues,
|
|
591
1085
|
isDirty: () => isDirty,
|
|
592
1086
|
setFieldValue: (name, value) => handleFieldChange(name, value),
|
|
593
1087
|
setFieldError: (name, message) => updateErrors({ [name]: message }),
|
|
594
1088
|
setErrors: (errors) => {
|
|
595
|
-
|
|
596
|
-
if (onValidationChange) onValidationChange(errors);
|
|
1089
|
+
replaceErrors(errors);
|
|
597
1090
|
}
|
|
598
1091
|
}));
|
|
1092
|
+
const setRepeaterSubFieldError = (0, import_react.useCallback)(
|
|
1093
|
+
(fieldName, rowIdx, subFieldName, errorMessage) => {
|
|
1094
|
+
const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
|
|
1095
|
+
const merged = { ...formErrorsRef.current };
|
|
1096
|
+
if (errorMessage) {
|
|
1097
|
+
merged[key] = errorMessage;
|
|
1098
|
+
} else {
|
|
1099
|
+
delete merged[key];
|
|
1100
|
+
}
|
|
1101
|
+
const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
|
|
1102
|
+
const match = k.match(/\[(\d+)\]\./);
|
|
1103
|
+
const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
|
|
1104
|
+
return { key: k, row };
|
|
1105
|
+
}).sort((a, b) => a.row - b.row);
|
|
1106
|
+
if (subErrors.length > 0) {
|
|
1107
|
+
const first = subErrors[0];
|
|
1108
|
+
merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
|
|
1109
|
+
} else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
|
|
1110
|
+
delete merged[fieldName];
|
|
1111
|
+
}
|
|
1112
|
+
replaceErrors(merged);
|
|
1113
|
+
},
|
|
1114
|
+
[replaceErrors]
|
|
1115
|
+
);
|
|
599
1116
|
const renderField = (field) => {
|
|
600
1117
|
const fieldValue = formValues[field.name];
|
|
601
|
-
const fieldError =
|
|
1118
|
+
const fieldError = formErrors[field.name] || null;
|
|
602
1119
|
const hasError = !!fieldError;
|
|
603
1120
|
const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
|
|
604
|
-
const isReadOnly = field.readOnly ||
|
|
1121
|
+
const isReadOnly = field.readOnly || formReadOnly;
|
|
1122
|
+
const isDisabled = disabled || field.disabled || formReadOnly;
|
|
605
1123
|
const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
|
|
606
1124
|
if (field.type === "display") {
|
|
607
1125
|
if (field.render) {
|
|
@@ -660,6 +1178,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
660
1178
|
tooltip: field.tooltip,
|
|
661
1179
|
required: isRequired,
|
|
662
1180
|
readOnly: isReadOnly,
|
|
1181
|
+
disabled: isDisabled,
|
|
663
1182
|
error: hasError,
|
|
664
1183
|
validationMessage: fieldError || void 0,
|
|
665
1184
|
...field.loading || validatingFields[field.name] ? { loading: true } : {},
|
|
@@ -792,6 +1311,9 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
792
1311
|
maxValidationMessage: field.maxValidationMessage,
|
|
793
1312
|
onChange: (v) => {
|
|
794
1313
|
handleFieldChange(field.name, { ...fieldValue, date: v, time: timeVal });
|
|
1314
|
+
},
|
|
1315
|
+
onBlur: (v) => {
|
|
1316
|
+
handleFieldBlur(field.name, { ...fieldValue, date: v, time: timeVal });
|
|
795
1317
|
}
|
|
796
1318
|
}
|
|
797
1319
|
)), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1 }, /* @__PURE__ */ import_react.default.createElement(
|
|
@@ -802,12 +1324,16 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
802
1324
|
description: field.description,
|
|
803
1325
|
tooltip: field.tooltip,
|
|
804
1326
|
readOnly: isReadOnly,
|
|
1327
|
+
disabled: isDisabled,
|
|
805
1328
|
error: hasError,
|
|
806
1329
|
value: timeVal,
|
|
807
1330
|
interval: field.interval,
|
|
808
1331
|
timezone: field.timezone,
|
|
809
1332
|
onChange: (v) => {
|
|
810
1333
|
handleFieldChange(field.name, { ...fieldValue, date: dateVal, time: v });
|
|
1334
|
+
},
|
|
1335
|
+
onBlur: (v) => {
|
|
1336
|
+
handleFieldBlur(field.name, { ...fieldValue, date: dateVal, time: v });
|
|
811
1337
|
}
|
|
812
1338
|
}
|
|
813
1339
|
)));
|
|
@@ -845,6 +1371,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
845
1371
|
textChecked: field.textChecked,
|
|
846
1372
|
textUnchecked: field.textUnchecked,
|
|
847
1373
|
readonly: isReadOnly,
|
|
1374
|
+
disabled: isDisabled,
|
|
848
1375
|
onChange: fieldOnChange,
|
|
849
1376
|
...field.fieldProps || {}
|
|
850
1377
|
}
|
|
@@ -857,6 +1384,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
857
1384
|
checked: !!fieldValue,
|
|
858
1385
|
description: field.description,
|
|
859
1386
|
readOnly: isReadOnly,
|
|
1387
|
+
disabled: isDisabled,
|
|
860
1388
|
inline: field.inline,
|
|
861
1389
|
variant: field.variant,
|
|
862
1390
|
onChange: fieldOnChange,
|
|
@@ -893,61 +1421,140 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
893
1421
|
case "repeater": {
|
|
894
1422
|
const rows = Array.isArray(fieldValue) ? fieldValue : [];
|
|
895
1423
|
const subFields = field.fields || [];
|
|
896
|
-
const minRows = field.min
|
|
897
|
-
const maxRows = field.max
|
|
898
|
-
const
|
|
899
|
-
const
|
|
1424
|
+
const minRows = typeof field.min === "number" ? field.min : 0;
|
|
1425
|
+
const maxRows = typeof field.max === "number" ? field.max : Infinity;
|
|
1426
|
+
const repeaterProps = field.repeaterProps || {};
|
|
1427
|
+
const renderAddControl = repeaterProps.renderAdd;
|
|
1428
|
+
const renderRemoveControl = repeaterProps.renderRemove;
|
|
1429
|
+
const renderMoveUpControl = repeaterProps.renderMoveUp;
|
|
1430
|
+
const renderMoveDownControl = repeaterProps.renderMoveDown;
|
|
1431
|
+
const addLabel = repeaterProps.addLabel || "Add";
|
|
1432
|
+
const removeLabel = repeaterProps.removeLabel || "Remove";
|
|
1433
|
+
const moveUpLabel = repeaterProps.moveUpLabel || "Up";
|
|
1434
|
+
const moveDownLabel = repeaterProps.moveDownLabel || "Down";
|
|
1435
|
+
const canEditRows = !isReadOnly && !isDisabled;
|
|
1436
|
+
const canAdd = rows.length < maxRows && canEditRows;
|
|
1437
|
+
const canRemove = rows.length > minRows && canEditRows;
|
|
1438
|
+
const canReorder = !!repeaterProps.reorderable && canEditRows;
|
|
1439
|
+
const repeaterHasNestedErrors = Object.keys(formErrors).some(
|
|
1440
|
+
(k) => k.startsWith(`${field.name}[`)
|
|
1441
|
+
);
|
|
1442
|
+
const firstNestedErrorKey = Object.keys(formErrors).find(
|
|
1443
|
+
(k) => k.startsWith(`${field.name}[`)
|
|
1444
|
+
);
|
|
1445
|
+
const repeaterErrorMessage = fieldError || (firstNestedErrorKey ? formErrors[firstNestedErrorKey] : null);
|
|
1446
|
+
const repeaterHasError = !!fieldError || repeaterHasNestedErrors;
|
|
900
1447
|
const addRow = () => {
|
|
901
1448
|
const emptyRow = {};
|
|
902
1449
|
for (const sf of subFields) {
|
|
903
|
-
emptyRow[sf.name] = sf.defaultValue !== void 0 ? sf.defaultValue :
|
|
1450
|
+
emptyRow[sf.name] = sf.defaultValue !== void 0 ? sf.defaultValue : getFieldEmptyValue(sf);
|
|
904
1451
|
}
|
|
905
1452
|
handleFieldChange(field.name, [...rows, emptyRow]);
|
|
906
1453
|
};
|
|
907
1454
|
const removeRow = (idx) => {
|
|
908
1455
|
handleFieldChange(field.name, rows.filter((_, i) => i !== idx));
|
|
909
1456
|
};
|
|
910
|
-
const
|
|
1457
|
+
const moveRow = (fromIndex, toIndex) => {
|
|
1458
|
+
if (toIndex < 0 || toIndex >= rows.length || toIndex === fromIndex) return;
|
|
1459
|
+
const updated = [...rows];
|
|
1460
|
+
const [moved] = updated.splice(fromIndex, 1);
|
|
1461
|
+
updated.splice(toIndex, 0, moved);
|
|
1462
|
+
handleFieldChange(field.name, updated);
|
|
1463
|
+
};
|
|
1464
|
+
const validateSubField = (rowIdx, subField, subValue, nextRows) => {
|
|
1465
|
+
const rowValues = { ...formValues, [field.name]: nextRows };
|
|
1466
|
+
const err = runValidators(subValue, subField, rowValues, fieldTypes);
|
|
1467
|
+
setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
|
|
1468
|
+
};
|
|
1469
|
+
const handleSubFieldChange = (rowIdx, subField, subValue) => {
|
|
911
1470
|
const updated = rows.map(
|
|
912
|
-
(row, i) => i ===
|
|
1471
|
+
(row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
|
|
913
1472
|
);
|
|
914
1473
|
handleFieldChange(field.name, updated);
|
|
1474
|
+
if (validateOnChange) {
|
|
1475
|
+
validateSubField(rowIdx, subField, subValue, updated);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
const handleSubFieldBlur = (rowIdx, subField, subValue) => {
|
|
1479
|
+
if (!validateOnBlur) return;
|
|
1480
|
+
const nextRows = rows.map(
|
|
1481
|
+
(row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
|
|
1482
|
+
);
|
|
1483
|
+
validateSubField(rowIdx, subField, subValue, nextRows);
|
|
915
1484
|
};
|
|
916
|
-
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { key: rowIdx, direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
|
|
1485
|
+
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { key: getRowKey(field.name, row, rowIdx), direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
|
|
917
1486
|
const sfValue = row[sf.name];
|
|
918
1487
|
const sfLabel = rowIdx === 0 ? sf.label : void 0;
|
|
919
|
-
const sfOptions = resolveOptions(sf, formValues);
|
|
1488
|
+
const sfOptions = resolveOptions(sf, { ...formValues, [field.name]: rows });
|
|
1489
|
+
const sfError = formErrors[getRepeaterErrorKey(field.name, rowIdx, sf.name)] || null;
|
|
920
1490
|
const sfProps = {
|
|
921
1491
|
name: `${field.name}-${rowIdx}-${sf.name}`,
|
|
922
1492
|
label: sfLabel,
|
|
923
1493
|
placeholder: sf.placeholder,
|
|
924
|
-
readOnly: isReadOnly,
|
|
1494
|
+
readOnly: sf.readOnly || isReadOnly,
|
|
1495
|
+
disabled: sf.disabled || isDisabled,
|
|
1496
|
+
error: !!sfError,
|
|
1497
|
+
validationMessage: sfError || void 0,
|
|
925
1498
|
...sf.fieldProps || {}
|
|
926
1499
|
};
|
|
927
1500
|
let sfElement;
|
|
928
1501
|
switch (sf.type) {
|
|
929
1502
|
case "select":
|
|
930
|
-
sfElement = /* @__PURE__ */ import_react.default.createElement(
|
|
1503
|
+
sfElement = /* @__PURE__ */ import_react.default.createElement(
|
|
1504
|
+
import_ui_extensions.Select,
|
|
1505
|
+
{
|
|
1506
|
+
...sfProps,
|
|
1507
|
+
value: sfValue,
|
|
1508
|
+
options: sfOptions,
|
|
1509
|
+
onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
|
|
1510
|
+
onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
|
|
1511
|
+
}
|
|
1512
|
+
);
|
|
931
1513
|
break;
|
|
932
1514
|
case "number":
|
|
933
|
-
sfElement = /* @__PURE__ */ import_react.default.createElement(
|
|
1515
|
+
sfElement = /* @__PURE__ */ import_react.default.createElement(
|
|
1516
|
+
import_ui_extensions.NumberInput,
|
|
1517
|
+
{
|
|
1518
|
+
...sfProps,
|
|
1519
|
+
value: sfValue,
|
|
1520
|
+
onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
|
|
1521
|
+
onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
|
|
1522
|
+
}
|
|
1523
|
+
);
|
|
934
1524
|
break;
|
|
935
1525
|
case "checkbox":
|
|
936
|
-
sfElement = /* @__PURE__ */ import_react.default.createElement(
|
|
1526
|
+
sfElement = /* @__PURE__ */ import_react.default.createElement(
|
|
1527
|
+
import_ui_extensions.Checkbox,
|
|
1528
|
+
{
|
|
1529
|
+
...sfProps,
|
|
1530
|
+
checked: !!sfValue,
|
|
1531
|
+
onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
|
|
1532
|
+
onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
|
|
1533
|
+
},
|
|
1534
|
+
sf.label
|
|
1535
|
+
);
|
|
937
1536
|
break;
|
|
938
1537
|
default:
|
|
939
|
-
sfElement = /* @__PURE__ */ import_react.default.createElement(
|
|
1538
|
+
sfElement = /* @__PURE__ */ import_react.default.createElement(
|
|
1539
|
+
import_ui_extensions.Input,
|
|
1540
|
+
{
|
|
1541
|
+
...sfProps,
|
|
1542
|
+
value: sfValue || "",
|
|
1543
|
+
onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
|
|
1544
|
+
onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
|
|
1545
|
+
}
|
|
1546
|
+
);
|
|
940
1547
|
}
|
|
941
1548
|
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { key: sf.name, flex: 1 }, sfElement);
|
|
942
|
-
}), canRemove && /* @__PURE__ */ import_react.default.createElement(
|
|
1549
|
+
}), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Inline, { gap: "xs" }, canReorder && rowIdx > 0 && (renderMoveUpControl ? renderMoveUpControl({ index: rowIdx, onClick: () => moveRow(rowIdx, rowIdx - 1) }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", size: "sm", onClick: () => moveRow(rowIdx, rowIdx - 1) }, moveUpLabel)), canReorder && rowIdx < rows.length - 1 && (renderMoveDownControl ? renderMoveDownControl({ index: rowIdx, onClick: () => moveRow(rowIdx, rowIdx + 1) }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", size: "sm", onClick: () => moveRow(rowIdx, rowIdx + 1) }, moveDownLabel)), canRemove && (renderRemoveControl ? renderRemoveControl({ index: rowIdx, onClick: () => removeRow(rowIdx) }) : /* @__PURE__ */ import_react.default.createElement(
|
|
943
1550
|
import_ui_extensions.Button,
|
|
944
1551
|
{
|
|
945
1552
|
variant: "secondary",
|
|
946
|
-
size: "
|
|
1553
|
+
size: "md",
|
|
947
1554
|
onClick: () => removeRow(rowIdx)
|
|
948
1555
|
},
|
|
949
|
-
|
|
950
|
-
))), canAdd && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.
|
|
1556
|
+
removeLabel
|
|
1557
|
+
))))), canAdd && (renderAddControl ? renderAddControl({ onClick: addRow, count: rows.length }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { onClick: addRow }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "flush" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "add" }), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, addLabel)))), repeaterHasError && repeaterErrorMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, repeaterErrorMessage));
|
|
951
1558
|
}
|
|
952
1559
|
default:
|
|
953
1560
|
return /* @__PURE__ */ import_react.default.createElement(
|
|
@@ -967,15 +1574,17 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
967
1574
|
if (field.width === "full" && columns > 1) return columns;
|
|
968
1575
|
return 1;
|
|
969
1576
|
};
|
|
970
|
-
const getDependents = (parentField) => visibleFields.filter(
|
|
971
|
-
|
|
1577
|
+
const getDependents = (parentField) => visibleFields.filter(
|
|
1578
|
+
(f) => getDependsOnName(f) === parentField.name && f.name !== parentField.name && getDependsOnDisplay(f) === "grouped"
|
|
1579
|
+
);
|
|
1580
|
+
const isDependent = (field) => getDependsOnName(field) && getDependsOnDisplay(field) === "grouped" && visibleFields.some((f) => f.name === getDependsOnName(field) && f.name !== field.name);
|
|
972
1581
|
const renderDependentGroup = (parentField, dependents) => {
|
|
973
|
-
const firstWithLabel = dependents.find((f) => f
|
|
974
|
-
const firstWithMessage = dependents.find((f) => f
|
|
975
|
-
const groupLabel = firstWithLabel
|
|
976
|
-
const rawMessage = firstWithMessage
|
|
1582
|
+
const firstWithLabel = dependents.find((f) => getDependsOnLabel(f)) || dependents[0];
|
|
1583
|
+
const firstWithMessage = dependents.find((f) => getDependsOnMessage(f)) || dependents[0];
|
|
1584
|
+
const groupLabel = getDependsOnLabel(firstWithLabel) || "Dependent properties";
|
|
1585
|
+
const rawMessage = getDependsOnMessage(firstWithMessage);
|
|
977
1586
|
const tooltipMessage = typeof rawMessage === "function" ? rawMessage(parentField.label) : rawMessage || "";
|
|
978
|
-
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { inline: true, variant: "dark", overlay: /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tooltip, null, tooltipMessage) }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "info" })))), dependents
|
|
1587
|
+
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { inline: true, variant: "dark", overlay: /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tooltip, null, tooltipMessage) }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "info" })))), renderFieldSubset(dependents)));
|
|
979
1588
|
};
|
|
980
1589
|
const renderGridLayout = (fieldSubset) => {
|
|
981
1590
|
const fieldList = fieldSubset || visibleFields;
|
|
@@ -1068,7 +1677,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
1068
1677
|
let i = 0;
|
|
1069
1678
|
while (i < fieldList.length) {
|
|
1070
1679
|
const field = fieldList[i];
|
|
1071
|
-
if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !field
|
|
1680
|
+
if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !getDependsOnName(field)) {
|
|
1072
1681
|
rows.push({ type: "pair", fields: [fieldList[i], fieldList[i + 1]] });
|
|
1073
1682
|
i += 2;
|
|
1074
1683
|
} else {
|
|
@@ -1104,9 +1713,12 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
1104
1713
|
let batch = [];
|
|
1105
1714
|
const flushBatch = () => {
|
|
1106
1715
|
if (batch.length === 0) return;
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1716
|
+
const chunks = maxColumns ? Array.from({ length: Math.ceil(batch.length / maxColumns) }, (_, i) => batch.slice(i * maxColumns, i * maxColumns + maxColumns)) : [batch];
|
|
1717
|
+
for (const chunk of chunks) {
|
|
1718
|
+
elements.push(
|
|
1719
|
+
/* @__PURE__ */ import_react.default.createElement(import_ui_extensions.AutoGrid, { key: `ag-${chunk[0].name}`, columnWidth, flexible: true, gap }, chunk.map((f) => /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, { key: f.name }, renderField(f))))
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1110
1722
|
batch = [];
|
|
1111
1723
|
};
|
|
1112
1724
|
for (const field of fieldList) {
|
|
@@ -1189,7 +1801,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
1189
1801
|
);
|
|
1190
1802
|
if (sec.info) {
|
|
1191
1803
|
elements.push(
|
|
1192
|
-
/* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { key: sec.id, direction: "row", align: "start", gap: "flush" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1 }, accordion), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { overlay: /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tooltip, null, sec.info) }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "info", size: "sm", screenReaderText: sec.info })))
|
|
1804
|
+
/* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { key: sec.id, direction: "row", align: "start", justify: "start", gap: "flush" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1 }, accordion), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", overlay: /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tooltip, null, sec.info) }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "info", size: "sm", screenReaderText: sec.info })))
|
|
1193
1805
|
);
|
|
1194
1806
|
} else {
|
|
1195
1807
|
elements.push(accordion);
|
|
@@ -1217,8 +1829,30 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
1217
1829
|
if (submitPosition === "none" || formReadOnly) return null;
|
|
1218
1830
|
const isLastStep = !isMultiStep || currentStep === steps.length - 1;
|
|
1219
1831
|
const isFirstStep = !isMultiStep || currentStep === 0;
|
|
1832
|
+
const buttonContext = {
|
|
1833
|
+
isMultiStep,
|
|
1834
|
+
isFirstStep,
|
|
1835
|
+
isLastStep,
|
|
1836
|
+
currentStep,
|
|
1837
|
+
totalSteps: isMultiStep ? steps.length : 1,
|
|
1838
|
+
disabled,
|
|
1839
|
+
loading: isLoading,
|
|
1840
|
+
labels: {
|
|
1841
|
+
submit: submitButtonLabel,
|
|
1842
|
+
cancel: cancelButtonLabel,
|
|
1843
|
+
back: backButtonLabel,
|
|
1844
|
+
next: nextButtonLabel
|
|
1845
|
+
},
|
|
1846
|
+
onBack: handleBack,
|
|
1847
|
+
onNext: handleNext,
|
|
1848
|
+
onCancel,
|
|
1849
|
+
onSubmit: handleSubmit
|
|
1850
|
+
};
|
|
1851
|
+
if (renderButtonsProp) {
|
|
1852
|
+
return renderButtonsProp(buttonContext);
|
|
1853
|
+
}
|
|
1220
1854
|
if (isMultiStep) {
|
|
1221
|
-
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: handleBack, disabled },
|
|
1855
|
+
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: handleBack, disabled }, backButtonLabel) : showCancel ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, " "), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Inline, { gap: "small" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy" }, "Step ", currentStep + 1, " of ", steps.length), isLastStep ? /* @__PURE__ */ import_react.default.createElement(
|
|
1222
1856
|
import_ui_extensions.LoadingButton,
|
|
1223
1857
|
{
|
|
1224
1858
|
variant: submitVariant,
|
|
@@ -1226,10 +1860,10 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
1226
1860
|
onClick: handleSubmit,
|
|
1227
1861
|
disabled
|
|
1228
1862
|
},
|
|
1229
|
-
|
|
1230
|
-
) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "primary", onClick: handleNext, disabled },
|
|
1863
|
+
submitButtonLabel
|
|
1864
|
+
) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
|
|
1231
1865
|
}
|
|
1232
|
-
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled },
|
|
1866
|
+
return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ import_react.default.createElement(
|
|
1233
1867
|
import_ui_extensions.LoadingButton,
|
|
1234
1868
|
{
|
|
1235
1869
|
variant: submitVariant,
|
|
@@ -1238,7 +1872,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
1238
1872
|
onClick: noFormWrapper ? handleSubmit : void 0,
|
|
1239
1873
|
disabled
|
|
1240
1874
|
},
|
|
1241
|
-
|
|
1875
|
+
submitButtonLabel
|
|
1242
1876
|
));
|
|
1243
1877
|
};
|
|
1244
1878
|
const formContent = /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap }, isMultiStep && showStepIndicator && /* @__PURE__ */ import_react.default.createElement(
|
|
@@ -1247,7 +1881,7 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
1247
1881
|
currentStep,
|
|
1248
1882
|
stepNames: steps.map((s) => s.title)
|
|
1249
1883
|
}
|
|
1250
|
-
), formReadOnly && readOnlyMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title:
|
|
1884
|
+
), formReadOnly && readOnlyMessage && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: readOnlyTitle, variant: "warning" }, readOnlyMessage), !addAlert && formError && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: errorTitle, variant: "danger" }, typeof formError === "string" ? formError : void 0), !addAlert && formSuccess && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Alert, { title: successTitle, variant: "success" }, formSuccess), isMultiStep && steps[currentStep] && steps[currentStep].render ? steps[currentStep].render({
|
|
1251
1885
|
values: formValues,
|
|
1252
1886
|
goNext: handleNext,
|
|
1253
1887
|
goBack: handleBack,
|
|
@@ -1259,7 +1893,15 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
1259
1893
|
if (noFormWrapper) {
|
|
1260
1894
|
return formContent;
|
|
1261
1895
|
}
|
|
1262
|
-
return /* @__PURE__ */ import_react.default.createElement(
|
|
1896
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
1897
|
+
import_ui_extensions.Form,
|
|
1898
|
+
{
|
|
1899
|
+
...formProps || {},
|
|
1900
|
+
onSubmit: handleSubmit,
|
|
1901
|
+
autoComplete
|
|
1902
|
+
},
|
|
1903
|
+
formContent
|
|
1904
|
+
);
|
|
1263
1905
|
});
|
|
1264
1906
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1265
1907
|
0 && (module.exports = {
|