diff --git a/frontend/src/components/DefinitionViewer.object-edit.test.tsx b/frontend/src/components/DefinitionViewer.object-edit.test.tsx index 42528b42..09877f6e 100644 --- a/frontend/src/components/DefinitionViewer.object-edit.test.tsx +++ b/frontend/src/components/DefinitionViewer.object-edit.test.tsx @@ -194,6 +194,47 @@ describe('DefinitionViewer object edit entry', () => { })); }); + it('opens an editable query tab for event definitions', async () => { + storeState.connections[0].config.type = 'mysql'; + backendApp.DBQuery.mockResolvedValue({ + success: true, + data: [{ + Event: 'daily_cleanup', + 'Create Event': 'CREATE EVENT `daily_cleanup`\nON SCHEDULE EVERY 1 DAY\nDO DELETE FROM logs', + }], + }); + + let renderer: any; + await act(async () => { + renderer = create(renderWithI18n(createTab({ + id: 'event-def-conn-1-main-daily_cleanup', + title: '事件: daily_cleanup', + type: 'event-def', + eventName: 'daily_cleanup', + viewName: undefined, + viewKind: undefined, + }))); + await flushPromises(); + }); + + const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0]; + + await act(async () => { + button.props.onClick(); + }); + + const query = storeState.addTab.mock.calls[0][0].query; + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + title: 'Edit Event: daily_cleanup', + type: 'query', + queryMode: 'object-edit', + query: expect.stringContaining('CREATE EVENT `daily_cleanup`'), + })); + expect(query).toContain('-- Edit Event: daily_cleanup'); + expect(query).toContain('ON SCHEDULE EVERY 1 DAY'); + expect(query).not.toContain('SHOW CREATE EVENT'); + }); + it('uses SQL Server catalog metadata when loading routine definitions', async () => { storeState.connections[0].config.type = 'sqlserver'; backendApp.DBQuery.mockResolvedValue({ diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 2c2ddd1b..926608a7 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -692,6 +692,23 @@ describe('Sidebar locate toolbar', () => { expect(onSelectSource).toContain('openSidebarObjectNode(info.node)'); }); + it('opens event edit menu with editable object SQL instead of a SHOW query', () => { + const sidebarSource = readSidebarSource(); + const menuSource = readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx'); + const actionsSource = readSourceFile('./sidebar/useSidebarObjectActions.tsx'); + const eventMenuSource = menuSource.slice( + menuSource.indexOf("} else if (node.type === 'db-event') {"), + menuSource.indexOf("} else if (node.type === 'table') {"), + ); + + expect(sidebarSource).toContain('openEditEvent,'); + expect(eventMenuSource).toContain('onClick: () => void openEditEvent(node)'); + expect(eventMenuSource).not.toContain('SHOW CREATE EVENT'); + expect(actionsSource).toContain('const openEditEvent = async (node: any) =>'); + expect(actionsSource).toContain("queryMode: 'object-edit'"); + expect(actionsSource).toContain('SHOW CREATE EVENT ${eventRef}'); + }); + it('wires external SQL directory file actions to dedicated Wails APIs', () => { const source = readSidebarSource(); const loadTablesSource = source.slice( diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 2af531a6..5af46376 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1929,6 +1929,7 @@ const Sidebar: React.FC<{ handleRebindSavedQuery, openRoutineDefinition, openEventDefinition, + openEditEvent, openSequenceDefinition, openPackageDefinition, openEditRoutine, @@ -2279,6 +2280,7 @@ const Sidebar: React.FC<{ openEditRoutine, handleDropRoutine, openEventDefinition, + openEditEvent, openSequenceDefinition, openPackageDefinition, resolveMessagePublishTarget, diff --git a/frontend/src/components/SidebarEditEventTab.i18n.test.ts b/frontend/src/components/SidebarEditEventTab.i18n.test.ts index a315ecba..cf1c67ac 100644 --- a/frontend/src/components/SidebarEditEventTab.i18n.test.ts +++ b/frontend/src/components/SidebarEditEventTab.i18n.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8'); +const source = readFileSync(new URL('./sidebar/useSidebarObjectActions.tsx', import.meta.url), 'utf8'); const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const; const key = 'sidebar.tab.edit_event'; diff --git a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx index 0d0ab1ab..8b03e1b5 100644 --- a/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx +++ b/frontend/src/components/sidebar/sidebarLegacyNodeMenu.tsx @@ -171,6 +171,7 @@ export const buildSidebarLegacyNodeMenuItems = ( openEditRoutine, handleDropRoutine, openEventDefinition, + openEditEvent, openSequenceDefinition, openPackageDefinition, resolveMessagePublishTarget, @@ -884,17 +885,7 @@ export const buildSidebarLegacyNodeMenuItems = ( key: 'edit-event-query', label: t('sidebar.menu.edit_definition'), icon: , - onClick: () => { - const { eventName, dbName, id } = node.dataRef; - addTab({ - id: `query-edit-event-${Date.now()}`, - title: t('sidebar.tab.edit_event', { name: eventName }), - type: 'query', - connectionId: id, - dbName, - query: `SHOW CREATE EVENT \`${String(eventName || '').replace(/`/g, '``')}\`;` - }); - } + onClick: () => void openEditEvent(node) }, ]; } else if (node.type === 'table') { diff --git a/frontend/src/components/sidebar/useSidebarObjectActions.tsx b/frontend/src/components/sidebar/useSidebarObjectActions.tsx index 42c16b53..463f0bd1 100644 --- a/frontend/src/components/sidebar/useSidebarObjectActions.tsx +++ b/frontend/src/components/sidebar/useSidebarObjectActions.tsx @@ -114,6 +114,38 @@ const resolveCopyObjectNameLabel = (node: any): string => { return t('sidebar.copy_object_name.label.table'); }; +const quoteMySqlIdentifier = (raw: string): string => `\`${String(raw || '').replace(/`/g, '``')}\``; + +const buildMySqlEventReference = (eventName: string, schemaName?: string): string => { + const parsed = splitQualifiedName(eventName); + const name = parsed.objectName || eventName; + const schema = parsed.schemaName || String(schemaName || '').trim(); + return [schema, name] + .filter(Boolean) + .map(quoteMySqlIdentifier) + .join('.'); +}; + +const ensureSidebarObjectEditSqlTerminator = (sql: string): string => { + const normalized = String(sql || '').trim(); + if (!normalized) return ''; + return /;\s*$/.test(normalized) ? normalized : `${normalized};`; +}; + +const extractMySqlEventCreateSql = (rows: any[]): string => { + if (!Array.isArray(rows) || rows.length === 0) return ''; + for (const row of rows) { + const keys = Object.keys(row || {}); + const sqlKey = keys.find(key => key.toLowerCase().includes('create event')); + if (!sqlKey) continue; + const definition = row[sqlKey]; + if (definition !== undefined && definition !== null && String(definition).trim()) { + return String(definition); + } + } + return ''; +}; + export const useSidebarObjectActions = ({ connections, connectionIds, @@ -986,6 +1018,52 @@ export const useSidebarObjectActions = ({ }); }; + const openEditEvent = async (node: any) => { + const conn = node.dataRef; + const eventName = String(conn?.eventName || '').trim(); + const dbName = String(conn?.dbName || '').trim(); + const id = String(conn?.id || '').trim(); + if (!eventName) return; + + const objectLabel = t('definition_viewer.object.event'); + const header = [ + `-- ${t('definition_viewer.edit.comment_title', { object: objectLabel, name: eventName })}`, + `-- ${t('definition_viewer.edit.comment_compatibility')}`, + ].join('\n') + '\n'; + let template = `${header}-- ${t('definition_viewer.edit.comment_empty_definition', { name: eventName })}\n`; + + try { + const dialect = getMetadataDialect(conn as SavedConnection); + if (dialect === 'mysql') { + const config = buildRuntimeConfig(conn, dbName); + const eventRef = buildMySqlEventReference(eventName, conn?.schemaName || dbName); + const result = await DBQuery( + buildRpcConnectionConfig(config) as any, + dbName, + `SHOW CREATE EVENT ${eventRef}`, + ); + if (result.success) { + const createSql = extractMySqlEventCreateSql(result.data as any[]); + if (createSql) { + template = `${header}${ensureSidebarObjectEditSqlTerminator(createSql)}`; + } + } + } + } catch { + // 降级使用空编辑模板,避免把 SHOW 语句当成可编辑定义。 + } + + addTab({ + id: `query-edit-event-${Date.now()}`, + title: t('sidebar.tab.edit_event', { name: eventName }), + type: 'query', + connectionId: id, + dbName, + query: template, + queryMode: 'object-edit', + }); + }; + const openSequenceDefinition = (node: any) => { const { sequenceName, dbName, id } = node.dataRef; addTab({ @@ -1246,6 +1324,7 @@ export const useSidebarObjectActions = ({ handleRebindSavedQuery, openRoutineDefinition, openEventDefinition, + openEditEvent, openSequenceDefinition, openPackageDefinition, openEditRoutine,