From 22cdeb677bc6f2ad75d182991494c76b0f077e79 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 25 Jun 2026 22:29:09 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AF=B9=E8=B1=A1=E8=B6=85=E9=93=BE=E6=8E=A5=E8=AF=AF?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E5=B7=A6=E6=A0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QueryEditor.external-sql-save.test.tsx | 147 ++++++---- frontend/src/components/QueryEditor.tsx | 274 +++++++++++++----- 2 files changed, 289 insertions(+), 132 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 237e7dd..d11c380 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -1656,7 +1656,7 @@ describe('QueryEditor external SQL save', () => { }); }); - expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'analytics' }); + expect(storeState.setActiveContext).not.toHaveBeenCalled(); expect(storeState.addTab).toHaveBeenCalledWith({ id: 'conn-1-analytics-table-events', title: 'events', @@ -1666,7 +1666,7 @@ describe('QueryEditor external SQL save', () => { tableName: 'events', objectType: 'table', }); - expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ + expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'gonavi:locate-sidebar-object', })); expect(preventDefault).toHaveBeenCalled(); @@ -1703,7 +1703,7 @@ describe('QueryEditor external SQL save', () => { }); }); - expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'mkefu_location_dev_local' }); + expect(storeState.setActiveContext).not.toHaveBeenCalled(); expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ type: 'table', connectionId: 'conn-1', @@ -1711,6 +1711,79 @@ describe('QueryEditor external SQL save', () => { tableName: 'fs_mkefu_regist_record', objectType: 'table', })); + expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ + type: 'gonavi:locate-sidebar-object', + })); + expect(preventDefault).toHaveBeenCalled(); + expect(stopPropagation).toHaveBeenCalled(); + }); + + it('opens a routine object-edit tab on ctrl click without locating the sidebar tree', async () => { + storeState.connections[0].config.type = 'postgres'; + editorState.value = 'call reporting.refresh_stats();'; + autoFetchState.visible = true; + backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] }); + backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [] }); + backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] }); + backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => { + const text = String(sql || ''); + if (text.includes('pg_get_functiondef')) { + return { + success: true, + data: [{ + routine_definition: 'CREATE OR REPLACE PROCEDURE reporting.refresh_stats() LANGUAGE plpgsql AS $$ BEGIN NULL; END; $$;', + }], + }; + } + if (text.includes('FROM pg_proc') || text.includes('information_schema.routines')) { + return { + success: true, + data: [{ schema_name: 'reporting', routine_name: 'refresh_stats', routine_type: 'PROCEDURE' }], + }; + } + return { success: true, data: [] }; + }); + + await act(async () => { + create(); + }); + await act(async () => { + for (let i = 0; i < 12; i += 1) { + await Promise.resolve(); + } + }); + + const preventDefault = vi.fn(); + const stopPropagation = vi.fn(); + await act(async () => { + editorState.mouseDownListeners[0]?.({ + target: { position: { lineNumber: 1, column: 21 } }, + event: { + browserEvent: { button: 0, buttons: 1 }, + leftButton: true, + ctrlKey: true, + metaKey: false, + preventDefault, + stopPropagation, + }, + }); + for (let i = 0; i < 8; i += 1) { + await Promise.resolve(); + } + }); + + expect(storeState.setActiveContext).not.toHaveBeenCalled(); + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + title: expect.stringContaining('refresh_stats'), + type: 'query', + connectionId: 'conn-1', + dbName: 'main', + queryMode: 'object-edit', + query: expect.stringContaining('CREATE OR REPLACE PROCEDURE reporting.refresh_stats()'), + })); + expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ + type: 'gonavi:locate-sidebar-object', + })); expect(preventDefault).toHaveBeenCalled(); expect(stopPropagation).toHaveBeenCalled(); }); @@ -3691,7 +3764,7 @@ describe('QueryEditor external SQL save', () => { }); }); - expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'main' }); + expect(storeState.setActiveContext).not.toHaveBeenCalled(); expect(storeState.addTab).toHaveBeenCalledWith({ id: 'view-def-conn-1-main-active_users', title: '视图:active_users', @@ -3703,13 +3776,8 @@ describe('QueryEditor external SQL save', () => { schemaName: 'reporting', sidebarLocateKey: 'conn-1-main-view-active_users', }); - expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ + expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'gonavi:locate-sidebar-object', - detail: expect.objectContaining({ - tabId: 'conn-1-main-view-active_users', - schemaName: 'reporting', - objectGroup: 'views', - }), })); }); @@ -3775,34 +3843,17 @@ describe('QueryEditor external SQL save', () => { schemaName: 'audit', sidebarLocateKey: 'conn-1-main-trigger-audit.users_bi-audit.users', }); - expect(storeState.addTab).toHaveBeenCalledWith({ - id: 'routine-def-conn-1-main-reporting.refresh_stats', - title: '存储过程:reporting.refresh_stats', - type: 'routine-def', + expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ + id: expect.stringMatching(/^query-edit-routine-conn-1-main-reporting\.refresh_stats-\d+$/), + title: '编辑 存储过程:reporting.refresh_stats', + type: 'query', connectionId: 'conn-1', dbName: 'main', - routineName: 'reporting.refresh_stats', - routineType: 'PROCEDURE', - schemaName: 'reporting', - sidebarLocateKey: 'conn-1-main-routine-reporting.refresh_stats', - }); - expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ - type: 'gonavi:locate-sidebar-object', - detail: expect.objectContaining({ - tabId: 'conn-1-main-trigger-audit.users_bi-audit.users', - triggerName: 'audit.users_bi', - schemaName: 'audit', - objectGroup: 'triggers', - }), + queryMode: 'object-edit', + query: expect.stringContaining('CREATE OR REPLACE PROCEDURE reporting.refresh_stats()'), })); - expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ + expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'gonavi:locate-sidebar-object', - detail: expect.objectContaining({ - tabId: 'conn-1-main-routine-reporting.refresh_stats', - routineName: 'reporting.refresh_stats', - schemaName: 'reporting', - objectGroup: 'routines', - }), })); }); @@ -3879,23 +3930,8 @@ describe('QueryEditor external SQL save', () => { schemaName: 'billing', sidebarLocateKey: 'package-def-conn-1-main-billing.pkg_order', }); - expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ + expect((window as any).dispatchEvent).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'gonavi:locate-sidebar-object', - detail: expect.objectContaining({ - tabId: 'sequence-def-conn-1-main-billing.order_seq', - sequenceName: 'billing.order_seq', - schemaName: 'billing', - objectGroup: 'sequences', - }), - })); - expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ - type: 'gonavi:locate-sidebar-object', - detail: expect.objectContaining({ - tabId: 'package-def-conn-1-main-billing.pkg_order', - packageName: 'billing.pkg_order', - schemaName: 'billing', - objectGroup: 'packages', - }), })); }); @@ -4003,9 +4039,10 @@ describe('QueryEditor external SQL save', () => { type: 'trigger', })); expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ - id: 'routine-def-conn-1-main-reporting.refresh_stats', - title: 'Procedure: reporting.refresh_stats', - type: 'routine-def', + id: expect.stringMatching(/^query-edit-routine-conn-1-main-reporting\.refresh_stats-\d+$/), + title: 'Edit Procedure: reporting.refresh_stats', + type: 'query', + queryMode: 'object-edit', })); }); @@ -7743,7 +7780,7 @@ describe('QueryEditor external SQL save', () => { }); }); - expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'front_end_sys' }); + expect(storeState.setActiveContext).not.toHaveBeenCalled(); expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ type: 'table', connectionId: 'conn-1', @@ -7825,7 +7862,7 @@ describe('QueryEditor external SQL save', () => { }); }); - expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'front_end_sys' }); + expect(storeState.setActiveContext).not.toHaveBeenCalled(); expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({ type: 'table', connectionId: 'conn-1', diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index dd8c54f..c8ded0c 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -35,6 +35,7 @@ import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert'; import { t as translate } from '../i18n'; import { buildSqlAnalysisWorkbenchTab } from '../utils/sqlAnalysisTab'; import { isLocalizedUntitledQueryTitle } from '../utils/queryTabTitle'; +import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition'; import { DUCKDB_ROWID_LOCATOR_COLUMN, ORACLE_ROWID_LOCATOR_COLUMN, @@ -63,6 +64,7 @@ import { type CompletionTableMeta, type CompletionTriggerMeta, type CompletionViewMeta, + type QueryEditorNavigationTarget, type QueryStatementPlan, QUERY_EDITOR_HOVER_DELAY_MS, QUERY_EDITOR_LIVE_DECORATION_MAX_TEXT_LENGTH, @@ -88,7 +90,6 @@ import { clearQueryEditorObjectDecorations, collectQueryEditorObjectDecorationCandidates, collectQueryEditorReferencedDatabaseNames, - dispatchQueryEditorSidebarLocate, getCaseInsensitiveValue, getFirstRowValue, getNormalizedPositionAtOffset, @@ -135,6 +136,52 @@ const buildQueryEditorMonacoActionLabel = (key: string): string => const QUERY_EDITOR_SQL_PROMPT_PLACEHOLDER = '{SQL}'; +const escapeQueryEditorObjectEditSqlLiteral = (value: unknown): string => ( + String(value || '').replace(/'/g, "''") +); + +const getQueryEditorObjectEditRawValue = (row: Record, candidateKeys: string[]): any => { + const keyMap = new Map(); + Object.keys(row || {}).forEach((key) => keyMap.set(key.toLowerCase(), row[key])); + for (const key of candidateKeys) { + if (keyMap.has(key.toLowerCase())) { + const value = keyMap.get(key.toLowerCase()); + if (value !== undefined && value !== null) return value; + } + } + return undefined; +}; + +const normalizeQueryEditorRoutineDefinitionForEdit = ( + definition: string, + routineName: string, + routineType: string, +): string => { + const text = String(definition || '').trim(); + if (!text) return ''; + if (/^\s*create\b/i.test(text)) return text; + if (/^\s*(function|procedure)\b/i.test(text)) { + return `CREATE OR REPLACE ${text}`; + } + const normalizedType = String(routineType || 'FUNCTION').trim().toUpperCase().includes('PROC') + ? 'PROCEDURE' + : 'FUNCTION'; + return `CREATE OR REPLACE ${normalizedType} ${routineName}\n${text}`; +}; + +const buildQueryEditorRoutineEditFallbackSql = ( + routineName: string, + routineType: string, +): string => { + const normalizedType = String(routineType || 'FUNCTION').trim().toUpperCase().includes('PROC') + ? 'PROCEDURE' + : 'FUNCTION'; + if (normalizedType === 'PROCEDURE') { + return `CREATE OR REPLACE PROCEDURE ${routineName}()\nBEGIN\n -- TODO: edit procedure body\nEND;`; + } + return `CREATE OR REPLACE FUNCTION ${routineName}()\nRETURNS INTEGER\nBEGIN\n -- TODO: edit function body\n RETURN 0;\nEND;`; +}; + const buildQueryEditorAiContextPrompt = (connection: any, database: string): string => { if (!connection) { return ''; @@ -1390,6 +1437,154 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }; }, [cancelEditorResizeFrame, handleMouseMove, handleMouseUp]); + const openRoutineObjectEditTab = useCallback(async ( + navigationTarget: Extract, + connectionId: string, + targetDbName: string, + ) => { + const targetRoutineName = String(navigationTarget.routineName || '').trim(); + if (!targetRoutineName) return; + + const normalizedRoutineType = String(navigationTarget.routineType || 'FUNCTION').trim().toUpperCase().includes('PROC') + ? 'PROCEDURE' + : 'FUNCTION'; + const routineTypeLabel = normalizedRoutineType === 'PROCEDURE' + ? translate('sidebar.object.procedure') + : translate('sidebar.object.function'); + const sqlTemplateHeader = `-- ${translate('sidebar.sql_template.edit_routine', { + type: routineTypeLabel, + name: targetRoutineName, + })}`; + let editSql = `${sqlTemplateHeader}\n-- ${translate('sidebar.sql_template.modify_then_execute')}\n${buildQueryEditorRoutineEditFallbackSql(targetRoutineName, normalizedRoutineType)}`; + + const conn = connectionsRef.current.find((item) => item.id === connectionId); + if (conn) { + const dialect = normalizeMetadataDialect(conn); + const parsedRoutine = splitSidebarQualifiedName(targetRoutineName); + const routineObjectName = parsedRoutine.objectName || targetRoutineName; + const routineSchemaName = String(navigationTarget.schemaName || parsedRoutine.schemaName || '').trim(); + const safeName = escapeQueryEditorObjectEditSqlLiteral(routineObjectName); + const safeSchema = escapeQueryEditorObjectEditSqlLiteral(routineSchemaName); + const safeDbName = escapeQueryEditorObjectEditSqlLiteral(targetDbName); + const config = { + ...conn.config, + port: Number(conn.config?.port), + password: conn.config?.password || '', + database: conn.config?.database || '', + useSSH: conn.config?.useSSH || false, + ssh: conn.config?.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' }, + }; + const queries = (() => { + switch (dialect) { + case 'mysql': + case 'starrocks': + return [ + `SHOW CREATE ${normalizedRoutineType} \`${routineObjectName.replace(/`/g, '``')}\``, + safeDbName + ? `SELECT ROUTINE_DEFINITION AS routine_definition FROM information_schema.routines WHERE routine_schema = '${safeDbName}' AND routine_name = '${safeName}' AND UPPER(routine_type) = '${normalizedRoutineType}' LIMIT 1` + : '', + ].filter(Boolean); + case 'postgres': + case 'kingbase': + case 'highgo': + case 'vastbase': + case 'opengauss': + case 'gaussdb': { + const schemaRef = safeSchema || 'public'; + return [`SELECT pg_get_functiondef(p.oid) AS routine_definition FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${schemaRef}' AND p.proname = '${safeName}' LIMIT 1`]; + } + case 'sqlserver': + return buildSqlServerObjectDefinitionQueries('routine', targetRoutineName, targetDbName, 'routine_definition'); + case 'oracle': + case 'dm': + case 'dameng': { + const owner = safeSchema || safeDbName; + return [ + owner + ? `SELECT TEXT FROM ALL_SOURCE WHERE OWNER = '${owner.toUpperCase()}' AND NAME = '${safeName.toUpperCase()}' AND TYPE = '${normalizedRoutineType}' ORDER BY LINE` + : `SELECT TEXT FROM USER_SOURCE WHERE NAME = '${safeName.toUpperCase()}' AND TYPE = '${normalizedRoutineType}' ORDER BY LINE`, + ]; + } + case 'duckdb': { + const schemaRef = safeSchema || 'main'; + return [ + `SELECT schema_name, function_name, parameters, macro_definition FROM duckdb_functions() WHERE internal = false AND lower(function_type) = 'macro' AND schema_name = '${schemaRef}' AND function_name = '${safeName}' LIMIT 1`, + ]; + } + default: + return []; + } + })(); + + for (const queryText of queries) { + try { + const result = await DBQuery(buildRpcConnectionConfig(config) as any, targetDbName, queryText); + if (!result.success || !Array.isArray(result.data) || result.data.length === 0) { + continue; + } + let definition = ''; + if (dialect === 'oracle' || dialect === 'dm' || dialect === 'dameng') { + definition = result.data.map((row: any) => row.text || row.TEXT || Object.values(row)[0] || '').join(''); + } else if (dialect === 'duckdb') { + const row = result.data[0] as Record; + const schemaName = String(getQueryEditorObjectEditRawValue(row, ['schema_name']) || routineSchemaName || '').trim(); + const functionName = String(getQueryEditorObjectEditRawValue(row, ['function_name', 'routine_name', 'name']) || routineObjectName || '').trim(); + const parametersRaw = getQueryEditorObjectEditRawValue(row, ['parameters']); + const macroDefinition = String(getQueryEditorObjectEditRawValue(row, ['macro_definition']) || '').trim(); + const parameters = Array.isArray(parametersRaw) + ? parametersRaw.map((item) => String(item ?? '').trim()).filter(Boolean).join(', ') + : String(parametersRaw ?? '').replace(/^\[|\]$/g, '').trim(); + const qualifiedName = schemaName ? `${schemaName}.${functionName}` : functionName; + if (qualifiedName && macroDefinition) { + definition = macroDefinition.startsWith('(') + ? `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS ${macroDefinition};` + : `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS TABLE ${macroDefinition};`; + } + } else if (dialect === 'sqlserver') { + definition = result.data + .map((row: any) => getQueryEditorObjectEditRawValue(row, ['routine_definition', 'definition', 'text', 'Text']) ?? '') + .map((value) => String(value)) + .join(''); + } else { + const row = result.data[0] as Record; + const direct = getQueryEditorObjectEditRawValue(row, ['routine_definition', 'definition']); + if (direct !== undefined && direct !== null && String(direct).trim()) { + definition = String(direct); + } else { + const createKey = Object.keys(row).find((key) => /create\s+(function|procedure)/i.test(key)); + definition = createKey ? String(row[createKey] || '') : ''; + } + } + + const normalizedDefinition = normalizeQueryEditorRoutineDefinitionForEdit( + definition, + targetRoutineName, + normalizedRoutineType, + ); + if (normalizedDefinition) { + editSql = `${sqlTemplateHeader}\n${normalizedDefinition}`; + break; + } + } catch { + // 查询最新定义失败时保留可编辑模板。 + } + } + } + + addTab({ + id: `query-edit-routine-${connectionId}-${targetDbName}-${targetRoutineName}-${Date.now()}`, + title: translate('sidebar.tab.edit_routine', { + type: routineTypeLabel, + name: targetRoutineName, + }), + type: 'query', + connectionId, + dbName: targetDbName, + query: editSql, + queryMode: 'object-edit', + }); + }, [addTab]); + // Setup Autocomplete and Editor const handleEditorDidMount: OnMount = (editor, monaco) => { editorRef.current = editor; @@ -1716,9 +1911,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc return; } - setCurrentDb(targetDbName); - currentDbRef.current = targetDbName; - setActiveContext({ connectionId, dbName: targetDbName }); if (navigationTarget.type === 'table') { const targetTableName = String(navigationTarget.tableName || '').trim(); if (!targetTableName) return; @@ -1731,13 +1923,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc tableName: targetTableName, objectType: 'table', }); - dispatchQueryEditorSidebarLocate({ - connectionId, - dbName: targetDbName, - tableName: targetTableName, - schemaName: navigationTarget.schemaName, - objectGroup: 'tables', - }); return; } @@ -1762,15 +1947,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc schemaName: targetSchemaName || undefined, sidebarLocateKey, }); - dispatchQueryEditorSidebarLocate({ - tabId: sidebarLocateKey, - connectionId, - dbName: targetDbName, - viewName: targetViewName, - tableName: targetViewName, - schemaName: targetSchemaName, - objectGroup: navigationTarget.type === 'materialized-view' ? 'materializedViews' : 'views', - }); return; } @@ -1791,15 +1967,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc schemaName: targetSchemaName || undefined, sidebarLocateKey, }); - dispatchQueryEditorSidebarLocate({ - tabId: sidebarLocateKey, - connectionId, - dbName: targetDbName, - triggerName: targetTriggerName, - tableName: targetTriggerName, - schemaName: targetSchemaName, - objectGroup: 'triggers', - }); return; } @@ -1818,15 +1985,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc schemaName: targetSchemaName || undefined, sidebarLocateKey, }); - dispatchQueryEditorSidebarLocate({ - tabId: sidebarLocateKey, - connectionId, - dbName: targetDbName, - sequenceName: targetSequenceName, - tableName: targetSequenceName, - schemaName: targetSchemaName, - objectGroup: 'sequences', - }); return; } @@ -1845,48 +2003,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc schemaName: targetSchemaName || undefined, sidebarLocateKey, }); - dispatchQueryEditorSidebarLocate({ - tabId: sidebarLocateKey, - connectionId, - dbName: targetDbName, - packageName: targetPackageName, - tableName: targetPackageName, - schemaName: targetSchemaName, - objectGroup: 'packages', - }); return; } - const targetRoutineName = String(navigationTarget.routineName || '').trim(); - if (!targetRoutineName) return; - const routineTypeLabel = navigationTarget.routineType === 'PROCEDURE' - ? translate('sidebar.object.procedure') - : translate('sidebar.object.function'); - const targetSchemaName = String(navigationTarget.schemaName || '').trim(); - const sidebarLocateKey = `${connectionId}-${targetDbName}-routine-${targetRoutineName}`; - addTab({ - id: `routine-def-${connectionId}-${targetDbName}-${targetRoutineName}`, - title: translate('sidebar.tab.routine_definition', { - type: routineTypeLabel, - name: targetRoutineName, - }), - type: 'routine-def', - connectionId, - dbName: targetDbName, - routineName: targetRoutineName, - routineType: navigationTarget.routineType, - schemaName: targetSchemaName || undefined, - sidebarLocateKey, - }); - dispatchQueryEditorSidebarLocate({ - tabId: sidebarLocateKey, - connectionId, - dbName: targetDbName, - routineName: targetRoutineName, - tableName: targetRoutineName, - schemaName: targetSchemaName, - objectGroup: 'routines', - }); + void openRoutineObjectEditTab(navigationTarget, connectionId, targetDbName); }); editor.onDidDispose?.(() => {