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.js CHANGED
@@ -29,8 +29,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // src/index.js
30
30
  var src_exports = {};
31
31
  __export(src_exports, {
32
+ AutoStatusTag: () => AutoStatusTag,
33
+ AutoTag: () => AutoTag,
32
34
  DataTable: () => DataTable,
33
35
  FormBuilder: () => FormBuilder,
36
+ createStatusTagSortComparator: () => createStatusTagSortComparator,
37
+ getAutoStatusTagVariant: () => getAutoStatusTagVariant,
38
+ getAutoTagVariant: () => getAutoTagVariant,
34
39
  useFormPrefill: () => useFormPrefill
35
40
  });
36
41
  module.exports = __toCommonJS(src_exports);
@@ -311,7 +316,9 @@ var DataTable = ({
311
316
  const initialSortState = (0, import_react.useMemo)(() => {
312
317
  return normalizeSortState(columns, defaultSort);
313
318
  }, [columns, defaultSort]);
314
- const [internalSearchTerm, setInternalSearchTerm] = (0, import_react.useState)("");
319
+ const [internalSearchTerm, setInternalSearchTerm] = (0, import_react.useState)(
320
+ () => serverSide && searchValue != null ? searchValue : ""
321
+ );
315
322
  const [internalFilterValues, setInternalFilterValues] = (0, import_react.useState)(() => {
316
323
  const init = {};
317
324
  filters.forEach((f) => {
@@ -322,7 +329,16 @@ var DataTable = ({
322
329
  const [internalSortState, setInternalSortState] = (0, import_react.useState)(initialSortState);
323
330
  const [currentPage, setCurrentPage] = (0, import_react.useState)(1);
324
331
  const [showMoreFilters, setShowMoreFilters] = (0, import_react.useState)(false);
332
+ const lastAppliedSearchRef = (0, import_react.useRef)(
333
+ serverSide && searchValue != null ? searchValue : ""
334
+ );
325
335
  const searchTerm = serverSide && searchValue != null ? searchValue : internalSearchTerm;
336
+ (0, import_react.useEffect)(() => {
337
+ if (!serverSide || searchValue == null) return;
338
+ if (searchValue === lastAppliedSearchRef.current) return;
339
+ lastAppliedSearchRef.current = searchValue;
340
+ setInternalSearchTerm(searchValue);
341
+ }, [serverSide, searchValue]);
326
342
  const filterValues = serverSide && externalFilterValues != null ? externalFilterValues : internalFilterValues;
327
343
  const externalSortState = (0, import_react.useMemo)(
328
344
  () => normalizeSortState(columns, externalSort),
@@ -356,15 +372,16 @@ var DataTable = ({
356
372
  const handleSearchChange = (0, import_react.useCallback)((term) => {
357
373
  setInternalSearchTerm(term);
358
374
  resetPage();
375
+ const dispatch = () => {
376
+ lastAppliedSearchRef.current = term;
377
+ fireSearchCallback(term);
378
+ fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
379
+ };
359
380
  if (searchDebounce > 0) {
360
381
  if (debounceRef.current) clearTimeout(debounceRef.current);
361
- debounceRef.current = setTimeout(() => {
362
- fireSearchCallback(term);
363
- fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
364
- }, searchDebounce);
382
+ debounceRef.current = setTimeout(dispatch, searchDebounce);
365
383
  } else {
366
- fireSearchCallback(term);
367
- fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
384
+ dispatch();
368
385
  }
369
386
  }, [searchDebounce, fireSearchCallback, fireParamsChange, resetPage, resetPageOnChange]);
370
387
  (0, import_react.useEffect)(() => () => {
@@ -446,10 +463,23 @@ var DataTable = ({
446
463
  if (serverSide) return filteredData;
447
464
  const activeField = Object.keys(sortState).find((k) => sortState[k] !== "none");
448
465
  if (!activeField) return filteredData;
466
+ const activeCol = columns.find((c) => c.field === activeField);
467
+ const sortOrder = Array.isArray(activeCol == null ? void 0 : activeCol.sortOrder) ? activeCol.sortOrder : null;
468
+ const sortOrderIndex = (val) => {
469
+ const idx = sortOrder.indexOf(val);
470
+ return idx === -1 ? sortOrder.length : idx;
471
+ };
449
472
  return [...filteredData].sort((a, b) => {
450
473
  const dir = sortState[activeField] === "ascending" ? 1 : -1;
451
474
  const aVal = a[activeField];
452
475
  const bVal = b[activeField];
476
+ if (typeof (activeCol == null ? void 0 : activeCol.sortComparator) === "function") {
477
+ return dir * activeCol.sortComparator(aVal, bVal, a, b);
478
+ }
479
+ if (sortOrder) {
480
+ const diff = sortOrderIndex(aVal) - sortOrderIndex(bVal);
481
+ if (diff !== 0) return dir * diff;
482
+ }
453
483
  if (aVal == null && bVal == null) return 0;
454
484
  if (aVal == null) return 1;
455
485
  if (bVal == null) return -1;
@@ -457,7 +487,7 @@ var DataTable = ({
457
487
  if (aVal > bVal) return dir;
458
488
  return 0;
459
489
  });
460
- }, [filteredData, sortState, serverSide]);
490
+ }, [filteredData, sortState, serverSide, columns]);
461
491
  const groupedData = (0, import_react.useMemo)(() => {
462
492
  if (!groupBy) return null;
463
493
  const source = serverSide ? data : sortedData;
@@ -509,28 +539,33 @@ var DataTable = ({
509
539
  return next;
510
540
  });
511
541
  }, []);
512
- const flatRows = (0, import_react.useMemo)(() => {
513
- if (!groupedData) return (serverSide ? data : sortedData).map((row) => ({ type: "data", row }));
514
- const flat = [];
515
- groupedData.forEach((group) => {
516
- flat.push({ type: "group-header", group });
517
- if (expandedGroups.has(group.key)) {
518
- group.rows.forEach((row) => flat.push({ type: "data", row }));
519
- }
520
- });
521
- return flat;
522
- }, [groupedData, sortedData, data, serverSide, expandedGroups]);
523
- const totalItems = serverSide ? totalCount || data.length : flatRows.length;
542
+ const datasetRows = (0, import_react.useMemo)(() => {
543
+ if (!groupedData) return serverSide ? data : sortedData;
544
+ return groupedData.flatMap((group) => group.rows);
545
+ }, [groupedData, sortedData, data, serverSide]);
546
+ const totalItems = serverSide ? totalCount || data.length : datasetRows.length;
524
547
  const pageCount = Math.ceil(totalItems / pageSize);
525
- let displayRows;
526
- if (serverSide) {
527
- displayRows = groupBy ? flatRows : data.map((row) => ({ type: "data", row }));
528
- } else {
529
- displayRows = flatRows.slice(
548
+ const paginatedRows = (0, import_react.useMemo)(() => {
549
+ if (serverSide) return datasetRows;
550
+ return datasetRows.slice(
530
551
  (activePage - 1) * pageSize,
531
552
  activePage * pageSize
532
553
  );
533
- }
554
+ }, [serverSide, datasetRows, activePage, pageSize]);
555
+ const displayRows = (0, import_react.useMemo)(() => {
556
+ if (!groupedData) return paginatedRows.map((row) => ({ type: "data", row }));
557
+ const pageRows = new Set(paginatedRows);
558
+ const rows = [];
559
+ groupedData.forEach((group) => {
560
+ const groupPageRows = group.rows.filter((row) => pageRows.has(row));
561
+ if (groupPageRows.length === 0) return;
562
+ rows.push({ type: "group-header", group });
563
+ if (expandedGroups.has(group.key)) {
564
+ groupPageRows.forEach((row) => rows.push({ type: "data", row }));
565
+ }
566
+ });
567
+ return rows;
568
+ }, [groupedData, paginatedRows, expandedGroups]);
534
569
  const footerData = serverSide ? data : filteredData;
535
570
  const activeChips = (0, import_react.useMemo)(() => {
536
571
  const chips = [];
@@ -641,8 +676,8 @@ var DataTable = ({
641
676
  return displayRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null);
642
677
  }, [serverSide, data, displayRows, rowIdField]);
643
678
  const allRowIds = (0, import_react.useMemo)(
644
- () => flatRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null),
645
- [flatRows, rowIdField]
679
+ () => datasetRows.map((row) => row[rowIdField]).filter((id) => id != null),
680
+ [datasetRows, rowIdField]
646
681
  );
647
682
  const handleSelectRow = (0, import_react.useCallback)((rowId, checked) => {
648
683
  const next = new Set(selectedIds);
@@ -689,19 +724,25 @@ var DataTable = ({
689
724
  if (row) onEditStart(row, field, currentValue);
690
725
  }
691
726
  }, [onEditStart, data, rowIdField]);
692
- const commitEdit = (0, import_react.useCallback)((row, field, value) => {
727
+ const commitEdit = (0, import_react.useCallback)((row, field, value, options = {}) => {
728
+ const { keepEditing = false } = options;
693
729
  const col = columns.find((c) => c.field === field);
694
730
  if (col == null ? void 0 : col.editValidate) {
695
731
  const result = col.editValidate(value, row);
696
732
  if (result !== true && result !== void 0 && result !== null) {
697
733
  setEditError(typeof result === "string" ? result : "Invalid value");
698
- return;
734
+ return false;
699
735
  }
700
736
  }
701
737
  if (onRowEdit) onRowEdit(row, field, value);
702
- setEditingCell(null);
703
- setEditValue(null);
738
+ if (!keepEditing) {
739
+ setEditingCell(null);
740
+ setEditValue(null);
741
+ } else {
742
+ setEditValue(value);
743
+ }
704
744
  setEditError(null);
745
+ return true;
705
746
  }, [onRowEdit, columns]);
706
747
  const renderEditControl = (col, row) => {
707
748
  const type = col.editType || "text";
@@ -760,12 +801,12 @@ var DataTable = ({
760
801
  case "datetime":
761
802
  return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.DateInput, { ...extra, name: `${fieldName}-date`, label: "", value: editValue == null ? void 0 : editValue.date, onChange: (val) => {
762
803
  const next = { ...editValue, date: val };
763
- setEditValue(next);
764
- if (onRowEdit) onRowEdit(row, col.field, next);
804
+ handleInput(next);
805
+ commitEdit(row, col.field, next, { keepEditing: true });
765
806
  }, onBlur: maybeExitDatetimeEdit }), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: "", value: editValue == null ? void 0 : editValue.time, onChange: (val) => {
766
807
  const next = { ...editValue, time: val };
767
- setEditValue(next);
768
- if (onRowEdit) onRowEdit(row, col.field, next);
808
+ handleInput(next);
809
+ commitEdit(row, col.field, next, { keepEditing: true });
769
810
  }, onBlur: maybeExitDatetimeEdit }));
770
811
  case "toggle":
771
812
  return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Toggle, { ...extra, name: fieldName, label: "", checked: !!editValue, onChange: commit });
@@ -970,12 +1011,12 @@ var DataTable = ({
970
1011
  }
971
1012
  );
972
1013
  };
973
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ import_react.default.createElement(
1014
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ import_react.default.createElement(
974
1015
  import_ui_extensions.SearchInput,
975
1016
  {
976
1017
  name: "datatable-search",
977
1018
  placeholder: searchPlaceholder,
978
- value: searchTerm,
1019
+ value: internalSearchTerm,
979
1020
  onChange: handleSearchChange
980
1021
  }
981
1022
  ), filters.slice(0, filterInlineLimit).map(renderFilterControl), filters.length > filterInlineLimit && /* @__PURE__ */ import_react.default.createElement(
@@ -1738,9 +1779,10 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1738
1779
  const inputDebounceRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1739
1780
  const rowKeyRef = (0, import_react2.useRef)(/* @__PURE__ */ new WeakMap());
1740
1781
  const rowKeyCounterRef = (0, import_react2.useRef)(0);
1782
+ const controlledBaselineLockedRef = (0, import_react2.useRef)(false);
1741
1783
  const initialSnapshot = (0, import_react2.useRef)(null);
1742
1784
  if (initialSnapshot.current === null) {
1743
- initialSnapshot.current = deepClone(computeInitialValues());
1785
+ initialSnapshot.current = deepClone(values != null ? values : computeInitialValues());
1744
1786
  }
1745
1787
  const formValues = values != null ? values : internalValues;
1746
1788
  const formErrors = controlledErrors != null ? controlledErrors : internalErrors;
@@ -1752,6 +1794,10 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1752
1794
  const draftValuesRef = (0, import_react2.useRef)(null);
1753
1795
  formValuesRef.current = formValues;
1754
1796
  formErrorsRef.current = formErrors;
1797
+ const syncDirtyBaseline = (0, import_react2.useCallback)((nextValues) => {
1798
+ initialSnapshot.current = deepClone(nextValues || {});
1799
+ prevAutoSaveValues.current = deepClone(nextValues || {});
1800
+ }, []);
1755
1801
  const fieldByName = (0, import_react2.useMemo)(() => {
1756
1802
  const map = /* @__PURE__ */ new Map();
1757
1803
  for (const field of fields) {
@@ -1828,6 +1874,11 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1828
1874
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1829
1875
  };
1830
1876
  }, []);
1877
+ (0, import_react2.useEffect)(() => {
1878
+ if (values == null) return;
1879
+ if (controlledBaselineLockedRef.current) return;
1880
+ syncDirtyBaseline(values);
1881
+ }, [values, syncDirtyBaseline]);
1831
1882
  const isDirty = (0, import_react2.useMemo)(() => {
1832
1883
  return !deepEqual(formValues, initialSnapshot.current);
1833
1884
  }, [formValues]);
@@ -1946,6 +1997,50 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1946
1997
  },
1947
1998
  [fieldTypes]
1948
1999
  );
2000
+ const setRepeaterSubFieldError = (0, import_react2.useCallback)(
2001
+ (fieldName, rowIdx, subFieldName, errorMessage) => {
2002
+ const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
2003
+ const merged = { ...formErrorsRef.current };
2004
+ if (errorMessage) {
2005
+ merged[key] = errorMessage;
2006
+ } else {
2007
+ delete merged[key];
2008
+ }
2009
+ const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
2010
+ const match = k.match(/\[(\d+)\]\./);
2011
+ const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
2012
+ return { key: k, row };
2013
+ }).sort((a, b) => a.row - b.row);
2014
+ if (subErrors.length > 0) {
2015
+ const first = subErrors[0];
2016
+ merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
2017
+ } else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
2018
+ delete merged[fieldName];
2019
+ }
2020
+ replaceErrors(merged);
2021
+ },
2022
+ [replaceErrors]
2023
+ );
2024
+ const expandValidationFields = (0, import_react2.useCallback)(
2025
+ (fieldSubset) => {
2026
+ const toValidate = fieldSubset || visibleFields;
2027
+ const expanded = [];
2028
+ for (const field of toValidate) {
2029
+ if (field.type === "fieldGroup" && field.items && field.fields) {
2030
+ for (const item of field.items) {
2031
+ for (const subField of field.fields(item)) {
2032
+ if (subField.visible && !subField.visible(formValues)) continue;
2033
+ expanded.push(subField);
2034
+ }
2035
+ }
2036
+ continue;
2037
+ }
2038
+ expanded.push(field);
2039
+ }
2040
+ return expanded;
2041
+ },
2042
+ [visibleFields, formValues]
2043
+ );
1949
2044
  const validateField = (0, import_react2.useCallback)(
1950
2045
  (name, value) => {
1951
2046
  const field = fieldByName.get(name);
@@ -1965,7 +2060,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1965
2060
  );
1966
2061
  const validateVisibleFields = (0, import_react2.useCallback)(
1967
2062
  (fieldSubset) => {
1968
- const toValidate = fieldSubset || visibleFields;
2063
+ const toValidate = expandValidationFields(fieldSubset);
1969
2064
  const errors = {};
1970
2065
  let hasErrors = false;
1971
2066
  for (const field of toValidate) {
@@ -1985,52 +2080,54 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
1985
2080
  }
1986
2081
  return { errors, hasErrors };
1987
2082
  },
1988
- [visibleFields, formValues, validateRepeaterField, fieldTypes, validationMessages]
2083
+ [expandValidationFields, formValues, validateRepeaterField, fieldTypes, validationMessages]
1989
2084
  );
1990
- const runAsyncValidation = (0, import_react2.useCallback)(
1991
- (name, value) => {
1992
- const field = fieldByName.get(name);
1993
- if (!field || field.type === "repeater") return null;
1994
- const val = value != null ? value : formValues[name];
1995
- const syncError = runValidators(val, field, formValues, fieldTypes, { includeCustomValidators: false, messages: validationMessages });
1996
- const prevController = asyncAbortRef.current.get(name);
2085
+ const runAsyncValidationTarget = (0, import_react2.useCallback)(
2086
+ (target) => {
2087
+ const { validationKey, field, value, allValues, applyError } = target || {};
2088
+ if (!field || !validationKey || field.type === "repeater" || field.type === "fieldGroup") return null;
2089
+ const syncError = runValidators(value, field, allValues, fieldTypes, {
2090
+ includeCustomValidators: false,
2091
+ messages: validationMessages
2092
+ });
2093
+ const prevController = asyncAbortRef.current.get(validationKey);
1997
2094
  if (prevController) prevController.abort();
1998
- asyncAbortRef.current.delete(name);
2095
+ asyncAbortRef.current.delete(validationKey);
1999
2096
  setValidatingFields((prev) => {
2000
- if (!prev[name]) return prev;
2097
+ if (!prev[validationKey]) return prev;
2001
2098
  const next = { ...prev };
2002
- delete next[name];
2099
+ delete next[validationKey];
2003
2100
  return next;
2004
2101
  });
2005
2102
  if (syncError) return null;
2006
- const version = (asyncValidationVersionRef.current.get(name) || 0) + 1;
2007
- asyncValidationVersionRef.current.set(name, version);
2103
+ const version = (asyncValidationVersionRef.current.get(validationKey) || 0) + 1;
2104
+ asyncValidationVersionRef.current.set(validationKey, version);
2008
2105
  const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
2009
- if (controller) asyncAbortRef.current.set(name, controller);
2106
+ if (controller) asyncAbortRef.current.set(validationKey, controller);
2010
2107
  let asyncPromises;
2011
2108
  try {
2012
2109
  asyncPromises = collectAsyncValidatorPromises(
2013
- val,
2110
+ value,
2014
2111
  field,
2015
- formValues,
2112
+ allValues,
2016
2113
  controller ? { signal: controller.signal } : void 0
2017
2114
  );
2018
2115
  } catch (err) {
2019
- updateErrors({ [name]: (err == null ? void 0 : err.message) || "Validation failed" });
2116
+ applyError((err == null ? void 0 : err.message) || "Validation failed");
2020
2117
  return null;
2021
2118
  }
2022
2119
  if (asyncPromises.length === 0) {
2023
- asyncAbortRef.current.delete(name);
2120
+ asyncAbortRef.current.delete(validationKey);
2024
2121
  return null;
2025
2122
  }
2026
2123
  const validationPromise = Promise.all(asyncPromises).then(
2027
2124
  (results) => {
2028
- if (asyncValidationVersionRef.current.get(name) !== version) return;
2029
- asyncValidationRef.current.delete(name);
2030
- asyncAbortRef.current.delete(name);
2125
+ if (asyncValidationVersionRef.current.get(validationKey) !== version) return;
2126
+ asyncValidationRef.current.delete(validationKey);
2127
+ asyncAbortRef.current.delete(validationKey);
2031
2128
  setValidatingFields((prev) => {
2032
2129
  const next = { ...prev };
2033
- delete next[name];
2130
+ delete next[validationKey];
2034
2131
  return next;
2035
2132
  });
2036
2133
  let err = null;
@@ -2041,50 +2138,128 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2041
2138
  break;
2042
2139
  }
2043
2140
  }
2044
- updateErrors({ [name]: err });
2141
+ applyError(err);
2045
2142
  },
2046
2143
  (rejection) => {
2047
- if (asyncValidationVersionRef.current.get(name) !== version) return;
2048
- asyncValidationRef.current.delete(name);
2049
- asyncAbortRef.current.delete(name);
2144
+ if (asyncValidationVersionRef.current.get(validationKey) !== version) return;
2145
+ asyncValidationRef.current.delete(validationKey);
2146
+ asyncAbortRef.current.delete(validationKey);
2050
2147
  setValidatingFields((prev) => {
2051
2148
  const next = { ...prev };
2052
- delete next[name];
2149
+ delete next[validationKey];
2053
2150
  return next;
2054
2151
  });
2055
2152
  if (rejection && rejection.name === "AbortError") return;
2056
- updateErrors({ [name]: (rejection == null ? void 0 : rejection.message) || "Validation failed" });
2153
+ applyError((rejection == null ? void 0 : rejection.message) || "Validation failed");
2057
2154
  }
2058
2155
  );
2059
- asyncValidationRef.current.set(name, validationPromise);
2060
- setValidatingFields((prev) => ({ ...prev, [name]: true }));
2156
+ asyncValidationRef.current.set(validationKey, validationPromise);
2157
+ setValidatingFields((prev) => ({ ...prev, [validationKey]: true }));
2061
2158
  return validationPromise;
2062
2159
  },
2063
- [fieldByName, formValues, fieldTypes, updateErrors]
2160
+ [fieldTypes, validationMessages]
2064
2161
  );
2065
- const triggerAsyncValidation = (0, import_react2.useCallback)(
2162
+ const runAsyncValidation = (0, import_react2.useCallback)(
2066
2163
  (name, value) => {
2067
2164
  const field = fieldByName.get(name);
2068
- if (!field || field.type === "repeater") return;
2069
- const debounceMs = field.validateDebounce;
2165
+ if (!field || field.type === "repeater" || field.type === "fieldGroup") return null;
2166
+ return runAsyncValidationTarget({
2167
+ validationKey: name,
2168
+ field,
2169
+ value: value != null ? value : formValues[name],
2170
+ allValues: formValues,
2171
+ applyError: (errorMessage) => updateErrors({ [name]: errorMessage })
2172
+ });
2173
+ },
2174
+ [fieldByName, formValues, runAsyncValidationTarget, updateErrors]
2175
+ );
2176
+ const triggerAsyncValidationTarget = (0, import_react2.useCallback)(
2177
+ (target) => {
2178
+ if (!(target == null ? void 0 : target.field) || !target.validationKey) return;
2179
+ const debounceMs = target.field.validateDebounce;
2070
2180
  if (debounceMs && debounceMs > 0) {
2071
- const existing = debounceTimersRef.current.get(name);
2181
+ const existing = debounceTimersRef.current.get(target.validationKey);
2072
2182
  if (existing) clearTimeout(existing);
2073
2183
  const timer = setTimeout(() => {
2074
- debounceTimersRef.current.delete(name);
2075
- runAsyncValidation(name, value);
2184
+ debounceTimersRef.current.delete(target.validationKey);
2185
+ runAsyncValidationTarget(target);
2076
2186
  }, debounceMs);
2077
- debounceTimersRef.current.set(name, timer);
2187
+ debounceTimersRef.current.set(target.validationKey, timer);
2078
2188
  } else {
2079
- runAsyncValidation(name, value);
2189
+ runAsyncValidationTarget(target);
2190
+ }
2191
+ },
2192
+ [runAsyncValidationTarget]
2193
+ );
2194
+ const triggerAsyncValidation = (0, import_react2.useCallback)(
2195
+ (name, value) => {
2196
+ const field = fieldByName.get(name);
2197
+ if (!field || field.type === "repeater" || field.type === "fieldGroup") return;
2198
+ triggerAsyncValidationTarget({
2199
+ validationKey: name,
2200
+ field,
2201
+ value: value != null ? value : formValuesRef.current[name],
2202
+ allValues: formValuesRef.current,
2203
+ applyError: (errorMessage) => updateErrors({ [name]: errorMessage })
2204
+ });
2205
+ },
2206
+ [fieldByName, triggerAsyncValidationTarget, updateErrors]
2207
+ );
2208
+ const getAsyncValidationTargets = (0, import_react2.useCallback)(
2209
+ (fieldSubset) => {
2210
+ const toValidate = fieldSubset || visibleFields;
2211
+ const targets = [];
2212
+ for (const field of toValidate) {
2213
+ if (field.type === "fieldGroup" && field.items && field.fields) {
2214
+ for (const item of field.items) {
2215
+ for (const subField of field.fields(item)) {
2216
+ if (subField.visible && !subField.visible(formValues)) continue;
2217
+ targets.push({
2218
+ validationKey: subField.name,
2219
+ field: subField,
2220
+ value: formValues[subField.name],
2221
+ allValues: formValues,
2222
+ applyError: (errorMessage) => updateErrors({ [subField.name]: errorMessage })
2223
+ });
2224
+ }
2225
+ }
2226
+ continue;
2227
+ }
2228
+ if (field.type === "repeater") {
2229
+ const rows = Array.isArray(formValues[field.name]) ? formValues[field.name] : [];
2230
+ const subFields = field.fields || [];
2231
+ rows.forEach((row, rowIdx) => {
2232
+ const rowValues = { ...formValues, [field.name]: rows };
2233
+ subFields.forEach((subField) => {
2234
+ if (subField.visible && !subField.visible(rowValues)) return;
2235
+ targets.push({
2236
+ validationKey: getRepeaterErrorKey(field.name, rowIdx, subField.name),
2237
+ field: subField,
2238
+ value: row == null ? void 0 : row[subField.name],
2239
+ allValues: rowValues,
2240
+ applyError: (errorMessage) => setRepeaterSubFieldError(field.name, rowIdx, subField.name, errorMessage)
2241
+ });
2242
+ });
2243
+ });
2244
+ continue;
2245
+ }
2246
+ targets.push({
2247
+ validationKey: field.name,
2248
+ field,
2249
+ value: formValues[field.name],
2250
+ allValues: formValues,
2251
+ applyError: (errorMessage) => updateErrors({ [field.name]: errorMessage })
2252
+ });
2080
2253
  }
2254
+ return targets;
2081
2255
  },
2082
- [fieldByName, runAsyncValidation]
2256
+ [visibleFields, formValues, setRepeaterSubFieldError, updateErrors]
2083
2257
  );
2084
2258
  const commitValues = (0, import_react2.useCallback)(
2085
2259
  (nextValues) => {
2086
2260
  formValuesRef.current = nextValues;
2087
2261
  if (values != null) {
2262
+ controlledBaselineLockedRef.current = true;
2088
2263
  if (onChange) onChange(nextValues);
2089
2264
  } else {
2090
2265
  setInternalValues(nextValues);
@@ -2102,7 +2277,8 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2102
2277
  [commitValues]
2103
2278
  );
2104
2279
  const handleFieldChange = (0, import_react2.useCallback)(
2105
- (name, value) => {
2280
+ (name, value, options = {}) => {
2281
+ const { clearNestedErrors = true } = options;
2106
2282
  const newValues = { ...formValuesRef.current, [name]: value };
2107
2283
  const queue = [name];
2108
2284
  const visited = /* @__PURE__ */ new Set();
@@ -2143,9 +2319,11 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2143
2319
  if (formErrorsRef.current[name] != null) {
2144
2320
  clearedErrors[name] = null;
2145
2321
  }
2146
- for (const key of Object.keys(formErrorsRef.current)) {
2147
- if (key.startsWith(`${name}[`)) {
2148
- clearedErrors[key] = null;
2322
+ if (clearNestedErrors) {
2323
+ for (const key of Object.keys(formErrorsRef.current)) {
2324
+ if (key.startsWith(`${name}[`)) {
2325
+ clearedErrors[key] = null;
2326
+ }
2149
2327
  }
2150
2328
  }
2151
2329
  draftValuesRef.current = newValues;
@@ -2214,7 +2392,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2214
2392
  replaceErrors(errors);
2215
2393
  return;
2216
2394
  }
2217
- const asyncSubmitValidations = allVisibleFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
2395
+ const asyncSubmitValidations = getAsyncValidationTargets(allVisibleFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
2218
2396
  if (asyncSubmitValidations.length > 0 || asyncValidationRef.current.size > 0) {
2219
2397
  const pendingValidations = [
2220
2398
  .../* @__PURE__ */ new Set([
@@ -2229,6 +2407,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2229
2407
  const reset = () => {
2230
2408
  const fresh = computeInitialValues();
2231
2409
  if (values == null) setInternalValues(fresh);
2410
+ controlledBaselineLockedRef.current = false;
2232
2411
  replaceErrors({});
2233
2412
  initialSnapshot.current = deepClone(fresh);
2234
2413
  prevAutoSaveValues.current = deepClone(fresh);
@@ -2268,7 +2447,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2268
2447
  if (controlledLoading == null) setInternalLoading(false);
2269
2448
  }
2270
2449
  },
2271
- [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, runAsyncValidation]
2450
+ [validateOnSubmit, allVisibleFields, validateVisibleFields, replaceErrors, onSubmit, values, controlledLoading, transformValues, onBeforeSubmit, onSubmitSuccess, onSubmitError, resetOnSuccess, formValues, fieldByName, getAsyncValidationTargets, runAsyncValidationTarget]
2272
2451
  );
2273
2452
  const handleNext = (0, import_react2.useCallback)(async () => {
2274
2453
  if (!isMultiStep) return;
@@ -2280,7 +2459,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2280
2459
  replaceErrors({ ...formErrorsRef.current, ...errors });
2281
2460
  return;
2282
2461
  }
2283
- const asyncStepValidations = stepFields.map((field) => runAsyncValidation(field.name, formValues[field.name])).filter(Boolean);
2462
+ const asyncStepValidations = getAsyncValidationTargets(stepFields).map((target) => runAsyncValidationTarget(target)).filter(Boolean);
2284
2463
  if (asyncStepValidations.length > 0 || asyncValidationRef.current.size > 0) {
2285
2464
  const pendingValidations = [
2286
2465
  .../* @__PURE__ */ new Set([
@@ -2305,7 +2484,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2305
2484
  } else {
2306
2485
  setInternalStep(nextStep);
2307
2486
  }
2308
- }, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, runAsyncValidation]);
2487
+ }, [isMultiStep, validateStepOnNext, steps, currentStep, formValues, validateVisibleFields, controlledStep, onStepChange, replaceErrors, allVisibleFields, getAsyncValidationTargets, runAsyncValidationTarget]);
2309
2488
  const handleBack = (0, import_react2.useCallback)(() => {
2310
2489
  if (!isMultiStep) return;
2311
2490
  const prevStep = Math.max(currentStep - 1, 0);
@@ -2337,6 +2516,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2337
2516
  reset: () => {
2338
2517
  const fresh = computeInitialValues();
2339
2518
  if (values == null) setInternalValues(fresh);
2519
+ controlledBaselineLockedRef.current = false;
2340
2520
  replaceErrors({});
2341
2521
  initialSnapshot.current = deepClone(fresh);
2342
2522
  prevAutoSaveValues.current = deepClone(fresh);
@@ -2349,30 +2529,6 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2349
2529
  replaceErrors(errors);
2350
2530
  }
2351
2531
  }));
2352
- const setRepeaterSubFieldError = (0, import_react2.useCallback)(
2353
- (fieldName, rowIdx, subFieldName, errorMessage) => {
2354
- const key = getRepeaterErrorKey(fieldName, rowIdx, subFieldName);
2355
- const merged = { ...formErrorsRef.current };
2356
- if (errorMessage) {
2357
- merged[key] = errorMessage;
2358
- } else {
2359
- delete merged[key];
2360
- }
2361
- const subErrors = Object.keys(merged).filter((k) => k.startsWith(`${fieldName}[`)).map((k) => {
2362
- const match = k.match(/\[(\d+)\]\./);
2363
- const row = match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
2364
- return { key: k, row };
2365
- }).sort((a, b) => a.row - b.row);
2366
- if (subErrors.length > 0) {
2367
- const first = subErrors[0];
2368
- merged[fieldName] = `Row ${first.row + 1}: ${merged[first.key]}`;
2369
- } else if (!merged[fieldName] || merged[fieldName].startsWith("Row ")) {
2370
- delete merged[fieldName];
2371
- }
2372
- replaceErrors(merged);
2373
- },
2374
- [replaceErrors]
2375
- );
2376
2532
  const renderField = (field) => {
2377
2533
  const fieldError = formErrors[field.name] || null;
2378
2534
  const rendered = renderFieldInner(field);
@@ -2852,12 +3008,13 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2852
3008
  const rowValues = { ...formValues, [field.name]: nextRows };
2853
3009
  const err = runValidators(subValue, subField, rowValues, fieldTypes, { messages: validationMessages });
2854
3010
  setRepeaterSubFieldError(field.name, rowIdx, subField.name, err);
3011
+ return err;
2855
3012
  };
2856
3013
  const handleSubFieldChange = (rowIdx, subField, subValue) => {
2857
3014
  const updated = rows.map(
2858
3015
  (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
2859
3016
  );
2860
- handleFieldChange(field.name, updated);
3017
+ handleFieldChange(field.name, updated, { clearNestedErrors: false });
2861
3018
  if (validateOnChange) {
2862
3019
  validateSubField(rowIdx, subField, subValue, updated);
2863
3020
  }
@@ -2867,13 +3024,24 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2867
3024
  const nextRows = rows.map(
2868
3025
  (row, i) => i === rowIdx ? { ...row, [subField.name]: subValue } : row
2869
3026
  );
2870
- validateSubField(rowIdx, subField, subValue, nextRows);
3027
+ const err = validateSubField(rowIdx, subField, subValue, nextRows);
3028
+ if (err) return;
3029
+ const validationKey = getRepeaterErrorKey(field.name, rowIdx, subField.name);
3030
+ const rowValues = { ...formValues, [field.name]: nextRows };
3031
+ triggerAsyncValidationTarget({
3032
+ validationKey,
3033
+ field: subField,
3034
+ value: subValue,
3035
+ allValues: rowValues,
3036
+ applyError: (errorMessage) => setRepeaterSubFieldError(field.name, rowIdx, subField.name, errorMessage)
3037
+ });
2871
3038
  };
2872
3039
  return /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { direction: "column", gap: "xs" }, field.label && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { format: { fontWeight: "demibold" } }, field.label, isRequired ? " *" : ""), field.description && /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Text, { variant: "microcopy" }, field.description), rows.map((row, rowIdx) => /* @__PURE__ */ import_react2.default.createElement(import_ui_extensions2.Flex, { key: getRowKey(field.name, row, rowIdx), direction: "row", gap: "xs", align: "end" }, subFields.map((sf) => {
2873
3040
  const sfValue = row[sf.name];
2874
3041
  const sfLabel = rowIdx === 0 ? sf.label : void 0;
2875
3042
  const sfOptions = resolveOptions(sf, { ...formValues, [field.name]: rows });
2876
3043
  const sfError = formErrors[getRepeaterErrorKey(field.name, rowIdx, sf.name)] || null;
3044
+ const validationKey = getRepeaterErrorKey(field.name, rowIdx, sf.name);
2877
3045
  const sfProps = {
2878
3046
  name: `${field.name}-${rowIdx}-${sf.name}`,
2879
3047
  label: sfLabel,
@@ -2882,6 +3050,7 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
2882
3050
  disabled: resolveDisabled(sf, formValues) || isDisabled,
2883
3051
  error: !!sfError,
2884
3052
  validationMessage: sfError || void 0,
3053
+ ...validatingFields[validationKey] ? { loading: true } : {},
2885
3054
  ...sf.fieldProps || {}
2886
3055
  };
2887
3056
  let sfElement;
@@ -3310,9 +3479,212 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
3310
3479
  formContent
3311
3480
  );
3312
3481
  });
3482
+
3483
+ // src/common-components/AutoTag.js
3484
+ var import_react3 = __toESM(require("react"));
3485
+ var import_ui_extensions3 = require("@hubspot/ui-extensions");
3486
+
3487
+ // src/utils/tagVariants.js
3488
+ var DEFAULT_VARIANT = "default";
3489
+ var DANGER_VARIANT = "danger";
3490
+ var ERROR_VARIANT = "error";
3491
+ var SUCCESS_MATCHERS = [
3492
+ "active",
3493
+ "success",
3494
+ "succeeded",
3495
+ "complete",
3496
+ "completed",
3497
+ "approved",
3498
+ "won",
3499
+ "healthy",
3500
+ "enabled",
3501
+ "connected",
3502
+ "paid",
3503
+ "live",
3504
+ "published",
3505
+ "available",
3506
+ "synced",
3507
+ "resolved"
3508
+ ];
3509
+ var WARNING_MATCHERS = [
3510
+ "warning",
3511
+ "at risk",
3512
+ "risky",
3513
+ "pending",
3514
+ "paused",
3515
+ "pause",
3516
+ "on hold",
3517
+ "hold",
3518
+ "review",
3519
+ "expiring",
3520
+ "trial",
3521
+ "in progress",
3522
+ "awaiting",
3523
+ "scheduled"
3524
+ ];
3525
+ var DANGER_MATCHERS = [
3526
+ "danger",
3527
+ "error",
3528
+ "failed",
3529
+ "failure",
3530
+ "inactive",
3531
+ "disabled",
3532
+ "blocked",
3533
+ "cancelled",
3534
+ "canceled",
3535
+ "rejected",
3536
+ "denied",
3537
+ "churned",
3538
+ "lost",
3539
+ "overdue",
3540
+ "expired",
3541
+ "offline",
3542
+ "deleted",
3543
+ "archived",
3544
+ "unpaid"
3545
+ ];
3546
+ var INFO_MATCHERS = [
3547
+ "info",
3548
+ "new",
3549
+ "queued",
3550
+ "processing",
3551
+ "progress",
3552
+ "upcoming",
3553
+ "draft",
3554
+ "open"
3555
+ ];
3556
+ var normalizeTagValue = (value) => {
3557
+ if (value == null) return "";
3558
+ if (typeof value === "boolean") return value ? "true" : "false";
3559
+ return String(value).trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
3560
+ };
3561
+ var matchesAny = (value, matchers) => matchers.some((matcher) => {
3562
+ if (value === matcher) return true;
3563
+ return ` ${value} `.includes(` ${matcher} `);
3564
+ });
3565
+ var getSemanticVariant = (value, options = {}) => {
3566
+ const normalized = normalizeTagValue(value);
3567
+ const fallback = options.fallback || DEFAULT_VARIANT;
3568
+ if (!normalized) return fallback;
3569
+ if (options.overrides) {
3570
+ const overrideKey = Object.keys(options.overrides).find(
3571
+ (key) => normalizeTagValue(key) === normalized
3572
+ );
3573
+ if (overrideKey) return options.overrides[overrideKey];
3574
+ }
3575
+ if (normalized === "true") return "success";
3576
+ if (normalized === "false") return fallback;
3577
+ if (matchesAny(normalized, SUCCESS_MATCHERS)) return "success";
3578
+ if (matchesAny(normalized, DANGER_MATCHERS)) return DANGER_VARIANT;
3579
+ if (matchesAny(normalized, WARNING_MATCHERS)) return "warning";
3580
+ if (matchesAny(normalized, INFO_MATCHERS)) return "info";
3581
+ return fallback;
3582
+ };
3583
+ var getAutoTagVariant = (value, options = {}) => {
3584
+ const semanticVariant = getSemanticVariant(value, options);
3585
+ return semanticVariant === DANGER_VARIANT ? ERROR_VARIANT : semanticVariant;
3586
+ };
3587
+ var getAutoStatusTagVariant = (value, options = {}) => getSemanticVariant(value, options);
3588
+ var getAutoTagDisplayValue = (value) => {
3589
+ if (typeof value === "boolean") return value ? "True" : "False";
3590
+ return value;
3591
+ };
3592
+ var DEFAULT_STATUS_TAG_COLOR_ORDER = [
3593
+ "success",
3594
+ "warning",
3595
+ "danger",
3596
+ "error",
3597
+ "info",
3598
+ "default"
3599
+ ];
3600
+ var createStatusTagSortComparator = (options = {}) => {
3601
+ const {
3602
+ variantOrder = DEFAULT_STATUS_TAG_COLOR_ORDER,
3603
+ overrides,
3604
+ fallback,
3605
+ getLabel
3606
+ } = options;
3607
+ const variantIndex = (variant) => {
3608
+ const idx = variantOrder.indexOf(variant);
3609
+ return idx === -1 ? variantOrder.length : idx;
3610
+ };
3611
+ const labelOf = (value) => {
3612
+ if (getLabel) return String(getLabel(value) ?? "");
3613
+ if (value == null) return "";
3614
+ return String(getAutoTagDisplayValue(value) ?? "");
3615
+ };
3616
+ return (aVal, bVal) => {
3617
+ const aVariant = getSemanticVariant(aVal, { overrides, fallback });
3618
+ const bVariant = getSemanticVariant(bVal, { overrides, fallback });
3619
+ const diff = variantIndex(aVariant) - variantIndex(bVariant);
3620
+ if (diff !== 0) return diff;
3621
+ return labelOf(aVal).localeCompare(labelOf(bVal));
3622
+ };
3623
+ };
3624
+
3625
+ // src/common-components/AutoTag.js
3626
+ var AutoTag = ({
3627
+ value,
3628
+ tag,
3629
+ children,
3630
+ variant,
3631
+ overrides,
3632
+ fallback,
3633
+ ...props
3634
+ }) => {
3635
+ const resolvedValue = value ?? tag ?? children;
3636
+ const displayValue = children ?? getAutoTagDisplayValue(resolvedValue);
3637
+ const resolvedVariant = variant || getAutoTagVariant(resolvedValue, {
3638
+ overrides,
3639
+ fallback
3640
+ });
3641
+ return import_react3.default.createElement(
3642
+ import_ui_extensions3.Tag,
3643
+ { variant: resolvedVariant, ...props },
3644
+ displayValue
3645
+ );
3646
+ };
3647
+
3648
+ // src/common-components/AutoStatusTag.js
3649
+ var import_react4 = __toESM(require("react"));
3650
+ var import_ui_extensions4 = require("@hubspot/ui-extensions");
3651
+ var AutoStatusTag = ({
3652
+ value,
3653
+ status,
3654
+ children,
3655
+ variant,
3656
+ overrides,
3657
+ fallback,
3658
+ ...props
3659
+ }) => {
3660
+ const resolvedValue = value ?? status ?? children;
3661
+ const displayValue = children ?? getAutoTagDisplayValue(resolvedValue);
3662
+ const resolvedVariant = variant || getAutoStatusTagVariant(resolvedValue, {
3663
+ overrides,
3664
+ fallback
3665
+ });
3666
+ return import_react4.default.createElement(
3667
+ import_ui_extensions4.StatusTag,
3668
+ { variant: resolvedVariant, ...props },
3669
+ displayValue
3670
+ );
3671
+ };
3672
+
3673
+ // src/common-components/KeyValueList.js
3674
+ var import_react5 = __toESM(require("react"));
3675
+ var import_ui_extensions5 = require("@hubspot/ui-extensions");
3676
+
3677
+ // src/common-components/SectionHeader.js
3678
+ var import_react6 = __toESM(require("react"));
3679
+ var import_ui_extensions6 = require("@hubspot/ui-extensions");
3313
3680
  // Annotate the CommonJS export names for ESM import in node:
3314
3681
  0 && (module.exports = {
3682
+ AutoStatusTag,
3683
+ AutoTag,
3315
3684
  DataTable,
3316
3685
  FormBuilder,
3686
+ createStatusTagSortComparator,
3687
+ getAutoStatusTagVariant,
3688
+ getAutoTagVariant,
3317
3689
  useFormPrefill
3318
3690
  });