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