From 3ed45fab41aef35f18b523e9300be39a3b2d10c7 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 26 Jun 2026 17:28:49 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sidebar):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=A1=A8=E9=BB=98=E8=AE=A4=E6=89=93=E5=BC=80=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E4=B8=8E=E5=B7=A6=E6=A0=91=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 工具设置新增双击表名行为开关,默认打开表数据,可切换到对象设计 - 左侧树、表概览卡片视图和列表视图统一按开关打开数据页内对象设计 - 调整 V2 左侧树横向滚动条布局,使滚动条保持在底部可见 --- frontend/src/App.tool-center.test.ts | 6 ++ frontend/src/App.tsx | 16 ++++ .../Sidebar.locate-toolbar.test.tsx | 22 ++++- frontend/src/components/Sidebar.tsx | 3 + .../TableOverview.tdengine.test.tsx | 92 ++++++++++++++++++- frontend/src/components/TableOverview.tsx | 29 +++++- .../sidebar/useSidebarSearchModel.tsx | 5 +- frontend/src/store.test.ts | 18 ++++ frontend/src/store.ts | 13 +++ frontend/src/v2-theme.css | 8 +- 10 files changed, 198 insertions(+), 14 deletions(-) diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index 3536462..d255422 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -403,6 +403,12 @@ describe('global appearance tokens', () => { expect(appSource).toContain('fontFamilyCode: resolvedMonoFontFamily'); expect(appSource).toContain("t('app.theme.data_table.font_size')"); expect(appSource).toContain("t('app.theme.data_table.sidebar_tree_font_size')"); + expect(appSource).toContain("const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data';"); + expect(appSource).toContain("t('app.theme.data_table.table_double_click_action')"); + expect(appSource).toContain("t('app.theme.data_table.table_double_click_action.open_data')"); + expect(appSource).toContain("t('app.theme.data_table.table_double_click_action.open_design')"); + expect(appSource).toContain("t('app.theme.data_table.table_double_click_action_hint')"); + expect(appSource).toContain("setAppearance({ tableDoubleClickAction: value as 'open-data' | 'open-design' })"); expect(appSource).toContain('buildFontFamilyOptions(runtimePlatform, \'ui\', installedFontFamilies, t)'); expect(appSource).toContain('buildFontFamilyOptions(runtimePlatform, \'mono\', installedFontFamilies, t)'); expect(appSource).toContain('ListInstalledFontFamilies()'); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7221d62..114ccbe 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -249,6 +249,7 @@ function App() { const effectiveSidebarTreeFontSize = sidebarTreeFontSizeFollowsGlobal ? effectiveFontSize : (sanitizeSidebarTreeFontSize(appearance.sidebarTreeFontSize) ?? effectiveFontSize); + const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data'; const tabDisplaySettings = useMemo( () => sanitizeTabDisplaySettings(appearance.tabDisplay), [appearance.tabDisplay], @@ -4789,6 +4790,21 @@ function App() { onChange={(checked) => setAppearance({ showDataTableVerticalBorders: checked })} /> +
+
{t('app.theme.data_table.table_double_click_action')}
+ setAppearance({ tableDoubleClickAction: value as 'open-data' | 'open-design' })} + /> +
+ {t('app.theme.data_table.table_double_click_action_hint')} +
+
{t('app.theme.data_table.density')}
{ expect(source).toContain('if (shouldLoadSidebarNodeOnExpand(node))'); }); + it('uses the appearance preference to switch table double-click to the embedded object designer', () => { + const source = readSidebarSource(); + + expect(source).toContain("const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data';"); + expect(source).toContain("type: 'table',"); + expect(source).toContain("initialViewMode: tableDoubleClickAction === 'open-design' ? 'fields' : undefined"); + expect(source).toContain("initialViewModeRequestId: tableDoubleClickAction === 'open-design' ? String(Date.now()) : undefined"); + expect(source).not.toContain("if (tableDoubleClickAction === 'open-design') {\n openDesign(node, 'columns', false);"); + expect(source).toContain('recordTableAccess(id, dbName, tableName);'); + }); + it('parses v2 command search prefixes into real search modes', () => { expect(parseV2CommandSearchQuery('@ payment_order')).toMatchObject({ mode: 'object', @@ -1087,8 +1098,8 @@ describe('Sidebar locate toolbar', () => { expect(source).not.toContain("overflowX: isV2Ui ? 'auto' : 'hidden'"); expect(source).toContain('scrollWidth={isV2Ui ? v2TreeHorizontalScrollWidth : undefined}'); expect(utilsSource).toContain('export const V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE = 32;'); - expect(source).toContain('const effectiveTreeHeight = isV2Ui && v2TreeHorizontalScrollWidth'); - expect(source).toContain('treeHeight - V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE'); + expect(source).toContain('const effectiveTreeHeight = treeHeight;'); + expect(source).not.toContain('treeHeight - V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE'); expect(source).toContain('height={effectiveTreeHeight}'); expect(source).toContain('treeData={isV2Ui ? v2VisibleTreeData : displayTreeData}'); expect(source).not.toContain('__v2-tree-horizontal-scroll-spacer__'); @@ -1096,14 +1107,15 @@ describe('Sidebar locate toolbar', () => { expect(css).toMatch(/\.gn-v2-explorer-tree-shell \{[^}]*--gn-v2-tree-horizontal-scroll-reserve: 32px;[^}]*overflow: hidden !important;/s); expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.sidebar-tree-scroll-content \{[^}]*display: flex;[^}]*height: 100%;[^}]*padding: 4px 0 0;/s); expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree \{[^}]*flex: 1 1 auto;[^}]*width: 100%;[^}]*min-width: 0;[^}]*height: 100%;/s); - expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list \{[^}]*height: calc\(100% - var\(--gn-v2-tree-horizontal-scroll-reserve\)\);[^}]*min-height: 0;[^}]*box-sizing: border-box;/s); - expect(css).not.toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list \{[^}]*height: 100%;/s); + expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list \{[^}]*position: relative;[^}]*height: 100%;[^}]*min-height: 0;[^}]*box-sizing: border-box;[^}]*padding-bottom: var\(--gn-v2-tree-horizontal-scroll-reserve\);/s); expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-holder-inner \{[^}]*width: 100%;[^}]*min-width: 100%;/s); expect(css).not.toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-holder-inner \{[^}]*width: max-content;/s); expect(css).not.toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list \{[^}]*position: static !important;/s); expect(css).not.toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-holder \{[^}]*calc\(100% - var\(--gn-v2-tree-horizontal-scroll-reserve\)\)/s); expect(css).not.toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-holder \{[^}]*overflow-x: auto !important;/s); - expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-scrollbar-horizontal \{[^}]*height: 12px !important;[^}]*bottom: calc\(\(var\(--gn-v2-tree-horizontal-scroll-reserve\) - 12px\) \* -1\) !important;/s); + expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-holder \{[^}]*padding-bottom: var\(--gn-v2-tree-horizontal-scroll-reserve\);/s); + expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-scrollbar-horizontal \{[^}]*height: 12px !important;[^}]*bottom: calc\(\(var\(--gn-v2-tree-horizontal-scroll-reserve\) - 12px\) \/ 2\) !important;/s); + expect(css).not.toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-scrollbar-horizontal \{[^}]*bottom: calc\(\(var\(--gn-v2-tree-horizontal-scroll-reserve\) - 12px\) \* -1\) !important;/s); expect(css).not.toContain('.gn-v2-tree-horizontal-scroll-spacer'); expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-scrollbar-horizontal \.ant-tree-list-scrollbar-thumb \{[^}]*height: 8px !important;/s); const treeContentWrapperCss = readCssRuleBlock(css, 'body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-node-content-wrapper'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 1d8cd6e..03d8e97 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -493,6 +493,7 @@ const Sidebar: React.FC<{ const v2UseLegacySidebarFilter = isV2Ui && v2SidebarSearchMode === 'filter'; const v2CommandSearchPersistentFilterEnabled = appearance.v2CommandSearchPersistentFilterEnabled === true; const v2PersistedSidebarFilter = appearance.v2SidebarPersistedFilter ?? ''; + const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data'; const [searchValue, setSearchValue] = useState(v2PersistedSidebarFilter); const deferredSearchValue = useDeferredValue(searchValue); const [searchScopes, setSearchScopes] = useState(['smart']); @@ -1561,6 +1562,8 @@ const Sidebar: React.FC<{ connectionId: id, dbName, tableName, + initialViewMode: tableDoubleClickAction === 'open-design' ? 'fields' : undefined, + initialViewModeRequestId: tableDoubleClickAction === 'open-design' ? String(Date.now()) : undefined, objectType: 'table', }); return; diff --git a/frontend/src/components/TableOverview.tdengine.test.tsx b/frontend/src/components/TableOverview.tdengine.test.tsx index 89aa300..a3bfd2a 100644 --- a/frontend/src/components/TableOverview.tdengine.test.tsx +++ b/frontend/src/components/TableOverview.tdengine.test.tsx @@ -6,7 +6,13 @@ import TableOverview from './TableOverview'; const storeState = vi.hoisted(() => ({ theme: 'light', - appearance: { uiVersion: 'legacy' as const }, + appearance: { + uiVersion: 'legacy', + tableDoubleClickAction: 'open-data', + } as { + uiVersion: 'legacy' | 'v2'; + tableDoubleClickAction: 'open-data' | 'open-design'; + }, connections: [ { id: 'conn-1', @@ -56,6 +62,12 @@ vi.mock('../utils/autoFetchVisibility', () => ({ vi.mock('../utils/connectionRpcConfig', () => ({ buildRpcConnectionConfig: (config: unknown) => config, })); +vi.mock('./ExportProgressModal', () => ({ + useExportProgressDialog: () => ({ + exportProgressModal: null, + runExportWithProgress: vi.fn(), + }), +})); vi.mock('./V2TableContextMenu', () => ({ V2TableContextMenuView: () => null, })); @@ -90,6 +102,8 @@ vi.mock('antd', () => { const Tooltip = ({ children }: any) =>
{children}
; const Modal: any = ({ children }: any) =>
{children}
; Modal.confirm = vi.fn(); + const Text = ({ children }: any) => {children}; + const Paragraph = ({ children }: any) =>

{children}

; return { Button, Dropdown, @@ -98,6 +112,7 @@ vi.mock('antd', () => { Modal, Spin, Tooltip, + Typography: { Text, Paragraph }, message: messageApi, }; }); @@ -126,6 +141,7 @@ const collectText = (node: any): string => { describe('TableOverview tdengine compatibility', () => { beforeEach(() => { vi.clearAllMocks(); + storeState.appearance = { uiVersion: 'legacy', tableDoubleClickAction: 'open-data' }; backendApp.DBGetTables.mockResolvedValue({ success: true, data: [ @@ -159,4 +175,78 @@ describe('TableOverview tdengine compatibility', () => { expect(renderedText).toContain('meters'); expect(renderedText).toContain('d001'); }); + + it('uses the table default open behavior for v2 card double-clicks', async () => { + storeState.appearance = { uiVersion: 'v2', tableDoubleClickAction: 'open-design' }; + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + await flushPromises(); + + const card = renderer!.root.findAll((node) => node.props.className === 'gn-v2-table-card')[0]; + expect(card).toBeTruthy(); + + await act(async () => { + card.props.onDoubleClick(); + }); + + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + type: 'table', + tableName: 'd001', + initialViewMode: 'fields', + initialViewModeRequestId: expect.any(String), + })); + expect(storeState.addTab).not.toHaveBeenCalledWith(expect.objectContaining({ + type: 'design', + tableName: 'd001', + })); + }); + + it('uses the table default open behavior for list double-clicks', async () => { + storeState.appearance = { uiVersion: 'v2', tableDoubleClickAction: 'open-design' }; + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + await flushPromises(); + + const viewModeActions = renderer!.root.findAll((node) => ( + typeof node.props.onClick === 'function' && node.props.style?.padding === '3px 7px' + )); + expect(viewModeActions.length).toBeGreaterThanOrEqual(2); + await act(async () => { + viewModeActions[1].props.onClick(); + }); + + const listRow = renderer!.root.findAll((node) => node.props.className === 'gn-v2-table-row')[0]; + expect(listRow).toBeTruthy(); + + await act(async () => { + listRow.props.onDoubleClick(); + }); + + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + type: 'table', + tableName: 'd001', + initialViewMode: 'fields', + initialViewModeRequestId: expect.any(String), + })); + expect(storeState.addTab).not.toHaveBeenCalledWith(expect.objectContaining({ + type: 'design', + tableName: 'd001', + })); + }); }); diff --git a/frontend/src/components/TableOverview.tsx b/frontend/src/components/TableOverview.tsx index e7336ef..f5928be 100644 --- a/frontend/src/components/TableOverview.tsx +++ b/frontend/src/components/TableOverview.tsx @@ -259,6 +259,7 @@ const TableOverview: React.FC = ({ tab }) => { const setSidebarTablePinned = useStore(state => state.setSidebarTablePinned); const darkMode = theme === 'dark'; const isV2Ui = appearance.uiVersion === 'v2'; + const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data'; const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform()); const [tables, setTables] = useState([]); @@ -464,6 +465,30 @@ const TableOverview: React.FC = ({ tab }) => { }); }, [connection, tab.dbName, addTab, setActiveContext, supportsDesignWrite, t]); + const openTableObjectDesigner = useCallback((tableName: string) => { + if (!connection) return; + setActiveContext({ connectionId: connection.id, dbName: tab.dbName || '' }); + addTab({ + id: `${connection.id}-${tab.dbName}-${tableName}`, + title: tableName, + type: 'table', + connectionId: connection.id, + dbName: tab.dbName, + tableName, + initialViewMode: 'fields', + initialViewModeRequestId: String(Date.now()), + objectType: 'table', + }); + }, [connection, tab.dbName, addTab, setActiveContext]); + + const openTableByDefaultAction = useCallback((tableName: string) => { + if (tableDoubleClickAction === 'open-design') { + openTableObjectDesigner(tableName); + return; + } + openTable(tableName); + }, [openTable, openTableObjectDesigner, tableDoubleClickAction]); + const openTableDdl = useCallback((tableName: string) => { if (!connection) return; setActiveContext({ connectionId: connection.id, dbName: tab.dbName || '' }); @@ -990,7 +1015,7 @@ const TableOverview: React.FC = ({ tab }) => { const renderCardTableContent = (table: TableStatRow) => (
openTable(table.name)} + onDoubleClick={() => openTableByDefaultAction(table.name)} onContextMenu={isV2Ui ? (event) => openV2OverviewContextMenu(event, table) : undefined} style={{ background: cardBg, @@ -1059,7 +1084,7 @@ const TableOverview: React.FC = ({ tab }) => { const content = (
openTable(table.name)} + onDoubleClick={() => openTableByDefaultAction(table.name)} onContextMenu={isV2Ui ? (event) => openV2OverviewContextMenu(event, table) : undefined} style={{ position: 'relative', diff --git a/frontend/src/components/sidebar/useSidebarSearchModel.tsx b/frontend/src/components/sidebar/useSidebarSearchModel.tsx index bdbb9c6..aa2871f 100644 --- a/frontend/src/components/sidebar/useSidebarSearchModel.tsx +++ b/frontend/src/components/sidebar/useSidebarSearchModel.tsx @@ -32,7 +32,6 @@ import { import type { SearchScope } from '../sidebarCoreUtils'; import { buildV2CommandSearchTreeIndex, - V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE, estimateV2TreeHorizontalScrollWidth, filterV2CommandSearchTreeItems, filterV2ExplorerTreeByKind, @@ -612,9 +611,7 @@ export const useSidebarSearchModel = ({ () => estimateV2TreeHorizontalScrollWidth(v2VisibleTreeData, treeViewportWidth), [treeViewportWidth, v2VisibleTreeData], ); - const effectiveTreeHeight = isV2Ui && v2TreeHorizontalScrollWidth - ? Math.max(1, treeHeight - V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE) - : treeHeight; + const effectiveTreeHeight = treeHeight; const v2TreeMetrics = useMemo(() => { const databaseTableCounts = new Map(); const objectGroupCounts = new Map(); diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index 30b5506..3297998 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -69,6 +69,7 @@ describe('store appearance persistence', () => { expect(appearance.opacity).toBe(0.75); expect(appearance.blur).toBe(6); expect(appearance.useNativeMacWindowControls).toBe(true); + expect(appearance.tableDoubleClickAction).toBe('open-data'); expect(appearance.v2SidebarSearchMode).toBe('command'); expect(appearance.v2CommandSearchPersistentFilterEnabled).toBe(false); expect(appearance.v2SidebarPersistedFilter).toBe(''); @@ -93,11 +94,13 @@ describe('store appearance persistence', () => { useStore.getState().setAppearance({ showDataTableVerticalBorders: true, dataTableDensity: 'compact', + tableDoubleClickAction: 'open-design', }); const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); expect(persisted.state.appearance.showDataTableVerticalBorders).toBe(true); expect(persisted.state.appearance.dataTableDensity).toBe('compact'); + expect(persisted.state.appearance.tableDoubleClickAction).toBe('open-design'); vi.resetModules(); const reloaded = await importStore(); @@ -105,6 +108,21 @@ describe('store appearance persistence', () => { expect(appearance.showDataTableVerticalBorders).toBe(true); expect(appearance.dataTableDensity).toBe('compact'); + expect(appearance.tableDoubleClickAction).toBe('open-design'); + }); + + it('sanitizes invalid table double-click appearance settings', async () => { + storage.setItem('lite-db-storage', JSON.stringify({ + state: { + appearance: { + tableDoubleClickAction: 'open-random', + }, + }, + version: 10, + })); + + const { useStore } = await importStore(); + expect(useStore.getState().appearance.tableDoubleClickAction).toBe('open-data'); }); it('persists language preference and sanitizes unsupported persisted values', async () => { diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 57e8ace..1164ba0 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -79,12 +79,15 @@ import { resolveConnectionProtectionConfig, } from "./utils/connectionReadOnly"; +export type TableDoubleClickAction = "open-data" | "open-design"; + export interface AppearanceSettings extends DataGridDisplaySettings { uiVersion: "legacy" | "v2"; enabled: boolean; opacity: number; blur: number; useNativeMacWindowControls: boolean; + tableDoubleClickAction: TableDoubleClickAction; v2SidebarSearchMode: "command" | "filter"; v2CommandSearchPersistentFilterEnabled: boolean; v2SidebarPersistedFilter: string; @@ -100,6 +103,7 @@ export const DEFAULT_APPEARANCE: AppearanceSettings = { opacity: 1.0, blur: 0, useNativeMacWindowControls: false, + tableDoubleClickAction: "open-data", v2SidebarSearchMode: "command", v2CommandSearchPersistentFilterEnabled: false, v2SidebarPersistedFilter: "", @@ -126,6 +130,12 @@ const sanitizeV2SidebarSearchMode = ( return value === "filter" ? "filter" : DEFAULT_APPEARANCE.v2SidebarSearchMode; }; +const sanitizeTableDoubleClickAction = ( + value: unknown, +): TableDoubleClickAction => { + return value === "open-design" ? "open-design" : DEFAULT_APPEARANCE.tableDoubleClickAction; +}; + const sanitizeV2SidebarPersistedFilter = (value: unknown): string => { if (typeof value !== "string") { return DEFAULT_APPEARANCE.v2SidebarPersistedFilter; @@ -2062,6 +2072,9 @@ const sanitizeAppearance = ( typeof appearance.useNativeMacWindowControls === "boolean" ? appearance.useNativeMacWindowControls : DEFAULT_APPEARANCE.useNativeMacWindowControls, + tableDoubleClickAction: sanitizeTableDoubleClickAction( + appearance.tableDoubleClickAction, + ), v2SidebarSearchMode: sanitizeV2SidebarSearchMode( appearance.v2SidebarSearchMode, ), diff --git a/frontend/src/v2-theme.css b/frontend/src/v2-theme.css index e460c56..9374a49 100644 --- a/frontend/src/v2-theme.css +++ b/frontend/src/v2-theme.css @@ -2608,12 +2608,16 @@ body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree { } body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list { - height: calc(100% - var(--gn-v2-tree-horizontal-scroll-reserve)); + position: relative; + height: 100%; min-height: 0; box-sizing: border-box; + padding-bottom: var(--gn-v2-tree-horizontal-scroll-reserve); } body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-holder { + box-sizing: border-box; + padding-bottom: var(--gn-v2-tree-horizontal-scroll-reserve); scrollbar-width: thin; } @@ -2621,7 +2625,7 @@ body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-scrollbar-h height: 12px !important; left: 8px !important; right: 8px !important; - bottom: calc((var(--gn-v2-tree-horizontal-scroll-reserve) - 12px) * -1) !important; + bottom: calc((var(--gn-v2-tree-horizontal-scroll-reserve) - 12px) / 2) !important; } body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-scrollbar-horizontal .ant-tree-list-scrollbar-thumb {