feat(tabs): add tab group pills with names, colors and collapsible side bar

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-04 01:23:25 +08:00
1 parent 0bdf232487
commit 2a4f1a45a9
15 files changed
+1253 -51

No files matched your search

+18 -1
View File
@@ -281,7 +281,8 @@ const rightSidebarPanelStorageKeys: Partial<Record<RightSidebarPanelId, string>>
let lastOpenedRightSidebarPanel = RIGHT_SIDEBAR_PANEL_IDS.find((panelId) => rightSidebarPanelRefs[panelId].value);
const sidebarOpen = ref(safeLocalStorageGet("dbx-sidebar-open") !== "false");
const aiPanelReady = ref(false);
const { sidebarWidth, aiPanelWidth, historyWidth, sqlLibraryWidth, sqlFilePanelWidth, startSidebarResize, startAiPanelResize, startHistoryResize, startSqlLibraryResize, startSqlFilePanelResize } = usePanelResize();
const { sidebarWidth, aiPanelWidth, historyWidth, sqlLibraryWidth, sqlFilePanelWidth, tabBarWidth, tabBarCollapsed, startSidebarResize, startAiPanelResize, startHistoryResize, startSqlLibraryResize, startSqlFilePanelResize, startLeftTabBarResize, startRightTabBarResize, setTabBarCollapsed } =
usePanelResize();
const aiAssistantRef = ref<AiAssistantHandle | null>(null);
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
const appTabBarRef = ref<InstanceType<typeof AppTabBar> | null>(null);
@@ -864,6 +865,18 @@ const tabWorkspaceLayoutClass = computed(() => {
if (settingsStore.editorSettings.tabPlacement === "right") return "flex-row-reverse";
return isVerticalTabPlacement.value ? "flex-row" : "flex-col";
});
function startTabBarResize(event: MouseEvent) {
if (settingsStore.editorSettings.tabPlacement === "right") {
startRightTabBarResize(event);
return;
}
startLeftTabBarResize(event);
}
function toggleTabBarCollapsed() {
setTabBarCollapsed(!tabBarCollapsed.value);
}
const updateNotificationsEnabled = computed(() => settingsStore.editorSettings.updateNotificationsEnabled);
function openSettings(initialTab = "appearance", initialSection?: string) {
@@ -3377,6 +3390,8 @@ onUnmounted(() => {
:agent-driver-update-count="toolbarAgentDriverUpdateCount"
:detached-drop-target="detachedDropTargetTabId !== null"
:can-detach-tabs="isDesktop"
:tab-bar-width="tabBarWidth"
:tab-bar-collapsed="tabBarCollapsed"
@toggle-zen-mode="toggleZenMode"
@activate-driver-store="openDriverStorePage"
@activate-settings-page="activateSettingsPage"
@@ -3390,6 +3405,8 @@ onUnmounted(() => {
@discard-all-tab-close="handleDiscardAllPendingTabClose"
@cancel-tab-close="cancelPendingAppClose"
@detach-tab="detachTab"
@start-resize="startTabBarResize"
@toggle-collapse="toggleTabBarCollapsed"
/>
<DetachedTabHeader
v-else-if="activeTab"
File diff suppressed because it is too large. Load diff
@@ -136,6 +136,38 @@ describe("AppTabBar special page selection", () => {
expect(tabBarSource).toContain(':style="specialTabActiveStyle(settingsPageActive)"');
expect(tabBarSource).toContain(':style="specialTabActiveStyle(driverStoreActive)"');
});
it("does not leave a query group highlighted behind an active special page", () => {
const groupActive = sourceBetween("function isTabGroupActive", "function toggleTabGroup");
expect(groupActive).toContain("if (props.driverStoreActive || props.settingsPageActive) return false;");
});
it("restores the active special page after closing the group containing the retained query tab", () => {
const restoreSurface = sourceBetween("function restoreSpecialRegularSurface", "function closeSpecialRegularSurfaces");
const closeGroup = sourceBetween("function closeTabGroup", "function tabsToRightInGroup");
expect(restoreSurface).toContain('if (surface === "settings" && props.settingsPageOpen) emit("activate-settings-page")');
expect(restoreSurface).toContain('if (surface === "driverStore" && props.driverStoreOpen) emit("activate-driver-store")');
expect(restoreSurface).toContain("const surface = pendingGroupCloseSpecialSurface.value;");
expect(restoreSurface).toContain("pendingGroupCloseSpecialSurface.value = null;");
expect(closeGroup).toContain('props.settingsPageActive ? "settings" : props.driverStoreActive ? "driverStore" : null');
expect(closeGroup).toContain("pendingGroupCloseSpecialSurface.value = activeSpecialSurface;");
expect(closeGroup).toContain("queryStore.closeTabsByIds(tabsToClose, finalActiveTabId, finishSpecialSurfacePreservingGroupClose)");
expect(closeGroup).toContain("restoreSpecialRegularSurface(activeSpecialSurface);");
});
it("keeps and restores the special page while dirty grouped tabs are confirmed or cancelled", () => {
const activeTabWatch = sourceBetween("watch(\n () => queryStore.activeTabId", "watch(\n () => props.driverStoreActive");
const dismissClose = sourceBetween("function dismissCloseConfirm", "function handleCancelClose");
const cancelClose = sourceBetween("function handleCancelClose", "const tabsContainerRef");
expect(activeTabWatch).toContain("restoreSpecialRegularSurface(pendingGroupCloseSpecialSurface.value);");
expect(dismissClose).toContain("pendingGroupCloseSpecialSurface.value = null;");
expect(dismissClose).toContain("restoreSpecialRegularSurface(specialSurface);");
expect(cancelClose).toContain("dismissCloseConfirm();");
expect(tabBarSource).toContain("if (!open) dismissCloseConfirm();");
});
});
describe("AppTabBar overflow search", () => {
@@ -162,3 +194,115 @@ describe("AppTabBar query execution status", () => {
expect(tabBarSource.match(/<\/TabExecutionStatus>/g)).toHaveLength(4);
});
});
describe("AppTabBar collapsible groups", () => {
it("supports a persistent icon-only vertical tab bar without losing tab actions", () => {
expect(tabBarSource).toContain("tabBarCollapsed?: boolean;");
expect(tabBarSource).toContain('"toggle-collapse": [];');
expect(tabBarSource).toContain('return { width: "3.5rem", flex: "0 0 3.5rem" };');
expect(tabBarSource).toContain('<component :is="tabBarCollapseIcon"');
expect(tabBarSource).toContain("@click=\"emit('toggle-collapse')\"");
expect(tabBarSource).toContain('v-if="!isTabBarCollapsed" class="panel-resize-handle"');
expect(tabBarSource).toContain(".vertical-tab-layout--collapsed .app-tab-pill");
expect(tabBarSource).toContain(".vertical-tab-layout.vertical-tab-layout--collapsed .tab-group-tab::before");
expect(tabBarSource.match(/v-if="isTabBarCollapsed && isDirtyTab\(tab\)"/g)).toHaveLength(2);
expect(tabBarSource).toMatch(/\(collapsed\) => \{\s*if \(collapsed\) tabSearchQuery\.value = "";/);
});
it("keeps horizontal placements immune to the persisted vertical collapse state", () => {
expect(tabBarSource).toContain("const isTabBarCollapsed = computed(() => isVerticalLayout.value && !!props.tabBarCollapsed);");
expect(tabBarSource).not.toContain('v-else-if="!tabBarCollapsed"');
expect(tabBarSource).not.toContain('v-if="!tabBarCollapsed" class="min-w-0 truncate');
expect(tabBarSource).not.toContain('v-if="tabBarCollapsed && isDirtyTab');
// The only raw-prop guards left render inside the vertical-only toolbar.
expect(tabBarSource.match(/!tabBarCollapsed/g)).toHaveLength(3);
});
it("keeps only grouping and collapse in the compact vertical toolbar", () => {
expect(tabBarSource).toContain('const verticalTabToolbarButtonClass = "inline-flex h-7 w-7');
expect(tabBarSource.match(/:trigger-class="verticalTabToolbarButtonClass"/g)).toHaveLength(1);
const toolbar = sourceBetween('<div v-if="!tabBarCollapsed" class="flex shrink-0 items-center gap-0">', "</div>\n <button");
expect(toolbar).toContain(':trigger-icon="ListFilter"');
expect(toolbar).not.toContain(':trigger-icon="ArrowDownUp"');
expect(toolbar).not.toContain(':trigger-icon="PanelTop"');
expect(tabBarSource).toContain('class="flex shrink-0 items-center gap-0.5 border-b p-1.5"');
expect(tabBarSource).toContain('class="flex shrink-0 items-center gap-0"');
});
it("keeps separate collapsed state for regular and fixed groups", () => {
expect(tabBarSource).toContain("const collapsedTabGroups = ref<Set<string>>(new Set());");
expect(tabBarSource).toContain('return `${tab.pinned ? "fixed" : "regular"}:${tabGroupKey(tab)}`;');
expect(tabBarSource).toContain("function toggleTabGroup(tab: QueryTab)");
expect(tabBarSource.match(/:aria-expanded="!isTabGroupCollapsed\(tab\)"/g)).toHaveLength(2);
expect(tabBarSource.match(/<CustomContextMenu v-if="!isTabGroupCollapsed\(tab\)"/g)).toHaveLength(2);
});
it("temporarily reveals matches and expands a collapsed group when one of its tabs becomes active", () => {
expect(tabBarSource).toContain('if (settingsStore.editorSettings.tabGroupMode === "none" || tabSearchQuery.value.trim()) return false;');
expect(tabBarSource).toContain("expandTabGroupForTab(tabId);");
});
it("uses compact group pills and places the accent next to content for top and bottom bars", () => {
expect(tabBarSource).toContain(':data-placement="settingsStore.editorSettings.tabPlacement"');
expect(tabBarSource).toContain(".app-tab-bar:not(.vertical-tab-layout) .tab-group-header");
expect(tabBarSource).toContain(".app-tab-bar:not(.vertical-tab-layout) .tab-group-header::after");
expect(tabBarSource).toContain(".app-tab-bar:not(.vertical-tab-layout) .tab-group-header--collapsed::after");
expect(tabBarSource).toContain('.app-tab-bar:not(.vertical-tab-layout)[data-placement="bottom"] .tab-group-tab::after');
expect(tabBarSource).toContain(".app-tab-bar:not(.vertical-tab-layout) .tab-group-tab--last::after");
expect(tabBarSource).toContain(".vertical-tab-layout .tab-group-tab::before");
});
it("supports persistent names and colors from the group header context menu", () => {
expect(tabBarSource.match(/:items="\(\) => getTabGroupMenuItems\(tab\)"/g)).toHaveLength(2);
expect(tabBarSource).toContain('["#2563eb", "#d97706", "#7c3aed", "#059669", "#dc2626", "#0891b2", "#db2777", "#475569"]');
expect(tabBarSource).toContain("tabGroupCustomizations: customizations");
expect(tabBarSource).toContain("data-tab-group-name-input");
expect(tabBarSource).toContain('type="color"');
});
it("exposes placement, grouping, and sorting preferences from the group context menu", () => {
const preferences = sourceBetween("function getTabPreferenceMenuItems", "function getTabGroupMenuItems");
const groupMenu = sourceBetween("function getTabGroupMenuItems", "function openTabGroupContextMenu");
expect(preferences).toContain('label: t("settings.tabPlacement")');
expect(preferences).toContain("action: () => updateTabPlacement(item.value)");
expect(preferences).toContain('label: t("settings.tabGroup")');
expect(preferences).toContain("action: () => updateTabGroupMode(item.value)");
expect(preferences).toContain('label: t("settings.tabSort")');
expect(preferences).toContain("action: () => updateTabSortMode(item.value)");
expect(preferences.match(/checked: item\.value === settingsStore\.editorSettings\./g)).toHaveLength(3);
expect(groupMenu).toContain("...getTabPreferenceMenuItems()");
});
it("exposes placement, grouping, and sorting preferences from each tab context menu", () => {
const menu = sourceBetween("function getTabMenuItems", "function handleSaveAndClose");
expect(menu).toContain("...getTabPreferenceMenuItems()");
});
it("waits for tab preference persistence and treats the whole group title as the context target", () => {
expect(tabBarSource).toContain("await settingsStore.updateEditorSettingsAndPersist(partial)");
expect(tabBarSource.match(/openTabGroupContextMenu\(\$event, onContextMenu\)/g)).toHaveLength(2);
expect(tabBarSource).toContain("document.getSelection()?.removeAllRanges()");
expect(tabBarSource).toContain("user-select: none;");
});
it("removes the unexplained fixed-tab divider from vertical placement", () => {
expect(tabBarSource).toContain("isVerticalLayout ? 'max-h-[40%] flex-col bg-background pt-1'");
expect(tabBarSource).toContain('<Pin v-if="!isTabBarCollapsed" class="tab-group-pin" aria-hidden="true" />');
});
it("uses a soft active shadow and a single indented rail for sidebar groups", () => {
expect(tabBarSource).toContain("if (isVerticalLayout.value) {");
expect(tabBarSource).toContain('"--app-tab-background": "var(--accent)"');
expect(tabBarSource).toContain('.vertical-tab-layout .app-tab-pill[data-active-tab="true"]');
expect(tabBarSource).toContain("inset 0 0 0 1px color-mix");
expect(tabBarSource).toContain(".vertical-tab-layout .tab-group-header:not(.tab-group-header--collapsed)::after");
expect(tabBarSource).toContain(".vertical-tab-layout .tab-group-tab::after");
expect(tabBarSource).toContain("margin-inline: 2.5rem 0.5rem;");
expect(tabBarSource).toContain("padding-inline-start: 0.65rem !important;");
expect(tabBarSource).toContain("left: -0.65rem;");
expect(tabBarSource).toContain(".vertical-tab-layout .tab-group-tab--first::before");
expect(tabBarSource).toContain("bottom: -1.08rem;");
expect(tabBarSource).toContain("top: 50%;");
expect(tabBarSource).toContain("border-bottom-left-radius: 0.1875rem;");
});
});
@@ -79,4 +79,19 @@ describe("usePanelResize", () => {
expect(aiPanelWidth.value).toBe(600);
expect(localStorage.getItem("dbx-ai-panel-width")).toBe("600");
});
it("persists the collapsed vertical tab bar state across composable instances", () => {
const first = usePanelResize();
expect(first.tabBarCollapsed.value).toBe(false);
first.setTabBarCollapsed(true);
expect(first.tabBarCollapsed.value).toBe(true);
expect(localStorage.getItem("dbx-tab-bar-collapsed")).toBe("true");
const restored = usePanelResize();
expect(restored.tabBarCollapsed.value).toBe(true);
restored.setTabBarCollapsed(false);
expect(localStorage.getItem("dbx-tab-bar-collapsed")).toBe("false");
});
});
@@ -21,6 +21,8 @@ export function usePanelResize() {
const historyWidth = ref(Number(safeLocalStorageGet("dbx-history-width")) || 288);
const sqlLibraryWidth = ref(Number(safeLocalStorageGet("dbx-sql-library-width")) || 288);
const sqlFilePanelWidth = ref(Number(safeLocalStorageGet("dbx-sql-file-panel-width")) || 288);
const tabBarWidth = ref(Number(safeLocalStorageGet("dbx-tab-bar-width")) || 240);
const tabBarCollapsed = ref(safeLocalStorageGet("dbx-tab-bar-collapsed") === "true");
function startPanelResize(widthRef: Ref<number>, storageKey: string, direction: "left" | "right", maxWidth: PanelMaxWidth = DEFAULT_PANEL_MAX_WIDTH) {
return (e: MouseEvent) => {
@@ -55,6 +57,13 @@ export function usePanelResize() {
const startHistoryResize = startPanelResize(historyWidth, "dbx-history-width", "left");
const startSqlLibraryResize = startPanelResize(sqlLibraryWidth, "dbx-sql-library-width", "left");
const startSqlFilePanelResize = startPanelResize(sqlFilePanelWidth, "dbx-sql-file-panel-width", "left");
const startLeftTabBarResize = startPanelResize(tabBarWidth, "dbx-tab-bar-width", "right");
const startRightTabBarResize = startPanelResize(tabBarWidth, "dbx-tab-bar-width", "left");
function setTabBarCollapsed(collapsed: boolean) {
tabBarCollapsed.value = collapsed;
safeLocalStorageSet("dbx-tab-bar-collapsed", String(collapsed));
}
return {
sidebarWidth,
@@ -62,10 +71,15 @@ export function usePanelResize() {
historyWidth,
sqlLibraryWidth,
sqlFilePanelWidth,
tabBarWidth,
tabBarCollapsed,
startSidebarResize,
startAiPanelResize,
startHistoryResize,
startSqlLibraryResize,
startSqlFilePanelResize,
startLeftTabBarResize,
startRightTabBarResize,
setTabBarCollapsed,
};
}
+12
View File
@@ -1496,11 +1496,20 @@ export default {
maximizeWindow: "Maximize",
closeWindow: "Close",
searchOpenTabs: "Search open tabs...",
collapseTabBar: "Collapse tab bar",
expandTabBar: "Expand tab bar",
switcherTitle: "Switch Tab",
switcherHint: "keep pressing to move, release to switch",
noMatchingTabs: "No matching tabs",
fixedTabs: "Fixed tabs",
openDataTabs: "Open tables",
editGroupTitle: "Edit group: {name}",
groupName: "Group name",
groupColor: "Group color",
groupColorAuto: "Use default color",
groupColorCustom: "Choose a custom color",
resetGroup: "Reset",
settingsSaveFailed: "Could not save tab settings: {message}",
},
databaseBrowser: {
search: "Search databases...",
@@ -3035,6 +3044,9 @@ export default {
compactTabTitle: "Shorten Tab Titles",
fullTabTitle: "Show Full Tab Titles",
closeTab: "Close Tab",
editTabGroup: "Edit Tab Group",
resetTabGroup: "Reset Tab Group",
closeTabGroup: "Close Tab Group",
closeFixedTab: "Close Fixed Tab",
closeLeftTabs: "Close Tabs to the Left",
closeRightTabs: "Close Tabs to the Right",
+12
View File
@@ -1463,11 +1463,20 @@ export default withEnglishFallback({
scrollRight: "Desplazar pestañas a la derecha",
openTabs: "Pestañas abiertas",
searchOpenTabs: "Buscar pestañas abiertas...",
collapseTabBar: "Contraer barra de pestañas",
expandTabBar: "Expandir barra de pestañas",
switcherTitle: "Cambiar de pestaña",
switcherHint: "sigue pulsando para moverte, suelta para cambiar",
noMatchingTabs: "No se encontraron pestañas",
fixedTabs: "Pestañas fijadas",
openDataTabs: "Tablas abiertas",
editGroupTitle: "Editar grupo: {name}",
groupName: "Nombre del grupo",
groupColor: "Color del grupo",
groupColorAuto: "Usar color predeterminado",
groupColorCustom: "Elegir color personalizado",
resetGroup: "Restablecer",
settingsSaveFailed: "No se pudo guardar la configuración de pestañas: {message}",
gridfs: "GridFS",
etcdDashboard: "Panel de etcd",
etcdAccessControl: "Control de acceso de etcd",
@@ -2952,6 +2961,9 @@ export default withEnglishFallback({
compactTabTitle: "Acortar títulos de pestañas",
fullTabTitle: "Mostrar títulos completos de pestañas",
closeTab: "Cerrar pestaña",
editTabGroup: "Editar grupo de pestañas",
resetTabGroup: "Restablecer grupo de pestañas",
closeTabGroup: "Cerrar grupo de pestañas",
closeFixedTab: "Cerrar pestaña fijada",
closeOtherTabs: "Cerrar otras pestañas",
closeOtherRegularTabs: "Cerrar otras pestañas normales",
+12
View File
@@ -1461,11 +1461,20 @@ export default withEnglishFallback({
scrollRight: "Scorri schede a destra",
openTabs: "Schede aperte",
searchOpenTabs: "Cerca schede aperte...",
collapseTabBar: "Comprimi barra delle schede",
expandTabBar: "Espandi barra delle schede",
switcherTitle: "Cambia scheda",
switcherHint: "continua a premere per spostarti, rilascia per cambiare",
noMatchingTabs: "Nessuna scheda corrispondente",
fixedTabs: "Schede bloccate",
openDataTabs: "Tabelle aperte",
editGroupTitle: "Modifica gruppo: {name}",
groupName: "Nome gruppo",
groupColor: "Colore gruppo",
groupColorAuto: "Usa colore predefinito",
groupColorCustom: "Scegli colore personalizzato",
resetGroup: "Ripristina",
settingsSaveFailed: "Impossibile salvare le impostazioni delle schede: {message}",
gridfs: "GridFS",
etcdDashboard: "etcd dashboard",
etcdAccessControl: "controllo accessi etcd",
@@ -2950,6 +2959,9 @@ export default withEnglishFallback({
compactTabTitle: "Accorcia Titoli Schede",
fullTabTitle: "Mostra titoli completi delle schede",
closeTab: "Chiudi Scheda",
editTabGroup: "Modifica gruppo di schede",
resetTabGroup: "Ripristina gruppo di schede",
closeTabGroup: "Chiudi Gruppo di Schede",
closeFixedTab: "Chiudi Scheda Fissa",
closeOtherTabs: "Chiudi Altre Schede",
closeOtherRegularTabs: "Chiudi Altre Schede Normali",
+12
View File
@@ -1480,11 +1480,20 @@ export default withEnglishFallback({
scrollRight: "タブを右にスクロール",
openTabs: "開いているタブ",
searchOpenTabs: "開いているタブを検索...",
collapseTabBar: "タブバーを折りたたむ",
expandTabBar: "タブバーを展開",
switcherTitle: "タブを切り替え",
switcherHint: "押し続けて移動、離して切り替え",
noMatchingTabs: "一致するタブがありません",
fixedTabs: "固定タブ",
openDataTabs: "開いているテーブル",
editGroupTitle: "グループを編集: {name}",
groupName: "グループ名",
groupColor: "グループの色",
groupColorAuto: "既定の色を使用",
groupColorCustom: "カスタムカラーを選択",
resetGroup: "リセット",
settingsSaveFailed: "タブ設定を保存できません: {message}",
zookeeper: "ZooKeeper",
consul: "Consul",
vector: "ベクター",
@@ -2974,6 +2983,9 @@ export default withEnglishFallback({
compactTabTitle: "タブタイトルを短縮",
fullTabTitle: "タブの完全なタイトルを表示",
closeTab: "タブを閉じる",
editTabGroup: "タブグループを編集",
resetTabGroup: "タブグループをリセット",
closeTabGroup: "このグループのタブを閉じる",
closeFixedTab: "固定タブを閉じる",
closeOtherTabs: "他のタブを閉じる",
closeOtherRegularTabs: "他の通常タブを閉じる",
+12
View File
@@ -1367,11 +1367,20 @@ export default withEnglishFallback({
scrollRight: "탭을 오른쪽으로 스크롤",
openTabs: "열린 탭",
searchOpenTabs: "열린 탭 검색...",
collapseTabBar: "탭 표시줄 접기",
expandTabBar: "탭 표시줄 펼치기",
switcherTitle: "탭 전환",
switcherHint: "계속 눌러 이동하고 놓으면 전환",
noMatchingTabs: "일치하는 탭이 없습니다",
fixedTabs: "고정 탭",
openDataTabs: "열린 테이블",
editGroupTitle: "그룹 편집: {name}",
groupName: "그룹 이름",
groupColor: "그룹 색상",
groupColorAuto: "기본 색상 사용",
groupColorCustom: "사용자 지정 색상 선택",
resetGroup: "초기화",
settingsSaveFailed: "탭 설정을 저장하지 못했습니다: {message}",
},
databaseBrowser: {
search: "데이터베이스 검색...",
@@ -2844,6 +2853,9 @@ export default withEnglishFallback({
compactTabTitle: "탭 제목 줄이기",
fullTabTitle: "전체 탭 제목 표시",
closeTab: "탭 닫기",
editTabGroup: "탭 그룹 편집",
resetTabGroup: "탭 그룹 초기화",
closeTabGroup: "이 그룹의 탭 닫기",
closeFixedTab: "고정 탭 닫기",
closeLeftTabs: "왼쪽 탭 닫기",
closeRightTabs: "오른쪽 탭 닫기",
+12
View File
@@ -1463,11 +1463,20 @@ export default withEnglishFallback({
scrollRight: "Rolar abas para a direita",
openTabs: "Abas abertas",
searchOpenTabs: "Pesquisar abas abertas...",
collapseTabBar: "Recolher barra de abas",
expandTabBar: "Expandir barra de abas",
switcherTitle: "Alternar aba",
switcherHint: "continue pressionando para mover, solte para alternar",
noMatchingTabs: "Nenhuma aba correspondente",
fixedTabs: "Abas fixadas",
openDataTabs: "Tabelas abertas",
editGroupTitle: "Editar grupo: {name}",
groupName: "Nome do grupo",
groupColor: "Cor do grupo",
groupColorAuto: "Usar cor padrão",
groupColorCustom: "Escolher cor personalizada",
resetGroup: "Redefinir",
settingsSaveFailed: "Falha ao salvar configurações de abas: {message}",
gridfs: "GridFS",
etcdDashboard: "painel etcd",
etcdAccessControl: "controle de acesso do etcd",
@@ -2952,6 +2961,9 @@ export default withEnglishFallback({
compactTabTitle: "Encurtar Títulos das Abas",
fullTabTitle: "Mostrar títulos completos das abas",
closeTab: "Fechar Aba",
editTabGroup: "Editar grupo de abas",
resetTabGroup: "Redefinir grupo de abas",
closeTabGroup: "Fechar Grupo de Abas",
closeFixedTab: "Fechar Aba Fixa",
closeOtherTabs: "Fechar Outras Abas",
closeOtherRegularTabs: "Fechar Outras Abas Normais",
+13 -1
View File
@@ -1419,12 +1419,21 @@ export default withEnglishFallback({
minimizeWindow: "最小化",
maximizeWindow: "最大化",
closeWindow: "关闭",
searchOpenTabs: "搜索已打开的标签页...",
searchOpenTabs: "搜索...",
collapseTabBar: "收起标签栏",
expandTabBar: "展开标签栏",
switcherTitle: "切换标签页",
switcherHint: "继续按选择,松开修饰键切换",
noMatchingTabs: "未找到匹配的标签页",
fixedTabs: "固定标签页",
openDataTabs: "已打开的数据表",
editGroupTitle: "编辑分组:{name}",
groupName: "分组名称",
groupColor: "分组颜色",
groupColorAuto: "使用默认颜色",
groupColorCustom: "选择自定义颜色",
resetGroup: "恢复默认",
settingsSaveFailed: "无法保存标签设置:{message}",
},
databaseBrowser: {
search: "搜索数据库...",
@@ -2958,6 +2967,9 @@ export default withEnglishFallback({
compactTabTitle: "缩短标签标题",
fullTabTitle: "完整标签标题",
closeTab: "关闭标签页",
editTabGroup: "编辑标签分组",
resetTabGroup: "恢复默认分组",
closeTabGroup: "关闭本组标签页",
closeFixedTab: "关闭固定标签页",
closeLeftTabs: "关闭左侧标签页",
closeRightTabs: "关闭右侧标签页",
+12
View File
@@ -1462,11 +1462,20 @@ export default withEnglishFallback({
scrollRight: "向右捲動分頁",
openTabs: "已開啟的分頁",
searchOpenTabs: "搜尋已開啟的分頁...",
collapseTabBar: "收合分頁列",
expandTabBar: "展開分頁列",
switcherTitle: "切換分頁",
switcherHint: "繼續按選擇,放開修飾鍵切換",
noMatchingTabs: "找不到符合的分頁",
fixedTabs: "固定分頁",
openDataTabs: "已開啟的資料表",
editGroupTitle: "編輯群組:{name}",
groupName: "群組名稱",
groupColor: "群組顏色",
groupColorAuto: "使用預設顏色",
groupColorCustom: "選擇自訂顏色",
resetGroup: "重設",
settingsSaveFailed: "無法儲存分頁設定:{message}",
gridfs: "GridFS",
etcdDashboard: "etcd 儀表板",
etcdAccessControl: "etcd 存取控制",
@@ -2949,6 +2958,9 @@ export default withEnglishFallback({
compactTabTitle: "縮短標籤標題",
fullTabTitle: "完整分頁標題",
closeTab: "關閉分頁",
editTabGroup: "編輯分頁群組",
resetTabGroup: "重設分頁群組",
closeTabGroup: "關閉本群組分頁",
closeFixedTab: "關閉固定分頁",
closeOtherTabs: "關閉其他分頁",
closeOtherRegularTabs: "關閉其他普通分頁",
+49
View File
@@ -38,6 +38,21 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ dataGridFilterEditorView: "invalid" } as any).dataGridFilterEditorView).toBe("quick");
});
it("normalizes persisted tab group names and colors", () => {
expect(normalizeEditorSettings({}).tabGroupCustomizations).toEqual({});
expect(
normalizeEditorSettings({
tabGroupCustomizations: {
"connection:local": { name: " Local services ", color: "#E11D48" },
"database-type:mysql": { name: "", color: "invalid" },
broken: "value",
},
} as any).tabGroupCustomizations,
).toEqual({
"connection:local": { name: "Local services", color: "#e11d48" },
});
});
it("defaults and bounds the persisted text filter panel height", () => {
expect(normalizeEditorSettings({}).dataGridTextFilterPanelHeight).toBe(168);
expect(normalizeEditorSettings({ dataGridTextFilterPanelHeight: 236.4 }).dataGridTextFilterPanelHeight).toBe(236);
@@ -894,6 +909,40 @@ describe("settingsStore persisted settings initialization", () => {
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ openDataTabsNextToActive: false }));
});
it("persists tab placement, grouping, sorting, and group customizations across restart", async () => {
let persistedSettings: Record<string, unknown> = {};
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
const saveEditorSettings = vi.fn(async (settings: Record<string, unknown>) => {
persistedSettings = JSON.parse(JSON.stringify(settings));
});
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
const { useSettingsStore } = await import("@/stores/settingsStore");
const firstStore = useSettingsStore();
await firstStore.initEditorSettings();
await firstStore.updateEditorSettingsAndPersist({
tabPlacement: "left",
tabGroupMode: "connection",
tabSortMode: "title-asc",
tabGroupCustomizations: {
"connection:local": { name: "Local services", color: "#2563eb" },
},
});
setActivePinia(createPinia());
const restartedStore = useSettingsStore();
await restartedStore.initEditorSettings();
expect(restartedStore.editorSettings).toMatchObject({
tabPlacement: "left",
tabGroupMode: "connection",
tabSortMode: "title-asc",
tabGroupCustomizations: {
"connection:local": { name: "Local services", color: "#2563eb" },
},
});
});
it("loads, persists, and reloads the cell detail button visibility", async () => {
let persistedSettings: Record<string, unknown> = { dataGridCellDetailButtonVisible: false };
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
+22
View File
@@ -576,6 +576,10 @@ const TAB_PLACEMENTS = ["top", "bottom", "left", "right"] as const;
export type TabPlacement = (typeof TAB_PLACEMENTS)[number];
const TAB_GROUP_MODES = ["none", "database-type", "connection"] as const;
export type TabGroupMode = (typeof TAB_GROUP_MODES)[number];
export interface TabGroupCustomization {
name?: string;
color?: string;
}
const TAB_SORT_MODES = ["manual", "created-asc", "title-asc"] as const;
export type TabSortMode = (typeof TAB_SORT_MODES)[number];
const DATA_GRID_RENDER_MODES = ["dom", "canvas"] as const;
@@ -711,6 +715,7 @@ export interface EditorSettings {
tabLayout: TabLayoutMode;
tabPlacement: TabPlacement;
tabGroupMode: TabGroupMode;
tabGroupCustomizations: Record<string, TabGroupCustomization>;
tabSortMode: TabSortMode;
appLayout: "separated" | "classic";
pageSize: number;
@@ -938,6 +943,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
tabLayout: "scroll",
tabPlacement: "top",
tabGroupMode: "none",
tabGroupCustomizations: {},
tabSortMode: "manual",
appLayout: "classic",
pageSize: 100,
@@ -1090,6 +1096,20 @@ function normalizeTabGroupMode(value: unknown): TabGroupMode {
return TAB_GROUP_MODES.includes(value as TabGroupMode) ? (value as TabGroupMode) : DEFAULT_EDITOR_SETTINGS.tabGroupMode;
}
export function normalizeTabGroupCustomizations(value: unknown): Record<string, TabGroupCustomization> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const customizations: Record<string, TabGroupCustomization> = {};
for (const [rawKey, rawCustomization] of Object.entries(value as Record<string, unknown>).slice(0, 200)) {
const key = rawKey.trim().slice(0, 512);
if (!key || !rawCustomization || typeof rawCustomization !== "object" || Array.isArray(rawCustomization)) continue;
const customization = rawCustomization as Record<string, unknown>;
const name = typeof customization.name === "string" ? customization.name.trim().slice(0, 80) : "";
const color = typeof customization.color === "string" && /^#[0-9a-f]{6}$/i.test(customization.color.trim()) ? customization.color.trim().toLowerCase() : "";
if (name || color) customizations[key] = { ...(name ? { name } : {}), ...(color ? { color } : {}) };
}
return customizations;
}
function normalizeTabSortMode(value: unknown): TabSortMode {
return TAB_SORT_MODES.includes(value as TabSortMode) ? (value as TabSortMode) : DEFAULT_EDITOR_SETTINGS.tabSortMode;
}
@@ -1366,6 +1386,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
tabLayout: normalizeTabLayout(settings.tabLayout),
tabPlacement: normalizeTabPlacement(settings.tabPlacement),
tabGroupMode: normalizeTabGroupMode(settings.tabGroupMode),
tabGroupCustomizations: normalizeTabGroupCustomizations(settings.tabGroupCustomizations),
tabSortMode: normalizeTabSortMode(settings.tabSortMode),
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
pageSize: normalizeResultPageSize(settings.pageSize),
@@ -2069,6 +2090,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.tabLayout !== undefined) editorSettings.value.tabLayout = normalizeTabLayout(partial.tabLayout);
if (partial.tabPlacement !== undefined) editorSettings.value.tabPlacement = normalizeTabPlacement(partial.tabPlacement);
if (partial.tabGroupMode !== undefined) editorSettings.value.tabGroupMode = normalizeTabGroupMode(partial.tabGroupMode);
if (partial.tabGroupCustomizations !== undefined) editorSettings.value.tabGroupCustomizations = normalizeTabGroupCustomizations(partial.tabGroupCustomizations);
if (partial.tabSortMode !== undefined) editorSettings.value.tabSortMode = normalizeTabSortMode(partial.tabSortMode);
if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout;
if (partial.pageSize !== undefined) editorSettings.value.pageSize = normalizeResultPageSize(partial.pageSize);