1 parent
492c9727ae
commit
332daba302
6 files changed
+272
-9
No files matched your search
+40
@@ -33,6 +33,46 @@ describe("connectionStore disconnect data-tab metadata freshness", () => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("disconnect keeps an unsaved query pending until the user resolves scoped close", async () => {
|
||||
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
|
||||
const disconnectDb = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth,
|
||||
disconnectDb,
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.disconnectTabHandlingMode = "close-tabs";
|
||||
store.connections = [pgConnection("conn-a"), pgConnection("conn-b")];
|
||||
store.connectedIds.add("conn-a");
|
||||
|
||||
const queryStore = useQueryStore();
|
||||
const queryId = queryStore.createTab("conn-a", "app", "draft query");
|
||||
queryStore.updateSql(queryId, "select 1;");
|
||||
const outsideId = queryStore.createTab("conn-b", "app", "outside");
|
||||
|
||||
await store.disconnect("conn-a");
|
||||
|
||||
expect(disconnectDb).toHaveBeenCalledWith("conn-a", undefined);
|
||||
expect(store.connectedIds.has("conn-a")).toBe(false);
|
||||
expect(queryStore.showCloseConfirm).toBe(true);
|
||||
expect(queryStore.pendingCloseTabId).toBe(queryId);
|
||||
expect(queryStore.tabs.map((tab) => tab.id)).toEqual([queryId, outsideId]);
|
||||
|
||||
queryStore.forceCloseAllPendingTabs();
|
||||
expect(queryStore.tabs.map((tab) => tab.id)).toEqual([outsideId]);
|
||||
}, 10_000);
|
||||
|
||||
it("clears data-tab metadata freshness for the disconnected connection only", async () => {
|
||||
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
|
||||
const disconnectDb = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -132,12 +132,14 @@ describe("connectionStore one_time runtime cleanup", () => {
|
||||
const store = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
store.connections = [previewConnection()];
|
||||
queryStore.createTab("preview-1", "", "sales.parquet", "query");
|
||||
const queryId = queryStore.createTab("preview-1", "", "sales.parquet", "query");
|
||||
queryStore.updateSql(queryId, "select 1;");
|
||||
expect(queryStore.tabs.some((tab) => tab.connectionId === "preview-1")).toBe(true);
|
||||
|
||||
await store.removeConnection("preview-1");
|
||||
|
||||
expect(queryStore.tabs.some((tab) => tab.connectionId === "preview-1")).toBe(false);
|
||||
expect(queryStore.showCloseConfirm).toBe(false);
|
||||
});
|
||||
|
||||
it("deleteConnectionGroups closes the tabs of removed one_time connections", async () => {
|
||||
|
||||
@@ -77,6 +77,28 @@ describe("queryStore structure editor close protection", () => {
|
||||
expect(store.closeConfirmContext).toBe("app");
|
||||
});
|
||||
|
||||
it("protects scoped dirty structure tabs when SQL confirmation is disabled", async () => {
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const settingsStore = useSettingsStore();
|
||||
settingsStore.editorSettings.confirmUnsavedSqlClose = false;
|
||||
const store = useQueryStore();
|
||||
const structureId = store.openTableStructure("mysql-1", "app", undefined, "users");
|
||||
const structureTab = store.tabs.find((item) => item.id === structureId)!;
|
||||
structureTab.structureDraft = { dirty: true } as typeof structureTab.structureDraft;
|
||||
const queryId = store.createTab("mysql-1", "app", "Query", "query");
|
||||
store.updateSql(queryId, "select 1");
|
||||
const outsideId = store.createTab("mysql-2", "app", "Outside", "query");
|
||||
|
||||
store.closeConnectionTabs("mysql-1");
|
||||
|
||||
expect(store.showCloseConfirm).toBe(true);
|
||||
expect(store.pendingCloseTabId).toBe(structureId);
|
||||
expect(store.closeConfirmDirtyTabIds).toEqual([structureId]);
|
||||
store.cancelClosePendingTab();
|
||||
expect(store.tabs.map((tab) => tab.id)).toEqual([structureId, queryId, outsideId]);
|
||||
});
|
||||
|
||||
it("discards a structure draft before force closing", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
@@ -824,7 +824,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const queryStore = useQueryStore();
|
||||
for (const connectionId of connectionIds) {
|
||||
queryStore.closeConnectionTabs(connectionId);
|
||||
queryStore.closeConnectionTabs(connectionId, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2429,7 +2429,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (pendingBatchCloseTabIds.value) {
|
||||
return pendingBatchCloseTabIds.value
|
||||
.map((id) => tabs.value.find((tab) => tab.id === id))
|
||||
.filter((tab): tab is QueryTab => !!tab && isTabDirty(tab))
|
||||
.filter((tab): tab is QueryTab => !!tab && shouldConfirmTabClose(tab))
|
||||
.map((tab) => tab.id);
|
||||
}
|
||||
const pendingTab = pendingCloseTabId.value ? tabs.value.find((tab) => tab.id === pendingCloseTabId.value) : undefined;
|
||||
@@ -2579,9 +2579,31 @@ export const useQueryStore = defineStore("query", () => {
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
pendingBatchCloseTabIds.value = uniqueIds;
|
||||
pendingBatchCloseFinalActiveTabId.value = finalActiveTabId;
|
||||
pendingBatchCloseComplete = onComplete ?? null;
|
||||
|
||||
const existingIds = pendingBatchCloseTabIds.value;
|
||||
if (existingIds) {
|
||||
// Sidebar bulk disconnects settle independently, so later scopes must join the open dialog instead of replacing it.
|
||||
const combinedIds = [...new Set([...existingIds, ...uniqueIds])];
|
||||
const preferredFinalActiveTabId = finalActiveTabId !== undefined ? finalActiveTabId : pendingBatchCloseFinalActiveTabId.value;
|
||||
pendingBatchCloseTabIds.value = combinedIds;
|
||||
pendingBatchCloseFinalActiveTabId.value = preferredFinalActiveTabId && combinedIds.includes(preferredFinalActiveTabId) ? activeTabAfterClosing(combinedIds, preferredFinalActiveTabId) : preferredFinalActiveTabId;
|
||||
if (onComplete) {
|
||||
const previousComplete = pendingBatchCloseComplete;
|
||||
pendingBatchCloseComplete = previousComplete
|
||||
? () => {
|
||||
try {
|
||||
previousComplete();
|
||||
} finally {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
: onComplete;
|
||||
}
|
||||
} else {
|
||||
pendingBatchCloseTabIds.value = uniqueIds;
|
||||
pendingBatchCloseFinalActiveTabId.value = finalActiveTabId;
|
||||
pendingBatchCloseComplete = onComplete ?? null;
|
||||
}
|
||||
continuePendingBatchClose();
|
||||
}
|
||||
|
||||
@@ -2757,6 +2779,17 @@ export const useQueryStore = defineStore("query", () => {
|
||||
return tabs.value.find((tab) => !closingIds.has(tab.id))?.id ?? null;
|
||||
}
|
||||
|
||||
function activeTabAfterClosing(ids: string[], preferredActiveTabId = activeTabId.value) {
|
||||
const closingIds = new Set(ids);
|
||||
if (preferredActiveTabId && !closingIds.has(preferredActiveTabId) && tabs.value.some((tab) => tab.id === preferredActiveTabId)) {
|
||||
return preferredActiveTabId;
|
||||
}
|
||||
const preferredIndex = preferredActiveTabId ? tabs.value.findIndex((tab) => tab.id === preferredActiveTabId) : -1;
|
||||
const remainingTabs = tabs.value.filter((tab) => !closingIds.has(tab.id));
|
||||
if (preferredIndex < 0) return remainingTabs[0]?.id ?? null;
|
||||
return remainingTabs[Math.min(preferredIndex, remainingTabs.length - 1)]?.id ?? null;
|
||||
}
|
||||
|
||||
function closeOtherRegularTabs(id: string) {
|
||||
const tab = tabs.value.find((item) => item.id === id);
|
||||
if (!tab || tab.pinned) return;
|
||||
@@ -2905,12 +2938,21 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
}
|
||||
|
||||
function closeConnectionTabs(connectionId: string) {
|
||||
closeTabsWhere((tab) => tab.connectionId === connectionId);
|
||||
function closeScopedTabsWhere(predicate: (tab: QueryTab) => boolean, options: { force?: boolean } = {}) {
|
||||
const ids = tabs.value.filter((tab) => predicate(tab)).map((tab) => tab.id);
|
||||
if (options.force) {
|
||||
closeTabsWhere(predicate);
|
||||
return;
|
||||
}
|
||||
beginBatchClose(ids, activeTabAfterClosing(ids));
|
||||
}
|
||||
|
||||
function closeConnectionTabs(connectionId: string, options?: { force?: boolean }) {
|
||||
closeScopedTabsWhere((tab) => tab.connectionId === connectionId, options);
|
||||
}
|
||||
|
||||
function closeDatabaseTabs(connectionId: string, database: string) {
|
||||
closeTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database);
|
||||
closeScopedTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database);
|
||||
}
|
||||
|
||||
function tabMatchesDroppedTableObject(tab: QueryTab, target: DroppedTableObjectTarget): boolean {
|
||||
|
||||
Reference in new issue
Block a user