feat: custom show/hide datatable col

This commit is contained in:
pa
2026-03-04 23:12:41 +09:00
parent 1decec4c69
commit 1be9d13cd4
11 changed files with 400 additions and 104 deletions
@@ -10,7 +10,75 @@
<colgroup>
<col v-for="col in table.getVisibleLeafColumns()" :key="col.id" :style="getColStyle(col)" />
</colgroup>
<TableHeader>
<ContextMenu v-if="enableColumnVisibility">
<ContextMenuTrigger as-child>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<template v-if="enableColumnReorder">
<DragDropProvider @dragEnd="onHeaderDragEnd">
<template v-for="(header, hIdx) in headerGroup.headers" :key="header.id">
<SortableTableHead
v-if="isReorderable(header)"
:header="header"
:index="reorderableIndex(headerGroup.headers, hIdx)"
:header-class="getHeaderClass(header)"
:pinned-style="getPinnedStyle(header.column)" />
<TableHead
v-else
:class="getHeaderClass(header)"
:style="getPinnedStyle(header.column)">
<template v-if="!header.isPlaceholder">
<FlexRender
:render="header.column.columnDef.header"
:props="header.getContext()" />
<div
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)">
<div
class="absolute right-0 top-0 h-full w-px bg-border dark:bg-border dark:brightness-[2]" />
</div>
</template>
</TableHead>
</template>
</DragDropProvider>
</template>
<template v-else>
<TableHead
v-for="header in headerGroup.headers"
:key="header.id"
:class="getHeaderClass(header)"
:style="getPinnedStyle(header.column)">
<template v-if="!header.isPlaceholder">
<FlexRender
:render="header.column.columnDef.header"
:props="header.getContext()" />
<div
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)">
<div
class="absolute right-0 top-0 h-full w-px bg-border dark:bg-border dark:brightness-[2]" />
</div>
</template>
</TableHead>
</template>
</TableRow>
</TableHeader>
</ContextMenuTrigger>
<ContextMenuContent class="w-48">
<ContextMenuCheckboxItem
v-for="col in toggleableColumns"
:key="col.id"
:model-value="col.getIsVisible()"
@update:model-value="col.toggleVisibility(!!$event)">
{{ resolveHeaderLabel(col) }}
</ContextMenuCheckboxItem>
</ContextMenuContent>
</ContextMenu>
<TableHeader v-else>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<template v-if="enableColumnReorder">
<DragDropProvider @dragEnd="onHeaderDragEnd">
@@ -196,7 +264,7 @@
} from '../pagination';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
import { ContextMenu, ContextMenuTrigger } from '../context-menu';
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuTrigger } from '../context-menu';
import DataTableEmpty from './DataTableEmpty.vue';
import SortableTableHead from './SortableTableHead.vue';
@@ -256,6 +324,10 @@
enableColumnReorder: {
type: Boolean,
default: true
},
enableColumnVisibility: {
type: Boolean,
default: true
}
});
@@ -389,6 +461,23 @@
props.table.setColumnOrder(fullOrder);
};
const toggleableColumns = computed(() => {
const cols = props.table?.getAllLeafColumns?.() ?? [];
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;
});
});
const resolveHeaderLabel = (col) => {
const label = col?.columnDef?.meta?.label;
if (typeof label === 'function') return label();
return label ?? col?.id ?? '';
};
const getColStyle = (col) => {
if (isSpacer(col)) return { width: '0px' };
@@ -198,4 +198,73 @@ describe('useVrcxVueTable persistence', () => {
);
expect(stored?.columnOrder).toBeUndefined();
});
it('persists columnVisibility to localStorage when visibility changes', async () => {
const { columnVisibility } = useVrcxVueTable({
data: [],
columns: makeColumns('name', 'date', 'status'),
persistKey: 'test-col-vis'
});
columnVisibility.value = { name: false, status: true };
await new Promise((r) => setTimeout(r, 300));
const stored = JSON.parse(
localStorage.getItem('vrcx:table:test-col-vis')
);
expect(stored).toBeTruthy();
expect(stored.columnVisibility).toEqual({ name: false, status: true });
});
it('restores persisted columnVisibility on init', () => {
localStorage.setItem(
'vrcx:table:test-restore-vis',
JSON.stringify({ columnVisibility: { date: false } })
);
const { columnVisibility } = useVrcxVueTable({
data: [],
columns: makeColumns('name', 'date', 'status'),
persistKey: 'test-restore-vis'
});
expect(columnVisibility.value).toEqual({ date: false });
});
it('filters stale columnVisibility entries on persist', async () => {
const { columnVisibility } = useVrcxVueTable({
data: [],
columns: makeColumns('name', 'date'),
persistKey: 'test-stale-vis'
});
columnVisibility.value = { removed_col: false, name: false };
await new Promise((r) => setTimeout(r, 300));
const stored = JSON.parse(
localStorage.getItem('vrcx:table:test-stale-vis')
);
expect(stored).toBeTruthy();
expect(stored.columnVisibility).toEqual({ name: false });
});
it('does not persist columnVisibility when persistColumnVisibility is false', async () => {
const { columnVisibility } = useVrcxVueTable({
data: [],
columns: makeColumns('name', 'date'),
persistKey: 'test-no-persist-vis',
persistColumnVisibility: false
});
columnVisibility.value = { name: false };
await new Promise((r) => setTimeout(r, 300));
const stored = JSON.parse(
localStorage.getItem('vrcx:table:test-no-persist-vis')
);
expect(stored?.columnVisibility).toBeUndefined();
});
});
+50 -1
View File
@@ -84,6 +84,25 @@ function filterOrderByColumns(order, columns) {
return order.filter((id) => ids.has(id));
}
/**
*
* @param visibility
* @param columns
*/
function filterVisibilityByColumns(visibility, columns) {
if (!visibility || typeof visibility !== 'object') {
return {};
}
const ids = new Set((columns ?? []).map((c) => c?.id).filter(Boolean));
const out = {};
for (const [key, value] of Object.entries(visibility)) {
if (ids.has(key)) {
out[key] = value;
}
}
return out;
}
/**
*
* @param col
@@ -200,6 +219,9 @@ export function useVrcxVueTable(options) {
enableColumnReorder = true,
initialColumnOrder,
enableColumnVisibility = true,
initialColumnVisibility,
fillRemainingSpace = true,
spacerColumnId = '__spacer',
@@ -207,6 +229,7 @@ export function useVrcxVueTable(options) {
persistColumnSizing = true,
persistSorting = true,
persistColumnOrder = true,
persistColumnVisibility = true,
persistDebounceMs = 200,
tableOptions = {}
@@ -222,6 +245,7 @@ export function useVrcxVueTable(options) {
const columnPinning = ref(initialColumnPinning ?? { left: [], right: [] });
const columnSizing = ref(initialColumnSizing ?? {});
const columnOrder = ref(initialColumnOrder ?? []);
const columnVisibility = ref(initialColumnVisibility ?? {});
const storageKey = persistKey ? `vrcx:table:${persistKey}` : null;
@@ -268,6 +292,10 @@ export function useVrcxVueTable(options) {
columnOrder.value = persisted.columnOrder;
}
if (persisted && persistColumnVisibility && persisted.columnVisibility) {
columnVisibility.value = persisted.columnVisibility;
}
const state = {};
const handlers = {};
const rowModels = {};
@@ -337,6 +365,13 @@ export function useVrcxVueTable(options) {
'onColumnOrderChange'
);
register(
enableColumnVisibility,
'columnVisibility',
columnVisibility,
'onColumnVisibilityChange'
);
if (enableFiltering) {
Object.assign(rowModels, {
getFilteredRowModel: getFilteredRowModel()
@@ -433,6 +468,19 @@ export function useVrcxVueTable(options) {
);
}
if (storageKey && persistColumnVisibility) {
watch(
columnVisibility,
(val) => {
const cols = table.getAllLeafColumns?.() ?? [];
persistWrite({
columnVisibility: filterVisibilityByColumns(val, cols)
});
},
{ deep: true }
);
}
return {
table,
sorting,
@@ -440,6 +488,7 @@ export function useVrcxVueTable(options) {
expanded,
columnPinning,
columnSizing,
columnOrder
columnOrder,
columnVisibility
};
}
+5 -1
View File
@@ -225,6 +225,7 @@ export const columns = [
{
accessorKey: 'created_at',
size: 140,
meta: { label: () => t('table.feed.date') },
header: ({ column }) => (
<Button
variant="ghost"
@@ -257,6 +258,7 @@ export const columns = [
accessorKey: 'type',
size: 130,
header: () => t('table.feed.type'),
meta: { label: () => t('table.feed.type') },
cell: ({ row }) => {
const type = row.getValue('type');
return (
@@ -272,6 +274,7 @@ export const columns = [
accessorKey: 'displayName',
size: 190,
header: () => t('table.feed.user'),
meta: { label: () => t('table.feed.user') },
cell: ({ row }) => {
const { showUserDialog } = useUserStore();
const original = row.original;
@@ -291,7 +294,8 @@ export const columns = [
enableSorting: false,
minSize: 100,
meta: {
stretch: true
stretch: true,
label: () => t('table.feed.detail')
},
cell: ({ row }) => {
const original = row.original;
+62 -35
View File
@@ -17,24 +17,27 @@ import {
const { t } = i18n.global;
const sortButton = ({ column, label, descFirst = false }) => (
<Button
variant="ghost"
size="sm"
class="-ml-2 h-8 px-2"
onClick={() => {
const sorted = column.getIsSorted();
if (!sorted && descFirst) {
column.toggleSorting(true);
return;
}
column.toggleSorting(sorted === 'asc');
}}
>
{label}
<ArrowUpDown class="ml-1 h-4 w-4" />
</Button>
);
const sortButton = ({ column, label, descFirst = false }) => {
const resolvedLabel = typeof label === 'function' ? label() : label;
return (
<Button
variant="ghost"
size="sm"
class="-ml-2 h-8 px-2"
onClick={() => {
const sorted = column.getIsSorted();
if (!sorted && descFirst) {
column.toggleSorting(true);
return;
}
column.toggleSorting(sorted === 'asc');
}}
>
{resolvedLabel}
<ArrowUpDown class="ml-1 h-4 w-4" />
</Button>
);
};
const compareNumbers = (a, b) => (a ?? 0) - (b ?? 0);
@@ -143,10 +146,11 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.no'),
label: () => t('table.friendList.no'),
descFirst: true
}),
size: 100,
meta: { label: () => t('table.friendList.no') },
sortingFn: sortByNumber((row) => row?.$friendNumber ?? 0),
cell: ({ row }) => <span>{row.original?.$friendNumber || ''}</span>
},
@@ -156,6 +160,7 @@ export const createColumns = ({
header: () => t('table.friendList.avatar'),
size: 90,
enableSorting: false,
meta: { label: () => t('table.friendList.avatar') },
cell: ({ row }) => {
const src = userImage(row.original, true);
return src ? (
@@ -175,9 +180,10 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.displayName')
label: () => t('table.friendList.displayName')
}),
size: 200,
meta: { label: () => t('table.friendList.displayName') },
sortingFn: sortByString((row) => row?.displayName ?? ''),
cell: ({ row }) => {
const style = randomUserColours?.value
@@ -196,9 +202,10 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.rank')
label: () => t('table.friendList.rank')
}),
size: 140,
meta: { label: () => t('table.friendList.rank') },
sortingFn: sortByNumber((row) => row?.$trustSortNum ?? 0),
cell: ({ row }) => {
if (randomUserColours?.value) {
@@ -222,11 +229,15 @@ export const createColumns = ({
id: 'status',
accessorFn: (row) => row?.status,
header: ({ column }) =>
sortButton({ column, label: t('table.friendList.status') }),
sortButton({
column,
label: () => t('table.friendList.status')
}),
minSize: 200,
sortingFn: sortByStatus,
meta: {
stretch: true
stretch: true,
label: () => t('table.friendList.status')
},
cell: ({ row }) => {
const status = row.original?.status;
@@ -250,8 +261,12 @@ export const createColumns = ({
id: 'language',
accessorFn: (row) => row?.$languages,
header: ({ column }) =>
sortButton({ column, label: t('table.friendList.language') }),
sortButton({
column,
label: () => t('table.friendList.language')
}),
size: 130,
meta: { label: () => t('table.friendList.language') },
sortingFn: sortByLanguages,
cell: ({ row }) => (
<div class="flex items-center">
@@ -279,6 +294,7 @@ export const createColumns = ({
header: () => t('table.friendList.bioLink'),
size: 130,
enableSorting: false,
meta: { label: () => t('table.friendList.bioLink') },
cell: ({ row }) => (
<div class="flex items-center">
{(row.original?.bioLinks ?? [])
@@ -305,12 +321,13 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.joinCount')
label: () => t('table.friendList.joinCount')
}),
size: 120,
sortingFn: sortByNumber((row) => row?.$joinCount ?? 0),
meta: {
class: 'text-right'
class: 'text-right',
label: () => t('table.friendList.joinCount')
}
},
{
@@ -319,12 +336,13 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.timeTogether')
label: () => t('table.friendList.timeTogether')
}),
size: 140,
sortingFn: sortByNumber((row) => row?.$timeSpent ?? 0),
meta: {
class: 'text-right'
class: 'text-right',
label: () => t('table.friendList.timeTogether')
},
cell: ({ row }) => {
const time = row.original?.$timeSpent;
@@ -337,9 +355,10 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.lastSeen')
label: () => t('table.friendList.lastSeen')
}),
size: 170,
meta: { label: () => t('table.friendList.lastSeen') },
sortingFn: sortByString((row) => row?.$lastSeen ?? ''),
cell: ({ row }) => {
const text = formatDateFilter(row.original?.$lastSeen, 'long');
@@ -352,12 +371,13 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.mutualFriends')
label: () => t('table.friendList.mutualFriends')
}),
size: 120,
sortingFn: sortByNumber((row) => row?.$mutualCount ?? 0),
meta: {
class: 'text-right'
class: 'text-right',
label: () => t('table.friendList.mutualFriends')
},
cell: ({ row }) => {
const count = row.original?.$mutualCount;
@@ -370,9 +390,10 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.lastActivity')
label: () => t('table.friendList.lastActivity')
}),
size: 200,
meta: { label: () => t('table.friendList.lastActivity') },
sortingFn: sortByString((row) => row?.last_activity ?? ''),
cell: ({ row }) => (
<span>
@@ -384,8 +405,12 @@ export const createColumns = ({
id: 'lastLogin',
accessorFn: (row) => row?.last_login,
header: ({ column }) =>
sortButton({ column, label: t('table.friendList.lastLogin') }),
sortButton({
column,
label: () => t('table.friendList.lastLogin')
}),
size: 200,
meta: { label: () => t('table.friendList.lastLogin') },
sortingFn: sortByString((row) => row?.last_login ?? ''),
cell: ({ row }) => (
<span>
@@ -399,9 +424,10 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.friendList.dateJoined')
label: () => t('table.friendList.dateJoined')
}),
size: 120,
meta: { label: () => t('table.friendList.dateJoined') },
sortingFn: sortByString((row) => row?.date_joined ?? ''),
cell: ({ row }) => <span>{row.original?.date_joined ?? ''}</span>
},
@@ -411,7 +437,8 @@ export const createColumns = ({
size: 100,
enableSorting: false,
meta: {
class: 'text-center'
class: 'text-center',
label: t('table.friendList.unfriend')
},
cell: ({ row }) => (
// TODO(icon): verify unfollow icon replacement
+6 -2
View File
@@ -31,6 +31,7 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
{
accessorKey: 'created_at',
size: 120,
meta: { label: () => t('table.friendLog.date') },
header: ({ column }) => (
<Button
variant="ghost"
@@ -64,6 +65,7 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
size: 160,
header: () => t('table.friendLog.type'),
meta: { label: () => t('table.friendLog.type') },
cell: ({ row }) => {
const type = row.getValue('type');
return (
@@ -79,7 +81,8 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
minSize: 80,
header: () => t('table.friendLog.user'),
meta: {
stretch: true
stretch: true,
label: () => t('table.friendLog.user')
},
cell: ({ row }) => {
const original = row.original;
@@ -113,7 +116,8 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
{
id: 'action',
meta: {
class: 'w-[80px] max-w-[80px] text-right'
class: 'w-[80px] max-w-[80px] text-right',
label: () => t('table.friendLog.action')
},
size: 80,
maxSize: 80,
+7 -2
View File
@@ -43,6 +43,7 @@ export const createColumns = ({ getCreatedAt, onDelete, onDeletePrompt }) => {
accessorFn: (row) => getCreatedAt(row),
id: 'created_at',
size: 140,
meta: { label: () => t('table.gameLog.date') },
header: ({ column }) => (
<Button
variant="ghost"
@@ -70,6 +71,7 @@ export const createColumns = ({ getCreatedAt, onDelete, onDeletePrompt }) => {
accessorKey: 'type',
size: 150,
header: () => t('table.gameLog.type'),
meta: { label: () => t('table.gameLog.type') },
cell: ({ row }) => {
const original = row.original;
const label = t(`view.game_log.filters.${original.type}`);
@@ -94,6 +96,7 @@ export const createColumns = ({ getCreatedAt, onDelete, onDeletePrompt }) => {
accessorKey: 'displayName',
size: 200,
header: () => t('table.gameLog.user'),
meta: { label: () => t('table.gameLog.user') },
cell: ({ row }) => {
const original = row.original;
const isFriend = original.isFriend;
@@ -121,7 +124,8 @@ export const createColumns = ({ getCreatedAt, onDelete, onDeletePrompt }) => {
enableSorting: false,
minSize: 150,
meta: {
stretch: true
stretch: true,
label: () => t('table.gameLog.detail')
},
cell: ({ row }) => {
const original = row.original;
@@ -248,7 +252,8 @@ export const createColumns = ({ getCreatedAt, onDelete, onDeletePrompt }) => {
{
id: 'action',
meta: {
class: 'text-right'
class: 'text-right',
label: () => t('table.gameLog.action')
},
size: 90,
minSize: 90,
+8 -3
View File
@@ -32,6 +32,7 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
{
accessorKey: 'created',
size: 120,
meta: { label: () => t('table.moderation.date') },
header: ({ column }) => (
<Button
variant="ghost"
@@ -64,6 +65,7 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
accessorKey: 'type',
size: 140,
header: () => t('table.moderation.type'),
meta: { label: () => t('table.moderation.type') },
cell: ({ row }) => {
const type = row.getValue('type');
const typeKey = `view.moderation.filters.${type}`;
@@ -79,7 +81,8 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
{
accessorKey: 'sourceDisplayName',
meta: {
class: 'overflow-hidden'
class: 'overflow-hidden',
label: () => t('table.moderation.source')
},
size: 120,
header: () => t('table.moderation.source'),
@@ -100,7 +103,8 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
size: 200,
minSize: 80,
meta: {
stretch: true
stretch: true,
label: () => t('table.moderation.target')
},
header: () => t('table.moderation.target'),
cell: ({ row }) => {
@@ -118,7 +122,8 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
{
id: 'action',
meta: {
class: 'text-right'
class: 'text-right',
label: () => t('table.moderation.action')
},
size: 80,
minSize: 80,
+45 -28
View File
@@ -31,24 +31,27 @@ import { i18n } from '../../plugin';
const { t } = i18n.global;
const sortButton = ({ column, label, descFirst = false }) => (
<Button
variant="ghost"
size="sm"
class="-ml-2 h-8 px-2"
onClick={() => {
const sorted = column.getIsSorted();
if (!sorted && descFirst) {
column.toggleSorting(true);
return;
}
column.toggleSorting(sorted === 'asc');
}}
>
{label}
<ArrowUpDown class="ml-1 h-4 w-4" />
</Button>
);
const sortButton = ({ column, label, descFirst = false }) => {
const resolvedLabel = typeof label === 'function' ? label() : label;
return (
<Button
variant="ghost"
size="sm"
class="-ml-2 h-8 px-2"
onClick={() => {
const sorted = column.getIsSorted();
if (!sorted && descFirst) {
column.toggleSorting(true);
return;
}
column.toggleSorting(sorted === 'asc');
}}
>
{resolvedLabel}
<ArrowUpDown class="ml-1 h-4 w-4" />
</Button>
);
};
export function getColumns({
onShowAvatarDialog,
@@ -102,8 +105,12 @@ export function getColumns({
id: 'name',
accessorKey: 'Name',
header: ({ column }) =>
sortButton({ column, label: t('dialog.avatar.info.name') }),
sortButton({
column,
label: () => t('dialog.avatar.info.name')
}),
size: 200,
meta: { label: () => t('dialog.avatar.info.name') },
cell: ({ row }) => {
const ref = row.original;
return (
@@ -124,6 +131,7 @@ export function getColumns({
header: () => t('dialog.avatar.info.platform'),
size: 120,
enableSorting: false,
meta: { label: () => t('dialog.avatar.info.platform') },
cell: ({ row }) => {
const ref = row.original;
const platforms = getAvailablePlatforms(ref.unityPackages);
@@ -160,6 +168,7 @@ export function getColumns({
header: () => t('dialog.avatar.info.tags'),
size: 150,
enableSorting: false,
meta: { label: () => t('dialog.avatar.info.tags') },
cell: ({ row }) => {
const tags = row.original.$tags || [];
if (!tags.length) return null;
@@ -198,6 +207,7 @@ export function getColumns({
accessorKey: 'releaseStatus',
header: () => t('dialog.avatar.tags.public'),
size: 120,
meta: { label: () => t('dialog.avatar.tags.public') },
cell: ({ row }) => {
const ref = row.original;
return (
@@ -215,12 +225,13 @@ export function getColumns({
header: ({ column }) =>
sortButton({
column,
label: t('dialog.avatar.info.time_spent'),
label: () => t('dialog.avatar.info.time_spent'),
descFirst: true
}),
size: 140,
meta: {
class: 'text-right'
class: 'text-right',
label: () => t('dialog.avatar.info.time_spent')
},
cell: ({ row }) => {
const time = row.original?.$timeSpent;
@@ -237,12 +248,13 @@ export function getColumns({
header: ({ column }) =>
sortButton({
column,
label: t('dialog.avatar.info.version'),
label: () => t('dialog.avatar.info.version'),
descFirst: true
}),
size: 90,
meta: {
class: 'text-right'
class: 'text-right',
label: () => t('dialog.avatar.info.version')
},
cell: ({ row }) => (
<span class=" text-sm">{row.original.version ?? '-'}</span>
@@ -255,9 +267,10 @@ export function getColumns({
header: ({ column }) =>
sortButton({
column,
label: t('dialog.avatar.info.pc_performance')
label: () => t('dialog.avatar.info.pc_performance')
}),
size: 140,
meta: { label: () => t('dialog.avatar.info.pc_performance') },
cell: ({ row }) => {
const perf = getPlatformInfo(row.original.unityPackages)?.pc
?.performanceRating;
@@ -276,9 +289,10 @@ export function getColumns({
header: ({ column }) =>
sortButton({
column,
label: t('dialog.avatar.info.android_performance')
label: () => t('dialog.avatar.info.android_performance')
}),
size: 140,
meta: { label: () => t('dialog.avatar.info.android_performance') },
cell: ({ row }) => {
const perf = getPlatformInfo(row.original.unityPackages)
?.android?.performanceRating;
@@ -297,9 +311,10 @@ export function getColumns({
header: ({ column }) =>
sortButton({
column,
label: t('dialog.avatar.info.ios_performance')
label: () => t('dialog.avatar.info.ios_performance')
}),
size: 140,
meta: { label: () => t('dialog.avatar.info.ios_performance') },
cell: ({ row }) => {
const perf = getPlatformInfo(row.original.unityPackages)?.ios
?.performanceRating;
@@ -316,10 +331,11 @@ export function getColumns({
header: ({ column }) =>
sortButton({
column,
label: t('dialog.avatar.info.last_updated'),
label: () => t('dialog.avatar.info.last_updated'),
descFirst: true
}),
size: 160,
meta: { label: () => t('dialog.avatar.info.last_updated') },
cell: ({ row }) => {
const ref = row.original;
return (
@@ -335,10 +351,11 @@ export function getColumns({
header: ({ column }) =>
sortButton({
column,
label: t('dialog.avatar.info.created_at'),
label: () => t('dialog.avatar.info.created_at'),
descFirst: true
}),
size: 160,
meta: { label: () => t('dialog.avatar.info.created_at') },
cell: ({ row }) => {
const ref = row.original;
return (
+11 -4
View File
@@ -102,6 +102,7 @@ export const createColumns = ({
accessorFn: (row) => getNotificationCreatedAtTs(row),
id: 'created_at',
size: 120,
meta: { label: () => t('table.notification.date') },
header: ({ column }) => (
<Button
variant="ghost"
@@ -151,6 +152,7 @@ export const createColumns = ({
accessorKey: 'type',
size: 180,
header: () => t('table.notification.type'),
meta: { label: () => t('table.notification.type') },
cell: ({ row }) => {
const original = row.original;
const typeKey = `view.notification.filters.${original.type}`;
@@ -228,7 +230,8 @@ export const createColumns = ({
{
accessorKey: 'senderUsername',
meta: {
class: 'overflow-hidden'
class: 'overflow-hidden',
label: () => t('table.notification.user')
},
size: 150,
header: () => t('table.notification.user'),
@@ -284,7 +287,8 @@ export const createColumns = ({
{
accessorKey: 'groupName',
meta: {
class: 'overflow-hidden'
class: 'overflow-hidden',
label: () => t('table.notification.group')
},
size: 150,
header: () => t('table.notification.group'),
@@ -390,6 +394,7 @@ export const createColumns = ({
accessorKey: 'photo',
size: 80,
header: () => t('table.notification.photo'),
meta: { label: () => t('table.notification.photo') },
cell: ({ row }) => {
const original = row.original;
if (original.type === 'boop') {
@@ -458,7 +463,8 @@ export const createColumns = ({
enableSorting: false,
meta: {
class: 'min-w-0 overflow-hidden',
stretch: true
stretch: true,
label: () => t('table.notification.message')
},
minSize: 100,
cell: ({ row }) => {
@@ -548,7 +554,8 @@ export const createColumns = ({
{
id: 'action',
meta: {
class: 'text-right'
class: 'text-right',
label: () => t('table.notification.action')
},
size: 120,
minSize: 120,
+46 -26
View File
@@ -20,24 +20,27 @@ import { i18n } from '../../plugin';
const { t } = i18n.global;
const sortButton = ({ column, label, descFirst = false }) => (
<Button
variant="ghost"
size="sm"
class="-ml-2 h-8 px-2"
onClick={() => {
const sorted = column.getIsSorted();
if (!sorted && descFirst) {
column.toggleSorting(true);
return;
}
column.toggleSorting(sorted === 'asc');
}}
>
{label}
<ArrowUpDown class="ml-1 h-4 w-4" />
</Button>
);
const sortButton = ({ column, label, descFirst = false }) => {
const resolvedLabel = typeof label === 'function' ? label() : label;
return (
<Button
variant="ghost"
size="sm"
class="-ml-2 h-8 px-2"
onClick={() => {
const sorted = column.getIsSorted();
if (!sorted && descFirst) {
column.toggleSorting(true);
return;
}
column.toggleSorting(sorted === 'asc');
}}
>
{resolvedLabel}
<ArrowUpDown class="ml-1 h-4 w-4" />
</Button>
);
};
const getInstanceIconWeight = (item) => {
if (!item) return 0;
@@ -70,6 +73,7 @@ export const createColumns = ({
header: () => t('table.playerList.avatar'),
size: 70,
enableSorting: false,
meta: { label: () => t('table.playerList.avatar') },
cell: ({ row }) => {
const userRef = row.original?.ref;
const src = userImage(userRef);
@@ -89,8 +93,12 @@ export const createColumns = ({
id: 'timer',
accessorFn: (row) => row?.timer,
header: ({ column }) =>
sortButton({ column, label: t('table.playerList.timer') }),
sortButton({
column,
label: () => t('table.playerList.timer')
}),
size: 90,
meta: { label: () => t('table.playerList.timer') },
sortingFn: (rowA, rowB) =>
(rowA.original?.timer ?? 0) - (rowB.original?.timer ?? 0),
cell: ({ row }) => <Timer epoch={row.original?.timer} />
@@ -101,9 +109,10 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.playerList.displayName')
label: () => t('table.playerList.displayName')
}),
size: 200,
meta: { label: () => t('table.playerList.displayName') },
sortingFn: (rowA, rowB) =>
sortAlphabetically(rowA.original, rowB.original, 'displayName'),
cell: ({ row }) => {
@@ -118,8 +127,9 @@ export const createColumns = ({
id: 'rank',
accessorFn: (row) => row?.ref?.$trustSortNum,
header: ({ column }) =>
sortButton({ column, label: t('table.playerList.rank') }),
sortButton({ column, label: () => t('table.playerList.rank') }),
size: 110,
meta: { label: () => t('table.playerList.rank') },
sortingFn: (rowA, rowB) =>
(rowA.original?.ref?.$trustSortNum ?? 0) -
(rowB.original?.ref?.$trustSortNum ?? 0),
@@ -143,7 +153,8 @@ export const createColumns = ({
size: 200,
minSize: 100,
meta: {
stretch: true
stretch: true,
label: () => t('table.playerList.status')
},
enableSorting: false,
cell: ({ row }) => {
@@ -170,9 +181,13 @@ export const createColumns = ({
id: 'photonId',
accessorFn: (row) => row?.photonId,
header: ({ column }) =>
sortButton({ column, label: t('table.playerList.photonId') }),
sortButton({
column,
label: () => t('table.playerList.photonId')
}),
size: 110,
enableHiding: true,
meta: { label: () => t('table.playerList.photonId') },
sortingFn: (rowA, rowB) =>
(rowA.original?.photonId ?? 0) - (rowB.original?.photonId ?? 0),
cell: ({ row }) => {
@@ -212,13 +227,14 @@ export const createColumns = ({
header: ({ column }) =>
sortButton({
column,
label: t('table.playerList.icon'),
label: () => t('table.playerList.icon'),
descFirst: true
}),
size: 90,
accessorFn: (row) => getInstanceIconWeight(row),
meta: {
class: 'text-center'
class: 'text-center',
label: () => t('table.playerList.icon')
},
sortingFn: (rowA, rowB, columnId) => {
const a = rowA.original;
@@ -291,6 +307,7 @@ export const createColumns = ({
header: () => t('table.playerList.platform'),
size: 90,
enableSorting: false,
meta: { label: () => t('table.playerList.platform') },
cell: ({ row }) => {
const userRef = row.original?.ref;
const platform = userRef?.$platform;
@@ -330,6 +347,7 @@ export const createColumns = ({
header: () => t('table.playerList.language'),
size: 100,
enableSorting: false,
meta: { label: () => t('table.playerList.language') },
cell: ({ row }) => {
const userRef = row.original?.ref;
const langs = userRef?.$languages ?? [];
@@ -366,6 +384,7 @@ export const createColumns = ({
header: () => t('table.playerList.bioLink'),
size: 100,
enableSorting: false,
meta: { label: () => t('table.playerList.bioLink') },
cell: ({ row }) => {
const links =
row.original?.ref?.bioLinks?.filter(Boolean) ?? [];
@@ -402,7 +421,8 @@ export const createColumns = ({
size: 150,
minSize: 20,
meta: {
stretch: true
stretch: true,
label: () => t('table.playerList.note')
},
enableSorting: false,
cell: ({ row }) => {