feat(context-menu): 增强字段与数据库名称复制交互

数据表列头支持按元数据复制字段注释;数据库节点旧版与 V2 菜单支持复制数据库名;补齐六语言文案与回归测试。

Fixes #547
This commit is contained in:
Syngnat
2026-07-19 18:17:14 +08:00
parent ea9ccb3648
commit 54c06ed113
18 changed files with 146 additions and 3 deletions

View File

@@ -1234,6 +1234,7 @@ describe('DataGrid DDL interactions', () => {
expect(renderer!.root.findByProps({ 'data-v2-column-context-menu': 'true' })).toBeTruthy();
expect(textContent(renderer!.root)).toContain(t('sidebar.v2_table_menu.copy_section'));
expect(textContent(renderer!.root)).toContain(t('data_grid.context_menu.copy_field_name'));
expect(textContent(renderer!.root)).toContain(t('data_grid.context_menu.copy_column_comment'));
expect(textContent(renderer!.root)).toContain(t('data_grid.context_menu.copy_column_data'));
expect(textContent(renderer!.root)).toContain(t('data_grid.context_menu.sort_ascending'));
expect(textContent(renderer!.root)).toContain(t('data_grid.context_menu.hide_column'));
@@ -1241,6 +1242,15 @@ describe('DataGrid DDL interactions', () => {
expect(textContent(renderer!.root)).toContain(t('data_grid.context_menu.hide_column_comment'));
expect(textContent(renderer!.root)).toContain('bigint');
expect(textContent(renderer!.root)).toContain('主键 ID');
await act(async () => {
findButton(renderer!, t('data_grid.context_menu.copy_column_comment')).props.onClick({
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
});
});
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('主键 ID');
renderer!.unmount();
});
@@ -1509,6 +1519,7 @@ describe('DataGrid DDL interactions', () => {
expect(content).toContain(t('data_grid.context_menu.column_unnamed_field'));
expect(content).toContain(t('data_grid.context_menu.column_unknown_type'));
expect(content).toContain(t('data_grid.context_menu.column_no_comment'));
expect(content).not.toContain(t('data_grid.context_menu.copy_column_comment'));
expect(content).toContain(t('data_grid.context_menu.show_column_type'));
expect(content).toContain(t('data_grid.context_menu.show_column_comment'));
renderer.unmount();
@@ -1586,6 +1597,7 @@ describe('DataGrid DDL interactions', () => {
"t('data_grid.context_menu.column_no_comment')",
"t('data_grid.context_menu.column_unnamed_field')",
"t('data_grid.context_menu.copy_field_name')",
"t('data_grid.context_menu.copy_column_comment')",
"t('data_grid.context_menu.copy_column_data')",
"t('data_grid.context_menu.sort_section')",
"t('data_grid.context_menu.sort_ascending')",
@@ -1611,6 +1623,7 @@ describe('DataGrid DDL interactions', () => {
'暂无备注',
'未命名字段',
'复制字段名称',
'复制注释',
'复制列数据',
'排序',
'升序排序',

View File

@@ -32,6 +32,7 @@ import Sidebar, {
isSidebarTablePinned,
SQLFileExecutionProgressContent,
resolveSidebarTableNameForCopy,
resolveSidebarDatabaseNameForCopy,
shouldKeepSidebarSwitcherCollapsedWhileLoading,
shouldClearSidebarActiveContextOnEmptySelect,
shouldSkipSidebarLoadOnExpandWhileDragging,
@@ -2499,6 +2500,7 @@ describe('Sidebar locate toolbar', () => {
expect(markup).toContain('data-v2-database-context-menu="true"');
expect(markup).toContain('mkefu_ai_dev');
expect(markup).toContain('DB');
expect(markup).toContain(t('sidebar.menu.copy_database_name'));
expect(markup).toContain(t('sidebar.menu.create_table'));
expect(markup).toContain(t('sidebar.menu.new_query'));
expect(markup).toContain(t('sidebar.sql_file_exec.title'));
@@ -2515,6 +2517,29 @@ describe('Sidebar locate toolbar', () => {
expect(markup).toContain(t('sidebar.v2_table_menu.item_with_suffix', { label: t('sidebar.menu.delete_database'), suffix: 'DROP' }));
});
it('resolves and wires database-name copy for both sidebar menu generations', () => {
expect(resolveSidebarDatabaseNameForCopy({
title: 'fallback_db',
dataRef: { dbName: ' main_db ' },
})).toBe('main_db');
expect(resolveSidebarDatabaseNameForCopy({ title: ' fallback_db ' })).toBe('fallback_db');
expect(resolveSidebarDatabaseNameForCopy(null)).toBe('');
const legacySource = readLegacyNodeMenuSource();
const menuSource = readSourceFile('./V2TableContextMenu.tsx');
const actionSource = readSourceFile('./sidebar/useSidebarV2ActionHandlers.tsx');
const objectActionSource = readSourceFile('./sidebar/useSidebarObjectActions.tsx');
expect(legacySource).toContain("t('sidebar.menu.copy_database_name')");
expect(legacySource).toContain("handleV2DatabaseContextMenuAction(node, 'copy-database-name')");
expect(menuSource).toContain("action: 'copy-database-name'");
expect(menuSource).toContain("t('sidebar.menu.copy_database_name')");
expect(actionSource).toContain("case 'copy-database-name':");
expect(actionSource).toContain('void handleCopyDatabaseName(node);');
expect(objectActionSource).toContain('const handleCopyDatabaseName = async (node: any) => {');
expect(objectActionSource).toContain('await navigator.clipboard.writeText(databaseName);');
});
it('renders the v2 database schema action for PostgreSQL-compatible databases', () => {
const markup = renderToStaticMarkup(
<V2DatabaseContextMenuView

View File

@@ -66,6 +66,7 @@ export {
isV2SidebarObjectNode,
resolveV2ObjectGroupTitle,
resolveSidebarTableNameForCopy,
resolveSidebarDatabaseNameForCopy,
parseV2CommandSearchQuery,
} from './sidebar/sidebarHelpers';
import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredValue } from 'react';
@@ -2285,6 +2286,7 @@ const Sidebar: React.FC<{
const {
handleCopyStructure,
handleCopyTableName,
handleCopyDatabaseName,
handleExport,
openExportDialog,
handleCopyTableAsInsert,
@@ -2439,6 +2441,7 @@ const Sidebar: React.FC<{
openTableDdlInDesigner,
openTableInERView,
handleCopyTableName,
handleCopyDatabaseName,
handleCopyStructure,
handleCopyTableAsInsert,
openCreateStarRocksRollup,

View File

@@ -8,6 +8,7 @@ const source = [
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
const requiredKeys = [
'sidebar.copy_object_name.label.database',
'sidebar.copy_object_name.label.table',
'sidebar.copy_object_name.label.view',
'sidebar.copy_object_name.label.materialized_view',
@@ -30,6 +31,7 @@ describe('Sidebar copy object name i18n', () => {
expect(source).not.toContain('`${label}为空,无法复制`');
expect(source).not.toContain('`${label}已复制到剪贴板`');
expect(source).not.toContain('`复制${label}失败: `');
expect(source).toContain("t('sidebar.copy_object_name.label.database')");
expect(source).toContain("t('sidebar.copy_object_name.label.view')");
expect(source).toContain("t('sidebar.copy_object_name.label.materialized_view')");
expect(source).toContain("t('sidebar.copy_object_name.label.sequence')");

View File

@@ -1,10 +1,11 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { buildSidebarLegacyNodeMenuItems } from './sidebar/sidebarLegacyNodeMenu';
const source = readFileSync(new URL('./sidebar/sidebarLegacyNodeMenu.tsx', import.meta.url), 'utf8');
describe('Sidebar legacy database menu order', () => {
it('keeps new query and run-sql ahead of close database', () => {
it('keeps copy database name first and query actions ahead of close database', () => {
const databaseMenuStart = source.indexOf("} else if (node.type === 'database') {");
const databaseMenuEnd = source.indexOf("} else if (node.type === 'view') {", databaseMenuStart);
@@ -12,14 +13,40 @@ describe('Sidebar legacy database menu order', () => {
expect(databaseMenuEnd).toBeGreaterThan(databaseMenuStart);
const databaseMenuSource = source.slice(databaseMenuStart, databaseMenuEnd);
const copyDatabaseNameIndex = databaseMenuSource.indexOf("key: 'copy-database-name'");
const newQueryIndex = databaseMenuSource.indexOf("key: 'new-query'");
const runSqlIndex = databaseMenuSource.indexOf("key: 'run-sql'");
const disconnectIndex = databaseMenuSource.indexOf("key: 'disconnect-db'");
expect(copyDatabaseNameIndex).toBeGreaterThanOrEqual(0);
expect(newQueryIndex).toBeGreaterThanOrEqual(0);
expect(runSqlIndex).toBeGreaterThanOrEqual(0);
expect(disconnectIndex).toBeGreaterThanOrEqual(0);
expect(copyDatabaseNameIndex).toBeLessThan(newQueryIndex);
expect(newQueryIndex).toBeLessThan(disconnectIndex);
expect(runSqlIndex).toBeLessThan(disconnectIndex);
});
it('routes copy database name through the shared database action handler', () => {
const handleV2DatabaseContextMenuAction = vi.fn();
const node = {
type: 'database',
title: 'main_db',
dataRef: { id: 'mysql-1', dbName: 'main_db', config: { type: 'mysql' } },
};
const items = buildSidebarLegacyNodeMenuItems(node, {
getMetadataDialect: () => 'mysql',
isPostgresSchemaDialect: () => false,
shouldHideSchemaPrefix: () => false,
isStructureOnlyDbType: () => false,
handleV2DatabaseContextMenuAction,
}) as Array<{ key?: string; onClick?: () => void }>;
const copyItem = items.find((item) => item?.key === 'copy-database-name');
expect(copyItem).toBeTruthy();
copyItem?.onClick?.();
expect(handleV2DatabaseContextMenuAction).toHaveBeenCalledWith(node, 'copy-database-name');
});
});

View File

@@ -24,6 +24,8 @@ const requiredKeys = [
'sidebar.menu.table_structure',
'sidebar.menu.design_table',
'sidebar.menu.copy_table_name',
'sidebar.menu.copy_database_name',
'sidebar.copy_object_name.label.database',
'sidebar.menu.copy_table_structure',
'sidebar.menu.backup_table_sql',
'sidebar.menu.rename_table',
@@ -158,6 +160,7 @@ describe('Sidebar object actions i18n', () => {
"t('sidebar.menu.table_structure')",
"t('sidebar.menu.design_table')",
"t('sidebar.menu.copy_table_name')",
"t('sidebar.menu.copy_database_name')",
"t('sidebar.menu.copy_table_structure')",
"t('sidebar.menu.backup_table_sql')",
"t('sidebar.menu.rename_table')",

View File

@@ -320,6 +320,7 @@ export const V2TableGroupContextMenuView: React.FC<{
};
export type V2DatabaseContextMenuActionKey =
| 'copy-database-name'
| 'new-table'
| 'new-schema'
| 'new-materialized-view'
@@ -378,6 +379,7 @@ export const V2DatabaseContextMenuView: React.FC<{
<div className="gn-v2-context-menu-body">
{renderItems([
{ action: 'copy-database-name', icon: <CopyOutlined />, title: t('sidebar.menu.copy_database_name'), kbd: primaryShortcut('C', shortcutPlatform), featured: true },
{ action: 'new-table', icon: <TableOutlined />, title: t('sidebar.menu.create_table'), kbd: primaryShortcut('N', shortcutPlatform), featured: true },
...(supportsSchemaActions ? [{ action: 'new-schema', icon: <FolderAddOutlined />, title: t('sidebar.v2_database_menu.new_schema') }] : []),
...(supportsSchemaVisibility ? [{ action: 'schema-visibility', icon: <FolderOpenOutlined />, title: t('sidebar.schema_visibility.menu.manage') }] : []),
@@ -647,6 +649,7 @@ export type V2CellContextMenuActionKey =
export type V2ColumnHeaderContextMenuActionKey =
| 'copy-field-name'
| 'copy-column-comment'
| 'copy-column-data'
| 'sort-asc'
| 'sort-desc'
@@ -707,6 +710,7 @@ export const V2ColumnHeaderContextMenuView: React.FC<{
<div className="gn-v2-context-menu-section-title">{t('sidebar.v2_table_menu.copy_section')}</div>
{renderItems([
{ action: 'copy-field-name', icon: <CopyOutlined />, title: t('data_grid.context_menu.copy_field_name'), kbd: primaryShortcut('C', shortcutPlatform), featured: true },
...(normalizedComment ? [{ action: 'copy-column-comment', icon: <CopyOutlined />, title: t('data_grid.context_menu.copy_column_comment') }] : []),
{ action: 'copy-column-data', icon: <CopyOutlined />, title: t('data_grid.context_menu.copy_column_data') },
])}

View File

@@ -272,6 +272,11 @@ export const resolveSidebarTableNameForCopy = (
return String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.sequenceName || node?.dataRef?.packageName || node?.dataRef?.eventName || node?.title || '').trim();
};
/** resolveSidebarDatabaseNameForCopy extracts the exact database identifier shown by the node. */
export const resolveSidebarDatabaseNameForCopy = (
node: Pick<SidebarNodeLike, 'title' | 'dataRef'> | null | undefined,
): string => String(node?.dataRef?.dbName || node?.title || '').trim();
// === 命令搜索相关类型与解析V2 Command Search===
/** 命令搜索模式default默认/ object@前缀,对象搜索)/ ai?或前缀AI 提问) */

View File

@@ -651,6 +651,12 @@ export const buildSidebarLegacyNodeMenuItems = (
&& shouldHideSchemaPrefix(databaseConn);
const canCreateTable = !isStructureOnlyDbType(String(databaseConn?.id || ''));
return [
{
key: 'copy-database-name',
label: t('sidebar.menu.copy_database_name'),
icon: <CopyOutlined />,
onClick: () => handleV2DatabaseContextMenuAction(node, 'copy-database-name')
},
...(canCreateTable ? [{
key: 'new-table',
label: t('sidebar.menu.create_table'),

View File

@@ -22,7 +22,10 @@ import {
getMetadataDialect,
splitQualifiedName,
} from './sidebarMetadataLoaders';
import { resolveSidebarTableNameForCopy } from './sidebarHelpers';
import {
resolveSidebarDatabaseNameForCopy,
resolveSidebarTableNameForCopy,
} from './sidebarHelpers';
import { normalizeMySQLViewDDLForEditing } from '../sidebarCoreUtils';
import {
DBQuery,
@@ -234,6 +237,21 @@ export const useSidebarObjectActions = ({
}
};
const handleCopyDatabaseName = async (node: any) => {
const databaseName = resolveSidebarDatabaseNameForCopy(node);
const label = t('sidebar.copy_object_name.label.database');
if (!databaseName) {
message.warning(t('sidebar.copy_object_name.empty', { label }));
return;
}
try {
await navigator.clipboard.writeText(databaseName);
message.success(t('sidebar.copy_object_name.copied', { label }));
} catch (e: any) {
message.error(t('sidebar.copy_object_name.failed', { label, error: e?.message || String(e) }));
}
};
const handleExport = async (node: any, options: { format: string; xlsxMaxRowsPerSheet?: number }) => {
const { config, dbName, tableName } = node.dataRef;
const sqlOptions = options.format === 'sql'
@@ -1314,6 +1332,7 @@ export const useSidebarObjectActions = ({
return {
handleCopyStructure,
handleCopyTableName,
handleCopyDatabaseName,
handleExport,
openExportDialog,
handleCopyTableAsInsert,

View File

@@ -67,6 +67,7 @@ type UseSidebarV2ActionHandlersArgs = {
openTableDdlInDesigner: (node: any) => void;
openTableInERView: (node: any) => void;
handleCopyTableName: (node: any) => Promise<void>;
handleCopyDatabaseName: (node: any) => Promise<void>;
handleCopyStructure: (node: any) => Promise<void>;
handleCopyTableAsInsert: (node: any) => Promise<void>;
openCreateStarRocksRollup: (node: any) => void;
@@ -130,6 +131,7 @@ export const useSidebarV2ActionHandlers = ({
openTableDdlInDesigner,
openTableInERView,
handleCopyTableName,
handleCopyDatabaseName,
handleCopyStructure,
handleCopyTableAsInsert,
openCreateStarRocksRollup,
@@ -304,6 +306,9 @@ export const useSidebarV2ActionHandlers = ({
const handleV2DatabaseContextMenuAction = (node: any, action: V2DatabaseContextMenuActionKey) => {
switch (action) {
case 'copy-database-name':
void handleCopyDatabaseName(node);
return;
case 'new-table':
openNewTableDesign(node);
return;

View File

@@ -124,6 +124,17 @@ const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeade
case 'copy-field-name':
copyToClipboard(columnName);
break;
case 'copy-column-comment': {
const columnMeta = columnMetaMap[columnName]
|| columnMetaMapByLowerName[columnName.toLowerCase()];
const comment = String(columnMeta?.comment || '').trim();
if (!comment) {
void message.info(translateDataGrid('data_grid.context_menu.column_no_comment'));
break;
}
copyToClipboard(comment);
break;
}
case 'copy-column-data':
handleCopyColumnData(columnName);
break;
@@ -186,6 +197,8 @@ const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeade
cellContextMenu.dataIndex,
cellContextMenu.title,
connectionId,
columnMetaMap,
columnMetaMapByLowerName,
copyToClipboard,
dbName,
displayColumnNames.length,