mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 00:42:47 +08:00
✨ feat(context-menu): 增强字段与数据库名称复制交互
数据表列头支持按元数据复制字段注释;数据库节点旧版与 V2 菜单支持复制数据库名;补齐六语言文案与回归测试。 Fixes #547
This commit is contained in:
@@ -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', () => {
|
||||
'暂无备注',
|
||||
'未命名字段',
|
||||
'复制字段名称',
|
||||
'复制注释',
|
||||
'复制列数据',
|
||||
'排序',
|
||||
'升序排序',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')");
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')",
|
||||
|
||||
@@ -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') },
|
||||
])}
|
||||
|
||||
|
||||
@@ -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 提问) */
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user