fix datatable header reorder issue

This commit is contained in:
pa
2026-03-07 00:23:02 +09:00
parent 9feef5d119
commit c42b126131
8 changed files with 264 additions and 45 deletions

View File

@@ -10,11 +10,11 @@
<colgroup>
<col v-for="col in table.getVisibleLeafColumns()" :key="col.id" :style="getColStyle(col)" />
</colgroup>
<ContextMenu v-if="enableColumnVisibility">
<ContextMenu v-if="showHeaderContextMenu">
<ContextMenuTrigger as-child>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<template v-if="enableColumnReorder">
<template v-if="effectiveColumnReorder">
<DragDropProvider @dragEnd="onHeaderDragEnd">
<template v-for="(header, hIdx) in headerGroup.headers" :key="header.id">
<SortableTableHead
@@ -76,21 +76,60 @@
@update:model-value="col.toggleVisibility(!!$event)">
{{ resolveHeaderLabel(col) }}
</ContextMenuCheckboxItem>
<template v-if="tcColumnOrderLocked != null">
<ContextMenuSeparator />
<ContextMenuCheckboxItem
:model-value="tcColumnOrderLocked"
@update:model-value="table.options.meta.columnOrderLocked.value = $event">
{{ t('table.header_menu.lock_column_order') }}
</ContextMenuCheckboxItem>
</template>
<template v-if="tcResetAll">
<ContextMenuSeparator />
<ContextMenuItem inset @select="tcResetAll">
{{ t('table.header_menu.reset_all') }}
</ContextMenuItem>
</template>
</ContextMenuContent>
</ContextMenu>
<TableHeader v-else>
<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)" />
<ContextMenu v-else-if="showHeaderContextMenuNoVisibility">
<ContextMenuTrigger as-child>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<template v-if="effectiveColumnReorder">
<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-else
v-for="header in headerGroup.headers"
:key="header.id"
:class="getHeaderClass(header)"
:style="getPinnedStyle(header.column)">
<template v-if="!header.isPlaceholder">
@@ -108,29 +147,44 @@
</template>
</TableHead>
</template>
</DragDropProvider>
</TableRow>
</TableHeader>
</ContextMenuTrigger>
<ContextMenuContent class="w-48">
<template v-if="tcColumnOrderLocked != null">
<ContextMenuCheckboxItem
:model-value="tcColumnOrderLocked"
@update:model-value="table.options.meta.columnOrderLocked.value = $event">
{{ t('table.header_menu.lock_column_order') }}
</ContextMenuCheckboxItem>
</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()" />
<template v-if="tcResetAll">
<ContextMenuSeparator v-if="tcColumnOrderLocked != null" />
<ContextMenuItem inset @select="tcResetAll">
{{ t('table.header_menu.reset_all') }}
</ContextMenuItem>
</template>
</ContextMenuContent>
</ContextMenu>
<TableHeader v-else>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<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
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>
class="absolute right-0 top-0 h-full w-px bg-border dark:bg-border dark:brightness-[2]" />
</div>
</template>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -254,6 +308,14 @@
import { useAppearanceSettingsStore } from '@/stores/';
import { useI18n } from 'vue-i18n';
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger
} from '../context-menu';
import {
Pagination,
PaginationContent,
@@ -271,7 +333,6 @@
isSpacer,
resolveHeaderLabel
} from './dataTableHelpers.js';
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuTrigger } from '../context-menu';
import DataTableEmpty from './DataTableEmpty.vue';
import SortableTableHead from './SortableTableHead.vue';
@@ -341,6 +402,25 @@
const { t } = useI18n();
const tableScrollRef = ref(null);
const tableMeta = computed(() => props.table?.options?.meta ?? {});
const tcResetAll = computed(() => tableMeta.value.resetAll ?? null);
const tcColumnOrderLocked = computed(() => {
const val = tableMeta.value.columnOrderLocked;
if (val == null) return null;
return typeof val === 'object' && 'value' in val ? val.value : val;
});
const showHeaderContextMenu = computed(
() => props.enableColumnVisibility || tcResetAll.value || tcColumnOrderLocked.value != null
);
const showHeaderContextMenuNoVisibility = computed(
() => !props.enableColumnVisibility && (tcResetAll.value || tcColumnOrderLocked.value != null)
);
const effectiveColumnReorder = computed(() => props.enableColumnReorder && tcColumnOrderLocked.value !== true);
const emptyType = computed(() => {
const totalRows = props.table?.getCoreRowModel?.().rows?.length ?? 0;
return totalRows === 0 ? 'nodata' : 'nomatch';
@@ -427,6 +507,7 @@
const reorderableIds = allColumns
.filter((col) => {
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;

View File

@@ -54,6 +54,7 @@
<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"
@pointerdown.stop
@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]" />

View File

@@ -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);
});

View File

@@ -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;