hs-uix 1.4.0 → 1.5.0

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
@@ -304,7 +304,9 @@ var DataTable = ({
304
304
  const initialSortState = useMemo(() => {
305
305
  return normalizeSortState(columns, defaultSort);
306
306
  }, [columns, defaultSort]);
307
- const [internalSearchTerm, setInternalSearchTerm] = useState("");
307
+ const [internalSearchTerm, setInternalSearchTerm] = useState(
308
+ () => serverSide && searchValue != null ? searchValue : ""
309
+ );
308
310
  const [internalFilterValues, setInternalFilterValues] = useState(() => {
309
311
  const init = {};
310
312
  filters.forEach((f) => {
@@ -315,7 +317,16 @@ var DataTable = ({
315
317
  const [internalSortState, setInternalSortState] = useState(initialSortState);
316
318
  const [currentPage, setCurrentPage] = useState(1);
317
319
  const [showMoreFilters, setShowMoreFilters] = useState(false);
320
+ const lastAppliedSearchRef = useRef(
321
+ serverSide && searchValue != null ? searchValue : ""
322
+ );
318
323
  const searchTerm = serverSide && searchValue != null ? searchValue : internalSearchTerm;
324
+ useEffect(() => {
325
+ if (!serverSide || searchValue == null) return;
326
+ if (searchValue === lastAppliedSearchRef.current) return;
327
+ lastAppliedSearchRef.current = searchValue;
328
+ setInternalSearchTerm(searchValue);
329
+ }, [serverSide, searchValue]);
319
330
  const filterValues = serverSide && externalFilterValues != null ? externalFilterValues : internalFilterValues;
320
331
  const externalSortState = useMemo(
321
332
  () => normalizeSortState(columns, externalSort),
@@ -349,15 +360,16 @@ var DataTable = ({
349
360
  const handleSearchChange = useCallback((term) => {
350
361
  setInternalSearchTerm(term);
351
362
  resetPage();
363
+ const dispatch = () => {
364
+ lastAppliedSearchRef.current = term;
365
+ fireSearchCallback(term);
366
+ fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
367
+ };
352
368
  if (searchDebounce > 0) {
353
369
  if (debounceRef.current) clearTimeout(debounceRef.current);
354
- debounceRef.current = setTimeout(() => {
355
- fireSearchCallback(term);
356
- fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
357
- }, searchDebounce);
370
+ debounceRef.current = setTimeout(dispatch, searchDebounce);
358
371
  } else {
359
- fireSearchCallback(term);
360
- fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
372
+ dispatch();
361
373
  }
362
374
  }, [searchDebounce, fireSearchCallback, fireParamsChange, resetPage, resetPageOnChange]);
363
375
  useEffect(() => () => {
@@ -439,10 +451,23 @@ var DataTable = ({
439
451
  if (serverSide) return filteredData;
440
452
  const activeField = Object.keys(sortState).find((k) => sortState[k] !== "none");
441
453
  if (!activeField) return filteredData;
454
+ const activeCol = columns.find((c) => c.field === activeField);
455
+ const sortOrder = Array.isArray(activeCol == null ? void 0 : activeCol.sortOrder) ? activeCol.sortOrder : null;
456
+ const sortOrderIndex = (val) => {
457
+ const idx = sortOrder.indexOf(val);
458
+ return idx === -1 ? sortOrder.length : idx;
459
+ };
442
460
  return [...filteredData].sort((a, b) => {
443
461
  const dir = sortState[activeField] === "ascending" ? 1 : -1;
444
462
  const aVal = a[activeField];
445
463
  const bVal = b[activeField];
464
+ if (typeof (activeCol == null ? void 0 : activeCol.sortComparator) === "function") {
465
+ return dir * activeCol.sortComparator(aVal, bVal, a, b);
466
+ }
467
+ if (sortOrder) {
468
+ const diff = sortOrderIndex(aVal) - sortOrderIndex(bVal);
469
+ if (diff !== 0) return dir * diff;
470
+ }
446
471
  if (aVal == null && bVal == null) return 0;
447
472
  if (aVal == null) return 1;
448
473
  if (bVal == null) return -1;
@@ -450,7 +475,7 @@ var DataTable = ({
450
475
  if (aVal > bVal) return dir;
451
476
  return 0;
452
477
  });
453
- }, [filteredData, sortState, serverSide]);
478
+ }, [filteredData, sortState, serverSide, columns]);
454
479
  const groupedData = useMemo(() => {
455
480
  if (!groupBy) return null;
456
481
  const source = serverSide ? data : sortedData;
@@ -502,28 +527,33 @@ var DataTable = ({
502
527
  return next;
503
528
  });
504
529
  }, []);
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;
530
+ const datasetRows = useMemo(() => {
531
+ if (!groupedData) return serverSide ? data : sortedData;
532
+ return groupedData.flatMap((group) => group.rows);
533
+ }, [groupedData, sortedData, data, serverSide]);
534
+ const totalItems = serverSide ? totalCount || data.length : datasetRows.length;
517
535
  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(
536
+ const paginatedRows = useMemo(() => {
537
+ if (serverSide) return datasetRows;
538
+ return datasetRows.slice(
523
539
  (activePage - 1) * pageSize,
524
540
  activePage * pageSize
525
541
  );
526
- }
542
+ }, [serverSide, datasetRows, activePage, pageSize]);
543
+ const displayRows = useMemo(() => {
544
+ if (!groupedData) return paginatedRows.map((row) => ({ type: "data", row }));
545
+ const pageRows = new Set(paginatedRows);
546
+ const rows = [];
547
+ groupedData.forEach((group) => {
548
+ const groupPageRows = group.rows.filter((row) => pageRows.has(row));
549
+ if (groupPageRows.length === 0) return;
550
+ rows.push({ type: "group-header", group });
551
+ if (expandedGroups.has(group.key)) {
552
+ groupPageRows.forEach((row) => rows.push({ type: "data", row }));
553
+ }
554
+ });
555
+ return rows;
556
+ }, [groupedData, paginatedRows, expandedGroups]);
527
557
  const footerData = serverSide ? data : filteredData;
528
558
  const activeChips = useMemo(() => {
529
559
  const chips = [];
@@ -634,8 +664,8 @@ var DataTable = ({
634
664
  return displayRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null);
635
665
  }, [serverSide, data, displayRows, rowIdField]);
636
666
  const allRowIds = useMemo(
637
- () => flatRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null),
638
- [flatRows, rowIdField]
667
+ () => datasetRows.map((row) => row[rowIdField]).filter((id) => id != null),
668
+ [datasetRows, rowIdField]
639
669
  );
640
670
  const handleSelectRow = useCallback((rowId, checked) => {
641
671
  const next = new Set(selectedIds);
@@ -682,19 +712,25 @@ var DataTable = ({
682
712
  if (row) onEditStart(row, field, currentValue);
683
713
  }
684
714
  }, [onEditStart, data, rowIdField]);
685
- const commitEdit = useCallback((row, field, value) => {
715
+ const commitEdit = useCallback((row, field, value, options = {}) => {
716
+ const { keepEditing = false } = options;
686
717
  const col = columns.find((c) => c.field === field);
687
718
  if (col == null ? void 0 : col.editValidate) {
688
719
  const result = col.editValidate(value, row);
689
720
  if (result !== true && result !== void 0 && result !== null) {
690
721
  setEditError(typeof result === "string" ? result : "Invalid value");
691
- return;
722
+ return false;
692
723
  }
693
724
  }
694
725
  if (onRowEdit) onRowEdit(row, field, value);
695
- setEditingCell(null);
696
- setEditValue(null);
726
+ if (!keepEditing) {
727
+ setEditingCell(null);
728
+ setEditValue(null);
729
+ } else {
730
+ setEditValue(value);
731
+ }
697
732
  setEditError(null);
733
+ return true;
698
734
  }, [onRowEdit, columns]);
699
735
  const renderEditControl = (col, row) => {
700
736
  const type = col.editType || "text";
@@ -753,12 +789,12 @@ var DataTable = ({
753
789
  case "datetime":
754
790
  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
791
  const next = { ...editValue, date: val };
756
- setEditValue(next);
757
- if (onRowEdit) onRowEdit(row, col.field, next);
792
+ handleInput(next);
793
+ commitEdit(row, col.field, next, { keepEditing: true });
758
794
  }, onBlur: maybeExitDatetimeEdit }), /* @__PURE__ */ React.createElement(TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: "", value: editValue == null ? void 0 : editValue.time, onChange: (val) => {
759
795
  const next = { ...editValue, time: val };
760
- setEditValue(next);
761
- if (onRowEdit) onRowEdit(row, col.field, next);
796
+ handleInput(next);
797
+ commitEdit(row, col.field, next, { keepEditing: true });
762
798
  }, onBlur: maybeExitDatetimeEdit }));
763
799
  case "toggle":
764
800
  return /* @__PURE__ */ React.createElement(Toggle, { ...extra, name: fieldName, label: "", checked: !!editValue, onChange: commit });
@@ -963,12 +999,12 @@ var DataTable = ({
963
999
  }
964
1000
  );
965
1001
  };
966
- return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ React.createElement(
1002
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ React.createElement(
967
1003
  SearchInput,
968
1004
  {
969
1005
  name: "datatable-search",
970
1006
  placeholder: searchPlaceholder,
971
- value: searchTerm,
1007
+ value: internalSearchTerm,
972
1008
  onChange: handleSearchChange
973
1009
  }
974
1010
  ), filters.slice(0, filterInlineLimit).map(renderFilterControl), filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(
@@ -1771,9 +1807,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1771
1807
  const inputDebounceRef = useRef2(/* @__PURE__ */ new Map());
1772
1808
  const rowKeyRef = useRef2(/* @__PURE__ */ new WeakMap());
1773
1809
  const rowKeyCounterRef = useRef2(0);
1810
+ const controlledBaselineLockedRef = useRef2(false);
1774
1811
  const initialSnapshot = useRef2(null);
1775
1812
  if (initialSnapshot.current === null) {
1776
- initialSnapshot.current = deepClone(computeInitialValues());
1813
+ initialSnapshot.current = deepClone(values != null ? values : computeInitialValues());
1777
1814
  }
1778
1815
  const formValues = values != null ? values : internalValues;
1779
1816
  const formErrors = controlledErrors != null ? controlledErrors : internalErrors;
@@ -1785,6 +1822,10 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1785
1822
  const draftValuesRef = useRef2(null);
1786
1823
  formValuesRef.current = formValues;
1787
1824
  formErrorsRef.current = formErrors;
1825
+ const syncDirtyBaseline = useCallback2((nextValues) => {
1826
+ initialSnapshot.current = deepClone(nextValues || {});
1827
+ prevAutoSaveValues.current = deepClone(nextValues || {});
1828
+ }, []);
1788
1829
  const fieldByName = useMemo2(() => {
1789
1830
  const map = /* @__PURE__ */ new Map();
1790
1831
  for (const field of fields) {
@@ -1861,6 +1902,11 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1861
1902
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1862
1903
  };
1863
1904
  }, []);
1905
+ useEffect2(() => {
1906
+ if (values == null) return;
1907
+ if (controlledBaselineLockedRef.current) return;
1908
+ syncDirtyBaseline(values);
1909
+ }, [values, syncDirtyBaseline]);
1864
1910
  const isDirty = useMemo2(() => {
1865
1911
  return !deepEqual(formValues, initialSnapshot.current);
1866
1912
  }, [formValues]);
@@ -1979,6 +2025,50 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1979
2025
  },
1980
2026
  [fieldTypes]
1981
2027
  );
2028
+ const setRepeaterSubFieldError = useCallback2(
2029
+ (fieldName, rowIdx, subFieldName, errorMessage) => {
2030
+ const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
2031
+ const merged = { ...formErrorsRef.current };
2032
+ if (errorMessage) {
2033
+ merged[key] = errorMessage;
2034
+ } else {
2035
+ delete merged[key];
2036
+ }
2037
+ const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
2038
+ const match = k.match(/\[(\d+)\]\./);
2039
+ const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
2040
+ return { key: k, row };
2041
+ }).sort((a, b) => a.row - b.row);
2042
+ if (subErrors.length > 0) {
2043
+ const first = subErrors[0];
2044
+ merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
2045
+ } else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
2046
+ delete merged[fieldName];
2047
+ }
2048
+ replaceErrors(merged);
2049
+ },
2050
+ [replaceErrors]
2051
+ );
2052
+ const expandValidationFields = useCallback2(
2053
+ (fieldSubset) => {
2054
+ const toValidate = fieldSubset || visibleFields;
2055
+ const expanded = [];
2056
+ for (const field of toValidate) {
2057
+ if (field.type === "fieldGroup" && field.items && field.fields) {
2058
+ for (const item of field.items) {
2059
+ for (const subField of field.fields(item)) {
2060
+ if (subField.visible && !subField.visible(formValues)) continue;
2061
+ expanded.push(subField);
2062
+ }
2063
+ }
2064
+ continue;
2065
+ }
2066
+ expanded.push(field);
2067
+ }
2068
+ return expanded;
2069
+ },
2070
+ [visibleFields, formValues]
2071
+ );
1982
2072
  const validateField = useCallback2(
1983
2073
  (name, value) => {
1984
2074
  const field = fieldByName.get(name);
@@ -1998,7 +2088,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
1998
2088
  );
1999
2089
  const validateVisibleFields = useCallback2(
2000
2090
  (fieldSubset) => {
2001
- const toValidate = fieldSubset || visibleFields;
2091
+ const toValidate = expandValidationFields(fieldSubset);
2002
2092
  const errors = {};
2003
2093
  let hasErrors = false;
2004
2094
  for (const field of toValidate) {
@@ -2018,52 +2108,54 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2018
2108
  }
2019
2109
  return { errors, hasErrors };
2020
2110
  },
2021
- [visibleFields, formValues, validateRepeaterField, fieldTypes, validationMessages]
2111
+ [expandValidationFields, formValues, validateRepeaterField, fieldTypes, validationMessages]
2022
2112
  );
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);
2113
+ const runAsyncValidationTarget = useCallback2(
2114
+ (target) => {
2115
+ const { validationKey, field, value, allValues, applyError } = target || {};
2116
+ if (!field || !validationKey || field.type === "repeater" || field.type === "fieldGroup") return null;
2117
+ const syncError = runValidators(value, field, allValues, fieldTypes, {
2118
+ includeCustomValidators: false,
2119
+ messages: validationMessages
2120
+ });
2121
+ const prevController = asyncAbortRef.current.get(validationKey);
2030
2122
  if (prevController) prevController.abort();
2031
- asyncAbortRef.current.delete(name);
2123
+ asyncAbortRef.current.delete(validationKey);
2032
2124
  setValidatingFields((prev) => {
2033
- if (!prev[name]) return prev;
2125
+ if (!prev[validationKey]) return prev;
2034
2126
  const next = { ...prev };
2035
- delete next[name];
2127
+ delete next[validationKey];
2036
2128
  return next;
2037
2129
  });
2038
2130
  if (syncError) return null;
2039
- const version = (asyncValidationVersionRef.current.get(name) || 0) + 1;
2040
- asyncValidationVersionRef.current.set(name, version);
2131
+ const version = (asyncValidationVersionRef.current.get(validationKey) || 0) + 1;
2132
+ asyncValidationVersionRef.current.set(validationKey, version);
2041
2133
  const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
2042
- if (controller) asyncAbortRef.current.set(name, controller);
2134
+ if (controller) asyncAbortRef.current.set(validationKey, controller);
2043
2135
  let asyncPromises;
2044
2136
  try {
2045
2137
  asyncPromises = collectAsyncValidatorPromises(
2046
- val,
2138
+ value,
2047
2139
  field,
2048
- formValues,
2140
+ allValues,
2049
2141
  controller ? { signal: controller.signal } : void 0
2050
2142
  );
2051
2143
  } catch (err) {
2052
- updateErrors({ [name]: (err == null ? void 0 : err.message) || "Validation failed" });
2144
+ applyError((err == null ? void 0 : err.message) || "Validation failed");
2053
2145
  return null;
2054
2146
  }
2055
2147
  if (asyncPromises.length === 0) {
2056
- asyncAbortRef.current.delete(name);
2148
+ asyncAbortRef.current.delete(validationKey);
2057
2149
  return null;
2058
2150
  }
2059
2151
  const validationPromise = Promise.all(asyncPromises).then(
2060
2152
  (results) => {
2061
- if (asyncValidationVersionRef.current.get(name) !== version) return;
2062
- asyncValidationRef.current.delete(name);
2063
- asyncAbortRef.current.delete(name);
2153
+ if (asyncValidationVersionRef.current.get(validationKey) !== version) return;
2154
+ asyncValidationRef.current.delete(validationKey);
2155
+ asyncAbortRef.current.delete(validationKey);
2064
2156
  setValidatingFields((prev) => {
2065
2157
  const next = { ...prev };
2066
- delete next[name];
2158
+ delete next[validationKey];
2067
2159
  return next;
2068
2160
  });
2069
2161
  let err = null;
@@ -2074,50 +2166,128 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2074
2166
  break;
2075
2167
  }
2076
2168
  }
2077
- updateErrors({ [name]: err });
2169
+ applyError(err);
2078
2170
  },
2079
2171
  (rejection) => {
2080
- if (asyncValidationVersionRef.current.get(name) !== version) return;
2081
- asyncValidationRef.current.delete(name);
2082
- asyncAbortRef.current.delete(name);
2172
+ if (asyncValidationVersionRef.current.get(validationKey) !== version) return;
2173
+ asyncValidationRef.current.delete(validationKey);
2174
+ asyncAbortRef.current.delete(validationKey);
2083
2175
  setValidatingFields((prev) => {
2084
2176
  const next = { ...prev };
2085
- delete next[name];
2177
+ delete next[validationKey];
2086
2178
  return next;
2087
2179
  });
2088
2180
  if (rejection && rejection.name === "AbortError") return;
2089
- updateErrors({ [name]: (rejection == null ? void 0 : rejection.message) || "Validation failed" });
2181
+ applyError((rejection == null ? void 0 : rejection.message) || "Validation failed");
2090
2182
  }
2091
2183
  );
2092
- asyncValidationRef.current.set(name, validationPromise);
2093
- setValidatingFields((prev) => ({ ...prev, [name]: true }));
2184
+ asyncValidationRef.current.set(validationKey, validationPromise);
2185
+ setValidatingFields((prev) => ({ ...prev, [validationKey]: true }));
2094
2186
  return validationPromise;
2095
2187
  },
2096
- [fieldByName, formValues, fieldTypes, updateErrors]
2188
+ [fieldTypes, validationMessages]
2097
2189
  );
2098
- const triggerAsyncValidation = useCallback2(
2190
+ const runAsyncValidation = useCallback2(
2099
2191
  (name, value) => {
2100
2192
  const field = fieldByName.get(name);
2101
- if (!field || field.type === "repeater") return;
2102
- const debounceMs = field.validateDebounce;
2193
+ if (!field || field.type === "repeater" || field.type === "fieldGroup") return null;
2194
+ return runAsyncValidationTarget({
2195
+ validationKey: name,
2196
+ field,
2197
+ value: value != null ? value : formValues[name],
2198
+ allValues: formValues,
2199
+ applyError: (errorMessage) => updateErrors({ [name]: errorMessage })
2200
+ });
2201
+ },
2202
+ [fieldByName, formValues, runAsyncValidationTarget, updateErrors]
2203
+ );
2204
+ const triggerAsyncValidationTarget = useCallback2(
2205
+ (target) => {
2206
+ if (!(target == null ? void 0 : target.field) || !target.validationKey) return;
2207
+ const debounceMs = target.field.validateDebounce;
2103
2208
  if (debounceMs && debounceMs > 0) {
2104
- const existing = debounceTimersRef.current.get(name);
2209
+ const existing = debounceTimersRef.current.get(target.validationKey);
2105
2210
  if (existing) clearTimeout(existing);
2106
2211
  const timer = setTimeout(() => {
2107
- debounceTimersRef.current.delete(name);
2108
- runAsyncValidation(name, value);
2212
+ debounceTimersRef.current.delete(target.validationKey);
2213
+ runAsyncValidationTarget(target);
2109
2214
  }, debounceMs);
2110
- debounceTimersRef.current.set(name, timer);
2215
+ debounceTimersRef.current.set(target.validationKey, timer);
2111
2216
  } else {
2112
- runAsyncValidation(name, value);
2217
+ runAsyncValidationTarget(target);
2218
+ }
2219
+ },
2220
+ [runAsyncValidationTarget]
2221
+ );
2222
+ const triggerAsyncValidation = useCallback2(
2223
+ (name, value) => {
2224
+ const field = fieldByName.get(name);
2225
+ if (!field || field.type === "repeater" || field.type === "fieldGroup") return;
2226
+ triggerAsyncValidationTarget({
2227
+ validationKey: name,
2228
+ field,
2229
+ value: value != null ? value : formValuesRef.current[name],
2230
+ allValues: formValuesRef.current,
2231
+ applyError: (errorMessage) => updateErrors({ [name]: errorMessage })
2232
+ });
2233
+ },
2234
+ [fieldByName, triggerAsyncValidationTarget, updateErrors]
2235
+ );
2236
+ const getAsyncValidationTargets = useCallback2(
2237
+ (fieldSubset) => {
2238
+ const toValidate = fieldSubset || visibleFields;
2239
+ const targets = [];
2240
+ for (const field of toValidate) {
2241
+ if (field.type === "fieldGroup" && field.items && field.fields) {
2242
+ for (const item of field.items) {
2243
+ for (const subField of field.fields(item)) {
2244
+ if (subField.visible && !subField.visible(formValues)) continue;
2245
+ targets.push({
2246
+ validationKey: subField.name,
2247
+ field: subField,
2248
+ value: formValues[subField.name],
2249
+ allValues: formValues,
2250
+ applyError: (errorMessage) => updateErrors({ [subField.name]: errorMessage })
2251
+ });
2252
+ }
2253
+ }
2254
+ continue;
2255
+ }
2256
+ if (field.type === "repeater") {
2257
+ const rows = Array.isArray(formValues[field.name]) ? formValues[field.name] : [];
2258
+ const subFields = field.fields || [];
2259
+ rows.forEach((row, rowIdx) => {
2260
+ const rowValues = { ...formValues, [field.name]: rows };
2261
+ subFields.forEach((subField) => {
2262
+ if (subField.visible && !subField.visible(rowValues)) return;
2263
+ targets.push({
2264
+ validationKey: getRepeaterErrorKey(field.name, rowIdx, subField.name),
2265
+ field: subField,
2266
+ value: row == null ? void 0 : row[subField.name],
2267
+ allValues: rowValues,
2268
+ applyError: (errorMessage) => setRepeaterSubFieldError(field.name, rowIdx, subField.name, errorMessage)
2269
+ });
2270
+ });
2271
+ });
2272
+ continue;
2273
+ }
2274
+ targets.push({
2275
+ validationKey: field.name,
2276
+ field,
2277
+ value: formValues[field.name],
2278
+ allValues: formValues,
2279
+ applyError: (errorMessage) => updateErrors({ [field.name]: errorMessage })
2280
+ });
2113
2281
  }
2282
+ return targets;
2114
2283
  },
2115
- [fieldByName, runAsyncValidation]
2284
+ [visibleFields, formValues, setRepeaterSubFieldError, updateErrors]
2116
2285
  );
2117
2286
  const commitValues = useCallback2(
2118
2287
  (nextValues) => {
2119
2288
  formValuesRef.current = nextValues;
2120
2289
  if (values != null) {
2290
+ controlledBaselineLockedRef.current = true;
2121
2291
  if (onChange) onChange(nextValues);
2122
2292
  } else {
2123
2293
  setInternalValues(nextValues);
@@ -2135,7 +2305,8 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2135
2305
  [commitValues]
2136
2306
  );
2137
2307
  const handleFieldChange = useCallback2(
2138
- (name, value) => {
2308
+ (name, value, options = {}) => {
2309
+ const { clearNestedErrors = true } = options;
2139
2310
  const newValues = { ...formValuesRef.current, [name]: value };
2140
2311
  const queue = [name];
2141
2312
  const visited = /* @__PURE__ */ new Set();
@@ -2176,9 +2347,11 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2176
2347
  if (formErrorsRef.current[name] != null) {
2177
2348
  clearedErrors[name] = null;
2178
2349
  }
2179
- for (const key of Object.keys(formErrorsRef.current)) {
2180
- if (key.startsWith(`${name}[`)) {
2181
- clearedErrors[key] = null;
2350
+ if (clearNestedErrors) {
2351
+ for (const key of Object.keys(formErrorsRef.current)) {
2352
+ if (key.startsWith(`${name}[`)) {
2353
+ clearedErrors[key] = null;
2354
+ }
2182
2355
  }
2183
2356
  }
2184
2357
  draftValuesRef.current = newValues;
@@ -2247,7 +2420,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2247
2420
  replaceErrors(errors);
2248
2421
  return;
2249
2422
  }
2250
- const asyncSubmitValidations = allVisibleFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
2423
+ const asyncSubmitValidations = getAsyncValidationTargets(allVisibleFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
2251
2424
  if (asyncSubmitValidations.length > 0 || asyncValidationRef.current.size > 0) {
2252
2425
  const pendingValidations = [
2253
2426
  .../* @__PURE__ */ new Set([
@@ -2262,6 +2435,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2262
2435
  const reset = () => {
2263
2436
  const fresh = computeInitialValues();
2264
2437
  if (values == null) setInternalValues(fresh);
2438
+ controlledBaselineLockedRef.current = false;
2265
2439
  replaceErrors({});
2266
2440
  initialSnapshot.current = deepClone(fresh);
2267
2441
  prevAutoSaveValues.current = deepClone(fresh);
@@ -2301,7 +2475,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2301
2475
  if (controlledLoading == null) setInternalLoading(false);
2302
2476
  }
2303
2477
  },
2304
- [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, runAsyncValidation]
2478
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget]
2305
2479
  );
2306
2480
  const handleNext = useCallback2(async () => {
2307
2481
  if (!isMultiStep) return;
@@ -2313,7 +2487,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2313
2487
  replaceErrors({ ...formErrorsRef.current, ...errors });
2314
2488
  return;
2315
2489
  }
2316
- const asyncStepValidations = stepFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
2490
+ const asyncStepValidations = getAsyncValidationTargets(stepFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
2317
2491
  if (asyncStepValidations.length > 0 || asyncValidationRef.current.size > 0) {
2318
2492
  const pendingValidations = [
2319
2493
  .../* @__PURE__ */ new Set([
@@ -2338,7 +2512,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2338
2512
  } else {
2339
2513
  setInternalStep(nextStep);
2340
2514
  }
2341
- }, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, runAsyncValidation]);
2515
+ }, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, getAsyncValidationTargets, runAsyncValidationTarget]);
2342
2516
  const handleBack = useCallback2(() => {
2343
2517
  if (!isMultiStep) return;
2344
2518
  const prevStep = Math.max(currentStep - 1, 0);
@@ -2370,6 +2544,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2370
2544
  reset: () => {
2371
2545
  const fresh = computeInitialValues();
2372
2546
  if (values == null) setInternalValues(fresh);
2547
+ controlledBaselineLockedRef.current = false;
2373
2548
  replaceErrors({});
2374
2549
  initialSnapshot.current = deepClone(fresh);
2375
2550
  prevAutoSaveValues.current = deepClone(fresh);
@@ -2382,30 +2557,6 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2382
2557
  replaceErrors(errors);
2383
2558
  }
2384
2559
  }));
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
2560
  const renderField = (field) => {
2410
2561
  const fieldError = formErrors[field.name] || null;
2411
2562
  const rendered = renderFieldInner(field);
@@ -2885,12 +3036,13 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2885
3036
  const rowValues = { ...formValues, [field.name]: nextRows };
2886
3037
  const err = runValidators(subValue, subField, rowValues, fieldTypes, { messages: validationMessages });
2887
3038
  setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
3039
+ return err;
2888
3040
  };
2889
3041
  const handleSubFieldChange = (rowIdx, subField, subValue) => {
2890
3042
  const updated = rows.map(
2891
3043
  (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
2892
3044
  );
2893
- handleFieldChange(field.name, updated);
3045
+ handleFieldChange(field.name, updated, { clearNestedErrors: false });
2894
3046
  if (validateOnChange) {
2895
3047
  validateSubField(rowIdx, subField, subValue, updated);
2896
3048
  }
@@ -2900,13 +3052,24 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2900
3052
  const nextRows = rows.map(
2901
3053
  (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
2902
3054
  );
2903
- validateSubField(rowIdx, subField, subValue, nextRows);
3055
+ const err = validateSubField(rowIdx, subField, subValue, nextRows);
3056
+ if (err) return;
3057
+ const validationKey = getRepeaterErrorKey(field.name, rowIdx, subField.name);
3058
+ const rowValues = { ...formValues, [field.name]: nextRows };
3059
+ triggerAsyncValidationTarget({
3060
+ validationKey,
3061
+ field: subField,
3062
+ value: subValue,
3063
+ allValues: rowValues,
3064
+ applyError: (errorMessage) => setRepeaterSubFieldError(field.name, rowIdx, subField.name, errorMessage)
3065
+ });
2904
3066
  };
2905
3067
  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
3068
  const sfValue = row[sf.name];
2907
3069
  const sfLabel = rowIdx === 0 ? sf.label : void 0;
2908
3070
  const sfOptions = resolveOptions(sf, { ...formValues, [field.name]: rows });
2909
3071
  const sfError = formErrors[getRepeaterErrorKey(field.name, rowIdx, sf.name)] || null;
3072
+ const validationKey = getRepeaterErrorKey(field.name, rowIdx, sf.name);
2910
3073
  const sfProps = {
2911
3074
  name: `${field.name}-${rowIdx}-${sf.name}`,
2912
3075
  label: sfLabel,
@@ -2915,6 +3078,7 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
2915
3078
  disabled: resolveDisabled(sf, formValues) || isDisabled,
2916
3079
  error: !!sfError,
2917
3080
  validationMessage: sfError || void 0,
3081
+ ...validatingFields[validationKey] ? { loading: true } : {},
2918
3082
  ...sf.fieldProps || {}
2919
3083
  };
2920
3084
  let sfElement;
@@ -3343,8 +3507,211 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
3343
3507
  formContent
3344
3508
  );
3345
3509
  });
3510
+
3511
+ // src/common-components/AutoTag.js
3512
+ import React3 from "react";
3513
+ import { Tag as Tag2 } from "@hubspot/ui-extensions";
3514
+
3515
+ // src/utils/tagVariants.js
3516
+ var DEFAULT_VARIANT = "default";
3517
+ var DANGER_VARIANT = "danger";
3518
+ var ERROR_VARIANT = "error";
3519
+ var SUCCESS_MATCHERS = [
3520
+ "active",
3521
+ "success",
3522
+ "succeeded",
3523
+ "complete",
3524
+ "completed",
3525
+ "approved",
3526
+ "won",
3527
+ "healthy",
3528
+ "enabled",
3529
+ "connected",
3530
+ "paid",
3531
+ "live",
3532
+ "published",
3533
+ "available",
3534
+ "synced",
3535
+ "resolved"
3536
+ ];
3537
+ var WARNING_MATCHERS = [
3538
+ "warning",
3539
+ "at risk",
3540
+ "risky",
3541
+ "pending",
3542
+ "paused",
3543
+ "pause",
3544
+ "on hold",
3545
+ "hold",
3546
+ "review",
3547
+ "expiring",
3548
+ "trial",
3549
+ "in progress",
3550
+ "awaiting",
3551
+ "scheduled"
3552
+ ];
3553
+ var DANGER_MATCHERS = [
3554
+ "danger",
3555
+ "error",
3556
+ "failed",
3557
+ "failure",
3558
+ "inactive",
3559
+ "disabled",
3560
+ "blocked",
3561
+ "cancelled",
3562
+ "canceled",
3563
+ "rejected",
3564
+ "denied",
3565
+ "churned",
3566
+ "lost",
3567
+ "overdue",
3568
+ "expired",
3569
+ "offline",
3570
+ "deleted",
3571
+ "archived",
3572
+ "unpaid"
3573
+ ];
3574
+ var INFO_MATCHERS = [
3575
+ "info",
3576
+ "new",
3577
+ "queued",
3578
+ "processing",
3579
+ "progress",
3580
+ "upcoming",
3581
+ "draft",
3582
+ "open"
3583
+ ];
3584
+ var normalizeTagValue = (value) => {
3585
+ if (value == null) return "";
3586
+ if (typeof value === "boolean") return value ? "true" : "false";
3587
+ return String(value).trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
3588
+ };
3589
+ var matchesAny = (value, matchers) => matchers.some((matcher) => {
3590
+ if (value === matcher) return true;
3591
+ return ` ${value} `.includes(` ${matcher} `);
3592
+ });
3593
+ var getSemanticVariant = (value, options = {}) => {
3594
+ const normalized = normalizeTagValue(value);
3595
+ const fallback = options.fallback || DEFAULT_VARIANT;
3596
+ if (!normalized) return fallback;
3597
+ if (options.overrides) {
3598
+ const overrideKey = Object.keys(options.overrides).find(
3599
+ (key) => normalizeTagValue(key) === normalized
3600
+ );
3601
+ if (overrideKey) return options.overrides[overrideKey];
3602
+ }
3603
+ if (normalized === "true") return "success";
3604
+ if (normalized === "false") return fallback;
3605
+ if (matchesAny(normalized, SUCCESS_MATCHERS)) return "success";
3606
+ if (matchesAny(normalized, DANGER_MATCHERS)) return DANGER_VARIANT;
3607
+ if (matchesAny(normalized, WARNING_MATCHERS)) return "warning";
3608
+ if (matchesAny(normalized, INFO_MATCHERS)) return "info";
3609
+ return fallback;
3610
+ };
3611
+ var getAutoTagVariant = (value, options = {}) => {
3612
+ const semanticVariant = getSemanticVariant(value, options);
3613
+ return semanticVariant === DANGER_VARIANT ? ERROR_VARIANT : semanticVariant;
3614
+ };
3615
+ var getAutoStatusTagVariant = (value, options = {}) => getSemanticVariant(value, options);
3616
+ var getAutoTagDisplayValue = (value) => {
3617
+ if (typeof value === "boolean") return value ? "True" : "False";
3618
+ return value;
3619
+ };
3620
+ var DEFAULT_STATUS_TAG_COLOR_ORDER = [
3621
+ "success",
3622
+ "warning",
3623
+ "danger",
3624
+ "error",
3625
+ "info",
3626
+ "default"
3627
+ ];
3628
+ var createStatusTagSortComparator = (options = {}) => {
3629
+ const {
3630
+ variantOrder = DEFAULT_STATUS_TAG_COLOR_ORDER,
3631
+ overrides,
3632
+ fallback,
3633
+ getLabel
3634
+ } = options;
3635
+ const variantIndex = (variant) => {
3636
+ const idx = variantOrder.indexOf(variant);
3637
+ return idx === -1 ? variantOrder.length : idx;
3638
+ };
3639
+ const labelOf = (value) => {
3640
+ if (getLabel) return String(getLabel(value) ?? "");
3641
+ if (value == null) return "";
3642
+ return String(getAutoTagDisplayValue(value) ?? "");
3643
+ };
3644
+ return (aVal, bVal) => {
3645
+ const aVariant = getSemanticVariant(aVal, { overrides, fallback });
3646
+ const bVariant = getSemanticVariant(bVal, { overrides, fallback });
3647
+ const diff = variantIndex(aVariant) - variantIndex(bVariant);
3648
+ if (diff !== 0) return diff;
3649
+ return labelOf(aVal).localeCompare(labelOf(bVal));
3650
+ };
3651
+ };
3652
+
3653
+ // src/common-components/AutoTag.js
3654
+ var AutoTag = ({
3655
+ value,
3656
+ tag,
3657
+ children,
3658
+ variant,
3659
+ overrides,
3660
+ fallback,
3661
+ ...props
3662
+ }) => {
3663
+ const resolvedValue = value ?? tag ?? children;
3664
+ const displayValue = children ?? getAutoTagDisplayValue(resolvedValue);
3665
+ const resolvedVariant = variant || getAutoTagVariant(resolvedValue, {
3666
+ overrides,
3667
+ fallback
3668
+ });
3669
+ return React3.createElement(
3670
+ Tag2,
3671
+ { variant: resolvedVariant, ...props },
3672
+ displayValue
3673
+ );
3674
+ };
3675
+
3676
+ // src/common-components/AutoStatusTag.js
3677
+ import React4 from "react";
3678
+ import { StatusTag } from "@hubspot/ui-extensions";
3679
+ var AutoStatusTag = ({
3680
+ value,
3681
+ status,
3682
+ children,
3683
+ variant,
3684
+ overrides,
3685
+ fallback,
3686
+ ...props
3687
+ }) => {
3688
+ const resolvedValue = value ?? status ?? children;
3689
+ const displayValue = children ?? getAutoTagDisplayValue(resolvedValue);
3690
+ const resolvedVariant = variant || getAutoStatusTagVariant(resolvedValue, {
3691
+ overrides,
3692
+ fallback
3693
+ });
3694
+ return React4.createElement(
3695
+ StatusTag,
3696
+ { variant: resolvedVariant, ...props },
3697
+ displayValue
3698
+ );
3699
+ };
3700
+
3701
+ // src/common-components/KeyValueList.js
3702
+ import React5 from "react";
3703
+ import { DescriptionList, DescriptionListItem, Flex as Flex3 } from "@hubspot/ui-extensions";
3704
+
3705
+ // src/common-components/SectionHeader.js
3706
+ import React6 from "react";
3707
+ import { Flex as Flex4, Heading, Text as Text3 } from "@hubspot/ui-extensions";
3346
3708
  export {
3709
+ AutoStatusTag,
3710
+ AutoTag,
3347
3711
  DataTable,
3348
3712
  FormBuilder,
3713
+ createStatusTagSortComparator,
3714
+ getAutoStatusTagVariant,
3715
+ getAutoTagVariant,
3349
3716
  useFormPrefill
3350
3717
  };