🐛 fix(sidebar): 放宽左侧树拖拽宽度上限

- 统一侧栏宽度常量,最大宽度放宽到 960px
- 拖拽时按窗口宽度预留主工作区空间
- 同步 v2 与 legacy 侧栏 CSS 最大宽度
- 持久化侧栏宽度时允许保存更宽配置并补充测试
This commit is contained in:
Syngnat
2026-07-02 15:20:59 +08:00
parent 9b5e785d67
commit 0707d60f0b
8 changed files with 96 additions and 15 deletions

View File

@@ -697,5 +697,5 @@ body[data-theme='dark'] .gonavi-query-editor-db-token {
/* Legacy sidebar resize bounds — mirror v2 .gn-v2-app-sider so Ant Design inline width locks do not collapse drag range. */
body[data-ui-version="legacy"] .ant-layout-sider {
min-width: 232px !important;
max-width: 420px !important;
max-width: min(960px, calc(100vw - 360px)) !important;
}

View File

@@ -26,6 +26,10 @@ const appSidebarResizeSource = readFileSync(
fileURLToPath(new globalThis.URL('./hooks/useAppSidebarResize.ts', import.meta.url)),
'utf8',
);
const sidebarLayoutSource = readFileSync(
fileURLToPath(new globalThis.URL('./utils/sidebarLayout.ts', import.meta.url)),
'utf8',
);
const getGlobalShortcutCaseBlock = (action: string) => {
const caseToken = `case '${action}':`;
@@ -285,8 +289,12 @@ describe('tool center menu entries', () => {
expect(appSidebarResizeSource).toContain('ghostRef.current.style.left = `${startGuideLeft + (newWidth - startWidth)}px`;');
});
it('keeps legacy sidebar resize bounds aligned with the v2 sider CSS limits', () => {
expect(appCss).toMatch(/body\[data-ui-version="legacy"\]\s+\.ant-layout-sider\s*\{[^}]*min-width:\s*232px\s*!important;[^}]*max-width:\s*420px\s*!important;/s);
it('keeps sidebar resize bounds aligned across drag logic and sider CSS limits', () => {
expect(sidebarLayoutSource).toContain('export const SIDEBAR_RESIZE_MAX_WIDTH = 960;');
expect(sidebarLayoutSource).toContain('export const SIDEBAR_MIN_WORKBENCH_WIDTH = 360;');
expect(appSidebarResizeSource).toContain('resolveSidebarResizeMaxWidth(window.innerWidth, minWidth)');
expect(appCss).toMatch(/body\[data-ui-version="legacy"\]\s+\.ant-layout-sider\s*\{[^}]*min-width:\s*232px\s*!important;[^}]*max-width:\s*min\(960px,\s*calc\(100vw - 360px\)\)\s*!important;/s);
expect(v2ThemeCss).toMatch(/body\[data-ui-version="v2"\]\s+\.gn-v2-app-sider\s*\{[^}]*min-width:\s*232px\s*!important;[^}]*max-width:\s*min\(960px,\s*calc\(100vw - 360px\)\)\s*!important;/s);
});
it('keeps connection modal warm-mounted while leaving the other heavyweight modals conditional', () => {

View File

@@ -1,7 +1,9 @@
import React, { useRef } from 'react';
const SIDEBAR_RESIZE_MIN_WIDTH = 200;
const SIDEBAR_RESIZE_MAX_WIDTH = 600;
import {
SIDEBAR_RESIZE_MAX_WIDTH,
SIDEBAR_RESIZE_MIN_WIDTH,
resolveSidebarResizeMaxWidth,
} from '../utils/sidebarLayout';
type SidebarResizeBounds = { minWidth: number; maxWidth: number };
type SidebarResizeDragState = SidebarResizeBounds & {
@@ -23,7 +25,8 @@ const resolveSidebarResizeBounds = (siderElement: Element | null): SidebarResize
const cssMinWidth = parseCssPixelValue(computed.minWidth);
const cssMaxWidth = parseCssPixelValue(computed.maxWidth);
const minWidth = Math.max(SIDEBAR_RESIZE_MIN_WIDTH, cssMinWidth && cssMinWidth > 0 ? cssMinWidth : SIDEBAR_RESIZE_MIN_WIDTH);
const maxWidth = Math.max(minWidth, Math.min(SIDEBAR_RESIZE_MAX_WIDTH, cssMaxWidth && cssMaxWidth > 0 ? cssMaxWidth : SIDEBAR_RESIZE_MAX_WIDTH));
const viewportMaxWidth = resolveSidebarResizeMaxWidth(window.innerWidth, minWidth);
const maxWidth = Math.max(minWidth, Math.min(viewportMaxWidth, cssMaxWidth && cssMaxWidth > 0 ? cssMaxWidth : viewportMaxWidth));
return { minWidth, maxWidth };
};

View File

@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SIDEBAR_RESIZE_MAX_WIDTH } from './utils/sidebarLayout';
class MemoryStorage implements Storage {
private data = new Map<string, string>();
@@ -360,6 +361,30 @@ describe('store appearance persistence', () => {
expect(appearance.v2SidebarPersistedFilter).toHaveLength(120);
});
it('persists wider sidebar widths and clamps oversized restored values', async () => {
const { useStore } = await importStore();
useStore.getState().setSidebarWidth(880);
expect(useStore.getState().sidebarWidth).toBe(880);
let persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
expect(persisted.state.sidebarWidth).toBe(880);
useStore.getState().setSidebarWidth(1200);
expect(useStore.getState().sidebarWidth).toBe(SIDEBAR_RESIZE_MAX_WIDTH);
storage.setItem('lite-db-storage', JSON.stringify({
state: {
sidebarWidth: 1200,
},
version: 13,
}));
vi.resetModules();
const reloaded = await importStore();
expect(reloaded.useStore.getState().sidebarWidth).toBe(SIDEBAR_RESIZE_MAX_WIDTH);
});
it('persists tab display appearance settings and sanitizes invalid elements', async () => {
const { useStore } = await importStore();

View File

@@ -87,6 +87,7 @@ import {
DEFAULT_QUERY_EDITOR_EDITOR_HEIGHT_RATIO,
sanitizeQueryEditorEditorHeightRatio,
} from "./utils/queryEditorSplitLayout";
import { sanitizeSidebarWidth } from "./utils/sidebarLayout";
import {
DEFAULT_SIDEBAR_TABLE_METADATA_FIELDS,
applySidebarTableMetadataFieldOrder,
@@ -2330,12 +2331,6 @@ const sanitizeWindowState = (
return "normal";
};
const sanitizeSidebarWidth = (value: unknown): number => {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return 330;
return Math.max(200, Math.min(600, Math.trunc(parsed)));
};
const sanitizeWindowBounds = (
value: unknown,
): { width: number; height: number; x: number; y: number } | null => {
@@ -3601,7 +3596,7 @@ export const useStore = create<AppState>()(
setWindowState: (state) => set({ windowState: state }),
setSidebarWidth: (width) =>
set({ sidebarWidth: Math.max(200, Math.min(600, Math.trunc(width))) }),
set({ sidebarWidth: sanitizeSidebarWidth(width) }),
// AI actions
toggleAIPanel: () =>

View File

@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import {
DEFAULT_SIDEBAR_WIDTH,
SIDEBAR_RESIZE_MAX_WIDTH,
SIDEBAR_RESIZE_MIN_WIDTH,
resolveSidebarResizeMaxWidth,
sanitizeSidebarWidth,
} from './sidebarLayout';
describe('sidebar layout bounds', () => {
it('allows wider persisted sidebar widths while keeping invalid values safe', () => {
expect(sanitizeSidebarWidth(880)).toBe(880);
expect(sanitizeSidebarWidth(1200)).toBe(SIDEBAR_RESIZE_MAX_WIDTH);
expect(sanitizeSidebarWidth(120)).toBe(SIDEBAR_RESIZE_MIN_WIDTH);
expect(sanitizeSidebarWidth('bad')).toBe(DEFAULT_SIDEBAR_WIDTH);
});
it('keeps enough workbench space when resolving drag width on smaller windows', () => {
expect(resolveSidebarResizeMaxWidth(1600)).toBe(SIDEBAR_RESIZE_MAX_WIDTH);
expect(resolveSidebarResizeMaxWidth(1180)).toBe(820);
expect(resolveSidebarResizeMaxWidth(480)).toBe(SIDEBAR_RESIZE_MIN_WIDTH);
});
});

View File

@@ -0,0 +1,27 @@
export const DEFAULT_SIDEBAR_WIDTH = 330;
export const SIDEBAR_RESIZE_MIN_WIDTH = 200;
export const SIDEBAR_SIDER_MIN_WIDTH = 232;
export const SIDEBAR_RESIZE_MAX_WIDTH = 960;
export const SIDEBAR_MIN_WORKBENCH_WIDTH = 360;
export const resolveSidebarResizeMaxWidth = (
viewportWidth: unknown,
minWidth = SIDEBAR_RESIZE_MIN_WIDTH,
): number => {
const parsedViewportWidth = Number(viewportWidth);
if (!Number.isFinite(parsedViewportWidth) || parsedViewportWidth <= 0) {
return SIDEBAR_RESIZE_MAX_WIDTH;
}
const viewportLimitedWidth = Math.trunc(parsedViewportWidth) - SIDEBAR_MIN_WORKBENCH_WIDTH;
return Math.max(minWidth, Math.min(SIDEBAR_RESIZE_MAX_WIDTH, viewportLimitedWidth));
};
export const sanitizeSidebarWidth = (value: unknown): number => {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return DEFAULT_SIDEBAR_WIDTH;
return Math.max(
SIDEBAR_RESIZE_MIN_WIDTH,
Math.min(SIDEBAR_RESIZE_MAX_WIDTH, Math.trunc(parsed)),
);
};

View File

@@ -1758,7 +1758,7 @@ body[data-ui-version="v2"] .gn-v2-sidebar-shell {
body[data-ui-version="v2"] .gn-v2-app-sider {
min-width: 232px !important;
max-width: 420px !important;
max-width: min(960px, calc(100vw - 360px)) !important;
}
body[data-ui-version="v2"] .gn-v2-sidebar-redesign {