feat(tabs): add duplicate tab feature in context menu

This commit is contained in:
t8y2
2026-06-11 01:37:58 +08:00
parent c8fb749ef6
commit c9203de1e7
8 changed files with 77 additions and 1 deletions
@@ -2,7 +2,7 @@
import { computed, ref, watch, nextTick } from "vue";
import type { CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Check, Lock } from "@lucide/vue";
import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Check, Lock, Copy } from "@lucide/vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import LightDropdown from "@/components/ui/LightDropdown.vue";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
@@ -83,6 +83,12 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
icon: Pencil,
visible: canRenameTab(tab),
},
{
label: t("contextMenu.duplicateTab"),
action: () => queryStore.duplicateTab(tab.id),
icon: Copy,
visible: canRenameTab(tab),
},
{ label: "", separator: true },
{
label: tab.pinned ? t("contextMenu.unpin") : t("contextMenu.pin"),
+1
View File
@@ -1010,6 +1010,7 @@
unpin: "Unpin",
refreshTab: "Refresh Data",
renameTab: "Rename Tab",
duplicateTab: "Duplicate Tab",
compactTabTitle: "Shorten Tab Titles",
closeTab: "Close Tab",
closeOtherTabs: "Close Other Tabs",
+1
View File
@@ -873,6 +873,7 @@
unpin: "Desfijar",
refreshTab: "Actualizar datos",
renameTab: "Renombrar pestaña",
duplicateTab: "Duplicar pestaña",
closeTab: "Cerrar pestaña",
closeOtherTabs: "Cerrar otras pestañas",
closeAllTabs: "Cerrar todas las pestañas",
+1
View File
@@ -974,6 +974,7 @@
unpin: "Sblocca",
refreshTab: "Aggiorna Dati",
renameTab: "Rinomina Scheda",
duplicateTab: "Duplica Scheda",
compactTabTitle: "Accorcia Titoli Schede",
closeTab: "Chiudi Scheda",
closeOtherTabs: "Chiudi Altre Schede",
+1
View File
@@ -974,6 +974,7 @@
unpin: "Desafixar",
refreshTab: "Atualizar Dados",
renameTab: "Renomear Aba",
duplicateTab: "Duplicar Aba",
compactTabTitle: "Encurtar Títulos das Abas",
closeTab: "Fechar Aba",
closeOtherTabs: "Fechar Outras Abas",
+1
View File
@@ -1009,6 +1009,7 @@
unpin: "取消置顶",
refreshTab: "刷新数据",
renameTab: "重命名标签页",
duplicateTab: "复制标签页",
compactTabTitle: "缩短标签标题",
closeTab: "关闭标签页",
closeOtherTabs: "关闭其他标签页",
+1
View File
@@ -953,6 +953,7 @@
unpin: "取消置頂",
refreshTab: "重新整理資料",
renameTab: "重新命名分頁",
duplicateTab: "複製分頁",
compactTabTitle: "縮短標籤標題",
closeTab: "關閉分頁",
closeOtherTabs: "關閉其他分頁",
+64
View File
@@ -397,6 +397,69 @@ export const useQueryStore = defineStore("query", () => {
activeTabId.value = next.activeTabId;
}
function duplicateTab(id: string) {
const idx = tabs.value.findIndex((t) => t.id === id);
if (idx < 0) return;
const original = tabs.value[idx];
const newId = uuid();
const newTab: QueryTab = {
id: newId,
title: original.title,
customTitle: original.customTitle,
connectionId: original.connectionId,
database: original.database,
schema: original.schema,
sql: original.sql,
savedSqlId: original.savedSqlId,
lastExecutedSql: undefined,
resultBaseSql: original.resultBaseSql,
resultSortedSql: undefined,
resultSortColumn: undefined,
resultSortColumnIndex: undefined,
resultSortDirection: undefined,
orderByInput: undefined,
resultPageSql: undefined,
resultPageLimit: undefined,
resultPageOffset: undefined,
resultCountSql: undefined,
resultTotalRowCount: undefined,
resultTotalRowCountLoading: undefined,
resultSessionId: undefined,
resultAccessedAt: undefined,
resultCacheKey: undefined,
resultCacheState: undefined,
pinned: false,
result: undefined,
results: undefined,
activeResultIndex: undefined,
explainPlan: undefined,
explainError: undefined,
explainSql: undefined,
lastExplainedSql: undefined,
isExecuting: false,
isCancelling: false,
queryExecutionStartedAt: undefined,
editorViewport: undefined,
editorSelection: undefined,
executionId: undefined,
isExplaining: false,
explainExecutionId: undefined,
mode: original.mode,
structureTableName: original.structureTableName,
objectBrowser: original.objectBrowser ? { ...original.objectBrowser } : undefined,
objectSource: original.objectSource ? { ...original.objectSource } : undefined,
tableMeta: original.tableMeta ? { ...original.tableMeta, columns: [...original.tableMeta.columns], primaryKeys: [...original.tableMeta.primaryKeys] } : undefined,
queryAnalysis: original.queryAnalysis ? { ...original.queryAnalysis, columns: original.queryAnalysis.columns.map((c) => ({ ...c })) } : undefined,
querySourceColumns: original.querySourceColumns ? [...original.querySourceColumns] : undefined,
queryEditabilityReason: original.queryEditabilityReason,
resultEvicted: undefined,
whereInput: original.whereInput,
previewSql: original.previewSql,
};
tabs.value.splice(idx + 1, 0, newTab);
activeTabId.value = newId;
}
function closeTabsWhere(predicate: (tab: QueryTab) => boolean) {
const closingIds = new Set(tabs.value.filter((tab) => predicate(tab)).map((tab) => tab.id));
if (closingIds.size === 0) return;
@@ -1535,6 +1598,7 @@ export const useQueryStore = defineStore("query", () => {
closeTab,
closeOtherTabs,
closeAllTabs,
duplicateTab,
closeConnectionTabs,
closeDatabaseTabs,
releaseConnectionTabs,