hs-uix 1.6.4 → 1.7.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.
@@ -1,6 +1,135 @@
1
1
  // packages/datatable/src/DataTable.jsx
2
- import React, { useState, useMemo, useEffect, useCallback, useRef } from "react";
2
+ import React, { useState, useMemo, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
3
+
4
+ // src/utils/query.js
3
5
  import Fuse from "fuse.js";
6
+ var getEmptyFilterValue = (filter) => {
7
+ const type = filter.type || "select";
8
+ if (type === "multiselect") return [];
9
+ if (type === "dateRange") return { from: null, to: null };
10
+ return "";
11
+ };
12
+ var isFilterActive = (filter, value) => {
13
+ const type = filter.type || "select";
14
+ if (type === "multiselect") return Array.isArray(value) && value.length > 0;
15
+ if (type === "dateRange") return value && (value.from || value.to);
16
+ return !!value;
17
+ };
18
+ var formatDateChip = (dateObj) => {
19
+ if (!dateObj) return "";
20
+ const { year, month, date } = dateObj;
21
+ return new Intl.DateTimeFormat("en-US", {
22
+ month: "short",
23
+ day: "numeric",
24
+ year: "numeric"
25
+ }).format(new Date(year, month, date));
26
+ };
27
+ var dateToTimestamp = (dateObj) => {
28
+ if (!dateObj) return null;
29
+ return new Date(dateObj.year, dateObj.month, dateObj.date).getTime();
30
+ };
31
+ var toStableKey = (value) => {
32
+ try {
33
+ return JSON.stringify(value);
34
+ } catch (_error) {
35
+ return String(value);
36
+ }
37
+ };
38
+ var filterRows = (rows, filters, values) => {
39
+ let result = rows;
40
+ for (const filter of filters || []) {
41
+ const value = values[filter.name];
42
+ if (!isFilterActive(filter, value)) continue;
43
+ const type = filter.type || "select";
44
+ if (filter.filterFn) {
45
+ result = result.filter((row) => filter.filterFn(row, value));
46
+ } else if (type === "multiselect") {
47
+ result = result.filter((row) => value.includes(row[filter.name]));
48
+ } else if (type === "dateRange") {
49
+ const fromTs = dateToTimestamp(value.from);
50
+ const toTs = value.to ? dateToTimestamp(value.to) + 864e5 - 1 : null;
51
+ result = result.filter((row) => {
52
+ const rowTs = new Date(row[filter.name]).getTime();
53
+ if (Number.isNaN(rowTs)) return false;
54
+ if (fromTs && rowTs < fromTs) return false;
55
+ if (toTs && rowTs > toTs) return false;
56
+ return true;
57
+ });
58
+ } else {
59
+ result = result.filter((row) => row[filter.name] === value);
60
+ }
61
+ }
62
+ return result;
63
+ };
64
+ var searchRows = (rows, term, fields, opts = {}) => {
65
+ const { fuzzy = false, fuzzyOptions } = opts;
66
+ const t = String(term ?? "").toLowerCase();
67
+ if (!t || !fields || fields.length === 0) return rows;
68
+ if (fuzzy) {
69
+ const fuse = new Fuse(rows, {
70
+ keys: fields,
71
+ threshold: 0.4,
72
+ distance: 100,
73
+ ignoreLocation: true,
74
+ ...fuzzyOptions
75
+ });
76
+ return fuse.search(t).map((r) => r.item);
77
+ }
78
+ return rows.filter(
79
+ (row) => fields.some((field) => {
80
+ const val = row[field];
81
+ return val && String(val).toLowerCase().includes(t);
82
+ })
83
+ );
84
+ };
85
+
86
+ // src/utils/interactionHooks.js
87
+ import { useRef, useEffect, useCallback } from "react";
88
+ import { useDebounce } from "@hubspot/ui-extensions";
89
+ var useDebouncedDispatch = (value, debounceMs, dispatch) => {
90
+ const debounced = useDebounce(value, debounceMs > 0 ? debounceMs : 300);
91
+ const pendingRef = useRef(null);
92
+ useEffect(() => {
93
+ if (debounceMs <= 0) return;
94
+ if (pendingRef.current == null) return;
95
+ if (debounced !== pendingRef.current) return;
96
+ const next = pendingRef.current;
97
+ pendingRef.current = null;
98
+ dispatch(next);
99
+ }, [debounceMs, debounced, dispatch]);
100
+ return useCallback(
101
+ (next) => {
102
+ if (debounceMs > 0) {
103
+ pendingRef.current = next;
104
+ } else {
105
+ pendingRef.current = null;
106
+ dispatch(next);
107
+ }
108
+ },
109
+ [debounceMs, dispatch]
110
+ );
111
+ };
112
+ var useSelectionReset = ({ resetKey, enabled, isControlled, clearSelection }) => {
113
+ const ref = useRef("");
114
+ useEffect(() => {
115
+ if (!enabled || isControlled) {
116
+ ref.current = resetKey;
117
+ return;
118
+ }
119
+ if (ref.current && ref.current !== resetKey) {
120
+ clearSelection();
121
+ }
122
+ ref.current = resetKey;
123
+ }, [resetKey, enabled, isControlled, clearSelection]);
124
+ };
125
+
126
+ // packages/datatable/src/editValidation.js
127
+ var editValidationError = (result) => {
128
+ if (result === true || result === void 0 || result === null) return null;
129
+ return typeof result === "string" ? result : "Invalid value";
130
+ };
131
+
132
+ // packages/datatable/src/DataTable.jsx
4
133
  import {
5
134
  Box,
6
135
  Button,
@@ -10,11 +139,9 @@ import {
10
139
  EmptyState,
11
140
  ErrorState,
12
141
  Flex,
13
- Heading,
14
142
  Icon,
15
143
  Input,
16
144
  Link,
17
- LoadingSpinner,
18
145
  MultiSelect,
19
146
  NumberInput,
20
147
  SearchInput,
@@ -28,25 +155,13 @@ import {
28
155
  TableHeader,
29
156
  TableRow,
30
157
  Tag,
158
+ Tile,
31
159
  Text,
32
160
  TextArea,
33
161
  TimeInput,
34
162
  Toggle,
35
163
  Tooltip
36
164
  } from "@hubspot/ui-extensions";
37
- var formatDateChip = (dateObj) => {
38
- if (!dateObj) return "";
39
- const { year, month, date } = dateObj;
40
- return new Intl.DateTimeFormat("en-US", {
41
- month: "short",
42
- day: "numeric",
43
- year: "numeric"
44
- }).format(new Date(year, month, date));
45
- };
46
- var dateToTimestamp = (dateObj) => {
47
- if (!dateObj) return null;
48
- return new Date(dateObj.year, dateObj.month, dateObj.date).getTime();
49
- };
50
165
  var NARROW_EDIT_TYPES = /* @__PURE__ */ new Set(["checkbox", "toggle"]);
51
166
  var DATE_PATTERN = /^\d{4}[-/]\d{2}[-/]\d{2}/;
52
167
  var BOOL_VALUES = /* @__PURE__ */ new Set(["true", "false", "yes", "no", "0", "1"]);
@@ -72,13 +187,6 @@ var serializeSortState = (sortState) => {
72
187
  if (!activeField) return null;
73
188
  return { field: activeField, direction: sortState[activeField] };
74
189
  };
75
- var toStableKey = (value) => {
76
- try {
77
- return JSON.stringify(value);
78
- } catch (_error) {
79
- return String(value);
80
- }
81
- };
82
190
  var computeAutoWidths = (columns, data) => {
83
191
  if (!data || data.length === 0) return {};
84
192
  const sample = data.slice(0, 50);
@@ -130,12 +238,6 @@ var computeAutoWidths = (columns, data) => {
130
238
  });
131
239
  return results;
132
240
  };
133
- var getEmptyFilterValue = (filter) => {
134
- const type = filter.type || "select";
135
- if (type === "multiselect") return [];
136
- if (type === "dateRange") return { from: null, to: null };
137
- return "";
138
- };
139
241
  var BOOLEAN_SELECT_OPTIONS = [
140
242
  { label: "Yes", value: true },
141
243
  { label: "No", value: false }
@@ -146,12 +248,6 @@ var resolveEditOptions = (col, data) => {
146
248
  if (sample && typeof sample[col.field] === "boolean") return BOOLEAN_SELECT_OPTIONS;
147
249
  return [];
148
250
  };
149
- var isFilterActive = (filter, value) => {
150
- const type = filter.type || "select";
151
- if (type === "multiselect") return Array.isArray(value) && value.length > 0;
152
- if (type === "dateRange") return value && (value.from || value.to);
153
- return !!value;
154
- };
155
251
  var DataTable = ({
156
252
  // Data
157
253
  data,
@@ -170,8 +266,8 @@ var DataTable = ({
170
266
  filters = [],
171
267
  showFilterBadges = true,
172
268
  // show active filter chips/badges
173
- showClearFiltersButton = true,
174
- // show "Clear all" filters reset button
269
+ showClearFiltersButton,
270
+ // show "Clear all" reset button; defaults to showFilterBadges when omitted
175
271
  filterInlineLimit = 2,
176
272
  // number of filters shown inline before overflow
177
273
  // Pagination
@@ -184,7 +280,7 @@ var DataTable = ({
184
280
  // show First/Last page buttons (default: auto when pageCount > 5)
185
281
  // Row count
186
282
  title,
187
- // optional title shown above the table toolbar
283
+ // optional title shown as demibold text above the table toolbar
188
284
  showRowCount = true,
189
285
  // show "X records" / "X of Y records" text
190
286
  rowCountBold = false,
@@ -321,11 +417,11 @@ var DataTable = ({
321
417
  const [internalSortState, setInternalSortState] = useState(initialSortState);
322
418
  const [currentPage, setCurrentPage] = useState(1);
323
419
  const [showMoreFilters, setShowMoreFilters] = useState(false);
324
- const lastAppliedSearchRef = useRef(
420
+ const lastAppliedSearchRef = useRef2(
325
421
  serverSide && searchValue != null ? searchValue : ""
326
422
  );
327
423
  const searchTerm = serverSide && searchValue != null ? searchValue : internalSearchTerm;
328
- useEffect(() => {
424
+ useEffect2(() => {
329
425
  if (!serverSide || searchValue == null) return;
330
426
  if (searchValue === lastAppliedSearchRef.current) return;
331
427
  lastAppliedSearchRef.current = searchValue;
@@ -338,14 +434,13 @@ var DataTable = ({
338
434
  );
339
435
  const sortState = serverSide && externalSort != null ? externalSortState : internalSortState;
340
436
  const activePage = serverSide && externalPage != null ? externalPage : currentPage;
341
- useEffect(() => {
437
+ useEffect2(() => {
342
438
  if (!serverSide) setCurrentPage(1);
343
439
  }, [internalSearchTerm, internalFilterValues, internalSortState, serverSide]);
344
- const debounceRef = useRef(null);
345
- const fireSearchCallback = useCallback((term) => {
440
+ const fireSearchCallback = useCallback2((term) => {
346
441
  if (serverSide && onSearchChange) onSearchChange(term);
347
442
  }, [serverSide, onSearchChange]);
348
- const fireParamsChange = useCallback((overrides) => {
443
+ const fireParamsChange = useCallback2((overrides) => {
349
444
  if (!onParamsChange) return;
350
445
  const nextSortState = overrides.sort != null ? normalizeSortState(columns, overrides.sort) : sortState;
351
446
  onParamsChange({
@@ -355,38 +450,35 @@ var DataTable = ({
355
450
  page: overrides.page != null ? overrides.page : activePage
356
451
  });
357
452
  }, [onParamsChange, columns, searchTerm, filterValues, sortState, activePage]);
358
- const resetPage = useCallback(() => {
453
+ const resetPage = useCallback2(() => {
359
454
  if (resetPageOnChange) {
360
455
  setCurrentPage(1);
361
456
  if (serverSide && onPageChange) onPageChange(1);
362
457
  }
363
458
  }, [resetPageOnChange, serverSide, onPageChange]);
364
- const handleSearchChange = useCallback((term) => {
459
+ const dispatchSearchChange = useCallback2((term) => {
460
+ lastAppliedSearchRef.current = term;
461
+ fireSearchCallback(term);
462
+ fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
463
+ }, [fireSearchCallback, fireParamsChange, resetPageOnChange]);
464
+ const dispatchSearchDebounced = useDebouncedDispatch(
465
+ internalSearchTerm,
466
+ searchDebounce,
467
+ dispatchSearchChange
468
+ );
469
+ const handleSearchChange = useCallback2((term) => {
365
470
  setInternalSearchTerm(term);
366
471
  resetPage();
367
- const dispatch = () => {
368
- lastAppliedSearchRef.current = term;
369
- fireSearchCallback(term);
370
- fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
371
- };
372
- if (searchDebounce > 0) {
373
- if (debounceRef.current) clearTimeout(debounceRef.current);
374
- debounceRef.current = setTimeout(dispatch, searchDebounce);
375
- } else {
376
- dispatch();
377
- }
378
- }, [searchDebounce, fireSearchCallback, fireParamsChange, resetPage, resetPageOnChange]);
379
- useEffect(() => () => {
380
- if (debounceRef.current) clearTimeout(debounceRef.current);
381
- }, []);
382
- const handleFilterChange = useCallback((name, value) => {
472
+ dispatchSearchDebounced(term);
473
+ }, [dispatchSearchDebounced, resetPage]);
474
+ const handleFilterChange = useCallback2((name, value) => {
383
475
  const next = { ...filterValues, [name]: value };
384
476
  setInternalFilterValues(next);
385
477
  if (serverSide && onFilterChange) onFilterChange(next);
386
478
  resetPage();
387
479
  fireParamsChange({ filters: next, page: resetPageOnChange ? 1 : void 0 });
388
480
  }, [filterValues, serverSide, onFilterChange, fireParamsChange, resetPage, resetPageOnChange]);
389
- const handleSortChange = useCallback((field) => {
481
+ const handleSortChange = useCallback2((field) => {
390
482
  const current = sortState[field] || "none";
391
483
  const nextDirection = current === "none" ? "ascending" : current === "ascending" ? "descending" : "none";
392
484
  const reset = {};
@@ -399,55 +491,19 @@ var DataTable = ({
399
491
  resetPage();
400
492
  fireParamsChange({ sort: next, page: resetPageOnChange ? 1 : void 0 });
401
493
  }, [sortState, columns, serverSide, onSortChange, fireParamsChange, resetPage, resetPageOnChange]);
402
- const handlePageChange = useCallback((page) => {
494
+ const handlePageChange = useCallback2((page) => {
403
495
  setCurrentPage(page);
404
496
  if (serverSide && onPageChange) onPageChange(page);
405
497
  fireParamsChange({ page });
406
498
  }, [serverSide, onPageChange, fireParamsChange]);
407
499
  const filteredData = useMemo(() => {
408
500
  if (serverSide) return data;
409
- let result = data;
410
- filters.forEach((filter) => {
411
- const value = filterValues[filter.name];
412
- if (!isFilterActive(filter, value)) return;
413
- const type = filter.type || "select";
414
- if (filter.filterFn) {
415
- result = result.filter((row) => filter.filterFn(row, value));
416
- } else if (type === "multiselect") {
417
- result = result.filter((row) => value.includes(row[filter.name]));
418
- } else if (type === "dateRange") {
419
- const fromTs = dateToTimestamp(value.from);
420
- const toTs = value.to ? dateToTimestamp(value.to) + 864e5 - 1 : null;
421
- result = result.filter((row) => {
422
- const rowTs = new Date(row[filter.name]).getTime();
423
- if (Number.isNaN(rowTs)) return false;
424
- if (fromTs && rowTs < fromTs) return false;
425
- if (toTs && rowTs > toTs) return false;
426
- return true;
427
- });
428
- } else {
429
- result = result.filter((row) => row[filter.name] === value);
430
- }
431
- });
501
+ let result = filterRows(data, filters, filterValues);
432
502
  if (searchTerm && searchFields.length > 0) {
433
- if (fuzzySearch) {
434
- const fuse = new Fuse(result, {
435
- keys: searchFields,
436
- threshold: 0.4,
437
- distance: 100,
438
- ignoreLocation: true,
439
- ...fuzzyOptions
440
- });
441
- result = fuse.search(searchTerm).map((r) => r.item);
442
- } else {
443
- const term = searchTerm.toLowerCase();
444
- result = result.filter(
445
- (row) => searchFields.some((field) => {
446
- const val = row[field];
447
- return val && String(val).toLowerCase().includes(term);
448
- })
449
- );
450
- }
503
+ result = searchRows(result, searchTerm, searchFields, {
504
+ fuzzy: fuzzySearch,
505
+ fuzzyOptions
506
+ });
451
507
  }
452
508
  return result;
453
509
  }, [data, filterValues, searchTerm, filters, searchFields, serverSide, fuzzySearch, fuzzyOptions]);
@@ -512,7 +568,7 @@ var DataTable = ({
512
568
  }
513
569
  return /* @__PURE__ */ new Set();
514
570
  });
515
- useEffect(() => {
571
+ useEffect2(() => {
516
572
  if (!groupedData) return;
517
573
  const defaultExpanded = (groupBy == null ? void 0 : groupBy.defaultExpanded) !== false;
518
574
  if (defaultExpanded) {
@@ -523,7 +579,7 @@ var DataTable = ({
523
579
  });
524
580
  }
525
581
  }, [groupedData, groupBy]);
526
- const toggleGroup = useCallback((key) => {
582
+ const toggleGroup = useCallback2((key) => {
527
583
  setExpandedGroups((prev) => {
528
584
  const next = new Set(prev);
529
585
  if (next.has(key)) next.delete(key);
@@ -584,7 +640,7 @@ var DataTable = ({
584
640
  });
585
641
  return chips;
586
642
  }, [filterValues, filters]);
587
- const handleFilterRemove = useCallback((key) => {
643
+ const handleFilterRemove = useCallback2((key) => {
588
644
  if (key === "all") {
589
645
  const cleared = {};
590
646
  filters.forEach((f) => {
@@ -620,13 +676,14 @@ var DataTable = ({
620
676
  const resolvedDateFromLabel = (labels == null ? void 0 : labels.dateFrom) || "From";
621
677
  const resolvedDateToLabel = (labels == null ? void 0 : labels.dateTo) || "To";
622
678
  const resolvedLoadingLabel = (labels == null ? void 0 : labels.loading) || `Loading ${pluralLabel}...`;
679
+ const resolvedLoadingMessage = (labels == null ? void 0 : labels.loadingMessage) || "This should only take a moment.";
623
680
  const resolvedErrorTitle = (labels == null ? void 0 : labels.errorTitle) || "Something went wrong.";
624
681
  const resolvedErrorMessage = (labels == null ? void 0 : labels.errorMessage) || "An error occurred while loading data.";
625
682
  const resolvedRetryMessage = (labels == null ? void 0 : labels.retryMessage) || "Please try again.";
683
+ const resolvedShowClearFiltersButton = showClearFiltersButton ?? showFilterBadges;
626
684
  const recordCountLabel = rowCountText ? rowCountText(shownOnPageCount, displayCount) : displayCount === totalDataCount ? `${totalDataCount} ${countLabel(totalDataCount)}` : `${displayCount} of ${totalDataCount} ${countLabel(totalDataCount)}`;
627
685
  const [internalSelectedIds, setInternalSelectedIds] = useState(/* @__PURE__ */ new Set());
628
- const selectionResetRef = useRef("");
629
- useEffect(() => {
686
+ useEffect2(() => {
630
687
  if (externalSelectedIds != null) {
631
688
  setInternalSelectedIds(new Set(externalSelectedIds));
632
689
  }
@@ -643,22 +700,21 @@ var DataTable = ({
643
700
  () => `${selectionQueryKey}::${selectionResetKey == null ? "" : toStableKey(selectionResetKey)}`,
644
701
  [selectionQueryKey, selectionResetKey]
645
702
  );
646
- useEffect(() => {
647
- if (!selectable || externalSelectedIds != null) {
648
- selectionResetRef.current = combinedSelectionResetKey;
649
- return;
650
- }
651
- if (selectionResetRef.current && selectionResetRef.current !== combinedSelectionResetKey) {
652
- setInternalSelectedIds(/* @__PURE__ */ new Set());
653
- }
654
- selectionResetRef.current = combinedSelectionResetKey;
655
- }, [combinedSelectionResetKey, selectable, externalSelectedIds]);
703
+ const clearSelection = useCallback2(() => setInternalSelectedIds(/* @__PURE__ */ new Set()), []);
704
+ useSelectionReset({
705
+ resetKey: combinedSelectionResetKey,
706
+ enabled: selectable,
707
+ isControlled: externalSelectedIds != null,
708
+ clearSelection
709
+ });
656
710
  const selectedIds = externalSelectedIds != null ? new Set(externalSelectedIds) : internalSelectedIds;
657
- const showToolbarCount = showRowCount && !title && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
658
- const showTitleCount = showRowCount && !!title && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
659
- const hasToolbarContent = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) || showToolbarCount;
711
+ const showToolbarCount = showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
712
+ const hasToolbarLeft = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || resolvedShowClearFiltersButton);
713
+ const countInTitleRow = !!title && showToolbarCount && !hasToolbarLeft;
714
+ const countInToolbar = showToolbarCount && !countInTitleRow;
715
+ const hasToolbarContent = hasToolbarLeft || countInToolbar;
660
716
  const showRowActionsColumn = !!rowActions && !(hideRowActionsWhenSelectionActive && selectable && selectedIds.size > 0);
661
- const applySelection = useCallback((nextSet) => {
717
+ const applySelection = useCallback2((nextSet) => {
662
718
  if (externalSelectedIds == null) {
663
719
  setInternalSelectedIds(nextSet);
664
720
  }
@@ -674,13 +730,13 @@ var DataTable = ({
674
730
  () => datasetRows.map((row) => row[rowIdField]).filter((id) => id != null),
675
731
  [datasetRows, rowIdField]
676
732
  );
677
- const handleSelectRow = useCallback((rowId, checked) => {
733
+ const handleSelectRow = useCallback2((rowId, checked) => {
678
734
  const next = new Set(selectedIds);
679
735
  if (checked) next.add(rowId);
680
736
  else next.delete(rowId);
681
737
  applySelection(next);
682
738
  }, [selectedIds, applySelection]);
683
- const handleSelectAll = useCallback((checked) => {
739
+ const handleSelectAll = useCallback2((checked) => {
684
740
  const next = new Set(selectedIds);
685
741
  pageRowIds.forEach((id) => {
686
742
  if (checked) next.add(id);
@@ -691,7 +747,7 @@ var DataTable = ({
691
747
  const allVisibleSelected = useMemo(() => {
692
748
  return pageRowIds.length > 0 && pageRowIds.every((id) => selectedIds.has(id));
693
749
  }, [pageRowIds, selectedIds]);
694
- const handleSelectAllRows = useCallback(() => {
750
+ const handleSelectAllRows = useCallback2(() => {
695
751
  const idsToAdd = serverSide ? pageRowIds : allRowIds;
696
752
  const next = new Set(selectedIds);
697
753
  idsToAdd.forEach((id) => next.add(id));
@@ -704,13 +760,13 @@ var DataTable = ({
704
760
  });
705
761
  }
706
762
  }, [serverSide, pageRowIds, allRowIds, selectedIds, applySelection, onSelectAllRequest, totalCount, data.length]);
707
- const handleDeselectAll = useCallback(() => {
763
+ const handleDeselectAll = useCallback2(() => {
708
764
  applySelection(/* @__PURE__ */ new Set());
709
765
  }, [applySelection]);
710
766
  const [editingCell, setEditingCell] = useState(null);
711
767
  const [editValue, setEditValue] = useState(null);
712
768
  const [editError, setEditError] = useState(null);
713
- const startEditing = useCallback((rowId, field, currentValue) => {
769
+ const startEditing = useCallback2((rowId, field, currentValue) => {
714
770
  setEditingCell({ rowId, field });
715
771
  setEditValue(currentValue);
716
772
  setEditError(null);
@@ -719,13 +775,13 @@ var DataTable = ({
719
775
  if (row) onEditStart(row, field, currentValue);
720
776
  }
721
777
  }, [onEditStart, data, rowIdField]);
722
- const commitEdit = useCallback((row, field, value, options = {}) => {
778
+ const commitEdit = useCallback2((row, field, value, options = {}) => {
723
779
  const { keepEditing = false } = options;
724
780
  const col = columns.find((c) => c.field === field);
725
781
  if (col == null ? void 0 : col.editValidate) {
726
- const result = col.editValidate(value, row);
727
- if (result !== true && result !== void 0 && result !== null) {
728
- setEditError(typeof result === "string" ? result : "Invalid value");
782
+ const err = editValidationError(col.editValidate(value, row));
783
+ if (err) {
784
+ setEditError(err);
729
785
  return false;
730
786
  }
731
787
  }
@@ -753,14 +809,7 @@ var DataTable = ({
753
809
  const extra = col.editProps || {};
754
810
  const validate = col.editValidate;
755
811
  const validationProps = validate && editError ? { error: true, validationMessage: editError } : {};
756
- const onInputValidate = validate ? (val) => {
757
- const result = validate(val, row);
758
- if (result !== true && result !== void 0 && result !== null) {
759
- setEditError(typeof result === "string" ? result : "Invalid value");
760
- } else {
761
- setEditError(null);
762
- }
763
- } : void 0;
812
+ const onInputValidate = validate ? (val) => setEditError(editValidationError(validate(val, row))) : void 0;
764
813
  const handleInput = (val) => {
765
814
  setEditValue(val);
766
815
  if (onInputValidate) onInputValidate(val);
@@ -836,9 +885,9 @@ var DataTable = ({
836
885
  const validate = col.editValidate;
837
886
  const fire = (val) => {
838
887
  if (validate) {
839
- const result = validate(val, row);
840
- if (result !== true && result !== void 0 && result !== null) {
841
- setInlineErrors((prev) => ({ ...prev, [cellKey]: typeof result === "string" ? result : "Invalid value" }));
888
+ const err = editValidationError(validate(val, row));
889
+ if (err) {
890
+ setInlineErrors((prev) => ({ ...prev, [cellKey]: err }));
842
891
  return;
843
892
  }
844
893
  setInlineErrors((prev) => {
@@ -853,16 +902,13 @@ var DataTable = ({
853
902
  const cellError = inlineErrors[cellKey];
854
903
  const validationProps = cellError ? { error: true, validationMessage: cellError } : {};
855
904
  const onInputValidate = validate ? (val) => {
856
- const result = validate(val, row);
857
- if (result !== true && result !== void 0 && result !== null) {
858
- setInlineErrors((prev) => ({ ...prev, [cellKey]: typeof result === "string" ? result : "Invalid value" }));
859
- } else {
860
- setInlineErrors((prev) => {
861
- const next = { ...prev };
862
- delete next[cellKey];
863
- return next;
864
- });
865
- }
905
+ const err = editValidationError(validate(val, row));
906
+ setInlineErrors((prev) => {
907
+ if (err) return { ...prev, [cellKey]: err };
908
+ const next = { ...prev };
909
+ delete next[cellKey];
910
+ return next;
911
+ });
866
912
  } : void 0;
867
913
  const emitInput = (val) => {
868
914
  if (onInputValidate) onInputValidate(val);
@@ -1006,7 +1052,7 @@ var DataTable = ({
1006
1052
  }
1007
1053
  );
1008
1054
  };
1009
- return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ React.createElement(Heading, null, title), showTitleCount && /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)), hasToolbarContent && /* @__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(
1055
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, title), countInTitleRow && /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)), hasToolbarContent && /* @__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(
1010
1056
  SearchInput,
1011
1057
  {
1012
1058
  name: "datatable-search",
@@ -1024,7 +1070,7 @@ var DataTable = ({
1024
1070
  /* @__PURE__ */ React.createElement(Icon, { name: "filter", size: "sm" }),
1025
1071
  " ",
1026
1072
  resolvedFiltersButtonLabel
1027
- )), showMoreFilters && filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, filters.slice(filterInlineLimit).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ React.createElement(Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), showClearFiltersButton && /* @__PURE__ */ React.createElement(
1073
+ )), showMoreFilters && filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, filters.slice(filterInlineLimit).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || resolvedShowClearFiltersButton) && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ React.createElement(Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), resolvedShowClearFiltersButton && /* @__PURE__ */ React.createElement(
1028
1074
  Button,
1029
1075
  {
1030
1076
  variant: "transparent",
@@ -1032,7 +1078,7 @@ var DataTable = ({
1032
1078
  onClick: () => handleFilterRemove("all")
1033
1079
  },
1034
1080
  resolvedClearAllLabel
1035
- )))), showToolbarCount && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), showSelectionBar && selectable && selectedIds.size > 0 && (renderSelectionBar ? renderSelectionBar({
1081
+ )))), countInToolbar && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), showSelectionBar && selectable && selectedIds.size > 0 && (renderSelectionBar ? renderSelectionBar({
1036
1082
  selectedIds,
1037
1083
  selectedCount: selectedIds.size,
1038
1084
  displayCount,
@@ -1052,11 +1098,15 @@ var DataTable = ({
1052
1098
  action.icon && /* @__PURE__ */ React.createElement(Icon, { name: action.icon, size: "sm" }),
1053
1099
  " ",
1054
1100
  action.label
1055
- )))), showRowCount && displayCount > 0 && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel))))), loading ? renderLoadingState ? renderLoadingState({ label: resolvedLoadingLabel }) : /* @__PURE__ */ React.createElement(LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? renderErrorState ? renderErrorState({
1101
+ )))), showRowCount && displayCount > 0 && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel))))), loading ? renderLoadingState ? renderLoadingState({ label: resolvedLoadingLabel }) : (
1102
+ // Same EmptyState layout as the empty state, just the "building" image
1103
+ // + a loading message — so loading and empty match with no layout shift.
1104
+ /* @__PURE__ */ React.createElement(Tile, null, /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedLoadingLabel, imageName: "building", layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedLoadingMessage))))
1105
+ ) : error ? renderErrorState ? renderErrorState({
1056
1106
  error,
1057
1107
  title: typeof error === "string" ? error : resolvedErrorTitle,
1058
1108
  message: typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage
1059
- }) : /* @__PURE__ */ React.createElement(ErrorState, { title: typeof error === "string" ? error : resolvedErrorTitle }, /* @__PURE__ */ React.createElement(Text, null, typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage)) : displayRows.length === 0 ? renderEmptyState ? renderEmptyState({ title: resolvedEmptyTitle, message: resolvedEmptyMessage }) : /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedEmptyMessage))) : /* @__PURE__ */ React.createElement(
1109
+ }) : /* @__PURE__ */ React.createElement(ErrorState, { title: typeof error === "string" ? error : resolvedErrorTitle }, /* @__PURE__ */ React.createElement(Text, null, typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage)) : displayRows.length === 0 ? renderEmptyState ? renderEmptyState({ title: resolvedEmptyTitle, message: resolvedEmptyMessage }) : /* @__PURE__ */ React.createElement(Tile, null, /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedEmptyMessage)))) : /* @__PURE__ */ React.createElement(
1060
1110
  Table,
1061
1111
  {
1062
1112
  bordered,