feat(tabs): add multi-row tab layout option

This commit is contained in:
gggaiitx 2026-07-17 00:11:47 +08:00 committed by GitHub
parent 17747f8a2d
commit 916aad66bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 216 additions and 13 deletions

View File

@ -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);
</div>
</div>
<Separator />
<div class="settings-appearance-group">
<Label>{{ t("settings.tabLayout") }}</Label>
<div class="settings-appearance-choice-grid">
<Button type="button" variant="outline" class="settings-choice-card h-auto justify-start border p-3" :class="editTabLayout === 'scroll' ? 'settings-choice-card--selected border-blue-300 ring-2 ring-blue-300/50' : ''" @click="setTabLayout('scroll')">
<div class="w-full min-w-0 text-left">
<div class="text-sm font-medium">{{ t("settings.tabLayoutScroll") }}</div>
<div class="text-xs text-muted-foreground">{{ t("settings.tabLayoutScrollDescription") }}</div>
</div>
</Button>
<Button type="button" variant="outline" class="settings-choice-card h-auto justify-start border p-3" :class="editTabLayout === 'wrap' ? 'settings-choice-card--selected border-blue-300 ring-2 ring-blue-300/50' : ''" @click="setTabLayout('wrap')">
<div class="w-full min-w-0 text-left">
<div class="text-sm font-medium">{{ t("settings.tabLayoutWrap") }}</div>
<div class="text-xs text-muted-foreground">{{ t("settings.tabLayoutWrapDescription") }}</div>
</div>
</Button>
</div>
</div>
<!-- <div v-if="!isWeb" class="space-y-2"> -->
<div class="settings-appearance-group">
<Label>{{ t("settings.iconTheme") }}</Label>

View File

@ -52,6 +52,7 @@ const tabDrag = useTabDrag((draggedId, targetId, position) => {
const editingTabId = ref<string | null>(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<CSSProperties>(() => ({
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
<div v-if="showRegularTabScrollbar" class="app-tab-scrollbar" :class="{ 'app-tab-scrollbar--dragging': isScrollbarDragging }" @pointerdown="startScrollbarDrag">
<div class="app-tab-scrollbar__thumb" :style="tabScrollbarThumbStyle" />
</div>
<div ref="tabsContainerRef" class="app-tab-scroll flex w-full min-w-0 flex-1 items-center overflow-x-auto" :class="isClassicLayout ? 'h-full' : 'h-full gap-1.5 py-1.5'" :style="tabsContainerStyle" @scroll="updateScrollButtons" @wheel="onTabsWheel">
<div
ref="tabsContainerRef"
class="app-tab-scroll flex w-full min-w-0 flex-1 items-center overflow-x-auto"
:class="[isClassicLayout ? 'h-full' : 'h-full gap-1.5 py-1.5', isWrapLayout ? 'wrap-mode' : '', isWrapLayout && isClassicLayout ? 'classic-wrap' : '']"
:style="tabsContainerStyle"
@scroll="updateScrollButtons"
@wheel="onTabsWheel"
>
<CustomContextMenu v-for="tab in regularTabs" :key="tab.id" :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
<Tooltip>
@ -735,7 +747,14 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
<div v-if="showFixedTabScrollbar" class="app-tab-scrollbar app-tab-scrollbar--bottom" :class="{ 'app-tab-scrollbar--dragging': isFixedScrollbarDragging }" @pointerdown="startFixedScrollbarDrag">
<div class="app-tab-scrollbar__thumb" :style="fixedTabScrollbarThumbStyle" />
</div>
<div ref="fixedTabsContainerRef" class="app-tab-scroll flex w-full min-w-0 flex-1 items-center overflow-x-auto" :class="isClassicLayout ? 'h-full' : 'h-full gap-1.5 py-1'" :style="tabsContainerStyle" @scroll="updateFixedScrollButtons" @wheel="onFixedTabsWheel">
<div
ref="fixedTabsContainerRef"
class="app-tab-scroll flex w-full min-w-0 flex-1 items-center overflow-x-auto"
:class="[isClassicLayout ? 'h-full' : 'h-full gap-1.5 py-1', isWrapLayout ? 'wrap-mode' : '', isWrapLayout && isClassicLayout ? 'classic-wrap' : '']"
:style="tabsContainerStyle"
@scroll="updateFixedScrollButtons"
@wheel="onFixedTabsWheel"
>
<CustomContextMenu v-for="tab in fixedTabs" :key="tab.id" :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
<Tooltip>
@ -916,6 +935,45 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
</template>
<style scoped>
/* 多行平铺模式:覆盖滚动相关样式,让标签换行展示 */
.app-tab-scroll.wrap-mode {
height: auto !important;
overflow: visible !important;
overflow-x: visible !important;
overflow-y: visible !important;
flex-wrap: wrap;
}
/* 父级 strip 容器在包含 wrap-mode 时也需要解除高度约束和裁剪 */
.app-tab-strip:has(.wrap-mode) {
height: auto !important;
overflow: visible !important;
}
/* 标签栏行容器(直接子 div在包含 wrap-mode 时解除固定高度和裁剪 */
.app-tab-bar > div:has(.wrap-mode) {
height: auto !important;
overflow: visible !important;
}
/* 标签栏本身也解除裁剪,让换行内容自然撑高 */
.app-tab-bar:has(.wrap-mode) {
overflow: visible !important;
}
/* 经典布局 + 多行模式:优化多行标签显示 */
.app-tab-scroll.classic-wrap {
row-gap: 0.25rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
align-items: flex-start;
}
/* 经典布局下 h-full 在 height:auto 容器中失效,改为固定高度 */
.app-tab-scroll.classic-wrap > div {
height: 2rem;
}
.dirty-tab-marker {
display: inline-flex;
width: 0.5rem;

View File

@ -3445,6 +3445,11 @@ export default {
appLayoutSeparatedDescription: "Clear panel separation with block-style tabs.",
appLayoutClassic: "Classic compact",
appLayoutClassicDescription: "Continuous tab bar with compact panel headers.",
tabLayout: "Tab bar overflow",
tabLayoutScroll: "Single row scroll",
tabLayoutScrollDescription: "Tabs scroll horizontally in one row when overflowing.",
tabLayoutWrap: "Multi-row wrap",
tabLayoutWrapDescription: "Tabs wrap to multiple rows to show all at once.",
iconTheme: "Icon and logo style",
iconThemeDefault: "Default",
iconThemeDefaultDescription: "Use the color DBX app icon.",

View File

@ -3218,6 +3218,11 @@ export default withEnglishFallback({
appLayoutSeparatedDescription: "Separación clara de paneles con pestañas en bloque.",
appLayoutClassic: "Clásico compacto",
appLayoutClassicDescription: "Barra de pestañas continua con encabezados de panel compactos.",
tabLayout: "Desbordamiento de barra de pestañas",
tabLayoutScroll: "Desplazamiento en una fila",
tabLayoutScrollDescription: "Las pestañas se desplazan horizontalmente al desbordarse.",
tabLayoutWrap: "Ajuste multilínea",
tabLayoutWrapDescription: "Las pestañas se ajustan en varias filas para mostrarlas todas.",
iconTheme: "Estilo de icono y logo",
iconThemeDefault: "Predeterminado",
iconThemeDefaultDescription: "Usar el icono de color de DBX.",

View File

@ -3216,6 +3216,11 @@ export default withEnglishFallback({
appLayoutSeparatedDescription: "Separazione chiara dei pannelli con schede stile blocco.",
appLayoutClassic: "Classico compatto",
appLayoutClassicDescription: "Barra delle schede continua con intestazioni di pannello compatte.",
tabLayout: "Overflow barra schede",
tabLayoutScroll: "Scorrimento su una riga",
tabLayoutScrollDescription: "Le schede scorrono orizzontalmente quando traboccano.",
tabLayoutWrap: "Righe multiple",
tabLayoutWrapDescription: "Le schede vanno a capo su più righe per mostrarle tutte.",
iconTheme: "Stile icona e logo",
iconThemeDefault: "Predefinito",
iconThemeDefaultDescription: "Utilizza l'icona colorata dell'app DBX.",

View File

@ -3217,6 +3217,11 @@ export default withEnglishFallback({
appLayoutSeparatedDescription: "ブロックスタイルのタブで明確なパネル分離。",
appLayoutClassic: "クラシックコンパクト",
appLayoutClassicDescription: "連続タブバーとコンパクトなパネルヘッダー。",
tabLayout: "タブバーのオーバーフロー",
tabLayoutScroll: "1行スクロール",
tabLayoutScrollDescription: "タブが1行に収まらないとき水平スクロールします。",
tabLayoutWrap: "複数行折り返し",
tabLayoutWrapDescription: "タブが1行に収まらないとき折り返してすべて表示します。",
iconTheme: "アイコンとロゴのスタイル",
iconThemeDefault: "デフォルト",
iconThemeDefaultDescription: "カラーDBXアプリアイコンを使用。",

View File

@ -3218,6 +3218,11 @@ export default withEnglishFallback({
appLayoutSeparatedDescription: "Separação clara de painéis com abas em estilo de bloco.",
appLayoutClassic: "Clássico compacto",
appLayoutClassicDescription: "Barra de abas contínua com cabeçalhos de painel compactos.",
tabLayout: "Estouro da barra de abas",
tabLayoutScroll: "Rolagem em linha única",
tabLayoutScrollDescription: "As abas rolam horizontalmente quando excedem o espaço.",
tabLayoutWrap: "Quebra em múltiplas linhas",
tabLayoutWrapDescription: "As abas quebram em múltiplas linhas para exibir todas.",
iconTheme: "Estilo do ícone e do logo",
iconThemeDefault: "Padrão",
iconThemeDefaultDescription: "Usar o ícone colorido do app DBX.",

View File

@ -3415,6 +3415,11 @@ export default withEnglishFallback({
appLayoutSeparatedDescription: "面板分隔更清晰,标签页为块状样式。",
appLayoutClassic: "经典紧凑",
appLayoutClassicDescription: "使用连续标签栏和更紧凑的面板标题。",
tabLayout: "标签栏溢出",
tabLayoutScroll: "单行滚动",
tabLayoutScrollDescription: "标签页超出一行时水平滚动显示。",
tabLayoutWrap: "多行平铺",
tabLayoutWrapDescription: "标签页超出一行时自动换行,平铺显示所有标签。",
iconTheme: "图标与 Logo 样式",
iconThemeDefault: "默认",
iconThemeDefaultDescription: "使用彩色 DBX 应用图标。",

View File

@ -3034,6 +3034,11 @@ export default withEnglishFallback({
appLayoutSeparatedDescription: "面板分隔更清晰,分頁為塊狀樣式。",
appLayoutClassic: "經典緊湊",
appLayoutClassicDescription: "使用連續標籤欄和更緊湊的面板標題。",
tabLayout: "標籤欄溢出",
tabLayoutScroll: "單行滾動",
tabLayoutScrollDescription: "標籤頁超出一行時水平滾動顯示。",
tabLayoutWrap: "多行平鋪",
tabLayoutWrapDescription: "標籤頁超出一行時自動換行,平鋪顯示所有標籤。",
iconTheme: "圖示與 Logo 樣式",
iconThemeDefault: "預設",
iconThemeDefaultDescription: "使用彩色 DBX 應用程式圖示。",

View File

@ -22,6 +22,7 @@ function makeSettings(overrides: Partial<EditorSettings> = {}): EditorSettings {
confirmUnsavedSqlClose: true,
objectBrowserViewMode: "list",
sqlVariableSyntaxOverrides: {},
tabLayout: "scroll",
...overrides,
};
}
@ -79,3 +80,52 @@ describe("editorSettingsPatchFromDraft", () => {
expect(patch.continueOnErrorOnBatch).toBeUndefined();
});
});
describe("EDITOR_SETTINGS_DRAFT_KEYS - tabLayout", () => {
it("includes tabLayout", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("tabLayout");
});
});
describe("editorSettingsDraftFromSettings - tabLayout", () => {
it("maps tabLayout from settings", () => {
expect(editorSettingsDraftFromSettings(makeSettings({ tabLayout: "wrap" })).tabLayout).toBe("wrap");
expect(editorSettingsDraftFromSettings(makeSettings({ tabLayout: "scroll" })).tabLayout).toBe("scroll");
});
});
describe("editorSettingsDraftChanged - tabLayout", () => {
it("detects change in tabLayout", () => {
const settings = makeSettings({ tabLayout: "scroll" });
const draft = editorSettingsDraftFromSettings(settings);
const base = editorSettingsDraftFromSettings(settings);
draft.tabLayout = "wrap";
expect(editorSettingsDraftChanged(draft, base)).toBe(true);
});
it("detects no change when tabLayout matches", () => {
const settings = makeSettings({ tabLayout: "wrap" });
const draft = editorSettingsDraftFromSettings(settings);
const base = editorSettingsDraftFromSettings(settings);
expect(editorSettingsDraftChanged(draft, base)).toBe(false);
});
});
describe("editorSettingsPatchFromDraft - tabLayout", () => {
it("includes tabLayout in patch when changed", () => {
const settings = makeSettings({ tabLayout: "scroll" });
const draft = editorSettingsDraftFromSettings(settings);
const base = editorSettingsDraftFromSettings(settings);
draft.tabLayout = "wrap";
const patch = editorSettingsPatchFromDraft(draft, base);
expect(patch.tabLayout).toBe("wrap");
});
it("omits tabLayout when unchanged", () => {
const settings = makeSettings({ tabLayout: "scroll" });
const draft = editorSettingsDraftFromSettings(settings);
const base = editorSettingsDraftFromSettings(settings);
const patch = editorSettingsPatchFromDraft(draft, base);
expect(patch.tabLayout).toBeUndefined();
});
});

View File

@ -21,6 +21,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"confirmDangerousSqlExecution",
"confirmUnsavedSqlClose",
"appLayout",
"tabLayout",
"showColumnCommentsInHeader",
"showColumnTypesInHeader",
"compactColumnHeaderActions",

View File

@ -127,3 +127,24 @@ describe("normalizeEditorSettings - continueOnErrorOnBatch", () => {
expect(normalizeEditorSettings({ continueOnErrorOnBatch: 1 } as any).continueOnErrorOnBatch).toBe(false);
});
});
describe("normalizeEditorSettings - tabLayout", () => {
it("defaults tabLayout to scroll", () => {
expect(normalizeEditorSettings({}).tabLayout).toBe("scroll");
});
it("preserves explicit scroll mode", () => {
expect(normalizeEditorSettings({ tabLayout: "scroll" }).tabLayout).toBe("scroll");
});
it("preserves explicit wrap mode", () => {
expect(normalizeEditorSettings({ tabLayout: "wrap" }).tabLayout).toBe("wrap");
});
it("falls back to scroll for invalid values", () => {
expect(normalizeEditorSettings({ tabLayout: "invalid" } as any).tabLayout).toBe("scroll");
expect(normalizeEditorSettings({ tabLayout: undefined } as any).tabLayout).toBe("scroll");
expect(normalizeEditorSettings({ tabLayout: null } as any).tabLayout).toBe("scroll");
expect(normalizeEditorSettings({ tabLayout: 123 } as any).tabLayout).toBe("scroll");
});
});

View File

@ -268,6 +268,8 @@ const COLUMN_WIDTH_DENSITIES = ["compact", "standard", "comfortable"] as const;
export type ColumnWidthDensity = (typeof COLUMN_WIDTH_DENSITIES)[number];
const CELL_DETAIL_PANEL_LAYOUTS = ["bottom", "right"] as const;
export type CellDetailPanelLayout = (typeof CELL_DETAIL_PANEL_LAYOUTS)[number];
const TAB_LAYOUT_MODES = ["scroll", "wrap"] as const;
export type TabLayoutMode = (typeof TAB_LAYOUT_MODES)[number];
const DATA_GRID_RENDER_MODES = ["dom", "canvas"] as const;
export type DataGridRenderMode = (typeof DATA_GRID_RENDER_MODES)[number];
const DATA_GRID_SEARCH_MODES = ["filter", "highlight"] as const;
@ -360,6 +362,7 @@ export interface EditorSettings {
confirmDangerousSqlExecution: boolean;
confirmUnsavedSqlClose: boolean;
compactTabTitle: boolean;
tabLayout: TabLayoutMode;
appLayout: "separated" | "classic";
pageSize: number;
infiniteScroll: boolean;
@ -501,6 +504,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
confirmDangerousSqlExecution: true,
confirmUnsavedSqlClose: true,
compactTabTitle: false,
tabLayout: "scroll",
appLayout: "classic",
pageSize: 100,
infiniteScroll: false,
@ -584,6 +588,10 @@ function normalizeColumnWidthDensity(value: unknown): ColumnWidthDensity {
return COLUMN_WIDTH_DENSITIES.includes(value as ColumnWidthDensity) ? (value as ColumnWidthDensity) : DEFAULT_EDITOR_SETTINGS.columnWidthDensity;
}
function normalizeTabLayout(value: unknown): TabLayoutMode {
return TAB_LAYOUT_MODES.includes(value as TabLayoutMode) ? (value as TabLayoutMode) : DEFAULT_EDITOR_SETTINGS.tabLayout;
}
function normalizeCellDetailPanelLayout(value: unknown): CellDetailPanelLayout {
return CELL_DETAIL_PANEL_LAYOUTS.includes(value as CellDetailPanelLayout) ? (value as CellDetailPanelLayout) : DEFAULT_EDITOR_SETTINGS.cellDetailPanelLayout;
}
@ -742,6 +750,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
confirmDangerousSqlExecution: settings.confirmDangerousSqlExecution ?? DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution,
confirmUnsavedSqlClose: settings.confirmUnsavedSqlClose ?? DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose,
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
tabLayout: normalizeTabLayout(settings.tabLayout),
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
pageSize: normalizeResultPageSize(settings.pageSize),
infiniteScroll: settings.infiniteScroll ?? DEFAULT_EDITOR_SETTINGS.infiniteScroll,
@ -1060,6 +1069,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.confirmDangerousSqlExecution !== undefined) editorSettings.value.confirmDangerousSqlExecution = partial.confirmDangerousSqlExecution;
if (partial.confirmUnsavedSqlClose !== undefined) editorSettings.value.confirmUnsavedSqlClose = partial.confirmUnsavedSqlClose;
if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle;
if (partial.tabLayout !== undefined) editorSettings.value.tabLayout = normalizeTabLayout(partial.tabLayout);
if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout;
if (partial.pageSize !== undefined) editorSettings.value.pageSize = normalizeResultPageSize(partial.pageSize);
if (partial.infiniteScroll !== undefined) editorSettings.value.infiniteScroll = partial.infiniteScroll;