@undevy-org/tablewright 0.2.1 → 0.3.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,11 +1,13 @@
1
- import { useState, useEffect, useMemo, useRef, useCallback, useLayoutEffect } from "react";
1
+ import { useState, useRef, useMemo, useCallback, useLayoutEffect, useEffect } from "react";
2
2
  import { useStore } from "zustand";
3
3
  import { useColumnResize } from "../components/data-table/hooks/use-column-resize.js";
4
4
  import { getVisiblePageNumbers } from "../components/data-table/hooks/use-table-pagination.js";
5
- import { computeColumnGaps } from "../lib/column-gaps.js";
6
- import { computeColumnStats } from "../lib/filter-stats.js";
7
- import { createTableOrchestrationStore, defaultValueForFilter } from "./table-orchestration-store.js";
8
- const EMPTY_ID_SET = /* @__PURE__ */ new Set();
5
+ import { createTableOrchestrationStore } from "./table-orchestration-store.js";
6
+ import { useColumnFilterStats } from "./table-orchestration/use-column-filter-stats.js";
7
+ import { useColumnGaps } from "./table-orchestration/use-column-gaps.js";
8
+ import { useFilteredRows } from "./table-orchestration/use-filtered-rows.js";
9
+ import { useOrchestrationActions } from "./table-orchestration/use-orchestration-actions.js";
10
+ import { useResponsiveColumns } from "./table-orchestration/use-responsive-columns.js";
9
11
  const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
10
12
  function useTableOrchestration(config) {
11
13
  const {
@@ -46,33 +48,11 @@ function useTableOrchestration(config) {
46
48
  const activeView = useStore(store, (s) => s.activeView);
47
49
  const columnsMenuOpen = useStore(store, (s) => s.columnsMenuOpen);
48
50
  const storedSelectedRowId = useStore(store, (s) => s.selectedRowId);
49
- const hasPriorities = Boolean(columnPriorities && Object.keys(columnPriorities).length > 0);
50
- const [isWide, setIsWide] = useState(true);
51
- const [isCompact, setIsCompact] = useState(false);
52
- useEffect(() => {
53
- if (!hasPriorities || typeof window === "undefined") return;
54
- const mql = window.matchMedia(`(min-width: ${wideBreakpoint}px)`);
55
- const onChange = (e) => setIsWide(e.matches);
56
- setIsWide(mql.matches);
57
- mql.addEventListener("change", onChange);
58
- return () => mql.removeEventListener("change", onChange);
59
- }, [hasPriorities, wideBreakpoint]);
60
- useEffect(() => {
61
- if (typeof window === "undefined") return;
62
- const mql = window.matchMedia(`(max-width: ${compactBreakpoint - 1}px)`);
63
- const onChange = (e) => setIsCompact(e.matches);
64
- setIsCompact(mql.matches);
65
- mql.addEventListener("change", onChange);
66
- return () => mql.removeEventListener("change", onChange);
67
- }, [compactBreakpoint]);
68
- const responsiveHiddenColumnIds = useMemo(() => {
69
- if (isWide || !columnPriorities) return EMPTY_ID_SET;
70
- const hidden = /* @__PURE__ */ new Set();
71
- for (const [id, priority] of Object.entries(columnPriorities)) {
72
- if (priority === 3) hidden.add(id);
73
- }
74
- return hidden;
75
- }, [isWide, columnPriorities]);
51
+ const { responsiveHiddenColumnIds, isCompact } = useResponsiveColumns(
52
+ columnPriorities,
53
+ wideBreakpoint,
54
+ compactBreakpoint
55
+ );
76
56
  const selectedIdsRef = useRef(storedSelectedIds);
77
57
  const headerCheckStateRef = useRef(false);
78
58
  const sortState = useMemo(() => {
@@ -92,89 +72,22 @@ function useTableOrchestration(config) {
92
72
  }
93
73
  return viewPresets.find((v) => v.key === activeView) ?? viewPresets[0];
94
74
  }, [activeView, viewPresets]);
95
- const allColumnFilterStats = useMemo(() => {
96
- const result = {};
97
- for (const [columnId, cfg] of Object.entries(filterConfigs)) {
98
- if (cfg.type === "compound" && cfg.subFilters) {
99
- for (const sub of cfg.subFilters) {
100
- const s = computeColumnStats(rows, sub.field, {
101
- type: sub.type,
102
- label: sub.label,
103
- options: sub.options
104
- });
105
- if (s) result[`${columnId}:${sub.key}`] = s;
106
- }
107
- } else {
108
- const s = computeColumnStats(rows, cfg.fieldName ?? columnId, cfg);
109
- if (s) result[columnId] = s;
110
- }
111
- }
112
- return result;
113
- }, [rows, filterConfigs]);
114
- const filteredRows = useMemo(() => {
115
- const search = searchQuery.trim().toLowerCase();
116
- const filtered = rows.filter((row) => {
117
- if (activePreset.customFilter && !activePreset.customFilter(row)) return false;
118
- if (search.length > 0 && !getSearchText(row).includes(search)) return false;
119
- return columnFilters.every((filter) => filterRow(row, filter));
120
- });
121
- if (!currentSort) return filtered;
122
- const { columnId, direction } = currentSort;
123
- return [...filtered].sort((a, b) => {
124
- const aVal = getSortValue(a, columnId);
125
- const bVal = getSortValue(b, columnId);
126
- if (aVal < bVal) return direction === "asc" ? -1 : 1;
127
- if (aVal > bVal) return direction === "asc" ? 1 : -1;
128
- return 0;
129
- });
130
- }, [
75
+ const filteredRows = useFilteredRows({
131
76
  rows,
77
+ activePreset,
132
78
  searchQuery,
133
79
  columnFilters,
134
80
  currentSort,
135
- activePreset,
136
- getSearchText,
137
81
  filterRow,
82
+ getSearchText,
138
83
  getSortValue
139
- ]);
140
- const filteredColumnFilterStats = useMemo(() => {
141
- if (columnFilters.length === 0 && searchQuery.trim().length === 0) return null;
142
- const result = {};
143
- for (const [columnId, cfg] of Object.entries(filterConfigs)) {
144
- if (cfg.type === "compound" && cfg.subFilters) {
145
- for (const sub of cfg.subFilters) {
146
- const s = computeColumnStats(filteredRows, sub.field, {
147
- type: sub.type,
148
- label: sub.label,
149
- options: sub.options
150
- });
151
- if (s) result[`${columnId}:${sub.key}`] = s;
152
- }
153
- } else {
154
- const s = computeColumnStats(filteredRows, cfg.fieldName ?? columnId, cfg);
155
- if (s) result[columnId] = s;
156
- }
157
- }
158
- return result;
159
- }, [filteredRows, filterConfigs, columnFilters, searchQuery]);
160
- const columnFilterStats = useMemo(() => {
161
- const result = {};
162
- for (const [columnId, cfg] of Object.entries(filterConfigs)) {
163
- if (cfg.type === "compound" && cfg.subFilters) {
164
- for (const sub of cfg.subFilters) {
165
- const key = `${columnId}:${sub.key}`;
166
- const all = allColumnFilterStats[key];
167
- if (!all) continue;
168
- result[key] = { all, filtered: filteredColumnFilterStats?.[key] ?? null };
169
- }
170
- } else {
171
- const all = allColumnFilterStats[columnId];
172
- if (!all) continue;
173
- result[columnId] = { all, filtered: filteredColumnFilterStats?.[columnId] ?? null };
174
- }
175
- }
176
- return result;
177
- }, [allColumnFilterStats, filteredColumnFilterStats, filterConfigs]);
84
+ });
85
+ const columnFilterStats = useColumnFilterStats(
86
+ rows,
87
+ filteredRows,
88
+ filterConfigs,
89
+ columnFilters.length > 0 || searchQuery.trim().length > 0
90
+ );
178
91
  const totalPages = Math.max(1, Math.ceil(filteredRows.length / rowsPerPage));
179
92
  const safePage = Math.min(storedPage, totalPages);
180
93
  const page = safePage;
@@ -208,167 +121,59 @@ function useTableOrchestration(config) {
208
121
  );
209
122
  const allOnPageSelected = pageIds.size > 0 && selectedOnPage === pageIds.size;
210
123
  const headerCheckState = allOnPageSelected ? true : selectedOnPage > 0 ? "indeterminate" : false;
211
- const columnGaps = useMemo(
212
- () => columnGroups ? computeColumnGaps(toggleableColumnIds, columnVisibility) : [],
213
- [columnGroups, toggleableColumnIds, columnVisibility]
214
- );
215
- const extendedColumnMeta = useMemo(() => {
216
- if (columnGaps.length === 0) return columnMeta;
217
- const meta = { ...columnMeta };
218
- for (const gap of columnGaps) {
219
- meta[`__gap_after_${gap.afterColumnId ?? "start"}`] = { minW: 32 };
220
- }
221
- return meta;
222
- }, [columnGaps, columnMeta]);
223
- const extendedWidths = useMemo(() => {
224
- if (columnGaps.length === 0) return widths;
225
- const merged = { ...widths };
226
- for (const gap of columnGaps) {
227
- merged[`__gap_after_${gap.afterColumnId ?? "start"}`] = 32;
228
- }
229
- return merged;
230
- }, [widths, columnGaps]);
124
+ const { columnGaps, extendedColumnMeta, extendedWidths } = useColumnGaps({
125
+ columnGroups,
126
+ toggleableColumnIds,
127
+ columnVisibility,
128
+ columnMeta,
129
+ widths
130
+ });
231
131
  const hiddenColumnsCount = useMemo(() => {
232
132
  return toggleableColumnIds.filter((id) => columnVisibility[id] === false).length;
233
133
  }, [toggleableColumnIds, columnVisibility]);
234
134
  const activeCustomFilterChip = activePreset.customFilterChip ?? null;
235
135
  selectedIdsRef.current = selectedIds;
236
136
  headerCheckStateRef.current = headerCheckState;
237
- const handleSearchSubmit = useCallback(
238
- (e) => {
239
- e.preventDefault();
240
- store.getState().submitSearch();
241
- },
242
- [store]
243
- );
244
- const handleHeaderSort = useCallback(
245
- (columnId, direction) => {
246
- store.getState().toggleSort(columnId, direction);
247
- },
248
- [store]
249
- );
250
- const handleViewChange = useCallback(
251
- (key) => {
252
- if (key === "custom") return;
253
- const preset = viewPresets.find((v) => v.key === key);
254
- if (!preset) return;
255
- store.getState().applyPreset({
256
- key,
257
- filters: preset.filters,
258
- sort: preset.sort,
259
- columnVisibility: preset.columnVisibility
260
- });
261
- },
262
- [viewPresets, store]
263
- );
264
- const handleAddColumnFilter = useCallback(
265
- (columnId) => {
266
- const cfg = filterConfigs[columnId];
267
- if (!cfg) return;
268
- store.getState().addColumnFilter(columnId, defaultValueForFilter(cfg));
269
- },
270
- [filterConfigs, store]
271
- );
272
- const handleOpenExistingFilter = useCallback(
273
- (columnId) => store.getState().setRequestOpenFilterId(columnId),
274
- [store]
275
- );
276
- const handleRequestOpenHandled = useCallback(
277
- () => store.getState().setRequestOpenFilterId(null),
278
- [store]
279
- );
280
- const handleRemoveColumnFilter = useCallback(
281
- (columnId) => store.getState().removeColumnFilter(columnId),
282
- [store]
283
- );
284
- const handleChangeColumnFilter = useCallback(
285
- (columnId, value) => store.getState().changeColumnFilter(columnId, value),
286
- [store]
287
- );
288
- const handleSetColumnFilter = useCallback(
289
- (columnId, value) => store.getState().setColumnFilter(columnId, value),
290
- [store]
291
- );
292
- const handleHeaderFilterClick = useCallback(
293
- (columnId) => {
294
- const state = store.getState();
295
- const isActive = state.columnFilters.some((f) => f.columnId === columnId);
296
- if (!isActive) {
297
- const cfg = filterConfigs[columnId];
298
- if (cfg) state.addColumnFilter(columnId, defaultValueForFilter(cfg));
299
- }
300
- state.setRequestOpenFilterId(columnId);
301
- },
302
- [filterConfigs, store]
303
- );
304
- const handleHideColumn = useCallback(
305
- (columnId) => store.getState().hideColumn(columnId),
306
- [store]
307
- );
308
- const handleResetColumns = useCallback(() => {
309
- store.getState().resetColumns();
310
- setWidths(() => {
311
- const w = {};
312
- for (const [id, m] of Object.entries(columnMeta)) w[id] = m.minW;
313
- return w;
314
- });
315
- }, [store, setWidths, columnMeta]);
316
- const handleRemoveSort = useCallback(() => store.getState().removeSort(), [store]);
317
- const handleRemoveCustomFilter = useCallback(() => {
318
- store.getState().goCustom();
319
- store.getState().setPage(1);
320
- }, [store]);
321
- const handleClearAll = useCallback(
322
- () => store.getState().clearAll(defaultViewKey),
323
- [defaultViewKey, store]
324
- );
325
- const handleRefresh = useCallback(() => store.getState().startRefresh(), [store]);
326
- const handleToggleRowSelection = useCallback(
327
- (id) => store.getState().toggleRowSelection(id),
328
- [store]
329
- );
330
- const handleToggleHeaderCheck = useCallback(() => {
331
- const ids = paginatedRows.map(getRowId);
332
- store.getState().toggleHeaderCheck(ids, allOnPageSelected);
333
- }, [paginatedRows, allOnPageSelected, getRowId, store]);
334
- const handleExpandRow = useCallback(
335
- (row, e) => {
336
- e.stopPropagation();
337
- const id = getRowId(row);
338
- store.getState().toggleSelectedRowId(id);
339
- },
340
- [getRowId, store]
341
- );
342
- const handleExpandGap = useCallback(
343
- (hiddenIds) => store.getState().showColumns(hiddenIds),
344
- [store]
345
- );
346
- const handlePageChange = useCallback((p) => store.getState().setPage(p), [store]);
347
- const handleRowsPerPageChange = useCallback(
348
- (value) => store.getState().setRowsPerPage(value),
349
- [store]
350
- );
351
- const setSearchInput = useCallback((v) => store.getState().setSearchInput(v), [store]);
352
- const setColumnVisibility = useCallback(
353
- (next) => store.getState().setColumnVisibility(next),
354
- [store]
355
- );
356
- const setDensity = useCallback(
357
- (v) => store.getState().setDensity(v),
358
- [store]
359
- );
360
- const setSelectedIds = useCallback(
361
- (next) => store.getState().setSelectedIds(next),
362
- [store]
363
- );
364
- const setColumnsMenuOpen = useCallback(
365
- (v) => store.getState().setColumnsMenuOpen(v),
366
- [store]
367
- );
368
- const setSelectedRowId = useCallback(
369
- (v) => store.getState().setSelectedRowId(v),
370
- [store]
371
- );
137
+ const {
138
+ handleSearchSubmit,
139
+ handleHeaderSort,
140
+ handleViewChange,
141
+ handleAddColumnFilter,
142
+ handleOpenExistingFilter,
143
+ handleRequestOpenHandled,
144
+ handleRemoveColumnFilter,
145
+ handleChangeColumnFilter,
146
+ handleSetColumnFilter,
147
+ handleHeaderFilterClick,
148
+ handleHideColumn,
149
+ handleResetColumns,
150
+ handleRemoveSort,
151
+ handleRemoveCustomFilter,
152
+ handleClearAll,
153
+ handleRefresh,
154
+ handleToggleRowSelection,
155
+ handleToggleHeaderCheck,
156
+ handleExpandRow,
157
+ handleExpandGap,
158
+ handlePageChange,
159
+ handleRowsPerPageChange,
160
+ setSearchInput,
161
+ setColumnVisibility,
162
+ setDensity,
163
+ setSelectedIds,
164
+ setColumnsMenuOpen,
165
+ setSelectedRowId
166
+ } = useOrchestrationActions({
167
+ store,
168
+ viewPresets,
169
+ filterConfigs,
170
+ columnMeta,
171
+ setWidths,
172
+ defaultViewKey,
173
+ paginatedRows,
174
+ allOnPageSelected,
175
+ getRowId
176
+ });
372
177
  const getSelectedIds = useCallback(() => selectedIdsRef.current, []);
373
178
  const getHeaderCheckState = useCallback(() => headerCheckStateRef.current, []);
374
179
  return {
package/dist/es/index.js CHANGED
@@ -65,7 +65,7 @@ import { getCompoundValue, getDateRange, getNumberRange, getStringArray, isFilte
65
65
  import { useTableOrchestration } from "./hooks/useTableOrchestration.js";
66
66
  import { createTableOrchestrationStore, defaultValueForFilter } from "./hooks/table-orchestration-store.js";
67
67
  import { useRowDragOverride } from "./hooks/useRowDragOverride.js";
68
- import { computeColumnGaps } from "./lib/column-gaps.js";
68
+ import { computeColumnGaps, gapColumnId } from "./lib/column-gaps.js";
69
69
  import { computeColumnStats } from "./lib/filter-stats.js";
70
70
  import { formatEnumLabel } from "./lib/format-enum.js";
71
71
  import { displayTimeZone, formatDateParts, formatLiveTimestamp, parsePossibleTimestamp } from "./lib/format-timestamp.js";
@@ -184,6 +184,7 @@ export {
184
184
  formatDateParts,
185
185
  formatEnumLabel,
186
186
  formatLiveTimestamp,
187
+ gapColumnId,
187
188
  getCompoundValue,
188
189
  getDateRange,
189
190
  getNumberRange,
@@ -1,3 +1,6 @@
1
+ function gapColumnId(afterColumnId) {
2
+ return `__gap_after_${afterColumnId ?? "start"}`;
3
+ }
1
4
  function computeColumnGaps(orderedColumnIds, columnVisibility) {
2
5
  const gaps = [];
3
6
  let lastVisibleId = null;
@@ -19,5 +22,6 @@ function computeColumnGaps(orderedColumnIds, columnVisibility) {
19
22
  return gaps;
20
23
  }
21
24
  export {
22
- computeColumnGaps
25
+ computeColumnGaps,
26
+ gapColumnId
23
27
  };