diff --git a/src/components/ui/data-table/DataTableLayout.vue b/src/components/ui/data-table/DataTableLayout.vue index 4b923529..2f2b2c39 100644 --- a/src/components/ui/data-table/DataTableLayout.vue +++ b/src/components/ui/data-table/DataTableLayout.vue @@ -12,23 +12,57 @@ - - @@ -144,8 +178,10 @@ + + diff --git a/src/lib/table/__tests__/useVrcxVueTable.test.js b/src/lib/table/__tests__/useVrcxVueTable.test.js index 4f9cbbc3..dd032a1a 100644 --- a/src/lib/table/__tests__/useVrcxVueTable.test.js +++ b/src/lib/table/__tests__/useVrcxVueTable.test.js @@ -2,6 +2,10 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { useVrcxVueTable } from '../useVrcxVueTable'; +/** + * + * @param {...any} ids + */ function makeColumns(...ids) { return ids.map((id) => ({ id, header: id, accessorKey: id })); } @@ -123,4 +127,75 @@ describe('useVrcxVueTable persistence', () => { expect(sorting.value).toEqual([{ id: 'date', desc: true }]); }); + + it('persists columnOrder to localStorage when order changes', async () => { + const { columnOrder } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date', 'status'), + persistKey: 'test-col-order' + }); + + columnOrder.value = ['date', 'name', 'status']; + + await new Promise((r) => setTimeout(r, 300)); + + const stored = JSON.parse( + localStorage.getItem('vrcx:table:test-col-order') + ); + expect(stored).toBeTruthy(); + expect(stored.columnOrder).toEqual(['date', 'name', 'status']); + }); + + it('restores persisted columnOrder on init', () => { + localStorage.setItem( + 'vrcx:table:test-restore-order', + JSON.stringify({ columnOrder: ['status', 'name', 'date'] }) + ); + + const { columnOrder } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date', 'status'), + persistKey: 'test-restore-order' + }); + + expect(columnOrder.value).toEqual(['status', 'name', 'date']); + }); + + it('filters stale columnOrder entries on persist', async () => { + const { columnOrder } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date'), + persistKey: 'test-stale-order' + }); + + // Include a column ID that doesn't exist + columnOrder.value = ['removed_col', 'date', 'name']; + + await new Promise((r) => setTimeout(r, 300)); + + const stored = JSON.parse( + localStorage.getItem('vrcx:table:test-stale-order') + ); + expect(stored).toBeTruthy(); + // 'removed_col' should be filtered out + expect(stored.columnOrder).toEqual(['date', 'name']); + }); + + it('does not persist columnOrder when persistColumnOrder is false', async () => { + const { columnOrder } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date'), + persistKey: 'test-no-persist-order', + persistColumnOrder: false + }); + + columnOrder.value = ['date', 'name']; + + await new Promise((r) => setTimeout(r, 300)); + + const stored = JSON.parse( + localStorage.getItem('vrcx:table:test-no-persist-order') + ); + expect(stored?.columnOrder).toBeUndefined(); + }); }); diff --git a/src/lib/table/useVrcxVueTable.js b/src/lib/table/useVrcxVueTable.js index 5e959c55..4b93829a 100644 --- a/src/lib/table/useVrcxVueTable.js +++ b/src/lib/table/useVrcxVueTable.js @@ -9,6 +9,10 @@ import { } from '@tanstack/vue-table'; import { computed, ref, unref, watch } from 'vue'; +/** + * + * @param str + */ function safeJsonParse(str) { if (!str) { return null; @@ -20,6 +24,11 @@ function safeJsonParse(str) { } } +/** + * + * @param fn + * @param wait + */ function debounce(fn, wait) { let t = 0; return (...args) => { @@ -30,6 +39,11 @@ function debounce(fn, wait) { }; } +/** + * + * @param sizing + * @param columns + */ function filterSizingByColumns(sizing, columns) { if (!sizing || typeof sizing !== 'object') { return {}; @@ -44,6 +58,11 @@ function filterSizingByColumns(sizing, columns) { return out; } +/** + * + * @param sorting + * @param columns + */ function filterSortingByColumns(sorting, columns) { if (!Array.isArray(sorting)) { return []; @@ -52,10 +71,31 @@ function filterSortingByColumns(sorting, columns) { return sorting.filter((s) => s && ids.has(s.id)); } +/** + * + * @param order + * @param columns + */ +function filterOrderByColumns(order, columns) { + if (!Array.isArray(order)) { + return []; + } + const ids = new Set((columns ?? []).map((c) => c?.id).filter(Boolean)); + return order.filter((id) => ids.has(id)); +} + +/** + * + * @param col + */ function getColumnId(col) { return col?.id ?? col?.accessorKey ?? null; } +/** + * + * @param columns + */ function findStretchColumnId(columns) { if (!Array.isArray(columns)) { return null; @@ -68,16 +108,32 @@ function findStretchColumnId(columns) { return null; } +/** + * + * @param updaterOrValue + * @param targetRef + */ function setRef(updaterOrValue, targetRef) { targetRef.value = isFunction(updaterOrValue) ? updaterOrValue(targetRef.value) : updaterOrValue; } +/** + * + * @param func + */ function resolveMaybeGetter(func) { return typeof func === 'function' ? func() : unref(func); } +/** + * + * @param columns + * @param enabled + * @param spacerId + * @param stretchAfterId + */ function withSpacerColumn(columns, enabled, spacerId, stretchAfterId) { if (!enabled) { return columns; @@ -116,6 +172,10 @@ function withSpacerColumn(columns, enabled, spacerId, stretchAfterId) { return [...columns, spacerColumn]; } +/** + * + * @param options + */ export function useVrcxVueTable(options) { const { getRowId, @@ -137,12 +197,16 @@ export function useVrcxVueTable(options) { columnResizeMode = 'onChange', initialColumnSizing, + enableColumnReorder = true, + initialColumnOrder, + fillRemainingSpace = true, spacerColumnId = '__spacer', persistKey, persistColumnSizing = true, persistSorting = true, + persistColumnOrder = true, persistDebounceMs = 200, tableOptions = {} @@ -157,9 +221,13 @@ export function useVrcxVueTable(options) { const pagination = ref(initialPagination ?? { pageIndex: 0, pageSize: 50 }); const columnPinning = ref(initialColumnPinning ?? { left: [], right: [] }); const columnSizing = ref(initialColumnSizing ?? {}); + const columnOrder = ref(initialColumnOrder ?? []); const storageKey = persistKey ? `vrcx:table:${persistKey}` : null; + /** + * + */ function readPersisted() { if (!storageKey) { return null; @@ -167,6 +235,10 @@ export function useVrcxVueTable(options) { return safeJsonParse(localStorage.getItem(storageKey)); } + /** + * + * @param patch + */ function writePersisted(patch) { if (!storageKey) { return; @@ -188,11 +260,28 @@ export function useVrcxVueTable(options) { columnSizing.value = persisted.columnSizing; } + if ( + persisted && + persistColumnOrder && + Array.isArray(persisted.columnOrder) + ) { + columnOrder.value = persisted.columnOrder; + } + const state = {}; const handlers = {}; const rowModels = {}; const extra = {}; + /** + * + * @param enabled + * @param key + * @param r + * @param onChangeKey + * @param rowModelPart + * @param extraPart + */ function register(enabled, key, r, onChangeKey, rowModelPart, extraPart) { if (!enabled) { return; @@ -241,6 +330,13 @@ export function useVrcxVueTable(options) { { enableColumnResizing: true, columnResizeMode } ); + register( + enableColumnReorder, + 'columnOrder', + columnOrder, + 'onColumnOrderChange' + ); + if (enableFiltering) { Object.assign(rowModels, { getFilteredRowModel: getFilteredRowModel() @@ -324,12 +420,26 @@ export function useVrcxVueTable(options) { ); } + if (storageKey && persistColumnOrder) { + watch( + columnOrder, + (val) => { + const cols = table.getAllLeafColumns?.() ?? []; + persistWrite({ + columnOrder: filterOrderByColumns(val, cols) + }); + }, + { deep: true } + ); + } + return { table, sorting, pagination, expanded, columnPinning, - columnSizing + columnSizing, + columnOrder }; }