diff --git a/frontend/src/components/DefinitionViewer.object-edit.test.tsx b/frontend/src/components/DefinitionViewer.object-edit.test.tsx index 3e314074..3efcb8a6 100644 --- a/frontend/src/components/DefinitionViewer.object-edit.test.tsx +++ b/frontend/src/components/DefinitionViewer.object-edit.test.tsx @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TabData } from '../types'; import { I18nProvider } from '../i18n/provider'; +import { clearQueryTabDraft, hasQueryTabDraft, setQueryTabDraft } from '../utils/sqlFileTabDrafts'; import DefinitionViewer from './DefinitionViewer'; const storeState = vi.hoisted(() => ({ @@ -102,6 +103,7 @@ describe('DefinitionViewer object edit entry', () => { storeState.setActiveContext.mockReset(); storeState.theme = 'light'; storeState.connections[0].config.type = 'postgres'; + clearQueryTabDraft('routine-def-conn-1-H2-H2.F_GET_BUSPRICE'); backendApp.DBQuery.mockReset(); backendApp.DBQuery.mockResolvedValue({ success: true, @@ -117,6 +119,9 @@ describe('DefinitionViewer object edit entry', () => { }); const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0]; + const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join('')); + expect(editorText).toContain('CREATE OR REPLACE VIEW reporting.active_users AS'); + expect(editorText).toContain('SELECT id, name FROM users;'); await act(async () => { button.props.onClick(); @@ -367,6 +372,10 @@ describe('DefinitionViewer object edit entry', () => { await flushPromises(); }); + const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join('')); + expect(editorText).toContain('CREATE OR REPLACE PROCEDURE proc_tally2accept(p_id IN NUMBER)'); + expect(editorText).toContain('v_count PLS_INTEGER;'); + const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0]; await act(async () => { @@ -379,6 +388,71 @@ describe('DefinitionViewer object edit entry', () => { expect(query).toContain('SELECT COUNT(*) INTO v_count FROM dual;'); }); + it('clears stale same-tab drafts before opening object edit so comments are not replaced', async () => { + storeState.connections[0].config.type = 'oracle'; + const tab = createTab({ + id: 'routine-def-conn-1-H2-H2.F_GET_BUSPRICE', + title: '函数: H2.F_GET_BUSPRICE', + type: 'routine-def', + dbName: 'H2', + routineName: 'H2.F_GET_BUSPRICE', + routineType: 'FUNCTION', + viewName: undefined, + viewKind: undefined, + }); + backendApp.DBQuery.mockResolvedValue({ + success: true, + data: [ + { TEXT: 'FUNCTION "F_GET_BUSPRICE"(p_compid t_store_d.compid%TYPE,\n' }, + { TEXT: ' p_busno t_store_d.busno%TYPE,\n' }, + { TEXT: ' p_wareid t_store_d.wareid%TYPE,\n' }, + { TEXT: ' p_type INTEGER)\n' }, + { TEXT: 'RETURN t_ware_saleprice.saleprice%TYPE IS\n\n' }, + { TEXT: 'BEGIN\n' }, + { TEXT: ' /*根据机构跟商品,返回对应的价格信息\n' }, + { TEXT: ' 1: p_compid 企业编码\n' }, + { TEXT: ' 2: p_busno 业务机构\n' }, + { TEXT: ' 3: p_wareid 商品编码\n' }, + { TEXT: ' 4: p_type 类型 1, 当前机构商品毛利率; 2, 当前机构商品毛利; 3, 当前机构商品最新进价; 4, 当前机构商品销售价\n' }, + { TEXT: ' */\n' }, + { TEXT: ' NULL; -- add by txy 1723 20171129 增加库存不等于0的条件\n' }, + { TEXT: 'END;\n' }, + ], + }); + + setQueryTabDraft(tab.id, [ + '-- 修改函数/存储过程: H2.F_GET_BUSPRICE', + 'CREATE OR REPLACE FUNCTION "F_GET_BUSPRICE" AS', + 'BEGIN', + ' /*根据机构跟商品,返回对应的价格信息', + ' 1: p_compid 企业编码', + ' */', + 'END;', + ].join('\n')); + expect(hasQueryTabDraft(tab.id)).toBe(true); + + let renderer: any; + await act(async () => { + renderer = create(renderWithI18n(tab)); + await flushPromises(); + }); + + const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join('')); + expect(editorText).toContain('CREATE OR REPLACE FUNCTION "F_GET_BUSPRICE"'); + expect(editorText).toContain('4: p_type 类型 1, 当前机构商品毛利率; 2, 当前机构商品毛利; 3, 当前机构商品最新进价; 4, 当前机构商品销售价'); + + const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0]; + await act(async () => { + await button.props.onClick(); + await flushPromises(); + }); + + const editQuery = String(storeState.addTab.mock.calls[0][0].query || ''); + expect(editQuery).toContain('4: p_type 类型 1, 当前机构商品毛利率; 2, 当前机构商品毛利; 3, 当前机构商品最新进价; 4, 当前机构商品销售价'); + expect(editQuery).toContain('NULL; -- add by txy 1723 20171129 增加库存不等于0的条件'); + expect(hasQueryTabDraft(tab.id)).toBe(false); + }); + it('opens an editable query tab for Oracle sequence definitions', async () => { storeState.connections[0].config.type = 'oracle'; backendApp.DBQuery.mockResolvedValue({ @@ -466,9 +540,9 @@ describe('DefinitionViewer object edit entry', () => { expect(String(backendApp.DBQuery.mock.calls[0][2] || '')).toContain("TYPE = 'PACKAGE'"); expect(String(backendApp.DBQuery.mock.calls[1][2] || '')).toContain("TYPE = 'PACKAGE BODY'"); const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join('')); - expect(editorText).toContain('PACKAGE pkg_order AS'); + expect(editorText).toContain('CREATE OR REPLACE PACKAGE pkg_order AS'); expect(editorText).toContain('PROCEDURE sync_order(p_id IN NUMBER);'); - expect(editorText).toContain('PACKAGE BODY pkg_order AS'); + expect(editorText).toContain('CREATE OR REPLACE PACKAGE BODY pkg_order AS'); expect(editorText).toContain('NULL;'); const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0]; diff --git a/frontend/src/components/DefinitionViewer.tsx b/frontend/src/components/DefinitionViewer.tsx index 0cd5b058..f0c2fef9 100644 --- a/frontend/src/components/DefinitionViewer.tsx +++ b/frontend/src/components/DefinitionViewer.tsx @@ -10,6 +10,7 @@ import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol'; import { useI18n } from '../i18n/provider'; import { splitQualifiedNameLast } from '../utils/qualifiedName'; import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition'; +import { clearQueryTabDraft } from '../utils/sqlFileTabDrafts'; interface DefinitionViewerProps { tab: TabData; @@ -118,6 +119,46 @@ const buildEditableDefinitionSql = ( return `${header}${ensureSqlStatementTerminator(normalizedDefinition)}`; }; +const buildDisplayDefinitionSql = ( + tab: TabData, + definition: string, + objectName: string, +): string => { + const normalizedDefinition = String(definition || '').trim(); + if (!normalizedDefinition || isCommentOnlyDefinition(normalizedDefinition)) { + return normalizedDefinition; + } + + if (tab.type === 'view-def' && !/^\s*create\b/i.test(normalizedDefinition)) { + if (/^\s*view\b/i.test(normalizedDefinition)) { + return ensureSqlStatementTerminator(normalizedDefinition.replace(/^\s*view\b/i, 'CREATE OR REPLACE VIEW')); + } + return `CREATE OR REPLACE VIEW ${objectName} AS\n${ensureSqlStatementTerminator(normalizedDefinition)}`; + } + + if (tab.type === 'sequence-def' && !/^\s*create\b/i.test(normalizedDefinition)) { + return ensureSqlStatementTerminator(`CREATE SEQUENCE ${objectName}\n${normalizedDefinition}`); + } + + if ( + tab.type === 'package-def' + && !/^\s*create\b/i.test(normalizedDefinition) + && /^\s*package\b/i.test(normalizedDefinition) + ) { + return withCreateOrReplacePackageHeaders(normalizedDefinition); + } + + if ( + tab.type === 'routine-def' + && !/^\s*create\b/i.test(normalizedDefinition) + && /^\s*(function|procedure)\b/i.test(normalizedDefinition) + ) { + return ensureSqlStatementTerminator(`CREATE OR REPLACE ${normalizedDefinition}`); + } + + return ensureSqlStatementTerminator(normalizedDefinition); +}; + const DefinitionViewer: React.FC = ({ tab }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -700,6 +741,7 @@ const DefinitionViewer: React.FC = ({ tab }) => { let queries: string[]; let extractFn: (dialect: string, data: any[]) => string; let resolvedObjectLabel: string; + let resolvedObjectName = ''; if (tab.type === 'view-def') { const viewName = tab.viewName || ''; @@ -711,6 +753,7 @@ const DefinitionViewer: React.FC = ({ tab }) => { resolvedObjectLabel = tab.viewKind === 'materialized' ? t('definition_viewer.object.materialized_view') : t('definition_viewer.object.view'); + resolvedObjectName = viewName; } else if (tab.type === 'event-def') { const eventName = tab.eventName || ''; if (!eventName) { @@ -719,6 +762,7 @@ const DefinitionViewer: React.FC = ({ tab }) => { queries = buildShowEventQueries(dialect, eventName, dbName); extractFn = extractEventDefinition; resolvedObjectLabel = t('definition_viewer.object.event'); + resolvedObjectName = eventName; } else if (tab.type === 'sequence-def') { const sequenceName = tab.sequenceName || ''; if (!sequenceName) { @@ -727,6 +771,7 @@ const DefinitionViewer: React.FC = ({ tab }) => { queries = buildShowSequenceQueries(dialect, sequenceName, dbName); extractFn = extractSequenceDefinition; resolvedObjectLabel = t('definition_viewer.object.sequence'); + resolvedObjectName = sequenceName; } else if (tab.type === 'package-def') { const packageName = tab.packageName || ''; if (!packageName) { @@ -735,6 +780,7 @@ const DefinitionViewer: React.FC = ({ tab }) => { queries = buildShowPackageQueries(dialect, packageName, dbName); extractFn = extractPackageDefinition; resolvedObjectLabel = t('definition_viewer.object.package'); + resolvedObjectName = packageName; } else { const routineName = tab.routineName || ''; const routineType = tab.routineType || 'FUNCTION'; @@ -744,6 +790,7 @@ const DefinitionViewer: React.FC = ({ tab }) => { queries = buildShowRoutineQueries(dialect, routineName, routineType, dbName); extractFn = extractRoutineDefinition; resolvedObjectLabel = t('definition_viewer.object.routine'); + resolvedObjectName = routineName; } if (!queries.length || String(queries[0] || '').startsWith('--')) { @@ -770,7 +817,11 @@ const DefinitionViewer: React.FC = ({ tab }) => { : await runQueryCandidates(config, dbName, queries); if (result.success && Array.isArray(result.data) && result.data.length > 0) { - return { success: true, definition: extractFn(dialect, result.data) }; + const rawDefinition = extractFn(dialect, result.data); + return { + success: true, + definition: buildDisplayDefinitionSql(tab, rawDefinition, resolvedObjectName), + }; } if (result.success) { @@ -944,6 +995,7 @@ const DefinitionViewer: React.FC = ({ tab }) => { setDefinition(latestDefinition); const query = buildEditableDefinitionSql(tab, latestDefinition, normalizedObjectName, editableDefinitionCopy); setActiveContext({ connectionId: tab.connectionId, dbName }); + clearQueryTabDraft(tab.id); addTab({ id: tab.id, title: editTabTitle,