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

Closes #9101
This commit is contained in:
t8y2 committed 2026-09-15 12:27:03 +08:00
1 parent faf259fcc2
commit 36b96ea32c
3 files changed
+22 -16

No files matched your search

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