From e4b9e558835a855cf55a9d9ba9063f3fcf1c9ebc Mon Sep 17 00:00:00 2001 From: hanxuanyu <2252193204@qq.com> Date: Thu, 13 Aug 2026 12:41:57 +0800 Subject: [PATCH] fix(grid): improve column drag scrolling and selection --- apps/desktop/src/components/grid/DataGrid.vue | 17 +- .../grid/DataGridColumnLayoutPopover.vue | 8 +- .../dataGridColumnLayoutPopover.spec.ts | 21 ++ .../src/components/layout/ContentArea.vue | 1 + .../__tests__/useDataGridColumnLayout.spec.ts | 288 +++++++++++++++++- .../__tests__/useDataGridSelection.spec.ts | 13 + .../composables/useDataGridColumnLayout.ts | 126 +++++++- .../src/composables/useDataGridSelection.ts | 10 + .../dataGridColumnHeaderInteraction.ts | 71 +++++ 9 files changed, 539 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 4c2e71f0a3..83412d4d2e 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -2123,6 +2123,16 @@ const gridViewportWidth = ref(0); let gridScrollLeftBeforeTranspose = 0; let gridScrollTopBeforeKeyboardTranspose: number | null = null; let restoreGridScrollTopAfterTranspose = false; + +function persistDraggedColumnOrder(indexes: number[]) { + const previousVisibleColumnIndexes = [...visibleColumnIndexes.value]; + persistColumnOrder(indexes); + selection.remapColumnSelection( + previousVisibleColumnIndexes, + indexes.filter((index) => !hiddenColumnIndexes.value.has(index)), + ); +} + const { renderedColumnOffsets, horizontalColumnWindow, @@ -2147,14 +2157,19 @@ const { viewportWidth: gridViewportWidth, rowNumberWidth, headerRef, + getScrollElement: gridScrollerElement, orderedColumnIndexes: orderedDisplayableColumnIndexes, hiddenColumnIndexes, getIsResizing, onResizeStart, onCanvasMouseLeave, onCanvasDrawSchedule: scheduleCanvasDraw, + onHorizontalScroll: (scroller) => { + updateGridHorizontalViewport(scroller); + if (headerRef.value) headerRef.value.scrollLeft = scroller.scrollLeft; + }, onRefreshMetrics: scheduleColumnLayoutRefresh, - onPersistColumnOrder: persistColumnOrder, + onPersistColumnOrder: persistDraggedColumnOrder, frozenColumnCount, }); diff --git a/apps/desktop/src/components/grid/DataGridColumnLayoutPopover.vue b/apps/desktop/src/components/grid/DataGridColumnLayoutPopover.vue index b5bd76a90a..8eed331fb1 100644 --- a/apps/desktop/src/components/grid/DataGridColumnLayoutPopover.vue +++ b/apps/desktop/src/components/grid/DataGridColumnLayoutPopover.vue @@ -20,9 +20,11 @@ const props = withDefaults( defineProps<{ grid?: DataGridColumnLayoutHandle; triggerClass?: string; + compact?: boolean; }>(), { triggerClass: "", + compact: false, }, ); @@ -299,14 +301,14 @@ onBeforeUnmount(resetColumnDragState); variant="ghost" size="sm" class="h-5 shrink-0 gap-1 px-1.5 text-xs text-foreground hover:bg-accent" - :class="[triggerClass, { 'bg-accent text-foreground': (grid?.hiddenColumnCount ?? 0) > 0 }]" + :class="[triggerClass, compact ? 'w-6 gap-0 px-0' : '', { 'bg-accent text-foreground': (grid?.hiddenColumnCount ?? 0) > 0 }]" :disabled="!grid" :title="t('grid.columnVisibility')" :aria-label="t('grid.columnVisibility')" > - {{ t("grid.columnVisibility") }} - {{ grid?.visibleColumnCount }}/{{ grid?.displayableColumnCount }} + {{ t("grid.columnVisibility") }} + {{ grid?.visibleColumnCount }}/{{ grid?.displayableColumnCount }} diff --git a/apps/desktop/src/components/grid/__tests__/dataGridColumnLayoutPopover.spec.ts b/apps/desktop/src/components/grid/__tests__/dataGridColumnLayoutPopover.spec.ts index d69aedb417..a9e1b17529 100644 --- a/apps/desktop/src/components/grid/__tests__/dataGridColumnLayoutPopover.spec.ts +++ b/apps/desktop/src/components/grid/__tests__/dataGridColumnLayoutPopover.spec.ts @@ -1,5 +1,7 @@ // @vitest-environment happy-dom +import { readFileSync } from "node:fs"; +import path from "node:path"; import { createApp, nextTick, type App } from "vue"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import i18n from "@/i18n"; @@ -39,6 +41,8 @@ vi.mock("@/components/ui/button", async () => { import DataGridColumnLayoutPopover from "../DataGridColumnLayoutPopover.vue"; +const contentAreaSource = readFileSync(path.resolve(process.cwd(), "apps/desktop/src/components/layout/ContentArea.vue"), "utf8"); + const mountedApps: Array<{ app: App; host: HTMLElement }> = []; function columnLayoutOptions(itemCount: number): DataGridColumnLayoutOption[] { @@ -137,6 +141,23 @@ afterEach(() => { }); describe("data grid column layout popover", () => { + it("is available from the query result toolbar and keeps a labelled compact trigger", async () => { + expect(contentAreaSource).toContain(''); + + const gridState = createGrid(4); + const host = document.createElement("div"); + document.body.append(host); + const app = createApp(DataGridColumnLayoutPopover, { grid: gridState.grid, compact: true }); + app.use(i18n); + app.mount(host); + mountedApps.push({ app, host }); + await nextTick(); + + const trigger = [...host.querySelectorAll("button")].find((button) => button.getAttribute("aria-label") === i18n.global.t("grid.columnVisibility")); + expect(trigger?.className).toContain("w-6"); + expect(trigger?.textContent?.trim()).toBe(""); + }); + it("renders only the visible field window plus a bounded buffer", () => { const window = dataGridColumnLayoutVirtualWindow({ itemCount: 500, diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 4c38b5e503..fddbdfa31f 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -1622,6 +1622,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand