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/index.mjs
CHANGED
|
@@ -1108,7 +1108,132 @@ var isValueEmpty = (value, field) => {
|
|
|
1108
1108
|
if ((field.type === "toggle" || field.type === "checkbox") && value === false) return true;
|
|
1109
1109
|
return false;
|
|
1110
1110
|
};
|
|
1111
|
-
var
|
|
1111
|
+
var isPromise = (value) => value && typeof value.then === "function";
|
|
1112
|
+
var isAsyncFunction = (fn) => fn && fn.constructor && fn.constructor.name === "AsyncFunction";
|
|
1113
|
+
var normalizeValidatorResult = (result) => {
|
|
1114
|
+
if (result === true || result === void 0 || result === null || result === false) return null;
|
|
1115
|
+
return String(result);
|
|
1116
|
+
};
|
|
1117
|
+
var isDateValueObject = (value) => isPlainObject(value) && Number.isInteger(value.year) && Number.isInteger(value.month) && Number.isInteger(value.date);
|
|
1118
|
+
var isTimeValueObject = (value) => isPlainObject(value) && Number.isInteger(value.hours) && Number.isInteger(value.minutes);
|
|
1119
|
+
var compareDateValues = (a, b) => {
|
|
1120
|
+
if (a.year !== b.year) return a.year - b.year;
|
|
1121
|
+
if (a.month !== b.month) return a.month - b.month;
|
|
1122
|
+
return a.date - b.date;
|
|
1123
|
+
};
|
|
1124
|
+
var compareTimeValues = (a, b) => {
|
|
1125
|
+
if (a.hours !== b.hours) return a.hours - b.hours;
|
|
1126
|
+
return a.minutes - b.minutes;
|
|
1127
|
+
};
|
|
1128
|
+
var runDefaultFieldValidator = (value, field, allValues) => {
|
|
1129
|
+
const errorPrefix = field.label || field.name;
|
|
1130
|
+
switch (field.type) {
|
|
1131
|
+
case "text":
|
|
1132
|
+
case "password":
|
|
1133
|
+
case "textarea":
|
|
1134
|
+
if (typeof value !== "string") return `${errorPrefix} must be text`;
|
|
1135
|
+
break;
|
|
1136
|
+
case "number":
|
|
1137
|
+
case "stepper":
|
|
1138
|
+
case "currency":
|
|
1139
|
+
if (typeof value !== "number" || Number.isNaN(value)) return `${errorPrefix} must be a number`;
|
|
1140
|
+
break;
|
|
1141
|
+
case "toggle":
|
|
1142
|
+
case "checkbox":
|
|
1143
|
+
if (typeof value !== "boolean") return `${errorPrefix} must be true or false`;
|
|
1144
|
+
break;
|
|
1145
|
+
case "select":
|
|
1146
|
+
case "radioGroup": {
|
|
1147
|
+
const options = resolveOptions(field, allValues);
|
|
1148
|
+
if (options.length > 0 && !options.some((o) => Object.is(o.value, value))) {
|
|
1149
|
+
return `${errorPrefix} has an invalid selection`;
|
|
1150
|
+
}
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
case "multiselect":
|
|
1154
|
+
case "checkboxGroup": {
|
|
1155
|
+
if (!Array.isArray(value)) return `${errorPrefix} must be a list`;
|
|
1156
|
+
const options = resolveOptions(field, allValues);
|
|
1157
|
+
if (options.length > 0) {
|
|
1158
|
+
const validValues = new Set(options.map((o) => o.value));
|
|
1159
|
+
const hasInvalid = value.some((item) => !validValues.has(item));
|
|
1160
|
+
if (hasInvalid) return `${errorPrefix} has an invalid selection`;
|
|
1161
|
+
}
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
case "date":
|
|
1165
|
+
if (!isDateValueObject(value)) return `${errorPrefix} has an invalid date`;
|
|
1166
|
+
break;
|
|
1167
|
+
case "time":
|
|
1168
|
+
if (!isTimeValueObject(value)) return `${errorPrefix} has an invalid time`;
|
|
1169
|
+
break;
|
|
1170
|
+
case "datetime": {
|
|
1171
|
+
if (isDateValueObject(value)) break;
|
|
1172
|
+
if (!isPlainObject(value)) return `${errorPrefix} has an invalid date/time`;
|
|
1173
|
+
const hasDate = value.date !== void 0;
|
|
1174
|
+
const hasTime = value.time !== void 0;
|
|
1175
|
+
if (!hasDate && !hasTime) return `${errorPrefix} has an invalid date/time`;
|
|
1176
|
+
if (hasDate && !isDateValueObject(value.date)) return `${errorPrefix} has an invalid date`;
|
|
1177
|
+
if (hasTime && !isTimeValueObject(value.time)) return `${errorPrefix} has an invalid time`;
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
case "repeater":
|
|
1181
|
+
if (!Array.isArray(value)) return `${errorPrefix} has invalid rows`;
|
|
1182
|
+
break;
|
|
1183
|
+
default:
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
};
|
|
1188
|
+
var runCustomSyncValidators = (value, field, allValues) => {
|
|
1189
|
+
const validators = Array.isArray(field.validators) ? field.validators : [];
|
|
1190
|
+
for (const validator of validators) {
|
|
1191
|
+
if (isAsyncFunction(validator)) continue;
|
|
1192
|
+
try {
|
|
1193
|
+
const result = validator(value, allValues);
|
|
1194
|
+
if (isPromise(result)) continue;
|
|
1195
|
+
const err = normalizeValidatorResult(result);
|
|
1196
|
+
if (err) return err;
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
return (err == null ? void 0 : err.message) || "Validation failed";
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (field.validate && !isAsyncFunction(field.validate)) {
|
|
1202
|
+
try {
|
|
1203
|
+
const result = field.validate(value, allValues);
|
|
1204
|
+
if (!isPromise(result)) {
|
|
1205
|
+
const err = normalizeValidatorResult(result);
|
|
1206
|
+
if (err) return err;
|
|
1207
|
+
}
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
return (err == null ? void 0 : err.message) || "Validation failed";
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return null;
|
|
1213
|
+
};
|
|
1214
|
+
var collectAsyncValidatorPromises = (value, field, allValues, context) => {
|
|
1215
|
+
const promises = [];
|
|
1216
|
+
const validators = Array.isArray(field.validators) ? field.validators : [];
|
|
1217
|
+
for (const validator of validators) {
|
|
1218
|
+
try {
|
|
1219
|
+
const result = validator(value, allValues, context);
|
|
1220
|
+
if (isPromise(result)) promises.push(result);
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
promises.push(Promise.reject(err));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (field.validate) {
|
|
1226
|
+
try {
|
|
1227
|
+
const result = field.validate(value, allValues, context);
|
|
1228
|
+
if (isPromise(result)) promises.push(result);
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
promises.push(Promise.reject(err));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return promises;
|
|
1234
|
+
};
|
|
1235
|
+
var runValidators = (value, field, allValues, fieldTypes, options = {}) => {
|
|
1236
|
+
const includeCustomValidators = options.includeCustomValidators !== false;
|
|
1112
1237
|
if (field.type === "display" || field.type === "crmPropertyList" || field.type === "crmAssociationPropertyList") return null;
|
|
1113
1238
|
const isRequired = resolveRequired(field, allValues);
|
|
1114
1239
|
const plugin = fieldTypes && fieldTypes[field.type];
|
|
@@ -1117,6 +1242,10 @@ var runValidators = (value, field, allValues, fieldTypes) => {
|
|
|
1117
1242
|
return `${field.label} is required`;
|
|
1118
1243
|
}
|
|
1119
1244
|
if (empty) return null;
|
|
1245
|
+
if (field.useDefaultValidators !== false) {
|
|
1246
|
+
const typeError = runDefaultFieldValidator(value, field, allValues);
|
|
1247
|
+
if (typeError) return typeError;
|
|
1248
|
+
}
|
|
1120
1249
|
if (field.pattern && typeof value === "string") {
|
|
1121
1250
|
if (!field.pattern.test(value)) {
|
|
1122
1251
|
return field.patternMessage || "Invalid format";
|
|
@@ -1138,10 +1267,25 @@ var runValidators = (value, field, allValues, fieldTypes) => {
|
|
|
1138
1267
|
return `Must be no more than ${field.max}`;
|
|
1139
1268
|
}
|
|
1140
1269
|
}
|
|
1141
|
-
if (field.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1270
|
+
if (field.type === "date" && isDateValueObject(value)) {
|
|
1271
|
+
if (field.min && isDateValueObject(field.min) && compareDateValues(value, field.min) < 0) {
|
|
1272
|
+
return field.minValidationMessage || "Date is too early";
|
|
1273
|
+
}
|
|
1274
|
+
if (field.max && isDateValueObject(field.max) && compareDateValues(value, field.max) > 0) {
|
|
1275
|
+
return field.maxValidationMessage || "Date is too late";
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (field.type === "time" && isTimeValueObject(value)) {
|
|
1279
|
+
if (field.min && isTimeValueObject(field.min) && compareTimeValues(value, field.min) < 0) {
|
|
1280
|
+
return field.minValidationMessage || "Time is too early";
|
|
1281
|
+
}
|
|
1282
|
+
if (field.max && isTimeValueObject(field.max) && compareTimeValues(value, field.max) > 0) {
|
|
1283
|
+
return field.maxValidationMessage || "Time is too late";
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (includeCustomValidators) {
|
|
1287
|
+
const customError = runCustomSyncValidators(value, field, allValues);
|
|
1288
|
+
if (customError) return customError;
|
|
1145
1289
|
}
|
|
1146
1290
|
return null;
|
|
1147
1291
|
};
|
|
@@ -1153,6 +1297,58 @@ var resolveOptions = (field, allValues) => {
|
|
|
1153
1297
|
if (typeof field.options === "function") return field.options(allValues);
|
|
1154
1298
|
return field.options || [];
|
|
1155
1299
|
};
|
|
1300
|
+
var getDependsOnName = (field) => field.dependsOnConfig && field.dependsOnConfig.field;
|
|
1301
|
+
var getDependsOnDisplay = (field) => field.dependsOnConfig && field.dependsOnConfig.display || "grouped";
|
|
1302
|
+
var getDependsOnLabel = (field) => field.dependsOnConfig && field.dependsOnConfig.label;
|
|
1303
|
+
var getDependsOnMessage = (field) => field.dependsOnConfig && field.dependsOnConfig.message;
|
|
1304
|
+
var getRepeaterErrorKey = (fieldName, rowIdx, subFieldName) => `${fieldName}[${rowIdx}].${subFieldName}`;
|
|
1305
|
+
var isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
|
|
1306
|
+
var deepEqual = (a, b) => {
|
|
1307
|
+
if (Object.is(a, b)) return true;
|
|
1308
|
+
if (typeof a !== typeof b) return false;
|
|
1309
|
+
if (a == null || b == null) return false;
|
|
1310
|
+
if (Array.isArray(a)) {
|
|
1311
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
1312
|
+
for (let i = 0; i < a.length; i++) {
|
|
1313
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
1314
|
+
}
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
if (a instanceof Date && b instanceof Date) {
|
|
1318
|
+
return a.getTime() === b.getTime();
|
|
1319
|
+
}
|
|
1320
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
1321
|
+
const aKeys = Object.keys(a);
|
|
1322
|
+
const bKeys = Object.keys(b);
|
|
1323
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
1324
|
+
for (const key of aKeys) {
|
|
1325
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
1326
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
1327
|
+
}
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
return false;
|
|
1331
|
+
};
|
|
1332
|
+
var fieldSetHasErrors = (errors, fields) => {
|
|
1333
|
+
if (!errors || !fields || fields.length === 0) return false;
|
|
1334
|
+
const names = new Set(fields.map((field) => field.name));
|
|
1335
|
+
return Object.keys(errors).some((errorKey) => {
|
|
1336
|
+
const base = errorKey.split("[")[0];
|
|
1337
|
+
return names.has(base);
|
|
1338
|
+
});
|
|
1339
|
+
};
|
|
1340
|
+
var deepClone = (value) => {
|
|
1341
|
+
if (Array.isArray(value)) return value.map(deepClone);
|
|
1342
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
1343
|
+
if (isPlainObject(value)) {
|
|
1344
|
+
const next = {};
|
|
1345
|
+
for (const key of Object.keys(value)) {
|
|
1346
|
+
next[key] = deepClone(value[key]);
|
|
1347
|
+
}
|
|
1348
|
+
return next;
|
|
1349
|
+
}
|
|
1350
|
+
return value;
|
|
1351
|
+
};
|
|
1156
1352
|
var useFormPrefill = (properties, mapping) => {
|
|
1157
1353
|
return useMemo2(() => {
|
|
1158
1354
|
if (!properties) return {};
|
|
@@ -1222,28 +1418,30 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1222
1418
|
// validate current step fields before Next
|
|
1223
1419
|
} = props;
|
|
1224
1420
|
const {
|
|
1225
|
-
|
|
1226
|
-
// submit
|
|
1421
|
+
labels,
|
|
1422
|
+
// { submit, cancel, back, next } — i18n label object
|
|
1227
1423
|
submitVariant = "primary",
|
|
1228
1424
|
// submit button variant
|
|
1229
1425
|
showCancel = false,
|
|
1230
1426
|
// show cancel button
|
|
1231
|
-
cancelLabel = "Cancel",
|
|
1232
|
-
// cancel button text
|
|
1233
1427
|
onCancel,
|
|
1234
1428
|
// () => void
|
|
1235
1429
|
submitPosition = "bottom",
|
|
1236
1430
|
// "bottom" | "none"
|
|
1237
1431
|
loading: controlledLoading,
|
|
1238
1432
|
// controlled loading state
|
|
1239
|
-
disabled = false
|
|
1433
|
+
disabled = false,
|
|
1240
1434
|
// disable entire form
|
|
1435
|
+
renderButtons: renderButtonsProp
|
|
1436
|
+
// custom action row renderer
|
|
1241
1437
|
} = props;
|
|
1242
1438
|
const {
|
|
1243
1439
|
columns = 1,
|
|
1244
1440
|
// number of grid columns (1 = full-width stack)
|
|
1245
1441
|
columnWidth,
|
|
1246
1442
|
// AutoGrid columnWidth — responsive layout (overrides columns)
|
|
1443
|
+
maxColumns,
|
|
1444
|
+
// cap number of columns per row in AutoGrid mode
|
|
1247
1445
|
layout,
|
|
1248
1446
|
// explicit row layout array (overrides columns + columnWidth)
|
|
1249
1447
|
sections,
|
|
@@ -1254,6 +1452,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1254
1452
|
// show * on required fields
|
|
1255
1453
|
noFormWrapper = false,
|
|
1256
1454
|
// skip HubSpot <Form> wrapper
|
|
1455
|
+
autoComplete,
|
|
1456
|
+
// form autoComplete attribute
|
|
1457
|
+
formProps,
|
|
1458
|
+
// pass-through props for Form wrapper
|
|
1257
1459
|
fieldTypes
|
|
1258
1460
|
// Record<string, FieldTypePlugin> — custom field type registry
|
|
1259
1461
|
} = props;
|
|
@@ -1264,8 +1466,12 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1264
1466
|
// string — form-level success alert
|
|
1265
1467
|
readOnly: formReadOnly = false,
|
|
1266
1468
|
// boolean — lock all fields
|
|
1267
|
-
readOnlyMessage
|
|
1469
|
+
readOnlyMessage,
|
|
1268
1470
|
// string — warning alert when readOnly
|
|
1471
|
+
alerts,
|
|
1472
|
+
// { addAlert, readOnlyTitle, errorTitle, successTitle }
|
|
1473
|
+
errors: controlledErrors
|
|
1474
|
+
// controlled validation errors
|
|
1269
1475
|
} = props;
|
|
1270
1476
|
const {
|
|
1271
1477
|
onDirtyChange,
|
|
@@ -1273,6 +1479,38 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1273
1479
|
autoSave
|
|
1274
1480
|
// { debounce: number, onAutoSave: (values) => void }
|
|
1275
1481
|
} = props;
|
|
1482
|
+
const submitButtonLabel = (labels == null ? void 0 : labels.submit) || "Submit";
|
|
1483
|
+
const cancelButtonLabel = (labels == null ? void 0 : labels.cancel) || "Cancel";
|
|
1484
|
+
const backButtonLabel = (labels == null ? void 0 : labels.back) || "Back";
|
|
1485
|
+
const nextButtonLabel = (labels == null ? void 0 : labels.next) || "Next";
|
|
1486
|
+
const addAlert = alerts == null ? void 0 : alerts.addAlert;
|
|
1487
|
+
const readOnlyTitle = (alerts == null ? void 0 : alerts.readOnlyTitle) || "Read Only";
|
|
1488
|
+
const errorTitle = (alerts == null ? void 0 : alerts.errorTitle) || "Error";
|
|
1489
|
+
const successTitle = (alerts == null ? void 0 : alerts.successTitle) || "Success";
|
|
1490
|
+
const prevErrorRef = useRef2(formError);
|
|
1491
|
+
const prevSuccessRef = useRef2(formSuccess);
|
|
1492
|
+
useEffect2(() => {
|
|
1493
|
+
if (!addAlert) return;
|
|
1494
|
+
if (formError && formError !== prevErrorRef.current) {
|
|
1495
|
+
addAlert({
|
|
1496
|
+
type: "danger",
|
|
1497
|
+
title: errorTitle,
|
|
1498
|
+
message: typeof formError === "string" ? formError : void 0
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
prevErrorRef.current = formError;
|
|
1502
|
+
}, [addAlert, formError, errorTitle]);
|
|
1503
|
+
useEffect2(() => {
|
|
1504
|
+
if (!addAlert) return;
|
|
1505
|
+
if (formSuccess && formSuccess !== prevSuccessRef.current) {
|
|
1506
|
+
addAlert({
|
|
1507
|
+
type: "success",
|
|
1508
|
+
title: successTitle,
|
|
1509
|
+
message: formSuccess
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
prevSuccessRef.current = formSuccess;
|
|
1513
|
+
}, [addAlert, formSuccess, successTitle]);
|
|
1276
1514
|
const computeInitialValues = () => {
|
|
1277
1515
|
const vals = {};
|
|
1278
1516
|
for (const field of fields) {
|
|
@@ -1288,25 +1526,99 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1288
1526
|
const [internalErrors, setInternalErrors] = useState2({});
|
|
1289
1527
|
const [internalStep, setInternalStep] = useState2(0);
|
|
1290
1528
|
const [internalLoading, setInternalLoading] = useState2(false);
|
|
1291
|
-
const [touchedFields, setTouchedFields] = useState2({});
|
|
1292
1529
|
const [validatingFields, setValidatingFields] = useState2({});
|
|
1293
1530
|
const asyncValidationRef = useRef2(/* @__PURE__ */ new Map());
|
|
1531
|
+
const asyncAbortRef = useRef2(/* @__PURE__ */ new Map());
|
|
1532
|
+
const asyncValidationVersionRef = useRef2(/* @__PURE__ */ new Map());
|
|
1294
1533
|
const debounceTimersRef = useRef2(/* @__PURE__ */ new Map());
|
|
1534
|
+
const inputDebounceRef = useRef2(/* @__PURE__ */ new Map());
|
|
1535
|
+
const rowKeyRef = useRef2(/* @__PURE__ */ new WeakMap());
|
|
1536
|
+
const rowKeyCounterRef = useRef2(0);
|
|
1295
1537
|
const initialSnapshot = useRef2(null);
|
|
1296
1538
|
if (initialSnapshot.current === null) {
|
|
1297
|
-
initialSnapshot.current =
|
|
1539
|
+
initialSnapshot.current = deepClone(computeInitialValues());
|
|
1298
1540
|
}
|
|
1299
1541
|
const formValues = values != null ? values : internalValues;
|
|
1542
|
+
const formErrors = controlledErrors != null ? controlledErrors : internalErrors;
|
|
1300
1543
|
const currentStep = controlledStep != null ? controlledStep : internalStep;
|
|
1301
1544
|
const isLoading = controlledLoading != null ? controlledLoading : internalLoading;
|
|
1302
1545
|
const isMultiStep = Array.isArray(steps) && steps.length > 0;
|
|
1546
|
+
const formValuesRef = useRef2(formValues);
|
|
1547
|
+
const formErrorsRef = useRef2(formErrors);
|
|
1548
|
+
const draftValuesRef = useRef2(null);
|
|
1549
|
+
formValuesRef.current = formValues;
|
|
1550
|
+
formErrorsRef.current = formErrors;
|
|
1551
|
+
const fieldByName = useMemo2(() => {
|
|
1552
|
+
const map = /* @__PURE__ */ new Map();
|
|
1553
|
+
for (const field of fields) map.set(field.name, field);
|
|
1554
|
+
return map;
|
|
1555
|
+
}, [fields]);
|
|
1556
|
+
const isDev = typeof process === "undefined" || !process.env || process.env.NODE_ENV !== "production";
|
|
1557
|
+
const configWarningsRef = useRef2(/* @__PURE__ */ new Set());
|
|
1558
|
+
const warnConfig = useCallback2((message) => {
|
|
1559
|
+
if (!isDev) return;
|
|
1560
|
+
if (configWarningsRef.current.has(message)) return;
|
|
1561
|
+
configWarningsRef.current.add(message);
|
|
1562
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
1563
|
+
console.warn(`[FormBuilder] ${message}`);
|
|
1564
|
+
}
|
|
1565
|
+
}, [isDev]);
|
|
1566
|
+
const replaceErrors = useCallback2(
|
|
1567
|
+
(nextErrors) => {
|
|
1568
|
+
if (controlledErrors == null) setInternalErrors(nextErrors);
|
|
1569
|
+
if (onValidationChange) onValidationChange(nextErrors);
|
|
1570
|
+
},
|
|
1571
|
+
[controlledErrors, onValidationChange]
|
|
1572
|
+
);
|
|
1573
|
+
const updateErrors = useCallback2(
|
|
1574
|
+
(newErrors) => {
|
|
1575
|
+
const mergeErrors = (base) => {
|
|
1576
|
+
const merged = { ...base, ...newErrors };
|
|
1577
|
+
for (const key of Object.keys(newErrors)) {
|
|
1578
|
+
if (newErrors[key] === null || newErrors[key] === void 0) {
|
|
1579
|
+
delete merged[key];
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return merged;
|
|
1583
|
+
};
|
|
1584
|
+
if (controlledErrors != null) {
|
|
1585
|
+
const merged = mergeErrors(formErrorsRef.current || {});
|
|
1586
|
+
if (onValidationChange) onValidationChange(merged);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
setInternalErrors((prev) => {
|
|
1590
|
+
const merged = mergeErrors(prev);
|
|
1591
|
+
if (onValidationChange) onValidationChange(merged);
|
|
1592
|
+
return merged;
|
|
1593
|
+
});
|
|
1594
|
+
},
|
|
1595
|
+
[controlledErrors, onValidationChange]
|
|
1596
|
+
);
|
|
1597
|
+
const getFieldEmptyValue = useCallback2(
|
|
1598
|
+
(field) => {
|
|
1599
|
+
const plugin = fieldTypes && fieldTypes[field.type];
|
|
1600
|
+
return plugin && plugin.getEmptyValue ? plugin.getEmptyValue() : getEmptyValue(field);
|
|
1601
|
+
},
|
|
1602
|
+
[fieldTypes]
|
|
1603
|
+
);
|
|
1604
|
+
const getRowKey = useCallback2((fieldName, row, index) => {
|
|
1605
|
+
if (!row || typeof row !== "object") return `${fieldName}-idx-${index}`;
|
|
1606
|
+
if (!rowKeyRef.current.has(row)) {
|
|
1607
|
+
rowKeyCounterRef.current += 1;
|
|
1608
|
+
rowKeyRef.current.set(row, `${fieldName}-row-${rowKeyCounterRef.current}`);
|
|
1609
|
+
}
|
|
1610
|
+
return rowKeyRef.current.get(row);
|
|
1611
|
+
}, []);
|
|
1303
1612
|
useEffect2(() => {
|
|
1304
1613
|
return () => {
|
|
1305
1614
|
for (const timer of debounceTimersRef.current.values()) clearTimeout(timer);
|
|
1615
|
+
for (const timer of inputDebounceRef.current.values()) clearTimeout(timer);
|
|
1616
|
+
for (const controller of asyncAbortRef.current.values()) controller.abort();
|
|
1617
|
+
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
|
1306
1618
|
};
|
|
1307
1619
|
}, []);
|
|
1308
1620
|
const isDirty = useMemo2(() => {
|
|
1309
|
-
return
|
|
1621
|
+
return !deepEqual(formValues, initialSnapshot.current);
|
|
1310
1622
|
}, [formValues]);
|
|
1311
1623
|
const prevDirtyRef = useRef2(false);
|
|
1312
1624
|
useEffect2(() => {
|
|
@@ -1316,36 +1628,129 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1316
1628
|
}
|
|
1317
1629
|
}, [isDirty, onDirtyChange]);
|
|
1318
1630
|
const autoSaveTimerRef = useRef2(null);
|
|
1631
|
+
const autoSaveRef = useRef2(autoSave);
|
|
1632
|
+
autoSaveRef.current = autoSave;
|
|
1633
|
+
const prevAutoSaveValues = useRef2(deepClone(formValues));
|
|
1319
1634
|
useEffect2(() => {
|
|
1320
|
-
|
|
1635
|
+
const cfg = autoSaveRef.current;
|
|
1636
|
+
if (!cfg || !cfg.onAutoSave || !isDirty) return;
|
|
1637
|
+
if (deepEqual(prevAutoSaveValues.current, formValues)) return;
|
|
1638
|
+
prevAutoSaveValues.current = deepClone(formValues);
|
|
1321
1639
|
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
|
1322
1640
|
autoSaveTimerRef.current = setTimeout(() => {
|
|
1323
1641
|
autoSaveTimerRef.current = null;
|
|
1324
|
-
|
|
1325
|
-
},
|
|
1642
|
+
autoSaveRef.current.onAutoSave(formValues);
|
|
1643
|
+
}, cfg.debounce || 1e3);
|
|
1326
1644
|
return () => {
|
|
1327
1645
|
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
|
1328
1646
|
};
|
|
1329
|
-
}, [formValues, isDirty
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1647
|
+
}, [formValues, isDirty]);
|
|
1648
|
+
const allVisibleFields = useMemo2(() => {
|
|
1649
|
+
return fields.filter((f) => {
|
|
1332
1650
|
if (f.visible && !f.visible(formValues)) return false;
|
|
1333
1651
|
return true;
|
|
1334
1652
|
});
|
|
1653
|
+
}, [fields, formValues]);
|
|
1654
|
+
const visibleFields = useMemo2(() => {
|
|
1655
|
+
let filtered = allVisibleFields;
|
|
1335
1656
|
if (isMultiStep && steps[currentStep] && steps[currentStep].fields) {
|
|
1336
1657
|
const stepFieldNames = new Set(steps[currentStep].fields);
|
|
1337
1658
|
filtered = filtered.filter((f) => stepFieldNames.has(f.name));
|
|
1338
1659
|
}
|
|
1339
1660
|
return filtered;
|
|
1340
|
-
}, [
|
|
1661
|
+
}, [allVisibleFields, isMultiStep, steps, currentStep]);
|
|
1662
|
+
useEffect2(() => {
|
|
1663
|
+
const nameSet = new Set(fields.map((f) => f.name));
|
|
1664
|
+
if (nameSet.size !== fields.length) {
|
|
1665
|
+
warnConfig("Duplicate field names detected. Field names must be unique.");
|
|
1666
|
+
}
|
|
1667
|
+
for (const field of fields) {
|
|
1668
|
+
const parentName = getDependsOnName(field);
|
|
1669
|
+
if (parentName && !nameSet.has(parentName)) {
|
|
1670
|
+
warnConfig(`Field "${field.name}" depends on missing field "${parentName}".`);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (steps) {
|
|
1674
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1675
|
+
const step = steps[i];
|
|
1676
|
+
if (!step.fields) continue;
|
|
1677
|
+
for (const fieldName of step.fields) {
|
|
1678
|
+
if (!nameSet.has(fieldName)) {
|
|
1679
|
+
warnConfig(`Step ${i + 1} references missing field "${fieldName}".`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (layout) {
|
|
1685
|
+
for (const row of layout) {
|
|
1686
|
+
for (const entry of row) {
|
|
1687
|
+
const fieldName = typeof entry === "string" ? entry : entry.field;
|
|
1688
|
+
if (!nameSet.has(fieldName)) {
|
|
1689
|
+
warnConfig(`Layout references missing field "${fieldName}".`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (sections) {
|
|
1695
|
+
for (const section of sections) {
|
|
1696
|
+
for (const fieldName of section.fields || []) {
|
|
1697
|
+
if (!nameSet.has(fieldName)) {
|
|
1698
|
+
warnConfig(`Section "${section.id}" references missing field "${fieldName}".`);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}, [fields, steps, layout, sections, warnConfig]);
|
|
1704
|
+
const validateRepeaterField = useCallback2(
|
|
1705
|
+
(field, value, allValues) => {
|
|
1706
|
+
const errors = {};
|
|
1707
|
+
const rows = Array.isArray(value) ? value : [];
|
|
1708
|
+
const subFields = field.fields || [];
|
|
1709
|
+
let firstSubError = null;
|
|
1710
|
+
if (resolveRequired(field, allValues) && rows.length === 0) {
|
|
1711
|
+
const requiredError = `${field.label} is required`;
|
|
1712
|
+
errors[field.name] = requiredError;
|
|
1713
|
+
return { errors, hasErrors: true };
|
|
1714
|
+
}
|
|
1715
|
+
if (typeof field.min === "number" && rows.length < field.min) {
|
|
1716
|
+
errors[field.name] = `Must have at least ${field.min} ${field.min === 1 ? "row" : "rows"}`;
|
|
1717
|
+
} else if (typeof field.max === "number" && rows.length > field.max) {
|
|
1718
|
+
errors[field.name] = `Must have no more than ${field.max} ${field.max === 1 ? "row" : "rows"}`;
|
|
1719
|
+
}
|
|
1720
|
+
rows.forEach((row, rowIdx) => {
|
|
1721
|
+
const rowValues = { ...allValues, [field.name]: rows };
|
|
1722
|
+
subFields.forEach((subField) => {
|
|
1723
|
+
if (subField.visible && !subField.visible(rowValues)) return;
|
|
1724
|
+
const err = runValidators(row == null ? void 0 : row[subField.name], subField, rowValues, fieldTypes);
|
|
1725
|
+
if (!err) return;
|
|
1726
|
+
const key = getRepeaterErrorKey(field.name, rowIdx, subField.name);
|
|
1727
|
+
errors[key] = err;
|
|
1728
|
+
if (!firstSubError) firstSubError = { row: rowIdx + 1, message: err };
|
|
1729
|
+
});
|
|
1730
|
+
});
|
|
1731
|
+
if (!errors[field.name] && firstSubError) {
|
|
1732
|
+
errors[field.name] = `Row ${firstSubError.row}: ${firstSubError.message}`;
|
|
1733
|
+
}
|
|
1734
|
+
return { errors, hasErrors: Object.keys(errors).length > 0 };
|
|
1735
|
+
},
|
|
1736
|
+
[fieldTypes]
|
|
1737
|
+
);
|
|
1341
1738
|
const validateField = useCallback2(
|
|
1342
1739
|
(name, value) => {
|
|
1343
|
-
const field =
|
|
1740
|
+
const field = fieldByName.get(name);
|
|
1344
1741
|
if (!field) return null;
|
|
1345
1742
|
if (field.visible && !field.visible(formValues)) return null;
|
|
1743
|
+
if (field.type === "repeater") {
|
|
1744
|
+
const repeaterResult = validateRepeaterField(
|
|
1745
|
+
field,
|
|
1746
|
+
value != null ? value : formValues[name],
|
|
1747
|
+
formValues
|
|
1748
|
+
);
|
|
1749
|
+
return repeaterResult.errors[name] || null;
|
|
1750
|
+
}
|
|
1346
1751
|
return runValidators(value != null ? value : formValues[name], field, formValues, fieldTypes);
|
|
1347
1752
|
},
|
|
1348
|
-
[
|
|
1753
|
+
[fieldByName, formValues, validateRepeaterField, fieldTypes]
|
|
1349
1754
|
);
|
|
1350
1755
|
const validateVisibleFields = useCallback2(
|
|
1351
1756
|
(fieldSubset) => {
|
|
@@ -1353,6 +1758,14 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1353
1758
|
const errors = {};
|
|
1354
1759
|
let hasErrors = false;
|
|
1355
1760
|
for (const field of toValidate) {
|
|
1761
|
+
if (field.type === "repeater") {
|
|
1762
|
+
const repeaterResult = validateRepeaterField(field, formValues[field.name], formValues);
|
|
1763
|
+
if (repeaterResult.hasErrors) {
|
|
1764
|
+
Object.assign(errors, repeaterResult.errors);
|
|
1765
|
+
hasErrors = true;
|
|
1766
|
+
}
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1356
1769
|
const err = runValidators(formValues[field.name], field, formValues, fieldTypes);
|
|
1357
1770
|
if (err) {
|
|
1358
1771
|
errors[field.name] = err;
|
|
@@ -1361,64 +1774,87 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1361
1774
|
}
|
|
1362
1775
|
return { errors, hasErrors };
|
|
1363
1776
|
},
|
|
1364
|
-
[visibleFields, formValues]
|
|
1365
|
-
);
|
|
1366
|
-
const updateErrors = useCallback2(
|
|
1367
|
-
(newErrors) => {
|
|
1368
|
-
setInternalErrors((prev) => {
|
|
1369
|
-
const merged = { ...prev, ...newErrors };
|
|
1370
|
-
for (const key of Object.keys(merged)) {
|
|
1371
|
-
if (newErrors[key] === null || newErrors[key] === void 0) {
|
|
1372
|
-
delete merged[key];
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
if (onValidationChange) onValidationChange(merged);
|
|
1376
|
-
return merged;
|
|
1377
|
-
});
|
|
1378
|
-
},
|
|
1379
|
-
[onValidationChange]
|
|
1777
|
+
[visibleFields, formValues, validateRepeaterField, fieldTypes]
|
|
1380
1778
|
);
|
|
1381
1779
|
const runAsyncValidation = useCallback2(
|
|
1382
1780
|
(name, value) => {
|
|
1383
|
-
const field =
|
|
1384
|
-
if (!field ||
|
|
1781
|
+
const field = fieldByName.get(name);
|
|
1782
|
+
if (!field || field.type === "repeater") return null;
|
|
1385
1783
|
const val = value != null ? value : formValues[name];
|
|
1386
|
-
const syncError = runValidators(val, field, formValues, fieldTypes);
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
(
|
|
1392
|
-
|
|
1784
|
+
const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false });
|
|
1785
|
+
const prevController = asyncAbortRef.current.get(name);
|
|
1786
|
+
if (prevController) prevController.abort();
|
|
1787
|
+
asyncAbortRef.current.delete(name);
|
|
1788
|
+
setValidatingFields((prev) => {
|
|
1789
|
+
if (!prev[name]) return prev;
|
|
1790
|
+
const next = { ...prev };
|
|
1791
|
+
delete next[name];
|
|
1792
|
+
return next;
|
|
1793
|
+
});
|
|
1794
|
+
if (syncError) return null;
|
|
1795
|
+
const version = (asyncValidationVersionRef.current.get(name) || 0) + 1;
|
|
1796
|
+
asyncValidationVersionRef.current.set(name, version);
|
|
1797
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
1798
|
+
if (controller) asyncAbortRef.current.set(name, controller);
|
|
1799
|
+
let asyncPromises;
|
|
1800
|
+
try {
|
|
1801
|
+
asyncPromises = collectAsyncValidatorPromises(
|
|
1802
|
+
val,
|
|
1803
|
+
field,
|
|
1804
|
+
formValues,
|
|
1805
|
+
controller ? { signal: controller.signal } : void 0
|
|
1806
|
+
);
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
updateErrors({ [name]: (err == null ? void 0 : err.message) || "Validation failed" });
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
if (asyncPromises.length === 0) {
|
|
1812
|
+
asyncAbortRef.current.delete(name);
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
const validationPromise = Promise.all(asyncPromises).then(
|
|
1816
|
+
(results) => {
|
|
1817
|
+
if (asyncValidationVersionRef.current.get(name) !== version) return;
|
|
1393
1818
|
asyncValidationRef.current.delete(name);
|
|
1819
|
+
asyncAbortRef.current.delete(name);
|
|
1394
1820
|
setValidatingFields((prev) => {
|
|
1395
1821
|
const next = { ...prev };
|
|
1396
1822
|
delete next[name];
|
|
1397
1823
|
return next;
|
|
1398
1824
|
});
|
|
1399
|
-
|
|
1825
|
+
let err = null;
|
|
1826
|
+
for (const result of results) {
|
|
1827
|
+
const normalized = normalizeValidatorResult(result);
|
|
1828
|
+
if (normalized) {
|
|
1829
|
+
err = normalized;
|
|
1830
|
+
break;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1400
1833
|
updateErrors({ [name]: err });
|
|
1401
1834
|
},
|
|
1402
1835
|
(rejection) => {
|
|
1403
|
-
if (
|
|
1836
|
+
if (asyncValidationVersionRef.current.get(name) !== version) return;
|
|
1404
1837
|
asyncValidationRef.current.delete(name);
|
|
1838
|
+
asyncAbortRef.current.delete(name);
|
|
1405
1839
|
setValidatingFields((prev) => {
|
|
1406
1840
|
const next = { ...prev };
|
|
1407
1841
|
delete next[name];
|
|
1408
1842
|
return next;
|
|
1409
1843
|
});
|
|
1844
|
+
if (rejection && rejection.name === "AbortError") return;
|
|
1410
1845
|
updateErrors({ [name]: (rejection == null ? void 0 : rejection.message) || "Validation failed" });
|
|
1411
1846
|
}
|
|
1412
1847
|
);
|
|
1413
1848
|
asyncValidationRef.current.set(name, validationPromise);
|
|
1414
1849
|
setValidatingFields((prev) => ({ ...prev, [name]: true }));
|
|
1850
|
+
return validationPromise;
|
|
1415
1851
|
},
|
|
1416
|
-
[
|
|
1852
|
+
[fieldByName, formValues, fieldTypes, updateErrors]
|
|
1417
1853
|
);
|
|
1418
1854
|
const triggerAsyncValidation = useCallback2(
|
|
1419
1855
|
(name, value) => {
|
|
1420
|
-
const field =
|
|
1421
|
-
if (!field ||
|
|
1856
|
+
const field = fieldByName.get(name);
|
|
1857
|
+
if (!field || field.type === "repeater") return;
|
|
1422
1858
|
const debounceMs = field.validateDebounce;
|
|
1423
1859
|
if (debounceMs && debounceMs > 0) {
|
|
1424
1860
|
const existing = debounceTimersRef.current.get(name);
|
|
@@ -1432,44 +1868,93 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1432
1868
|
runAsyncValidation(name, value);
|
|
1433
1869
|
}
|
|
1434
1870
|
},
|
|
1435
|
-
[
|
|
1871
|
+
[fieldByName, runAsyncValidation]
|
|
1436
1872
|
);
|
|
1437
|
-
const
|
|
1438
|
-
(
|
|
1873
|
+
const commitValues = useCallback2(
|
|
1874
|
+
(nextValues) => {
|
|
1875
|
+
formValuesRef.current = nextValues;
|
|
1439
1876
|
if (values != null) {
|
|
1440
|
-
if (onChange) onChange(
|
|
1877
|
+
if (onChange) onChange(nextValues);
|
|
1441
1878
|
} else {
|
|
1442
|
-
setInternalValues(
|
|
1879
|
+
setInternalValues(nextValues);
|
|
1443
1880
|
}
|
|
1444
1881
|
},
|
|
1445
|
-
[values, onChange
|
|
1882
|
+
[values, onChange]
|
|
1883
|
+
);
|
|
1884
|
+
const setFieldValueSilent = useCallback2(
|
|
1885
|
+
(name, value) => {
|
|
1886
|
+
const base = draftValuesRef.current || formValuesRef.current || {};
|
|
1887
|
+
const nextValues = { ...base, [name]: value };
|
|
1888
|
+
draftValuesRef.current = nextValues;
|
|
1889
|
+
commitValues(nextValues);
|
|
1890
|
+
},
|
|
1891
|
+
[commitValues]
|
|
1446
1892
|
);
|
|
1447
1893
|
const handleFieldChange = useCallback2(
|
|
1448
1894
|
(name, value) => {
|
|
1449
|
-
const newValues = { ...
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1895
|
+
const newValues = { ...formValuesRef.current, [name]: value };
|
|
1896
|
+
const queue = [name];
|
|
1897
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1898
|
+
const clearedErrors = {};
|
|
1899
|
+
while (queue.length > 0) {
|
|
1900
|
+
const current = queue.shift();
|
|
1901
|
+
if (!current || visited.has(current)) continue;
|
|
1902
|
+
visited.add(current);
|
|
1903
|
+
fields.forEach((dep) => {
|
|
1904
|
+
const parentName = getDependsOnName(dep);
|
|
1905
|
+
if (parentName !== current || dep.name === current) return;
|
|
1906
|
+
if (!dep.options) return;
|
|
1907
|
+
const depOptions = resolveOptions(dep, newValues);
|
|
1908
|
+
const depValue = newValues[dep.name];
|
|
1909
|
+
if (depValue == null || depValue === "") return;
|
|
1910
|
+
const validValues = new Set(depOptions.map((o) => o.value));
|
|
1911
|
+
let nextDepValue = depValue;
|
|
1912
|
+
let changed = false;
|
|
1913
|
+
if (Array.isArray(depValue)) {
|
|
1914
|
+
const filtered = depValue.filter((v) => validValues.has(v));
|
|
1915
|
+
if (filtered.length !== depValue.length) {
|
|
1916
|
+
nextDepValue = filtered;
|
|
1917
|
+
changed = true;
|
|
1918
|
+
}
|
|
1919
|
+
} else if (!validValues.has(depValue)) {
|
|
1920
|
+
nextDepValue = getFieldEmptyValue(dep);
|
|
1921
|
+
changed = true;
|
|
1922
|
+
}
|
|
1923
|
+
if (changed) {
|
|
1924
|
+
newValues[dep.name] = nextDepValue;
|
|
1925
|
+
queue.push(dep.name);
|
|
1926
|
+
if (formErrorsRef.current[dep.name] != null) {
|
|
1927
|
+
clearedErrors[dep.name] = null;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1454
1931
|
}
|
|
1455
|
-
if (
|
|
1456
|
-
|
|
1457
|
-
|
|
1932
|
+
if (formErrorsRef.current[name] != null) {
|
|
1933
|
+
clearedErrors[name] = null;
|
|
1934
|
+
}
|
|
1935
|
+
for (const key of Object.keys(formErrorsRef.current)) {
|
|
1936
|
+
if (key.startsWith(`${name}[`)) {
|
|
1937
|
+
clearedErrors[key] = null;
|
|
1938
|
+
}
|
|
1458
1939
|
}
|
|
1459
|
-
|
|
1940
|
+
draftValuesRef.current = newValues;
|
|
1941
|
+
commitValues(newValues);
|
|
1942
|
+
if (onFieldChange) onFieldChange(name, value, newValues);
|
|
1943
|
+
if (Object.keys(clearedErrors).length > 0) updateErrors(clearedErrors);
|
|
1944
|
+
const field = fieldByName.get(name);
|
|
1460
1945
|
if (field && field.onFieldChange) {
|
|
1461
1946
|
field.onFieldChange(value, newValues, {
|
|
1462
1947
|
setFieldValue: setFieldValueSilent,
|
|
1463
1948
|
setFieldError: (fieldName, message) => updateErrors({ [fieldName]: message })
|
|
1464
1949
|
});
|
|
1465
1950
|
}
|
|
1951
|
+
draftValuesRef.current = null;
|
|
1466
1952
|
},
|
|
1467
|
-
[
|
|
1953
|
+
[fields, getFieldEmptyValue, commitValues, onFieldChange, updateErrors, fieldByName, setFieldValueSilent]
|
|
1468
1954
|
);
|
|
1469
|
-
const inputDebounceRef = useRef2(/* @__PURE__ */ new Map());
|
|
1470
1955
|
const handleDebouncedFieldChange = useCallback2(
|
|
1471
1956
|
(name, value) => {
|
|
1472
|
-
const field =
|
|
1957
|
+
const field = fieldByName.get(name);
|
|
1473
1958
|
const debounceMs = field && field.debounce;
|
|
1474
1959
|
if (debounceMs && debounceMs > 0) {
|
|
1475
1960
|
const existing = inputDebounceRef.current.get(name);
|
|
@@ -1483,26 +1968,24 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1483
1968
|
handleFieldChange(name, value);
|
|
1484
1969
|
}
|
|
1485
1970
|
},
|
|
1486
|
-
[
|
|
1971
|
+
[fieldByName, handleFieldChange]
|
|
1487
1972
|
);
|
|
1488
1973
|
const handleFieldInput = useCallback2(
|
|
1489
1974
|
(name, value) => {
|
|
1490
|
-
if (validateOnChange)
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
}
|
|
1975
|
+
if (!validateOnChange) return;
|
|
1976
|
+
const err = validateField(name, value);
|
|
1977
|
+
updateErrors({ [name]: err });
|
|
1494
1978
|
},
|
|
1495
1979
|
[validateOnChange, validateField, updateErrors]
|
|
1496
1980
|
);
|
|
1497
1981
|
const handleFieldBlur = useCallback2(
|
|
1498
1982
|
(name, value) => {
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
}
|
|
1983
|
+
if (!validateOnBlur) return;
|
|
1984
|
+
const resolvedValue = value != null ? value : formValues[name];
|
|
1985
|
+
const err = validateField(name, resolvedValue);
|
|
1986
|
+
updateErrors({ [name]: err });
|
|
1987
|
+
if (!err) {
|
|
1988
|
+
triggerAsyncValidation(name, resolvedValue);
|
|
1506
1989
|
}
|
|
1507
1990
|
},
|
|
1508
1991
|
[validateOnBlur, validateField, updateErrors, formValues, triggerAsyncValidation]
|
|
@@ -1511,30 +1994,33 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1511
1994
|
async (e) => {
|
|
1512
1995
|
if (e && e.preventDefault) e.preventDefault();
|
|
1513
1996
|
if (validateOnSubmit) {
|
|
1514
|
-
const
|
|
1515
|
-
const { errors, hasErrors } = validateVisibleFields(allVisible);
|
|
1997
|
+
const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
|
|
1516
1998
|
if (hasErrors) {
|
|
1517
|
-
|
|
1518
|
-
if (onValidationChange) onValidationChange(errors);
|
|
1999
|
+
replaceErrors(errors);
|
|
1519
2000
|
return;
|
|
1520
2001
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
2002
|
+
const asyncSubmitValidations = allVisibleFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
|
|
2003
|
+
if (asyncSubmitValidations.length > 0 || asyncValidationRef.current.size > 0) {
|
|
2004
|
+
const pendingValidations = [
|
|
2005
|
+
.../* @__PURE__ */ new Set([
|
|
2006
|
+
...asyncSubmitValidations,
|
|
2007
|
+
...Array.from(asyncValidationRef.current.values())
|
|
2008
|
+
])
|
|
2009
|
+
];
|
|
2010
|
+
await Promise.all(pendingValidations);
|
|
2011
|
+
if (fieldSetHasErrors(formErrorsRef.current, allVisibleFields)) return;
|
|
1526
2012
|
}
|
|
1527
2013
|
}
|
|
1528
2014
|
const reset = () => {
|
|
1529
2015
|
const fresh = computeInitialValues();
|
|
1530
2016
|
if (values == null) setInternalValues(fresh);
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2017
|
+
replaceErrors({});
|
|
2018
|
+
initialSnapshot.current = deepClone(fresh);
|
|
2019
|
+
prevAutoSaveValues.current = deepClone(fresh);
|
|
1534
2020
|
};
|
|
1535
2021
|
const rawValues = {};
|
|
1536
2022
|
for (const key of Object.keys(formValues)) {
|
|
1537
|
-
const f =
|
|
2023
|
+
const f = fieldByName.get(key);
|
|
1538
2024
|
if (f && (f.type === "display" || f.type === "crmPropertyList" || f.type === "crmAssociationPropertyList")) continue;
|
|
1539
2025
|
rawValues[key] = formValues[key];
|
|
1540
2026
|
}
|
|
@@ -1558,25 +2044,34 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1558
2044
|
if (controlledLoading == null) setInternalLoading(false);
|
|
1559
2045
|
}
|
|
1560
2046
|
},
|
|
1561
|
-
[validateOnSubmit,
|
|
2047
|
+
[validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, runAsyncValidation]
|
|
1562
2048
|
);
|
|
1563
|
-
const handleNext = useCallback2(() => {
|
|
2049
|
+
const handleNext = useCallback2(async () => {
|
|
1564
2050
|
if (!isMultiStep) return;
|
|
1565
2051
|
if (validateStepOnNext && steps[currentStep] && steps[currentStep].fields) {
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
);
|
|
2052
|
+
const stepFieldNames = new Set(steps[currentStep].fields);
|
|
2053
|
+
const stepFields = allVisibleFields.filter((f) => stepFieldNames.has(f.name));
|
|
1569
2054
|
const { errors, hasErrors } = validateVisibleFields(stepFields);
|
|
1570
2055
|
if (hasErrors) {
|
|
1571
|
-
|
|
1572
|
-
if (onValidationChange) onValidationChange({ ...internalErrors, ...errors });
|
|
2056
|
+
replaceErrors({ ...formErrorsRef.current, ...errors });
|
|
1573
2057
|
return;
|
|
1574
2058
|
}
|
|
2059
|
+
const asyncStepValidations = stepFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
|
|
2060
|
+
if (asyncStepValidations.length > 0 || asyncValidationRef.current.size > 0) {
|
|
2061
|
+
const pendingValidations = [
|
|
2062
|
+
.../* @__PURE__ */ new Set([
|
|
2063
|
+
...asyncStepValidations,
|
|
2064
|
+
...Array.from(asyncValidationRef.current.values())
|
|
2065
|
+
])
|
|
2066
|
+
];
|
|
2067
|
+
await Promise.all(pendingValidations);
|
|
2068
|
+
if (fieldSetHasErrors(formErrorsRef.current, stepFields)) return;
|
|
2069
|
+
}
|
|
1575
2070
|
}
|
|
1576
2071
|
if (steps[currentStep] && steps[currentStep].validate) {
|
|
1577
2072
|
const result = steps[currentStep].validate(formValues);
|
|
1578
2073
|
if (result !== true && result) {
|
|
1579
|
-
|
|
2074
|
+
replaceErrors({ ...formErrorsRef.current, ...result });
|
|
1580
2075
|
return;
|
|
1581
2076
|
}
|
|
1582
2077
|
}
|
|
@@ -1586,7 +2081,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1586
2081
|
} else {
|
|
1587
2082
|
setInternalStep(nextStep);
|
|
1588
2083
|
}
|
|
1589
|
-
}, [isMultiStep, validateStepOnNext, steps, currentStep,
|
|
2084
|
+
}, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, runAsyncValidation]);
|
|
1590
2085
|
const handleBack = useCallback2(() => {
|
|
1591
2086
|
if (!isMultiStep) return;
|
|
1592
2087
|
const prevStep = Math.max(currentStep - 1, 0);
|
|
@@ -1611,33 +2106,56 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1611
2106
|
useImperativeHandle(ref, () => ({
|
|
1612
2107
|
submit: handleSubmit,
|
|
1613
2108
|
validate: () => {
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
setInternalErrors(errors);
|
|
2109
|
+
const { errors, hasErrors } = validateVisibleFields(allVisibleFields);
|
|
2110
|
+
replaceErrors(errors);
|
|
1617
2111
|
return { valid: !hasErrors, errors };
|
|
1618
2112
|
},
|
|
1619
2113
|
reset: () => {
|
|
1620
2114
|
const fresh = computeInitialValues();
|
|
1621
2115
|
if (values == null) setInternalValues(fresh);
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
2116
|
+
replaceErrors({});
|
|
2117
|
+
initialSnapshot.current = deepClone(fresh);
|
|
2118
|
+
prevAutoSaveValues.current = deepClone(fresh);
|
|
1625
2119
|
},
|
|
1626
2120
|
getValues: () => formValues,
|
|
1627
2121
|
isDirty: () => isDirty,
|
|
1628
2122
|
setFieldValue: (name, value) => handleFieldChange(name, value),
|
|
1629
2123
|
setFieldError: (name, message) => updateErrors({ [name]: message }),
|
|
1630
2124
|
setErrors: (errors) => {
|
|
1631
|
-
|
|
1632
|
-
if (onValidationChange) onValidationChange(errors);
|
|
2125
|
+
replaceErrors(errors);
|
|
1633
2126
|
}
|
|
1634
2127
|
}));
|
|
2128
|
+
const setRepeaterSubFieldError = useCallback2(
|
|
2129
|
+
(fieldName, rowIdx, subFieldName, errorMessage) => {
|
|
2130
|
+
const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
|
|
2131
|
+
const merged = { ...formErrorsRef.current };
|
|
2132
|
+
if (errorMessage) {
|
|
2133
|
+
merged[key] = errorMessage;
|
|
2134
|
+
} else {
|
|
2135
|
+
delete merged[key];
|
|
2136
|
+
}
|
|
2137
|
+
const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
|
|
2138
|
+
const match = k.match(/\[(\d+)\]\./);
|
|
2139
|
+
const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
|
|
2140
|
+
return { key: k, row };
|
|
2141
|
+
}).sort((a, b) => a.row - b.row);
|
|
2142
|
+
if (subErrors.length > 0) {
|
|
2143
|
+
const first = subErrors[0];
|
|
2144
|
+
merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
|
|
2145
|
+
} else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
|
|
2146
|
+
delete merged[fieldName];
|
|
2147
|
+
}
|
|
2148
|
+
replaceErrors(merged);
|
|
2149
|
+
},
|
|
2150
|
+
[replaceErrors]
|
|
2151
|
+
);
|
|
1635
2152
|
const renderField = (field) => {
|
|
1636
2153
|
const fieldValue = formValues[field.name];
|
|
1637
|
-
const fieldError =
|
|
2154
|
+
const fieldError = formErrors[field.name] || null;
|
|
1638
2155
|
const hasError = !!fieldError;
|
|
1639
2156
|
const isRequired = showRequiredIndicator && resolveRequired(field, formValues);
|
|
1640
|
-
const isReadOnly = field.readOnly ||
|
|
2157
|
+
const isReadOnly = field.readOnly || formReadOnly;
|
|
2158
|
+
const isDisabled = disabled || field.disabled || formReadOnly;
|
|
1641
2159
|
const fieldOnChange = field.debounce ? (v) => handleDebouncedFieldChange(field.name, v) : (v) => handleFieldChange(field.name, v);
|
|
1642
2160
|
if (field.type === "display") {
|
|
1643
2161
|
if (field.render) {
|
|
@@ -1696,6 +2214,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1696
2214
|
tooltip: field.tooltip,
|
|
1697
2215
|
required: isRequired,
|
|
1698
2216
|
readOnly: isReadOnly,
|
|
2217
|
+
disabled: isDisabled,
|
|
1699
2218
|
error: hasError,
|
|
1700
2219
|
validationMessage: fieldError || void 0,
|
|
1701
2220
|
...field.loading || validatingFields[field.name] ? { loading: true } : {},
|
|
@@ -1828,6 +2347,9 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1828
2347
|
maxValidationMessage: field.maxValidationMessage,
|
|
1829
2348
|
onChange: (v) => {
|
|
1830
2349
|
handleFieldChange(field.name, { ...fieldValue, date: v, time: timeVal });
|
|
2350
|
+
},
|
|
2351
|
+
onBlur: (v) => {
|
|
2352
|
+
handleFieldBlur(field.name, { ...fieldValue, date: v, time: timeVal });
|
|
1831
2353
|
}
|
|
1832
2354
|
}
|
|
1833
2355
|
)), /* @__PURE__ */ React2.createElement(Box2, { flex: 1 }, /* @__PURE__ */ React2.createElement(
|
|
@@ -1838,12 +2360,16 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1838
2360
|
description: field.description,
|
|
1839
2361
|
tooltip: field.tooltip,
|
|
1840
2362
|
readOnly: isReadOnly,
|
|
2363
|
+
disabled: isDisabled,
|
|
1841
2364
|
error: hasError,
|
|
1842
2365
|
value: timeVal,
|
|
1843
2366
|
interval: field.interval,
|
|
1844
2367
|
timezone: field.timezone,
|
|
1845
2368
|
onChange: (v) => {
|
|
1846
2369
|
handleFieldChange(field.name, { ...fieldValue, date: dateVal, time: v });
|
|
2370
|
+
},
|
|
2371
|
+
onBlur: (v) => {
|
|
2372
|
+
handleFieldBlur(field.name, { ...fieldValue, date: dateVal, time: v });
|
|
1847
2373
|
}
|
|
1848
2374
|
}
|
|
1849
2375
|
)));
|
|
@@ -1881,6 +2407,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1881
2407
|
textChecked: field.textChecked,
|
|
1882
2408
|
textUnchecked: field.textUnchecked,
|
|
1883
2409
|
readonly: isReadOnly,
|
|
2410
|
+
disabled: isDisabled,
|
|
1884
2411
|
onChange: fieldOnChange,
|
|
1885
2412
|
...field.fieldProps || {}
|
|
1886
2413
|
}
|
|
@@ -1893,6 +2420,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1893
2420
|
checked: !!fieldValue,
|
|
1894
2421
|
description: field.description,
|
|
1895
2422
|
readOnly: isReadOnly,
|
|
2423
|
+
disabled: isDisabled,
|
|
1896
2424
|
inline: field.inline,
|
|
1897
2425
|
variant: field.variant,
|
|
1898
2426
|
onChange: fieldOnChange,
|
|
@@ -1929,61 +2457,140 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1929
2457
|
case "repeater": {
|
|
1930
2458
|
const rows = Array.isArray(fieldValue) ? fieldValue : [];
|
|
1931
2459
|
const subFields = field.fields || [];
|
|
1932
|
-
const minRows = field.min
|
|
1933
|
-
const maxRows = field.max
|
|
1934
|
-
const
|
|
1935
|
-
const
|
|
2460
|
+
const minRows = typeof field.min === "number" ? field.min : 0;
|
|
2461
|
+
const maxRows = typeof field.max === "number" ? field.max : Infinity;
|
|
2462
|
+
const repeaterProps = field.repeaterProps || {};
|
|
2463
|
+
const renderAddControl = repeaterProps.renderAdd;
|
|
2464
|
+
const renderRemoveControl = repeaterProps.renderRemove;
|
|
2465
|
+
const renderMoveUpControl = repeaterProps.renderMoveUp;
|
|
2466
|
+
const renderMoveDownControl = repeaterProps.renderMoveDown;
|
|
2467
|
+
const addLabel = repeaterProps.addLabel || "Add";
|
|
2468
|
+
const removeLabel = repeaterProps.removeLabel || "Remove";
|
|
2469
|
+
const moveUpLabel = repeaterProps.moveUpLabel || "Up";
|
|
2470
|
+
const moveDownLabel = repeaterProps.moveDownLabel || "Down";
|
|
2471
|
+
const canEditRows = !isReadOnly && !isDisabled;
|
|
2472
|
+
const canAdd = rows.length < maxRows && canEditRows;
|
|
2473
|
+
const canRemove = rows.length > minRows && canEditRows;
|
|
2474
|
+
const canReorder = !!repeaterProps.reorderable && canEditRows;
|
|
2475
|
+
const repeaterHasNestedErrors = Object.keys(formErrors).some(
|
|
2476
|
+
(k) => k.startsWith(`${field.name}[`)
|
|
2477
|
+
);
|
|
2478
|
+
const firstNestedErrorKey = Object.keys(formErrors).find(
|
|
2479
|
+
(k) => k.startsWith(`${field.name}[`)
|
|
2480
|
+
);
|
|
2481
|
+
const repeaterErrorMessage = fieldError || (firstNestedErrorKey ? formErrors[firstNestedErrorKey] : null);
|
|
2482
|
+
const repeaterHasError = !!fieldError || repeaterHasNestedErrors;
|
|
1936
2483
|
const addRow = () => {
|
|
1937
2484
|
const emptyRow = {};
|
|
1938
2485
|
for (const sf of subFields) {
|
|
1939
|
-
emptyRow[sf.name] = sf.defaultValue !== void 0 ? sf.defaultValue :
|
|
2486
|
+
emptyRow[sf.name] = sf.defaultValue !== void 0 ? sf.defaultValue : getFieldEmptyValue(sf);
|
|
1940
2487
|
}
|
|
1941
2488
|
handleFieldChange(field.name, [...rows, emptyRow]);
|
|
1942
2489
|
};
|
|
1943
2490
|
const removeRow = (idx) => {
|
|
1944
2491
|
handleFieldChange(field.name, rows.filter((_, i) => i !== idx));
|
|
1945
2492
|
};
|
|
1946
|
-
const
|
|
2493
|
+
const moveRow = (fromIndex, toIndex) => {
|
|
2494
|
+
if (toIndex < 0 || toIndex >= rows.length || toIndex === fromIndex) return;
|
|
2495
|
+
const updated = [...rows];
|
|
2496
|
+
const [moved] = updated.splice(fromIndex, 1);
|
|
2497
|
+
updated.splice(toIndex, 0, moved);
|
|
2498
|
+
handleFieldChange(field.name, updated);
|
|
2499
|
+
};
|
|
2500
|
+
const validateSubField = (rowIdx, subField, subValue, nextRows) => {
|
|
2501
|
+
const rowValues = { ...formValues, [field.name]: nextRows };
|
|
2502
|
+
const err = runValidators(subValue, subField, rowValues, fieldTypes);
|
|
2503
|
+
setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
|
|
2504
|
+
};
|
|
2505
|
+
const handleSubFieldChange = (rowIdx, subField, subValue) => {
|
|
1947
2506
|
const updated = rows.map(
|
|
1948
|
-
(row, i) => i ===
|
|
2507
|
+
(row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
|
|
1949
2508
|
);
|
|
1950
2509
|
handleFieldChange(field.name, updated);
|
|
2510
|
+
if (validateOnChange) {
|
|
2511
|
+
validateSubField(rowIdx, subField, subValue, updated);
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
const handleSubFieldBlur = (rowIdx, subField, subValue) => {
|
|
2515
|
+
if (!validateOnBlur) return;
|
|
2516
|
+
const nextRows = rows.map(
|
|
2517
|
+
(row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
|
|
2518
|
+
);
|
|
2519
|
+
validateSubField(rowIdx, subField, subValue, nextRows);
|
|
1951
2520
|
};
|
|
1952
|
-
return /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ React2.createElement(Text2, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ React2.createElement(Text2, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ React2.createElement(Flex2, { key: rowIdx, direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
|
|
2521
|
+
return /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ React2.createElement(Text2, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ React2.createElement(Text2, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ React2.createElement(Flex2, { key: getRowKey(field.name, row, rowIdx), direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
|
|
1953
2522
|
const sfValue = row[sf.name];
|
|
1954
2523
|
const sfLabel = rowIdx === 0 ? sf.label : void 0;
|
|
1955
|
-
const sfOptions = resolveOptions(sf, formValues);
|
|
2524
|
+
const sfOptions = resolveOptions(sf, { ...formValues, [field.name]: rows });
|
|
2525
|
+
const sfError = formErrors[getRepeaterErrorKey(field.name, rowIdx, sf.name)] || null;
|
|
1956
2526
|
const sfProps = {
|
|
1957
2527
|
name: `${field.name}-${rowIdx}-${sf.name}`,
|
|
1958
2528
|
label: sfLabel,
|
|
1959
2529
|
placeholder: sf.placeholder,
|
|
1960
|
-
readOnly: isReadOnly,
|
|
2530
|
+
readOnly: sf.readOnly || isReadOnly,
|
|
2531
|
+
disabled: sf.disabled || isDisabled,
|
|
2532
|
+
error: !!sfError,
|
|
2533
|
+
validationMessage: sfError || void 0,
|
|
1961
2534
|
...sf.fieldProps || {}
|
|
1962
2535
|
};
|
|
1963
2536
|
let sfElement;
|
|
1964
2537
|
switch (sf.type) {
|
|
1965
2538
|
case "select":
|
|
1966
|
-
sfElement = /* @__PURE__ */ React2.createElement(
|
|
2539
|
+
sfElement = /* @__PURE__ */ React2.createElement(
|
|
2540
|
+
Select2,
|
|
2541
|
+
{
|
|
2542
|
+
...sfProps,
|
|
2543
|
+
value: sfValue,
|
|
2544
|
+
options: sfOptions,
|
|
2545
|
+
onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
|
|
2546
|
+
onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
|
|
2547
|
+
}
|
|
2548
|
+
);
|
|
1967
2549
|
break;
|
|
1968
2550
|
case "number":
|
|
1969
|
-
sfElement = /* @__PURE__ */ React2.createElement(
|
|
2551
|
+
sfElement = /* @__PURE__ */ React2.createElement(
|
|
2552
|
+
NumberInput2,
|
|
2553
|
+
{
|
|
2554
|
+
...sfProps,
|
|
2555
|
+
value: sfValue,
|
|
2556
|
+
onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
|
|
2557
|
+
onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
|
|
2558
|
+
}
|
|
2559
|
+
);
|
|
1970
2560
|
break;
|
|
1971
2561
|
case "checkbox":
|
|
1972
|
-
sfElement = /* @__PURE__ */ React2.createElement(
|
|
2562
|
+
sfElement = /* @__PURE__ */ React2.createElement(
|
|
2563
|
+
Checkbox2,
|
|
2564
|
+
{
|
|
2565
|
+
...sfProps,
|
|
2566
|
+
checked: !!sfValue,
|
|
2567
|
+
onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
|
|
2568
|
+
onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
|
|
2569
|
+
},
|
|
2570
|
+
sf.label
|
|
2571
|
+
);
|
|
1973
2572
|
break;
|
|
1974
2573
|
default:
|
|
1975
|
-
sfElement = /* @__PURE__ */ React2.createElement(
|
|
2574
|
+
sfElement = /* @__PURE__ */ React2.createElement(
|
|
2575
|
+
Input2,
|
|
2576
|
+
{
|
|
2577
|
+
...sfProps,
|
|
2578
|
+
value: sfValue || "",
|
|
2579
|
+
onChange: (v) => handleSubFieldChange(rowIdx, sf, v),
|
|
2580
|
+
onBlur: (v) => handleSubFieldBlur(rowIdx, sf, v)
|
|
2581
|
+
}
|
|
2582
|
+
);
|
|
1976
2583
|
}
|
|
1977
2584
|
return /* @__PURE__ */ React2.createElement(Box2, { key: sf.name, flex: 1 }, sfElement);
|
|
1978
|
-
}), canRemove && /* @__PURE__ */ React2.createElement(
|
|
2585
|
+
}), /* @__PURE__ */ React2.createElement(Inline, { gap: "xs" }, canReorder && rowIdx > 0 && (renderMoveUpControl ? renderMoveUpControl({ index: rowIdx, onClick: () => moveRow(rowIdx, rowIdx - 1) }) : /* @__PURE__ */ React2.createElement(Button2, { 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__ */ React2.createElement(Button2, { variant: "secondary", size: "sm", onClick: () => moveRow(rowIdx, rowIdx + 1) }, moveDownLabel)), canRemove && (renderRemoveControl ? renderRemoveControl({ index: rowIdx, onClick: () => removeRow(rowIdx) }) : /* @__PURE__ */ React2.createElement(
|
|
1979
2586
|
Button2,
|
|
1980
2587
|
{
|
|
1981
2588
|
variant: "secondary",
|
|
1982
|
-
size: "
|
|
2589
|
+
size: "md",
|
|
1983
2590
|
onClick: () => removeRow(rowIdx)
|
|
1984
2591
|
},
|
|
1985
|
-
|
|
1986
|
-
))), canAdd && /* @__PURE__ */ React2.createElement(
|
|
2592
|
+
removeLabel
|
|
2593
|
+
))))), canAdd && (renderAddControl ? renderAddControl({ onClick: addRow, count: rows.length }) : /* @__PURE__ */ React2.createElement(Link2, { onClick: addRow }, /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", align: "center", gap: "flush" }, /* @__PURE__ */ React2.createElement(Icon2, { name: "add" }), /* @__PURE__ */ React2.createElement(Text2, { format: { fontWeight: "demibold" } }, addLabel)))), repeaterHasError && repeaterErrorMessage && /* @__PURE__ */ React2.createElement(Text2, { variant: "microcopy" }, repeaterErrorMessage));
|
|
1987
2594
|
}
|
|
1988
2595
|
default:
|
|
1989
2596
|
return /* @__PURE__ */ React2.createElement(
|
|
@@ -2003,15 +2610,17 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2003
2610
|
if (field.width === "full" && columns > 1) return columns;
|
|
2004
2611
|
return 1;
|
|
2005
2612
|
};
|
|
2006
|
-
const getDependents = (parentField) => visibleFields.filter(
|
|
2007
|
-
|
|
2613
|
+
const getDependents = (parentField) => visibleFields.filter(
|
|
2614
|
+
(f) => getDependsOnName(f) === parentField.name && f.name !== parentField.name && getDependsOnDisplay(f) === "grouped"
|
|
2615
|
+
);
|
|
2616
|
+
const isDependent = (field) => getDependsOnName(field) && getDependsOnDisplay(field) === "grouped" && visibleFields.some((f) => f.name === getDependsOnName(field) && f.name !== field.name);
|
|
2008
2617
|
const renderDependentGroup = (parentField, dependents) => {
|
|
2009
|
-
const firstWithLabel = dependents.find((f) => f
|
|
2010
|
-
const firstWithMessage = dependents.find((f) => f
|
|
2011
|
-
const groupLabel = firstWithLabel
|
|
2012
|
-
const rawMessage = firstWithMessage
|
|
2618
|
+
const firstWithLabel = dependents.find((f) => getDependsOnLabel(f)) || dependents[0];
|
|
2619
|
+
const firstWithMessage = dependents.find((f) => getDependsOnMessage(f)) || dependents[0];
|
|
2620
|
+
const groupLabel = getDependsOnLabel(firstWithLabel) || "Dependent properties";
|
|
2621
|
+
const rawMessage = getDependsOnMessage(firstWithMessage);
|
|
2013
2622
|
const tooltipMessage = typeof rawMessage === "function" ? rawMessage(parentField.label) : rawMessage || "";
|
|
2014
|
-
return /* @__PURE__ */ React2.createElement(Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap }, /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React2.createElement(Text2, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ React2.createElement(Link2, { inline: true, variant: "dark", overlay: /* @__PURE__ */ React2.createElement(Tooltip, null, tooltipMessage) }, /* @__PURE__ */ React2.createElement(Icon2, { name: "info" })))), dependents
|
|
2623
|
+
return /* @__PURE__ */ React2.createElement(Tile, { key: `dep-${parentField.name}`, compact: true }, /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap }, /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React2.createElement(Text2, { format: { fontWeight: "demibold" } }, groupLabel, " ", tooltipMessage && /* @__PURE__ */ React2.createElement(Link2, { inline: true, variant: "dark", overlay: /* @__PURE__ */ React2.createElement(Tooltip, null, tooltipMessage) }, /* @__PURE__ */ React2.createElement(Icon2, { name: "info" })))), renderFieldSubset(dependents)));
|
|
2015
2624
|
};
|
|
2016
2625
|
const renderGridLayout = (fieldSubset) => {
|
|
2017
2626
|
const fieldList = fieldSubset || visibleFields;
|
|
@@ -2104,7 +2713,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2104
2713
|
let i = 0;
|
|
2105
2714
|
while (i < fieldList.length) {
|
|
2106
2715
|
const field = fieldList[i];
|
|
2107
|
-
if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !field
|
|
2716
|
+
if (field.width === "half" && i + 1 < fieldList.length && fieldList[i + 1].width === "half" && !getDependsOnName(field)) {
|
|
2108
2717
|
rows.push({ type: "pair", fields: [fieldList[i], fieldList[i + 1]] });
|
|
2109
2718
|
i += 2;
|
|
2110
2719
|
} else {
|
|
@@ -2140,9 +2749,12 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2140
2749
|
let batch = [];
|
|
2141
2750
|
const flushBatch = () => {
|
|
2142
2751
|
if (batch.length === 0) return;
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2752
|
+
const chunks = maxColumns ? Array.from({ length: Math.ceil(batch.length / maxColumns) }, (_, i) => batch.slice(i * maxColumns, i * maxColumns + maxColumns)) : [batch];
|
|
2753
|
+
for (const chunk of chunks) {
|
|
2754
|
+
elements.push(
|
|
2755
|
+
/* @__PURE__ */ React2.createElement(AutoGrid, { key: `ag-${chunk[0].name}`, columnWidth, flexible: true, gap }, chunk.map((f) => /* @__PURE__ */ React2.createElement(React2.Fragment, { key: f.name }, renderField(f))))
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2146
2758
|
batch = [];
|
|
2147
2759
|
};
|
|
2148
2760
|
for (const field of fieldList) {
|
|
@@ -2225,7 +2837,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2225
2837
|
);
|
|
2226
2838
|
if (sec.info) {
|
|
2227
2839
|
elements.push(
|
|
2228
|
-
/* @__PURE__ */ React2.createElement(Flex2, { key: sec.id, direction: "row", align: "start", gap: "flush" }, /* @__PURE__ */ React2.createElement(Box2, { flex: 1 }, accordion), /* @__PURE__ */ React2.createElement(Link2, { overlay: /* @__PURE__ */ React2.createElement(Tooltip, null, sec.info) }, /* @__PURE__ */ React2.createElement(Icon2, { name: "info", size: "sm", screenReaderText: sec.info })))
|
|
2840
|
+
/* @__PURE__ */ React2.createElement(Flex2, { key: sec.id, direction: "row", align: "start", justify: "start", gap: "flush" }, /* @__PURE__ */ React2.createElement(Box2, { flex: 1 }, accordion), /* @__PURE__ */ React2.createElement(Link2, { variant: "dark", overlay: /* @__PURE__ */ React2.createElement(Tooltip, null, sec.info) }, /* @__PURE__ */ React2.createElement(Icon2, { name: "info", size: "sm", screenReaderText: sec.info })))
|
|
2229
2841
|
);
|
|
2230
2842
|
} else {
|
|
2231
2843
|
elements.push(accordion);
|
|
@@ -2253,8 +2865,30 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2253
2865
|
if (submitPosition === "none" || formReadOnly) return null;
|
|
2254
2866
|
const isLastStep = !isMultiStep || currentStep === steps.length - 1;
|
|
2255
2867
|
const isFirstStep = !isMultiStep || currentStep === 0;
|
|
2868
|
+
const buttonContext = {
|
|
2869
|
+
isMultiStep,
|
|
2870
|
+
isFirstStep,
|
|
2871
|
+
isLastStep,
|
|
2872
|
+
currentStep,
|
|
2873
|
+
totalSteps: isMultiStep ? steps.length : 1,
|
|
2874
|
+
disabled,
|
|
2875
|
+
loading: isLoading,
|
|
2876
|
+
labels: {
|
|
2877
|
+
submit: submitButtonLabel,
|
|
2878
|
+
cancel: cancelButtonLabel,
|
|
2879
|
+
back: backButtonLabel,
|
|
2880
|
+
next: nextButtonLabel
|
|
2881
|
+
},
|
|
2882
|
+
onBack: handleBack,
|
|
2883
|
+
onNext: handleNext,
|
|
2884
|
+
onCancel,
|
|
2885
|
+
onSubmit: handleSubmit
|
|
2886
|
+
};
|
|
2887
|
+
if (renderButtonsProp) {
|
|
2888
|
+
return renderButtonsProp(buttonContext);
|
|
2889
|
+
}
|
|
2256
2890
|
if (isMultiStep) {
|
|
2257
|
-
return /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ React2.createElement(Button2, { variant: "secondary", onClick: handleBack, disabled },
|
|
2891
|
+
return /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", justify: "between", align: "center" }, !isFirstStep ? /* @__PURE__ */ React2.createElement(Button2, { variant: "secondary", onClick: handleBack, disabled }, backButtonLabel) : showCancel ? /* @__PURE__ */ React2.createElement(Button2, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel) : /* @__PURE__ */ React2.createElement(Text2, null, " "), /* @__PURE__ */ React2.createElement(Inline, { gap: "small" }, /* @__PURE__ */ React2.createElement(Text2, { variant: "microcopy" }, "Step ", currentStep + 1, " of ", steps.length), isLastStep ? /* @__PURE__ */ React2.createElement(
|
|
2258
2892
|
LoadingButton,
|
|
2259
2893
|
{
|
|
2260
2894
|
variant: submitVariant,
|
|
@@ -2262,10 +2896,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2262
2896
|
onClick: handleSubmit,
|
|
2263
2897
|
disabled
|
|
2264
2898
|
},
|
|
2265
|
-
|
|
2266
|
-
) : /* @__PURE__ */ React2.createElement(Button2, { variant: "primary", onClick: handleNext, disabled },
|
|
2899
|
+
submitButtonLabel
|
|
2900
|
+
) : /* @__PURE__ */ React2.createElement(Button2, { variant: "primary", onClick: handleNext, disabled }, nextButtonLabel)));
|
|
2267
2901
|
}
|
|
2268
|
-
return /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ React2.createElement(Button2, { variant: "secondary", onClick: onCancel, disabled },
|
|
2902
|
+
return /* @__PURE__ */ React2.createElement(Flex2, { direction: "row", justify: showCancel ? "between" : "start", gap: "sm" }, showCancel && /* @__PURE__ */ React2.createElement(Button2, { variant: "secondary", onClick: onCancel, disabled }, cancelButtonLabel), /* @__PURE__ */ React2.createElement(
|
|
2269
2903
|
LoadingButton,
|
|
2270
2904
|
{
|
|
2271
2905
|
variant: submitVariant,
|
|
@@ -2274,7 +2908,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2274
2908
|
onClick: noFormWrapper ? handleSubmit : void 0,
|
|
2275
2909
|
disabled
|
|
2276
2910
|
},
|
|
2277
|
-
|
|
2911
|
+
submitButtonLabel
|
|
2278
2912
|
));
|
|
2279
2913
|
};
|
|
2280
2914
|
const formContent = /* @__PURE__ */ React2.createElement(Flex2, { direction: "column", gap }, isMultiStep && showStepIndicator && /* @__PURE__ */ React2.createElement(
|
|
@@ -2283,7 +2917,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2283
2917
|
currentStep,
|
|
2284
2918
|
stepNames: steps.map((s) => s.title)
|
|
2285
2919
|
}
|
|
2286
|
-
), formReadOnly && readOnlyMessage && /* @__PURE__ */ React2.createElement(Alert, { title:
|
|
2920
|
+
), formReadOnly && readOnlyMessage && /* @__PURE__ */ React2.createElement(Alert, { title: readOnlyTitle, variant: "warning" }, readOnlyMessage), !addAlert && formError && /* @__PURE__ */ React2.createElement(Alert, { title: errorTitle, variant: "danger" }, typeof formError === "string" ? formError : void 0), !addAlert && formSuccess && /* @__PURE__ */ React2.createElement(Alert, { title: successTitle, variant: "success" }, formSuccess), isMultiStep && steps[currentStep] && steps[currentStep].render ? steps[currentStep].render({
|
|
2287
2921
|
values: formValues,
|
|
2288
2922
|
goNext: handleNext,
|
|
2289
2923
|
goBack: handleBack,
|
|
@@ -2295,7 +2929,15 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
2295
2929
|
if (noFormWrapper) {
|
|
2296
2930
|
return formContent;
|
|
2297
2931
|
}
|
|
2298
|
-
return /* @__PURE__ */ React2.createElement(
|
|
2932
|
+
return /* @__PURE__ */ React2.createElement(
|
|
2933
|
+
Form,
|
|
2934
|
+
{
|
|
2935
|
+
...formProps || {},
|
|
2936
|
+
onSubmit: handleSubmit,
|
|
2937
|
+
autoComplete
|
|
2938
|
+
},
|
|
2939
|
+
formContent
|
|
2940
|
+
);
|
|
2299
2941
|
});
|
|
2300
2942
|
export {
|
|
2301
2943
|
DataTable,
|