fix(layout): fix plugin tab layout with vertical tab bar

Closes #9101
This commit is contained in:
t8y2
2026-09-15 12:27:03 +08:00
parent faf259fcc2
commit 36b96ea32c
3 changed files with 22 additions and 16 deletions
+7 -2
View File
@@ -1007,6 +1007,7 @@ useScheduledDatabaseBackups({ scheduler: true });
const appVersion = ref("");
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
const isVerticalTabPlacement = computed(() => settingsStore.editorSettings.tabPlacement === "left" || settingsStore.editorSettings.tabPlacement === "right");
// Every pane's vertical strip writes back to this shared width/collapse state.
function startTabBarResize(event: PointerEvent) {
@@ -3748,8 +3749,12 @@ onUnmounted(() => {
</DialogFooter>
</DialogContent>
</Dialog>
<div v-show="!driverStoreActive && !pluginCenterActive && !settingsStore.settingsPageActive" class="flex min-h-0 min-w-0 flex-1 flex-col">
<div class="flex flex-col min-h-0" :class="activeTab?.mode === 'plugin-workbench' ? 'flex-none' : 'flex-1'">
<div
v-show="!driverStoreActive && !pluginCenterActive && !settingsStore.settingsPageActive"
class="flex min-h-0 min-w-0 flex-1"
:class="activeTab?.mode === 'plugin-workbench' && isVerticalTabPlacement ? (settingsStore.editorSettings.tabPlacement === 'right' ? 'flex-row-reverse' : 'flex-row') : 'flex-col'"
>
<div class="flex min-h-0" :class="activeTab?.mode === 'plugin-workbench' ? (isVerticalTabPlacement ? 'h-full flex-none flex-col' : 'flex-none flex-col') : 'flex-1 flex-col'">
<SqlEditorWorkspace
ref="contentAreaRef"
:content-suppressed="activeTab?.mode === 'plugin-workbench'"
@@ -114,6 +114,7 @@ const { t } = useI18n();
const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
const isVerticalTabLayout = computed(() => settingsStore.editorSettings.tabPlacement === "left" || settingsStore.editorSettings.tabPlacement === "right");
const verticalTabsWithSuppressedContent = computed(() => props.contentSuppressed && isVerticalTabLayout.value);
const globalTabBarPortal = inject(GROUP_TAB_BAR_PORTAL, null);
const workspaceTabBarPortal = createGroupTabBarPortal(isVerticalTabLayout);
// A special page owns navigation while active. Otherwise side tabs stay
@@ -257,16 +258,14 @@ function handleFocusStatement(tabId: string, range: StatementRange | null): bool
</script>
<template>
<!-- contentSuppressed (plugin workbench tab active): the App.vue wrapper is
flex-none with indefinite height, so h-full/flex-1 here would collapse
the workspace to 0 and clip the group tab strips (overflow-hidden).
Size to content instead — same contract as EditorGroup's suppressed
h-auto; normal mode keeps h-full/flex-1 to fill the column. -->
<div class="sql-editor-workspace relative flex min-h-0 min-w-0 overflow-hidden" :class="[contentSuppressed ? 'h-auto' : 'h-full flex-1', workspaceClass, settingsStore.editorSettings.tabPlacement === 'right' ? 'flex-row-reverse' : 'flex-row']">
<div
class="sql-editor-workspace relative flex min-h-0 min-w-0 overflow-hidden"
:class="[verticalTabsWithSuppressedContent ? 'h-full flex-none' : contentSuppressed ? 'h-auto' : 'h-full flex-1', workspaceClass, settingsStore.editorSettings.tabPlacement === 'right' ? 'flex-row-reverse' : 'flex-row']"
>
<div v-show="isVerticalTabLayout && showTabNavigation !== false" data-workspace-tab-navigation class="flex min-h-0 shrink-0 flex-col overflow-hidden" :style="tabNavigationStyle">
<div v-for="group in queryStore.groups" :key="group.id" :ref="(element) => setTabBarTarget(group.id, element)" :data-workspace-tab-target="group.id" class="flex min-h-0 min-w-0 flex-1" @pointerdown.capture="queryStore.focusGroup(group.id)" @focusin="queryStore.focusGroup(group.id)" />
</div>
<div data-workspace-content class="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div data-workspace-content class="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden" :class="{ hidden: verticalTabsWithSuppressedContent }">
<!-- Suppressed mode: a plugin workbench tab owns the layout. Render the
groups' tab strips directly (no Splitpanes, no shared result pane) so
App.vue's always-mounted plugin layer can fill the remaining column
@@ -87,6 +87,7 @@ vi.mock("@/components/layout/QueryResultSurface.vue", () => ({
import SqlEditorWorkspace from "../SqlEditorWorkspace.vue";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import type { QueryTab } from "@/types/database";
function tab(id: string): QueryTab {
@@ -338,6 +339,7 @@ describe("SqlEditorWorkspace mount contract", () => {
it("keeps group tab strips mounted and drops the splitpanes workspace while suppressed", async () => {
const store = useQueryStore();
const settingsStore = useSettingsStore();
store.tabs = [tab("tab-a"), { ...tab("tab-b"), mode: "plugin-workbench" } as unknown as ReturnType<typeof tab>];
store.activeTabId = "tab-b";
store.groups = [
@@ -347,6 +349,7 @@ describe("SqlEditorWorkspace mount contract", () => {
store.focusedGroupId = "g2";
store.orientation = "vertical";
store.sizes = [50, 50];
settingsStore.editorSettings.tabPlacement = "left";
const host = createHost();
const app = createApp(SqlEditorWorkspace, {
@@ -376,14 +379,13 @@ describe("SqlEditorWorkspace mount contract", () => {
expect(groups[1]?.getAttribute("data-group-id")).toBe("g2");
expect(groups[1]?.getAttribute("data-content-suppressed")).toBe("true");
// The App.vue wrapper is flex-none with indefinite height while a plugin
// tab is active: the workspace must size to its strip content, or the
// h-full/flex-1 fill contract collapses it to zero height and clips the
// strips away (overflow-hidden) — the "plugin tab bar vanishes" regression.
// Vertical plugin tabs share the row with the plugin workbench, so their
// workspace must keep the full-height contract while content is suppressed.
const workspaceRoot = host.querySelector<HTMLElement>(".sql-editor-workspace");
expect(workspaceRoot?.classList.contains("h-auto")).toBe(true);
expect(workspaceRoot?.classList.contains("h-full")).toBe(false);
expect(workspaceRoot?.classList.contains("flex-1")).toBe(false);
expect(workspaceRoot?.classList.contains("h-full")).toBe(true);
expect(workspaceRoot?.classList.contains("flex-none")).toBe(true);
expect(workspaceRoot?.classList.contains("h-auto")).toBe(false);
expect(host.querySelector<HTMLElement>("[data-workspace-content]")?.classList.contains("hidden")).toBe(true);
app.unmount();
host.remove();