feat(tabs): add multi-row tab layout option
This commit is contained in:
parent
17747f8a2d
commit
916aad66bf
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -3217,6 +3217,11 @@ export default withEnglishFallback({
|
|||
appLayoutSeparatedDescription: "ブロックスタイルのタブで明確なパネル分離。",
|
||||
appLayoutClassic: "クラシックコンパクト",
|
||||
appLayoutClassicDescription: "連続タブバーとコンパクトなパネルヘッダー。",
|
||||
tabLayout: "タブバーのオーバーフロー",
|
||||
tabLayoutScroll: "1行スクロール",
|
||||
tabLayoutScrollDescription: "タブが1行に収まらないとき水平スクロールします。",
|
||||
tabLayoutWrap: "複数行折り返し",
|
||||
tabLayoutWrapDescription: "タブが1行に収まらないとき折り返してすべて表示します。",
|
||||
iconTheme: "アイコンとロゴのスタイル",
|
||||
iconThemeDefault: "デフォルト",
|
||||
iconThemeDefaultDescription: "カラーDBXアプリアイコンを使用。",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -3415,6 +3415,11 @@ export default withEnglishFallback({
|
|||
appLayoutSeparatedDescription: "面板分隔更清晰,标签页为块状样式。",
|
||||
appLayoutClassic: "经典紧凑",
|
||||
appLayoutClassicDescription: "使用连续标签栏和更紧凑的面板标题。",
|
||||
tabLayout: "标签栏溢出",
|
||||
tabLayoutScroll: "单行滚动",
|
||||
tabLayoutScrollDescription: "标签页超出一行时水平滚动显示。",
|
||||
tabLayoutWrap: "多行平铺",
|
||||
tabLayoutWrapDescription: "标签页超出一行时自动换行,平铺显示所有标签。",
|
||||
iconTheme: "图标与 Logo 样式",
|
||||
iconThemeDefault: "默认",
|
||||
iconThemeDefaultDescription: "使用彩色 DBX 应用图标。",
|
||||
|
|
|
|||
|
|
@ -3034,6 +3034,11 @@ export default withEnglishFallback({
|
|||
appLayoutSeparatedDescription: "面板分隔更清晰,分頁為塊狀樣式。",
|
||||
appLayoutClassic: "經典緊湊",
|
||||
appLayoutClassicDescription: "使用連續標籤欄和更緊湊的面板標題。",
|
||||
tabLayout: "標籤欄溢出",
|
||||
tabLayoutScroll: "單行滾動",
|
||||
tabLayoutScrollDescription: "標籤頁超出一行時水平滾動顯示。",
|
||||
tabLayoutWrap: "多行平鋪",
|
||||
tabLayoutWrapDescription: "標籤頁超出一行時自動換行,平鋪顯示所有標籤。",
|
||||
iconTheme: "圖示與 Logo 樣式",
|
||||
iconThemeDefault: "預設",
|
||||
iconThemeDefaultDescription: "使用彩色 DBX 應用程式圖示。",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
|||
"confirmDangerousSqlExecution",
|
||||
"confirmUnsavedSqlClose",
|
||||
"appLayout",
|
||||
"tabLayout",
|
||||
"showColumnCommentsInHeader",
|
||||
"showColumnTypesInHeader",
|
||||
"compactColumnHeaderActions",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue