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(