feat(tabs): 默认启用双行标签展示

- 主行默认仅展示对象名,副行依次展示对象类型、连接名和数据库
- 调整设置项顺序并默认关闭 Schema 与 Host/IP
- 升级持久化版本并仅迁移未自定义的旧默认配置
- 补充默认快照、标签文案和配置迁移回归测试
This commit is contained in:
Syngnat
2026-07-24 14:00:49 +08:00
parent 340e7837a3
commit e8eb6366c3
4 changed files with 147 additions and 34 deletions

View File

@@ -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: {

View File

@@ -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<TabDisplaySettings>;
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<AppearanceSettings> | 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,

View File

@@ -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: [],
});

View File

@@ -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<TabDisplayElementKey, { labelKey: string; descriptionKey: string }> = {
connection: {
@@ -54,28 +54,34 @@ export const TAB_DISPLAY_ELEMENT_META: Record<TabDisplayElementKey, { labelKey:
},
};
export const DEFAULT_TAB_DISPLAY_SETTINGS: TabDisplaySettings = {
layout: 'single',
const DEFAULT_SINGLE_TAB_DISPLAY_SNAPSHOT: TabDisplayLayoutSnapshot = {
primaryElements: ['connection', 'kind', 'object'],
secondaryElements: [],
};
const DEFAULT_DOUBLE_TAB_DISPLAY_SNAPSHOT: TabDisplayLayoutSnapshot = {
primaryElements: ['object'],
secondaryElements: ['kind', 'connection', 'database'],
};
export const DEFAULT_TAB_DISPLAY_SETTINGS: TabDisplaySettings = {
layout: 'double',
primaryElements: [...DEFAULT_DOUBLE_TAB_DISPLAY_SNAPSHOT.primaryElements],
secondaryElements: [...DEFAULT_DOUBLE_TAB_DISPLAY_SNAPSHOT.secondaryElements],
};
export const getCurrentTabDisplaySnapshot = (settings: TabDisplaySettings): TabDisplayLayoutSnapshot => ({
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<TabDisplayElementKey>();
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<TabDisplaySettings>;
const layout = raw.layout === 'single' || raw.layout === 'double'
? raw.layout
: DEFAULT_TAB_DISPLAY_SETTINGS.layout;
const fallback = getDefaultTabDisplaySnapshot(layout);
const used = new Set<TabDisplayElementKey>();
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;
}