mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-13 00:13:33 +08:00
🐛 fix(definition-viewer): 修复对象修改保留完整定义
- 统一对象查看页定义展示,补齐可复制的 CREATE OR REPLACE DDL - 打开对象修改前清理同标签旧草稿,避免旧草稿覆盖完整注释 - 补充 Oracle 函数、过程和包定义的回归测试
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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<DefinitionViewerProps> = ({ tab }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -700,6 +741,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ 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<DefinitionViewerProps> = ({ 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<DefinitionViewerProps> = ({ 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<DefinitionViewerProps> = ({ 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<DefinitionViewerProps> = ({ 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<DefinitionViewerProps> = ({ 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<DefinitionViewerProps> = ({ 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<DefinitionViewerProps> = ({ tab }) => {
|
||||
setDefinition(latestDefinition);
|
||||
const query = buildEditableDefinitionSql(tab, latestDefinition, normalizedObjectName, editableDefinitionCopy);
|
||||
setActiveContext({ connectionId: tab.connectionId, dbName });
|
||||
clearQueryTabDraft(tab.id);
|
||||
addTab({
|
||||
id: tab.id,
|
||||
title: editTabTitle,
|
||||
|
||||
Reference in New Issue
Block a user