From 5433f067d70153e7d310baaafffbd3db95e2ec70 Mon Sep 17 00:00:00 2001 From: OOMEcho <228389787@qq.com> Date: Wed, 30 Sep 2026 19:47:51 +0800 Subject: [PATCH] fix(tabs): allow duplicating data tabs with fresh results --- .../EditorGroupTabBar.behavior.spec.ts | 14 ++++++ .../src/lib/__tests__/tabs/tabMenu.spec.ts | 20 +++++++- apps/desktop/src/lib/tabs/tabMenu.ts | 2 +- .../queryStore.tableDataRefresh.spec.ts | 47 +++++++++++++++++++ apps/desktop/src/stores/queryStore.ts | 17 ++++--- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts b/apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts index cfc0eccffc..af23ab7815 100644 --- a/apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts @@ -167,6 +167,20 @@ describe("EditorGroupTabBar behavior", () => { setActivePinia(pinia); }); + it("offers duplicate without rename on a data tab", async () => { + const store = useQueryStore(); + const id = store.createTab("pg-1", "app", "users", "data", "public"); + const { app, host } = mountBar(store.groups[0].id, [id], id, pinia); + await settle(); + + const menu = JSON.parse(host.querySelector("[data-menu-items]")!.dataset.menuItems!); + expect(menu).toContainEqual(expect.objectContaining({ label: "Duplicate", visible: true })); + expect(menu).not.toContainEqual(expect.objectContaining({ label: "Rename" })); + + app.unmount(); + host.remove(); + }); + it("activates a tab on plain click", async () => { const store = useQueryStore(); const firstId = store.createTab("pg-1", "app", "Query 1", "query"); diff --git a/apps/desktop/src/lib/__tests__/tabs/tabMenu.spec.ts b/apps/desktop/src/lib/__tests__/tabs/tabMenu.spec.ts index b142c7acd5..95d48942ec 100644 --- a/apps/desktop/src/lib/__tests__/tabs/tabMenu.spec.ts +++ b/apps/desktop/src/lib/__tests__/tabs/tabMenu.spec.ts @@ -4,7 +4,7 @@ import { createCloseAllTabMenuItem, createCloseLeftTabMenuItem, createCloseOther const t = (key: string) => key; describe("shared tab menu helpers", () => { - it("builds rename and duplicate items only when renaming is allowed", () => { + it("builds rename and duplicate items for query tabs", () => { const items = createRenameDuplicateTabItems({ tab: { id: "t1" } as never, t, @@ -29,6 +29,24 @@ describe("shared tab menu helpers", () => { expect(queryOnly[0]?.visible).toBe(false); }); + it("allows duplicating data tabs without allowing rename", () => { + let duplicated = false; + const items = createRenameDuplicateTabItems({ + tab: { id: "data", mode: "data" } as never, + t, + canRename: false, + onRename: () => undefined, + onDuplicate: () => { + duplicated = true; + }, + }); + expect(items).toHaveLength(1); + expect(items[0]?.label).toBe("contextMenu.duplicateTab"); + expect(items[0]?.visible).toBe(true); + items[0]?.action?.(); + expect(duplicated).toBe(true); + }); + it("builds locate and pin menu items with the expected actions", () => { let located = false; let toggled = false; diff --git a/apps/desktop/src/lib/tabs/tabMenu.ts b/apps/desktop/src/lib/tabs/tabMenu.ts index 2d57277707..5f0f05ef16 100644 --- a/apps/desktop/src/lib/tabs/tabMenu.ts +++ b/apps/desktop/src/lib/tabs/tabMenu.ts @@ -17,7 +17,7 @@ export function createRenameDuplicateTabItems(options: { tab: QueryTab; t: (key: label: options.t("contextMenu.duplicateTab"), action: options.onDuplicate, icon: Copy, - visible: options.canRename, + visible: options.canRename || options.tab.mode === "data", }, ]; } diff --git a/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts index b4eacb80b5..38b2f7166c 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts @@ -77,6 +77,53 @@ describe("queryStore table data refresh", () => { ]); }); + it("loads a duplicated data tab with its filter, database sort and page without sharing results", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const id = store.createTab("pg-1", "app", "users", "data", "public"); + store.setTableMeta(id, { + database: "app", + schema: "public", + tableName: "users", + tableType: "TABLE", + columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }], + primaryKeys: ["id"], + }); + const original = store.tabs.find((tab) => tab.id === id)!; + original.whereInput = "status = 'ACTIVE'"; + original.orderByInput = "id DESC"; + original.resultSortColumn = "id"; + original.resultSortDirection = "desc"; + original.resultSortMode = "database"; + original.resultPageLimit = 25; + original.resultPageOffset = 50; + original.result = { columns: ["id"], rows: [[99]], affected_rows: 0, execution_time_ms: 1 }; + mocks.executeMulti.mockResolvedValue([{ columns: ["id"], rows: [[42]], affected_rows: 0, execution_time_ms: 1 }]); + + store.duplicateTab(id); + const copy = store.tabs[1]; + await vi.waitFor(() => expect(copy.result?.rows).toEqual([[42]])); + + expect(copy.id).not.toBe(id); + expect(copy.mode).toBe("data"); + expect(copy.connectionId).toBe("pg-1"); + expect(copy.database).toBe("app"); + expect(copy.schema).toBe("public"); + expect(store.activeTabId).toBe(copy.id); + expect(store.groups[0].tabIds).toEqual([id, copy.id]); + expect(mocks.buildTableSelectSql).toHaveBeenCalledWith( + expect.objectContaining({ + whereInput: "status = 'ACTIVE'", + orderBy: "id DESC", + limit: 25, + offset: 50, + }), + ); + expect(mocks.executeMulti).toHaveBeenCalledTimes(1); + expect(original.result?.rows).toEqual([[99]]); + expect(copy.result).not.toBe(original.result); + }); + it("refreshes only matching data tabs after a table mutation", async () => { const { useQueryStore } = await import("@/stores/queryStore"); const store = useQueryStore(); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 6ca2b3012e..88e732b2a3 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -4645,18 +4645,18 @@ export const useQueryStore = defineStore("query", () => { lastExecutedSql: undefined, resultBaseSql: original.resultBaseSql, resultSortedSql: undefined, - resultSortColumn: undefined, - resultSortColumnIndex: undefined, - resultSortDirection: undefined, - resultSortMode: undefined, + resultSortColumn: original.mode === "data" && original.resultSortMode === "database" ? original.resultSortColumn : undefined, + resultSortColumnIndex: original.mode === "data" && original.resultSortMode === "database" ? original.resultSortColumnIndex : undefined, + resultSortDirection: original.mode === "data" && original.resultSortMode === "database" ? original.resultSortDirection : undefined, + resultSortMode: original.mode === "data" && original.resultSortMode === "database" ? original.resultSortMode : undefined, resultLocalSortOriginalRows: undefined, resultLocalSortOriginalLargeValueCells: undefined, resultLocalSortOriginalMongoDocuments: undefined, resultLocalSortOriginalMongoCopyDocuments: undefined, - orderByInput: undefined, + orderByInput: original.mode === "data" ? original.orderByInput : undefined, resultPageSql: undefined, - resultPageLimit: undefined, - resultPageOffset: undefined, + resultPageLimit: original.mode === "data" ? original.resultPageLimit : undefined, + resultPageOffset: original.mode === "data" ? original.resultPageOffset : undefined, resultExecutedPageLimit: undefined, resultExecutedPageOffset: undefined, resultCountSql: undefined, @@ -4696,6 +4696,8 @@ export const useQueryStore = defineStore("query", () => { objectSource: original.objectSource ? { ...original.objectSource } : undefined, sourceView: original.sourceView, tableMeta: original.tableMeta ? { ...original.tableMeta, columns: [...original.tableMeta.columns], primaryKeys: [...original.tableMeta.primaryKeys] } : undefined, + tableMetaGeneration: original.mode === "data" ? original.tableMetaGeneration : undefined, + tableMetaUpdatedAt: original.mode === "data" ? original.tableMetaUpdatedAt : undefined, queryAnalysis: original.queryAnalysis ? { ...original.queryAnalysis, sources: original.queryAnalysis.sources?.map((source) => ({ ...source })), columns: original.queryAnalysis.columns.map((c) => ({ ...c })) } : undefined, querySourceColumns: original.querySourceColumns ? [...original.querySourceColumns] : undefined, queryWriteTargets: original.queryWriteTargets?.map((target) => ({ ...target, sourceColumns: [...target.sourceColumns] })), @@ -4717,6 +4719,7 @@ export const useQueryStore = defineStore("query", () => { focusedGroupId.value = owner.id; } activeTabId.value = newId; + if (newTab.mode === "data") void refreshDataTab(newId); } function closeTabsWhere(predicate: (tab: QueryTab) => boolean) {