From 9870959ca61d0350b820109ecf65c0535e55e0f0 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 10 Jul 2026 22:00:05 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(ui):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=A7=E7=89=88=E7=BC=96=E8=BE=91=E5=99=A8=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E5=B9=B6=E8=BF=81=E7=A7=BB=E9=BB=98=E8=AE=A4=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E8=87=B3=20V2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复旧版 SQL 编辑器在弹性布局中的遮挡问题 - 将既有用户的界面偏好一次性迁移至 V2 并保留后续手动选择 - 覆盖持久化迁移与旧版编辑器布局回归测试 --- frontend/src/App.css | 14 ++++++++ .../QueryEditor.legacy-layout.test.ts | 13 +++++++ frontend/src/components/QueryEditor.tsx | 4 +-- frontend/src/store.test.ts | 34 ++++++++++++++++++- frontend/src/store.ts | 20 ++++++++--- 5 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/QueryEditor.legacy-layout.test.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index 1e22cf69..6004458c 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -239,6 +239,20 @@ body[data-theme='light'] ::-webkit-scrollbar-thumb:hover { background-clip: content-box; } +.gn-query-monaco-stage { + position: relative; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.gn-query-monaco-shell { + flex: 1 1 auto; + min-height: 0; + min-width: 0; +} + .gn-query-monaco-stage .monaco-editor .suggest-details-container { min-height: 260px; } diff --git a/frontend/src/components/QueryEditor.legacy-layout.test.ts b/frontend/src/components/QueryEditor.legacy-layout.test.ts new file mode 100644 index 00000000..42a182ee --- /dev/null +++ b/frontend/src/components/QueryEditor.legacy-layout.test.ts @@ -0,0 +1,13 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const queryEditorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8'); +const appCss = readFileSync(new URL('../App.css', import.meta.url), 'utf8'); + +describe('QueryEditor legacy layout', () => { + it('keeps the legacy Monaco editor inside a flexing stage and shell', () => { + expect(queryEditorSource).toContain("className={isV2Ui ? 'gn-v2-query-monaco-shell gn-query-monaco-shell' : 'gn-query-monaco-shell'}"); + expect(appCss).toMatch(/\.gn-query-monaco-stage\s*\{[\s\S]*?position:\s*relative;[\s\S]*?display:\s*flex;[\s\S]*?flex-direction:\s*column;[\s\S]*?min-height:\s*0;[\s\S]*?overflow:\s*hidden;/); + expect(appCss).toMatch(/\.gn-query-monaco-shell\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?min-height:\s*0;[\s\S]*?min-width:\s*0;/); + }); +}); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 80c7b32c..55a568d4 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -8022,8 +8022,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc >
{ const { useStore } = await importStore(); const appearance = useStore.getState().appearance; - expect(appearance.uiVersion).toBe('legacy'); + expect(appearance.uiVersion).toBe('v2'); expect(appearance.enabled).toBe(false); expect(appearance.opacity).toBe(0.75); expect(appearance.blur).toBe(6); @@ -92,6 +92,38 @@ describe('store appearance persistence', () => { }); }); + it('migrates an existing legacy UI selection to V2', async () => { + storage.setItem('lite-db-storage', JSON.stringify({ + state: { + appearance: { + uiVersion: 'legacy', + }, + }, + version: 13, + })); + + const { useStore } = await importStore(); + expect(useStore.getState().appearance.uiVersion).toBe('v2'); + + const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); + expect(persisted.version).toBe(14); + expect(persisted.state.appearance.uiVersion).toBe('v2'); + }); + + it('keeps a legacy UI selection made after the V2 migration', async () => { + storage.setItem('lite-db-storage', JSON.stringify({ + state: { + appearance: { + uiVersion: 'legacy', + }, + }, + version: 14, + })); + + const { useStore } = await importStore(); + expect(useStore.getState().appearance.uiVersion).toBe('legacy'); + }); + it('persists DataGrid appearance settings and restores them after reload', async () => { const { useStore } = await importStore(); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 797f95f2..e66f4c58 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -137,7 +137,7 @@ export const MIN_V2_SIDEBAR_RAIL_SCALE = 1.0; export const MAX_V2_SIDEBAR_RAIL_SCALE = 1.8; export const DEFAULT_APPEARANCE: AppearanceSettings = { - uiVersion: "legacy", + uiVersion: "v2", enabled: true, opacity: 1.0, blur: 0, @@ -217,7 +217,8 @@ 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 = 13; +const PERSIST_VERSION = 14; +const UI_VERSION_V2_MIGRATION_VERSION = 14; const PERSIST_STORAGE_KEY = "lite-db-storage"; const PERSIST_WRITE_DEBOUNCE_MS = 160; const MAX_PERSISTED_QUERY_TABS = 20; @@ -4505,7 +4506,12 @@ export const useStore = create()( ) as Partial; captureLegacySavedQueriesSnapshot(state.savedQueries, state.connections); const nextState: Partial = { ...state }; - nextState.connections = sanitizeConnections(state.connections); + // 缺失连接表示由启动阶段从后端加载,迁移时不能把它写成空数组, + // 否则会让持久化的侧栏根节点顺序提前丢失。 + nextState.connections = + state.connections === undefined + ? undefined + : sanitizeConnections(state.connections); const safeTabs = sanitizeQueryTabs(state.tabs); nextState.tabs = safeTabs; nextState.activeTabId = sanitizeActiveTabId(state.activeTabId, safeTabs); @@ -4535,7 +4541,13 @@ export const useStore = create()( nextState.languagePreference = sanitizeLanguagePreference( state.languagePreference, ); - nextState.appearance = sanitizeAppearance(state.appearance, version); + const appearance = sanitizeAppearance(state.appearance, version); + // 旧版界面在窄布局下可能遮挡 SQL 编辑器。仅在本次版本升级时 + // 将已保存的选择迁移至 V2,之后用户仍可自行切换并保留偏好。 + nextState.appearance = + version < UI_VERSION_V2_MIGRATION_VERSION + ? { ...appearance, uiVersion: "v2" } + : appearance; nextState.uiScale = sanitizeUiScale(state.uiScale); nextState.fontSize = sanitizeFontSize(state.fontSize); nextState.startupFullscreen = sanitizeStartupFullscreen(