From 47ec339d5a33fd57caa3560313540cdb3da0732f Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 20 May 2026 16:22:17 +0800 Subject: [PATCH] fix: preserve pending row edits when switching tabs (#362) Wrap ContentArea in KeepAlive to cache tab instances across switches. Add a module-level pending-changes cache in useDataGridEditor so edits survive DataGrid destruction caused by result eviction and reload. --- apps/desktop/src/App.vue | 104 +++++++++--------- apps/desktop/src/components/grid/DataGrid.vue | 8 +- .../src/components/layout/ContentArea.vue | 2 + .../composables/useDataGridColumnResize.ts | 22 ++-- .../src/composables/useDataGridEditor.ts | 46 +++++++- apps/desktop/src/stores/queryStore.ts | 16 ++- apps/desktop/src/types/database.ts | 1 + apps/desktop/vite.config.ts | 1 - 8 files changed, 135 insertions(+), 65 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index ddae633f8d..8c78ac5b63 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -204,10 +204,11 @@ const saveSqlFolders = computed(() => { watch( () => queryStore.activeTabId, - () => { + (id) => { selectedSql.value = ""; activeOutputView.value = "result"; showDriverStore.value = false; + if (id) queryStore.reloadEvictedTab(id); }, ); @@ -825,55 +826,58 @@ onUnmounted(() => { @set-default-database="setActiveDatabaseAsDefault" @clear-default-database="clearActiveDefaultDatabase" /> - + + + Promise; customSave?: (changes: { dirtyRows: Map>; @@ -1054,9 +1055,6 @@ const visibleColumnIndexes = computed(() => visibleColumnIndexesForFilter(displayableColumnIndexes.value, hiddenColumnIndexes.value), ); const visibleColumns = computed(() => visibleColumnIndexes.value.map((index) => props.result.columns[index])); -const visibleRows = computed(() => - props.result.rows.map((row) => visibleColumnIndexes.value.map((index) => row[index])), -); const visibleColumnCount = computed(() => visibleColumnIndexes.value.length); const displayableColumnCount = computed(() => displayableColumnIndexes.value.length); const hiddenColumnCount = computed(() => displayableColumnCount.value - visibleColumnCount.value); @@ -1088,7 +1086,8 @@ function actualColumnIndex(visibleColumnIndex: number): number { // --- Column resize composable --- const { initColumnWidths, onResizeStart, autoFitColumn, columnVars, getIsResizing } = useDataGridColumnResize({ columns: visibleColumns, - rows: visibleRows, + sourceRows: computed(() => props.result.rows), + columnIndexes: visibleColumnIndexes, gridRef, }); const gridStyle = computed(() => ({ @@ -1321,6 +1320,7 @@ const editor = useDataGridEditor({ getRowItem, pageSize, currentPage, + cacheKey: computed(() => props.cacheKey), emit, }); diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index b480106bfd..e2e985dc64 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -336,6 +336,7 @@ defineExpose({ focusSearch, refreshData }); ; - rows: ComputedRef; + sourceRows: ComputedRef; + columnIndexes: ComputedRef; gridRef: Ref; } export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) { - const { columns, rows, gridRef } = options; + const { columns, sourceRows, columnIndexes, gridRef } = options; const columnWidths = ref([]); const { width: gridWidth } = useElementSize(gridRef); let isResizing = false; - function sampleColumnValues(rowData: CellValue[][], colIdx: number): CellValue[] { - return rowData.slice(0, DATA_GRID_SAMPLE_ROWS).map((row) => row[colIdx] ?? null); + function sampleColumnValues(visibleColIdx: number): CellValue[] { + const actualColIdx = columnIndexes.value[visibleColIdx]; + const rows = sourceRows.value; + const end = Math.min(rows.length, DATA_GRID_SAMPLE_ROWS); + const values: CellValue[] = []; + for (let i = 0; i < end; i++) { + values.push(rows[i][actualColIdx] ?? null); + } + return values; } function initColumnWidths() { if (columnWidths.value.length !== columns.value.length) { - const rowData = rows.value; columnWidths.value = columns.value.map((colName, colIdx) => { return calculateDataGridColumnWidth({ columnName: colName, - sampleValues: sampleColumnValues(rowData, colIdx), + sampleValues: sampleColumnValues(colIdx), }); }); } @@ -61,10 +68,9 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) function autoFitColumn(colIdx: number) { const colName = columns.value[colIdx]; if (!colName) return; - const rowData = rows.value; columnWidths.value[colIdx] = calculateDataGridColumnWidth({ columnName: colName, - sampleValues: sampleColumnValues(rowData, colIdx), + sampleValues: sampleColumnValues(colIdx), }); } diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index 3ba5a9b4cd..6cf8cc362b 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -1,4 +1,4 @@ -import { ref, computed, nextTick, watch, type ComputedRef, type Ref } from "vue"; +import { ref, computed, nextTick, watch, onBeforeUnmount, type ComputedRef, type Ref } from "vue"; import * as api from "@/lib/api"; import { buildDataGridRollbackStatements, @@ -73,6 +73,7 @@ export interface UseDataGridEditorOptions { getRowItem: (rowId: number) => RowItem | undefined; pageSize: Ref; currentPage: Ref; + cacheKey?: ComputedRef; emit: { ( event: "reload", @@ -86,6 +87,15 @@ export interface UseDataGridEditorOptions { }; } +interface PendingChangesSnapshot { + newRows: CellValue[][]; + dirtyRows: Map>; + deletedRows: Set; + columnCount: number; +} + +const pendingChangesCache = new Map(); + export function useDataGridEditor(options: UseDataGridEditorOptions) { const connectionStore = useConnectionStore(); const historyStore = useHistoryStore(); @@ -110,6 +120,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { getRowItem, pageSize, currentPage, + cacheKey, } = options; const editingCell = ref<{ rowId: number; col: number } | null>(null); @@ -119,6 +130,20 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { const newRows = ref([]); const deletedRows = ref>(new Set()); + // Restore cached pending changes from a previous instance (e.g. after result eviction + reload) + const key = cacheKey?.value; + if (key) { + const cached = pendingChangesCache.get(key); + if (cached && cached.columnCount === result.value.columns.length) { + newRows.value = cached.newRows; + dirtyRows.value = cached.dirtyRows; + deletedRows.value = cached.deletedRows; + pendingChangesCache.delete(key); + } else { + pendingChangesCache.delete(key); + } + } + const dirtyRowCount = computed(() => dirtyRows.value.size); const newRowCount = computed(() => newRows.value.length); const deletedRowCount = computed(() => deletedRows.value.size); @@ -137,6 +162,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { (!!customSave?.value || (!!connectionId.value && !!database.value && !!tableMeta.value)), ); + if (hasPendingChanges.value && useTransaction.value) { + transactionActive.value = true; + } + function enterTransaction() { transactionActive.value = true; } @@ -714,6 +743,21 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { }, ); + // Save pending changes before the component is destroyed so they can be + // restored if a new DataGrid instance is created for the same tab + // (e.g. after result eviction + reload). + onBeforeUnmount(() => { + const k = cacheKey?.value; + if (k && hasPendingChanges.value) { + pendingChangesCache.set(k, { + newRows: [...newRows.value.map((r) => [...r])], + dirtyRows: new Map([...dirtyRows.value].map(([i, m]) => [i, new Map(m)])), + deletedRows: new Set(deletedRows.value), + columnCount: result.value.columns.length, + }); + } + }); + return { editingCell, editValue, diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 7f01d4370e..d2c95a7924 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -47,7 +47,7 @@ export const useQueryStore = defineStore("query", () => { const restored = loadSavedTabs(); const tabs = ref(restored.tabs); const activeTabId = ref(restored.activeTabId); - const MAX_CACHED_RESULTS = 10; + const MAX_CACHED_RESULTS = 5; async function closeResultSession(tab: QueryTab | undefined, preserveSessionId?: string) { const sessionId = tab?.resultSessionId ?? tab?.result?.session_id; @@ -713,6 +713,7 @@ export const useQueryStore = defineStore("query", () => { const toEvict = inactive.slice(0, inactive.length - MAX_CACHED_RESULTS); toEvict.forEach((t) => { t.result = undefined; + t.resultEvicted = true; t.queryAnalysis = undefined; t.querySourceColumns = undefined; t.queryEditabilityReason = undefined; @@ -720,6 +721,18 @@ export const useQueryStore = defineStore("query", () => { } } + async function reloadEvictedTab(id: string) { + const tab = tabs.value.find((t) => t.id === id); + if (!tab || !tab.resultEvicted) return; + tab.resultEvicted = false; + const sql = tab.lastExecutedSql ?? tab.sql; + if (!sql?.trim()) return; + await executeTabSql(tab.id, sql, { + resultBaseSql: tab.resultBaseSql ?? sql, + resultSortedSql: tab.resultSortedSql, + }); + } + return { tabs, activeTabId, @@ -746,5 +759,6 @@ export const useQueryStore = defineStore("query", () => { explainTabSql, cancelTabExecution, cancelTabExplain, + reloadEvictedTab, }; }); diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index e5ab6693fb..033a58a81f 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -335,6 +335,7 @@ export interface QueryTab { | "primary-key-not-returned" | "aliased-columns" | "metadata-unavailable"; + resultEvicted?: boolean; whereInput?: string; } diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index e05845ea01..5b74d34880 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -31,7 +31,6 @@ export default defineConfig(async () => ({ "@codemirror/theme-one-dark", ], "sql-formatter": ["sql-formatter"], - echarts: ["echarts", "vue-echarts"], ui: ["reka-ui"], marked: ["marked"], },