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 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+ v-if="header.column.getCanResize?.()"
+ class="absolute right-0 top-0 h-full w-2 cursor-col-resize touch-none select-none opacity-0 transition-opacity group-hover:opacity-100"
+ @mousedown.stop="header.getResizeHandler?.()($event)"
+ @touchstart.stop="header.getResizeHandler?.()($event)">
+
+
+
+
+
@@ -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
};
}