fix(tabs): close dropped table tabs
This commit is contained in:
parent
90d2ede740
commit
32a9496d2a
|
|
@ -602,6 +602,7 @@ async function confirmDrop() {
|
|||
await api.executeQuery(props.connection.id, props.database, sql);
|
||||
const successKey = row.type === "VIEW" ? "contextMenu.dropViewSuccess" : row.type === "PROCEDURE" ? "contextMenu.dropProcedureSuccess" : row.type === "FUNCTION" ? "contextMenu.dropFunctionSuccess" : "contextMenu.dropTableSuccess";
|
||||
toast(t(successKey, { name: row.name }));
|
||||
closeDroppedTableObjectTabsForRow(row);
|
||||
await reload();
|
||||
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, row.schema || selectedSchema.value);
|
||||
} catch (e: any) {
|
||||
|
|
@ -668,6 +669,25 @@ function openStructureEditor(row: ObjectBrowserRow) {
|
|||
queryStore.openTableStructure(props.connection.id, props.database, row.schema || selectedSchema.value, row.name);
|
||||
}
|
||||
|
||||
function droppedTableObjectTypeForRow(row: ObjectBrowserRow): "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | null {
|
||||
if (row.type === "TABLE") return "TABLE";
|
||||
if (row.type === "VIEW") return "VIEW";
|
||||
if (row.type === "MATERIALIZED_VIEW") return "MATERIALIZED_VIEW";
|
||||
return null;
|
||||
}
|
||||
|
||||
function closeDroppedTableObjectTabsForRow(row: ObjectBrowserRow) {
|
||||
const objectType = droppedTableObjectTypeForRow(row);
|
||||
if (!objectType) return;
|
||||
queryStore.closeDroppedTableObjectTabs({
|
||||
connectionId: props.connection.id,
|
||||
database: props.database,
|
||||
schema: row.schema || selectedSchema.value,
|
||||
name: row.name,
|
||||
objectType,
|
||||
});
|
||||
}
|
||||
|
||||
function openDiagram(row: ObjectBrowserRow) {
|
||||
connectionStore.diagramSource = {
|
||||
connectionId: props.connection.id,
|
||||
|
|
@ -796,6 +816,7 @@ async function confirmBatchDropTables() {
|
|||
name: row.name,
|
||||
});
|
||||
await api.executeQuery(props.connection.id, props.database, sql);
|
||||
closeDroppedTableObjectTabsForRow(row);
|
||||
}
|
||||
toast(t("objects.batchDropSuccess", { count: targets.length }));
|
||||
clearTableSelection();
|
||||
|
|
|
|||
|
|
@ -1963,6 +1963,28 @@ function canDropTreeNode(node: TreeNode): boolean {
|
|||
return canDropTableChildObjectNode(node);
|
||||
}
|
||||
|
||||
function droppedTableObjectTypeForNode(node: TreeNode): "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | null {
|
||||
if (node.type === "table") return "TABLE";
|
||||
if (node.type === "view") return "VIEW";
|
||||
if (node.type === "materialized_view") return "MATERIALIZED_VIEW";
|
||||
return null;
|
||||
}
|
||||
|
||||
function closeDroppedTableObjectTabsForNode(node: TreeNode) {
|
||||
const objectType = droppedTableObjectTypeForNode(node);
|
||||
if (!objectType || !node.connectionId || !node.database) return;
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
const dataTabSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
|
||||
queryStore.closeDroppedTableObjectTabs({
|
||||
connectionId: node.connectionId,
|
||||
database: node.database,
|
||||
schema: dataTabSchema,
|
||||
schemaCandidates: [node.schema, dataTabSchema],
|
||||
name: node.label,
|
||||
objectType,
|
||||
});
|
||||
}
|
||||
|
||||
function selectedBatchDropTargets(): TreeNode[] {
|
||||
const selected = selectedTreeNodesInVisibleOrder();
|
||||
if (selected.length <= 1 || !selected.some((node) => node.id === props.node.id)) return [];
|
||||
|
|
@ -2185,6 +2207,7 @@ async function confirmDropObject() {
|
|||
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
|
||||
const msgKey = node.type === "view" ? "contextMenu.dropViewSuccess" : node.type === "materialized_view" ? "contextMenu.dropViewSuccess" : node.type === "procedure" ? "contextMenu.dropProcedureSuccess" : "contextMenu.dropFunctionSuccess";
|
||||
toast(t(msgKey, { name: node.label }), 3000);
|
||||
closeDroppedTableObjectTabsForNode(node);
|
||||
if (node.type === "view" || node.type === "materialized_view") {
|
||||
connectionStore.removeTreeNode(node.id);
|
||||
} else {
|
||||
|
|
@ -2247,6 +2270,7 @@ async function confirmBatchDrop() {
|
|||
const sql = await dropSqlForTreeNode(target);
|
||||
if (!sql) continue;
|
||||
await api.executeQuery(target.connectionId, target.database, sql, target.schema);
|
||||
closeDroppedTableObjectTabsForNode(target);
|
||||
connectionStore.removeTreeNode(target.id);
|
||||
}
|
||||
toast(t("contextMenu.batchDropSuccess", { count: targets.length }), 3000);
|
||||
|
|
@ -2390,6 +2414,7 @@ async function confirmDropTable() {
|
|||
const sql = dropTablePreviewSql.value || (await buildDropTableSql(tableAdminSqlOptions()));
|
||||
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
|
||||
toast(t("contextMenu.dropTableSuccess", { name: node.label }), 3000);
|
||||
closeDroppedTableObjectTabsForNode(node);
|
||||
connectionStore.removeTreeNode(node.id);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,107 @@ describe("queryStore database open state", () => {
|
|||
expect(store.isDatabaseOpen("pg-1", "analytics")).toBe(false);
|
||||
});
|
||||
|
||||
it("closes data and structure tabs for a dropped table object", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const queryId = store.createTab("pg-1", "app", "Query", "query", "public");
|
||||
const dataId = store.createTab("pg-1", "app", "users", "data", "public");
|
||||
store.setTableMeta(dataId, {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
const otherSchemaDataId = store.createTab("pg-1", "app", "users", "data", "archive");
|
||||
store.setTableMeta(otherSchemaDataId, {
|
||||
schema: "archive",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
const otherConnectionDataId = store.createTab("pg-2", "app", "users", "data", "public");
|
||||
store.setTableMeta(otherConnectionDataId, {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
const structureId = store.openTableStructure("pg-1", "app", "public", "users");
|
||||
|
||||
store.activeTabId = dataId;
|
||||
store.closeDroppedTableObjectTabs({
|
||||
connectionId: "pg-1",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
name: "users",
|
||||
objectType: "TABLE",
|
||||
});
|
||||
|
||||
expect(store.tabs.some((tab) => tab.id === dataId)).toBe(false);
|
||||
expect(store.tabs.some((tab) => tab.id === structureId)).toBe(false);
|
||||
expect(store.tabs.some((tab) => tab.id === otherSchemaDataId)).toBe(true);
|
||||
expect(store.tabs.some((tab) => tab.id === otherConnectionDataId)).toBe(true);
|
||||
expect(store.tabs.some((tab) => tab.id === queryId)).toBe(true);
|
||||
expect(store.activeTabId).not.toBe(dataId);
|
||||
});
|
||||
|
||||
it("closes data tabs but keeps structure tabs for dropped views", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const dataId = store.createTab("pg-1", "app", "report_view", "data", "public");
|
||||
store.setTableMeta(dataId, {
|
||||
schema: "public",
|
||||
tableName: "report_view",
|
||||
tableType: "VIEW",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
const structureId = store.openTableStructure("pg-1", "app", "public", "report_view");
|
||||
|
||||
store.closeDroppedTableObjectTabs({
|
||||
connectionId: "pg-1",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
name: "report_view",
|
||||
objectType: "VIEW",
|
||||
});
|
||||
|
||||
expect(store.tabs.some((tab) => tab.id === dataId)).toBe(false);
|
||||
expect(store.tabs.some((tab) => tab.id === structureId)).toBe(true);
|
||||
});
|
||||
|
||||
it("matches dropped table schema candidates", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const dataId = store.createTab("pg-1", "app", "orders", "data", "app");
|
||||
store.setTableMeta(dataId, {
|
||||
schema: "app",
|
||||
tableName: "orders",
|
||||
tableType: "TABLE",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
const structureId = store.openTableStructure("pg-1", "app", undefined, "orders");
|
||||
|
||||
store.closeDroppedTableObjectTabs({
|
||||
connectionId: "pg-1",
|
||||
database: "app",
|
||||
schema: "app",
|
||||
schemaCandidates: [undefined, "app"],
|
||||
name: "orders",
|
||||
objectType: "TABLE",
|
||||
});
|
||||
|
||||
expect(store.tabs.some((tab) => tab.id === dataId)).toBe(false);
|
||||
expect(store.tabs.some((tab) => tab.id === structureId)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not restore open tabs when launch restore mode is none", async () => {
|
||||
const persistedTabs = JSON.stringify([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -62,6 +62,17 @@ interface BuildQueryResultExportRequestOptions {
|
|||
format: "csv" | "xlsx";
|
||||
}
|
||||
|
||||
type DroppedTableObjectType = "TABLE" | "VIEW" | "MATERIALIZED_VIEW";
|
||||
|
||||
interface DroppedTableObjectTarget {
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
schemaCandidates?: Array<string | undefined>;
|
||||
name: string;
|
||||
objectType?: DroppedTableObjectType;
|
||||
}
|
||||
|
||||
function tabClientSessionId(tab: Pick<QueryTab, "id">, suffix?: (typeof BACKGROUND_CLIENT_SESSION_SUFFIXES)[number]): string {
|
||||
return suffix ? `${tab.id}:${suffix}` : tab.id;
|
||||
}
|
||||
|
|
@ -70,6 +81,15 @@ function resultRunCacheKey(tabId: string, runId: string): string {
|
|||
return `tab:${tabId}:run:${runId}`;
|
||||
}
|
||||
|
||||
function normalizeOptionalSchema(schema: string | null | undefined): string {
|
||||
return schema?.trim() ?? "";
|
||||
}
|
||||
|
||||
function droppedTableObjectSchemaCandidates(target: DroppedTableObjectTarget): Set<string> {
|
||||
const schemas = target.schemaCandidates?.length ? target.schemaCandidates : [target.schema];
|
||||
return new Set(schemas.map(normalizeOptionalSchema));
|
||||
}
|
||||
|
||||
function markQueryResultRowsRaw(result: QueryResult): QueryResult {
|
||||
markRaw(result.rows);
|
||||
return result;
|
||||
|
|
@ -1219,6 +1239,30 @@ export const useQueryStore = defineStore("query", () => {
|
|||
closeTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database);
|
||||
}
|
||||
|
||||
function tabMatchesDroppedTableObject(tab: QueryTab, target: DroppedTableObjectTarget): boolean {
|
||||
if (tab.connectionId !== target.connectionId || tab.database !== target.database) return false;
|
||||
const targetSchemas = droppedTableObjectSchemaCandidates(target);
|
||||
|
||||
if (tab.mode === "data") {
|
||||
const tableMeta = tableMetaForDataTab(tab);
|
||||
if (!tableMeta || tableMeta.tableName !== target.name) return false;
|
||||
return targetSchemas.has(normalizeOptionalSchema(tableMeta.schema ?? tab.schema));
|
||||
}
|
||||
|
||||
if ((target.objectType ?? "TABLE") === "TABLE" && tab.mode === "structure") {
|
||||
if ((tab.structureTableName || "") !== target.name) return false;
|
||||
return targetSchemas.has(normalizeOptionalSchema(tab.schema));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function closeDroppedTableObjectTabs(target: DroppedTableObjectTarget) {
|
||||
// A dropped table-like object makes existing data/structure tabs stale; close
|
||||
// them immediately instead of letting the next refresh fail against a missing object.
|
||||
closeTabsWhere((tab) => tabMatchesDroppedTableObject(tab, target));
|
||||
}
|
||||
|
||||
function releaseTabsWhere(predicate: (tab: QueryTab) => boolean) {
|
||||
closeTabsWhere((tab) => predicate(tab) && tab.mode !== "query");
|
||||
tabs.value
|
||||
|
|
@ -2857,6 +2901,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
duplicateTab,
|
||||
closeConnectionTabs,
|
||||
closeDatabaseTabs,
|
||||
closeDroppedTableObjectTabs,
|
||||
releaseConnectionTabs,
|
||||
releaseDatabaseTabs,
|
||||
isDatabaseOpen,
|
||||
|
|
|
|||
Loading…
Reference in New Issue