diff --git a/src/components/ui/data-table/__tests__/dataTableHelpers.test.js b/src/components/ui/data-table/__tests__/dataTableHelpers.test.js
index d8d6bb35..75fdf91a 100644
--- a/src/components/ui/data-table/__tests__/dataTableHelpers.test.js
+++ b/src/components/ui/data-table/__tests__/dataTableHelpers.test.js
@@ -85,12 +85,12 @@ describe('getToggleableColumns', () => {
expect(getToggleableColumns(cols)[0].id).toBe('name');
});
- it('excludes stretch columns', () => {
+ it('includes stretch columns', () => {
const cols = [
mockCol('name', { label: 'Name' }),
mockCol('detail', { stretch: true, label: 'Detail' })
];
- expect(getToggleableColumns(cols)).toHaveLength(1);
+ expect(getToggleableColumns(cols)).toHaveLength(2);
});
it('excludes columns with disableVisibilityToggle', () => {
@@ -153,24 +153,31 @@ describe('getColStyle', () => {
describe('isReorderable', () => {
const noPinning = () => false;
- it('returns true for normal column', () => {
- const header = { column: mockCol('name') };
+ it('returns true for normal column with label', () => {
+ const header = { column: mockCol('name', { label: 'Name' }) };
expect(isReorderable(header, noPinning)).toBe(true);
});
+ it('returns false for column without label', () => {
+ const header = { column: mockCol('expander') };
+ expect(isReorderable(header, noPinning)).toBe(false);
+ });
+
it('returns false for spacer column', () => {
const header = { column: { id: '__spacer', columnDef: { meta: {} } } };
expect(isReorderable(header, noPinning)).toBe(false);
});
it('returns false for pinned column', () => {
- const header = { column: mockCol('name') };
+ const header = { column: mockCol('name', { label: 'Name' }) };
const isPinned = () => true;
expect(isReorderable(header, isPinned)).toBe(false);
});
it('returns false for columns with disableReorder', () => {
- const header = { column: mockCol('name', { disableReorder: true }) };
+ const header = {
+ column: mockCol('name', { label: 'Name', disableReorder: true })
+ };
expect(isReorderable(header, noPinning)).toBe(false);
});
diff --git a/src/components/ui/data-table/dataTableHelpers.js b/src/components/ui/data-table/dataTableHelpers.js
index be4b27e7..3e6c5dd6 100644
--- a/src/components/ui/data-table/dataTableHelpers.js
+++ b/src/components/ui/data-table/dataTableHelpers.js
@@ -40,7 +40,6 @@ export function getToggleableColumns(cols) {
if (!Array.isArray(cols)) return [];
return cols.filter((col) => {
if (isSpacer(col)) return false;
- if (isStretch(col)) return false;
if (col.columnDef?.meta?.disableVisibilityToggle) return false;
if (!col.columnDef?.meta?.label) return false;
return true;
@@ -71,6 +70,7 @@ export function isReorderable(header, getPinnedState) {
const col = header?.column;
if (!col) return false;
if (isSpacer(col)) return false;
+ if (!col.columnDef?.meta?.label) return false;
if (getPinnedState?.(col)) return false;
if (col.columnDef?.meta?.disableReorder) return false;
return true;
diff --git a/src/lib/table/__tests__/useVrcxVueTable.test.js b/src/lib/table/__tests__/useVrcxVueTable.test.js
index 71ae5b33..fb5ff71c 100644
--- a/src/lib/table/__tests__/useVrcxVueTable.test.js
+++ b/src/lib/table/__tests__/useVrcxVueTable.test.js
@@ -332,4 +332,88 @@ describe('useVrcxVueTable persistence', () => {
);
expect(stored?.pageSize).toBeUndefined();
});
+
+ it('resetAll clears both columnSizing and columnOrder', async () => {
+ const { columnSizing, columnOrder, resetAll } = useVrcxVueTable({
+ data: [],
+ columns: makeColumns('name', 'date'),
+ persistKey: 'test-reset-all',
+ enableColumnResizing: true,
+ enableColumnReorder: true
+ });
+
+ columnSizing.value = { name: 200 };
+ columnOrder.value = ['date', 'name'];
+ await new Promise((r) => setTimeout(r, 300));
+
+ resetAll();
+
+ expect(columnSizing.value).toEqual({});
+ expect(columnOrder.value).toEqual([]);
+
+ const stored = JSON.parse(
+ localStorage.getItem('vrcx:table:test-reset-all')
+ );
+ expect(stored?.columnSizing).toBeUndefined();
+ expect(stored?.columnOrder).toBeUndefined();
+ });
+
+ it('persists columnOrderLocked to localStorage', async () => {
+ const { columnOrderLocked } = useVrcxVueTable({
+ data: [],
+ columns: makeColumns('name', 'date'),
+ persistKey: 'test-lock-order'
+ });
+
+ expect(columnOrderLocked.value).toBe(false);
+
+ columnOrderLocked.value = true;
+
+ // Watcher is async, wait for it to fire
+ await new Promise((r) => setTimeout(r, 50));
+
+ const stored = JSON.parse(
+ localStorage.getItem('vrcx:table:test-lock-order')
+ );
+ expect(stored?.columnOrderLocked).toBe(true);
+ });
+
+ it('restores columnOrderLocked from localStorage on init', () => {
+ localStorage.setItem(
+ 'vrcx:table:test-restore-lock',
+ JSON.stringify({ columnOrderLocked: true })
+ );
+
+ const { columnOrderLocked } = useVrcxVueTable({
+ data: [],
+ columns: makeColumns('name', 'date'),
+ persistKey: 'test-restore-lock'
+ });
+
+ expect(columnOrderLocked.value).toBe(true);
+ });
+
+ it('attaches controls to table.options.meta when persistKey is set', () => {
+ const { table } = useVrcxVueTable({
+ data: [],
+ columns: makeColumns('name', 'date'),
+ persistKey: 'test-meta'
+ });
+
+ const meta = table.options.meta;
+ expect(meta.resetAll).toBeTypeOf('function');
+ expect(meta.columnOrderLocked).toBeDefined();
+ expect(meta.columnOrderLocked.value).toBe(false);
+ });
+
+ it('does not attach controls to meta without persistKey', () => {
+ const { table } = useVrcxVueTable({
+ data: [],
+ columns: makeColumns('name', 'date')
+ });
+
+ const meta = table.options.meta;
+ expect(meta.resetAll).toBeUndefined();
+ expect(meta.columnOrderLocked).toBeUndefined();
+ });
});
diff --git a/src/lib/table/useVrcxVueTable.js b/src/lib/table/useVrcxVueTable.js
index 4757df35..683cbd00 100644
--- a/src/lib/table/useVrcxVueTable.js
+++ b/src/lib/table/useVrcxVueTable.js
@@ -273,6 +273,21 @@ export function useVrcxVueTable(options) {
localStorage.setItem(storageKey, JSON.stringify(next));
}
+ /**
+ * @param keys
+ */
+ function removePersisted(keys) {
+ if (!storageKey) {
+ return;
+ }
+ const cur = safeJsonParse(localStorage.getItem(storageKey)) ?? {};
+ for (const key of keys) {
+ delete cur[key];
+ }
+ cur.updatedAt = Date.now();
+ localStorage.setItem(storageKey, JSON.stringify(cur));
+ }
+
const persisted = readPersisted();
let resolvedSorting = initialSorting ?? [];
@@ -309,6 +324,9 @@ export function useVrcxVueTable(options) {
};
}
+ // Column order lock — persisted per-table
+ const columnOrderLocked = ref(persisted?.columnOrderLocked === true);
+
const state = {};
const handlers = {};
const rowModels = {};
@@ -417,6 +435,11 @@ export function useVrcxVueTable(options) {
state,
+ meta: {
+ resetAll: storageKey ? resetAll : undefined,
+ columnOrderLocked: storageKey ? columnOrderLocked : undefined
+ },
+
...tableOptions
});
@@ -505,6 +528,20 @@ export function useVrcxVueTable(options) {
);
}
+ if (storageKey) {
+ watch(columnOrderLocked, (val) => {
+ writePersisted({ columnOrderLocked: val });
+ });
+ }
+
+ /**
+ */
+ function resetAll() {
+ columnSizing.value = {};
+ columnOrder.value = [];
+ removePersisted(['columnSizing', 'columnOrder']);
+ }
+
return {
table,
sorting,
@@ -513,6 +550,8 @@ export function useVrcxVueTable(options) {
columnPinning,
columnSizing,
columnOrder,
- columnVisibility
+ columnVisibility,
+ columnOrderLocked,
+ resetAll
};
}
diff --git a/src/localization/en.json b/src/localization/en.json
index f5cf2acf..a838ad4c 100644
--- a/src/localization/en.json
+++ b/src/localization/en.json
@@ -2417,6 +2417,10 @@
"traveling": "Traveling"
},
"table": {
+ "header_menu": {
+ "reset_all": "Reset All",
+ "lock_column_order": "Lock Column Order"
+ },
"pagination": {
"rows_per_page": "Rows per page",
"first": "First",
diff --git a/src/views/PlayerList/columns.jsx b/src/views/PlayerList/columns.jsx
index 4a01bc4c..84cbd030 100644
--- a/src/views/PlayerList/columns.jsx
+++ b/src/views/PlayerList/columns.jsx
@@ -187,7 +187,10 @@ export const createColumns = ({
}),
size: 110,
enableHiding: true,
- meta: { label: () => t('table.playerList.photonId') },
+ meta: {
+ label: () => t('table.playerList.photonId'),
+ disableVisibilityToggle: true
+ },
sortingFn: (rowA, rowB) =>
(rowA.original?.photonId ?? 0) - (rowB.original?.photonId ?? 0),
cell: ({ row }) => {