@crispy-seed/data-table 0.4.0 → 0.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.cjs +767 -154
- package/dist/index.d.ts +96 -2
- package/dist/index.js +767 -155
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -878,18 +878,33 @@ function useDataTableColumnCalc(props) {
|
|
|
878
878
|
return next;
|
|
879
879
|
}
|
|
880
880
|
/**
|
|
881
|
-
* Drag-to-reorder:
|
|
882
|
-
* `
|
|
883
|
-
*
|
|
884
|
-
* equal-valued copy) when either key is missing or
|
|
881
|
+
* Drag-to-reorder: moves `dragKey` to `dropKey`'s own ORIGINAL index (the standard
|
|
882
|
+
* `arrayMove` two-splice shape -- remove `dragKey`, then reinsert it at `dropKey`'s
|
|
883
|
+
* pre-removal index of the now-shorter array), the same semantics dnd-kit's own
|
|
884
|
+
* `arrayMove` uses. A no-op (an equal-valued copy) when either key is missing or
|
|
885
|
+
* they're the same key.
|
|
886
|
+
*
|
|
887
|
+
* An EARLIER version of this function instead reinserted `dragKey` immediately before
|
|
888
|
+
* `dropKey`'s post-removal position. That reads the same for a multi-column jump, but
|
|
889
|
+
* is a silent no-op for the single most common drag gesture there is: dragging a
|
|
890
|
+
* column onto its own immediate RIGHT neighbor. Concretely, for `["a","b"]`, dragging
|
|
891
|
+
* "a" onto "b": removing "a" leaves `["b"]`, and "immediately before b" is index 0 --
|
|
892
|
+
* exactly where "a" already was, so nothing moved even though a real drag-and-drop
|
|
893
|
+
* gesture happened (caught by `DataTable.columns.test.tsx`'s own end-to-end drag test,
|
|
894
|
+
* which fires a real `dragover` DOM event and asserts the header order actually
|
|
895
|
+
* changed -- it didn't). This version's two-splice shape lands the dragged column at
|
|
896
|
+
* the target's own original slot instead, which swaps immediate neighbors correctly in
|
|
897
|
+
* both directions and still matches the old multi-jump behavior for a LEFTWARD drag
|
|
898
|
+
* (only a RIGHTWARD multi-column jump now lands one slot further, after the drop
|
|
899
|
+
* target instead of before it -- see this file's own test for both directions).
|
|
885
900
|
*/ function reorderColumnKey(order, dragKey, dropKey) {
|
|
886
901
|
if (dragKey === dropKey || !order.includes(dragKey) || !order.includes(dropKey)) {
|
|
887
902
|
return [
|
|
888
903
|
...order
|
|
889
904
|
];
|
|
890
905
|
}
|
|
906
|
+
const dropIndex = order.indexOf(dropKey);
|
|
891
907
|
const withoutDrag = order.filter((key)=>key !== dragKey);
|
|
892
|
-
const dropIndex = withoutDrag.indexOf(dropKey);
|
|
893
908
|
return [
|
|
894
909
|
...withoutDrag.slice(0, dropIndex),
|
|
895
910
|
dragKey,
|
|
@@ -901,6 +916,24 @@ function useDataTableColumnCalc(props) {
|
|
|
901
916
|
* nor balloon past a sane cap from one fast mouse move). */ function clampColumnWidth(width, min = 80, max = 640) {
|
|
902
917
|
return Math.min(max, Math.max(min, width));
|
|
903
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Reconciles a persisted/controlled PINNED set against the table's current column
|
|
921
|
+
* KEYS -- the same drop-what's-gone rule `reconcileColumnOrder` already applies to
|
|
922
|
+
* `columnOrder`, but pins never need the "append what's new" half of that: an unpinned
|
|
923
|
+
* column simply isn't in the set, appending it here would silently pin every brand-new
|
|
924
|
+
* column by accident. A key no longer in `keys` (hidden isn't "gone" -- only removed
|
|
925
|
+
* from `columns` entirely) is dropped; everything else keeps its order.
|
|
926
|
+
*/ function reconcilePinnedColumns(pinned, keys) {
|
|
927
|
+
const keySet = new Set(keys);
|
|
928
|
+
return pinned.filter((key)=>keySet.has(key));
|
|
929
|
+
}
|
|
930
|
+
/** Pin/unpin: adds `key` if absent, removes it if present -- column-menu.tsx's own
|
|
931
|
+
* "Pin column"/"Unpin column" toggle is one call of this, the same shape
|
|
932
|
+
* `toggleMembership` already gives row selection. Kept as its own named function
|
|
933
|
+
* (rather than reusing `toggleMembership` directly) so a pin-specific doc comment and
|
|
934
|
+
* call sites aren't reading a generically-named helper for a column-specific concern. */ function togglePinnedColumn(pinned, key) {
|
|
935
|
+
return toggleMembership(pinned, key);
|
|
936
|
+
}
|
|
904
937
|
/**
|
|
905
938
|
* Infinite scroll's own gate: fire `onLoadMore` only when the sentinel is ACTUALLY
|
|
906
939
|
* intersecting (not e.g. an observer's initial `false` callback, which fires once on
|
|
@@ -914,19 +947,90 @@ function useDataTableColumnCalc(props) {
|
|
|
914
947
|
*/ function shouldTriggerLoadMore(isIntersecting, hasMore, loadingMore) {
|
|
915
948
|
return hasMore && !loadingMore;
|
|
916
949
|
}
|
|
950
|
+
function isPlainObject(value) {
|
|
951
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
952
|
+
}
|
|
953
|
+
/** Structural validation for ONE parsed view -- every field of `DataTableViewState` is
|
|
954
|
+
* optional, so this only rejects a field that's actually PRESENT but the wrong shape
|
|
955
|
+
* (a consumer's storage from a future/older version of this shape), not a field that's
|
|
956
|
+
* simply missing. */ function isValidDataTableView(value) {
|
|
957
|
+
if (!isPlainObject(value)) return false;
|
|
958
|
+
if (typeof value["id"] !== "string" || typeof value["name"] !== "string") return false;
|
|
959
|
+
if (!isPlainObject(value["state"])) return false;
|
|
960
|
+
const state = value["state"];
|
|
961
|
+
if (state["columnOrder"] !== undefined && !isStringArray(state["columnOrder"])) return false;
|
|
962
|
+
if (state["hiddenColumnKeys"] !== undefined && !isStringArray(state["hiddenColumnKeys"])) return false;
|
|
963
|
+
if (state["pinnedColumnKeys"] !== undefined && !isStringArray(state["pinnedColumnKeys"])) return false;
|
|
964
|
+
if (state["columnWidths"] !== undefined && !isPlainObject(state["columnWidths"])) return false;
|
|
965
|
+
if (state["toolbarSorts"] !== undefined && !Array.isArray(state["toolbarSorts"])) return false;
|
|
966
|
+
if (state["toolbarFilters"] !== undefined && !Array.isArray(state["toolbarFilters"])) return false;
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
function isStringArray(value) {
|
|
970
|
+
return Array.isArray(value) && value.every((entry)=>typeof entry === "string");
|
|
971
|
+
}
|
|
972
|
+
/** JSON-serializes a saved-views list for the default localStorage store -- pure so it
|
|
973
|
+
* can be unit- and mutation-tested without touching `window.localStorage` at all (the
|
|
974
|
+
* hook's own effect, `useDataTableViews.ts`, is the only thing that actually touches
|
|
975
|
+
* storage; this is just the byte-for-byte shape it writes/reads). */ function serializeDataTableViews(views) {
|
|
976
|
+
return JSON.stringify(views);
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Parses a saved-views blob back into a `DataTableView[]`, defensively: malformed JSON,
|
|
980
|
+
* a non-array root, or any single malformed entry drops just that problem (an empty
|
|
981
|
+
* list, or that one entry) rather than throwing -- a consumer's localStorage can hold a
|
|
982
|
+
* PRIOR version's shape after this ships a new field, and one bad/stale entry shouldn't
|
|
983
|
+
* cost every other saved view.
|
|
984
|
+
*/ function deserializeDataTableViews(raw) {
|
|
985
|
+
if (!raw) return [];
|
|
986
|
+
let parsed;
|
|
987
|
+
try {
|
|
988
|
+
parsed = JSON.parse(raw);
|
|
989
|
+
} catch {
|
|
990
|
+
return [];
|
|
991
|
+
}
|
|
992
|
+
if (!Array.isArray(parsed)) return [];
|
|
993
|
+
return parsed.filter(isValidDataTableView);
|
|
994
|
+
}
|
|
995
|
+
/** Inserts `view` (by `id`), or replaces the existing entry with the same `id` in
|
|
996
|
+
* place -- "Save" (new view) and "update the currently loaded view" are both one call
|
|
997
|
+
* of this, matching `moveColumnKey`/`reorderColumnKey`'s own "one pure op per user
|
|
998
|
+
* gesture" shape. */ function upsertDataTableView(views, view) {
|
|
999
|
+
const index = views.findIndex((existing)=>existing.id === view.id);
|
|
1000
|
+
if (index === -1) return [
|
|
1001
|
+
...views,
|
|
1002
|
+
view
|
|
1003
|
+
];
|
|
1004
|
+
const next = [
|
|
1005
|
+
...views
|
|
1006
|
+
];
|
|
1007
|
+
next[index] = view;
|
|
1008
|
+
return next;
|
|
1009
|
+
}
|
|
1010
|
+
/** Renames the view with this `id` -- a no-op (an equal-valued copy) if no view has
|
|
1011
|
+
* that id. */ function renameDataTableView(views, id, name) {
|
|
1012
|
+
return views.map((view)=>view.id === id ? {
|
|
1013
|
+
...view,
|
|
1014
|
+
name
|
|
1015
|
+
} : view);
|
|
1016
|
+
}
|
|
1017
|
+
/** Removes the view with this `id`. */ function removeDataTableView(views, id) {
|
|
1018
|
+
return views.filter((view)=>view.id !== id);
|
|
1019
|
+
}
|
|
917
1020
|
const EMPTY_STRINGS = [];
|
|
918
1021
|
const EMPTY_RECORD = {};
|
|
919
1022
|
/**
|
|
920
1023
|
* Owns everything DATATABLE-ATTIO-SPEC.md's column menu + reorder + resize tranche
|
|
921
|
-
* needs -- order, visibility, a per-column label override,
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
* separate ones; drag-to-reorder
|
|
926
|
-
* built on top of the order/width
|
|
1024
|
+
* needs -- order, visibility, a per-column label override, a per-column width, and
|
|
1025
|
+
* (Attio's "Pin column") a per-column left-pinned flag -- the way useDataTableSelection
|
|
1026
|
+
* owns the one selected-id SET rather than one hook per row. All five are "one value
|
|
1027
|
+
* per column key" maps/lists/sets a consumer may want to persist (a saved view), so
|
|
1028
|
+
* they share this hook instead of splitting into five separate ones; drag-to-reorder
|
|
1029
|
+
* and drag-to-resize (below) are the two INTERACTIONS built on top of the order/width
|
|
1030
|
+
* state this hook already owns, not independent state
|
|
927
1031
|
* of their own.
|
|
928
1032
|
*/ function useDataTableColumns(props) {
|
|
929
|
-
const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange } = props;
|
|
1033
|
+
const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
|
|
930
1034
|
const [rawOrder, setRawOrder] = useControllableState$1({
|
|
931
1035
|
prop: columnOrderProp,
|
|
932
1036
|
defaultProp: defaultColumnOrder ?? columnKeys,
|
|
@@ -963,6 +1067,36 @@ const EMPTY_RECORD = {};
|
|
|
963
1067
|
columnOrder,
|
|
964
1068
|
hiddenSet
|
|
965
1069
|
]);
|
|
1070
|
+
const [rawPinnedColumnKeys, setRawPinnedColumnKeys] = useControllableState$1({
|
|
1071
|
+
prop: pinnedProp,
|
|
1072
|
+
defaultProp: defaultPinnedColumnKeys,
|
|
1073
|
+
onChange: onPinnedColumnKeysChange
|
|
1074
|
+
});
|
|
1075
|
+
// Reconciled against `columnKeys` every render, the same way `columnOrder` is above --
|
|
1076
|
+
// a controlled `pinnedColumnKeys` a consumer persisted (e.g. a saved view) before a
|
|
1077
|
+
// column was removed from `columns` must not keep pinning a ghost key.
|
|
1078
|
+
const pinnedColumnKeys = React.useMemo(()=>reconcilePinnedColumns(rawPinnedColumnKeys ?? EMPTY_STRINGS, columnKeys), [
|
|
1079
|
+
rawPinnedColumnKeys,
|
|
1080
|
+
columnKeys
|
|
1081
|
+
]);
|
|
1082
|
+
const pinnedSet = React.useMemo(()=>new Set(pinnedColumnKeys), [
|
|
1083
|
+
pinnedColumnKeys
|
|
1084
|
+
]);
|
|
1085
|
+
const isColumnPinned = React.useCallback((key)=>pinnedSet.has(key), [
|
|
1086
|
+
pinnedSet
|
|
1087
|
+
]);
|
|
1088
|
+
const pinColumn = React.useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>current.includes(key) ? current : [
|
|
1089
|
+
...current,
|
|
1090
|
+
key
|
|
1091
|
+
]), [
|
|
1092
|
+
setRawPinnedColumnKeys
|
|
1093
|
+
]);
|
|
1094
|
+
const unpinColumn = React.useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>current.filter((existing)=>existing !== key)), [
|
|
1095
|
+
setRawPinnedColumnKeys
|
|
1096
|
+
]);
|
|
1097
|
+
const togglePinColumn = React.useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>togglePinnedColumn(current, key)), [
|
|
1098
|
+
setRawPinnedColumnKeys
|
|
1099
|
+
]);
|
|
966
1100
|
const isColumnHidden = React.useCallback((key)=>hiddenSet.has(key), [
|
|
967
1101
|
hiddenSet
|
|
968
1102
|
]);
|
|
@@ -1007,22 +1141,30 @@ const EMPTY_RECORD = {};
|
|
|
1007
1141
|
columnKeys
|
|
1008
1142
|
]);
|
|
1009
1143
|
// -- drag-to-reorder ---------------------------------------------------------------
|
|
1010
|
-
// Live state only (which column is currently being dragged) -- the styled layer
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1144
|
+
// Live state only (which column is currently being dragged) -- the styled layer's
|
|
1145
|
+
// header cells are natively `draggable`, so `dragOverColumn` is driven straight off
|
|
1146
|
+
// each cell's own `onDragOver` (one real browser event per column the pointer
|
|
1147
|
+
// crosses), which reorders immediately rather than waiting for a drop. That is
|
|
1148
|
+
// exactly what Attio's own header drag does (the column being passed visibly swaps
|
|
1149
|
+
// live), and it means `endColumnDrag` never has to also commit a move -- there is
|
|
1150
|
+
// nothing left to commit.
|
|
1017
1151
|
const [draggingColumnKey, setDraggingColumnKey] = React.useState(null);
|
|
1018
1152
|
const beginColumnDrag = React.useCallback((key)=>setDraggingColumnKey(key), []);
|
|
1153
|
+
// Reads `draggingColumnKey` straight from this render's own closure rather than via
|
|
1154
|
+
// `setDraggingColumnKey`'s functional-updater form (an earlier version of this hook
|
|
1155
|
+
// nested the `reorderColumn` call inside that updater, to read a "live" value --
|
|
1156
|
+
// `draggingColumnKey` never actually CHANGES during a drag, so that state update was
|
|
1157
|
+
// pure ceremony, and nesting one hook's setState inside another's updater callback is
|
|
1158
|
+
// exactly what silently dropped the reorder in DataTable.columns.test.tsx's own
|
|
1159
|
+
// end-to-end drag test: a REAL `dragover` DOM event fired, `dragOverColumn` fired with
|
|
1160
|
+
// the right key, but `columnOrder` never actually moved). Each `onDragOver` is its own
|
|
1161
|
+
// discrete browser event (not a tight synchronous loop within one render), so a plain
|
|
1162
|
+
// closure read is exactly as live as the updater trick was trying to be.
|
|
1019
1163
|
const dragOverColumn = React.useCallback((hoverKey)=>{
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
reorderColumn(current, hoverKey);
|
|
1023
|
-
return current;
|
|
1024
|
-
});
|
|
1164
|
+
if (!draggingColumnKey || draggingColumnKey === hoverKey) return;
|
|
1165
|
+
reorderColumn(draggingColumnKey, hoverKey);
|
|
1025
1166
|
}, [
|
|
1167
|
+
draggingColumnKey,
|
|
1026
1168
|
reorderColumn
|
|
1027
1169
|
]);
|
|
1028
1170
|
const endColumnDrag = React.useCallback(()=>setDraggingColumnKey(null), []);
|
|
@@ -1058,10 +1200,15 @@ const EMPTY_RECORD = {};
|
|
|
1058
1200
|
hiddenColumnKeys,
|
|
1059
1201
|
columnWidths,
|
|
1060
1202
|
columnLabels,
|
|
1203
|
+
pinnedColumnKeys,
|
|
1061
1204
|
isColumnHidden,
|
|
1062
1205
|
hideColumn,
|
|
1063
1206
|
showColumn,
|
|
1064
1207
|
toggleColumnVisibility,
|
|
1208
|
+
isColumnPinned,
|
|
1209
|
+
pinColumn,
|
|
1210
|
+
unpinColumn,
|
|
1211
|
+
togglePinColumn,
|
|
1065
1212
|
getColumnLabel,
|
|
1066
1213
|
renameColumn,
|
|
1067
1214
|
getColumnWidth,
|
|
@@ -1710,7 +1857,7 @@ function useDataTableRowContext({ strict = true } = {}) {
|
|
|
1710
1857
|
return context;
|
|
1711
1858
|
}
|
|
1712
1859
|
const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1713
|
-
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
|
|
1860
|
+
const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
|
|
1714
1861
|
const api = useDataTable({
|
|
1715
1862
|
sort,
|
|
1716
1863
|
defaultSort,
|
|
@@ -1742,6 +1889,9 @@ const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
|
1742
1889
|
columnLabels,
|
|
1743
1890
|
defaultColumnLabels,
|
|
1744
1891
|
onColumnLabelsChange,
|
|
1892
|
+
pinnedColumnKeys,
|
|
1893
|
+
defaultPinnedColumnKeys,
|
|
1894
|
+
onPinnedColumnKeysChange,
|
|
1745
1895
|
columnCalcOps,
|
|
1746
1896
|
defaultColumnCalcOps,
|
|
1747
1897
|
onColumnCalcOpsChange,
|
|
@@ -1829,6 +1979,132 @@ const DataTableCell$1 = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
|
1829
1979
|
});
|
|
1830
1980
|
DataTableCell$1.displayName = "DataTableCell";
|
|
1831
1981
|
|
|
1982
|
+
const EMPTY_VIEWS = [];
|
|
1983
|
+
function readStoredViews(storageKey) {
|
|
1984
|
+
if (!storageKey || typeof window === "undefined") return EMPTY_VIEWS;
|
|
1985
|
+
try {
|
|
1986
|
+
return deserializeDataTableViews(window.localStorage.getItem(storageKey));
|
|
1987
|
+
} catch {
|
|
1988
|
+
// A private-browsing tab / blocked storage throws on access, not just on quota --
|
|
1989
|
+
// this hook degrades to in-memory-only rather than crash the table over it.
|
|
1990
|
+
return EMPTY_VIEWS;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
/** A reasonably unique id for a newly created view -- `crypto.randomUUID` where it
|
|
1994
|
+
* exists (every real browser + modern Node), a plain timestamp+random fallback
|
|
1995
|
+
* otherwise (older jsdom/test environments) so this hook never throws just because a
|
|
1996
|
+
* test runner's `crypto` is a stub. */ function generateViewId() {
|
|
1997
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1998
|
+
return crypto.randomUUID();
|
|
1999
|
+
}
|
|
2000
|
+
return `view-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Owns the saved-views LIST (create/rename/delete/select) and its default persistence
|
|
2004
|
+
* (localStorage, pluggable away by controlling `views`/`onViewsChange` instead) -- the
|
|
2005
|
+
* same controllable shape every other DataTable state hook already uses
|
|
2006
|
+
* (`useDataTableColumns`, `useDataTableToolbarSort`, ...). Read `DataTableViewsControl`
|
|
2007
|
+
* (`views.tsx`, the styled layer) for how a consumer wires `captureState`/`applyState`
|
|
2008
|
+
* around this to actually save/restore column + sort + filter state.
|
|
2009
|
+
*/ function useDataTableViews(props) {
|
|
2010
|
+
const { views: viewsProp, defaultViews = EMPTY_VIEWS, onViewsChange, activeViewId: activeViewIdProp, defaultActiveViewId = null, onActiveViewIdChange, storageKey } = props;
|
|
2011
|
+
// Lazy, ONE-TIME read of localStorage as this render's default -- re-reading it on
|
|
2012
|
+
// every render (rather than a lazy ref) would clobber whatever the user just did in
|
|
2013
|
+
// THIS tab with whatever another tab most recently wrote. Only fires at all when this
|
|
2014
|
+
// hook truly owns persistence: `views` is uncontrolled AND a `storageKey` was given.
|
|
2015
|
+
const initialViewsRef = React.useRef(null);
|
|
2016
|
+
if (initialViewsRef.current === null) {
|
|
2017
|
+
if (viewsProp !== undefined) {
|
|
2018
|
+
initialViewsRef.current = defaultViews;
|
|
2019
|
+
} else {
|
|
2020
|
+
const stored = readStoredViews(storageKey);
|
|
2021
|
+
// Nothing in storage yet (first run, or no `storageKey` at all) -- seed from
|
|
2022
|
+
// `defaultViews` instead of an empty list, matching every other hook's own
|
|
2023
|
+
// `defaultProp` semantics.
|
|
2024
|
+
initialViewsRef.current = stored.length > 0 ? stored : defaultViews;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
const [views = EMPTY_VIEWS, setViews] = useControllableState$1({
|
|
2028
|
+
prop: viewsProp,
|
|
2029
|
+
defaultProp: initialViewsRef.current,
|
|
2030
|
+
onChange: onViewsChange
|
|
2031
|
+
});
|
|
2032
|
+
// Persists every change back to `storageKey` -- same "only when this hook owns
|
|
2033
|
+
// persistence" gate as the lazy read above. Skips the FIRST render (the effect would
|
|
2034
|
+
// otherwise immediately re-write back the exact bytes it just read, or stomp an
|
|
2035
|
+
// empty default over a controlled consumer's storage before it ever mounts).
|
|
2036
|
+
const skipFirstPersistRef = React.useRef(true);
|
|
2037
|
+
React.useEffect(()=>{
|
|
2038
|
+
if (viewsProp !== undefined || !storageKey || typeof window === "undefined") return;
|
|
2039
|
+
if (skipFirstPersistRef.current) {
|
|
2040
|
+
skipFirstPersistRef.current = false;
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
try {
|
|
2044
|
+
window.localStorage.setItem(storageKey, serializeDataTableViews(views));
|
|
2045
|
+
} catch {
|
|
2046
|
+
// Storage full/blocked -- the view still exists in memory for this session.
|
|
2047
|
+
}
|
|
2048
|
+
}, [
|
|
2049
|
+
views,
|
|
2050
|
+
viewsProp,
|
|
2051
|
+
storageKey
|
|
2052
|
+
]);
|
|
2053
|
+
const [activeViewId = null, setActiveViewId] = useControllableState$1({
|
|
2054
|
+
prop: activeViewIdProp,
|
|
2055
|
+
defaultProp: defaultActiveViewId,
|
|
2056
|
+
onChange: onActiveViewIdChange
|
|
2057
|
+
});
|
|
2058
|
+
const activeView = views.find((view)=>view.id === activeViewId) ?? null;
|
|
2059
|
+
const createView = React.useCallback((name, state)=>{
|
|
2060
|
+
const id = generateViewId();
|
|
2061
|
+
setViews((current = EMPTY_VIEWS)=>upsertDataTableView(current, {
|
|
2062
|
+
id,
|
|
2063
|
+
name,
|
|
2064
|
+
state
|
|
2065
|
+
}));
|
|
2066
|
+
setActiveViewId(id);
|
|
2067
|
+
return id;
|
|
2068
|
+
}, [
|
|
2069
|
+
setViews,
|
|
2070
|
+
setActiveViewId
|
|
2071
|
+
]);
|
|
2072
|
+
const updateView = React.useCallback((id, state)=>{
|
|
2073
|
+
setViews((current = EMPTY_VIEWS)=>{
|
|
2074
|
+
const existing = current.find((view)=>view.id === id);
|
|
2075
|
+
return existing ? upsertDataTableView(current, {
|
|
2076
|
+
...existing,
|
|
2077
|
+
state
|
|
2078
|
+
}) : current;
|
|
2079
|
+
});
|
|
2080
|
+
}, [
|
|
2081
|
+
setViews
|
|
2082
|
+
]);
|
|
2083
|
+
const renameView = React.useCallback((id, name)=>setViews((current = EMPTY_VIEWS)=>renameDataTableView(current, id, name)), [
|
|
2084
|
+
setViews
|
|
2085
|
+
]);
|
|
2086
|
+
const deleteView = React.useCallback((id)=>{
|
|
2087
|
+
setViews((current = EMPTY_VIEWS)=>removeDataTableView(current, id));
|
|
2088
|
+
setActiveViewId((current)=>current === id ? null : current);
|
|
2089
|
+
}, [
|
|
2090
|
+
setViews,
|
|
2091
|
+
setActiveViewId
|
|
2092
|
+
]);
|
|
2093
|
+
const selectView = React.useCallback((id)=>setActiveViewId(id), [
|
|
2094
|
+
setActiveViewId
|
|
2095
|
+
]);
|
|
2096
|
+
return {
|
|
2097
|
+
views,
|
|
2098
|
+
activeViewId,
|
|
2099
|
+
activeView,
|
|
2100
|
+
createView,
|
|
2101
|
+
updateView,
|
|
2102
|
+
renameView,
|
|
2103
|
+
deleteView,
|
|
2104
|
+
selectView
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1832
2108
|
function r(e) {
|
|
1833
2109
|
var t, f, n = "";
|
|
1834
2110
|
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
@@ -2354,76 +2630,121 @@ function RatingCell({ value, maxRating = 5 }) {
|
|
|
2354
2630
|
});
|
|
2355
2631
|
}
|
|
2356
2632
|
|
|
2357
|
-
/**
|
|
2358
|
-
*
|
|
2359
|
-
*
|
|
2360
|
-
*
|
|
2361
|
-
|
|
2633
|
+
/** Fallback width (px) used to compute a pinned column's cumulative sticky offset when
|
|
2634
|
+
* it has no explicit `width` and hasn't been drag-resized -- pinning needs a STATIC
|
|
2635
|
+
* number to put in an inline `insetInlineStart`, and `table-layout: fixed` only
|
|
2636
|
+
* decides how an unset column's share of the leftover width actually renders at paint
|
|
2637
|
+
* time (something no amount of JS math here can know ahead of it). Matches
|
|
2638
|
+
* `clampColumnWidth`'s own floor (react-headless's `dom.ts`) -- a real pinned column
|
|
2639
|
+
* narrower than this only needs its offset "wide enough not to overlap", not exact. */ const DEFAULT_PINNED_COLUMN_WIDTH = 180;
|
|
2640
|
+
/**
|
|
2641
|
+
* Every column this table actually renders, in RENDER order: any pinned columns first
|
|
2642
|
+
* (their own relative order preserved from `visibleColumnOrder`, not a separate pin-
|
|
2643
|
+
* order of their own), then everything else in normal order -- resolved label/width,
|
|
2644
|
+
* and this file's own sticky-left bookkeeping (`pinned`/`pinOffset`/`pinnedEdge`). Read
|
|
2645
|
+
* by the header, the body, the ColumnCalc footer AND the `<colgroup>` alike -- all four
|
|
2646
|
+
* must agree on this exact order, since a `<colgroup>`'s `<col>`s are matched
|
|
2647
|
+
* POSITIONALLY to every row's cells; splitting "order" (here) from "which one is
|
|
2648
|
+
* pinned" into two independently-ordered helpers would let a `<colgroup>` built from
|
|
2649
|
+
* one and a header row built from the other drift out of sync under `table-layout:
|
|
2650
|
+
* fixed`'s own column-to-cell matching.
|
|
2651
|
+
*/ function useEffectiveColumns(columns, options) {
|
|
2652
|
+
const { visibleColumnOrder, getColumnLabel, getColumnWidth, pinnedColumnKeys } = useDataTableContext();
|
|
2653
|
+
const { selectable, stickyFirst } = options;
|
|
2362
2654
|
return React__namespace.useMemo(()=>{
|
|
2363
2655
|
const byKey = new Map(columns.map((column)=>[
|
|
2364
2656
|
column.key,
|
|
2365
2657
|
column
|
|
2366
2658
|
]));
|
|
2367
|
-
|
|
2659
|
+
const visibleKeySet = new Set(visibleColumnOrder);
|
|
2660
|
+
const explicitPins = new Set((pinnedColumnKeys ?? []).filter((key)=>byKey.has(key) && visibleKeySet.has(key)));
|
|
2661
|
+
// Legacy fallback (DataTable.columns.test.tsx's own `stickyFirst` tests, predating
|
|
2662
|
+
// per-column pin): no EXPLICIT pin at all, `stickyFirst` pins exactly the first
|
|
2663
|
+
// visible column -- the single-column behavior this file now generalizes to a
|
|
2664
|
+
// whole pinned GROUP.
|
|
2665
|
+
const pinnedSet = explicitPins.size > 0 ? explicitPins : stickyFirst && visibleColumnOrder.length > 0 ? new Set([
|
|
2666
|
+
visibleColumnOrder[0]
|
|
2667
|
+
]) : explicitPins;
|
|
2668
|
+
// Stable partition: pinned columns first (their relative order among THEMSELVES
|
|
2669
|
+
// preserved from `visibleColumnOrder`), then every other column, also in its
|
|
2670
|
+
// existing relative order. `Array.prototype.sort` is spec-stable (ES2019+).
|
|
2671
|
+
const ordered = [
|
|
2672
|
+
...visibleColumnOrder
|
|
2673
|
+
].sort((a, b)=>{
|
|
2674
|
+
const aPinned = pinnedSet.has(a) ? 0 : 1;
|
|
2675
|
+
const bPinned = pinnedSet.has(b) ? 0 : 1;
|
|
2676
|
+
return aPinned - bPinned;
|
|
2677
|
+
});
|
|
2678
|
+
let cursor = selectable ? 40 : 0;
|
|
2679
|
+
let pinnedCount = 0;
|
|
2680
|
+
const entries = ordered.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>{
|
|
2681
|
+
const width = getColumnWidth(column.key) ?? column.width;
|
|
2682
|
+
const pinned = pinnedSet.has(column.key);
|
|
2683
|
+
const pinOffset = pinned ? cursor : 0;
|
|
2684
|
+
if (pinned) {
|
|
2685
|
+
cursor += width ?? DEFAULT_PINNED_COLUMN_WIDTH;
|
|
2686
|
+
pinnedCount += 1;
|
|
2687
|
+
}
|
|
2688
|
+
return {
|
|
2368
2689
|
column,
|
|
2369
2690
|
label: getColumnLabel(column.key, column.header),
|
|
2370
|
-
width
|
|
2371
|
-
|
|
2691
|
+
width,
|
|
2692
|
+
pinned,
|
|
2693
|
+
pinOffset,
|
|
2694
|
+
pinnedEdge: false
|
|
2695
|
+
};
|
|
2696
|
+
});
|
|
2697
|
+
if (pinnedCount > 0) entries[pinnedCount - 1].pinnedEdge = true;
|
|
2698
|
+
return entries;
|
|
2372
2699
|
}, [
|
|
2373
2700
|
columns,
|
|
2374
2701
|
visibleColumnOrder,
|
|
2375
2702
|
getColumnLabel,
|
|
2376
|
-
getColumnWidth
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
*
|
|
2384
|
-
*
|
|
2385
|
-
*
|
|
2386
|
-
*
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
2389
|
-
*
|
|
2390
|
-
*
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
style: {
|
|
2415
|
-
insetInlineStart: 0
|
|
2416
|
-
}
|
|
2417
|
-
};
|
|
2418
|
-
}
|
|
2419
|
-
if (stickyFirst && isFirst && selectable) {
|
|
2420
|
-
// The trailing edge of the pinned group -- past the 40px selection cell, so this
|
|
2421
|
-
// is where the scroll shadow actually renders unobstructed.
|
|
2703
|
+
getColumnWidth,
|
|
2704
|
+
pinnedColumnKeys,
|
|
2705
|
+
selectable,
|
|
2706
|
+
stickyFirst
|
|
2707
|
+
]);
|
|
2708
|
+
}
|
|
2709
|
+
/** The 40px selection column's own sticky-left state -- `stickyFirst` (the table-wide
|
|
2710
|
+
* prop) alone undercounts "should the checkbox column stick" the moment ANY per-column
|
|
2711
|
+
* pin is live, controlled or not, since the live pinned SET only exists once
|
|
2712
|
+
* `DataTablePrimitiveRoot`'s provider has mounted (an uncontrolled consumer never
|
|
2713
|
+
* hands its pinned columns back up as a prop for `DataTable.tsx` to read directly).
|
|
2714
|
+
* Mirrors `DataTable.tsx`'s own `resolvedStickyLast = stickyLast ||
|
|
2715
|
+
* columns.some(actions)` widening, just resolved one level down (live STATE, not
|
|
2716
|
+
* static column config) -- read via context from a component under the provider, not
|
|
2717
|
+
* computed above it. */ function useSelectionStickyFirst(stickyFirst) {
|
|
2718
|
+
const { pinnedColumnKeys } = useDataTableContext();
|
|
2719
|
+
return stickyFirst || (pinnedColumnKeys?.length ?? 0) > 0;
|
|
2720
|
+
}
|
|
2721
|
+
/** Sticky positioning for the 40px SELECTION cell only (header/body/footer) -- data
|
|
2722
|
+
* columns use their own `pinned`/`pinOffset`/`pinnedEdge` (`useEffectiveColumns`,
|
|
2723
|
+
* above) instead, via `pinnedCellProps` below. The selection cell is always the
|
|
2724
|
+
* leading edge of the pinned-left group when it's sticky at all, so it never itself
|
|
2725
|
+
* carries `data-sticky-edge` -- the trailing-most pinned DATA column does (its own
|
|
2726
|
+
* `pinnedEdge`), the same split the original single-column `stickyFirst` already drew
|
|
2727
|
+
* between the selection cell and Company/Name. */ function selectionStickyProps(stickyFirst) {
|
|
2728
|
+
return stickyFirst ? {
|
|
2729
|
+
"data-sticky": "",
|
|
2730
|
+
style: {
|
|
2731
|
+
insetInlineStart: 0
|
|
2732
|
+
}
|
|
2733
|
+
} : {};
|
|
2734
|
+
}
|
|
2735
|
+
/** Sticky positioning for one DATA cell (header, body, or ColumnCalc footer) -- pinned-
|
|
2736
|
+
* left (this column's own `pinOffset`, `data-sticky-edge` only on the group's
|
|
2737
|
+
* trailing-most member) takes priority over sticky-right (`stickyLast`, always just
|
|
2738
|
+
* the last VISIBLE column, e.g. an `actions` column) since a column can only stick to
|
|
2739
|
+
* one edge. */ function pinnedCellProps(entry, stickyLast, isLast) {
|
|
2740
|
+
if (entry.pinned) {
|
|
2422
2741
|
return {
|
|
2423
2742
|
"data-sticky": "",
|
|
2424
|
-
|
|
2743
|
+
...entry.pinnedEdge ? {
|
|
2744
|
+
"data-sticky-edge": ""
|
|
2745
|
+
} : {},
|
|
2425
2746
|
style: {
|
|
2426
|
-
insetInlineStart:
|
|
2747
|
+
insetInlineStart: entry.pinOffset
|
|
2427
2748
|
}
|
|
2428
2749
|
};
|
|
2429
2750
|
}
|
|
@@ -3471,20 +3792,16 @@ function RatingEditor({ column, value, pending, onCommit }) {
|
|
|
3471
3792
|
return column.editor === "switch" || column.type === "rating" || column.type === "checkbox";
|
|
3472
3793
|
}
|
|
3473
3794
|
function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast }) {
|
|
3474
|
-
const effective = useEffectiveColumns(columns
|
|
3795
|
+
const effective = useEffectiveColumns(columns, {
|
|
3796
|
+
selectable,
|
|
3797
|
+
stickyFirst
|
|
3798
|
+
});
|
|
3475
3799
|
const { startEdit } = useDataTableContext();
|
|
3476
3800
|
return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
|
|
3477
|
-
children: effective.map((
|
|
3478
|
-
const
|
|
3801
|
+
children: effective.map((entry, index)=>{
|
|
3802
|
+
const { column } = entry;
|
|
3479
3803
|
const isLast = index === effective.length - 1;
|
|
3480
|
-
const sticky =
|
|
3481
|
-
selectable,
|
|
3482
|
-
stickyFirst,
|
|
3483
|
-
stickyLast,
|
|
3484
|
-
isSelectionCell: false,
|
|
3485
|
-
isFirst,
|
|
3486
|
-
isLast
|
|
3487
|
-
});
|
|
3804
|
+
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
3488
3805
|
// A gated `editable` column (everything except the always-live widgets)
|
|
3489
3806
|
// opens on double-click -- Attio's own measured gesture (crisp's #411),
|
|
3490
3807
|
// matched here rather than a single click, which stays "select this cell"
|
|
@@ -3720,19 +4037,15 @@ const MenuItemLabel = withItemContext(withItemStateProps(Primitive.span), "label
|
|
|
3720
4037
|
withItemContext(withItemStateProps(Primitive.span), "description");
|
|
3721
4038
|
|
|
3722
4039
|
function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast }) {
|
|
3723
|
-
const effective = useEffectiveColumns(columns
|
|
4040
|
+
const effective = useEffectiveColumns(columns, {
|
|
4041
|
+
selectable,
|
|
4042
|
+
stickyFirst
|
|
4043
|
+
});
|
|
3724
4044
|
return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
|
|
3725
|
-
children: effective.map((
|
|
3726
|
-
const
|
|
4045
|
+
children: effective.map((entry, index)=>{
|
|
4046
|
+
const { column } = entry;
|
|
3727
4047
|
const isLast = index === effective.length - 1;
|
|
3728
|
-
const sticky =
|
|
3729
|
-
selectable,
|
|
3730
|
-
stickyFirst,
|
|
3731
|
-
stickyLast,
|
|
3732
|
-
isSelectionCell: false,
|
|
3733
|
-
isFirst,
|
|
3734
|
-
isLast
|
|
3735
|
-
});
|
|
4048
|
+
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
3736
4049
|
return /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterCell, {
|
|
3737
4050
|
"data-sticky": sticky["data-sticky"],
|
|
3738
4051
|
"data-sticky-edge": sticky["data-sticky-edge"],
|
|
@@ -3916,20 +4229,16 @@ const DEFAULT_SORT_ICON = {
|
|
|
3916
4229
|
* `visibleColumnOrder`/labels/widths change, without also re-deriving `sortedData`'s
|
|
3917
4230
|
* render above it. */ function ColumnHeaderCells({ columns, columnMenu, reorderable, resizable, sortIcon, selectable, stickyFirst, stickyLast }) {
|
|
3918
4231
|
const { draggingColumnKey, beginColumnDrag, dragOverColumn, endColumnDrag } = useDataTableContext();
|
|
3919
|
-
const effective = useEffectiveColumns(columns
|
|
4232
|
+
const effective = useEffectiveColumns(columns, {
|
|
4233
|
+
selectable,
|
|
4234
|
+
stickyFirst
|
|
4235
|
+
});
|
|
3920
4236
|
const [renamingKey, setRenamingKey] = React__namespace.useState(null);
|
|
3921
4237
|
return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
|
|
3922
|
-
children: effective.map((
|
|
3923
|
-
const
|
|
4238
|
+
children: effective.map((entry, index)=>{
|
|
4239
|
+
const { column, label, width } = entry;
|
|
3924
4240
|
const isLast = index === effective.length - 1;
|
|
3925
|
-
const sticky =
|
|
3926
|
-
selectable,
|
|
3927
|
-
stickyFirst,
|
|
3928
|
-
stickyLast,
|
|
3929
|
-
isSelectionCell: false,
|
|
3930
|
-
isFirst,
|
|
3931
|
-
isLast
|
|
3932
|
-
});
|
|
4241
|
+
const sticky = pinnedCellProps(entry, stickyLast, isLast);
|
|
3933
4242
|
return /*#__PURE__*/ jsxRuntime.jsxs(DataTableHeaderCell, {
|
|
3934
4243
|
columnKey: column.key,
|
|
3935
4244
|
sortable: column.sortable,
|
|
@@ -4059,8 +4368,9 @@ function ColumnRenameInput({ column, initialLabel, onDone }) {
|
|
|
4059
4368
|
* permanently, so this one carries its own marker attribute distinct from that
|
|
4060
4369
|
* slot's OTHER occupants (the sort chevron, the filter trigger), which stay visible
|
|
4061
4370
|
* under their own separate rules regardless of hover. */ function ColumnMenu({ column, onRename }) {
|
|
4062
|
-
const { moveColumn, hideColumn } = useDataTableContext();
|
|
4371
|
+
const { moveColumn, hideColumn, isColumnPinned, togglePinColumn } = useDataTableContext();
|
|
4063
4372
|
const columnLabel = typeof column.header === "string" ? column.header : column.key;
|
|
4373
|
+
const pinned = isColumnPinned(column.key);
|
|
4064
4374
|
const stopPropagation = (event)=>event.stopPropagation();
|
|
4065
4375
|
return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
|
|
4066
4376
|
children: [
|
|
@@ -4102,6 +4412,12 @@ function ColumnRenameInput({ column, initialLabel, onDone }) {
|
|
|
4102
4412
|
children: "Move right"
|
|
4103
4413
|
})
|
|
4104
4414
|
}),
|
|
4415
|
+
/*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
|
|
4416
|
+
onClick: ()=>togglePinColumn(column.key),
|
|
4417
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
|
|
4418
|
+
children: pinned ? "Unpin column" : "Pin column"
|
|
4419
|
+
})
|
|
4420
|
+
}),
|
|
4105
4421
|
/*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
|
|
4106
4422
|
onClick: ()=>hideColumn(column.key),
|
|
4107
4423
|
children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
|
|
@@ -9322,7 +9638,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
|
|
|
9322
9638
|
"aria-label": "Move sort up",
|
|
9323
9639
|
disabled: index === 0,
|
|
9324
9640
|
onClick: ()=>moveToolbarSort(index, index - 1),
|
|
9325
|
-
style: iconButtonStyle,
|
|
9641
|
+
style: iconButtonStyle$1,
|
|
9326
9642
|
children: /*#__PURE__*/ jsxRuntime.jsx(IconArrowUpLine_default, {
|
|
9327
9643
|
size: 13,
|
|
9328
9644
|
strokeWidth: 1.75
|
|
@@ -9333,7 +9649,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
|
|
|
9333
9649
|
"aria-label": "Move sort down",
|
|
9334
9650
|
disabled: index === toolbarSorts.length - 1,
|
|
9335
9651
|
onClick: ()=>moveToolbarSort(index, index + 1),
|
|
9336
|
-
style: iconButtonStyle,
|
|
9652
|
+
style: iconButtonStyle$1,
|
|
9337
9653
|
children: /*#__PURE__*/ jsxRuntime.jsx(IconArrowDownLine_default, {
|
|
9338
9654
|
size: 13,
|
|
9339
9655
|
strokeWidth: 1.75
|
|
@@ -9343,7 +9659,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
|
|
|
9343
9659
|
type: "button",
|
|
9344
9660
|
"aria-label": "Remove sort",
|
|
9345
9661
|
onClick: ()=>removeToolbarSort(index),
|
|
9346
|
-
style: iconButtonStyle,
|
|
9662
|
+
style: iconButtonStyle$1,
|
|
9347
9663
|
children: /*#__PURE__*/ jsxRuntime.jsx(IconXmarkLine_default, {
|
|
9348
9664
|
size: 13,
|
|
9349
9665
|
strokeWidth: 1.75
|
|
@@ -9358,7 +9674,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
|
|
|
9358
9674
|
disabled: availableColumns.length === 0,
|
|
9359
9675
|
onClick: ()=>availableColumns[0] && addToolbarSort(availableColumns[0].key),
|
|
9360
9676
|
style: {
|
|
9361
|
-
...iconButtonStyle,
|
|
9677
|
+
...iconButtonStyle$1,
|
|
9362
9678
|
width: "auto",
|
|
9363
9679
|
gap: 6,
|
|
9364
9680
|
padding: "6px 8px",
|
|
@@ -9380,7 +9696,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
|
|
|
9380
9696
|
]
|
|
9381
9697
|
});
|
|
9382
9698
|
}
|
|
9383
|
-
const iconButtonStyle = {
|
|
9699
|
+
const iconButtonStyle$1 = {
|
|
9384
9700
|
display: "inline-flex",
|
|
9385
9701
|
alignItems: "center",
|
|
9386
9702
|
justifyContent: "center",
|
|
@@ -9478,7 +9794,7 @@ const formatFilterDate = (iso)=>{
|
|
|
9478
9794
|
type: "button",
|
|
9479
9795
|
"aria-label": `Remove ${column ? columnLabel(column) : filter.key} filter`,
|
|
9480
9796
|
onClick: ()=>removeToolbarFilter(filter.id),
|
|
9481
|
-
style: iconButtonStyle,
|
|
9797
|
+
style: iconButtonStyle$1,
|
|
9482
9798
|
children: /*#__PURE__*/ jsxRuntime.jsx(IconXmarkLine_default, {
|
|
9483
9799
|
size: 12,
|
|
9484
9800
|
strokeWidth: 1.75
|
|
@@ -9735,7 +10051,7 @@ const formatFilterDate = (iso)=>{
|
|
|
9735
10051
|
type: "button",
|
|
9736
10052
|
onClick: ()=>setPickingField(true),
|
|
9737
10053
|
style: {
|
|
9738
|
-
...iconButtonStyle,
|
|
10054
|
+
...iconButtonStyle$1,
|
|
9739
10055
|
width: "auto",
|
|
9740
10056
|
gap: 6,
|
|
9741
10057
|
padding: "6px 8px",
|
|
@@ -9820,7 +10136,7 @@ function RowCount({ shown, total, countLabel }) {
|
|
|
9820
10136
|
}
|
|
9821
10137
|
|
|
9822
10138
|
function DataTableInner(props, ref) {
|
|
9823
|
-
const { columns, data, surface, className, locale = "en-US", sort, defaultSort, onSortChange, sortIcon, selectable, selectedIds, defaultSelectedIds, onSelectionChange, selectAllLabel = "Select all rows", getRowSelectionLabel = ()=>"Select row", selectionCheckedIcon, selectionIndeterminateIcon, query, defaultQuery, onQueryChange, searchPlaceholder = "Search…", searchLabel = "Search table", toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, showCount = true, countLabel, toolbarActions, columnMenu = false, reorderable = true, resizable = true, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, onColumnsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, stickyFirst = false, stickyLast = false, onEdit, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", loading = false, loadingLabel = "Loading...", error, empty = "No results.", onLoadMore, hasMore, loadingMore, scrollProps, ...restProps } = props;
|
|
10139
|
+
const { columns, data, surface, className, locale = "en-US", sort, defaultSort, onSortChange, sortIcon, selectable, selectedIds, defaultSelectedIds, onSelectionChange, selectAllLabel = "Select all rows", getRowSelectionLabel = ()=>"Select row", selectionCheckedIcon, selectionIndeterminateIcon, query, defaultQuery, onQueryChange, searchPlaceholder = "Search…", searchLabel = "Search table", toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, showCount = true, countLabel, toolbarActions, columnMenu = false, reorderable = true, resizable = true, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, onColumnsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, stickyFirst = false, stickyLast = false, onEdit, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", loading = false, loadingLabel = "Loading...", error, empty = "No results.", onLoadMore, hasMore, loadingMore, scrollProps, ...restProps } = props;
|
|
9824
10140
|
const classNames = dataTable({
|
|
9825
10141
|
surface,
|
|
9826
10142
|
interactive
|
|
@@ -9956,6 +10272,9 @@ function DataTableInner(props, ref) {
|
|
|
9956
10272
|
columnLabels: columnLabels,
|
|
9957
10273
|
defaultColumnLabels: defaultColumnLabels,
|
|
9958
10274
|
onColumnLabelsChange: onColumnLabelsChange,
|
|
10275
|
+
pinnedColumnKeys: pinnedColumnKeys,
|
|
10276
|
+
defaultPinnedColumnKeys: defaultPinnedColumnKeys,
|
|
10277
|
+
onPinnedColumnKeysChange: onPinnedColumnKeysChange,
|
|
9959
10278
|
columnCalcOps: columnCalcOps,
|
|
9960
10279
|
defaultColumnCalcOps: defaultColumnCalcOps,
|
|
9961
10280
|
onColumnCalcOpsChange: onColumnCalcOpsChange,
|
|
@@ -9999,19 +10318,13 @@ function DataTableInner(props, ref) {
|
|
|
9999
10318
|
children: [
|
|
10000
10319
|
/*#__PURE__*/ jsxRuntime.jsx(DataTableColGroup, {
|
|
10001
10320
|
columns: columns,
|
|
10002
|
-
selectable: !!selectable
|
|
10321
|
+
selectable: !!selectable,
|
|
10322
|
+
stickyFirst: stickyFirst
|
|
10003
10323
|
}),
|
|
10004
10324
|
/*#__PURE__*/ jsxRuntime.jsxs(DataTableHeader, {
|
|
10005
10325
|
children: [
|
|
10006
|
-
selectable && /*#__PURE__*/ jsxRuntime.jsx(
|
|
10007
|
-
|
|
10008
|
-
selectable,
|
|
10009
|
-
stickyFirst,
|
|
10010
|
-
stickyLast: resolvedStickyLast,
|
|
10011
|
-
isSelectionCell: true,
|
|
10012
|
-
isFirst: false,
|
|
10013
|
-
isLast: false
|
|
10014
|
-
}),
|
|
10326
|
+
selectable && /*#__PURE__*/ jsxRuntime.jsx(SelectionHeaderCell, {
|
|
10327
|
+
stickyFirst: stickyFirst,
|
|
10015
10328
|
children: /*#__PURE__*/ jsxRuntime.jsx(SelectAllCheckbox, {
|
|
10016
10329
|
label: selectAllLabel,
|
|
10017
10330
|
checkedIcon: selectionCheckedIcon ?? DEFAULT_CHECKED_ICON,
|
|
@@ -10062,7 +10375,7 @@ function DataTableInner(props, ref) {
|
|
|
10062
10375
|
"data-clickable": onRowClick ? "" : undefined,
|
|
10063
10376
|
onClick: onRowClick ? ()=>onRowClick(row) : undefined,
|
|
10064
10377
|
children: [
|
|
10065
|
-
selectable && /*#__PURE__*/ jsxRuntime.jsx(
|
|
10378
|
+
selectable && /*#__PURE__*/ jsxRuntime.jsx(SelectionCell, {
|
|
10066
10379
|
// The row itself carries `onClick={onRowClick}` above, and a
|
|
10067
10380
|
// native click on this cell's checkbox bubbles up through it
|
|
10068
10381
|
// (React attaches DataTableRow's handler on the <tr>, which
|
|
@@ -10070,14 +10383,7 @@ function DataTableInner(props, ref) {
|
|
|
10070
10383
|
// it from at -- so this has to live here, not in the consumer).
|
|
10071
10384
|
// Without this, checking a row's box also fires `onRowClick`.
|
|
10072
10385
|
onClick: (event)=>event.stopPropagation(),
|
|
10073
|
-
|
|
10074
|
-
selectable,
|
|
10075
|
-
stickyFirst,
|
|
10076
|
-
stickyLast: resolvedStickyLast,
|
|
10077
|
-
isSelectionCell: true,
|
|
10078
|
-
isFirst: false,
|
|
10079
|
-
isLast: false
|
|
10080
|
-
}),
|
|
10386
|
+
stickyFirst: stickyFirst,
|
|
10081
10387
|
children: /*#__PURE__*/ jsxRuntime.jsx(RowCheckbox, {
|
|
10082
10388
|
label: getRowSelectionLabel(row),
|
|
10083
10389
|
checkedIcon: selectionCheckedIcon ?? DEFAULT_CHECKED_ICON
|
|
@@ -10109,15 +10415,8 @@ function DataTableInner(props, ref) {
|
|
|
10109
10415
|
// is what makes every row's selection column line up now, not a matching
|
|
10110
10416
|
// per-slot width formula, so there is no more reason for this cell to
|
|
10111
10417
|
// borrow a different slot than its footer siblings.
|
|
10112
|
-
/*#__PURE__*/ jsxRuntime.jsx(
|
|
10113
|
-
|
|
10114
|
-
selectable,
|
|
10115
|
-
stickyFirst,
|
|
10116
|
-
stickyLast: resolvedStickyLast,
|
|
10117
|
-
isSelectionCell: true,
|
|
10118
|
-
isFirst: false,
|
|
10119
|
-
isLast: false
|
|
10120
|
-
})
|
|
10418
|
+
/*#__PURE__*/ jsxRuntime.jsx(SelectionFooterCell, {
|
|
10419
|
+
stickyFirst: stickyFirst
|
|
10121
10420
|
}),
|
|
10122
10421
|
/*#__PURE__*/ jsxRuntime.jsx(ColumnCalcCells, {
|
|
10123
10422
|
columns: columns,
|
|
@@ -10153,7 +10452,16 @@ DataTableInner.displayName = "DataTable";
|
|
|
10153
10452
|
* `DataTableInner`) purely so it can call `useDataTableContext()` -- that context is
|
|
10154
10453
|
* provided by the `DataTablePrimitiveRoot` this renders as a SIBLING of, one level
|
|
10155
10454
|
* up from where `DataTableInner` itself sits. */ function ColumnsChangeEffect({ columns, onColumnsChange }) {
|
|
10156
|
-
|
|
10455
|
+
// `selectable`/`stickyFirst` are both `false` here on purpose: this convenience blob
|
|
10456
|
+
// never renders a row of its own, so it needs neither the +40px selection offset nor
|
|
10457
|
+
// `stickyFirst`'s legacy "no explicit pin -> pin the first visible column" fallback.
|
|
10458
|
+
// An EXPLICIT per-column pin (`pinnedColumnKeys`) still reorders this list the same
|
|
10459
|
+
// way it reorders every rendered row -- this callback reports "what's showing now",
|
|
10460
|
+
// and a pinned column showing first IS what's showing now.
|
|
10461
|
+
const effective = useEffectiveColumns(columns, {
|
|
10462
|
+
selectable: false,
|
|
10463
|
+
stickyFirst: false
|
|
10464
|
+
});
|
|
10157
10465
|
// A NEW array every time `effective` itself is a new array (its own `useMemo`
|
|
10158
10466
|
// recomputes only when order/hidden/label/width actually change) -- `header`
|
|
10159
10467
|
// resolves to the menu's rename override (falling back to the column's own static
|
|
@@ -10188,8 +10496,16 @@ const DataTableTable = withContext$1(DataTableTable$1, "table");
|
|
|
10188
10496
|
* out of sync between the three. A column with no explicit width gets a bare `<col>`
|
|
10189
10497
|
* (no `style`): `table-layout: fixed`'s own algorithm gives every such column an equal
|
|
10190
10498
|
* share of whatever space is left, matching the old flex grid's `flex: 1 1 0%`
|
|
10191
|
-
* default with no extra code needed here. */ function DataTableColGroup({ columns, selectable }) {
|
|
10192
|
-
|
|
10499
|
+
* default with no extra code needed here. */ function DataTableColGroup({ columns, selectable, stickyFirst }) {
|
|
10500
|
+
// MUST use the same pinned-first ordering every OTHER row (header/body/footer)
|
|
10501
|
+
// renders in (`useEffectiveColumns`'s own doc comment) -- a `<colgroup>`'s `<col>`s
|
|
10502
|
+
// are matched positionally to each row's cells, so if a pinned column visually moves
|
|
10503
|
+
// to the front of the header/body but not here, every column's width applies to the
|
|
10504
|
+
// WRONG cell.
|
|
10505
|
+
const effective = useEffectiveColumns(columns, {
|
|
10506
|
+
selectable,
|
|
10507
|
+
stickyFirst
|
|
10508
|
+
});
|
|
10193
10509
|
return /*#__PURE__*/ jsxRuntime.jsxs("colgroup", {
|
|
10194
10510
|
children: [
|
|
10195
10511
|
selectable && /*#__PURE__*/ jsxRuntime.jsx("col", {
|
|
@@ -10275,6 +10591,32 @@ function DataTableFooter({ children, ...props }) {
|
|
|
10275
10591
|
});
|
|
10276
10592
|
}
|
|
10277
10593
|
const DataTableFooterCell = withContext$1(Primitive.td, "footerCell");
|
|
10594
|
+
// The selection column's OWN sticky wrappers -- one each for header/body/footer,
|
|
10595
|
+
// reading the live pinned set via `useSelectionStickyFirst` (column-layout.ts) instead
|
|
10596
|
+
// of the `stickyFirst` PROP alone, so the checkbox column sticks the moment ANY column
|
|
10597
|
+
// is pinned, uncontrolled or not (see that hook's own doc comment for why this can't
|
|
10598
|
+
// just be computed once, above, in `DataTableInner`).
|
|
10599
|
+
function SelectionHeaderCell({ stickyFirst, children }) {
|
|
10600
|
+
const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
|
|
10601
|
+
return /*#__PURE__*/ jsxRuntime.jsx(DataTableSelectionHeaderCell, {
|
|
10602
|
+
...selectionStickyProps(resolvedStickyFirst),
|
|
10603
|
+
children: children
|
|
10604
|
+
});
|
|
10605
|
+
}
|
|
10606
|
+
function SelectionCell({ stickyFirst, onClick, children }) {
|
|
10607
|
+
const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
|
|
10608
|
+
return /*#__PURE__*/ jsxRuntime.jsx(DataTableSelectionCell, {
|
|
10609
|
+
onClick: onClick,
|
|
10610
|
+
...selectionStickyProps(resolvedStickyFirst),
|
|
10611
|
+
children: children
|
|
10612
|
+
});
|
|
10613
|
+
}
|
|
10614
|
+
function SelectionFooterCell({ stickyFirst }) {
|
|
10615
|
+
const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
|
|
10616
|
+
return /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterCell, {
|
|
10617
|
+
...selectionStickyProps(resolvedStickyFirst)
|
|
10618
|
+
});
|
|
10619
|
+
}
|
|
10278
10620
|
////////////////////////////////////////////////////////////////////////////////////
|
|
10279
10621
|
// Fix (bug report): karrot icons default to a 24px intrinsic SVG attribute size
|
|
10280
10622
|
// -- see column-menu.tsx's own `SORT_ICON_SIZE` comment for the full mechanism.
|
|
@@ -10330,4 +10672,275 @@ function SelectAllCheckbox({ label, checkedIcon, indeterminateIcon }) {
|
|
|
10330
10672
|
});
|
|
10331
10673
|
}
|
|
10332
10674
|
|
|
10675
|
+
const rowStyle = {
|
|
10676
|
+
display: "flex",
|
|
10677
|
+
alignItems: "center",
|
|
10678
|
+
gap: 4
|
|
10679
|
+
};
|
|
10680
|
+
const rowButtonStyle = {
|
|
10681
|
+
flex: 1,
|
|
10682
|
+
minWidth: 0,
|
|
10683
|
+
textAlign: "left",
|
|
10684
|
+
display: "flex",
|
|
10685
|
+
alignItems: "center",
|
|
10686
|
+
gap: 6,
|
|
10687
|
+
padding: "6px 8px",
|
|
10688
|
+
border: "none",
|
|
10689
|
+
borderRadius: 6,
|
|
10690
|
+
background: "transparent",
|
|
10691
|
+
color: "var(--seed-color-fg-neutral)",
|
|
10692
|
+
fontSize: 13,
|
|
10693
|
+
cursor: "pointer",
|
|
10694
|
+
overflow: "hidden",
|
|
10695
|
+
textOverflow: "ellipsis",
|
|
10696
|
+
whiteSpace: "nowrap"
|
|
10697
|
+
};
|
|
10698
|
+
const smallButtonStyle = {
|
|
10699
|
+
flexShrink: 0,
|
|
10700
|
+
border: "none",
|
|
10701
|
+
borderRadius: 6,
|
|
10702
|
+
background: "transparent",
|
|
10703
|
+
color: "var(--seed-color-fg-neutral-subtle)",
|
|
10704
|
+
fontSize: 12,
|
|
10705
|
+
padding: "4px 6px",
|
|
10706
|
+
cursor: "pointer"
|
|
10707
|
+
};
|
|
10708
|
+
const iconButtonStyle = {
|
|
10709
|
+
display: "inline-flex",
|
|
10710
|
+
alignItems: "center",
|
|
10711
|
+
justifyContent: "center",
|
|
10712
|
+
width: 22,
|
|
10713
|
+
height: 22,
|
|
10714
|
+
flexShrink: 0,
|
|
10715
|
+
border: "none",
|
|
10716
|
+
borderRadius: 6,
|
|
10717
|
+
background: "transparent",
|
|
10718
|
+
color: "var(--seed-color-fg-neutral-subtle)",
|
|
10719
|
+
cursor: "pointer"
|
|
10720
|
+
};
|
|
10721
|
+
/**
|
|
10722
|
+
* A compact saved-views switcher -- current view name (or `label`) as the trigger,
|
|
10723
|
+
* a popover listing every saved view (click to switch, inline Rename/Delete), a
|
|
10724
|
+
* trailing "Save as new view", and (once a view is active) "Update view" to overwrite
|
|
10725
|
+
* the loaded view with the table's current state.
|
|
10726
|
+
*/ function DataTableViewsControl({ label = "Views", showClearRow = false, clearLabel, captureState, applyState, newViewLabel = "Save as new view", updateViewLabel = "Update view", renameLabel = "Rename", deleteLabel = "Delete", renameInputLabel = (name)=>`Rename ${name}`, ...viewsProps }) {
|
|
10727
|
+
const viewsApi = useDataTableViews(viewsProps);
|
|
10728
|
+
const { activeViewId, activeView } = viewsApi;
|
|
10729
|
+
const triggerLabel = activeView ? activeView.name : label;
|
|
10730
|
+
return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
|
|
10731
|
+
children: [
|
|
10732
|
+
/*#__PURE__*/ jsxRuntime.jsx(MenuTrigger, {
|
|
10733
|
+
asChild: true,
|
|
10734
|
+
children: /*#__PURE__*/ jsxRuntime.jsxs(ActionButton, {
|
|
10735
|
+
variant: "neutralOutline",
|
|
10736
|
+
size: "xsmall",
|
|
10737
|
+
"data-active": activeViewId ? "" : undefined,
|
|
10738
|
+
children: [
|
|
10739
|
+
/*#__PURE__*/ jsxRuntime.jsx(IconArrowUpArrowDownLine_default, {
|
|
10740
|
+
size: 14,
|
|
10741
|
+
strokeWidth: 1.75
|
|
10742
|
+
}),
|
|
10743
|
+
triggerLabel,
|
|
10744
|
+
/*#__PURE__*/ jsxRuntime.jsx(IconChevronDownSmallLine_default, {
|
|
10745
|
+
size: 14,
|
|
10746
|
+
strokeWidth: 1.75
|
|
10747
|
+
})
|
|
10748
|
+
]
|
|
10749
|
+
})
|
|
10750
|
+
}),
|
|
10751
|
+
/*#__PURE__*/ jsxRuntime.jsx(MenuPositioner, {
|
|
10752
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(MenuContent, {
|
|
10753
|
+
style: {
|
|
10754
|
+
minWidth: 240
|
|
10755
|
+
},
|
|
10756
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(MenuScrollArea, {
|
|
10757
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(ViewsMenuBody, {
|
|
10758
|
+
viewsApi: viewsApi,
|
|
10759
|
+
captureState: captureState,
|
|
10760
|
+
applyState: applyState,
|
|
10761
|
+
label: label,
|
|
10762
|
+
showClearRow: showClearRow,
|
|
10763
|
+
clearLabel: clearLabel,
|
|
10764
|
+
newViewLabel: newViewLabel,
|
|
10765
|
+
updateViewLabel: updateViewLabel,
|
|
10766
|
+
renameLabel: renameLabel,
|
|
10767
|
+
deleteLabel: deleteLabel,
|
|
10768
|
+
renameInputLabel: renameInputLabel
|
|
10769
|
+
})
|
|
10770
|
+
})
|
|
10771
|
+
})
|
|
10772
|
+
})
|
|
10773
|
+
]
|
|
10774
|
+
});
|
|
10775
|
+
}
|
|
10776
|
+
DataTableViewsControl.displayName = "DataTableViewsControl";
|
|
10777
|
+
/**
|
|
10778
|
+
* The popover's own body -- split out from `DataTableViewsControl` purely so it can
|
|
10779
|
+
* call `useMenuContext()` (`@seed-design/react-menu`), which only resolves for a
|
|
10780
|
+
* descendant of `MenuRoot`, not the component that RENDERS `MenuRoot` itself (the same
|
|
10781
|
+
* reason `column-layout.ts`'s `useSelectionStickyFirst` is its own component-level
|
|
10782
|
+
* hook rather than something computed in `DataTable.tsx` above its provider). Plain
|
|
10783
|
+
* `<button>` rows here (not seed `MenuItem`s -- `toolbar-conditions.tsx`'s own Sort/
|
|
10784
|
+
* Filter popovers make the same call) don't auto-close the menu on click the way a real
|
|
10785
|
+
* `MenuItem` does, so switching/creating/updating a view calls `setOpen(false)` here
|
|
10786
|
+
* explicitly; renaming and deleting deliberately do NOT close it, so a consumer can
|
|
10787
|
+
* clean up more than one view without reopening the popover each time.
|
|
10788
|
+
*/ function ViewsMenuBody({ viewsApi, captureState, applyState, label, showClearRow, clearLabel, newViewLabel, updateViewLabel, renameLabel, deleteLabel, renameInputLabel }) {
|
|
10789
|
+
const { views, activeViewId, createView, updateView, renameView, deleteView, selectView } = viewsApi;
|
|
10790
|
+
const { setOpen } = reactMenu.useMenuContext();
|
|
10791
|
+
const [renamingId, setRenamingId] = React__namespace.useState(null);
|
|
10792
|
+
const [renameValue, setRenameValue] = React__namespace.useState("");
|
|
10793
|
+
// Same race `column-menu.tsx`'s own inline rename input already guards against:
|
|
10794
|
+
// starting a rename swaps the clicked "Rename" button for an input in the SAME
|
|
10795
|
+
// MenuContent this popover's own floating-ui focus management watches, and its
|
|
10796
|
+
// focus-guard spans can steal focus right back (to the content, or the trigger)
|
|
10797
|
+
// one frame after mount -- BEFORE this ref flips true -- so `onBlur` ignores that
|
|
10798
|
+
// one spurious blur and only commits a REAL blur (an actual click/tab away) after.
|
|
10799
|
+
const renameReadyRef = React__namespace.useRef(false);
|
|
10800
|
+
const renameInputRef = React__namespace.useRef(null);
|
|
10801
|
+
const handleSelect = (id)=>{
|
|
10802
|
+
selectView(id);
|
|
10803
|
+
if (id !== null) {
|
|
10804
|
+
const view = views.find((v)=>v.id === id);
|
|
10805
|
+
if (view) applyState(view.state);
|
|
10806
|
+
}
|
|
10807
|
+
setOpen(false);
|
|
10808
|
+
};
|
|
10809
|
+
const handleCreate = ()=>{
|
|
10810
|
+
createView(`View ${views.length + 1}`, captureState());
|
|
10811
|
+
setOpen(false);
|
|
10812
|
+
};
|
|
10813
|
+
const handleUpdate = ()=>{
|
|
10814
|
+
if (activeViewId) updateView(activeViewId, captureState());
|
|
10815
|
+
setOpen(false);
|
|
10816
|
+
};
|
|
10817
|
+
const startRename = (view)=>{
|
|
10818
|
+
renameReadyRef.current = false;
|
|
10819
|
+
setRenamingId(view.id);
|
|
10820
|
+
setRenameValue(view.name);
|
|
10821
|
+
};
|
|
10822
|
+
const commitRename = ()=>{
|
|
10823
|
+
if (renamingId && renameValue.trim()) renameView(renamingId, renameValue.trim());
|
|
10824
|
+
setRenamingId(null);
|
|
10825
|
+
};
|
|
10826
|
+
React__namespace.useEffect(()=>{
|
|
10827
|
+
if (!renamingId) return;
|
|
10828
|
+
// Re-grab focus (and select the text) one frame after mount, AFTER whatever
|
|
10829
|
+
// floating-ui's own focus-guard restore was going to do -- see this component's
|
|
10830
|
+
// own `renameReadyRef` doc comment above.
|
|
10831
|
+
const frame = requestAnimationFrame(()=>{
|
|
10832
|
+
renameReadyRef.current = true;
|
|
10833
|
+
renameInputRef.current?.focus();
|
|
10834
|
+
renameInputRef.current?.select();
|
|
10835
|
+
});
|
|
10836
|
+
return ()=>cancelAnimationFrame(frame);
|
|
10837
|
+
}, [
|
|
10838
|
+
renamingId
|
|
10839
|
+
]);
|
|
10840
|
+
return /*#__PURE__*/ jsxRuntime.jsxs("div", {
|
|
10841
|
+
style: {
|
|
10842
|
+
display: "flex",
|
|
10843
|
+
flexDirection: "column",
|
|
10844
|
+
gap: 2,
|
|
10845
|
+
padding: "4px 8px"
|
|
10846
|
+
},
|
|
10847
|
+
children: [
|
|
10848
|
+
showClearRow && /*#__PURE__*/ jsxRuntime.jsx("button", {
|
|
10849
|
+
type: "button",
|
|
10850
|
+
onClick: ()=>handleSelect(null),
|
|
10851
|
+
"data-active": !activeViewId ? "" : undefined,
|
|
10852
|
+
style: rowButtonStyle,
|
|
10853
|
+
children: clearLabel ?? label
|
|
10854
|
+
}),
|
|
10855
|
+
views.length === 0 && /*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
10856
|
+
style: {
|
|
10857
|
+
fontSize: 13,
|
|
10858
|
+
color: "var(--seed-color-fg-neutral-subtle)",
|
|
10859
|
+
padding: "4px 8px"
|
|
10860
|
+
},
|
|
10861
|
+
children: "No saved views yet."
|
|
10862
|
+
}),
|
|
10863
|
+
views.map((view)=>renamingId === view.id ? /*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
10864
|
+
style: rowStyle,
|
|
10865
|
+
children: /*#__PURE__*/ jsxRuntime.jsx("input", {
|
|
10866
|
+
ref: renameInputRef,
|
|
10867
|
+
// biome-ignore lint/a11y/noAutofocus: opening this input IS the user's own "Rename" click a moment earlier, not an unannounced page-load autofocus (same call column-menu.tsx's own inline rename input makes).
|
|
10868
|
+
autoFocus: true,
|
|
10869
|
+
"aria-label": renameInputLabel(view.name),
|
|
10870
|
+
value: renameValue,
|
|
10871
|
+
onChange: (event)=>setRenameValue(event.target.value),
|
|
10872
|
+
onBlur: ()=>{
|
|
10873
|
+
if (renameReadyRef.current) commitRename();
|
|
10874
|
+
},
|
|
10875
|
+
onKeyDown: (event)=>{
|
|
10876
|
+
event.stopPropagation();
|
|
10877
|
+
if (event.key === "Enter") commitRename();
|
|
10878
|
+
if (event.key === "Escape") setRenamingId(null);
|
|
10879
|
+
},
|
|
10880
|
+
style: {
|
|
10881
|
+
flex: 1,
|
|
10882
|
+
minWidth: 0,
|
|
10883
|
+
font: "inherit",
|
|
10884
|
+
fontSize: 13,
|
|
10885
|
+
padding: "6px 8px",
|
|
10886
|
+
border: "1px solid var(--seed-color-border-neutral)",
|
|
10887
|
+
borderRadius: 6,
|
|
10888
|
+
background: "var(--seed-color-bg-surface)",
|
|
10889
|
+
color: "var(--seed-color-fg-neutral)"
|
|
10890
|
+
}
|
|
10891
|
+
})
|
|
10892
|
+
}, view.id) : /*#__PURE__*/ jsxRuntime.jsxs("div", {
|
|
10893
|
+
style: rowStyle,
|
|
10894
|
+
children: [
|
|
10895
|
+
/*#__PURE__*/ jsxRuntime.jsx("button", {
|
|
10896
|
+
type: "button",
|
|
10897
|
+
onClick: ()=>handleSelect(view.id),
|
|
10898
|
+
"data-active": view.id === activeViewId ? "" : undefined,
|
|
10899
|
+
style: rowButtonStyle,
|
|
10900
|
+
children: view.name
|
|
10901
|
+
}),
|
|
10902
|
+
/*#__PURE__*/ jsxRuntime.jsx("button", {
|
|
10903
|
+
type: "button",
|
|
10904
|
+
onClick: ()=>startRename(view),
|
|
10905
|
+
style: smallButtonStyle,
|
|
10906
|
+
children: renameLabel
|
|
10907
|
+
}),
|
|
10908
|
+
/*#__PURE__*/ jsxRuntime.jsx("button", {
|
|
10909
|
+
type: "button",
|
|
10910
|
+
"aria-label": `${deleteLabel} ${view.name}`,
|
|
10911
|
+
onClick: ()=>deleteView(view.id),
|
|
10912
|
+
style: iconButtonStyle,
|
|
10913
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(IconXmarkLine_default, {
|
|
10914
|
+
size: 13,
|
|
10915
|
+
strokeWidth: 1.75
|
|
10916
|
+
})
|
|
10917
|
+
})
|
|
10918
|
+
]
|
|
10919
|
+
}, view.id)),
|
|
10920
|
+
activeViewId && /*#__PURE__*/ jsxRuntime.jsx("button", {
|
|
10921
|
+
type: "button",
|
|
10922
|
+
onClick: handleUpdate,
|
|
10923
|
+
style: {
|
|
10924
|
+
...rowButtonStyle,
|
|
10925
|
+
color: "var(--seed-color-fg-neutral-subtle)"
|
|
10926
|
+
},
|
|
10927
|
+
children: updateViewLabel
|
|
10928
|
+
}),
|
|
10929
|
+
/*#__PURE__*/ jsxRuntime.jsxs("button", {
|
|
10930
|
+
type: "button",
|
|
10931
|
+
onClick: handleCreate,
|
|
10932
|
+
style: rowButtonStyle,
|
|
10933
|
+
children: [
|
|
10934
|
+
/*#__PURE__*/ jsxRuntime.jsx(IconPlusLine_default, {
|
|
10935
|
+
size: 13,
|
|
10936
|
+
strokeWidth: 1.75
|
|
10937
|
+
}),
|
|
10938
|
+
newViewLabel
|
|
10939
|
+
]
|
|
10940
|
+
})
|
|
10941
|
+
]
|
|
10942
|
+
});
|
|
10943
|
+
}
|
|
10944
|
+
|
|
10333
10945
|
exports.DataTable = DataTable;
|
|
10946
|
+
exports.DataTableViewsControl = DataTableViewsControl;
|