From dac9e1f571dfbb71e94fdd92bea2211a1bf68932 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 29 Jun 2026 17:09:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sidebar):=20=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E6=89=B9=E9=87=8F=E5=BA=93=E8=A1=A8=E5=8D=B1=E9=99=A9?= =?UTF-8?q?=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复批量表和批量库按钮到原批量操作弹窗\n- 补回批量删除表和删除库的危险确认与执行逻辑\n- 删除表仅处理 table 对象,避免视图误走 DropTable\n- 补齐多语言文案与入口/删除回归测试 --- .../Sidebar.locate-toolbar.test.tsx | 6 + frontend/src/components/Sidebar.tsx | 12 +- .../SidebarBatchActions.i18n.test.ts | 11 ++ .../SidebarBatchClearFeedback.i18n.test.ts | 42 +++++ ...arBatchDatabaseExportFeedback.i18n.test.ts | 38 +++++ .../sidebar/SidebarBatchExportModals.tsx | 37 +++- .../sidebar/useSidebarBatchExport.ts | 160 +++++++++++++++++- shared/i18n/de-DE.json | 13 ++ shared/i18n/en-US.json | 13 ++ shared/i18n/ja-JP.json | 13 ++ shared/i18n/ru-RU.json | 13 ++ shared/i18n/zh-CN.json | 13 ++ shared/i18n/zh-TW.json | 13 ++ 13 files changed, 371 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 926608a..b5b1a07 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -804,6 +804,12 @@ describe('Sidebar locate toolbar', () => { expect(markup).toContain('data-sidebar-create-group-action="true"'); expect(markup).toContain('data-sidebar-batch-table-action="true"'); expect(markup).toContain('data-sidebar-batch-database-action="true"'); + expect(source).toContain('openBatchTableExport: () => openBatchOperationModal(),'); + expect(source).toContain('openBatchDatabaseExport: () => openBatchDatabaseModal(),'); + expect(source).toContain('onClick={() => openBatchOperationModal()}'); + expect(source).toContain('onClick={() => openBatchDatabaseModal()}'); + expect(source).not.toContain('openBatchTableExport: () => openBatchTableExportWorkbench()'); + expect(source).not.toContain('openBatchDatabaseExport: () => openBatchDatabaseExportWorkbench()'); expect(markup).toContain('data-sidebar-open-external-sql-file-action="true"'); expect(markup).toContain('data-sidebar-locate-current-tab-action="true"'); expect(markup).toContain('data-gonavi-new-query-action="true"'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 5af4637..58c673c 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -773,12 +773,14 @@ const Sidebar: React.FC<{ handleDatabaseChange, handleBatchExport, handleBatchClear, + handleBatchDeleteTables, handleCheckAll, handleInvertSelection, openBatchDatabaseModal, openBatchDatabaseExportWorkbench, handleDbConnectionChange, handleBatchDbExport, + handleBatchDbDelete, handleCheckAllDb, handleInvertSelectionDb, } = useSidebarBatchExport({ @@ -2618,8 +2620,8 @@ const Sidebar: React.FC<{ }, handlers: { openCreateTagModal: () => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); }, - openBatchTableExport: () => openBatchTableExportWorkbench(), - openBatchDatabaseExport: () => openBatchDatabaseExportWorkbench(), + openBatchTableExport: () => openBatchOperationModal(), + openBatchDatabaseExport: () => openBatchDatabaseModal(), openExternalSqlFile: handleOpenSQLFileFromToolbar, locateActiveTab: handleLocateActiveTabInSidebar, toggleAI: onToggleAI ?? (() => {}), @@ -2857,7 +2859,7 @@ const Sidebar: React.FC<{ icon={} aria-label={t('sidebar.action.batch_tables')} data-sidebar-batch-table-action="true" - onClick={() => openBatchTableExportWorkbench()} + onClick={() => openBatchOperationModal()} style={{ color: legacyToolbarButtonColor }} /> @@ -2870,7 +2872,7 @@ const Sidebar: React.FC<{ icon={} aria-label={t('sidebar.action.batch_databases')} data-sidebar-batch-database-action="true" - onClick={() => openBatchDatabaseExportWorkbench()} + onClick={() => openBatchDatabaseModal()} style={{ color: legacyToolbarButtonColor }} /> @@ -3097,6 +3099,7 @@ const Sidebar: React.FC<{ handleConnectionChange={handleConnectionChange} handleDatabaseChange={handleDatabaseChange} handleBatchClear={handleBatchClear} + handleBatchDeleteTables={handleBatchDeleteTables} handleBatchExport={handleBatchExport} handleCheckAll={handleCheckAll} handleInvertSelection={handleInvertSelection} @@ -3108,6 +3111,7 @@ const Sidebar: React.FC<{ setCheckedDbKeys={setCheckedDbKeys} handleDbConnectionChange={handleDbConnectionChange} handleBatchDbExport={handleBatchDbExport} + handleBatchDbDelete={handleBatchDbDelete} handleCheckAllDb={handleCheckAllDb} handleInvertSelectionDb={handleInvertSelectionDb} /> diff --git a/frontend/src/components/SidebarBatchActions.i18n.test.ts b/frontend/src/components/SidebarBatchActions.i18n.test.ts index d54d052..15aff00 100644 --- a/frontend/src/components/SidebarBatchActions.i18n.test.ts +++ b/frontend/src/components/SidebarBatchActions.i18n.test.ts @@ -11,6 +11,8 @@ const requiredKeys = [ 'sidebar.action.batch_tables', 'sidebar.action.batch_databases', 'sidebar.action.clear_tables', + 'sidebar.action.delete_tables', + 'sidebar.action.delete_database_count', 'sidebar.action.export_schema', 'sidebar.action.export_data_only', 'sidebar.action.backup_schema_data', @@ -63,6 +65,7 @@ describe('Sidebar batch actions i18n', () => { [ '清空表', + '删除表', '导出结构', '仅数据(INSERT)', '备份(结构+数据)', @@ -118,6 +121,8 @@ describe('Sidebar batch actions i18n', () => { [ 'sidebar.action.clear_tables', + 'sidebar.action.delete_tables', + 'sidebar.action.delete_database_count', 'sidebar.action.export_schema', 'sidebar.action.export_data_only', 'sidebar.action.backup_schema_data', @@ -148,6 +153,10 @@ describe('Sidebar batch actions i18n', () => { expect(batchModalSource, key).toContain(`t('${key}'`); }); + expect(batchModalSource).toContain('handleBatchDeleteTables'); + expect(batchModalSource).toContain('handleBatchDbDelete'); + expect(batchModalSource).toContain("item.objectType === 'table'"); + [ 'sidebar.tab.batch_export_objects', 'sidebar.tab.batch_export_objects_database', @@ -170,6 +179,8 @@ describe('Sidebar batch actions i18n', () => { requiredKeys.forEach((key) => { expect(catalog[key], `${locale}:${key}`).toBeTruthy(); }); + expect(placeholders(catalog['sidebar.action.delete_database_count'])).toEqual(['count']); + expect(placeholders(catalog['sidebar.action.delete_tables'])).toEqual([]); expect(placeholders(catalog['sidebar.tab.batch_export_objects'])).toEqual([]); expect(placeholders(catalog['sidebar.tab.batch_export_objects_database'])).toEqual(['database']); expect(placeholders(catalog['sidebar.tab.batch_export_databases'])).toEqual([]); diff --git a/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts index 4c9b669..4c81c98 100644 --- a/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts +++ b/frontend/src/components/SidebarBatchClearFeedback.i18n.test.ts @@ -12,10 +12,24 @@ const requiredKeys = [ 'sidebar.message.clearing_selected_tables', 'sidebar.message.clear_success', 'sidebar.message.clear_failed', + 'sidebar.message.select_table_required', + 'sidebar.modal.confirm_delete_selected_tables.title', + 'sidebar.modal.confirm_delete_selected_tables.content', + 'sidebar.message.deleting_selected_tables', + 'sidebar.message.delete_tables_success', + 'sidebar.message.delete_tables_failed', ] as const; const extractHandleBatchClearBlock = (): string => { const start = source.indexOf('const handleBatchClear = async'); + const end = source.indexOf('const handleBatchDeleteTables = async', start); + expect(start).toBeGreaterThanOrEqual(0); + expect(end).toBeGreaterThan(start); + return source.slice(start, end); +}; + +const extractHandleBatchDeleteTablesBlock = (): string => { + const start = source.indexOf('const handleBatchDeleteTables = async'); const end = source.indexOf('const handleCheckAll = (checked: boolean)', start); expect(start).toBeGreaterThanOrEqual(0); expect(end).toBeGreaterThan(start); @@ -55,6 +69,28 @@ describe('Sidebar batch clear feedback i18n', () => { expect(block).toContain("res.message !== '已取消'"); }); + it('localizes handleBatchDeleteTables validation, confirmation, loading, success, and failure wrappers', () => { + const block = extractHandleBatchDeleteTablesBlock(); + + expect(block).toContain('DropTable'); + expect(block).toContain("item.objectType === 'table'"); + expect(block).toContain("t('sidebar.message.select_table_required')"); + expect(block).toContain("t('sidebar.modal.confirm_delete_selected_tables.title')"); + expect(block).toContain("t('sidebar.modal.confirm_delete_selected_tables.content'"); + expect(block).toContain("t('sidebar.action.delete')"); + expect(block).toContain('okButtonProps: { danger: true }'); + expect(block).toContain("t('sidebar.action.cancel')"); + expect(block).toContain("t('sidebar.message.deleting_selected_tables'"); + expect(block).toContain("t('sidebar.message.delete_tables_success'"); + expect(block).toContain("t('sidebar.message.delete_tables_failed'"); + expect(block).toContain('connection: conn.name'); + expect(block).toContain('database: dbName'); + expect(block).toContain('count: tableNames.length'); + expect(block).toContain('count: successKeys.length'); + expect(block).toContain('table: failed.table'); + expect(block).toContain('error: failed.error'); + }); + it('keeps batch clear feedback keys available with stable placeholders', () => { locales.forEach((locale) => { const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record; @@ -69,6 +105,12 @@ describe('Sidebar batch clear feedback i18n', () => { expect(placeholders(catalog['sidebar.message.clearing_selected_tables'])).toEqual(['count']); expect(placeholders(catalog['sidebar.message.clear_success'])).toEqual([]); expect(placeholders(catalog['sidebar.message.clear_failed'])).toEqual(['error']); + expect(placeholders(catalog['sidebar.message.select_table_required'])).toEqual([]); + expect(placeholders(catalog['sidebar.modal.confirm_delete_selected_tables.title'])).toEqual([]); + expect(placeholders(catalog['sidebar.modal.confirm_delete_selected_tables.content'])).toEqual(['connection', 'count', 'database']); + expect(placeholders(catalog['sidebar.message.deleting_selected_tables'])).toEqual(['count']); + expect(placeholders(catalog['sidebar.message.delete_tables_success'])).toEqual(['count']); + expect(placeholders(catalog['sidebar.message.delete_tables_failed'])).toEqual(['error', 'table']); }); }); }); diff --git a/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts b/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts index 320c19c..c08aa6a 100644 --- a/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts +++ b/frontend/src/components/SidebarBatchDatabaseExportFeedback.i18n.test.ts @@ -10,6 +10,11 @@ const requiredKeys = [ 'sidebar.message.exporting_database_backup', 'sidebar.message.database_export_success', 'sidebar.message.database_export_failed', + 'sidebar.modal.confirm_delete_selected_databases.title', + 'sidebar.modal.confirm_delete_selected_databases.content', + 'sidebar.message.deleting_selected_databases', + 'sidebar.message.delete_databases_success', + 'sidebar.message.delete_databases_failed', ] as const; const extractBatchDatabaseExportBlock = (): string => { @@ -20,6 +25,14 @@ const extractBatchDatabaseExportBlock = (): string => { return source.slice(start, end); }; +const extractHandleBatchDbDeleteBlock = (): string => { + const start = source.indexOf('const handleBatchDbDelete = async'); + const end = source.indexOf('const handleCheckAllDb = (checked: boolean)', start); + expect(start).toBeGreaterThanOrEqual(0); + expect(end).toBeGreaterThan(start); + return source.slice(start, end); +}; + const placeholders = (value: string): string[] => [...value.matchAll(/\{\{(\w+)\}\}/g)] .map((match) => match[1]) .sort(); @@ -47,6 +60,26 @@ describe('Sidebar batch database export feedback i18n', () => { expect(block).toContain("res.message !== '已取消'"); }); + it('localizes handleBatchDbDelete confirmation, loading, success, and failure wrappers', () => { + const block = extractHandleBatchDbDeleteBlock(); + + expect(block).toContain('DropDatabase'); + expect(block).toContain("t('sidebar.message.select_database_required')"); + expect(block).toContain("t('sidebar.modal.confirm_delete_selected_databases.title')"); + expect(block).toContain("t('sidebar.modal.confirm_delete_selected_databases.content'"); + expect(block).toContain("t('sidebar.action.delete')"); + expect(block).toContain('okButtonProps: { danger: true }'); + expect(block).toContain("t('sidebar.action.cancel')"); + expect(block).toContain("t('sidebar.message.deleting_selected_databases'"); + expect(block).toContain("t('sidebar.message.delete_databases_success'"); + expect(block).toContain("t('sidebar.message.delete_databases_failed'"); + expect(block).toContain('connection: batchConnContext.name'); + expect(block).toContain('count: selectedDbs.length'); + expect(block).toContain('count: successKeys.length'); + expect(block).toContain('database: failed.database'); + expect(block).toContain('error: failed.error'); + }); + it('keeps batch database export feedback keys available with stable placeholders', () => { locales.forEach((locale) => { const catalog = JSON.parse(readFileSync(new URL(`../../../shared/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record; @@ -59,6 +92,11 @@ describe('Sidebar batch database export feedback i18n', () => { expect(placeholders(catalog['sidebar.message.exporting_database_backup'])).toEqual(['database']); expect(placeholders(catalog['sidebar.message.database_export_success'])).toEqual(['database']); expect(placeholders(catalog['sidebar.message.database_export_failed'])).toEqual(['database', 'error']); + expect(placeholders(catalog['sidebar.modal.confirm_delete_selected_databases.title'])).toEqual([]); + expect(placeholders(catalog['sidebar.modal.confirm_delete_selected_databases.content'])).toEqual(['connection', 'count']); + expect(placeholders(catalog['sidebar.message.deleting_selected_databases'])).toEqual(['count']); + expect(placeholders(catalog['sidebar.message.delete_databases_success'])).toEqual(['count']); + expect(placeholders(catalog['sidebar.message.delete_databases_failed'])).toEqual(['database', 'error']); }); }); }); diff --git a/frontend/src/components/sidebar/SidebarBatchExportModals.tsx b/frontend/src/components/sidebar/SidebarBatchExportModals.tsx index d57a470..9602693 100644 --- a/frontend/src/components/sidebar/SidebarBatchExportModals.tsx +++ b/frontend/src/components/sidebar/SidebarBatchExportModals.tsx @@ -13,16 +13,11 @@ import Modal from '../common/ResizableDraggableModal'; import type { SavedConnection } from '../../types'; import { t } from '../../i18n'; import type { + BatchObjectItem, BatchObjectFilterType, BatchSelectionScope, } from './useSidebarBatchExport'; -type BatchObjectItem = { - key: string; - dbName?: string; - title: React.ReactNode; -}; - type SidebarBatchExportModalsProps = { connections: SavedConnection[]; modalPanelStyle: React.CSSProperties; @@ -55,6 +50,7 @@ type SidebarBatchExportModalsProps = { handleConnectionChange: (connectionId: string) => void; handleDatabaseChange: (databaseName: string) => void; handleBatchClear: () => void; + handleBatchDeleteTables: () => void; handleBatchExport: (mode: 'schema' | 'dataOnly' | 'backup') => void; handleCheckAll: (checked: boolean) => void; handleInvertSelection: () => void; @@ -66,6 +62,7 @@ type SidebarBatchExportModalsProps = { setCheckedDbKeys: (keys: string[]) => void; handleDbConnectionChange: (connectionId: string) => void; handleBatchDbExport: (includeData: boolean) => void; + handleBatchDbDelete: () => void; handleCheckAllDb: (checked: boolean) => void; handleInvertSelectionDb: () => void; }; @@ -102,6 +99,7 @@ export const SidebarBatchExportModals: React.FC = handleConnectionChange, handleDatabaseChange, handleBatchClear, + handleBatchDeleteTables, handleBatchExport, handleCheckAll, handleInvertSelection, @@ -113,9 +111,13 @@ export const SidebarBatchExportModals: React.FC = setCheckedDbKeys, handleDbConnectionChange, handleBatchDbExport, + handleBatchDbDelete, handleCheckAllDb, handleInvertSelectionDb, -}) => ( +}) => { + const selectedTableCount = batchTables.filter(item => checkedTableKeys.includes(item.key) && item.objectType === 'table').length; + + return ( <> = {t('sidebar.action.cancel')} + , + ,