feat(tabs): add adjacent data tab opening option

Closes #3723
This commit is contained in:
t8y2 committed 2026-08-15 00:52:24 +00:00
1 parent fbaf556f3a
commit 4768dbb99a
19 files changed
+192 -7

No files matched your search

@@ -399,6 +399,7 @@ const editSidebarOpenDatabaseOnSingleClick = ref(settingsStore.editorSettings.si
const editOpenTabsRestoreMode = ref<OpenTabsRestoreMode>(settingsStore.editorSettings.openTabsRestoreMode);
const editDisconnectTabHandlingMode = ref<DisconnectTabHandlingMode>(settingsStore.editorSettings.disconnectTabHandlingMode);
const editDataTabReuseMode = ref<DataTabReuseMode>(settingsStore.editorSettings.dataTabReuseMode);
const editOpenDataTabsNextToActive = ref(settingsStore.editorSettings.openDataTabsNextToActive);
const editPrefillNewQueryWithSelect = ref(settingsStore.editorSettings.prefillNewQueryWithSelect);
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
@@ -520,6 +521,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
openTabsRestoreMode: editOpenTabsRestoreMode.value,
disconnectTabHandlingMode: editDisconnectTabHandlingMode.value,
dataTabReuseMode: editDataTabReuseMode.value,
openDataTabsNextToActive: editOpenDataTabsNextToActive.value,
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect.value,
updateNotificationsEnabled: editUpdateNotificationsEnabled.value,
sidebarObjectInfoMode: editSidebarObjectInfoMode.value,
@@ -799,6 +801,7 @@ function syncEditorSettingsDraftFromStore() {
editOpenTabsRestoreMode.value = settingsStore.editorSettings.openTabsRestoreMode;
editDisconnectTabHandlingMode.value = settingsStore.editorSettings.disconnectTabHandlingMode;
editDataTabReuseMode.value = settingsStore.editorSettings.dataTabReuseMode;
editOpenDataTabsNextToActive.value = settingsStore.editorSettings.openDataTabsNextToActive;
editPrefillNewQueryWithSelect.value = settingsStore.editorSettings.prefillNewQueryWithSelect;
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
@@ -1014,6 +1017,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
editClickTableNavigationTarget.value = DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget;
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
@@ -1123,6 +1127,7 @@ function resetAllDefaults() {
editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
@@ -4564,6 +4569,15 @@ onUnmounted(() => {
</Button>
</div>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="open-data-tabs-next-to-active">{{ t("settings.openDataTabsNextToActive") }}</Label>
<HelpTooltip :label="t('settings.openDataTabsNextToActive')">
{{ t("settings.openDataTabsNextToActiveDescription") }}
</HelpTooltip>
</div>
<Switch id="open-data-tabs-next-to-active" v-model="editOpenDataTabsNextToActive" />
</div>
<div class="space-y-2">
<Label>{{ t("settings.sidebarObjectDisplay") }}</Label>
<div class="grid grid-cols-2 gap-2">
@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
activeTabId: null as string | null,
cachedMetadata: undefined as unknown,
dataTabReuseMode: "same-table" as DataTabReuseMode,
openDataTabsNextToActive: false,
ensureConnected: vi.fn(),
executeTabSql: vi.fn(),
loadTableMetadata: vi.fn(),
@@ -32,7 +33,7 @@ vi.mock("@/stores/queryStore", () => ({
get activeTabId() {
return mocks.activeTabId;
},
createTab: (connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string, _initialSql?: string, catalog?: string, options: { forceNew?: boolean } = {}) => {
createTab: (connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string, _initialSql?: string, catalog?: string, options: { forceNew?: boolean; insertAfterActive?: boolean } = {}) => {
if (!options.forceNew) {
const existing = mocks.tabs.find((tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || "") && (tab.catalog || "") === (catalog || ""));
if (existing) {
@@ -54,7 +55,9 @@ vi.mock("@/stores/queryStore", () => ({
isCancelling: false,
isExplaining: false,
} as QueryTab;
mocks.tabs.push(tab);
const activeIndex = options.insertAfterActive ? mocks.tabs.findIndex((item) => item.id === mocks.activeTabId) : -1;
if (activeIndex >= 0) mocks.tabs.splice(activeIndex + 1, 0, tab);
else mocks.tabs.push(tab);
mocks.activeTabId = tab.id;
return tab.id;
},
@@ -87,7 +90,7 @@ vi.mock("@/stores/queryStore", () => ({
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({ editorSettings: { dataTabReuseMode: mocks.dataTabReuseMode, pageSize: 100 } }),
useSettingsStore: () => ({ editorSettings: { dataTabReuseMode: mocks.dataTabReuseMode, openDataTabsNextToActive: mocks.openDataTabsNextToActive, pageSize: 100 } }),
}));
vi.mock("@/lib/database/jdbcDialect", () => ({
@@ -141,6 +144,7 @@ describe("useSidebarDataOpenRuntime", () => {
mocks.activeTabId = null;
mocks.cachedMetadata = undefined;
mocks.dataTabReuseMode = "same-table";
mocks.openDataTabsNextToActive = false;
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.buildTableSelectSql.mockResolvedValue("SELECT * FROM users");
mocks.executeTabSql.mockImplementation(async () => {
@@ -174,7 +178,24 @@ describe("useSidebarDataOpenRuntime", () => {
expect(mocks.tabs).toHaveLength(2);
});
it("keeps adjacent placement for every tab created in always-new mode", async () => {
mocks.dataTabReuseMode = "always-new";
mocks.openDataTabsNextToActive = true;
mocks.tabs.push(
{ id: "query-1", connectionId: "connection-1", database: "app", title: "Query 1", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
{ id: "query-2", connectionId: "connection-1", database: "app", title: "Query 2", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
);
mocks.activeTabId = "query-1";
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["Query 1", "users", "users", "Query 2"]);
});
it("reuses a sidebar tab for the same table in same-table mode", async () => {
mocks.openDataTabsNextToActive = true;
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData(tableNode);
@@ -191,8 +212,34 @@ describe("useSidebarDataOpenRuntime", () => {
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["users", "orders"]);
});
it("opens a new table data tab next to the active tab when enabled", async () => {
mocks.openDataTabsNextToActive = true;
mocks.tabs.push(
{ id: "query-1", connectionId: "connection-1", database: "app", title: "Query 1", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
{ id: "query-2", connectionId: "connection-1", database: "app", title: "Query 2", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
);
mocks.activeTabId = "query-1";
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["Query 1", "users", "Query 2"]);
});
it("keeps appending new table data tabs when adjacent opening is disabled", async () => {
mocks.tabs.push(
{ id: "query-1", connectionId: "connection-1", database: "app", title: "Query 1", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
{ id: "query-2", connectionId: "connection-1", database: "app", title: "Query 2", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
);
mocks.activeTabId = "query-1";
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["Query 1", "Query 2", "users"]);
});
it("reuses the active safe data tab for a different table in active-tab mode", async () => {
mocks.dataTabReuseMode = "active-tab";
mocks.openDataTabsNextToActive = true;
const ordersNode = { ...tableNode, id: "table-orders", label: "orders" };
mocks.loadTableMetadata.mockImplementation(async (request: { database: string; schema?: string; tableName: string; tableType?: string }) => ({
metadata: {
@@ -272,6 +319,20 @@ describe("useSidebarDataOpenRuntime", () => {
expect(mocks.tabs).toHaveLength(2);
});
it("opens a new HBase data tab next to the active tab when enabled", async () => {
mocks.databaseType = "hbase";
mocks.openDataTabsNextToActive = true;
mocks.tabs.push(
{ id: "query-1", connectionId: "connection-1", database: "app", title: "Query 1", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
{ id: "query-2", connectionId: "connection-1", database: "app", title: "Query 2", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
);
mocks.activeTabId = "query-1";
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["Query 1", "users", "Query 2"]);
});
it("starts cold-cache OceanBase metadata before the table query", async () => {
await useSidebarDataOpenRuntime().openData(tableNode);
@@ -34,7 +34,10 @@ export function useSidebarDataOpenRuntime() {
const reuseMode = options.reuseMode ?? settingsStore.editorSettings.dataTabReuseMode;
if (config?.db_type === "hbase") {
await connectionStore.ensureConnected(node.connectionId);
const tabId = queryStore.createTab(node.connectionId, node.database, node.label, "hbase", undefined, node.label, undefined, { forceNew: openMode === "new-tab" || reuseMode === "always-new" });
const tabId = queryStore.createTab(node.connectionId, node.database, node.label, "hbase", undefined, node.label, undefined, {
forceNew: openMode === "new-tab" || reuseMode === "always-new",
insertAfterActive: settingsStore.editorSettings.openDataTabsNextToActive,
});
queryStore.updateSql(tabId, node.label);
return;
}
@@ -174,7 +177,10 @@ export function useSidebarDataOpenRuntime() {
resetReusedDataTabState(existingDataTabCandidate.tab);
return existingDataTabCandidate.tab.id;
}
return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema, undefined, node.catalog, { forceNew: true });
return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema, undefined, node.catalog, {
forceNew: true,
insertAfterActive: settingsStore.editorSettings.openDataTabsNextToActive,
});
})();
openDataLog("info", "tab-created", { traceId, tabId, elapsed: elapsed() });
logPhase("tab-created", { tabId });
+2
View File
@@ -5812,6 +5812,8 @@ export default {
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Keep related tabs, SQL text, and current results without extra cleanup.",
reuseDataTab: "Reuse data tab",
reuseDataTabDescription: "Choose how opening a table uses data tabs.",
openDataTabsNextToActive: "Open new data tabs next to the active tab",
openDataTabsNextToActiveDescription: "Place newly created table data tabs immediately after the active tab.",
dataTabReuseAlwaysNew: "Always open a new tab",
dataTabReuseAlwaysNewDescription: "Create a new data tab every time, even when the table is already open.",
dataTabReuseSameTable: "Reuse the same table",
+2
View File
@@ -5556,6 +5556,8 @@ export default withEnglishFallback({
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Conserva las pestañas relacionadas, el texto SQL y los resultados actuales sin limpieza adicional.",
reuseDataTab: "Reutilizar pestaña de datos",
reuseDataTabDescription: "Elige cómo se usan las pestañas de datos al abrir una tabla.",
openDataTabsNextToActive: "Abrir nuevas pestañas de datos junto a la activa",
openDataTabsNextToActiveDescription: "Coloca las nuevas pestañas de datos de tablas justo después de la pestaña activa.",
dataTabReuseAlwaysNew: "Abrir siempre una pestaña nueva",
dataTabReuseAlwaysNewDescription: "Crea una pestaña de datos nueva cada vez, aunque la tabla ya esté abierta.",
dataTabReuseSameTable: "Reutilizar solo la misma tabla",
+2
View File
@@ -5556,6 +5556,8 @@ export default withEnglishFallback({
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Mantieni le schede correlate, il testo SQL e i risultati correnti senza ulteriore pulizia.",
reuseDataTab: "Riusa scheda dati",
reuseDataTabDescription: "Scegli come usare le schede dati quando apri una tabella.",
openDataTabsNextToActive: "Apri le nuove schede dati accanto a quella attiva",
openDataTabsNextToActiveDescription: "Posiziona le nuove schede dati delle tabelle subito dopo la scheda attiva.",
dataTabReuseAlwaysNew: "Apri sempre una nuova scheda",
dataTabReuseAlwaysNewDescription: "Crea ogni volta una nuova scheda dati, anche se la tabella è già aperta.",
dataTabReuseSameTable: "Riusa solo la stessa tabella",
+2
View File
@@ -5583,6 +5583,8 @@ export default withEnglishFallback({
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "関連タブ、SQLテキスト、現在の結果を追加のクリーンアップなしで保持します。",
reuseDataTab: "データタブを再利用",
reuseDataTabDescription: "テーブルを開くときのデータタブの使い方を選択します。",
openDataTabsNextToActive: "新しいデータタブをアクティブタブの隣に開く",
openDataTabsNextToActiveDescription: "新しく作成したテーブルのデータタブをアクティブタブの直後に配置します。",
dataTabReuseAlwaysNew: "常に新しいタブを開く",
dataTabReuseAlwaysNewDescription: "同じテーブルが開いていても、毎回新しいデータタブを作成します。",
dataTabReuseSameTable: "同じテーブルだけ再利用",
+2
View File
@@ -5292,6 +5292,8 @@ export default withEnglishFallback({
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "관련 탭, SQL 텍스트, 현재 결과를 추가 정리 없이 유지합니다.",
reuseDataTab: "데이터 탭 재사용",
reuseDataTabDescription: "테이블을 열 때 데이터 탭을 사용하는 방식을 선택합니다.",
openDataTabsNextToActive: "새 데이터 탭을 활성 탭 옆에 열기",
openDataTabsNextToActiveDescription: "새로 만든 테이블 데이터 탭을 활성 탭 바로 뒤에 배치합니다.",
dataTabReuseAlwaysNew: "항상 새 탭 열기",
dataTabReuseAlwaysNewDescription: "테이블이 이미 열려 있어도 매번 새 데이터 탭을 만듭니다.",
dataTabReuseSameTable: "같은 테이블만 재사용",
+2
View File
@@ -5558,6 +5558,8 @@ export default withEnglishFallback({
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Manter abas relacionadas, texto SQL e resultados atuais sem limpeza adicional.",
reuseDataTab: "Reutilizar aba de dados",
reuseDataTabDescription: "Escolha como as abas de dados são usadas ao abrir uma tabela.",
openDataTabsNextToActive: "Abrir novas abas de dados ao lado da aba ativa",
openDataTabsNextToActiveDescription: "Coloca as novas abas de dados de tabelas logo após a aba ativa.",
dataTabReuseAlwaysNew: "Sempre abrir uma nova aba",
dataTabReuseAlwaysNewDescription: "Cria uma nova aba de dados todas as vezes, mesmo que a tabela já esteja aberta.",
dataTabReuseSameTable: "Reutilizar apenas a mesma tabela",
+2
View File
@@ -5799,6 +5799,8 @@ export default withEnglishFallback({
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相关页签、SQL 文本和当前结果,不做额外处理。",
reuseDataTab: "复用数据标签页",
reuseDataTabDescription: "选择打开表时如何使用数据标签页。",
openDataTabsNextToActive: "在当前标签页旁打开新数据标签页",
openDataTabsNextToActiveDescription: "将新建的表数据标签页放在当前标签页之后。",
dataTabReuseAlwaysNew: "始终新建标签页",
dataTabReuseAlwaysNewDescription: "每次打开表都新建标签页,即使该表已经打开。",
dataTabReuseSameTable: "仅复用同一张表",
+2
View File
@@ -4893,6 +4893,8 @@ export default withEnglishFallback({
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相關分頁、SQL 文字與目前結果,不另外做清理。",
reuseDataTab: "重複使用資料分頁",
reuseDataTabDescription: "選擇開啟資料表時如何使用資料分頁。",
openDataTabsNextToActive: "在目前分頁旁開啟新的資料分頁",
openDataTabsNextToActiveDescription: "將新建的資料表資料分頁放在目前分頁之後。",
dataTabReuseAlwaysNew: "一律開啟新分頁",
dataTabReuseAlwaysNewDescription: "每次開啟資料表都建立新分頁,即使該資料表已經開啟。",
dataTabReuseSameTable: "僅重複使用同一資料表",
@@ -61,6 +61,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataTabReuseMode");
});
it("includes adjacent data-tab opening", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("openDataTabsNextToActive");
});
it("includes data grid type colors", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("colorizeDataGridCellTypes");
});
@@ -208,6 +212,14 @@ describe("editorSettingsDraftChanged", () => {
expect(editorSettingsDraftChanged(draft, base)).toBe(true);
});
it("detects adjacent data-tab opening changes", () => {
const settings = makeSettings({ openDataTabsNextToActive: false });
const draft = editorSettingsDraftFromSettings(settings);
const base = editorSettingsDraftFromSettings(settings);
draft.openDataTabsNextToActive = true;
expect(editorSettingsDraftChanged(draft, base)).toBe(true);
});
it("detects completionTriggerMode change", () => {
const settings = makeSettings({ completionTriggerMode: "positional" } as Partial<EditorSettings>);
const draft = editorSettingsDraftFromSettings(settings);
@@ -266,6 +278,14 @@ describe("editorSettingsPatchFromDraft", () => {
expect(editorSettingsPatchFromDraft(draft, base).dataTabReuseMode).toBe("always-new");
});
it("includes adjacent data-tab opening when changed", () => {
const settings = makeSettings({ openDataTabsNextToActive: false });
const draft = editorSettingsDraftFromSettings(settings);
const base = editorSettingsDraftFromSettings(settings);
draft.openDataTabsNextToActive = true;
expect(editorSettingsPatchFromDraft(draft, base).openDataTabsNextToActive).toBe(true);
});
it("includes completionTriggerMode in patch when changed", () => {
const settings = makeSettings({ completionTriggerMode: "positional" } as Partial<EditorSettings>);
const draft = editorSettingsDraftFromSettings(settings);
@@ -0,0 +1,13 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const settingsDialogSource = readFileSync(new URL("../../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
describe("openDataTabsNextToActive settings control", () => {
it("binds the navigation switch through apply and both reset paths", () => {
expect(settingsDialogSource).toContain("const editOpenDataTabsNextToActive = ref(settingsStore.editorSettings.openDataTabsNextToActive)");
expect(settingsDialogSource).toContain("openDataTabsNextToActive: editOpenDataTabsNextToActive.value");
expect(settingsDialogSource.match(/editOpenDataTabsNextToActive\.value = DEFAULT_EDITOR_SETTINGS\.openDataTabsNextToActive/g)).toHaveLength(2);
expect(settingsDialogSource).toContain('id="open-data-tabs-next-to-active" v-model="editOpenDataTabsNextToActive"');
});
});
@@ -58,6 +58,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"openTabsRestoreMode",
"disconnectTabHandlingMode",
"dataTabReuseMode",
"openDataTabsNextToActive",
"prefillNewQueryWithSelect",
"updateNotificationsEnabled",
"sidebarObjectInfoMode",
@@ -159,6 +159,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "navigation-sidebar", category: "navigation", titleKey: "settings.sidebarActivation", targetId: "navigation" },
{ id: "navigation-routine-source", category: "navigation", titleKey: "settings.routineSourceOpenMode", descriptionKey: "settings.routineSourceOpenModeDescription", targetId: "navigation" },
{ id: "navigation-reuse-data-tab", category: "navigation", titleKey: "settings.reuseDataTab", descriptionKey: "settings.reuseDataTabDescription", targetId: "navigation" },
{ id: "navigation-open-data-tabs-next-to-active", category: "navigation", titleKey: "settings.openDataTabsNextToActive", descriptionKey: "settings.openDataTabsNextToActiveDescription", targetId: "navigation" },
{ id: "navigation-object-display", category: "navigation", titleKey: "settings.sidebarObjectDisplay", targetId: "navigation" },
{ id: "navigation-object-info", category: "navigation", titleKey: "settings.sidebarObjectInfoMode", descriptionKey: "settings.sidebarObjectInfoModeDescription", targetId: "navigation" },
{ id: "navigation-table-search", category: "navigation", titleKey: "settings.sidebarTableSearchEnabled", descriptionKey: "settings.sidebarTableSearchEnabledDescription", targetId: "navigation" },
@@ -59,6 +59,28 @@ describe("queryStore switchTab", () => {
expect(queryStore.activeTabId).toBe(tab2Id);
});
it("inserts a tab after the active tab only when explicitly requested", async () => {
const queryStore = useQueryStore();
const firstId = queryStore.createTab("pg-1", "app", "Query 1", "query");
const trailingId = queryStore.createTab("pg-1", "app", "Query 2", "query");
queryStore.switchTab(firstId);
const dataId = queryStore.createTab("pg-1", "app", "users", "data", "public", undefined, undefined, { forceNew: true, insertAfterActive: true });
const appendedId = queryStore.createTab("pg-1", "app", "Query 3", "query", undefined, undefined, undefined, { forceNew: true });
expect(queryStore.tabs.map((tab) => tab.id)).toEqual([firstId, dataId, trailingId, appendedId]);
});
it("appends when adjacent insertion has no valid active tab", async () => {
const queryStore = useQueryStore();
const firstId = queryStore.createTab("pg-1", "app", "Query 1", "query");
queryStore.activeTabId = "missing-tab";
const dataId = queryStore.createTab("pg-1", "app", "users", "data", "public", undefined, undefined, { forceNew: true, insertAfterActive: true });
expect(queryStore.tabs.map((tab) => tab.id)).toEqual([firstId, dataId]);
});
it("deactivates settings page when reopening an existing special tab", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const { useSettingsStore } = await import("@/stores/settingsStore");
+25
View File
@@ -28,6 +28,14 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ reuseDataTab: true } as any).dataTabReuseMode).toBe("same-table");
});
it("keeps adjacent data-tab opening disabled unless explicitly enabled", () => {
expect(normalizeEditorSettings({}).openDataTabsNextToActive).toBe(false);
expect(normalizeEditorSettings({ openDataTabsNextToActive: true }).openDataTabsNextToActive).toBe(true);
expect(normalizeEditorSettings({ openDataTabsNextToActive: false }).openDataTabsNextToActive).toBe(false);
expect(normalizeEditorSettings({ openDataTabsNextToActive: "true" } as any).openDataTabsNextToActive).toBe(false);
expect(normalizeEditorSettings({ openDataTabsNextToActive: null } as any).openDataTabsNextToActive).toBe(false);
});
it("defaults and bounds the regular expression match limit", () => {
expect(normalizeEditorSettings({}).regexMaxMatchCount).toBe(1000);
expect(normalizeEditorSettings({ regexMaxMatchCount: 2500 }).regexMaxMatchCount).toBe(2500);
@@ -663,6 +671,23 @@ describe("settingsStore persisted settings initialization", () => {
);
});
it("loads and persists adjacent data-tab opening", async () => {
const loadEditorSettings = vi.fn().mockResolvedValue({ openDataTabsNextToActive: true });
const saveEditorSettings = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
await store.initEditorSettings();
expect(store.editorSettings.openDataTabsNextToActive).toBe(true);
await store.updateEditorSettingsAndPersist({ openDataTabsNextToActive: false });
expect(store.editorSettings.openDataTabsNextToActive).toBe(false);
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ openDataTabsNextToActive: false }));
});
it("shares concurrent initialization and applies startup changes after saved settings load", async () => {
let resolveLoad!: (value: unknown) => void;
const loadEditorSettings = vi.fn(
+4 -2
View File
@@ -1620,7 +1620,7 @@ export const useQueryStore = defineStore("query", () => {
return tabs.value.find((tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || "") && (tab.catalog || "") === (catalog || ""));
}
function createTab(connectionId: string, database: string, title?: string, mode: QueryTab["mode"] = "query", schema?: string, initialSql?: string, catalog?: string, options: { forceNew?: boolean; activate?: boolean; forceWordWrap?: boolean } = {}) {
function createTab(connectionId: string, database: string, title?: string, mode: QueryTab["mode"] = "query", schema?: string, initialSql?: string, catalog?: string, options: { forceNew?: boolean; activate?: boolean; forceWordWrap?: boolean; insertAfterActive?: boolean } = {}) {
if (title && !options.forceNew) {
const existing = findTabByIdentity(connectionId, database, title, mode, schema, catalog);
if (existing) {
@@ -1646,7 +1646,9 @@ export const useQueryStore = defineStore("query", () => {
mode,
};
if (mode === "query") tab.originalSql = initialSql ?? "";
tabs.value.push(tab);
const activeIndex = options.insertAfterActive ? tabs.value.findIndex((item) => item.id === activeTabId.value) : -1;
if (activeIndex >= 0) tabs.value.splice(activeIndex + 1, 0, tab);
else tabs.value.push(tab);
if (options.activate !== false) activeTabId.value = id;
return id;
}
+4
View File
@@ -580,6 +580,7 @@ export interface EditorSettings {
openTabsRestoreMode: OpenTabsRestoreMode;
disconnectTabHandlingMode: DisconnectTabHandlingMode;
dataTabReuseMode: DataTabReuseMode;
openDataTabsNextToActive: boolean;
prefillNewQueryWithSelect: boolean;
updateNotificationsEnabled: boolean;
sidebarHiddenTablePrefixes: string[];
@@ -778,6 +779,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
openTabsRestoreMode: "all",
disconnectTabHandlingMode: "close-tabs",
dataTabReuseMode: DEFAULT_DATA_TAB_REUSE_MODE,
openDataTabsNextToActive: false,
prefillNewQueryWithSelect: true,
updateNotificationsEnabled: true,
sidebarHiddenTablePrefixes: [],
@@ -1142,6 +1144,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
}
).reuseDataTab,
),
openDataTabsNextToActive: typeof settings.openDataTabsNextToActive === "boolean" ? settings.openDataTabsNextToActive : DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive,
prefillNewQueryWithSelect: typeof settings.prefillNewQueryWithSelect === "boolean" ? settings.prefillNewQueryWithSelect : DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect,
updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
@@ -1702,6 +1705,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode);
if (partial.disconnectTabHandlingMode !== undefined) editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode(partial.disconnectTabHandlingMode);
if (partial.dataTabReuseMode !== undefined) editorSettings.value.dataTabReuseMode = normalizeDataTabReuseMode(partial.dataTabReuseMode);
if (partial.openDataTabsNextToActive !== undefined) editorSettings.value.openDataTabsNextToActive = partial.openDataTabsNextToActive === true;
if (partial.prefillNewQueryWithSelect !== undefined) editorSettings.value.prefillNewQueryWithSelect = partial.prefillNewQueryWithSelect;
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);