diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index 2a2e1292..73f923eb 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -96,9 +96,61 @@ describe('store appearance persistence', () => { expect(appearance.customMonoFontFamily).toBeNull(); expect(appearance.newQuerySqlTemplate).toBeNull(); expect(appearance.tabDisplay).toEqual({ + layout: 'double', + primaryElements: ['object'], + secondaryElements: ['kind', 'connection', 'database'], + }); + }); + + it('migrates the previous tab display default without overwriting custom settings', async () => { + storage.setItem('lite-db-storage', JSON.stringify({ + state: { + appearance: { + tabDisplay: { + layout: 'single', + primaryElements: ['connection', 'kind', 'object'], + secondaryElements: [], + }, + }, + }, + version: 19, + })); + + const migrated = await importStore(); + expect(migrated.useStore.getState().appearance.tabDisplay).toEqual({ + layout: 'double', + primaryElements: ['object'], + secondaryElements: ['kind', 'connection', 'database'], + }); + expect(JSON.parse(storage.getItem('lite-db-storage') || '{}').version).toBe(20); + + storage.setItem('lite-db-storage', JSON.stringify({ + state: { + appearance: { + tabDisplay: { + layout: 'single', + primaryElements: ['connection', 'kind', 'object'], + secondaryElements: [], + double: { + primaryElements: ['object', 'host'], + secondaryElements: ['kind', 'connection'], + }, + }, + }, + }, + version: 19, + })); + vi.resetModules(); + + const customized = await importStore(); + expect(customized.useStore.getState().appearance.tabDisplay).toEqual({ layout: 'single', primaryElements: ['connection', 'kind', 'object'], secondaryElements: [], + double: { + primaryElements: ['object', 'host'], + secondaryElements: ['kind', 'connection'], + }, }); }); @@ -123,7 +175,7 @@ describe('store appearance persistence', () => { expect(appearance.sqlEditorFontSizeFollowGlobal).toBe(false); const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); - expect(persisted.version).toBe(19); + expect(persisted.version).toBe(20); expect(persisted.state.appearance.sqlEditorFontSize).toBe(17); expect(persisted.state.appearance.sqlEditorFontSizeFollowGlobal).toBe(false); }); @@ -142,7 +194,7 @@ describe('store appearance persistence', () => { expect(useStore.getState().appearance.uiVersion).toBe('v2'); const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); - expect(persisted.version).toBe(19); + expect(persisted.version).toBe(20); expect(persisted.state.appearance.uiVersion).toBe('v2'); }); @@ -1322,7 +1374,7 @@ describe('store appearance persistence', () => { )).toEqual(legacyTag?.childOrder); const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); - expect(persisted.version).toBe(19); + expect(persisted.version).toBe(20); expect(persisted.state.connectionTags[0].childOrder).toEqual([ 'connection:conn-a', 'connection:conn-b', @@ -3194,7 +3246,7 @@ describe('store appearance persistence', () => { mac: { combo: 'Meta+K', enabled: false }, windows: { combo: 'Ctrl+K', enabled: true }, }); - expect(JSON.parse(storage.getItem('lite-db-storage') || '{}').version).toBe(19); + expect(JSON.parse(storage.getItem('lite-db-storage') || '{}').version).toBe(20); storage.setItem('lite-db-storage', JSON.stringify({ state: { diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 15241cfe..a85f0a41 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -266,8 +266,9 @@ const MIN_KEEPALIVE_INTERVAL_MINUTES = 1; const MAX_KEEPALIVE_INTERVAL_MINUTES = 1440; const DEFAULT_DIAGNOSTIC_TIMEOUT_SECONDS = 15; const MAX_DIAGNOSTIC_TIMEOUT_SECONDS = 300; -const PERSIST_VERSION = 19; +const PERSIST_VERSION = 20; const SQL_EDITOR_FONT_SIZE_SPLIT_VERSION = 19; +const TAB_DISPLAY_DEFAULT_MIGRATION_VERSION = 20; const UI_VERSION_V2_MIGRATION_VERSION = 14; const SIDEBAR_SEARCH_SHORTCUT_MIGRATION_VERSION = 18; const PERSIST_STORAGE_KEY = "lite-db-storage"; @@ -2908,6 +2909,21 @@ const sanitizePinnedSidebarTables = (value: unknown): string[] => { ); }; +const isLegacyDefaultTabDisplaySettings = (value: unknown): boolean => { + if (!value || typeof value !== "object") return false; + const raw = value as Partial; + return raw.layout === "single" + && Array.isArray(raw.primaryElements) + && raw.primaryElements.length === 3 + && raw.primaryElements[0] === "connection" + && raw.primaryElements[1] === "kind" + && raw.primaryElements[2] === "object" + && Array.isArray(raw.secondaryElements) + && raw.secondaryElements.length === 0 + && raw.single === undefined + && raw.double === undefined; +}; + const sanitizeAppearance = ( appearance: Partial | undefined, version: number, @@ -2962,7 +2978,10 @@ const sanitizeAppearance = ( customUIFontFamily: sanitizeFontFamilyInput(appearance.customUIFontFamily), customMonoFontFamily: sanitizeFontFamilyInput(appearance.customMonoFontFamily), newQuerySqlTemplate: sanitizeNewQuerySqlTemplate(appearance.newQuerySqlTemplate), - tabDisplay: sanitizeTabDisplaySettings(appearance.tabDisplay), + tabDisplay: version < TAB_DISPLAY_DEFAULT_MIGRATION_VERSION + && isLegacyDefaultTabDisplaySettings(appearance.tabDisplay) + ? sanitizeTabDisplaySettings(DEFAULT_TAB_DISPLAY_SETTINGS) + : sanitizeTabDisplaySettings(appearance.tabDisplay), redisDbAliases: sanitizeRedisDbAliases(appearance.redisDbAliases), showDataTableVerticalBorders: dataGridDisplaySettings.showDataTableVerticalBorders, diff --git a/frontend/src/utils/tabDisplay.test.ts b/frontend/src/utils/tabDisplay.test.ts index a19681e7..bc6ad987 100644 --- a/frontend/src/utils/tabDisplay.test.ts +++ b/frontend/src/utils/tabDisplay.test.ts @@ -6,6 +6,8 @@ import { applyTabDisplaySettingsPatch, buildTabDisplayModel, buildTabDisplayTitle, + DEFAULT_TAB_DISPLAY_SETTINGS, + getDefaultTabDisplaySnapshot, getTabDisplayKindLabel, resolveTabDisplayElementOrder, resolveConnectionHostSummary, @@ -36,6 +38,22 @@ const redisConnection: SavedConnection = { }; describe('tabDisplay', () => { + it('uses the requested double-line tab display defaults', () => { + expect(DEFAULT_TAB_DISPLAY_SETTINGS).toEqual({ + layout: 'double', + primaryElements: ['object'], + secondaryElements: ['kind', 'connection', 'database'], + }); + expect(getDefaultTabDisplaySnapshot('single')).toEqual({ + primaryElements: ['connection', 'kind', 'object'], + secondaryElements: [], + }); + expect(getDefaultTabDisplaySnapshot('double')).toEqual({ + primaryElements: ['object'], + secondaryElements: ['kind', 'connection', 'database'], + }); + }); + it('builds compact host summary for multi-host redis connections', () => { expect(resolveConnectionHostSummary(redisConnection.config)).toBe('10.10.0.12 +2'); }); @@ -232,7 +250,7 @@ describe('tabDisplay', () => { })).toBe('andon_events SCHEMA:ldf_server 192.168.10.8'); }); - it('builds the default configurable model with connection, type and compact object name', () => { + it('builds the default configurable model with the object on the primary line', () => { const connection: SavedConnection = { id: 'kingbase-1', name: 'Kingbase DEV', @@ -253,7 +271,12 @@ describe('tabDisplay', () => { tableName: 'ldf_server.andon_events', }; - expect(buildTabDisplayModel(tableTab, connection).fullTitle).toBe('[DEV] TABLE andon_events'); + const model = buildTabDisplayModel(tableTab, connection); + + expect(model.layout).toBe('double'); + expect(model.primaryText).toBe('andon_events'); + expect(model.secondaryText).toBe('TABLE·[DEV]·appdb'); + expect(model.fullTitle).toBe('andon_events · TABLE·[DEV]·appdb'); }); it('keeps query tab labels compact when the title is raw SQL', () => { @@ -279,7 +302,8 @@ describe('tabDisplay', () => { const model = buildTabDisplayModel(queryTab, connection); - expect(model.primaryText).toBe('[开发240] SQL New query'); + expect(model.primaryText).toBe('New query'); + expect(model.secondaryText).toBe('SQL·[开发240]·front_end_sys'); expect(model.fullTitle).not.toContain('fs_org_auth_application'); expect(model.fullTitle).not.toContain('select *'); }); @@ -296,7 +320,8 @@ describe('tabDisplay', () => { const model = buildTabDisplayModel(queryTab, undefined, undefined, keyEchoTranslate); - expect(model.primaryText).toBe('SQL T(New query)'); + expect(model.primaryText).toBe('T(New query)'); + expect(model.secondaryText).toBe('SQL·front_end_sys'); expect(model.fullTitle).not.toContain('fs_org_auth_application'); expect(model.fullTitle).not.toContain('select *'); }); @@ -313,7 +338,8 @@ describe('tabDisplay', () => { const model = buildTabDisplayModel(queryTab, undefined, undefined, keyEchoTranslate); - expect(model.primaryText).toBe('SQL T(New query)'); + expect(model.primaryText).toBe('T(New query)'); + expect(model.secondaryText).toBe('SQL·front_end_sys'); }); it('relocalizes database-scoped untitled query labels with the current database name', () => { @@ -328,7 +354,8 @@ describe('tabDisplay', () => { const model = buildTabDisplayModel(queryTab, undefined, undefined, keyEchoTranslate); - expect(model.primaryText).toBe('SQL T(New query (main))'); + expect(model.primaryText).toBe('T(New query (main))'); + expect(model.secondaryText).toBe('SQL·main'); }); it('uses SQL file names as compact query tab object labels', () => { @@ -343,7 +370,8 @@ describe('tabDisplay', () => { const model = buildTabDisplayModel(queryTab); - expect(model.primaryText).toBe('SQL monthly-report.sql'); + expect(model.primaryText).toBe('monthly-report.sql'); + expect(model.secondaryText).toBe('SQL'); }); it('builds configurable double-line tab display models', () => { @@ -385,7 +413,7 @@ describe('tabDisplay', () => { primaryElements: ['schema', 'schema', 'bad' as never], secondaryElements: ['object', 'schema', 'host'], })).toEqual({ - layout: 'single', + layout: 'double', primaryElements: ['schema'], secondaryElements: ['object', 'host'], }); @@ -396,7 +424,7 @@ describe('tabDisplay', () => { secondaryElements: [], })).toEqual({ layout: 'double', - primaryElements: ['connection', 'kind', 'object'], + primaryElements: ['object'], secondaryElements: [], }); diff --git a/frontend/src/utils/tabDisplay.ts b/frontend/src/utils/tabDisplay.ts index eddd27ff..0a949af6 100644 --- a/frontend/src/utils/tabDisplay.ts +++ b/frontend/src/utils/tabDisplay.ts @@ -3,7 +3,7 @@ import { t as catalogTranslate } from '../i18n/catalog'; import type { I18nParams } from '../i18n/types'; import { resolveLocalizedUntitledQueryTitle } from './queryTabTitle'; -export const TAB_DISPLAY_ELEMENT_KEYS = ['connection', 'kind', 'object', 'database', 'schema', 'host'] as const; +export const TAB_DISPLAY_ELEMENT_KEYS = ['object', 'kind', 'connection', 'database', 'schema', 'host'] as const; export type TabDisplayElementKey = typeof TAB_DISPLAY_ELEMENT_KEYS[number]; export type TabDisplayLayout = 'single' | 'double'; @@ -25,7 +25,7 @@ export type TabDisplayTranslate = (key: string, params?: I18nParams) => string; const defaultTranslate: TabDisplayTranslate = (key, params) => catalogTranslate('en-US', key, params); -export const TAB_DISPLAY_SECONDARY_DEFAULT_KEYS: TabDisplayElementKey[] = ['connection', 'database', 'schema', 'host']; +export const TAB_DISPLAY_SECONDARY_DEFAULT_KEYS: TabDisplayElementKey[] = ['kind', 'connection', 'database', 'schema', 'host']; export const TAB_DISPLAY_ELEMENT_META: Record = { connection: { @@ -54,28 +54,34 @@ export const TAB_DISPLAY_ELEMENT_META: Record ({ primaryElements: [...settings.primaryElements], secondaryElements: [...settings.secondaryElements], }); export const getDefaultTabDisplaySnapshot = (layout: TabDisplayLayout): TabDisplayLayoutSnapshot => { - if (layout === 'single') { - return { - primaryElements: [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements], - secondaryElements: [], - }; - } - + const snapshot = layout === 'single' + ? DEFAULT_SINGLE_TAB_DISPLAY_SNAPSHOT + : DEFAULT_DOUBLE_TAB_DISPLAY_SNAPSHOT; return { - primaryElements: [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements], - secondaryElements: TAB_DISPLAY_SECONDARY_DEFAULT_KEYS.filter((key) => !DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements.includes(key)), + primaryElements: [...snapshot.primaryElements], + secondaryElements: [...snapshot.secondaryElements], }; }; @@ -148,7 +154,10 @@ const sanitizeTabDisplayElementList = ( return result; }; -const sanitizeTabDisplayLayoutSnapshot = (value: unknown): TabDisplayLayoutSnapshot | null => { +const sanitizeTabDisplayLayoutSnapshot = ( + value: unknown, + layout: TabDisplayLayout, +): TabDisplayLayoutSnapshot | null => { if (!value || typeof value !== 'object') { return null; } @@ -156,8 +165,9 @@ const sanitizeTabDisplayLayoutSnapshot = (value: unknown): TabDisplayLayoutSnaps const used = new Set(); const primaryElements = sanitizeTabDisplayElementList(raw.primaryElements, used); const secondaryElements = sanitizeTabDisplayElementList(raw.secondaryElements, used); + const fallback = getDefaultTabDisplaySnapshot(layout); return { - primaryElements: primaryElements.length > 0 ? primaryElements : [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements], + primaryElements: primaryElements.length > 0 ? primaryElements : fallback.primaryElements, secondaryElements, }; }; @@ -167,16 +177,20 @@ export const sanitizeTabDisplaySettings = (value: unknown): TabDisplaySettings = return { ...DEFAULT_TAB_DISPLAY_SETTINGS, primaryElements: [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements], secondaryElements: [...DEFAULT_TAB_DISPLAY_SETTINGS.secondaryElements] }; } const raw = value as Partial; + const layout = raw.layout === 'single' || raw.layout === 'double' + ? raw.layout + : DEFAULT_TAB_DISPLAY_SETTINGS.layout; + const fallback = getDefaultTabDisplaySnapshot(layout); const used = new Set(); const primaryElements = sanitizeTabDisplayElementList(raw.primaryElements, used); const secondaryElements = sanitizeTabDisplayElementList(raw.secondaryElements, used); const result: TabDisplaySettings = { - layout: raw.layout === 'double' ? 'double' : 'single', - primaryElements: primaryElements.length > 0 ? primaryElements : [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements], + layout, + primaryElements: primaryElements.length > 0 ? primaryElements : fallback.primaryElements, secondaryElements, }; - const single = sanitizeTabDisplayLayoutSnapshot(raw.single); - const double = sanitizeTabDisplayLayoutSnapshot(raw.double); + const single = sanitizeTabDisplayLayoutSnapshot(raw.single, 'single'); + const double = sanitizeTabDisplayLayoutSnapshot(raw.double, 'double'); if (single) { result.single = single; }