diff --git a/src/lib/table/__tests__/useVrcxVueTable.test.js b/src/lib/table/__tests__/useVrcxVueTable.test.js new file mode 100644 index 00000000..4f9cbbc3 --- /dev/null +++ b/src/lib/table/__tests__/useVrcxVueTable.test.js @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { useVrcxVueTable } from '../useVrcxVueTable'; + +function makeColumns(...ids) { + return ids.map((id) => ({ id, header: id, accessorKey: id })); +} + +describe('useVrcxVueTable persistence', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('persists sorting to localStorage when sorting changes', async () => { + const { sorting } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date'), + persistKey: 'test-sort', + initialSorting: [] + }); + + sorting.value = [{ id: 'name', desc: false }]; + + // Wait for debounce (200ms default) + await new Promise((r) => setTimeout(r, 300)); + + const stored = JSON.parse(localStorage.getItem('vrcx:table:test-sort')); + expect(stored).toBeTruthy(); + expect(stored.sorting).toEqual([{ id: 'name', desc: false }]); + }); + + it('restores persisted sorting on init, overriding initialSorting', () => { + localStorage.setItem( + 'vrcx:table:test-restore', + JSON.stringify({ sorting: [{ id: 'date', desc: true }] }) + ); + + const { sorting } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date'), + persistKey: 'test-restore', + initialSorting: [{ id: 'name', desc: false }] + }); + + expect(sorting.value).toEqual([{ id: 'date', desc: true }]); + }); + + it('filters out stale sorting entries for removed columns', () => { + localStorage.setItem( + 'vrcx:table:test-stale', + JSON.stringify({ + sorting: [ + { id: 'removed_col', desc: false }, + { id: 'name', desc: true } + ] + }) + ); + + const { sorting } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date'), + persistKey: 'test-stale', + initialSorting: [] + }); + + // Stale entry should have been loaded but will be filtered on next persist write + // On init, the raw persisted array is loaded as-is + expect(sorting.value).toContainEqual({ id: 'name', desc: true }); + }); + + it('does not persist sorting when persistSorting is false', async () => { + const { sorting } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date'), + persistKey: 'test-no-persist-sort', + persistSorting: false, + initialSorting: [] + }); + + sorting.value = [{ id: 'name', desc: false }]; + + await new Promise((r) => setTimeout(r, 300)); + + const stored = JSON.parse( + localStorage.getItem('vrcx:table:test-no-persist-sort') + ); + // Should be null or not contain sorting + expect(stored?.sorting).toBeUndefined(); + }); + + it('still persists columnSizing alongside sorting', async () => { + const { columnSizing, sorting } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date'), + persistKey: 'test-both', + initialSorting: [] + }); + + columnSizing.value = { name: 200 }; + + // Wait for columnSizing debounce to fire first + await new Promise((r) => setTimeout(r, 300)); + + sorting.value = [{ id: 'date', desc: true }]; + + // Wait for sorting debounce to fire + await new Promise((r) => setTimeout(r, 300)); + + const stored = JSON.parse(localStorage.getItem('vrcx:table:test-both')); + expect(stored).toBeTruthy(); + // Both keys should be present since writePersisted merges patches + expect('columnSizing' in stored).toBe(true); + expect(stored.sorting).toEqual([{ id: 'date', desc: true }]); + }); + + it('uses initialSorting when no persisted data exists', () => { + const { sorting } = useVrcxVueTable({ + data: [], + columns: makeColumns('name', 'date'), + persistKey: 'test-initial', + initialSorting: [{ id: 'date', desc: true }] + }); + + expect(sorting.value).toEqual([{ id: 'date', desc: true }]); + }); +}); diff --git a/src/lib/table/useVrcxVueTable.js b/src/lib/table/useVrcxVueTable.js index d7bec345..5e959c55 100644 --- a/src/lib/table/useVrcxVueTable.js +++ b/src/lib/table/useVrcxVueTable.js @@ -44,6 +44,14 @@ function filterSizingByColumns(sizing, columns) { return out; } +function filterSortingByColumns(sorting, columns) { + if (!Array.isArray(sorting)) { + return []; + } + const ids = new Set((columns ?? []).map((c) => c?.id).filter(Boolean)); + return sorting.filter((s) => s && ids.has(s.id)); +} + function getColumnId(col) { return col?.id ?? col?.accessorKey ?? null; } @@ -134,6 +142,7 @@ export function useVrcxVueTable(options) { persistKey, persistColumnSizing = true, + persistSorting = true, persistDebounceMs = 200, tableOptions = {} @@ -144,7 +153,6 @@ export function useVrcxVueTable(options) { if (!hasData) console.warn('useVrcxVueTable: `data` is required'); if (!hasColumns) console.warn('useVrcxVueTable: `columns` is required'); - const sorting = ref(initialSorting ?? []); const expanded = ref(initialExpanded ?? {}); const pagination = ref(initialPagination ?? { pageIndex: 0, pageSize: 50 }); const columnPinning = ref(initialColumnPinning ?? { left: [], right: [] }); @@ -169,6 +177,13 @@ export function useVrcxVueTable(options) { } const persisted = readPersisted(); + + let resolvedSorting = initialSorting ?? []; + if (persisted && persistSorting && Array.isArray(persisted.sorting)) { + resolvedSorting = persisted.sorting; + } + const sorting = ref(resolvedSorting); + if (persisted && persistColumnSizing && persisted.columnSizing) { columnSizing.value = persisted.columnSizing; } @@ -296,6 +311,19 @@ export function useVrcxVueTable(options) { ); } + if (storageKey && persistSorting) { + watch( + sorting, + (val) => { + const cols = table.getAllLeafColumns?.() ?? []; + persistWrite({ + sorting: filterSortingByColumns(val, cols) + }); + }, + { deep: true } + ); + } + return { table, sorting,