♻️ refactor(sidebar): 抽离独立工具函数到 sidebarHelpers 模块

- 新建 sidebar/sidebarHelpers.ts:迁出 6 个无内部类型依赖的纯函数(formatSidebarRowCount/hasSidebarLazyChildren/getV2RailConnectionGroupBadgeText 等)+ V2ExplorerFilter 类型 + V2_RAIL_UNGROUPED 常量
- Sidebar.tsx 通过 import + re-export 双向引用,外部测试文件的 `from './Sidebar'` 保持兼容
- 文件规模:Sidebar.tsx 从 10275 减至 10243 行,建立 sidebar/ 子目录作为后续拆分的目标归宿
This commit is contained in:
Syngnat
2026-06-19 14:11:54 +08:00
parent dc54d24d2b
commit 87bd16c4ba
2 changed files with 118 additions and 50 deletions

View File

@@ -1,4 +1,22 @@
import Modal from './common/ResizableDraggableModal';
import {
V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
formatSidebarRowCount,
hasSidebarLazyChildren,
shouldClearSidebarActiveContextOnEmptySelect,
getV2RailConnectionGroupBadgeText,
isV2SidebarObjectNode,
type V2ExplorerFilter,
} from './sidebar/sidebarHelpers';
// 重新导出,保持外部测试文件的 `from './Sidebar'` 兼容
export {
V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
formatSidebarRowCount,
hasSidebarLazyChildren,
shouldClearSidebarActiveContextOnEmptySelect,
getV2RailConnectionGroupBadgeText,
isV2SidebarObjectNode,
} from './sidebar/sidebarHelpers';
import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredValue } from 'react';
import { createPortal } from 'react-dom';
import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Badge, Checkbox, Space, Select, Popover, Tooltip, Progress, Switch } from 'antd';
@@ -278,19 +296,6 @@ export const SQLFileExecutionProgressContent: React.FC<SQLFileExecutionProgressS
</>
);
const isV2SidebarObjectNode = (node: Pick<TreeNode, 'type'> | null | undefined): boolean => {
return node?.type === 'table'
|| node?.type === 'view'
|| node?.type === 'materialized-view'
|| node?.type === 'db-trigger'
|| node?.type === 'db-event'
|| node?.type === 'routine';
};
export const hasSidebarLazyChildren = (children: unknown): boolean => {
return Array.isArray(children) && children.length > 0;
};
export const shouldLoadSidebarNodeOnExpand = (
node: Pick<TreeNode, 'type' | 'children' | 'isLeaf'> | null | undefined,
): boolean => {
@@ -399,12 +404,6 @@ export const buildSidebarTableChildrenForUi = (
return buildV2SidebarTableSectionedChildren(parentKey, tableNodes);
};
export const formatSidebarRowCount = (count: number): string => {
if (!Number.isFinite(count) || count < 0) return '';
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
return String(Math.round(count));
};
const buildConnectionRootQueryTabTitle = () => t('query.new');
@@ -418,9 +417,6 @@ type BatchTableExportMode = 'schema' | 'backup' | 'dataOnly';
type BatchObjectType = 'table' | 'view';
type BatchObjectFilterType = 'all' | BatchObjectType;
type BatchSelectionScope = 'filtered' | 'all';
export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events';
export const V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID = '__gonavi-v2-ungrouped-connections__';
export interface V2RailConnectionGroup {
id: string;
@@ -508,33 +504,6 @@ export const buildV2RailConnectionGroups = (
return groups;
};
export const getV2RailConnectionGroupBadgeText = (name: unknown, fallback = t('connection.sidebar.group.badge')): string => {
const trimmed = String(name ?? '').trim();
if (!trimmed) return fallback;
const cjkParts = trimmed.match(/[\u4e00-\u9fa5]/g);
if (cjkParts && cjkParts.length > 0) {
return cjkParts.slice(0, 1).join('');
}
const latinTokens = trimmed.match(/[a-z0-9]+/gi) || [];
if (latinTokens.length >= 2) {
const firstToken = latinTokens[0] || '';
const secondToken = latinTokens[1] || '';
return `${firstToken[0] || ''}${secondToken[0] || ''}`.toUpperCase();
}
if (latinTokens.length === 1) {
const token = latinTokens[0] || '';
const alphaPrefix = token.match(/^[a-z]+/i)?.[0] || '';
if (alphaPrefix) {
return alphaPrefix.slice(0, 2).toUpperCase();
}
const trailingDigits = token.match(/(\d{2,})$/)?.[1];
if (trailingDigits) {
return trailingDigits.slice(-2).toUpperCase();
}
return token.slice(0, 2).toUpperCase();
}
return trimmed.slice(0, 2);
};
const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; labelKey: string }> = [
{ key: 'all', labelKey: 'sidebar.command_search.object_kind.all' },
@@ -894,7 +863,6 @@ export const resolveV2ActiveConnectionId = ({
|| '';
};
export const shouldClearSidebarActiveContextOnEmptySelect = (isV2Ui: boolean): boolean => !isV2Ui;
type DriverStatusSnapshot = {
type: string;

View File

@@ -0,0 +1,100 @@
// Sidebar 工具函数集合(第一期:纯函数 + 共享常量/类型)。
//
// 本文件是 Sidebar.tsx 拆分的第一步,只搬迁完全独立、无内部类型依赖的工具函数。
// 后续 PR 会继续搬迁更多工具函数和子组件。
//
// 设计原则:
// - 只放纯函数(无副作用、无 React state
// - 不依赖 Sidebar.tsx 内部的 TreeNode 类型(用结构化类型参数代替)
// - 共享常量和类型集中管理,便于跨文件复用
import { t } from '../../i18n';
// === 共享常量 ===
/** V2 Rail 中"未分组连接"组的固定 ID */
export const V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID = '__gonavi-v2-ungrouped-connections__';
// === 共享类型 ===
/** V2 资源管理器过滤维度 */
export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events';
// === 纯函数 ===
/**
* formatSidebarRowCount 把行数格式化为人类可读的简短形式。
* - >= 1M 显示为 "1.2M"
* - >= 1K 显示为 "1.2K"
* - 否则显示原数字
* - 非法值NaN/负数)返回空字符串
*/
export const formatSidebarRowCount = (count: number): string => {
if (!Number.isFinite(count) || count < 0) return '';
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`;
return String(Math.round(count));
};
/**
* hasSidebarLazyChildren 判断树节点的 children 是否已加载(用于按需展开)。
*/
export const hasSidebarLazyChildren = (children: unknown): boolean => {
return Array.isArray(children) && children.length > 0;
};
/**
* shouldClearSidebarActiveContextOnEmptySelect 判断在空选择时是否清空激活上下文。
* 仅 legacy UI 需要清空V2 UI 保留上下文。
*/
export const shouldClearSidebarActiveContextOnEmptySelect = (isV2Ui: boolean): boolean => !isV2Ui;
/**
* getV2RailConnectionGroupBadgeText 从组名生成 1-2 字符的徽章文本。
* 中文取首字;英文取前两个 token 的首字母大写;其他取前 2 字符。
*/
export const getV2RailConnectionGroupBadgeText = (
name: unknown,
fallback = t('connection.sidebar.group.badge'),
): string => {
const trimmed = String(name ?? '').trim();
if (!trimmed) return fallback;
const cjkParts = trimmed.match(/[一-龥]/g);
if (cjkParts && cjkParts.length > 0) {
return cjkParts.slice(0, 1).join('');
}
const latinTokens = trimmed.match(/[a-z0-9]+/gi) || [];
if (latinTokens.length >= 2) {
const firstToken = latinTokens[0] || '';
const secondToken = latinTokens[1] || '';
return `${firstToken[0] || ''}${secondToken[0] || ''}`.toUpperCase();
}
if (latinTokens.length === 1) {
const token = latinTokens[0] || '';
const alphaPrefix = token.match(/^[a-z]+/i)?.[0] || '';
if (alphaPrefix) {
return alphaPrefix.slice(0, 2).toUpperCase();
}
const trailingDigits = token.match(/(\d{2,})$/)?.[1];
if (trailingDigits) {
return trailingDigits.slice(-2).toUpperCase();
}
return token.slice(0, 2).toUpperCase();
}
return trimmed.slice(0, 2);
};
/**
* isV2SidebarObjectNode 判断节点是否是 SQL 对象类型(表/视图/触发器/事件/存储过程)。
* 接收结构化类型而非 TreeNode避免对 Sidebar 内部类型的硬依赖。
*/
export const isV2SidebarObjectNode = (
node: { type?: string } | null | undefined,
): boolean => {
return node?.type === 'table'
|| node?.type === 'view'
|| node?.type === 'materialized-view'
|| node?.type === 'db-trigger'
|| node?.type === 'db-event'
|| node?.type === 'routine';
};