From 916aad66bfa5fad6cdaec8b1ecc9246d05f83337 Mon Sep 17 00:00:00 2001
From: gggaiitx <62124152+gggaiitx@users.noreply.github.com>
Date: Fri, 17 Jul 2026 00:11:47 +0800
Subject: [PATCH] feat(tabs): add multi-row tab layout option
---
.../editor/EditorSettingsDialog.vue | 28 +++++++
.../src/components/layout/AppTabBar.vue | 84 ++++++++++++++++---
apps/desktop/src/i18n/locales/en.ts | 5 ++
apps/desktop/src/i18n/locales/es.ts | 5 ++
apps/desktop/src/i18n/locales/it.ts | 5 ++
apps/desktop/src/i18n/locales/ja.ts | 5 ++
apps/desktop/src/i18n/locales/pt-BR.ts | 5 ++
apps/desktop/src/i18n/locales/zh-CN.ts | 5 ++
apps/desktop/src/i18n/locales/zh-TW.ts | 5 ++
.../__tests__/editorSettingsDraft.spec.ts | 50 +++++++++++
.../src/lib/settings/editorSettingsDraft.ts | 1 +
.../stores/__tests__/settingsStore.spec.ts | 21 +++++
apps/desktop/src/stores/settingsStore.ts | 10 +++
13 files changed, 216 insertions(+), 13 deletions(-)
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
index 603912c5b..bfc235473 100644
--- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue
+++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
@@ -271,6 +271,7 @@ const editConfirmDangerousSqlExecution = ref(settingsStore.editorSettings.confir
const editContinueOnErrorOnBatch = ref(settingsStore.editorSettings.continueOnErrorOnBatch);
const editConfirmUnsavedSqlClose = ref(settingsStore.editorSettings.confirmUnsavedSqlClose);
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
+const editTabLayout = ref(settingsStore.editorSettings.tabLayout);
const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon);
const editQuitOnClose = ref(settingsStore.desktopSettings.quit_on_close);
const desktopCloseBehaviorResetPending = ref(false);
@@ -409,6 +410,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
continueOnErrorOnBatch: editContinueOnErrorOnBatch.value,
confirmUnsavedSqlClose: editConfirmUnsavedSqlClose.value,
appLayout: editAppLayout.value,
+ tabLayout: editTabLayout.value,
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
@@ -688,6 +690,7 @@ function syncEditorSettingsDraftFromStore() {
editContinueOnErrorOnBatch.value = settingsStore.editorSettings.continueOnErrorOnBatch;
editConfirmUnsavedSqlClose.value = settingsStore.editorSettings.confirmUnsavedSqlClose;
editAppLayout.value = settingsStore.editorSettings.appLayout;
+ editTabLayout.value = settingsStore.editorSettings.tabLayout;
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
@@ -889,6 +892,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editCustomThemes.value = [...DEFAULT_EDITOR_SETTINGS.customThemes];
editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId;
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
+ editTabLayout.value = DEFAULT_EDITOR_SETTINGS.tabLayout;
editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon;
editQuitOnClose.value = DEFAULT_DESKTOP_SETTINGS.quit_on_close;
desktopCloseBehaviorResetPending.value = true;
@@ -1260,6 +1264,10 @@ function setAppLayout(value: InterfaceLayout) {
editAppLayout.value = value;
}
+function setTabLayout(value: "scroll" | "wrap") {
+ editTabLayout.value = value;
+}
+
function setSidebarActivation(value: "single" | "double") {
editSidebarActivation.value = value;
}
@@ -3234,6 +3242,26 @@ onUnmounted(cleanupPreviewEditor);
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue
index 4eab8c9ce..f29bd6ccb 100644
--- a/apps/desktop/src/components/layout/AppTabBar.vue
+++ b/apps/desktop/src/components/layout/AppTabBar.vue
@@ -52,6 +52,7 @@ const tabDrag = useTabDrag((draggedId, targetId, position) => {
const editingTabId = ref
(null);
const editingTitle = ref("");
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
+const isWrapLayout = computed(() => settingsStore.editorSettings.tabLayout === "wrap");
const fixedTabs = computed(() => queryStore.tabs.filter((tab) => tab.pinned));
const regularTabs = computed(() => queryStore.tabs.filter((tab) => !tab.pinned));
const hasFixedTabs = computed(() => fixedTabs.value.length > 0);
@@ -342,12 +343,14 @@ watch(
() => queryStore.activeTabId,
() => {
nextTick(() => {
- for (const container of [tabsContainerRef.value, fixedTabsContainerRef.value]) {
- if (!container) continue;
- const activeEl = container.querySelector('[data-active-tab="true"]');
- if (activeEl) {
- activeEl.scrollIntoView({ behavior: tabScrollBehavior.value, block: "nearest", inline: activeTabScrollInline(container, queryStore.activeTabId) });
- break;
+ if (!isWrapLayout.value) {
+ for (const container of [tabsContainerRef.value, fixedTabsContainerRef.value]) {
+ if (!container) continue;
+ const activeEl = container.querySelector('[data-active-tab="true"]');
+ if (activeEl) {
+ activeEl.scrollIntoView({ behavior: tabScrollBehavior.value, block: "nearest", inline: activeTabScrollInline(container, queryStore.activeTabId) });
+ break;
+ }
}
}
updateAllScrollButtons();
@@ -361,6 +364,7 @@ watch(
(show) => {
if (!show) return;
nextTick(() => {
+ if (isWrapLayout.value) return;
const container = tabsContainerRef.value;
if (!container) return;
const el = container.querySelector("[data-driver-store-tab]");
@@ -377,6 +381,7 @@ watch(
(show) => {
if (!show) return;
nextTick(() => {
+ if (isWrapLayout.value) return;
const container = tabsContainerRef.value;
if (!container) return;
const el = container.querySelector("[data-settings-page-tab]");
@@ -434,9 +439,9 @@ function tabDatabaseIconType(tab: QueryTab) {
return connection.driver_profile || connection.db_type;
}
-const showRegularTabScrollbar = computed(() => hasTabOverflow.value);
-const showFixedTabScrollbar = computed(() => hasFixedTabOverflow.value);
-const showRegularTabOverflowControls = computed(() => regularTabs.value.length > 0 && hasTabOverflow.value);
+const showRegularTabScrollbar = computed(() => hasTabOverflow.value && !isWrapLayout.value);
+const showFixedTabScrollbar = computed(() => hasFixedTabOverflow.value && !isWrapLayout.value);
+const showRegularTabOverflowControls = computed(() => regularTabs.value.length > 0 && hasTabOverflow.value && !isWrapLayout.value);
const regularTabOverflowOpen = ref(false);
const fixedTabOverflowOpen = ref(false);
const tabBarClass = computed(() => [isClassicLayout.value ? "bg-muted" : "border-b bg-background", hasFixedTabs.value ? "flex-col" : "", isClassicLayout.value && hasFixedTabs.value ? "border-b" : ""]);
@@ -504,8 +509,8 @@ const fixedTabScrollbarThumbStyle = computed(() => ({
width: `${fixedScrollThumbWidthPercent.value}%`,
}));
-const tabTailDragRegionClass = computed(() => (showRegularTabOverflowControls.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
-const fixedTabTailDragRegionClass = computed(() => (showFixedTabScrollbar.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
+const tabTailDragRegionClass = computed(() => (showRegularTabOverflowControls.value || isWrapLayout.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
+const fixedTabTailDragRegionClass = computed(() => (showFixedTabScrollbar.value || isWrapLayout.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
const tabOverflowControlClass = computed(() =>
isClassicLayout.value
@@ -551,7 +556,14 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
-