fix(layout): merge special page tabs into the editor tab strip
Settings and driver store no longer sit on a strip of their own: while open but inactive they append to the focused group's tab strip (v0.6.2 order), and while active the app-level strip re-renders the open tabs with mode icons and inactive styling as the mouse path back to the editors. Special tabs follow the classic/separated presentation, including the classic active underline.
This commit is contained in:
11 files changed
+266
-100
No files matched your search
@@ -781,6 +781,15 @@ function requestActiveEditorExecuteInNewResultTab() {
|
||||
// Per-group editor toolbars call back into this App-owned orchestration. The
|
||||
// group focuses itself on pointerdown/focusin before any toolbar event, so the
|
||||
// acting tab is passed explicitly instead of relying on the focused group.
|
||||
// Declared above the provide: the object references the variable itself, while
|
||||
// the lazy getter safely reads later-declared update-count refs at render time.
|
||||
const specialPageTabs = computed(() => ({
|
||||
settingsOpen: settingsPageTabOpen.value,
|
||||
settingsActive: settingsStore.settingsPageActive,
|
||||
driverStoreOpen: driverStoreTabOpen.value,
|
||||
driverStoreActive: driverStoreActive.value,
|
||||
driverUpdateCount: toolbarAgentDriverUpdateCount.value,
|
||||
}));
|
||||
provide(EDITOR_TOOLBAR_ACTIONS, {
|
||||
explainMode,
|
||||
blockDangerousRedisCommands,
|
||||
@@ -804,6 +813,11 @@ provide(EDITOR_TOOLBAR_ACTIONS, {
|
||||
changeSchema: changeActiveSchema,
|
||||
setDefaultDatabase: setActiveDatabaseAsDefault,
|
||||
clearDefaultDatabase: clearActiveDefaultDatabase,
|
||||
specialPageTabs,
|
||||
activateSettingsPage,
|
||||
closeSettingsPage,
|
||||
activateDriverStore: () => openDriverStorePage(),
|
||||
closeDriverStore: closeDriverStorePage,
|
||||
});
|
||||
|
||||
// Upstream "preview changes" entry: dormant until the group toolbar wires the
|
||||
@@ -3445,6 +3459,7 @@ onUnmounted(() => {
|
||||
:can-detach-tabs="isDesktop"
|
||||
@activate-driver-store="openDriverStorePage"
|
||||
@activate-settings-page="activateSettingsPage"
|
||||
@activate-tab="activateQueryTab"
|
||||
@close-driver-store="closeDriverStorePage"
|
||||
@close-settings-page="closeSettingsPage"
|
||||
@save-tab="handleSaveTab"
|
||||
@@ -3495,12 +3510,9 @@ onUnmounted(() => {
|
||||
@update:open="(open: boolean) => (open ? activateSettingsPage() : closeSettingsPage())"
|
||||
@check-updates="checkUpdates()"
|
||||
/>
|
||||
<!-- Keep the workspace mounted with its group tab strips visible while a special page is active:
|
||||
the strips are the mouse path back to the editors (clicking a tab deactivates the page). -->
|
||||
<div v-if="queryStore.tabs.length > 0" class="flex flex-col flex-1 min-h-0">
|
||||
<div v-if="queryStore.tabs.length > 0" v-show="!driverStoreActive && !settingsStore.settingsPageActive" class="flex flex-col flex-1 min-h-0">
|
||||
<SqlEditorWorkspace
|
||||
ref="contentAreaRef"
|
||||
:special-surface-active="driverStoreActive || settingsStore.settingsPageActive"
|
||||
@locate-tab="locateTabInSidebar"
|
||||
@toggle-zen-mode="toggleZenMode"
|
||||
@start-resize="startTabBarResize"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref, watch } from "vue";
|
||||
import { computed, onUnmounted, ref, watch, type CSSProperties } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { X, Minimize2, Maximize2, Settings, Package, AlertTriangle } from "@lucide/vue";
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
@@ -8,7 +8,8 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { tabDisplayTitle } from "@/lib/tabs/tabPresentation";
|
||||
import TabModeIcon from "./TabModeIcon.vue";
|
||||
import { tabColorStyle, tabDisplayTitle, tabIconClass } from "@/lib/tabs/tabPresentation";
|
||||
import "./appTabBar.css";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -26,6 +27,7 @@ const emit = defineEmits<{
|
||||
"close-driver-store": [];
|
||||
"activate-settings-page": [];
|
||||
"close-settings-page": [];
|
||||
"activate-tab": [tabId: string];
|
||||
"save-tab": [tabId: string];
|
||||
"discard-tab-close": [];
|
||||
"save-all-tab-close": [];
|
||||
@@ -114,13 +116,26 @@ const specialStripRowClass = computed(() => (isClassicLayout.value ? "h-9 items-
|
||||
const specialStripScrollClass = computed(() => (isClassicLayout.value ? "h-full items-center overflow-x-auto" : "h-full items-center gap-1.5 overflow-x-auto py-1.5"));
|
||||
const specialStripBarClass = computed(() => (isClassicLayout.value ? "bg-muted" : `bg-background ${settingsStore.editorSettings.tabPlacement === "bottom" ? "border-t" : "border-b"}`));
|
||||
|
||||
function specialTabClass(active: boolean, widthClass: string): string[] {
|
||||
function specialTabClass(active: boolean, widthClass = ""): string[] {
|
||||
if (isClassicLayout.value) {
|
||||
return ["h-full border-r border-border/80 font-medium dark:border-border/45", active ? "bg-background text-foreground" : "text-foreground/70 hover:text-foreground/90"];
|
||||
}
|
||||
return [`h-7 ${widthClass} rounded-md border`, active ? "border-ring font-medium text-foreground" : "border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90"];
|
||||
}
|
||||
|
||||
function specialTabColorStyle(active: boolean): CSSProperties | undefined {
|
||||
// Special pages carry no connection color; mirror tabColorStyle's classic
|
||||
// active underline so they read as selected like every other tab.
|
||||
if (!isClassicLayout.value) return undefined;
|
||||
return active ? { boxShadow: "inset 0 -2px 0 var(--ring)" } : undefined;
|
||||
}
|
||||
|
||||
// While a special page hides the workspace, the group tab strips disappear
|
||||
// with it. Re-render the open tabs here so a click is the way back to the
|
||||
// editors (v0.6.2 kept everything in one strip; the split-pane refactor moved
|
||||
// regular tabs into the hidden workspace and left no mouse path back).
|
||||
const overlayReturnTabs = computed(() => (props.settingsPageActive || props.driverStoreActive ? queryStore.tabs : []));
|
||||
|
||||
type SpecialRegularSurface = "driverStore" | "settings";
|
||||
|
||||
function closeSpecialRegularSurfaces(keep?: SpecialRegularSurface) {
|
||||
@@ -212,10 +227,36 @@ function handleCancelClose() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="driverStoreOpen || settingsPageOpen" class="app-tab-bar relative flex w-full min-w-0 shrink-0 overflow-hidden" :class="[specialStripBarClass, { 'ring-2 ring-primary ring-inset': detachedDropTarget }]" data-main-tab-bar>
|
||||
<!-- Renders only while a special page owns the main area (the workspace and
|
||||
its group strips are hidden then); open-but-inactive special pages
|
||||
append to the focused group's tab strip instead of a row of their own. -->
|
||||
<div v-if="driverStoreActive || settingsPageActive" class="app-tab-bar relative flex w-full min-w-0 shrink-0 overflow-hidden" :class="[specialStripBarClass, { 'ring-2 ring-primary ring-inset': detachedDropTarget }]" data-main-tab-bar>
|
||||
<div class="flex w-full min-w-0 shrink-0 overflow-hidden" :class="specialStripRowClass">
|
||||
<div class="app-tab-strip relative h-full min-w-0 flex-1 overflow-hidden">
|
||||
<div class="app-tab-scroll flex h-full w-full min-w-0 flex-1" :class="specialStripScrollClass">
|
||||
<!-- Open tabs as the mouse path back while a special page owns the main area -->
|
||||
<div
|
||||
v-for="tab in overlayReturnTabs"
|
||||
:key="tab.id"
|
||||
data-return-tab
|
||||
class="app-tab-pill group flex cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="specialTabClass(false)"
|
||||
:style="tabColorStyle(tab, false, isClassicLayout)"
|
||||
:title="tabDisplayTitle(tab, t)"
|
||||
data-active-tab="false"
|
||||
@click="emit('activate-tab', tab.id)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
>
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<TabModeIcon :tab="tab" class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<span class="min-w-0 flex-1 truncate">{{ tabDisplayTitle(tab, t) }}</span>
|
||||
<span v-if="queryStore.isTabDirty(tab)" aria-hidden="true" class="dirty-tab-marker">*</span>
|
||||
<button class="shrink-0 rounded p-0.5 hover:bg-muted-foreground/20" :aria-label="t('contextMenu.closeTab')" :title="t('contextMenu.closeTab')" @pointerdown.stop @click.stop="queryStore.closeTab(tab.id)">
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings Page Tab -->
|
||||
<CustomContextMenu v-if="settingsPageOpen" :items="getSpecialRegularTabMenuItems('settings')" v-slot="{ onContextMenu }">
|
||||
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
|
||||
@@ -223,6 +264,7 @@ function handleCancelClose() {
|
||||
data-settings-page-tab
|
||||
class="app-tab-pill group flex cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="specialTabClass(!!settingsPageActive, 'min-w-36')"
|
||||
:style="specialTabColorStyle(!!settingsPageActive)"
|
||||
:data-active-tab="settingsPageActive"
|
||||
@click="emit('activate-settings-page')"
|
||||
@mousedown.middle.prevent="emit('close-settings-page')"
|
||||
@@ -245,6 +287,7 @@ function handleCancelClose() {
|
||||
data-driver-store-tab
|
||||
class="app-tab-pill group flex cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="specialTabClass(!!driverStoreActive, 'min-w-38')"
|
||||
:style="specialTabColorStyle(!!driverStoreActive)"
|
||||
:data-active-tab="driverStoreActive"
|
||||
@click="emit('activate-driver-store')"
|
||||
@mousedown.middle.prevent="emit('close-driver-store')"
|
||||
|
||||
@@ -27,8 +27,6 @@ const props = defineProps<
|
||||
tabBarCollapsed?: boolean;
|
||||
canDetachTabs?: boolean;
|
||||
detachedDropTarget?: boolean;
|
||||
/** App-level special pages (settings / driver store) hide the pane content but keep the tab strip clickable. */
|
||||
contentHidden?: boolean;
|
||||
}
|
||||
>();
|
||||
|
||||
@@ -139,16 +137,21 @@ const groupExecutableSql = computed(() => {
|
||||
:tab-bar-collapsed="tabBarCollapsed"
|
||||
:can-detach-tabs="canDetachTabs"
|
||||
:detached-drop-target="detachedDropTarget"
|
||||
:special-page-tabs="toolbar.specialPageTabs.value"
|
||||
@activate-tab="$emit('activate-tab', $event)"
|
||||
@locate-tab="$emit('locate-tab', $event)"
|
||||
@toggle-zen-mode="$emit('toggle-zen-mode')"
|
||||
@start-resize="$emit('start-resize', $event)"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@detach-tab="$emit('detach-tab', $event)"
|
||||
@activate-settings="toolbar.activateSettingsPage()"
|
||||
@close-settings="toolbar.closeSettingsPage()"
|
||||
@activate-driver-store="toolbar.activateDriverStore()"
|
||||
@close-driver-store="toolbar.closeDriverStore()"
|
||||
/>
|
||||
<!-- The toolbar stays at the top of the pane's content column in every
|
||||
placement; only the tab bar moves around it. -->
|
||||
<div v-show="!contentHidden" data-group-content class="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<EditorToolbar
|
||||
v-if="activeTab && showGroupToolbar"
|
||||
:active-tab="activeTab"
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Network,
|
||||
Package,
|
||||
PanelTop,
|
||||
Pencil,
|
||||
PencilRuler,
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
RotateCcw,
|
||||
RotateCw,
|
||||
Search,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Table2,
|
||||
TableProperties,
|
||||
@@ -97,6 +99,8 @@ const props = defineProps<{
|
||||
canDetachTabs?: boolean;
|
||||
/** A detached tab is being dragged over this bar — highlight it as the drop target. */
|
||||
detachedDropTarget?: boolean;
|
||||
/** App-level special pages (settings / driver store) appended after the tabs. */
|
||||
specialPageTabs?: { settingsOpen: boolean; settingsActive: boolean; driverStoreOpen: boolean; driverStoreActive: boolean; driverUpdateCount: number };
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -106,6 +110,10 @@ const emit = defineEmits<{
|
||||
"start-resize": [event: MouseEvent];
|
||||
"toggle-collapse": [];
|
||||
"detach-tab": [tab: QueryTab];
|
||||
"activate-settings": [];
|
||||
"close-settings": [];
|
||||
"activate-driver-store": [];
|
||||
"close-driver-store": [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -122,6 +130,21 @@ const editingTitle = ref("");
|
||||
// with the drag state. A fresh pointerdown always resets it.
|
||||
const suppressNextTabClick = ref(false);
|
||||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
// Special pages append to the focused group's strip only: one instance at a
|
||||
// time, in the pane the user is working in (v0.6.2 kept them in the single strip).
|
||||
const showSpecialPageTabs = computed(() => !!props.specialPageTabs && (props.specialPageTabs.settingsOpen || props.specialPageTabs.driverStoreOpen) && queryStore.focusedGroupId === props.groupId);
|
||||
|
||||
function specialPageTabClass(active: boolean): string[] {
|
||||
if (isClassicLayout.value) {
|
||||
return ["h-full border-r border-border/80 font-medium dark:border-border/45", active ? "bg-background text-foreground" : "text-foreground/70 hover:text-foreground/90"];
|
||||
}
|
||||
return ["h-7 rounded-md border", active ? "border-ring font-medium text-foreground" : "border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90"];
|
||||
}
|
||||
|
||||
function specialPageTabStyle(active: boolean) {
|
||||
if (!isClassicLayout.value) return undefined;
|
||||
return active ? { boxShadow: "inset 0 -2px 0 var(--ring)" } : undefined;
|
||||
}
|
||||
const isVerticalLayout = computed(() => settingsStore.editorSettings.tabPlacement === "left" || settingsStore.editorSettings.tabPlacement === "right");
|
||||
const isWrapLayout = computed(() => !isVerticalLayout.value && settingsStore.editorSettings.tabLayout === "wrap");
|
||||
// The icon-only collapse only exists in the vertical toolbar; horizontal
|
||||
@@ -1250,6 +1273,47 @@ watch(
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
</template>
|
||||
<!-- Settings / driver store append to the focused strip like v0.6.2
|
||||
(vertical strips get them with the #8213 column-mode redesign) -->
|
||||
<template v-if="showSpecialPageTabs && !isVerticalLayout">
|
||||
<div
|
||||
v-if="specialPageTabs?.settingsOpen"
|
||||
data-settings-page-tab
|
||||
class="app-tab-pill group flex shrink-0 cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="specialPageTabClass(!!specialPageTabs?.settingsActive)"
|
||||
:style="specialPageTabStyle(!!specialPageTabs?.settingsActive)"
|
||||
:data-active-tab="specialPageTabs?.settingsActive"
|
||||
:title="t('settings.title')"
|
||||
@click="emit('activate-settings')"
|
||||
@mousedown.middle.prevent="emit('close-settings')"
|
||||
>
|
||||
<Settings class="h-3.5 w-3.5 shrink-0 text-sky-600 dark:text-sky-400" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ t("settings.title") }}</span>
|
||||
<button class="shrink-0 rounded p-0.5 hover:bg-muted-foreground/20" :aria-label="t('common.close')" :title="t('common.close')" @click.stop="emit('close-settings')">
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="specialPageTabs?.driverStoreOpen"
|
||||
data-driver-store-tab
|
||||
class="app-tab-pill group flex shrink-0 cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
|
||||
:class="specialPageTabClass(!!specialPageTabs?.driverStoreActive)"
|
||||
:style="specialPageTabStyle(!!specialPageTabs?.driverStoreActive)"
|
||||
:data-active-tab="specialPageTabs?.driverStoreActive"
|
||||
:title="t('toolbar.driverManager')"
|
||||
@click="emit('activate-driver-store')"
|
||||
@mousedown.middle.prevent="emit('close-driver-store')"
|
||||
>
|
||||
<Package class="h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ t("toolbar.driverManager") }}</span>
|
||||
<span v-if="(specialPageTabs?.driverUpdateCount ?? 0) > 0" class="inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium leading-none text-white" :aria-label="t('toolbar.updatableDriverCount')">
|
||||
{{ (specialPageTabs?.driverUpdateCount ?? 0) > 99 ? "99+" : specialPageTabs?.driverUpdateCount }}
|
||||
</span>
|
||||
<button class="shrink-0 rounded p-0.5 hover:bg-muted-foreground/20" :aria-label="t('common.close')" :title="t('common.close')" @click.stop="emit('close-driver-store')">
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div :class="tabTailDragRegionClass" data-tauri-drag-region />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,6 @@ const props = defineProps<
|
||||
tabBarCollapsed?: boolean;
|
||||
canDetachTabs?: boolean;
|
||||
detachedDropTarget?: boolean;
|
||||
/** While an App-level special page (settings / driver store) is active, keep only the group tab strips visible. */
|
||||
specialSurfaceActive?: boolean;
|
||||
}
|
||||
>();
|
||||
const emit = defineEmits<
|
||||
@@ -124,9 +122,7 @@ const showResultPane = ref(true);
|
||||
// split and 0, so collapsing/expanding glides instead of jumping: the pane
|
||||
// transition for this outer split is re-enabled in sqlEditorWorkspace.css
|
||||
// (globals.css kills it for horizontal splitpanes).
|
||||
// Special pages own the main area: the shared result pane collapses so only
|
||||
// the per-group tab strips remain clickable as the way back to the workspace.
|
||||
const resultPaneTargetSize = computed(() => (showSharedResult.value && showResultPane.value && !props.specialSurfaceActive ? resultPaneSize.value : 0));
|
||||
const resultPaneTargetSize = computed(() => (showSharedResult.value && showResultPane.value ? resultPaneSize.value : 0));
|
||||
const editorPaneSize = computed(() => 100 - resultPaneTargetSize.value);
|
||||
// Entrance choreography is hydration-gated: groups present at first render
|
||||
// never animate (no load choreography); only groups created later — by a
|
||||
@@ -221,7 +217,6 @@ function handleFocusStatement(tabId: string, range: StatementRange | null): bool
|
||||
:tab-bar-collapsed="tabBarCollapsed"
|
||||
:can-detach-tabs="canDetachTabs"
|
||||
:detached-drop-target="detachedDropTarget"
|
||||
:content-hidden="specialSurfaceActive"
|
||||
class="h-full"
|
||||
v-bind="editorGroupBindings"
|
||||
@focus-group="queryStore.focusGroup($event)"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { AlertTriangle, CalendarClock, Code2, Database, Gauge, KeyRound, Network, PencilRuler, ShieldCheck, Table2, TableProperties } from "@lucide/vue";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import { tabDatabaseIconType } from "@/lib/tabs/tabPresentation";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
|
||||
// Mirrors EditorGroupTabBar's per-mode tab icon chain so surfaces outside the
|
||||
// group bar (e.g. the special-page strip's return tabs) render identical icons.
|
||||
defineProps<{ tab: QueryTab }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview' || tab.mode === 'etcd-dashboard' || tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" />
|
||||
<Database v-else-if="tab.mode === 'databases'" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" />
|
||||
<Gauge v-else-if="tab.mode === 'dolt-version-control'" />
|
||||
<Code2 v-else />
|
||||
</template>
|
||||
@@ -84,6 +84,75 @@ describe("AppTabBar layout-aware special tabs", () => {
|
||||
it("keeps both special tabs on the shared presentation helper", () => {
|
||||
expect(tabBarSource).toContain("specialTabClass(!!settingsPageActive, 'min-w-36')");
|
||||
expect(tabBarSource).toContain("specialTabClass(!!driverStoreActive, 'min-w-38')");
|
||||
// 经典布局激活的特殊标签与普通标签一样显示 inset 下划线指示条。
|
||||
expect(tabBarSource).toContain('return active ? { boxShadow: "inset 0 -2px 0 var(--ring)" } : undefined;');
|
||||
expect(tabBarSource).toContain(':style="specialTabColorStyle(!!settingsPageActive)"');
|
||||
expect(tabBarSource).toContain(':style="specialTabColorStyle(!!driverStoreActive)"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppTabBar return tabs while a special page is active", () => {
|
||||
// 特殊页激活时工作区被整体隐藏,分组标签条随之消失;必须在特殊条里重渲
|
||||
// 染打开的标签,否则用户没有任何鼠标路径切回编辑器(v0.6.4 回归)。
|
||||
// 未激活的特殊页不再渲染独立条,由分组标签条末尾承载(见 EditorGroupTabBar)。
|
||||
it("renders the app-level strip only while a special page is active", () => {
|
||||
expect(tabBarSource).toContain('v-if="driverStoreActive || settingsPageActive"');
|
||||
});
|
||||
|
||||
it("re-renders the open tabs only while settings or the driver store is active", () => {
|
||||
expect(tabBarSource).toContain("(props.settingsPageActive || props.driverStoreActive ? queryStore.tabs : [])");
|
||||
expect(tabBarSource).toContain('v-for="tab in overlayReturnTabs"');
|
||||
expect(tabBarSource).toContain("data-return-tab");
|
||||
expect(tabBarSource).toContain("@click=\"emit('activate-tab', tab.id)\"");
|
||||
// 普通标签在前,设置/驱动管理追加在末尾(v0.6.2 的顺序;新开的特殊页
|
||||
// 不应插到最前面)。
|
||||
const indices = ['v-for="tab in overlayReturnTabs"', "data-settings-page-tab", "data-driver-store-tab"].map((marker) => tabBarSource.indexOf(marker));
|
||||
expect(indices.every((index) => index >= 0)).toBe(true);
|
||||
expect([...indices].sort((a, b) => a - b)).toEqual(indices);
|
||||
});
|
||||
|
||||
it("keeps return tabs inactive while the special page owns the surface", () => {
|
||||
// 特殊页激活期间只有一个激活面:返回标签不得保留上一个标签的高亮。
|
||||
expect(tabBarSource).toContain(':class="specialTabClass(false)"');
|
||||
expect(tabBarSource).toContain(':style="tabColorStyle(tab, false, isClassicLayout)"');
|
||||
expect(tabBarSource).toContain('data-active-tab="false"');
|
||||
});
|
||||
|
||||
it("gives return tabs the native mode icon", () => {
|
||||
expect(tabBarSource).toContain(':class="tabIconClass(tab)"');
|
||||
expect(tabBarSource).toContain('<TabModeIcon :tab="tab" class="h-3.5 w-3.5" />');
|
||||
});
|
||||
|
||||
it("keeps return tabs closable with the shared dirty-confirm flow", () => {
|
||||
expect(tabBarSource).toContain('@mousedown.middle.prevent="queryStore.closeTab(tab.id)"');
|
||||
expect(tabBarSource).toContain('@click.stop="queryStore.closeTab(tab.id)"');
|
||||
});
|
||||
|
||||
it("wires the return-tab click to the query-surface activation in App", () => {
|
||||
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
|
||||
expect(appSource).toContain('@activate-tab="activateQueryTab"');
|
||||
// activateQueryTab flips the special surfaces off and switches to the tab.
|
||||
expect(appSource).toMatch(/function activateQueryTab\(tabId: string\): boolean \{[\s\S]*?activateQuerySurface\(\);/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Group strip special page tabs", () => {
|
||||
it("appends open special pages to the focused group strip like v0.6.2", () => {
|
||||
expect(groupTabBarSource).toContain("queryStore.focusedGroupId === props.groupId");
|
||||
expect(groupTabBarSource).toContain("data-settings-page-tab");
|
||||
expect(groupTabBarSource).toContain("data-driver-store-tab");
|
||||
expect(groupTabBarSource).toContain("@click=\"emit('activate-settings')\"");
|
||||
expect(groupTabBarSource).toContain("@click=\"emit('activate-driver-store')\"");
|
||||
expect(groupTabBarSource).toContain('t("toolbar.driverManager")');
|
||||
// 经典布局下的激活下划线与普通标签一致。
|
||||
expect(groupTabBarSource).toContain('return active ? { boxShadow: "inset 0 -2px 0 var(--ring)" } : undefined;');
|
||||
});
|
||||
|
||||
it("wires the group strip through the injected special page actions", () => {
|
||||
const groupSource = readFileSync(new URL("../EditorGroup.vue", import.meta.url), "utf8");
|
||||
expect(groupSource).toContain(':special-page-tabs="toolbar.specialPageTabs.value"');
|
||||
expect(groupSource).toContain('@activate-settings="toolbar.activateSettingsPage()"');
|
||||
expect(groupSource).toContain('@close-driver-store="toolbar.closeDriverStore()"');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -182,48 +182,4 @@ describe("EditorGroup mount contract", () => {
|
||||
app2.unmount();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("keeps the tab strip mounted while special pages hide the pane content", async () => {
|
||||
const store = useQueryStore();
|
||||
const queryId = store.createTab("pg-1", "app", "Query 1", "query");
|
||||
|
||||
const mountGroup = async (contentHidden: boolean) => {
|
||||
const host = createHost();
|
||||
const app = createApp(EditorGroup, {
|
||||
groupId: "group-1",
|
||||
tabIds: [queryId],
|
||||
activeTabId: queryId,
|
||||
activeTab: tab(queryId),
|
||||
activeConnection: undefined,
|
||||
executableSql: "SELECT 1",
|
||||
activeOutputView: "result",
|
||||
formatSqlRequest: null,
|
||||
compressSqlRequest: null,
|
||||
selectedSql: "",
|
||||
cursorPos: 0,
|
||||
blockDangerousRedisCommands: false,
|
||||
contentHidden,
|
||||
});
|
||||
app.use(pinia);
|
||||
app.use(i18n);
|
||||
app.mount(host);
|
||||
await nextTick();
|
||||
return { host, app };
|
||||
};
|
||||
|
||||
// 设置/驱动管理等 App 级页面接管主区域时,分组内容隐藏但标签条必须保留,
|
||||
// 否则用户没有任何鼠标路径切回编辑器(v0.6.3 分栏重构把整个工作区藏掉的回归)。
|
||||
const hidden = await mountGroup(true);
|
||||
expect(hidden.host.querySelector('[data-test="group-tabbar"]')).not.toBeNull();
|
||||
const hiddenContent = hidden.host.querySelector<HTMLElement>("[data-group-content]");
|
||||
expect(hiddenContent?.style.display).toBe("none");
|
||||
hidden.app.unmount();
|
||||
hidden.host.remove();
|
||||
|
||||
const visible = await mountGroup(false);
|
||||
const visibleContent = visible.host.querySelector<HTMLElement>("[data-group-content]");
|
||||
expect(visibleContent?.style.display).not.toBe("none");
|
||||
visible.app.unmount();
|
||||
visible.host.remove();
|
||||
});
|
||||
});
|
||||
+12
-15
@@ -3,30 +3,27 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
|
||||
|
||||
// The editor-content wrapper used to carry a v-show guard that hid the whole
|
||||
// workspace while the settings page or driver store was active. That guard
|
||||
// also display:none-d every group tab strip, so opening a special page left
|
||||
// the user with no mouse path back to the editors (v0.6.4 regression). The
|
||||
// wrapper now stays visible and the workspace hides only the pane CONTENT via
|
||||
// specialSurfaceActive (group tab strips remain clickable). These assertions
|
||||
// pin the sibling order and the new hand-off so a future merge cannot
|
||||
// re-introduce either the guard or nesting the special surfaces inside it.
|
||||
// The editor-content wrapper carries a v-show guard that hides it while the
|
||||
// settings page or driver store is the active surface. A past merge resolved
|
||||
// this region by nesting the special surfaces INSIDE that guarded wrapper,
|
||||
// which display:none-d the settings page exactly when it was active. These
|
||||
// assertions pin the sibling order so a future merge cannot re-introduce it.
|
||||
describe("App main content surface structure", () => {
|
||||
it("keeps the editor workspace visible while special pages hide only its content", () => {
|
||||
it("hides only the editor workspace via the surface guard, never the special pages", () => {
|
||||
const guard = 'v-show="!driverStoreActive && !settingsStore.settingsPageActive"';
|
||||
// The wrapper guard is gone; the special-page state flows into the workspace instead.
|
||||
expect(appSource).not.toContain(guard);
|
||||
expect(appSource).toContain(':special-surface-active="driverStoreActive || settingsStore.settingsPageActive"');
|
||||
// One guarded wrapper; the welcome screen's v-else-if reuses the flags in
|
||||
// its own condition and must not be mistaken for a second guard.
|
||||
expect(appSource.split(guard).length - 1).toBe(1);
|
||||
|
||||
// Source order: special surfaces first, then the editor wrapper.
|
||||
const markers = ["<DriverStorePage", "<EditorSettingsPage", '<div v-if="queryStore.tabs.length > 0"', "<SqlEditorWorkspace", "<WelcomeScreen"];
|
||||
// Source order: special surfaces first, then the guarded editor wrapper.
|
||||
const markers = ["<DriverStorePage", "<EditorSettingsPage", guard, "<SqlEditorWorkspace", "<WelcomeScreen"];
|
||||
const indices = markers.map((marker) => appSource.indexOf(marker));
|
||||
expect(indices.every((index) => index >= 0)).toBe(true);
|
||||
expect([...indices].sort((a, b) => a - b)).toEqual(indices);
|
||||
});
|
||||
|
||||
it("guards the editor wrapper with the open-tab count and chains the welcome screen to it", () => {
|
||||
expect(appSource).toContain('<div v-if="queryStore.tabs.length > 0" class="flex flex-col flex-1 min-h-0">');
|
||||
expect(appSource).toContain('<div v-if="queryStore.tabs.length > 0" v-show="!driverStoreActive && !settingsStore.settingsPageActive"');
|
||||
expect(appSource).toContain('v-else-if="queryStore.tabs.length === 0 && !driverStoreActive && !settingsStore.settingsPageActive"');
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,15 @@ import type { InjectionKey, Ref } from "vue";
|
||||
* the acting tab from the explicit tabId argument (never re-reading the global
|
||||
* active tab after an await).
|
||||
*/
|
||||
/** Open/active state of the App-level special pages (settings / driver store). */
|
||||
export interface SpecialPageTabsState {
|
||||
settingsOpen: boolean;
|
||||
settingsActive: boolean;
|
||||
driverStoreOpen: boolean;
|
||||
driverStoreActive: boolean;
|
||||
driverUpdateCount: number;
|
||||
}
|
||||
|
||||
export interface EditorToolbarActions {
|
||||
explainMode: Ref<"explain" | "autotrace">;
|
||||
blockDangerousRedisCommands: Ref<boolean>;
|
||||
@@ -34,6 +43,12 @@ export interface EditorToolbarActions {
|
||||
changeSchema(tabId: string, schema: string | undefined): void;
|
||||
setDefaultDatabase(tabId: string): void;
|
||||
clearDefaultDatabase(tabId: string): void;
|
||||
/** Special pages appended to the focused group's tab strip while open but inactive. */
|
||||
specialPageTabs: Ref<SpecialPageTabsState>;
|
||||
activateSettingsPage(): void;
|
||||
closeSettingsPage(): void;
|
||||
activateDriverStore(): void;
|
||||
closeDriverStore(): void;
|
||||
}
|
||||
|
||||
export const EDITOR_TOOLBAR_ACTIONS: InjectionKey<EditorToolbarActions> = Symbol("dbx:editor-toolbar-actions");
|
||||
@@ -47,6 +62,9 @@ export function createNoopEditorToolbarActions(): EditorToolbarActions {
|
||||
const noop = () => undefined;
|
||||
const mode = { value: "explain" } as Ref<"explain" | "autotrace">;
|
||||
const flag = { value: true } as Ref<boolean>;
|
||||
const specialPageTabs = {
|
||||
value: { settingsOpen: false, settingsActive: false, driverStoreOpen: false, driverStoreActive: false, driverUpdateCount: 0 },
|
||||
} as Ref<SpecialPageTabsState>;
|
||||
return {
|
||||
explainMode: mode,
|
||||
blockDangerousRedisCommands: flag,
|
||||
@@ -70,5 +88,10 @@ export function createNoopEditorToolbarActions(): EditorToolbarActions {
|
||||
changeSchema: noop,
|
||||
setDefaultDatabase: noop,
|
||||
clearDefaultDatabase: noop,
|
||||
specialPageTabs,
|
||||
activateSettingsPage: noop,
|
||||
closeSettingsPage: noop,
|
||||
activateDriverStore: noop,
|
||||
closeDriverStore: noop,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user