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 });
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5583,6 +5583,8 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "関連タブ、SQLテキスト、現在の結果を追加のクリーンアップなしで保持します。",
|
||||
reuseDataTab: "データタブを再利用",
|
||||
reuseDataTabDescription: "テーブルを開くときのデータタブの使い方を選択します。",
|
||||
openDataTabsNextToActive: "新しいデータタブをアクティブタブの隣に開く",
|
||||
openDataTabsNextToActiveDescription: "新しく作成したテーブルのデータタブをアクティブタブの直後に配置します。",
|
||||
dataTabReuseAlwaysNew: "常に新しいタブを開く",
|
||||
dataTabReuseAlwaysNewDescription: "同じテーブルが開いていても、毎回新しいデータタブを作成します。",
|
||||
dataTabReuseSameTable: "同じテーブルだけ再利用",
|
||||
|
||||
@@ -5292,6 +5292,8 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "관련 탭, SQL 텍스트, 현재 결과를 추가 정리 없이 유지합니다.",
|
||||
reuseDataTab: "데이터 탭 재사용",
|
||||
reuseDataTabDescription: "테이블을 열 때 데이터 탭을 사용하는 방식을 선택합니다.",
|
||||
openDataTabsNextToActive: "새 데이터 탭을 활성 탭 옆에 열기",
|
||||
openDataTabsNextToActiveDescription: "새로 만든 테이블 데이터 탭을 활성 탭 바로 뒤에 배치합니다.",
|
||||
dataTabReuseAlwaysNew: "항상 새 탭 열기",
|
||||
dataTabReuseAlwaysNewDescription: "테이블이 이미 열려 있어도 매번 새 데이터 탭을 만듭니다.",
|
||||
dataTabReuseSameTable: "같은 테이블만 재사용",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5799,6 +5799,8 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相关页签、SQL 文本和当前结果,不做额外处理。",
|
||||
reuseDataTab: "复用数据标签页",
|
||||
reuseDataTabDescription: "选择打开表时如何使用数据标签页。",
|
||||
openDataTabsNextToActive: "在当前标签页旁打开新数据标签页",
|
||||
openDataTabsNextToActiveDescription: "将新建的表数据标签页放在当前标签页之后。",
|
||||
dataTabReuseAlwaysNew: "始终新建标签页",
|
||||
dataTabReuseAlwaysNewDescription: "每次打开表都新建标签页,即使该表已经打开。",
|
||||
dataTabReuseSameTable: "仅复用同一张表",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user