feat(tabs): default to compact tab titles via migration

This commit is contained in:
zipg
2026-10-16 00:03:40 +08:00
committed by GitHub
parent e302e2003b
commit b58ae49e46
4 changed files with 113 additions and 10 deletions
@@ -54,6 +54,9 @@ beforeEach(() => {
removeItem: vi.fn((key: string) => values.delete(key)),
});
setActivePinia(createPinia());
// Compact titles are the shipped default now (#12310); pin the flag so these
// assertions keep describing full titles and compact cases opt in themselves.
useSettingsStore().editorSettings.compactTabTitle = false;
});
function queryTab(overrides: Partial<QueryTab>): QueryTab {
+76
View File
@@ -24,12 +24,21 @@ import {
type RightSidebarPanelState,
transitionRightSidebarPanels,
WELCOME_PAGE_DEFAULT_VERSION,
COMPACT_TAB_TITLE_DEFAULT_VERSION,
} from "@/stores/settingsStore";
import type { AiConfigItem } from "@/types/ai";
import { DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION } from "@/lib/dataGrid/dataGridCopyExtractor";
import { DEFAULT_MONO_FONT_FAMILY } from "@/lib/app/appFonts";
describe("normalizeEditorSettings", () => {
it("defaults tab titles to the compact presentation while preserving an explicit opt-out", () => {
expect(DEFAULT_EDITOR_SETTINGS.compactTabTitle).toBe(true);
expect(normalizeEditorSettings({}).compactTabTitle).toBe(true);
expect(normalizeEditorSettings({ compactTabTitle: false }).compactTabTitle).toBe(true);
expect(normalizeEditorSettings({ compactTabTitle: false, compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION }).compactTabTitle).toBe(false);
expect(normalizeEditorSettings({ compactTabTitle: "false" } as any).compactTabTitle).toBe(true);
});
it("keeps empty-column hiding opt-in and independent from NULL-only hiding", () => {
expect(normalizeEditorSettings({})).toMatchObject({ dataGridHideNullColumns: false, dataGridHideEmptyColumns: false });
expect(normalizeEditorSettings({ dataGridHideNullColumns: true })).toMatchObject({ dataGridHideNullColumns: true, dataGridHideEmptyColumns: false });
@@ -70,6 +79,19 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ welcomePageMode: "workspace", welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION }).welcomePageMode).toBe("workspace");
});
it("defaults compact tab titles on and keeps an explicit choice packed with its version marker", () => {
expect(DEFAULT_EDITOR_SETTINGS.compactTabTitle).toBe(true);
// Fresh blobs never saw the old unwrapped default, so they take the marker and the new default.
expect(normalizeEditorSettings({})).toMatchObject({ compactTabTitle: true, compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION });
// A persisted `false` from the unwrapped-title builds carries no marker and migrates on.
expect(normalizeEditorSettings({ compactTabTitle: false }).compactTabTitle).toBe(true);
expect(normalizeEditorSettings({ compactTabTitle: false }).compactTabTitleDefaultVersion).toBe(COMPACT_TAB_TITLE_DEFAULT_VERSION);
// With the marker the value is the user's own and must survive untouched, including `false`.
expect(normalizeEditorSettings({ compactTabTitle: false, compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION }).compactTabTitle).toBe(false);
expect(normalizeEditorSettings({ compactTabTitle: true, compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION }).compactTabTitle).toBe(true);
expect(normalizeEditorSettings({ compactTabTitle: "yes" as any, compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION }).compactTabTitle).toBe(false);
});
it("defaults and sanitizes AI conversation typography independently", () => {
expect(normalizeEditorSettings({})).toMatchObject({ aiFontFamily: "", aiFontSize: 12 });
expect(
@@ -1629,6 +1651,57 @@ describe("settingsStore persisted settings initialization", () => {
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ welcomePageMode: "intro", welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION }));
});
it("migrates a persisted off compact tab title to the new default and keeps later explicit choices", async () => {
const loadEditorSettings = vi.fn().mockResolvedValueOnce({
compactTabTitle: false,
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
dataGridExtractorOptionsMigrationVersion: DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION,
});
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();
// The marker is absent, so `false` is the old default rather than a choice.
expect(store.editorSettings.compactTabTitle).toBe(true);
expect(store.editorSettings.compactTabTitleDefaultVersion).toBe(COMPACT_TAB_TITLE_DEFAULT_VERSION);
await vi.waitFor(() => expect(saveEditorSettings).toHaveBeenCalledOnce());
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ compactTabTitle: true, compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION }));
// Turning it back off must stick now that the marker is stamped.
store.updateEditorSettings({ compactTabTitle: false });
expect(store.editorSettings.compactTabTitle).toBe(false);
await vi.waitFor(() => expect(saveEditorSettings).toHaveBeenCalledTimes(2));
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ compactTabTitle: false, compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION }));
});
it("does not re-migrate an already stamped compact tab title", async () => {
const loadEditorSettings = vi.fn().mockResolvedValueOnce({
compactTabTitle: false,
compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION,
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
dataGridExtractorOptionsMigrationVersion: DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION,
});
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.compactTabTitle).toBe(false);
await vi.waitFor(() => expect(store.isEditorSettingsLoaded).toBe(true));
// Other migration flags in this stub may still trigger an eager save; the
// point is that the compact title is never flipped on a stamped value.
if (saveEditorSettings.mock.calls.length) {
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ compactTabTitle: false, compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION }));
}
});
it("persists table completion schema qualification updates", async () => {
const loadEditorSettings = vi.fn().mockResolvedValue({});
const saveEditorSettings = vi.fn().mockResolvedValue(undefined);
@@ -2041,6 +2114,7 @@ describe("settingsStore editor settings persistence", () => {
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION,
compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION,
});
const saveEditorSettings = vi.fn().mockRejectedValueOnce(new Error("save failed")).mockResolvedValueOnce(undefined);
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
@@ -2089,6 +2163,7 @@ describe("settingsStore editor settings persistence", () => {
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION,
compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION,
});
const saveEditorSettings = vi.fn().mockImplementationOnce(
() =>
@@ -2126,6 +2201,7 @@ describe("settingsStore editor settings persistence", () => {
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION,
compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION,
});
const saveEditorSettings = vi.fn().mockImplementationOnce(
() =>
+21 -4
View File
@@ -1003,6 +1003,7 @@ export interface EditorSettings {
welcomePageMode: WelcomePageMode;
welcomePageModeDefaultVersion: number;
compactTabTitle: boolean;
compactTabTitleDefaultVersion: number;
tabLayout: TabLayoutMode;
tabPlacement: TabPlacement;
colorizeConnectionTabs: boolean;
@@ -1311,6 +1312,11 @@ export const SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION = 1;
// v1: the welcome page default moved from "intro" back to "workspace"; persisted
// blobs from the intro-default builds lack this marker and are migrated.
export const WELCOME_PAGE_DEFAULT_VERSION = 1;
// v1: compact tab titles became the default. A stored `compactTabTitle: false`
// cannot be told apart from the old default, so this marker separates the two:
// blobs written by the unwrapped-title builds lack it and migrate to the new
// default, while an explicit toggle afterwards stamps the marker and wins.
export const COMPACT_TAB_TITLE_DEFAULT_VERSION = 1;
export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
fontFamily: DEFAULT_MONO_FONT_FAMILY,
@@ -1366,7 +1372,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
savedSqlOpenTargetMode: "saved",
welcomePageMode: "workspace",
welcomePageModeDefaultVersion: WELCOME_PAGE_DEFAULT_VERSION,
compactTabTitle: false,
compactTabTitle: true,
compactTabTitleDefaultVersion: COMPACT_TAB_TITLE_DEFAULT_VERSION,
tabLayout: "scroll",
tabPlacement: "top",
colorizeConnectionTabs: true,
@@ -1903,6 +1910,9 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
const savedWelcomePageDefaultVersion = settings.welcomePageModeDefaultVersion;
const welcomePageModeDefaultVersion = typeof savedWelcomePageDefaultVersion === "number" && savedWelcomePageDefaultVersion >= WELCOME_PAGE_DEFAULT_VERSION ? savedWelcomePageDefaultVersion : WELCOME_PAGE_DEFAULT_VERSION;
const hasCurrentWelcomePageDefault = welcomePageModeDefaultVersion === savedWelcomePageDefaultVersion;
const savedCompactTabTitleDefaultVersion = settings.compactTabTitleDefaultVersion;
const compactTabTitleDefaultVersion = typeof savedCompactTabTitleDefaultVersion === "number" && savedCompactTabTitleDefaultVersion >= COMPACT_TAB_TITLE_DEFAULT_VERSION ? savedCompactTabTitleDefaultVersion : COMPACT_TAB_TITLE_DEFAULT_VERSION;
const hasCurrentCompactTabTitleDefault = compactTabTitleDefaultVersion === savedCompactTabTitleDefaultVersion;
// The active id can only be validated once the scheme list it points into is known.
const dataGridTypeColorSchemes = normalizeDataGridTypeColorSchemes(settings.dataGridTypeColorSchemes);
const savedExtractorMigrationVersion = settings.dataGridExtractorOptionsMigrationVersion;
@@ -2018,7 +2028,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
savedSqlOpenTargetMode: settings.savedSqlOpenTargetMode === "current" ? "current" : DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode,
welcomePageMode: hasCurrentWelcomePageDefault && (settings.welcomePageMode === "intro" || settings.welcomePageMode === "workspace") ? settings.welcomePageMode : DEFAULT_EDITOR_SETTINGS.welcomePageMode,
welcomePageModeDefaultVersion,
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
compactTabTitle: hasCurrentCompactTabTitleDefault ? settings.compactTabTitle === true : DEFAULT_EDITOR_SETTINGS.compactTabTitle,
compactTabTitleDefaultVersion,
tabLayout: normalizeTabLayout(settings.tabLayout),
tabPlacement: normalizeTabPlacement(settings.tabPlacement),
colorizeConnectionTabs: typeof settings.colorizeConnectionTabs === "boolean" ? settings.colorizeConnectionTabs : DEFAULT_EDITOR_SETTINGS.colorizeConnectionTabs,
@@ -2446,6 +2457,7 @@ export const useSettingsStore = defineStore("settings", () => {
};
const needsExecuteModeDefaultMigration = typeof savedSettings.executeModeDefaultVersion !== "number" || savedSettings.executeModeDefaultVersion < EXECUTE_MODE_CURRENT_DEFAULT_VERSION;
const needsWelcomePageDefaultMigration = typeof savedSettings.welcomePageModeDefaultVersion !== "number" || savedSettings.welcomePageModeDefaultVersion < WELCOME_PAGE_DEFAULT_VERSION;
const needsCompactTabTitleDefaultMigration = typeof savedSettings.compactTabTitleDefaultVersion !== "number" || savedSettings.compactTabTitleDefaultVersion < COMPACT_TAB_TITLE_DEFAULT_VERSION;
const needsTabNavigationShortcutMigration = needsTabNavigationHistoryShortcutMigration(savedSettings.shortcuts);
const savedNullText = (savedSettings.dataGridExtractorOptions as Partial<DataGridExtractorOptions> | undefined)?.dsv?.nullText;
// v2 also resets a historically persisted includeDatabaseName=true, so
@@ -2455,7 +2467,7 @@ export const useSettingsStore = defineStore("settings", () => {
const needsDataGridExtractorOptionsMigration =
(typeof savedSettings.dataGridExtractorOptionsMigrationVersion !== "number" || savedSettings.dataGridExtractorOptionsMigrationVersion < DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION) && (savedNullText === "NULL" || savedSqlIncludeDatabaseName === true);
const savedUpdateDownloadSource = (saved as { updateDownloadSource?: unknown }).updateDownloadSource;
if (savedUpdateDownloadSource === "atomgit" || needsExecuteModeDefaultMigration || needsWelcomePageDefaultMigration || needsTabNavigationShortcutMigration || needsSidebarBrowseObjectsMigration || needsDataGridExtractorOptionsMigration) {
if (savedUpdateDownloadSource === "atomgit" || needsExecuteModeDefaultMigration || needsWelcomePageDefaultMigration || needsCompactTabTitleDefaultMigration || needsTabNavigationShortcutMigration || needsSidebarBrowseObjectsMigration || needsDataGridExtractorOptionsMigration) {
// Persist one-time migrations so removed or unsafe defaults cannot reappear.
await enqueueEditorSettingsSave().catch(() => {});
}
@@ -2903,7 +2915,12 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.appCloseUnsavedTabsMode !== undefined) editorSettings.value.appCloseUnsavedTabsMode = normalizeAppCloseUnsavedTabsMode(partial.appCloseUnsavedTabsMode);
if (partial.savedSqlOpenTargetMode !== undefined) editorSettings.value.savedSqlOpenTargetMode = partial.savedSqlOpenTargetMode === "current" ? "current" : "saved";
if (partial.welcomePageMode !== undefined) editorSettings.value.welcomePageMode = partial.welcomePageMode === "intro" || partial.welcomePageMode === "workspace" ? partial.welcomePageMode : DEFAULT_EDITOR_SETTINGS.welcomePageMode;
if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle;
if (partial.compactTabTitle !== undefined) {
editorSettings.value.compactTabTitle = partial.compactTabTitle;
// Stamping the current version marks this as a deliberate choice, so a
// later default change must not overwrite it (see normalizeEditorSettings).
editorSettings.value.compactTabTitleDefaultVersion = COMPACT_TAB_TITLE_DEFAULT_VERSION;
}
if (partial.tabLayout !== undefined) editorSettings.value.tabLayout = normalizeTabLayout(partial.tabLayout);
if (partial.tabPlacement !== undefined) editorSettings.value.tabPlacement = normalizeTabPlacement(partial.tabPlacement);
if (partial.colorizeConnectionTabs !== undefined) editorSettings.value.colorizeConnectionTabs = partial.colorizeConnectionTabs === true;
+13 -6
View File
@@ -19,8 +19,15 @@ import {
import i18n, { setLocale } from "../../apps/desktop/src/i18n/index.ts";
import { restoreOpenTabsState } from "../../apps/desktop/src/lib/app/openTabsPersistence.ts";
import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts";
import { useSettingsStore } from "../../apps/desktop/src/stores/settingsStore.ts";
import type { ConnectionConfig, QueryResult, QueryTab } from "../../apps/desktop/src/types/database.ts";
function activateTestPinia() {
setActivePinia(createPinia());
// Keep legacy title assertions explicit now that compact titles are the default.
useSettingsStore().editorSettings.compactTabTitle = false;
}
function installMemoryStorage() {
const values = new Map<string, string>();
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
@@ -76,7 +83,7 @@ function result(columns: string[], overrides: Partial<QueryResult> = {}): QueryR
test("query tab display title uses custom title when present", () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
activateTestPinia();
useConnectionStore().addEphemeralConnection(conn("conn-1"));
const t = (key: string) => key;
@@ -90,7 +97,7 @@ test("query tab display title uses custom title when present", () => {
test("restored MQTT tab titles ignore legacy persisted text and follow the current locale", async () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
activateTestPinia();
useConnectionStore().addEphemeralConnection({
...conn("conn-1"),
name: "test-mqtt",
@@ -125,7 +132,7 @@ test("restored MQTT tab titles ignore legacy persisted text and follow the curre
test("jdbc tabs use the connection target when database is empty", () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
activateTestPinia();
useConnectionStore().addEphemeralConnection({
...conn("conn-1"),
db_type: "jdbc",
@@ -158,7 +165,7 @@ test("jdbc tabs use the connection target when database is empty", () => {
test("zookeeper tabs use key browser labels", () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
activateTestPinia();
useConnectionStore().addEphemeralConnection({
...conn("conn-1"),
name: "ZK Prod",
@@ -178,7 +185,7 @@ test("zookeeper tabs use key browser labels", () => {
test("HBase tabs identify the table and namespace", () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
activateTestPinia();
useConnectionStore().addEphemeralConnection({
...conn("conn-1"),
name: "HBase Dev",
@@ -198,7 +205,7 @@ test("HBase tabs identify the table and namespace", () => {
test("GridFS tabs use dedicated titles and labels", () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
activateTestPinia();
useConnectionStore().addEphemeralConnection({
...conn("conn-1"),
name: "uat-mongo",