hs-uix 1.4.0 → 1.4.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/dist/index.mjs CHANGED
@@ -502,28 +502,33 @@ var DataTable = ({
502
502
  return next;
503
503
  });
504
504
  }, []);
505
- const flatRows = useMemo(() => {
506
- if (!groupedData) return (serverSide ? data : sortedData).map((row) => ({ type: "data", row }));
507
- const flat = [];
508
- groupedData.forEach((group) => {
509
- flat.push({ type: "group-header", group });
510
- if (expandedGroups.has(group.key)) {
511
- group.rows.forEach((row) => flat.push({ type: "data", row }));
512
- }
513
- });
514
- return flat;
515
- }, [groupedData, sortedData, data, serverSide, expandedGroups]);
516
- const totalItems = serverSide ? totalCount || data.length : flatRows.length;
505
+ const datasetRows = useMemo(() => {
506
+ if (!groupedData) return serverSide ? data : sortedData;
507
+ return groupedData.flatMap((group) => group.rows);
508
+ }, [groupedData, sortedData, data, serverSide]);
509
+ const totalItems = serverSide ? totalCount || data.length : datasetRows.length;
517
510
  const pageCount = Math.ceil(totalItems / pageSize);
518
- let displayRows;
519
- if (serverSide) {
520
- displayRows = groupBy ? flatRows : data.map((row) => ({ type: "data", row }));
521
- } else {
522
- displayRows = flatRows.slice(
511
+ const paginatedRows = useMemo(() => {
512
+ if (serverSide) return datasetRows;
513
+ return datasetRows.slice(
523
514
  (activePage - 1) * pageSize,
524
515
  activePage * pageSize
525
516
  );
526
- }
517
+ }, [serverSide, datasetRows, activePage, pageSize]);
518
+ const displayRows = useMemo(() => {
519
+ if (!groupedData) return paginatedRows.map((row) => ({ type: "data", row }));
520
+ const pageRows = new Set(paginatedRows);
521
+ const rows = [];
522
+ groupedData.forEach((group) => {
523
+ const groupPageRows = group.rows.filter((row) => pageRows.has(row));
524
+ if (groupPageRows.length === 0) return;
525
+ rows.push({ type: "group-header", group });
526
+ if (expandedGroups.has(group.key)) {
527
+ groupPageRows.forEach((row) => rows.push({ type: "data", row }));
528
+ }
529
+ });
530
+ return rows;
531
+ }, [groupedData, paginatedRows, expandedGroups]);
527
532
  const footerData = serverSide ? data : filteredData;
528
533
  const activeChips = useMemo(() => {
529
534
  const chips = [];
@@ -634,8 +639,8 @@ var DataTable = ({
634
639
  return displayRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null);
635
640
  }, [serverSide, data, displayRows, rowIdField]);
636
641
  const allRowIds = useMemo(
637
- () => flatRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null),
638
- [flatRows, rowIdField]
642
+ () => datasetRows.map((row) => row[rowIdField]).filter((id) => id != null),
643
+ [datasetRows, rowIdField]
639
644
  );
640
645
  const handleSelectRow = useCallback((rowId, checked) => {
641
646
  const next = new Set(selectedIds);
@@ -682,19 +687,25 @@ var DataTable = ({
682
687
  if (row) onEditStart(row, field, currentValue);
683
688
  }
684
689
  }, [onEditStart, data, rowIdField]);
685
- const commitEdit = useCallback((row, field, value) => {
690
+ const commitEdit = useCallback((row, field, value, options = {}) => {
691
+ const { keepEditing = false } = options;
686
692
  const col = columns.find((c) => c.field === field);
687
693
  if (col == null ? void 0 : col.editValidate) {
688
694
  const result = col.editValidate(value, row);
689
695
  if (result !== true && result !== void 0 && result !== null) {
690
696
  setEditError(typeof result === "string" ? result : "Invalid value");
691
- return;
697
+ return false;
692
698
  }
693
699
  }
694
700
  if (onRowEdit) onRowEdit(row, field, value);
695
- setEditingCell(null);
696
- setEditValue(null);
701
+ if (!keepEditing) {
702
+ setEditingCell(null);
703
+ setEditValue(null);
704
+ } else {
705
+ setEditValue(value);
706
+ }
697
707
  setEditError(null);
708
+ return true;
698
709
  }, [onRowEdit, columns]);
699
710
  const renderEditControl = (col, row) => {
700
711
  const type = col.editType || "text";
@@ -753,12 +764,12 @@ var DataTable = ({
753
764
  case "datetime":
754
765
  return /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: `${fieldName}-date`, label: "", value: editValue == null ? void 0 : editValue.date, onChange: (val) => {
755
766
  const next = { ...editValue, date: val };
756
- setEditValue(next);
757
- if (onRowEdit) onRowEdit(row, col.field, next);
767
+ handleInput(next);
768
+ commitEdit(row, col.field, next, { keepEditing: true });
758
769
  }, onBlur: maybeExitDatetimeEdit }), /* @__PURE__ */ React.createElement(TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: "", value: editValue == null ? void 0 : editValue.time, onChange: (val) => {
759
770
  const next = { ...editValue, time: val };
760
- setEditValue(next);
761
- if (onRowEdit) onRowEdit(row, col.field, next);
771
+ handleInput(next);
772
+ commitEdit(row, col.field, next, { keepEditing: true });
762
773
  }, onBlur: maybeExitDatetimeEdit }));
763
774
  case "toggle":
764
775
  return /* @__PURE__ */ React.createElement(Toggle, { ...extra, name: fieldName, label: "", checked: !!editValue, onChange: commit });
@@ -1771,9 +1782,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1771
1782
  const inputDebounceRef = useRef2(/* @__PURE__ */ new Map());
1772
1783
  const rowKeyRef = useRef2(/* @__PURE__ */ new WeakMap());
1773
1784
  const rowKeyCounterRef = useRef2(0);
1785
+ const controlledBaselineLockedRef = useRef2(false);
1774
1786
  const initialSnapshot = useRef2(null);
1775
1787
  if (initialSnapshot.current === null) {
1776
- initialSnapshot.current = deepClone(computeInitialValues());
1788
+ initialSnapshot.current = deepClone(values != null ? values : computeInitialValues());
1777
1789
  }
1778
1790
  const formValues = values != null ? values : internalValues;
1779
1791
  const formErrors = controlledErrors != null ? controlledErrors : internalErrors;
@@ -1785,6 +1797,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1785
1797
  const draftValuesRef = useRef2(null);
1786
1798
  formValuesRef.current = formValues;
1787
1799
  formErrorsRef.current = formErrors;
1800
+ const syncDirtyBaseline = useCallback2((nextValues) => {
1801
+ initialSnapshot.current = deepClone(nextValues || {});
1802
+ prevAutoSaveValues.current = deepClone(nextValues || {});
1803
+ }, []);
1788
1804
  const fieldByName = useMemo2(() => {
1789
1805
  const map = /* @__PURE__ */ new Map();
1790
1806
  for (const field of fields) {
@@ -1861,6 +1877,11 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1861
1877
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1862
1878
  };
1863
1879
  }, []);
1880
+ useEffect2(() => {
1881
+ if (values == null) return;
1882
+ if (controlledBaselineLockedRef.current) return;
1883
+ syncDirtyBaseline(values);
1884
+ }, [values, syncDirtyBaseline]);
1864
1885
  const isDirty = useMemo2(() => {
1865
1886
  return !deepEqual(formValues, initialSnapshot.current);
1866
1887
  }, [formValues]);
@@ -1979,6 +2000,50 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1979
2000
  },
1980
2001
  [fieldTypes]
1981
2002
  );
2003
+ const setRepeaterSubFieldError = useCallback2(
2004
+ (fieldName, rowIdx, subFieldName, errorMessage) => {
2005
+ const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
2006
+ const merged = { ...formErrorsRef.current };
2007
+ if (errorMessage) {
2008
+ merged[key] = errorMessage;
2009
+ } else {
2010
+ delete merged[key];
2011
+ }
2012
+ const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
2013
+ const match = k.match(/\[(\d+)\]\./);
2014
+ const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
2015
+ return { key: k, row };
2016
+ }).sort((a, b) => a.row - b.row);
2017
+ if (subErrors.length > 0) {
2018
+ const first = subErrors[0];
2019
+ merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
2020
+ } else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
2021
+ delete merged[fieldName];
2022
+ }
2023
+ replaceErrors(merged);
2024
+ },
2025
+ [replaceErrors]
2026
+ );
2027
+ const expandValidationFields = useCallback2(
2028
+ (fieldSubset) => {
2029
+ const toValidate = fieldSubset || visibleFields;
2030
+ const expanded = [];
2031
+ for (const field of toValidate) {
2032
+ if (field.type === "fieldGroup" && field.items && field.fields) {
2033
+ for (const item of field.items) {
2034
+ for (const subField of field.fields(item)) {
2035
+ if (subField.visible && !subField.visible(formValues)) continue;
2036
+ expanded.push(subField);
2037
+ }
2038
+ }
2039
+ continue;
2040
+ }
2041
+ expanded.push(field);
2042
+ }
2043
+ return expanded;
2044
+ },
2045
+ [visibleFields, formValues]
2046
+ );
1982
2047
  const validateField = useCallback2(
1983
2048
  (name, value) => {
1984
2049
  const field = fieldByName.get(name);
@@ -1998,7 +2063,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1998
2063
  );
1999
2064
  const validateVisibleFields = useCallback2(
2000
2065
  (fieldSubset) => {
2001
- const toValidate = fieldSubset || visibleFields;
2066
+ const toValidate = expandValidationFields(fieldSubset);
2002
2067
  const errors = {};
2003
2068
  let hasErrors = false;
2004
2069
  for (const field of toValidate) {
@@ -2018,52 +2083,54 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2018
2083
  }
2019
2084
  return { errors, hasErrors };
2020
2085
  },
2021
- [visibleFields, formValues, validateRepeaterField, fieldTypes, validationMessages]
2086
+ [expandValidationFields, formValues, validateRepeaterField, fieldTypes, validationMessages]
2022
2087
  );
2023
- const runAsyncValidation = useCallback2(
2024
- (name, value) => {
2025
- const field = fieldByName.get(name);
2026
- if (!field || field.type === "repeater") return null;
2027
- const val = value != null ? value : formValues[name];
2028
- const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false, messages: validationMessages });
2029
- const prevController = asyncAbortRef.current.get(name);
2088
+ const runAsyncValidationTarget = useCallback2(
2089
+ (target) => {
2090
+ const { validationKey, field, value, allValues, applyError } = target || {};
2091
+ if (!field || !validationKey || field.type === "repeater" || field.type === "fieldGroup") return null;
2092
+ const syncError = runValidators(value, field, allValues, fieldTypes, {
2093
+ includeCustomValidators: false,
2094
+ messages: validationMessages
2095
+ });
2096
+ const prevController = asyncAbortRef.current.get(validationKey);
2030
2097
  if (prevController) prevController.abort();
2031
- asyncAbortRef.current.delete(name);
2098
+ asyncAbortRef.current.delete(validationKey);
2032
2099
  setValidatingFields((prev) => {
2033
- if (!prev[name]) return prev;
2100
+ if (!prev[validationKey]) return prev;
2034
2101
  const next = { ...prev };
2035
- delete next[name];
2102
+ delete next[validationKey];
2036
2103
  return next;
2037
2104
  });
2038
2105
  if (syncError) return null;
2039
- const version = (asyncValidationVersionRef.current.get(name) || 0) + 1;
2040
- asyncValidationVersionRef.current.set(name, version);
2106
+ const version = (asyncValidationVersionRef.current.get(validationKey) || 0) + 1;
2107
+ asyncValidationVersionRef.current.set(validationKey, version);
2041
2108
  const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
2042
- if (controller) asyncAbortRef.current.set(name, controller);
2109
+ if (controller) asyncAbortRef.current.set(validationKey, controller);
2043
2110
  let asyncPromises;
2044
2111
  try {
2045
2112
  asyncPromises = collectAsyncValidatorPromises(
2046
- val,
2113
+ value,
2047
2114
  field,
2048
- formValues,
2115
+ allValues,
2049
2116
  controller ? { signal: controller.signal } : void 0
2050
2117
  );
2051
2118
  } catch (err) {
2052
- updateErrors({ [name]: (err == null ? void 0 : err.message) || "Validation failed" });
2119
+ applyError((err == null ? void 0 : err.message) || "Validation failed");
2053
2120
  return null;
2054
2121
  }
2055
2122
  if (asyncPromises.length === 0) {
2056
- asyncAbortRef.current.delete(name);
2123
+ asyncAbortRef.current.delete(validationKey);
2057
2124
  return null;
2058
2125
  }
2059
2126
  const validationPromise = Promise.all(asyncPromises).then(
2060
2127
  (results) => {
2061
- if (asyncValidationVersionRef.current.get(name) !== version) return;
2062
- asyncValidationRef.current.delete(name);
2063
- asyncAbortRef.current.delete(name);
2128
+ if (asyncValidationVersionRef.current.get(validationKey) !== version) return;
2129
+ asyncValidationRef.current.delete(validationKey);
2130
+ asyncAbortRef.current.delete(validationKey);
2064
2131
  setValidatingFields((prev) => {
2065
2132
  const next = { ...prev };
2066
- delete next[name];
2133
+ delete next[validationKey];
2067
2134
  return next;
2068
2135
  });
2069
2136
  let err = null;
@@ -2074,50 +2141,128 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2074
2141
  break;
2075
2142
  }
2076
2143
  }
2077
- updateErrors({ [name]: err });
2144
+ applyError(err);
2078
2145
  },
2079
2146
  (rejection) => {
2080
- if (asyncValidationVersionRef.current.get(name) !== version) return;
2081
- asyncValidationRef.current.delete(name);
2082
- asyncAbortRef.current.delete(name);
2147
+ if (asyncValidationVersionRef.current.get(validationKey) !== version) return;
2148
+ asyncValidationRef.current.delete(validationKey);
2149
+ asyncAbortRef.current.delete(validationKey);
2083
2150
  setValidatingFields((prev) => {
2084
2151
  const next = { ...prev };
2085
- delete next[name];
2152
+ delete next[validationKey];
2086
2153
  return next;
2087
2154
  });
2088
2155
  if (rejection && rejection.name === "AbortError") return;
2089
- updateErrors({ [name]: (rejection == null ? void 0 : rejection.message) || "Validation failed" });
2156
+ applyError((rejection == null ? void 0 : rejection.message) || "Validation failed");
2090
2157
  }
2091
2158
  );
2092
- asyncValidationRef.current.set(name, validationPromise);
2093
- setValidatingFields((prev) => ({ ...prev, [name]: true }));
2159
+ asyncValidationRef.current.set(validationKey, validationPromise);
2160
+ setValidatingFields((prev) => ({ ...prev, [validationKey]: true }));
2094
2161
  return validationPromise;
2095
2162
  },
2096
- [fieldByName, formValues, fieldTypes, updateErrors]
2163
+ [fieldTypes, validationMessages]
2097
2164
  );
2098
- const triggerAsyncValidation = useCallback2(
2165
+ const runAsyncValidation = useCallback2(
2099
2166
  (name, value) => {
2100
2167
  const field = fieldByName.get(name);
2101
- if (!field || field.type === "repeater") return;
2102
- const debounceMs = field.validateDebounce;
2168
+ if (!field || field.type === "repeater" || field.type === "fieldGroup") return null;
2169
+ return runAsyncValidationTarget({
2170
+ validationKey: name,
2171
+ field,
2172
+ value: value != null ? value : formValues[name],
2173
+ allValues: formValues,
2174
+ applyError: (errorMessage) => updateErrors({ [name]: errorMessage })
2175
+ });
2176
+ },
2177
+ [fieldByName, formValues, runAsyncValidationTarget, updateErrors]
2178
+ );
2179
+ const triggerAsyncValidationTarget = useCallback2(
2180
+ (target) => {
2181
+ if (!(target == null ? void 0 : target.field) || !target.validationKey) return;
2182
+ const debounceMs = target.field.validateDebounce;
2103
2183
  if (debounceMs && debounceMs > 0) {
2104
- const existing = debounceTimersRef.current.get(name);
2184
+ const existing = debounceTimersRef.current.get(target.validationKey);
2105
2185
  if (existing) clearTimeout(existing);
2106
2186
  const timer = setTimeout(() => {
2107
- debounceTimersRef.current.delete(name);
2108
- runAsyncValidation(name, value);
2187
+ debounceTimersRef.current.delete(target.validationKey);
2188
+ runAsyncValidationTarget(target);
2109
2189
  }, debounceMs);
2110
- debounceTimersRef.current.set(name, timer);
2190
+ debounceTimersRef.current.set(target.validationKey, timer);
2111
2191
  } else {
2112
- runAsyncValidation(name, value);
2192
+ runAsyncValidationTarget(target);
2113
2193
  }
2114
2194
  },
2115
- [fieldByName, runAsyncValidation]
2195
+ [runAsyncValidationTarget]
2196
+ );
2197
+ const triggerAsyncValidation = useCallback2(
2198
+ (name, value) => {
2199
+ const field = fieldByName.get(name);
2200
+ if (!field || field.type === "repeater" || field.type === "fieldGroup") return;
2201
+ triggerAsyncValidationTarget({
2202
+ validationKey: name,
2203
+ field,
2204
+ value: value != null ? value : formValuesRef.current[name],
2205
+ allValues: formValuesRef.current,
2206
+ applyError: (errorMessage) => updateErrors({ [name]: errorMessage })
2207
+ });
2208
+ },
2209
+ [fieldByName, triggerAsyncValidationTarget, updateErrors]
2210
+ );
2211
+ const getAsyncValidationTargets = useCallback2(
2212
+ (fieldSubset) => {
2213
+ const toValidate = fieldSubset || visibleFields;
2214
+ const targets = [];
2215
+ for (const field of toValidate) {
2216
+ if (field.type === "fieldGroup" && field.items && field.fields) {
2217
+ for (const item of field.items) {
2218
+ for (const subField of field.fields(item)) {
2219
+ if (subField.visible && !subField.visible(formValues)) continue;
2220
+ targets.push({
2221
+ validationKey: subField.name,
2222
+ field: subField,
2223
+ value: formValues[subField.name],
2224
+ allValues: formValues,
2225
+ applyError: (errorMessage) => updateErrors({ [subField.name]: errorMessage })
2226
+ });
2227
+ }
2228
+ }
2229
+ continue;
2230
+ }
2231
+ if (field.type === "repeater") {
2232
+ const rows = Array.isArray(formValues[field.name]) ? formValues[field.name] : [];
2233
+ const subFields = field.fields || [];
2234
+ rows.forEach((row, rowIdx) => {
2235
+ const rowValues = { ...formValues, [field.name]: rows };
2236
+ subFields.forEach((subField) => {
2237
+ if (subField.visible && !subField.visible(rowValues)) return;
2238
+ targets.push({
2239
+ validationKey: getRepeaterErrorKey(field.name, rowIdx, subField.name),
2240
+ field: subField,
2241
+ value: row == null ? void 0 : row[subField.name],
2242
+ allValues: rowValues,
2243
+ applyError: (errorMessage) => setRepeaterSubFieldError(field.name, rowIdx, subField.name, errorMessage)
2244
+ });
2245
+ });
2246
+ });
2247
+ continue;
2248
+ }
2249
+ targets.push({
2250
+ validationKey: field.name,
2251
+ field,
2252
+ value: formValues[field.name],
2253
+ allValues: formValues,
2254
+ applyError: (errorMessage) => updateErrors({ [field.name]: errorMessage })
2255
+ });
2256
+ }
2257
+ return targets;
2258
+ },
2259
+ [visibleFields, formValues, setRepeaterSubFieldError, updateErrors]
2116
2260
  );
2117
2261
  const commitValues = useCallback2(
2118
2262
  (nextValues) => {
2119
2263
  formValuesRef.current = nextValues;
2120
2264
  if (values != null) {
2265
+ controlledBaselineLockedRef.current = true;
2121
2266
  if (onChange) onChange(nextValues);
2122
2267
  } else {
2123
2268
  setInternalValues(nextValues);
@@ -2135,7 +2280,8 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2135
2280
  [commitValues]
2136
2281
  );
2137
2282
  const handleFieldChange = useCallback2(
2138
- (name, value) => {
2283
+ (name, value, options = {}) => {
2284
+ const { clearNestedErrors = true } = options;
2139
2285
  const newValues = { ...formValuesRef.current, [name]: value };
2140
2286
  const queue = [name];
2141
2287
  const visited = /* @__PURE__ */ new Set();
@@ -2176,9 +2322,11 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2176
2322
  if (formErrorsRef.current[name] != null) {
2177
2323
  clearedErrors[name] = null;
2178
2324
  }
2179
- for (const key of Object.keys(formErrorsRef.current)) {
2180
- if (key.startsWith(`${name}[`)) {
2181
- clearedErrors[key] = null;
2325
+ if (clearNestedErrors) {
2326
+ for (const key of Object.keys(formErrorsRef.current)) {
2327
+ if (key.startsWith(`${name}[`)) {
2328
+ clearedErrors[key] = null;
2329
+ }
2182
2330
  }
2183
2331
  }
2184
2332
  draftValuesRef.current = newValues;
@@ -2247,7 +2395,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2247
2395
  replaceErrors(errors);
2248
2396
  return;
2249
2397
  }
2250
- const asyncSubmitValidations = allVisibleFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
2398
+ const asyncSubmitValidations = getAsyncValidationTargets(allVisibleFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
2251
2399
  if (asyncSubmitValidations.length > 0 || asyncValidationRef.current.size > 0) {
2252
2400
  const pendingValidations = [
2253
2401
  .../* @__PURE__ */ new Set([
@@ -2262,6 +2410,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2262
2410
  const reset = () => {
2263
2411
  const fresh = computeInitialValues();
2264
2412
  if (values == null) setInternalValues(fresh);
2413
+ controlledBaselineLockedRef.current = false;
2265
2414
  replaceErrors({});
2266
2415
  initialSnapshot.current = deepClone(fresh);
2267
2416
  prevAutoSaveValues.current = deepClone(fresh);
@@ -2301,7 +2450,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2301
2450
  if (controlledLoading == null) setInternalLoading(false);
2302
2451
  }
2303
2452
  },
2304
- [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, runAsyncValidation]
2453
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget]
2305
2454
  );
2306
2455
  const handleNext = useCallback2(async () => {
2307
2456
  if (!isMultiStep) return;
@@ -2313,7 +2462,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2313
2462
  replaceErrors({ ...formErrorsRef.current, ...errors });
2314
2463
  return;
2315
2464
  }
2316
- const asyncStepValidations = stepFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
2465
+ const asyncStepValidations = getAsyncValidationTargets(stepFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
2317
2466
  if (asyncStepValidations.length > 0 || asyncValidationRef.current.size > 0) {
2318
2467
  const pendingValidations = [
2319
2468
  .../* @__PURE__ */ new Set([
@@ -2338,7 +2487,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2338
2487
  } else {
2339
2488
  setInternalStep(nextStep);
2340
2489
  }
2341
- }, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, runAsyncValidation]);
2490
+ }, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, getAsyncValidationTargets, runAsyncValidationTarget]);
2342
2491
  const handleBack = useCallback2(() => {
2343
2492
  if (!isMultiStep) return;
2344
2493
  const prevStep = Math.max(currentStep - 1, 0);
@@ -2370,6 +2519,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2370
2519
  reset: () => {
2371
2520
  const fresh = computeInitialValues();
2372
2521
  if (values == null) setInternalValues(fresh);
2522
+ controlledBaselineLockedRef.current = false;
2373
2523
  replaceErrors({});
2374
2524
  initialSnapshot.current = deepClone(fresh);
2375
2525
  prevAutoSaveValues.current = deepClone(fresh);
@@ -2382,30 +2532,6 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2382
2532
  replaceErrors(errors);
2383
2533
  }
2384
2534
  }));
2385
- const setRepeaterSubFieldError = useCallback2(
2386
- (fieldName, rowIdx, subFieldName, errorMessage) => {
2387
- const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
2388
- const merged = { ...formErrorsRef.current };
2389
- if (errorMessage) {
2390
- merged[key] = errorMessage;
2391
- } else {
2392
- delete merged[key];
2393
- }
2394
- const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
2395
- const match = k.match(/\[(\d+)\]\./);
2396
- const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
2397
- return { key: k, row };
2398
- }).sort((a, b) => a.row - b.row);
2399
- if (subErrors.length > 0) {
2400
- const first = subErrors[0];
2401
- merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
2402
- } else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
2403
- delete merged[fieldName];
2404
- }
2405
- replaceErrors(merged);
2406
- },
2407
- [replaceErrors]
2408
- );
2409
2535
  const renderField = (field) => {
2410
2536
  const fieldError = formErrors[field.name] || null;
2411
2537
  const rendered = renderFieldInner(field);
@@ -2885,12 +3011,13 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2885
3011
  const rowValues = { ...formValues, [field.name]: nextRows };
2886
3012
  const err = runValidators(subValue, subField, rowValues, fieldTypes, { messages: validationMessages });
2887
3013
  setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
3014
+ return err;
2888
3015
  };
2889
3016
  const handleSubFieldChange = (rowIdx, subField, subValue) => {
2890
3017
  const updated = rows.map(
2891
3018
  (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
2892
3019
  );
2893
- handleFieldChange(field.name, updated);
3020
+ handleFieldChange(field.name, updated, { clearNestedErrors: false });
2894
3021
  if (validateOnChange) {
2895
3022
  validateSubField(rowIdx, subField, subValue, updated);
2896
3023
  }
@@ -2900,13 +3027,24 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2900
3027
  const nextRows = rows.map(
2901
3028
  (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
2902
3029
  );
2903
- validateSubField(rowIdx, subField, subValue, nextRows);
3030
+ const err = validateSubField(rowIdx, subField, subValue, nextRows);
3031
+ if (err) return;
3032
+ const validationKey = getRepeaterErrorKey(field.name, rowIdx, subField.name);
3033
+ const rowValues = { ...formValues, [field.name]: nextRows };
3034
+ triggerAsyncValidationTarget({
3035
+ validationKey,
3036
+ field: subField,
3037
+ value: subValue,
3038
+ allValues: rowValues,
3039
+ applyError: (errorMessage) => setRepeaterSubFieldError(field.name, rowIdx, subField.name, errorMessage)
3040
+ });
2904
3041
  };
2905
3042
  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) => {
2906
3043
  const sfValue = row[sf.name];
2907
3044
  const sfLabel = rowIdx === 0 ? sf.label : void 0;
2908
3045
  const sfOptions = resolveOptions(sf, { ...formValues, [field.name]: rows });
2909
3046
  const sfError = formErrors[getRepeaterErrorKey(field.name, rowIdx, sf.name)] || null;
3047
+ const validationKey = getRepeaterErrorKey(field.name, rowIdx, sf.name);
2910
3048
  const sfProps = {
2911
3049
  name: `${field.name}-${rowIdx}-${sf.name}`,
2912
3050
  label: sfLabel,
@@ -2915,6 +3053,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2915
3053
  disabled: resolveDisabled(sf, formValues) || isDisabled,
2916
3054
  error: !!sfError,
2917
3055
  validationMessage: sfError || void 0,
3056
+ ...validatingFields[validationKey] ? { loading: true } : {},
2918
3057
  ...sf.fieldProps || {}
2919
3058
  };
2920
3059
  let sfElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hs-uix",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Production-ready UI components for HubSpot UI Extensions — DataTable, FormBuilder, and more",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",