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:
1 parent
fe6fead2dc
commit
47ec339d5a
8 files changed
+135
-65
No files matched your search
+54
-50
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -335,6 +335,7 @@ export interface QueryTab {
|
||||
| "primary-key-not-returned"
|
||||
| "aliased-columns"
|
||||
| "metadata-unavailable";
|
||||
resultEvicted?: boolean;
|
||||
whereInput?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user