From ae2b27c4b4162d9d2ce2228eaea4574fc1ae8cdc Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 12 Jun 2026 17:39:34 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(table-designer):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=A1=A8=E8=AE=BE=E8=AE=A1=E8=A7=A6=E5=8F=91=E5=99=A8?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改触发器从固定弹窗改为对象编辑 SQL 标签页 - 生成删除旧触发器和创建新触发器脚本,便于执行前审查 - 抽出触发器编辑 SQL 构造工具,统一 TriggerViewer 与 TableDesigner 逻辑 - 保留新增触发器原弹窗路径,降低行为变更范围 - 新增触发器编辑入口与 SQL 构造回归测试 Refs #557 --- .../TableDesigner.trigger-edit.test.ts | 39 ++++++++++++++ frontend/src/components/TableDesigner.tsx | 20 +++++-- frontend/src/components/TriggerViewer.tsx | 26 +-------- frontend/src/utils/triggerEditSql.test.ts | 19 +++++++ frontend/src/utils/triggerEditSql.ts | 53 +++++++++++++++++++ 5 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/TableDesigner.trigger-edit.test.ts create mode 100644 frontend/src/utils/triggerEditSql.test.ts create mode 100644 frontend/src/utils/triggerEditSql.ts diff --git a/frontend/src/components/TableDesigner.trigger-edit.test.ts b/frontend/src/components/TableDesigner.trigger-edit.test.ts new file mode 100644 index 0000000..1f43061 --- /dev/null +++ b/frontend/src/components/TableDesigner.trigger-edit.test.ts @@ -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)'); + }); +}); diff --git a/frontend/src/components/TableDesigner.tsx b/frontend/src/components/TableDesigner.tsx index 3b9a167..de05033 100644 --- a/frontend/src/components/TableDesigner.tsx +++ b/frontend/src/components/TableDesigner.tsx @@ -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 = () => { diff --git a/frontend/src/components/TriggerViewer.tsx b/frontend/src/components/TriggerViewer.tsx index 17d6ab0..ea51980 100644 --- a/frontend/src/components/TriggerViewer.tsx +++ b/frontend/src/components/TriggerViewer.tsx @@ -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, keys: string[]): any => { const normalizedKeyMap = new Map(); 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 = ({ tab }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/frontend/src/utils/triggerEditSql.test.ts b/frontend/src/utils/triggerEditSql.test.ts new file mode 100644 index 0000000..19c09b0 --- /dev/null +++ b/frontend/src/utils/triggerEditSql.test.ts @@ -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); + }); +}); diff --git a/frontend/src/utils/triggerEditSql.ts b/frontend/src/utils/triggerEditSql.ts new file mode 100644 index 0000000..1b8cfc2 --- /dev/null +++ b/frontend/src/utils/triggerEditSql.ts @@ -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}`; +};