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.
This commit is contained in:
t8y2 committed 2026-05-20 16:22:17 +08:00
1 parent fe6fead2dc
commit 47ec339d5a
8 files changed
+135 -65

No files matched your search

+54 -50
View File
@@ -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"
/>
<ContentArea
ref="contentAreaRef"
:active-tab="activeTab"
:active-connection="activeConnection"
:executable-sql="executableSql"
:active-output-view="activeOutputView"
:format-sql-request-id="formatSqlRequestId"
:selected-sql="selectedSql"
:cursor-pos="cursorPos"
@update:active-output-view="activeOutputView = $event"
@fix-with-ai="fixWithAi"
@execute="tryExecute()"
@cancel="cancelActiveExecution()"
@explain="tryExplain()"
@editor-update="
(v: string) => {
if (queryStore.activeTabId) queryStore.updateSql(queryStore.activeTabId, v);
}
"
@editor-selection-change="(v: string) => (selectedSql = v)"
@editor-cursor-change="(p: number) => (cursorPos = p)"
@format-error="toast(t('toolbar.formatSqlFailed'))"
@save-sql="void openSaveSqlDialog()"
@reload="
(
sql?: string,
searchText?: string,
whereInput?: string,
orderBy?: string,
limit?: number,
offset?: number,
) => onReloadData(sql, searchText, whereInput, orderBy, limit, offset)
"
@paginate="onPaginate"
@sort="onSort"
@execute-sql="onExecuteSql"
@click-table="onClickTable"
@open-object-table="
(target) =>
activeTab &&
openTableTarget({
connectionId: activeTab.connectionId,
database: activeTab.database,
schema: target.schema,
tableName: target.tableName,
})
"
@object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)"
/>
<KeepAlive :max="20">
<ContentArea
ref="contentAreaRef"
:key="activeTab.id"
:active-tab="activeTab"
:active-connection="activeConnection"
:executable-sql="executableSql"
:active-output-view="activeOutputView"
:format-sql-request-id="formatSqlRequestId"
:selected-sql="selectedSql"
:cursor-pos="cursorPos"
@update:active-output-view="activeOutputView = $event"
@fix-with-ai="fixWithAi"
@execute="tryExecute()"
@cancel="cancelActiveExecution()"
@explain="tryExplain()"
@editor-update="
(v: string) => {
if (queryStore.activeTabId) queryStore.updateSql(queryStore.activeTabId, v);
}
"
@editor-selection-change="(v: string) => (selectedSql = v)"
@editor-cursor-change="(p: number) => (cursorPos = p)"
@format-error="toast(t('toolbar.formatSqlFailed'))"
@save-sql="void openSaveSqlDialog()"
@reload="
(
sql?: string,
searchText?: string,
whereInput?: string,
orderBy?: string,
limit?: number,
offset?: number,
) => onReloadData(sql, searchText, whereInput, orderBy, limit, offset)
"
@paginate="onPaginate"
@sort="onSort"
@execute-sql="onExecuteSql"
@click-table="onClickTable"
@open-object-table="
(target) =>
activeTab &&
openTableTarget({
connectionId: activeTab.connectionId,
database: activeTab.database,
schema: target.schema,
tableName: target.tableName,
})
"
@object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)"
/>
</KeepAlive>
</div>
<WelcomeScreen
v-else
@@ -157,6 +157,7 @@ const props = defineProps<{
pageLimit?: number;
countSql?: string;
loading?: boolean;
cacheKey?: string;
onExecuteSql?: (sql: string) => Promise<void>;
customSave?: (changes: {
dirtyRows: Map<number, Map<number, string | number | boolean | null>>;
@@ -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,
});
@@ -336,6 +336,7 @@ defineExpose({ focusSearch, refreshData });
<DataGrid
v-if="activeTab.result"
:key="`${activeTab.id}-${activeTab.activeResultIndex ?? 0}`"
:cache-key="`${activeTab.id}-${activeTab.activeResultIndex ?? 0}`"
class="flex-1 min-h-0"
:result="activeTab.result"
:sql="activeTab.lastExecutedSql || activeTab.sql"
@@ -538,6 +539,7 @@ defineExpose({ focusSearch, refreshData });
ref="dataGridRef"
class="flex-1 min-h-0"
:key="activeTab.id"
:cache-key="activeTab.id"
:result="activeTab.result"
:sql="activeTab.sql"
:loading="activeTab.isExecuting"
@@ -12,28 +12,35 @@ const ROW_NUM_WIDTH = 48;
export interface UseDataGridColumnResizeOptions {
columns: ComputedRef<string[]>;
rows: ComputedRef<CellValue[][]>;
sourceRows: ComputedRef<CellValue[][]>;
columnIndexes: ComputedRef<number[]>;
gridRef: Ref<HTMLDivElement | undefined>;
}
export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions) {
const { columns, rows, gridRef } = options;
const { columns, sourceRows, columnIndexes, gridRef } = options;
const columnWidths = ref<number[]>([]);
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),
});
}
@@ -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<number>;
currentPage: Ref<number>;
cacheKey?: ComputedRef<string | undefined>;
emit: {
(
event: "reload",
@@ -86,6 +87,15 @@ export interface UseDataGridEditorOptions {
};
}
interface PendingChangesSnapshot {
newRows: CellValue[][];
dirtyRows: Map<number, Map<number, CellValue>>;
deletedRows: Set<number>;
columnCount: number;
}
const pendingChangesCache = new Map<string, PendingChangesSnapshot>();
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<CellValue[][]>([]);
const deletedRows = ref<Set<number>>(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,
+15 -1
View File
@@ -47,7 +47,7 @@ export const useQueryStore = defineStore("query", () => {
const restored = loadSavedTabs();
const tabs = ref<QueryTab[]>(restored.tabs);
const activeTabId = ref<string | null>(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,
};
});
+1
View File
@@ -335,6 +335,7 @@ export interface QueryTab {
| "primary-key-not-returned"
| "aliased-columns"
| "metadata-unavailable";
resultEvicted?: boolean;
whereInput?: string;
}