mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 09:21:38 +08:00
♻️ 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:
@@ -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;
|
||||
|
||||
100
frontend/src/components/sidebar/sidebarHelpers.ts
Normal file
100
frontend/src/components/sidebar/sidebarHelpers.ts
Normal 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';
|
||||
};
|
||||
Reference in New Issue
Block a user