From 707296c71d6d2e2ab4a0f34b1c234a7f5a9df0fd Mon Sep 17 00:00:00 2001 From: jearhe <0verme7753@gmail.com> Date: Mon, 24 Aug 2026 18:47:31 +0800 Subject: [PATCH] fix(data-grid): preserve resized column widths across query reruns --- apps/desktop/src/components/grid/DataGrid.vue | 3 ++- .../src/components/layout/ContentArea.vue | 17 +++++++++++- .../__tests__/useDataGridColumnResize.spec.ts | 27 +++++++++++++++++++ .../__tests__/tabs/tabPresentation.spec.ts | 22 ++++++++++++--- apps/desktop/src/lib/tabs/tabPresentation.ts | 4 +++ packages/app-tests/tabPresentation.test.ts | 10 +++++++ 6 files changed, 78 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 05ac623332..3fbf1b3b88 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -452,6 +452,7 @@ interface DataGridProps { countTotalRows?: () => Promise; loading?: boolean; cacheKey?: string; + columnWidthCacheKey?: string; pendingStateKey?: string; exportSql?: string; onExecuteSql?: (sql: string) => Promise; @@ -2714,7 +2715,7 @@ function scrollToColumnIndex(columnIndex: number) { // --- Column resize composable --- const columnWidthDensity = computed(() => settingsStore.editorSettings.columnWidthDensity); const tableFontFamily = computed(() => settingsStore.editorSettings.tableFontFamily); -const columnWidthCacheKey = computed(() => props.cacheKey?.trim() || undefined); +const columnWidthCacheKey = computed(() => props.columnWidthCacheKey?.trim() || props.cacheKey?.trim() || undefined); const columnStructureSignature = computed(() => createDataGridColumnStructureSignature(props.result.columns, props.result.column_types)); // Bumped once the configured header font is ready, so widths measured against a temporary fallback // font get re-measured without reacting to unrelated fonts loaded elsewhere in the application. diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index ecd147a26c..2dd4715f59 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -125,7 +125,20 @@ import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGr import { useToast } from "@/composables/useToast"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState"; -import { databaseDisplayNameForTab, executionSummaryItems, queryResultExecutionSql, resultGridCacheKey, resultGridInstanceKey, resultRunItems, resultSourceRange, resultSqlForGrid, statementExecutionMarkers, tabularResultItems, type ExecutionSummaryItem } from "@/lib/tabs/tabPresentation"; +import { + databaseDisplayNameForTab, + executionSummaryItems, + queryResultExecutionSql, + resultGridCacheKey, + resultGridColumnWidthCacheKey, + resultGridInstanceKey, + resultRunItems, + resultSourceRange, + resultSqlForGrid, + statementExecutionMarkers, + tabularResultItems, + type ExecutionSummaryItem, +} from "@/lib/tabs/tabPresentation"; import { defaultQueryResultArchiveFileName } from "@/lib/query/queryResultArchive"; import { saveQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile"; import { isTableDataEditable } from "@/lib/table/tableEditing"; @@ -443,6 +456,7 @@ const allResultExportSheets = computed(() => ); const resultRuns = computed(() => resultRunItems(props.activeTab)); const activeResultGridCacheKey = computed(() => resultGridCacheKey(props.activeTab)); +const activeResultGridColumnWidthCacheKey = computed(() => resultGridColumnWidthCacheKey(props.activeTab)); const activeResultGridInstanceKey = computed(() => resultGridInstanceKey(props.activeTab)); const activeResultSql = computed(() => resultSqlForGrid(props.activeTab)); const activeResultExportSql = computed(() => queryResultExecutionSql(props.activeTab)); @@ -1716,6 +1730,7 @@ defineExpose({ ref="dataGridRef" :key="activeResultGridInstanceKey" :cache-key="activeResultGridCacheKey" + :column-width-cache-key="activeResultGridColumnWidthCacheKey" :pending-state-key="activeResultGridInstanceKey" class="flex-1 min-h-0" :result="activeTab.result" diff --git a/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts index ec75bba0f8..ca310f8c55 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridColumnResize.spec.ts @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import { DATA_GRID_COL_AUTO_FIT_MAX_WIDTH, DATA_GRID_COL_MIN_WIDTH, sampleDataGridColumnValues } from "@/lib/dataGrid/dataGridColumnWidth"; import { clearDataGridColumnWidthStates, createDataGridColumnMeasurementSignature, createDataGridColumnStructureSignature, DATA_GRID_COLUMN_WIDTH_STATE_LIMIT, dataGridColumnWidthStateCount, loadDataGridColumnWidthState, saveDataGridColumnWidthState } from "@/lib/dataGrid/dataGridColumnWidthState"; import { DATA_GRID_ROW_NUM_WIDTH, dataGridRowNumberColumnWidth, resizeDataGridColumnWidth, useDataGridColumnResize } from "@/composables/useDataGridColumnResize"; +import { resultGridColumnWidthCacheKey } from "@/lib/tabs/tabPresentation"; function createResizeState(options: { columns: string[]; @@ -178,6 +179,32 @@ describe("useDataGridColumnResize", () => { expect(remounted.columnWidths.value).toEqual(first.columnWidths.value); }); + it("restores manually resized widths when the active result run changes", () => { + const firstResult = { id: "tab-1", activeResultRunId: "run-1", activeResultIndex: 2 }; + const rerunResult = { ...firstResult, activeResultRunId: "run-2" }; + const firstCacheKey = resultGridColumnWidthCacheKey(firstResult); + + expect(resultGridColumnWidthCacheKey(rerunResult)).toBe(firstCacheKey); + + const first = createResizeState({ + columns: ["id", "resource"], + rows: [[1, "lock"]], + cacheKey: firstCacheKey, + }); + first.initColumnWidths(); + first.onResizeStart(1, new MouseEvent("mousedown", { clientX: 100, cancelable: true })); + document.dispatchEvent(new MouseEvent("mouseup", { clientX: 260 })); + + const remounted = createResizeState({ + columns: ["id", "resource"], + rows: [[1, "lock"]], + cacheKey: resultGridColumnWidthCacheKey(rerunResult), + }); + remounted.initColumnWidths(); + + expect(remounted.columnWidths.value).toEqual(first.columnWidths.value); + }); + it("isolates widths by result cache key", () => { const first = createResizeState({ columns: ["id"], rows: [["x".repeat(120)]], cacheKey: "result-a" }); first.initColumnWidths(); diff --git a/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts b/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts index 32fb9f6267..0bf3864859 100644 --- a/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts +++ b/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts @@ -1,7 +1,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { createPinia, setActivePinia } from "pinia"; import { useConnectionStore } from "@/stores/connectionStore"; -import { connectionGroupDisplayName, executionSummaryItems, middleEllipsis, queryResultBaseSql, queryResultExecutionSql, resultGridCacheKey, resultGridInstanceKey, resultSourceRange, statementExecutionMarkers, tabDisplayTitle, tabTooltipLines, tabularResultItems } from "@/lib/tabs/tabPresentation"; +import { + connectionGroupDisplayName, + executionSummaryItems, + middleEllipsis, + queryResultBaseSql, + queryResultExecutionSql, + resultGridCacheKey, + resultGridColumnWidthCacheKey, + resultGridInstanceKey, + resultSourceRange, + statementExecutionMarkers, + tabDisplayTitle, + tabTooltipLines, + tabularResultItems, +} from "@/lib/tabs/tabPresentation"; import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint"; import type { ConnectionConfig, QueryTab } from "@/types/database"; @@ -136,12 +150,14 @@ describe("query result labels", () => { describe("query result grid identity", () => { it("separates rerun payloads while retaining run and result-set identity", () => { const first = queryTab({ activeResultRunId: "run-1", activeResultIndex: 2, resultGridRevision: "execution-1" }); - const rerun = queryTab({ activeResultRunId: "run-1", activeResultIndex: 2, resultGridRevision: "execution-2" }); + const rerun = queryTab({ activeResultRunId: "run-2", activeResultIndex: 2, resultGridRevision: "execution-2" }); expect(resultGridInstanceKey(first)).toBe("tab-1-run-1-2-execution-1"); expect(resultGridInstanceKey(rerun)).not.toBe(resultGridInstanceKey(first)); expect(resultGridInstanceKey({ ...first, activeResultIndex: 1 })).toBe("tab-1-run-1-1-execution-1"); - expect(resultGridCacheKey(rerun)).toBe(resultGridCacheKey(first)); + expect(resultGridCacheKey(rerun)).not.toBe(resultGridCacheKey(first)); + expect(resultGridColumnWidthCacheKey(rerun)).toBe(resultGridColumnWidthCacheKey(first)); + expect(resultGridColumnWidthCacheKey({ ...first, activeResultIndex: 1 })).not.toBe(resultGridColumnWidthCacheKey(first)); }); }); diff --git a/apps/desktop/src/lib/tabs/tabPresentation.ts b/apps/desktop/src/lib/tabs/tabPresentation.ts index e5397c70e1..5a88badd18 100644 --- a/apps/desktop/src/lib/tabs/tabPresentation.ts +++ b/apps/desktop/src/lib/tabs/tabPresentation.ts @@ -358,6 +358,10 @@ export function resultGridCacheKey(tab: Pick & Partial>): string { + return `result-column-width-${tab.id}-${tab.activeResultIndex ?? 0}`; +} + export function resultGridInstanceKey(tab: Pick): string { return `${resultGridCacheKey(tab)}-${tab.resultGridRevision ?? "initial"}`; } diff --git a/packages/app-tests/tabPresentation.test.ts b/packages/app-tests/tabPresentation.test.ts index c00988b839..7556edbc90 100644 --- a/packages/app-tests/tabPresentation.test.ts +++ b/packages/app-tests/tabPresentation.test.ts @@ -8,6 +8,7 @@ import { middleEllipsis, nextExecutionSummaryView, resultGridCacheKey, + resultGridColumnWidthCacheKey, resultRunItems, resultSourceRange, resultSqlForGrid, @@ -356,6 +357,15 @@ test("result grid cache key includes result run id and statement result index", assert.equal(resultGridCacheKey(queryTab({ activeResultIndex: undefined })), "tab-1-current-0"); }); +test("result grid column width cache key ignores result run id and isolates result indexes", () => { + const first = queryTab({ activeResultRunId: "run-1", activeResultIndex: 2 }); + const rerun = queryTab({ activeResultRunId: "run-2", activeResultIndex: 2 }); + + assert.equal(resultGridColumnWidthCacheKey(first), "result-column-width-tab-1-2"); + assert.equal(resultGridColumnWidthCacheKey(rerun), resultGridColumnWidthCacheKey(first)); + assert.notEqual(resultGridColumnWidthCacheKey(queryTab({ activeResultIndex: 1 })), resultGridColumnWidthCacheKey(first)); +}); + test("execution summary items include table and non-table statement results", () => { const items = executionSummaryItems({ results: [result([], { affected_rows: 4 }), result(["id"], { rows: [[1], [2], [3]] }), { ...result(["Error"]), rows: [["boom"]], execution_error: true }],