fix(tabs): allow duplicating data tabs with fresh results

This commit is contained in:
OOMEcho
2026-09-30 19:47:51 +08:00
committed by GitHub
parent 5017aa7bb9
commit 5433f067d7
5 changed files with 91 additions and 9 deletions
@@ -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<HTMLElement>("[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");
+19 -1
View File
@@ -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;
+1 -1
View File
@@ -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",
},
];
}
@@ -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();
+10 -7
View File
@@ -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) {