mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 09:13:36 +08:00
✨ feat(tabs): 默认启用双行标签展示
- 主行默认仅展示对象名,副行依次展示对象类型、连接名和数据库 - 调整设置项顺序并默认关闭 Schema 与 Host/IP - 升级持久化版本并仅迁移未自定义的旧默认配置 - 补充默认快照、标签文案和配置迁移回归测试
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user