🐛 fix(table-designer): 优化表设计触发器修改入口

- 修改触发器从固定弹窗改为对象编辑 SQL 标签页
- 生成删除旧触发器和创建新触发器脚本,便于执行前审查
- 抽出触发器编辑 SQL 构造工具,统一 TriggerViewer 与 TableDesigner 逻辑
- 保留新增触发器原弹窗路径,降低行为变更范围
- 新增触发器编辑入口与 SQL 构造回归测试
Refs #557
This commit is contained in:
Syngnat
2026-06-12 17:39:34 +08:00
parent 8519748512
commit ae2b27c4b4
5 changed files with 128 additions and 29 deletions

View File

@@ -0,0 +1,39 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
const tableDesignerSource = readFileSync(
path.resolve(__dirname, './TableDesigner.tsx'),
'utf8',
);
const getFunctionBlock = (source: string, name: string): string => {
const start = source.indexOf(`const ${name} = () => {`);
expect(start).toBeGreaterThanOrEqual(0);
const nextFunction = source.indexOf('\n const ', start + 1);
expect(nextFunction).toBeGreaterThan(start);
return source.slice(start, nextFunction);
};
describe('TableDesigner trigger edit entry', () => {
it('opens trigger edits in an object-edit query tab instead of the fixed modal', () => {
const editBlock = getFunctionBlock(tableDesignerSource, 'handleEditTrigger');
expect(editBlock).toContain('setActiveContext({ connectionId: tab.connectionId, dbName });');
expect(editBlock).toContain('addTab({');
expect(editBlock).toContain("type: 'query'");
expect(editBlock).toContain("queryMode: 'object-edit'");
expect(editBlock).toContain('buildEditableTriggerSql(selectedTrigger.name, createSql');
expect(editBlock).toContain('dropSql: buildDropTriggerSql(selectedTrigger.name)');
expect(editBlock).not.toContain('setIsTriggerEditModalOpen(true)');
});
it('keeps trigger creation on the existing modal path', () => {
const createBlock = getFunctionBlock(tableDesignerSource, 'handleCreateTrigger');
expect(createBlock).toContain("setTriggerEditMode('create')");
expect(createBlock).toContain('setTriggerEditSql(generateTriggerTemplate())');
expect(createBlock).toContain('setIsTriggerEditModalOpen(true)');
});
});

View File

@@ -20,6 +20,7 @@ import {
getColumnDefinitionExtra,
normalizeColumnDefinition,
} from '../utils/columnDefinition';
import { buildEditableTriggerSql } from '../utils/triggerEditSql';
import {
isMysqlFamilyDialect as isMysqlFamilySqlDialect,
isOracleLikeDialect as isOracleLikeSqlDialect,
@@ -451,6 +452,8 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em
const [inlineCommentEditingKey, setInlineCommentEditingKey] = useState('');
const connections = useStore(state => state.connections);
const addTab = useStore(state => state.addTab);
const setActiveContext = useStore(state => state.setActiveContext);
const theme = useStore(state => state.theme);
const appearance = useStore(state => state.appearance);
const darkMode = theme === 'dark';
@@ -1058,8 +1061,6 @@ END;`;
const handleEditTrigger = () => {
if (!selectedTrigger) return;
setTriggerEditMode('edit');
// 构建完整的 CREATE TRIGGER 语句
const dbType = getDbType();
const tblName = tab.tableName || '';
let createSql = '';
@@ -1073,8 +1074,19 @@ ${selectedTrigger.statement}`;
createSql = selectedTrigger.statement || '-- 无法获取完整的触发器定义';
}
setTriggerEditSql(createSql);
setIsTriggerEditModalOpen(true);
const dbName = String(tab.dbName || '').trim();
setActiveContext({ connectionId: tab.connectionId, dbName });
addTab({
id: `query-edit-trigger-${tab.connectionId}-${dbName}-${tab.tableName || ''}-${selectedTrigger.name}-${Date.now()}`,
title: `修改触发器: ${selectedTrigger.name}`,
type: 'query',
connectionId: tab.connectionId,
dbName,
query: buildEditableTriggerSql(selectedTrigger.name, createSql, {
dropSql: buildDropTriggerSql(selectedTrigger.name),
}),
queryMode: 'object-edit',
});
};
const handleDeleteTrigger = () => {

View File

@@ -8,17 +8,12 @@ import { DBQuery } from '../../wailsjs/go/app/App';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
import { splitQualifiedNameLast } from '../utils/qualifiedName';
import { buildEditableTriggerSql } from '../utils/triggerEditSql';
interface TriggerViewerProps {
tab: TabData;
}
const ensureSqlStatementTerminator = (sql: string): string => {
const normalized = String(sql || '').trim();
if (!normalized) return '';
return /;\s*$/.test(normalized) ? normalized : `${normalized};`;
};
const getCaseInsensitiveRawValue = (row: Record<string, any>, keys: string[]): any => {
const normalizedKeyMap = new Map<string, string>();
Object.keys(row || {}).forEach((key) => normalizedKeyMap.set(key.toLowerCase(), key));
@@ -105,25 +100,6 @@ const buildOracleLikeTriggerDDLFromMetadata = (
return `CREATE OR REPLACE TRIGGER ${qualifiedTriggerName}\n${triggerType} ${triggeringEvent} ON ${qualifiedTableName}${normalizedWhenClause}\n${triggerBody}`;
};
const buildEditableTriggerSql = (triggerName: string, triggerDefinition: string): string => {
const normalizedName = String(triggerName || '').trim();
const normalizedDefinition = String(triggerDefinition || '').trim();
const header = `-- 修改触发器: ${normalizedName}\n-- \n`;
if (!normalizedDefinition) {
return `${header}-- CREATE TRIGGER \n`;
}
if (/^\s*create\s+(?:or\s+replace\s+)?trigger\b/i.test(normalizedDefinition)) {
return `${header}${ensureSqlStatementTerminator(normalizedDefinition)}`;
}
if (/^\s*trigger\b/i.test(normalizedDefinition)) {
return `${header}${ensureSqlStatementTerminator(normalizedDefinition.replace(/^\s*trigger\b/i, 'CREATE OR REPLACE TRIGGER'))}`;
}
if (/^\s*(?:before|after|instead\s+of)\b/i.test(normalizedDefinition)) {
return `${header}${ensureSqlStatementTerminator(`CREATE OR REPLACE TRIGGER ${normalizedName}\n${normalizedDefinition}`)}`;
}
return `${header}-- CREATE TRIGGER \n${ensureSqlStatementTerminator(normalizedDefinition)}`;
};
const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

View File

@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';
import { buildEditableTriggerSql } from './triggerEditSql';
describe('triggerEditSql', () => {
it('builds a replace-style trigger edit script with drop and create statements', () => {
const sql = buildEditableTriggerSql(
'bit_check',
'CREATE TRIGGER `bit_check`\nBEFORE INSERT ON `c_check`\nFOR EACH ROW\nBEGIN\n SET NEW.flag = 1;\nEND',
{ dropSql: 'DROP TRIGGER IF EXISTS `bit_check`' },
);
expect(sql).toContain('-- 修改触发器: bit_check');
expect(sql).toContain('表设计修改会先删除原触发器,再创建新触发器');
expect(sql).toContain('DROP TRIGGER IF EXISTS `bit_check`;');
expect(sql).toContain('CREATE TRIGGER `bit_check`');
expect(sql.trim().endsWith(';')).toBe(true);
});
});

View File

@@ -0,0 +1,53 @@
export const ensureSqlStatementTerminator = (sql: string): string => {
const normalized = String(sql || '').trim();
if (!normalized) return '';
return /;\s*$/.test(normalized) ? normalized : `${normalized};`;
};
const buildTriggerEditHeader = (
triggerName: string,
options?: { dropSql?: string },
): string => {
const normalizedName = String(triggerName || '').trim();
const hint = String(options?.dropSql || '').trim()
? '表设计修改会先删除原触发器,再创建新触发器,请确认后执行'
: '请确认语法兼容当前数据库后执行';
return `-- 修改触发器: ${normalizedName}\n-- ${hint}\n`;
};
const normalizeEditableTriggerDefinition = (
triggerName: string,
triggerDefinition: string,
): string => {
const normalizedName = String(triggerName || '').trim();
const normalizedDefinition = String(triggerDefinition || '').trim();
if (!normalizedDefinition) {
return '-- 当前触发器定义为空,请补全 CREATE TRIGGER 语句后执行';
}
if (/^\s*create\s+(?:or\s+replace\s+)?trigger\b/i.test(normalizedDefinition)) {
return ensureSqlStatementTerminator(normalizedDefinition);
}
if (/^\s*trigger\b/i.test(normalizedDefinition)) {
return ensureSqlStatementTerminator(
normalizedDefinition.replace(/^\s*trigger\b/i, 'CREATE OR REPLACE TRIGGER'),
);
}
if (/^\s*(?:before|after|instead\s+of)\b/i.test(normalizedDefinition)) {
return ensureSqlStatementTerminator(`CREATE OR REPLACE TRIGGER ${normalizedName}\n${normalizedDefinition}`);
}
return `-- 当前数据源仅返回触发器定义片段,请补全 CREATE TRIGGER 语句后执行\n${ensureSqlStatementTerminator(normalizedDefinition)}`;
};
export const buildEditableTriggerSql = (
triggerName: string,
triggerDefinition: string,
options?: { dropSql?: string },
): string => {
const header = buildTriggerEditHeader(triggerName, options);
const dropSql = String(options?.dropSql || '').trim();
const createSql = normalizeEditableTriggerDefinition(triggerName, triggerDefinition);
if (!dropSql) {
return `${header}${createSql}`;
}
return `${header}${ensureSqlStatementTerminator(dropSql)}\n${createSql}`;
};