@@ -1119,6 +1217,46 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
flex-wrap: wrap;
}
+.vertical-tab-layout {
+ flex: 0 0 15rem;
+}
+
+.vertical-tab-layout .app-tab-strip {
+ height: auto;
+ min-height: 0;
+ width: 100%;
+}
+
+.vertical-tab-layout .app-tab-pill {
+ width: 100%;
+ min-width: 0 !important;
+ min-height: 2rem;
+ height: 2rem !important;
+ border-right: 0;
+}
+
+.vertical-tab-layout .app-tab-scroll > * {
+ width: 100%;
+ flex: none;
+}
+
+.vertical-tab-layout .tab-group-header {
+ width: 100%;
+ padding: 0.45rem 0.6rem 0.2rem;
+ color: var(--muted-foreground);
+ font-size: 0.7rem;
+ font-weight: 600;
+ line-height: 1rem;
+ text-transform: uppercase;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.app-tab-scroll:not(.wrap-mode) .tab-group-header {
+ align-self: stretch;
+}
+
/* 父级 strip 容器在包含 wrap-mode 时也需要解除高度约束和裁剪 */
.app-tab-strip:has(.wrap-mode) {
height: auto !important;
diff --git a/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts b/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts
index 2d50e95b62..0c44685561 100644
--- a/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts
+++ b/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts
@@ -98,7 +98,7 @@ describe("AppTabBar right-side close action", () => {
});
it("waits for query tab confirmation before closing special surfaces", () => {
- expect(tabBarSource).toMatch(/queryStore\.closeRightTabs\(tab\.id, \(\) => \{[\s\S]*closeSpecialRegularSurfaces\(\);/);
+ expect(tabBarSource).toMatch(/queryStore\.closeTabsByIds\(tabsToClose, finalActiveTabId, \(\) => \{[\s\S]*closeSpecialRegularSurfaces\(\);/);
expect(tabBarSource).toContain("if (shouldActivateTarget) activateTab(tab.id)");
});
@@ -142,7 +142,8 @@ describe("AppTabBar overflow search", () => {
it("filters every open tab by its display and source titles", () => {
expect(tabBarSource).toContain('const tabSearchQuery = ref("");');
expect(tabBarSource).toContain("const filteredOpenTabs = computed(() => {");
- expect(tabBarSource).toContain("return queryStore.tabs.filter((tab) => tabTitleText(tab).toLocaleLowerCase().includes(query) || tab.title.toLocaleLowerCase().includes(query));");
+ expect(tabBarSource).toContain("return displayedTabs.value.filter((tab) => tabMatchesSearch(tab, query));");
+ expect(tabBarSource).toContain("return tabTitleText(tab).toLocaleLowerCase().includes(query) || tab.title.toLocaleLowerCase().includes(query) || connectionName.toLocaleLowerCase().includes(query);");
});
it("provides the same focused search control and empty state in both overflow menus", () => {
diff --git a/apps/desktop/src/composables/useTabDrag.ts b/apps/desktop/src/composables/useTabDrag.ts
index 7dd7e0eb16..9121a742f7 100644
--- a/apps/desktop/src/composables/useTabDrag.ts
+++ b/apps/desktop/src/composables/useTabDrag.ts
@@ -35,6 +35,7 @@ let pending: {
sourceEl: HTMLElement | null;
} | null = null;
let onDropCallback: ((draggedId: string, targetId: string, position: TabDropPosition) => boolean) | null = null;
+let dragAxis: () => "horizontal" | "vertical" = () => "horizontal";
let ghostEl: HTMLElement | null = null;
function createGhost(sourceEl: HTMLElement, x: number, y: number) {
@@ -82,8 +83,8 @@ function onPointerMove(event: PointerEvent) {
if (!pending && !state.active) return;
if (pending && !state.active) {
- const dx = event.clientX - pending.x;
- if (Math.abs(dx) < TAB_DRAG_HORIZONTAL_THRESHOLD) return;
+ const delta = dragAxis() === "vertical" ? event.clientY - pending.y : event.clientX - pending.x;
+ if (Math.abs(delta) < TAB_DRAG_HORIZONTAL_THRESHOLD) return;
state.active = true;
state.draggedId = pending.id;
state.startX = pending.x;
@@ -131,9 +132,10 @@ function ensureListeners() {
listenersAttached = true;
}
-export function useTabDrag(onDrop: (draggedId: string, targetId: string, position: TabDropPosition) => boolean) {
+export function useTabDrag(onDrop: (draggedId: string, targetId: string, position: TabDropPosition) => boolean, axis: () => "horizontal" | "vertical" = () => "horizontal") {
ensureListeners();
onDropCallback = onDrop;
+ dragAxis = axis;
function startDrag(event: PointerEvent, tabId: string) {
if (event.button !== 0) return;
@@ -163,9 +165,10 @@ export function useTabDrag(onDrop: (draggedId: string, targetId: string, positio
const el = event.currentTarget as HTMLElement;
const rect = el.getBoundingClientRect();
- const x = event.clientX - rect.left;
-
- state.dropPosition = x < rect.width / 2 ? "before" : "after";
+ const vertical = axis() === "vertical";
+ const offset = vertical ? event.clientY - rect.top : event.clientX - rect.left;
+ const size = vertical ? rect.height : rect.width;
+ state.dropPosition = offset < size / 2 ? "before" : "after";
}
function clearTarget(tabId: string) {
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index d403d2979b..d4335d8ce7 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -6355,6 +6355,21 @@ export default {
tabLayoutScrollDescription: "Tabs scroll horizontally in one row when overflowing.",
tabLayoutWrap: "Multi-row wrap",
tabLayoutWrapDescription: "Tabs wrap to multiple rows to show all at once.",
+ tabPlacement: "Tab bar position",
+ tabPlacementTop: "Top",
+ tabPlacementBottom: "Bottom",
+ tabPlacementLeft: "Left side",
+ tabPlacementRight: "Right side",
+ tabPlacementDescription: "Side tab bars scroll vertically; multi-row wrapping applies only above or below content.",
+ tabGroup: "Group by",
+ tabGroupNone: "No grouping",
+ tabGroupDatabaseType: "Group by database type",
+ tabGroupConnection: "Group by connection",
+ tabSort: "Sort by",
+ tabSortManual: "Manual order",
+ tabSortCreated: "Creation time",
+ tabSortTitle: "Tab name",
+ tabOrganizationDescription: "Grouping and sorting affect only the displayed order. Dragging is available in manual order.",
iconTheme: "Icon and logo style",
iconThemeDefault: "Default",
iconThemeDefaultDescription: "Use the color DBX app icon.",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index f07937e1bd..f4d1e53c23 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -6031,6 +6031,21 @@ export default withEnglishFallback({
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.",
+ tabPlacement: "Posición de la barra de pestañas",
+ tabPlacementTop: "Arriba",
+ tabPlacementBottom: "Abajo",
+ tabPlacementLeft: "Lado izquierdo",
+ tabPlacementRight: "Lado derecho",
+ tabPlacementDescription: "Las barras laterales se desplazan verticalmente; el ajuste multilínea solo se aplica arriba o abajo.",
+ tabGroup: "Agrupar por",
+ tabGroupNone: "Sin agrupación",
+ tabGroupDatabaseType: "Agrupar por tipo de base de datos",
+ tabGroupConnection: "Agrupar por conexión",
+ tabSort: "Ordenar por",
+ tabSortManual: "Orden manual",
+ tabSortCreated: "Hora de creación",
+ tabSortTitle: "Nombre de pestaña",
+ tabOrganizationDescription: "La agrupación y la ordenación solo afectan al orden visible. Arrastre disponible en orden manual.",
iconTheme: "Estilo de icono y logo",
iconThemeDefault: "Predeterminado",
iconThemeDefaultDescription: "Usar el icono de color de DBX.",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 97edbb878f..e39ca692d7 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -6031,6 +6031,21 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "Le schede scorrono orizzontalmente quando traboccano.",
tabLayoutWrap: "Righe multiple",
tabLayoutWrapDescription: "Le schede vanno a capo su più righe per mostrarle tutte.",
+ tabPlacement: "Posizione barra schede",
+ tabPlacementTop: "In alto",
+ tabPlacementBottom: "In basso",
+ tabPlacementLeft: "Lato sinistro",
+ tabPlacementRight: "Lato destro",
+ tabPlacementDescription: "Le barre laterali scorrono verticalmente; le righe multiple valgono solo sopra o sotto il contenuto.",
+ tabGroup: "Raggruppa per",
+ tabGroupNone: "Nessun raggruppamento",
+ tabGroupDatabaseType: "Raggruppa per tipo di database",
+ tabGroupConnection: "Raggruppa per connessione",
+ tabSort: "Ordina per",
+ tabSortManual: "Ordine manuale",
+ tabSortCreated: "Data di creazione",
+ tabSortTitle: "Nome scheda",
+ tabOrganizationDescription: "Raggruppamento e ordinamento influenzano solo l'ordine visibile. Il trascinamento è disponibile nell'ordine manuale.",
iconTheme: "Stile icona e logo",
iconThemeDefault: "Predefinito",
iconThemeDefaultDescription: "Utilizza l'icona colorata dell'app DBX.",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 8f622e4b1e..2307c9cd5b 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -6069,6 +6069,21 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "タブが1行に収まらないとき水平スクロールします。",
tabLayoutWrap: "複数行折り返し",
tabLayoutWrapDescription: "タブが1行に収まらないとき折り返してすべて表示します。",
+ tabPlacement: "タブバーの位置",
+ tabPlacementTop: "上",
+ tabPlacementBottom: "下",
+ tabPlacementLeft: "左側",
+ tabPlacementRight: "右側",
+ tabPlacementDescription: "サイドのタブバーは縦にスクロールします。複数行表示はコンテンツの上下でのみ利用できます。",
+ tabGroup: "グループ化",
+ tabGroupNone: "グループ化しない",
+ tabGroupDatabaseType: "データベース種別でグループ化",
+ tabGroupConnection: "接続ごとにグループ化",
+ tabSort: "並べ替え",
+ tabSortManual: "手動順",
+ tabSortCreated: "作成日時",
+ tabSortTitle: "タブ名",
+ tabOrganizationDescription: "グループ化と並べ替えは表示順にのみ影響します。ドラッグは手動順で利用できます。",
iconTheme: "アイコンとロゴのスタイル",
iconThemeDefault: "デフォルト",
iconThemeDefaultDescription: "カラーDBXアプリアイコンを使用。",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index 8874c74a2f..b5a2e3a7f7 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -5757,6 +5757,21 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "탭이 넘칠 때 한 행에서 가로로 스크롤됩니다.",
tabLayoutWrap: "다중 행 줄바꿈",
tabLayoutWrapDescription: "탭이 여러 행으로 줄바꿈되어 모두 한 번에 표시됩니다.",
+ tabPlacement: "탭 표시줄 위치",
+ tabPlacementTop: "위쪽",
+ tabPlacementBottom: "아래쪽",
+ tabPlacementLeft: "왼쪽",
+ tabPlacementRight: "오른쪽",
+ tabPlacementDescription: "사이드 탭 표시줄은 세로로 스크롤됩니다. 여러 줄 표시는 콘텐츠 위 또는 아래에서만 적용됩니다.",
+ tabGroup: "그룹 기준",
+ tabGroupNone: "그룹화 안 함",
+ tabGroupDatabaseType: "데이터베이스 유형별 그룹화",
+ tabGroupConnection: "연결별 그룹화",
+ tabSort: "정렬 기준",
+ tabSortManual: "수동 순서",
+ tabSortCreated: "생성 시간",
+ tabSortTitle: "탭 이름",
+ tabOrganizationDescription: "그룹화와 정렬은 표시 순서에만 영향을 줍니다. 끌기는 수동 순서에서 사용할 수 있습니다.",
iconTheme: "아이콘 및 로고 스타일",
iconThemeDefault: "기본",
iconThemeDefaultDescription: "컬러 DBX 앱 아이콘을 사용합니다.",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 0a38d0e8f5..78ae74d1cd 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -6033,6 +6033,21 @@ export default withEnglishFallback({
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.",
+ tabPlacement: "Posição da barra de abas",
+ tabPlacementTop: "Superior",
+ tabPlacementBottom: "Inferior",
+ tabPlacementLeft: "Lado esquerdo",
+ tabPlacementRight: "Lado direito",
+ tabPlacementDescription: "As barras laterais rolam verticalmente; a quebra em múltiplas linhas só se aplica acima ou abaixo do conteúdo.",
+ tabGroup: "Agrupar por",
+ tabGroupNone: "Sem agrupamento",
+ tabGroupDatabaseType: "Agrupar por tipo de banco de dados",
+ tabGroupConnection: "Agrupar por conexão",
+ tabSort: "Ordenar por",
+ tabSortManual: "Ordem manual",
+ tabSortCreated: "Hora de criação",
+ tabSortTitle: "Nome da aba",
+ tabOrganizationDescription: "Agrupamento e ordenação afetam apenas a ordem exibida. Arrastar fica disponível na ordem manual.",
iconTheme: "Estilo do ícone e do logo",
iconThemeDefault: "Padrão",
iconThemeDefaultDescription: "Usar o ícone colorido do app DBX.",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 2b8494ceb0..f0b7ba15f3 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -6338,6 +6338,21 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "标签页超出一行时水平滚动显示。",
tabLayoutWrap: "多行平铺",
tabLayoutWrapDescription: "标签页超出一行时自动换行,平铺显示所有标签。",
+ tabPlacement: "标签栏位置",
+ tabPlacementTop: "上方",
+ tabPlacementBottom: "下方",
+ tabPlacementLeft: "左侧",
+ tabPlacementRight: "右侧",
+ tabPlacementDescription: "侧边标签栏使用纵向滚动;多行平铺仅适用于上方或下方。",
+ tabGroup: "分组方式",
+ tabGroupNone: "不分组",
+ tabGroupDatabaseType: "按数据库类型分组",
+ tabGroupConnection: "按连接分组",
+ tabSort: "排序方式",
+ tabSortManual: "手动排序",
+ tabSortCreated: "按创建时间",
+ tabSortTitle: "按标签页名称",
+ tabOrganizationDescription: "分组和排序只影响显示顺序;仅手动排序时支持拖拽。",
iconTheme: "图标与 Logo 样式",
iconThemeDefault: "默认",
iconThemeDefaultDescription: "使用彩色 DBX 应用图标。",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index b4f53325ce..56cd64da80 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -5353,6 +5353,21 @@ export default withEnglishFallback({
tabLayoutScrollDescription: "標籤頁超出一行時水平滾動顯示。",
tabLayoutWrap: "多行平鋪",
tabLayoutWrapDescription: "標籤頁超出一行時自動換行,平鋪顯示所有標籤。",
+ tabPlacement: "標籤欄位置",
+ tabPlacementTop: "上方",
+ tabPlacementBottom: "下方",
+ tabPlacementLeft: "左側",
+ tabPlacementRight: "右側",
+ tabPlacementDescription: "側邊標籤欄使用垂直捲動;多行平鋪僅適用於上方或下方。",
+ tabGroup: "分組方式",
+ tabGroupNone: "不分組",
+ tabGroupDatabaseType: "依資料庫類型分組",
+ tabGroupConnection: "依連線分組",
+ tabSort: "排序方式",
+ tabSortManual: "手動排序",
+ tabSortCreated: "依建立時間",
+ tabSortTitle: "依標籤頁名稱",
+ tabOrganizationDescription: "分組和排序只影響顯示順序;只有手動排序時可拖曳。",
iconTheme: "圖示與 Logo 樣式",
iconThemeDefault: "預設",
iconThemeDefaultDescription: "使用彩色 DBX 應用程式圖示。",
diff --git a/apps/desktop/src/lib/app/openTabsPersistence.ts b/apps/desktop/src/lib/app/openTabsPersistence.ts
index c93bd6827f..84d73a8a20 100644
--- a/apps/desktop/src/lib/app/openTabsPersistence.ts
+++ b/apps/desktop/src/lib/app/openTabsPersistence.ts
@@ -17,6 +17,7 @@ export interface SavedQueryResultRun {
export interface SavedOpenTab {
id: string;
+ createdAt?: number;
title: string;
customTitle?: boolean;
connectionId: string;
@@ -91,6 +92,7 @@ function restoredOriginalSql(tab: SavedOpenTab, mode: QueryTab["mode"], sql: str
export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
return tabs.map((tab) => ({
id: tab.id,
+ ...(typeof tab.createdAt === "number" ? { createdAt: tab.createdAt } : {}),
title: tab.title,
...(tab.customTitle ? { customTitle: true } : {}),
connectionId: tab.connectionId,
diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts
index 0842ab9b39..5ff48f990c 100644
--- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts
+++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts
@@ -34,6 +34,9 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"savedSqlOpenTargetMode",
"appLayout",
"tabLayout",
+ "tabPlacement",
+ "tabGroupMode",
+ "tabSortMode",
"showColumnCommentsInHeader",
"showColumnTypesInHeader",
"dataGridShowTransposeFieldMetadata",
diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts
index 598a88650f..c559cad117 100644
--- a/apps/desktop/src/stores/queryStore.ts
+++ b/apps/desktop/src/stores/queryStore.ts
@@ -733,6 +733,16 @@ export const useQueryStore = defineStore("query", () => {
const t = getI18nT();
const settingsStore = useSettingsStore();
const tabs = ref
([]);
+ // New tabs are created by many feature-specific entry points. Tracking id
+ // changes here gives every one a stable timestamp without duplicating the
+ // field at each call site; restored legacy tabs retain their saved order.
+ function ensureTabCreationTimes() {
+ const migrationBase = Date.now() - tabs.value.length;
+ tabs.value.forEach((tab, index) => {
+ if (typeof tab.createdAt !== "number" || !Number.isFinite(tab.createdAt)) tab.createdAt = migrationBase + index;
+ });
+ }
+ watch(() => tabs.value.map((tab) => tab.id), ensureTabCreationTimes, { immediate: true, flush: "sync" });
// Default auto-commit for a fresh query tab follows the user's global
// "默认事务提交方式" setting (Settings > Editor), not a hard-coded constant.
const defaultAutoCommitForDbTypeWithSetting = (dbType?: string) => defaultAutoCommitForDbType(dbType, settingsStore.editorSettings.defaultTransactionMode);
@@ -2858,20 +2868,8 @@ export const useQueryStore = defineStore("query", () => {
);
}
- function closeLeftTabs(id: string, onComplete?: () => void) {
- const target = tabs.value.find((tab) => tab.id === id);
- if (!target) return;
-
- const groupedTabs = tabs.value.filter((tab) => Boolean(tab.pinned) === Boolean(target.pinned));
- const targetIndex = groupedTabs.findIndex((tab) => tab.id === id);
- const ids = groupedTabs.slice(0, targetIndex).map((tab) => tab.id);
- if (ids.length === 0) {
- onComplete?.();
- return;
- }
-
- const finalActiveTabId = activeTabId.value && !ids.includes(activeTabId.value) ? activeTabId.value : id;
- beginBatchClose(ids, finalActiveTabId, onComplete);
+ function closeTabsByIds(ids: Iterable, finalActiveTabId?: string | null, onComplete?: () => void) {
+ beginBatchClose([...ids], finalActiveTabId, onComplete);
}
function closeRightTabs(id: string, onComplete?: () => void) {
@@ -2887,7 +2885,7 @@ export const useQueryStore = defineStore("query", () => {
}
const finalActiveTabId = activeTabId.value && !ids.includes(activeTabId.value) ? activeTabId.value : id;
- beginBatchClose(ids, finalActiveTabId, onComplete);
+ closeTabsByIds(ids, finalActiveTabId, onComplete);
}
function finalActiveTabAfterClosing(ids: string[]) {
@@ -6852,7 +6850,7 @@ export const useQueryStore = defineStore("query", () => {
discardTabChanges,
requestAppCloseConfirmation,
closeOtherTabs,
- closeLeftTabs,
+ closeTabsByIds,
closeRightTabs,
closeOtherRegularTabs,
closeRegularTabs,
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index 68240a142e..25b68404de 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -476,6 +476,12 @@ 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 TAB_PLACEMENTS = ["top", "bottom", "left", "right"] as const;
+export type TabPlacement = (typeof TAB_PLACEMENTS)[number];
+const TAB_GROUP_MODES = ["none", "database-type", "connection"] as const;
+export type TabGroupMode = (typeof TAB_GROUP_MODES)[number];
+const TAB_SORT_MODES = ["manual", "created-asc", "title-asc"] as const;
+export type TabSortMode = (typeof TAB_SORT_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;
@@ -607,6 +613,9 @@ export interface EditorSettings {
savedSqlOpenTargetMode: SavedSqlOpenTargetMode;
compactTabTitle: boolean;
tabLayout: TabLayoutMode;
+ tabPlacement: TabPlacement;
+ tabGroupMode: TabGroupMode;
+ tabSortMode: TabSortMode;
appLayout: "separated" | "classic";
pageSize: number;
tableOpenPageSize: number;
@@ -829,6 +838,9 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
savedSqlOpenTargetMode: "saved",
compactTabTitle: false,
tabLayout: "scroll",
+ tabPlacement: "top",
+ tabGroupMode: "none",
+ tabSortMode: "manual",
appLayout: "classic",
pageSize: 100,
tableOpenPageSize: 100,
@@ -970,6 +982,18 @@ function normalizeTabLayout(value: unknown): TabLayoutMode {
return TAB_LAYOUT_MODES.includes(value as TabLayoutMode) ? (value as TabLayoutMode) : DEFAULT_EDITOR_SETTINGS.tabLayout;
}
+function normalizeTabPlacement(value: unknown): TabPlacement {
+ return TAB_PLACEMENTS.includes(value as TabPlacement) ? (value as TabPlacement) : DEFAULT_EDITOR_SETTINGS.tabPlacement;
+}
+
+function normalizeTabGroupMode(value: unknown): TabGroupMode {
+ return TAB_GROUP_MODES.includes(value as TabGroupMode) ? (value as TabGroupMode) : DEFAULT_EDITOR_SETTINGS.tabGroupMode;
+}
+
+function normalizeTabSortMode(value: unknown): TabSortMode {
+ return TAB_SORT_MODES.includes(value as TabSortMode) ? (value as TabSortMode) : DEFAULT_EDITOR_SETTINGS.tabSortMode;
+}
+
function normalizeCellDetailPanelLayout(value: unknown): CellDetailPanelLayout {
return CELL_DETAIL_PANEL_LAYOUTS.includes(value as CellDetailPanelLayout) ? (value as CellDetailPanelLayout) : DEFAULT_EDITOR_SETTINGS.cellDetailPanelLayout;
}
@@ -1240,6 +1264,9 @@ export function normalizeEditorSettings(settings: Partial, exist
savedSqlOpenTargetMode: settings.savedSqlOpenTargetMode === "current" ? "current" : DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode,
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
tabLayout: normalizeTabLayout(settings.tabLayout),
+ tabPlacement: normalizeTabPlacement(settings.tabPlacement),
+ tabGroupMode: normalizeTabGroupMode(settings.tabGroupMode),
+ tabSortMode: normalizeTabSortMode(settings.tabSortMode),
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
pageSize: normalizeResultPageSize(settings.pageSize),
tableOpenPageSize: normalizeResultPageSize(settings.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize),
@@ -1864,6 +1891,9 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.savedSqlOpenTargetMode !== undefined) editorSettings.value.savedSqlOpenTargetMode = partial.savedSqlOpenTargetMode === "current" ? "current" : "saved";
if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle;
if (partial.tabLayout !== undefined) editorSettings.value.tabLayout = normalizeTabLayout(partial.tabLayout);
+ if (partial.tabPlacement !== undefined) editorSettings.value.tabPlacement = normalizeTabPlacement(partial.tabPlacement);
+ if (partial.tabGroupMode !== undefined) editorSettings.value.tabGroupMode = normalizeTabGroupMode(partial.tabGroupMode);
+ if (partial.tabSortMode !== undefined) editorSettings.value.tabSortMode = normalizeTabSortMode(partial.tabSortMode);
if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout;
if (partial.pageSize !== undefined) editorSettings.value.pageSize = normalizeResultPageSize(partial.pageSize);
if (partial.tableOpenPageSize !== undefined) editorSettings.value.tableOpenPageSize = normalizeResultPageSize(partial.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize);
diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts
index 99a49d4e0e..b6ba078266 100644
--- a/apps/desktop/src/types/database.ts
+++ b/apps/desktop/src/types/database.ts
@@ -1115,6 +1115,8 @@ export interface QueryPageJumpProgress {
export interface QueryTab {
id: string;
+ /** Stable creation time used when tabs are displayed in creation order. */
+ createdAt?: number;
title: string;
customTitle?: boolean;
/** Force the editor to word-wrap regardless of the global setting, e.g. for auto-generated single-line templates. */