fix(tabs): prompt before closing dirty tabs
This commit is contained in:
1 parent
ee883ce2b8
commit
d6a0da7169
2 files changed
+119
-27
No files matched your search
@@ -5,7 +5,6 @@ import { useI18n } from "vue-i18n";
|
|||||||
import type { DatabaseType, QueryResult, QueryTab, TableInfoTab } from "@/types/database";
|
import type { DatabaseType, QueryResult, QueryTab, TableInfoTab } from "@/types/database";
|
||||||
import { orderPinnedFirst } from "@/lib/pinnedItems";
|
import { orderPinnedFirst } from "@/lib/pinnedItems";
|
||||||
import { canCancelQueryExecution } from "@/lib/queryExecutionState";
|
import { canCancelQueryExecution } from "@/lib/queryExecutionState";
|
||||||
import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
|
|
||||||
import { buildExplainSql, parseExplainResult, parseDamengExplainText } from "@/lib/explainPlan";
|
import { buildExplainSql, parseExplainResult, parseDamengExplainText } from "@/lib/explainPlan";
|
||||||
import { allEditableColumnsWriteable, allPrimaryKeysPresent, analyzeEditableQuery, sourceColumnsForResult, type EditableQueryInfo } from "@/lib/sqlAnalysis";
|
import { allEditableColumnsWriteable, allPrimaryKeysPresent, analyzeEditableQuery, sourceColumnsForResult, type EditableQueryInfo } from "@/lib/sqlAnalysis";
|
||||||
import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence";
|
import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersistence";
|
||||||
@@ -191,6 +190,8 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
const activeTabId = ref<string | null>(restored.activeTabId);
|
const activeTabId = ref<string | null>(restored.activeTabId);
|
||||||
const showCloseConfirm = ref(false);
|
const showCloseConfirm = ref(false);
|
||||||
const pendingCloseTabId = ref<string | null>(null);
|
const pendingCloseTabId = ref<string | null>(null);
|
||||||
|
const pendingBatchCloseTabIds = ref<string[] | null>(null);
|
||||||
|
const pendingBatchCloseFinalActiveTabId = ref<string | null | undefined>(undefined);
|
||||||
for (const tab of restored.tabs) {
|
for (const tab of restored.tabs) {
|
||||||
if (tab.mode === "data") void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
|
if (tab.mode === "data") void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
|
||||||
}
|
}
|
||||||
@@ -797,6 +798,53 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
if (tab) tab.originalSql = tab.sql;
|
if (tab) tab.originalSql = tab.sql;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function finishPendingBatchClose() {
|
||||||
|
const finalActiveTabId = pendingBatchCloseFinalActiveTabId.value;
|
||||||
|
pendingBatchCloseTabIds.value = null;
|
||||||
|
pendingBatchCloseFinalActiveTabId.value = undefined;
|
||||||
|
if (finalActiveTabId !== undefined) {
|
||||||
|
activeTabId.value = finalActiveTabId && tabs.value.some((tab) => tab.id === finalActiveTabId) ? finalActiveTabId : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function continuePendingBatchClose() {
|
||||||
|
const pendingIds = pendingBatchCloseTabIds.value;
|
||||||
|
if (!pendingIds) return;
|
||||||
|
|
||||||
|
const remainingIds = pendingIds.filter((id) => tabs.value.some((tab) => tab.id === id));
|
||||||
|
pendingBatchCloseTabIds.value = remainingIds;
|
||||||
|
if (remainingIds.length === 0) {
|
||||||
|
finishPendingBatchClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirtyTab = remainingIds.map((id) => tabs.value.find((tab) => tab.id === id)).find((tab): tab is QueryTab => !!tab && isTabDirty(tab));
|
||||||
|
if (dirtyTab) {
|
||||||
|
// Batch close must pause before dropping dirty query tabs so the existing save/discard dialog can protect unsaved SQL.
|
||||||
|
pendingCloseTabId.value = dirtyTab.id;
|
||||||
|
showCloseConfirm.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishPendingBatchClose();
|
||||||
|
for (const id of remainingIds) closeTab(id, { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginBatchClose(ids: string[], finalActiveTabId?: string | null) {
|
||||||
|
const uniqueIds = [...new Set(ids)].filter((id) => tabs.value.some((tab) => tab.id === id));
|
||||||
|
if (uniqueIds.length === 0) return;
|
||||||
|
pendingBatchCloseTabIds.value = uniqueIds;
|
||||||
|
pendingBatchCloseFinalActiveTabId.value = finalActiveTabId;
|
||||||
|
continuePendingBatchClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumePendingBatchCloseAfter(id: string) {
|
||||||
|
const pendingIds = pendingBatchCloseTabIds.value;
|
||||||
|
if (!pendingIds?.includes(id)) return;
|
||||||
|
pendingBatchCloseTabIds.value = pendingIds.filter((pendingId) => pendingId !== id);
|
||||||
|
continuePendingBatchClose();
|
||||||
|
}
|
||||||
|
|
||||||
function closeTab(id: string, { force = false }: { force?: boolean } = {}) {
|
function closeTab(id: string, { force = false }: { force?: boolean } = {}) {
|
||||||
const tab = tabs.value.find((t) => t.id === id);
|
const tab = tabs.value.find((t) => t.id === id);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
@@ -818,6 +866,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
if (activeTabId.value === id) {
|
if (activeTabId.value === id) {
|
||||||
activeTabId.value = tabs.value[Math.min(idx, tabs.value.length - 1)]?.id ?? null;
|
activeTabId.value = tabs.value[Math.min(idx, tabs.value.length - 1)]?.id ?? null;
|
||||||
}
|
}
|
||||||
|
if (force) resumePendingBatchCloseAfter(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceClosePendingTab() {
|
function forceClosePendingTab() {
|
||||||
@@ -830,6 +879,8 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
function cancelClosePendingTab() {
|
function cancelClosePendingTab() {
|
||||||
pendingCloseTabId.value = null;
|
pendingCloseTabId.value = null;
|
||||||
showCloseConfirm.value = false;
|
showCloseConfirm.value = false;
|
||||||
|
pendingBatchCloseTabIds.value = null;
|
||||||
|
pendingBatchCloseFinalActiveTabId.value = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveAndClosePendingTab() {
|
function saveAndClosePendingTab() {
|
||||||
@@ -841,35 +892,18 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeOtherTabs(id: string) {
|
function closeOtherTabs(id: string) {
|
||||||
tabs.value
|
if (!tabs.value.some((tab) => tab.id === id)) return;
|
||||||
.filter((tab) => tab.id !== id)
|
beginBatchClose(
|
||||||
.forEach((tab) => {
|
tabs.value.filter((tab) => tab.id !== id).map((tab) => tab.id),
|
||||||
clearDataGridPendingSnapshotsForTab(tab.id);
|
id,
|
||||||
if (tab.isExecuting) void cancelTabExecution(tab.id);
|
);
|
||||||
if (tab.isExplaining) void cancelTabExplain(tab.id);
|
|
||||||
void closeResultSession(tab);
|
|
||||||
void closeClientConnectionSession(tab);
|
|
||||||
clearResultRunSnapshots(tab);
|
|
||||||
clearResultPayload(tab);
|
|
||||||
});
|
|
||||||
const next = closeOtherTabsState(tabs.value, activeTabId.value, id);
|
|
||||||
tabs.value = next.tabs;
|
|
||||||
activeTabId.value = next.activeTabId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAllTabs() {
|
function closeAllTabs() {
|
||||||
tabs.value.forEach((tab) => {
|
beginBatchClose(
|
||||||
clearDataGridPendingSnapshotsForTab(tab.id);
|
tabs.value.map((tab) => tab.id),
|
||||||
if (tab.isExecuting) void cancelTabExecution(tab.id);
|
null,
|
||||||
if (tab.isExplaining) void cancelTabExplain(tab.id);
|
);
|
||||||
void closeResultSession(tab);
|
|
||||||
void closeClientConnectionSession(tab);
|
|
||||||
clearResultRunSnapshots(tab);
|
|
||||||
clearResultPayload(tab);
|
|
||||||
});
|
|
||||||
const next = closeAllTabsState(tabs.value, activeTabId.value);
|
|
||||||
tabs.value = next.tabs;
|
|
||||||
activeTabId.value = next.activeTabId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateTab(id: string) {
|
function duplicateTab(id: string) {
|
||||||
|
|||||||
@@ -236,6 +236,64 @@ test("marked-clean object source tabs close without unsaved confirmation", () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("close all tabs pauses on unsaved query tabs", () => {
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
const store = useQueryStore();
|
||||||
|
const queryId = store.createTab("conn-1", "db", "draft query");
|
||||||
|
store.updateSql(queryId, "select 1;");
|
||||||
|
const dataId = store.createTab("conn-1", "db", "users", "data");
|
||||||
|
|
||||||
|
store.closeAllTabs();
|
||||||
|
|
||||||
|
assert.equal(store.showCloseConfirm, true);
|
||||||
|
assert.equal(store.pendingCloseTabId, queryId);
|
||||||
|
assert.deepEqual(
|
||||||
|
store.tabs.map((tab) => tab.id),
|
||||||
|
[queryId, dataId],
|
||||||
|
);
|
||||||
|
|
||||||
|
store.forceClosePendingTab();
|
||||||
|
|
||||||
|
assert.equal(store.showCloseConfirm, false);
|
||||||
|
assert.deepEqual(store.tabs, []);
|
||||||
|
assert.equal(store.activeTabId, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("close other tabs pauses on unsaved query tabs before keeping target tab", () => {
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
const store = useQueryStore();
|
||||||
|
const queryId = store.createTab("conn-1", "db", "draft query");
|
||||||
|
store.updateSql(queryId, "select 1;");
|
||||||
|
const dataId = store.createTab("conn-1", "db", "users", "data");
|
||||||
|
|
||||||
|
store.closeOtherTabs(dataId);
|
||||||
|
|
||||||
|
assert.equal(store.showCloseConfirm, true);
|
||||||
|
assert.equal(store.pendingCloseTabId, queryId);
|
||||||
|
assert.deepEqual(
|
||||||
|
store.tabs.map((tab) => tab.id),
|
||||||
|
[queryId, dataId],
|
||||||
|
);
|
||||||
|
|
||||||
|
store.cancelClosePendingTab();
|
||||||
|
|
||||||
|
assert.equal(store.showCloseConfirm, false);
|
||||||
|
assert.deepEqual(
|
||||||
|
store.tabs.map((tab) => tab.id),
|
||||||
|
[queryId, dataId],
|
||||||
|
);
|
||||||
|
|
||||||
|
store.closeOtherTabs(dataId);
|
||||||
|
store.forceClosePendingTab();
|
||||||
|
|
||||||
|
assert.equal(store.showCloseConfirm, false);
|
||||||
|
assert.deepEqual(
|
||||||
|
store.tabs.map((tab) => tab.id),
|
||||||
|
[dataId],
|
||||||
|
);
|
||||||
|
assert.equal(store.activeTabId, dataId);
|
||||||
|
});
|
||||||
|
|
||||||
test("editing query sql preserves the displayed result editability state", () => {
|
test("editing query sql preserves the displayed result editability state", () => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
const store = useQueryStore();
|
const store = useQueryStore();
|
||||||
|
|||||||
Reference in new issue
Block a user