♻️ refactor(sidebar): 抽出 V2 树标题渲染

This commit is contained in:
Syngnat
2026-06-19 18:03:28 +08:00
parent 293a8ff3e9
commit 1dea343aa2
3 changed files with 173 additions and 132 deletions

View File

@@ -70,6 +70,7 @@ const readSidebarSource = () => [
readSourceFile('./sidebar/SidebarExternalSqlWorkflow.tsx'),
readSourceFile('./sidebar/useSidebarTreeLoaders.tsx'),
readSourceFile('./sidebar/SidebarEntityModals.tsx'),
readSourceFile('./sidebar/SidebarTreeTitle.tsx'),
readSourceFile('./sidebarV2Utils.ts'),
].join('\n');
const readLegacyNodeMenuSource = () => readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx');
@@ -2340,9 +2341,9 @@ describe('Sidebar locate toolbar', () => {
const externalSqlFlowStart = source.indexOf('const handleAddExternalSQLDirectory = async (node: any) => {');
const externalSqlFlowEnd = source.indexOf('const cancelSQLFileExecution = () => {', externalSqlFlowStart);
const externalSqlFlowSource = source.slice(externalSqlFlowStart, externalSqlFlowEnd);
const treeTitleStart = source.indexOf('const renderV2TreeTitle = (node: any, hoverTitle: string, statusBadge: React.ReactNode) => {');
const treeTitleEnd = source.indexOf('const selectConnectionFromRail', treeTitleStart);
const treeTitleSource = source.slice(treeTitleStart, treeTitleEnd);
const treeTitleSource = readSourceFile('./sidebar/SidebarTreeTitle.tsx');
const treeTitleStart = 0;
const treeTitleEnd = treeTitleSource.length;
const externalSqlMenuStart = legacyMenuSource.indexOf("if (node.type === 'external-sql-root') {", legacyMenuSource.indexOf('// 已存查询节点的右键菜单'));
const externalSqlMenuEnd = legacyMenuSource.indexOf("if (node.type === 'external-sql-directory') {", externalSqlMenuStart);
const externalSqlMenuSource = legacyMenuSource.slice(externalSqlMenuStart, externalSqlMenuEnd);
@@ -2593,11 +2594,12 @@ describe('Sidebar locate toolbar', () => {
expect(tableGroupCallSource).not.toContain(rawSnippet);
});
const treeTitleStart = sidebarSource.indexOf('const renderV2TreeTitle');
const treeTitleEnd = sidebarSource.indexOf('const selectConnectionFromRail', treeTitleStart);
const treeTitleModuleSource = readSourceFile('./sidebar/SidebarTreeTitle.tsx');
const treeTitleStart = treeTitleModuleSource.indexOf('export const renderSidebarV2TreeTitle');
const treeTitleEnd = treeTitleModuleSource.length;
expect(treeTitleStart).toBeGreaterThanOrEqual(0);
expect(treeTitleEnd).toBeGreaterThan(treeTitleStart);
const treeTitleSource = sidebarSource.slice(treeTitleStart, treeTitleEnd);
const treeTitleSource = treeTitleModuleSource.slice(treeTitleStart, treeTitleEnd);
expect(treeTitleSource).toContain('const objectGroupTitle = resolveV2ObjectGroupTitle(node);');
expect(treeTitleSource).toContain('if (objectGroupTitle) return objectGroupTitle;');

View File

@@ -19,6 +19,7 @@ import {
} from './sidebar/useSidebarBatchExport';
import { SidebarBatchExportModals } from './sidebar/SidebarBatchExportModals';
import { SidebarEntityModals } from './sidebar/SidebarEntityModals';
import { renderSidebarV2TreeTitle } from './sidebar/SidebarTreeTitle';
import {
normalizeDriverType,
useSidebarTreeLoaders,
@@ -108,9 +109,7 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Badge, Checkbo
MoreOutlined,
ToolOutlined,
SettingOutlined,
BarsOutlined,
StarFilled,
StarOutlined
BarsOutlined
} from '@ant-design/icons';
import {
buildSidebarRootConnectionToken,
@@ -4151,129 +4150,17 @@ const Sidebar: React.FC<{
void fetchV2TableContextMenuStats(node);
};
const renderV2TreeTitle = (node: any, hoverTitle: string, statusBadge: React.ReactNode) => {
const rawTitle = String(node.title ?? '');
const groupKey = String(node?.dataRef?.groupKey || '');
const dragText = resolveSidebarObjectDragText(node);
if (node.type === 'v2-table-section') {
return (
<span
className="gn-v2-tree-section-title"
data-section-kind={node?.dataRef?.sectionKind || undefined}
title={rawTitle}
>
{rawTitle}
</span>
);
}
const displayTitle = (() => {
if (node.type === 'queries-folder') return t('sidebar.tree.saved_queries');
if (node.type === 'external-sql-root') return t('sidebar.external_sql.root');
if (node.type === 'object-group') {
const objectGroupTitle = resolveV2ObjectGroupTitle(node);
if (objectGroupTitle) return objectGroupTitle;
}
return rawTitle;
})();
const metaText = getV2TreeMetaText(node);
const isMono = node.type === 'table'
|| node.type === 'view'
|| node.type === 'materialized-view'
|| node.type === 'db-trigger'
|| node.type === 'db-event'
|| node.type === 'routine'
|| node.type === 'saved-query'
|| node.type === 'external-sql-file';
const titleClassName = [
'gn-v2-tree-title',
isMono ? 'is-mono' : '',
node.type === 'object-group' ? 'is-group' : '',
node.type === 'table' && node?.dataRef?.pinnedSidebarTable ? 'is-pinned-table' : '',
].filter(Boolean).join(' ');
const tablePinAction = node.type === 'table' ? (
<button
type="button"
className={[
'gn-v2-table-pin-action',
node?.dataRef?.pinnedSidebarTable ? 'is-pinned' : '',
].filter(Boolean).join(' ')}
title={node?.dataRef?.pinnedSidebarTable ? t('sidebar.action.unpin_table') : t('sidebar.action.pin_table')}
aria-label={node?.dataRef?.pinnedSidebarTable ? t('sidebar.action.unpin_table') : t('sidebar.action.pin_table')}
aria-pressed={node?.dataRef?.pinnedSidebarTable ? true : false}
data-v2-sidebar-table-pin-action="true"
onMouseDown={(event) => {
event.preventDefault();
event.stopPropagation();
}}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
toggleSidebarTablePinned(node);
}}
onDoubleClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
>
{node?.dataRef?.pinnedSidebarTable ? <StarFilled /> : <StarOutlined />}
</button>
) : null;
if (node.type === 'connection') {
return (
<span
className={`${titleClassName} is-connection`}
title={hoverTitle}
data-node-type={node.type}
data-sidebar-node-key={String(node.key || '')}
data-sidebar-node-type={String(node.type || '')}
>
{statusBadge}
<span className="gn-v2-tree-connection-copy">
<span className="gn-v2-tree-label">{displayTitle}</span>
</span>
</span>
);
}
return (
<>
<span
className={titleClassName}
title={hoverTitle}
draggable={!!dragText}
data-node-type={node.type}
data-group-key={groupKey || undefined}
data-sidebar-node-key={String(node.key || '')}
data-sidebar-node-type={String(node.type || '')}
onDragStart={dragText ? (event) => {
snapshotTreeSelectionBeforeDrag();
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
setIsTreeDragging(true);
event.stopPropagation();
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.setData('text/plain', dragText);
event.dataTransfer.setData(
SIDEBAR_SQL_EDITOR_DRAG_MIME,
encodeSidebarSqlEditorDragPayload({
text: dragText,
nodeType: node.type,
connectionId: String(node?.dataRef?.id || ''),
dbName: String(node?.dataRef?.dbName || ''),
}),
);
} : undefined}
onDragEnd={dragText ? () => {
restoreTreeSelectionAfterDrag();
setIsTreeDragging(false);
} : undefined}
>
{statusBadge}
<span className="gn-v2-tree-label">{displayTitle}</span>
{metaText && <span className="gn-v2-tree-count">{metaText}</span>}
</span>
{tablePinAction}
</>
);
};
const renderV2TreeTitle = (node: any, hoverTitle: string, statusBadge: React.ReactNode) => renderSidebarV2TreeTitle({
node,
hoverTitle,
statusBadge,
getV2TreeMetaText,
toggleSidebarTablePinned,
snapshotTreeSelectionBeforeDrag,
restoreTreeSelectionAfterDrag,
treeDragSelectSuppressUntilRef,
setIsTreeDragging,
});
const selectConnectionFromRail = useCallback((conn: SavedConnection) => {
const key = conn.id;

View File

@@ -0,0 +1,152 @@
import React from 'react';
import { StarFilled, StarOutlined } from '@ant-design/icons';
import { t } from '../../i18n';
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from '../../utils/sidebarSqlDrag';
import { resolveSidebarObjectDragText } from '../sidebarCoreUtils';
import { resolveV2ObjectGroupTitle } from './sidebarHelpers';
type SidebarV2TreeTitleOptions = {
node: any;
hoverTitle: string;
statusBadge: React.ReactNode;
getV2TreeMetaText: (node: any) => string;
toggleSidebarTablePinned: (node: any) => void;
snapshotTreeSelectionBeforeDrag: () => void;
restoreTreeSelectionAfterDrag: () => void;
treeDragSelectSuppressUntilRef: React.MutableRefObject<number>;
setIsTreeDragging: (dragging: boolean) => void;
};
export const renderSidebarV2TreeTitle = ({
node,
hoverTitle,
statusBadge,
getV2TreeMetaText,
toggleSidebarTablePinned,
snapshotTreeSelectionBeforeDrag,
restoreTreeSelectionAfterDrag,
treeDragSelectSuppressUntilRef,
setIsTreeDragging,
}: SidebarV2TreeTitleOptions): React.ReactNode => {
const rawTitle = String(node.title ?? '');
const groupKey = String(node?.dataRef?.groupKey || '');
const dragText = resolveSidebarObjectDragText(node);
if (node.type === 'v2-table-section') {
return (
<span
className="gn-v2-tree-section-title"
data-section-kind={node?.dataRef?.sectionKind || undefined}
title={rawTitle}
>
{rawTitle}
</span>
);
}
const displayTitle = (() => {
if (node.type === 'queries-folder') return t('sidebar.tree.saved_queries');
if (node.type === 'external-sql-root') return t('sidebar.external_sql.root');
if (node.type === 'object-group') {
const objectGroupTitle = resolveV2ObjectGroupTitle(node);
if (objectGroupTitle) return objectGroupTitle;
}
return rawTitle;
})();
const metaText = getV2TreeMetaText(node);
const isMono = node.type === 'table'
|| node.type === 'view'
|| node.type === 'materialized-view'
|| node.type === 'db-trigger'
|| node.type === 'db-event'
|| node.type === 'routine'
|| node.type === 'saved-query'
|| node.type === 'external-sql-file';
const titleClassName = [
'gn-v2-tree-title',
isMono ? 'is-mono' : '',
node.type === 'object-group' ? 'is-group' : '',
node.type === 'table' && node?.dataRef?.pinnedSidebarTable ? 'is-pinned-table' : '',
].filter(Boolean).join(' ');
const tablePinAction = node.type === 'table' ? (
<button
type="button"
className={[
'gn-v2-table-pin-action',
node?.dataRef?.pinnedSidebarTable ? 'is-pinned' : '',
].filter(Boolean).join(' ')}
title={node?.dataRef?.pinnedSidebarTable ? t('sidebar.action.unpin_table') : t('sidebar.action.pin_table')}
aria-label={node?.dataRef?.pinnedSidebarTable ? t('sidebar.action.unpin_table') : t('sidebar.action.pin_table')}
aria-pressed={node?.dataRef?.pinnedSidebarTable ? true : false}
data-v2-sidebar-table-pin-action="true"
onMouseDown={(event) => {
event.preventDefault();
event.stopPropagation();
}}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
toggleSidebarTablePinned(node);
}}
onDoubleClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
>
{node?.dataRef?.pinnedSidebarTable ? <StarFilled /> : <StarOutlined />}
</button>
) : null;
if (node.type === 'connection') {
return (
<span
className={`${titleClassName} is-connection`}
title={hoverTitle}
data-node-type={node.type}
data-sidebar-node-key={String(node.key || '')}
data-sidebar-node-type={String(node.type || '')}
>
{statusBadge}
<span className="gn-v2-tree-connection-copy">
<span className="gn-v2-tree-label">{displayTitle}</span>
</span>
</span>
);
}
return (
<>
<span
className={titleClassName}
title={hoverTitle}
draggable={!!dragText}
data-node-type={node.type}
data-group-key={groupKey || undefined}
data-sidebar-node-key={String(node.key || '')}
data-sidebar-node-type={String(node.type || '')}
onDragStart={dragText ? (event) => {
snapshotTreeSelectionBeforeDrag();
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
setIsTreeDragging(true);
event.stopPropagation();
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.setData('text/plain', dragText);
event.dataTransfer.setData(
SIDEBAR_SQL_EDITOR_DRAG_MIME,
encodeSidebarSqlEditorDragPayload({
text: dragText,
nodeType: node.type,
connectionId: String(node?.dataRef?.id || ''),
dbName: String(node?.dataRef?.dbName || ''),
}),
);
} : undefined}
onDragEnd={dragText ? () => {
restoreTreeSelectionAfterDrag();
setIsTreeDragging(false);
} : undefined}
>
{statusBadge}
<span className="gn-v2-tree-label">{displayTitle}</span>
{metaText && <span className="gn-v2-tree-count">{metaText}</span>}
</span>
{tablePinAction}
</>
);
};