mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-21 05:53:46 +08:00
🐛 fix(sql-editor): 修复对象超链接定位并支持侧栏拖拽插入
- 修复视图、触发器、过程超链接打开后的左树定位失败 - 修正触发器对象内容不显示及错误提示文案 - 支持左侧对象名拖拽插入 SQL 编辑器
This commit is contained in:
@@ -1 +1 @@
|
||||
0295a42fd931778d85157816d79d29e5
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
@@ -88,7 +88,7 @@ const editorState = vi.hoisted(() => {
|
||||
const state = {
|
||||
value: '',
|
||||
editor: null as any,
|
||||
domNode: { style: { cursor: '' } },
|
||||
domNode: { style: { cursor: '' }, addEventListener: vi.fn(), removeEventListener: vi.fn() },
|
||||
position: { lineNumber: 1, column: 1 },
|
||||
selection: null as any,
|
||||
providers: [] as any[],
|
||||
@@ -1022,7 +1022,17 @@ describe('QueryEditor external SQL save', () => {
|
||||
dbName: 'main',
|
||||
viewName: 'active_users',
|
||||
viewKind: 'view',
|
||||
schemaName: 'reporting',
|
||||
sidebarLocateKey: 'conn-1-main-view-active_users',
|
||||
});
|
||||
expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'gonavi:locate-sidebar-object',
|
||||
detail: expect.objectContaining({
|
||||
tabId: 'conn-1-main-view-active_users',
|
||||
schemaName: 'reporting',
|
||||
objectGroup: 'views',
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it('opens trigger and routine tabs on ctrl left click inside the editor', async () => {
|
||||
@@ -1083,6 +1093,9 @@ describe('QueryEditor external SQL save', () => {
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
triggerName: 'audit.users_bi',
|
||||
triggerTableName: 'audit.users',
|
||||
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',
|
||||
@@ -1092,6 +1105,8 @@ describe('QueryEditor external SQL save', () => {
|
||||
dbName: 'main',
|
||||
routineName: 'reporting.refresh_stats',
|
||||
routineType: 'PROCEDURE',
|
||||
schemaName: 'reporting',
|
||||
sidebarLocateKey: 'conn-1-main-routine-reporting.refresh_stats',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2435,6 +2450,50 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(document.removeEventListener).toHaveBeenCalledWith('mouseup', expect.any(Function));
|
||||
});
|
||||
|
||||
it('inserts sidebar object text when dropped into the SQL editor', async () => {
|
||||
const domListeners: Record<string, ((event?: any) => void)[]> = {};
|
||||
editorState.domNode = {
|
||||
style: { cursor: '' },
|
||||
addEventListener: vi.fn((type: string, listener: (event?: any) => void) => {
|
||||
domListeners[type] ||= [];
|
||||
domListeners[type].push(listener);
|
||||
}),
|
||||
removeEventListener: vi.fn(),
|
||||
} as any;
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: 'select * from ' })} />);
|
||||
});
|
||||
|
||||
editorState.position = { lineNumber: 1, column: 'select * from '.length + 1 };
|
||||
|
||||
await act(async () => {
|
||||
domListeners.drop?.forEach((listener) => listener({
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
dataTransfer: {
|
||||
getData: (type: string) => {
|
||||
if (type === 'application/x-gonavi-sql-object') {
|
||||
return JSON.stringify({ text: 'reporting.active_users' });
|
||||
}
|
||||
if (type === 'text/plain') {
|
||||
return 'reporting.active_users';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
expect(editorState.editor.executeEdits).toHaveBeenCalledWith(
|
||||
'gonavi-sidebar-drop',
|
||||
[expect.objectContaining({ text: 'reporting.active_users' })],
|
||||
);
|
||||
expect(editorState.value).toContain('reporting.active_users');
|
||||
});
|
||||
|
||||
it('runs selected SQL before cursor SQL', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { resolveCurrentSqlStatementRange, resolveExecutableSql } from '../utils/
|
||||
import { isMacLikePlatform } from '../utils/appearance';
|
||||
import { splitSidebarQualifiedName } from '../utils/sidebarLocate';
|
||||
import { normalizeSidebarViewName } from '../utils/sidebarMetadata';
|
||||
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag';
|
||||
import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert';
|
||||
import { ORACLE_ROWID_LOCATOR_COLUMN, type EditRowLocator } from '../utils/rowLocator';
|
||||
import { getQueryTabDraft, hasQueryTabDraft, setQueryTabDraft, setSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
|
||||
@@ -241,6 +242,14 @@ type QueryStatementPlan = {
|
||||
warning?: string;
|
||||
};
|
||||
|
||||
const readSidebarSqlDropText = (event: DragEvent): string => {
|
||||
const payload = decodeSidebarSqlEditorDragPayload(String(event.dataTransfer?.getData(SIDEBAR_SQL_EDITOR_DRAG_MIME) || ''));
|
||||
if (payload?.text) {
|
||||
return payload.text;
|
||||
}
|
||||
return String(event.dataTransfer?.getData('text/plain') || '').trim();
|
||||
};
|
||||
|
||||
const stripQueryIdentifierQuotes = (part: string): string => {
|
||||
const text = String(part || '').trim();
|
||||
if (!text) return '';
|
||||
@@ -2007,6 +2016,41 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return query || '';
|
||||
};
|
||||
|
||||
const insertTextIntoEditorAtPosition = useCallback((text: string, position?: { lineNumber: number; column: number } | null) => {
|
||||
const editor = editorRef.current;
|
||||
const monaco = monacoRef.current;
|
||||
const targetPosition = normalizeEditorPosition(position || editor?.getPosition?.() || lastEditorCursorPositionRef.current);
|
||||
if (!editor || !monaco?.Range || !targetPosition || !text) {
|
||||
return false;
|
||||
}
|
||||
editor.focus?.();
|
||||
editor.setPosition?.(targetPosition);
|
||||
editor.executeEdits?.('gonavi-sidebar-drop', [{
|
||||
range: new monaco.Range(
|
||||
targetPosition.lineNumber,
|
||||
targetPosition.column,
|
||||
targetPosition.lineNumber,
|
||||
targetPosition.column,
|
||||
),
|
||||
text,
|
||||
forceMoveMarkers: true,
|
||||
}]);
|
||||
editor.pushUndoStop?.();
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const handleSidebarObjectDrop = useCallback((event: DragEvent) => {
|
||||
const dragText = readSidebarSqlDropText(event);
|
||||
if (!dragText) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const editor = editorRef.current;
|
||||
const dropTarget = editor?.getTargetAtClientPoint?.(event.clientX, event.clientY);
|
||||
insertTextIntoEditorAtPosition(dragText, normalizeEditorPosition(dropTarget?.position));
|
||||
}, [insertTextIntoEditorAtPosition]);
|
||||
|
||||
const handleSelectCurrentStatement = () => {
|
||||
const editor = editorRef.current;
|
||||
const monaco = monacoRef.current;
|
||||
@@ -2509,6 +2553,19 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
editor.updateOptions?.({ mouseStyle: 'text' });
|
||||
setQueryEditorMouseCursor(editor, '');
|
||||
};
|
||||
const editorDomNode = editor.getDomNode?.();
|
||||
const handleEditorDragOver = (rawEvent: Event) => {
|
||||
const event = rawEvent as DragEvent;
|
||||
const dragText = readSidebarSqlDropText(event);
|
||||
if (!dragText) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
};
|
||||
const handleEditorDrop = (rawEvent: Event) => {
|
||||
handleSidebarObjectDrop(rawEvent as DragEvent);
|
||||
};
|
||||
|
||||
// 应用透明主题(主题由 MonacoEditor 包装组件按需注册)
|
||||
monaco.editor.setTheme(darkMode ? 'transparent-dark' : 'transparent-light');
|
||||
@@ -2598,6 +2655,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
window.addEventListener('keydown', syncModifierState);
|
||||
window.addEventListener('keyup', syncModifierState);
|
||||
window.addEventListener('blur', handleWindowBlur);
|
||||
editorDomNode?.addEventListener('dragover', handleEditorDragOver);
|
||||
editorDomNode?.addEventListener('drop', handleEditorDrop);
|
||||
|
||||
editor.onMouseDown?.((event: any) => {
|
||||
const browserEvent = event?.event;
|
||||
@@ -2681,6 +2740,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
if (navigationTarget.type === 'view' || navigationTarget.type === 'materialized-view') {
|
||||
const targetViewName = String(navigationTarget.viewName || '').trim();
|
||||
if (!targetViewName) return;
|
||||
const targetSchemaName = String(navigationTarget.schemaName || '').trim();
|
||||
const sidebarLocateKey = navigationTarget.type === 'materialized-view'
|
||||
? `${connectionId}-${targetDbName}-materialized-view-${targetViewName}`
|
||||
: `${connectionId}-${targetDbName}-view-${targetViewName}`;
|
||||
addTab({
|
||||
id: `view-def-${connectionId}-${targetDbName}-${targetViewName}`,
|
||||
title: `${navigationTarget.type === 'materialized-view' ? '物化视图' : '视图'}: ${targetViewName}`,
|
||||
@@ -2689,13 +2752,16 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
dbName: targetDbName,
|
||||
viewName: targetViewName,
|
||||
viewKind: navigationTarget.type === 'materialized-view' ? 'materialized' : 'view',
|
||||
schemaName: targetSchemaName || undefined,
|
||||
sidebarLocateKey,
|
||||
});
|
||||
dispatchQueryEditorSidebarLocate({
|
||||
tabId: sidebarLocateKey,
|
||||
connectionId,
|
||||
dbName: targetDbName,
|
||||
viewName: targetViewName,
|
||||
tableName: targetViewName,
|
||||
schemaName: navigationTarget.schemaName,
|
||||
schemaName: targetSchemaName,
|
||||
objectGroup: navigationTarget.type === 'materialized-view' ? 'materializedViews' : 'views',
|
||||
});
|
||||
return;
|
||||
@@ -2704,6 +2770,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
if (navigationTarget.type === 'trigger') {
|
||||
const targetTriggerName = String(navigationTarget.triggerName || '').trim();
|
||||
if (!targetTriggerName) return;
|
||||
const targetTriggerTableName = String(navigationTarget.tableName || '').trim();
|
||||
const targetSchemaName = String(navigationTarget.schemaName || '').trim();
|
||||
const sidebarLocateKey = `${connectionId}-${targetDbName}-trigger-${targetTriggerName}-${targetTriggerTableName}`;
|
||||
addTab({
|
||||
id: `trigger-${connectionId}-${targetDbName}-${targetTriggerName}`,
|
||||
title: `触发器: ${targetTriggerName}`,
|
||||
@@ -2711,13 +2780,17 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
connectionId,
|
||||
dbName: targetDbName,
|
||||
triggerName: targetTriggerName,
|
||||
triggerTableName: targetTriggerTableName || undefined,
|
||||
schemaName: targetSchemaName || undefined,
|
||||
sidebarLocateKey,
|
||||
});
|
||||
dispatchQueryEditorSidebarLocate({
|
||||
tabId: sidebarLocateKey,
|
||||
connectionId,
|
||||
dbName: targetDbName,
|
||||
triggerName: targetTriggerName,
|
||||
tableName: targetTriggerName,
|
||||
schemaName: navigationTarget.schemaName,
|
||||
schemaName: targetSchemaName,
|
||||
objectGroup: 'triggers',
|
||||
});
|
||||
return;
|
||||
@@ -2725,6 +2798,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
|
||||
const targetRoutineName = String(navigationTarget.routineName || '').trim();
|
||||
if (!targetRoutineName) return;
|
||||
const targetSchemaName = String(navigationTarget.schemaName || '').trim();
|
||||
const sidebarLocateKey = `${connectionId}-${targetDbName}-routine-${targetRoutineName}`;
|
||||
addTab({
|
||||
id: `routine-def-${connectionId}-${targetDbName}-${targetRoutineName}`,
|
||||
title: `${navigationTarget.routineType === 'PROCEDURE' ? '存储过程' : '函数'}: ${targetRoutineName}`,
|
||||
@@ -2733,13 +2808,16 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
dbName: targetDbName,
|
||||
routineName: targetRoutineName,
|
||||
routineType: navigationTarget.routineType,
|
||||
schemaName: targetSchemaName || undefined,
|
||||
sidebarLocateKey,
|
||||
});
|
||||
dispatchQueryEditorSidebarLocate({
|
||||
tabId: sidebarLocateKey,
|
||||
connectionId,
|
||||
dbName: targetDbName,
|
||||
routineName: targetRoutineName,
|
||||
tableName: targetRoutineName,
|
||||
schemaName: navigationTarget.schemaName,
|
||||
schemaName: targetSchemaName,
|
||||
objectGroup: 'routines',
|
||||
});
|
||||
});
|
||||
@@ -2755,6 +2833,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
window.removeEventListener('keydown', syncModifierState);
|
||||
window.removeEventListener('keyup', syncModifierState);
|
||||
window.removeEventListener('blur', handleWindowBlur);
|
||||
editorDomNode?.removeEventListener('dragover', handleEditorDragOver);
|
||||
editorDomNode?.removeEventListener('drop', handleEditorDrop);
|
||||
});
|
||||
|
||||
refreshObjectDecorations();
|
||||
|
||||
@@ -82,6 +82,7 @@ import { buildJVMDiagnosticActionDescriptor, buildJVMMonitoringActionDescriptors
|
||||
import { buildTableSelectQuery } from '../utils/objectQueryTemplates';
|
||||
import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts';
|
||||
import { buildExternalSQLDirectoryId, buildExternalSQLRootNode, buildExternalSQLTabId, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
|
||||
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag';
|
||||
import JVMModeBadge from './jvm/JVMModeBadge';
|
||||
import {
|
||||
V2DatabaseContextMenuView,
|
||||
@@ -165,6 +166,16 @@ const isV2SidebarObjectNode = (node: Pick<TreeNode, 'type'> | null | undefined):
|
||||
|| node?.type === 'routine';
|
||||
};
|
||||
|
||||
const resolveSidebarObjectDragText = (node: Pick<TreeNode, 'type' | 'title' | 'dataRef'> | null | undefined): string => {
|
||||
const dataRef = node?.dataRef || {};
|
||||
if (node?.type === 'table') return String(dataRef.tableName || node?.title || '').trim();
|
||||
if (node?.type === 'view' || node?.type === 'materialized-view') return String(dataRef.viewName || dataRef.tableName || node?.title || '').trim();
|
||||
if (node?.type === 'db-trigger') return String(dataRef.triggerName || node?.title || '').trim();
|
||||
if (node?.type === 'routine') return String(dataRef.routineName || node?.title || '').trim();
|
||||
if (node?.type === 'db-event') return String(dataRef.eventName || node?.title || '').trim();
|
||||
return '';
|
||||
};
|
||||
|
||||
export const hasSidebarLazyChildren = (children: unknown): boolean => {
|
||||
return Array.isArray(children) && children.length > 0;
|
||||
};
|
||||
@@ -2926,7 +2937,7 @@ const Sidebar: React.FC<{
|
||||
key: `${conn.id}-${conn.dbName}-trigger-${entry.triggerName}-${entry.tableName}`,
|
||||
icon: <FunctionOutlined />,
|
||||
type: 'db-trigger',
|
||||
dataRef: { ...conn, triggerName: entry.triggerName, triggerTableName: entry.tableName, schemaName: entry.schemaName },
|
||||
dataRef: { ...conn, triggerName: entry.triggerName, triggerTableName: entry.tableName, tableName: entry.tableName, schemaName: entry.schemaName },
|
||||
isLeaf: true,
|
||||
});
|
||||
|
||||
@@ -3121,7 +3132,15 @@ const Sidebar: React.FC<{
|
||||
const target = resolveSidebarLocateTarget(request, {
|
||||
groupBySchema: shouldHideSchemaPrefix(conn),
|
||||
});
|
||||
const objectLabel = request.objectGroup === 'materializedViews' ? '物化视图' : (request.objectGroup === 'views' ? '视图' : '表');
|
||||
const objectLabel = request.objectGroup === 'materializedViews'
|
||||
? '物化视图'
|
||||
: request.objectGroup === 'views'
|
||||
? '视图'
|
||||
: request.objectGroup === 'triggers'
|
||||
? '触发器'
|
||||
: request.objectGroup === 'routines'
|
||||
? '函数/存储过程'
|
||||
: '表';
|
||||
|
||||
let path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
|
||||
const dbLoadKey = `dbs-${request.connectionId}`;
|
||||
@@ -3440,14 +3459,17 @@ const Sidebar: React.FC<{
|
||||
});
|
||||
return;
|
||||
} else if (node.type === 'db-trigger') {
|
||||
const { triggerName, dbName, id } = node.dataRef;
|
||||
const { triggerName, triggerTableName, schemaName, dbName, id } = node.dataRef;
|
||||
addTab({
|
||||
id: `trigger-${node.key}`,
|
||||
title: `触发器: ${triggerName}`,
|
||||
type: 'trigger',
|
||||
connectionId: id,
|
||||
dbName,
|
||||
triggerName
|
||||
triggerName,
|
||||
triggerTableName,
|
||||
schemaName,
|
||||
sidebarLocateKey: String(node.key || ''),
|
||||
});
|
||||
return;
|
||||
} else if (node.type === 'db-event') {
|
||||
@@ -6758,6 +6780,7 @@ const Sidebar: React.FC<{
|
||||
const renderV2TreeTitle = (node: any, hoverTitle: string, statusBadge: React.ReactNode) => {
|
||||
const rawTitle = String(node.title ?? '');
|
||||
const groupKey = String(node?.dataRef?.groupKey || '');
|
||||
const dragText = resolveSidebarObjectDragText(node);
|
||||
if (node.type === 'v2-table-section') {
|
||||
return (
|
||||
<span
|
||||
@@ -6846,10 +6869,32 @@ const Sidebar: React.FC<{
|
||||
<span
|
||||
className={titleClassName}
|
||||
title={hoverTitle}
|
||||
draggable={!!dragText}
|
||||
data-node-type={node.type}
|
||||
data-group-key={groupKey || undefined}
|
||||
data-sidebar-node-key={String(node.key || '')}
|
||||
data-sidebar-node-type={String(node.type || '')}
|
||||
onDragStart={dragText ? (event) => {
|
||||
snapshotTreeSelectionBeforeDrag();
|
||||
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
|
||||
setIsTreeDragging(true);
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('text/plain', dragText);
|
||||
event.dataTransfer.setData(
|
||||
SIDEBAR_SQL_EDITOR_DRAG_MIME,
|
||||
encodeSidebarSqlEditorDragPayload({
|
||||
text: dragText,
|
||||
nodeType: node.type,
|
||||
connectionId: String(node?.dataRef?.id || ''),
|
||||
dbName: String(node?.dataRef?.dbName || ''),
|
||||
}),
|
||||
);
|
||||
} : undefined}
|
||||
onDragEnd={dragText ? () => {
|
||||
restoreTreeSelectionAfterDrag();
|
||||
setIsTreeDragging(false);
|
||||
} : undefined}
|
||||
>
|
||||
{statusBadge}
|
||||
<span className="gn-v2-tree-label">{displayTitle}</span>
|
||||
@@ -8089,6 +8134,7 @@ const Sidebar: React.FC<{
|
||||
) : null;
|
||||
|
||||
const displayTitle = String(node.title ?? '');
|
||||
const dragText = resolveSidebarObjectDragText(node);
|
||||
let hoverTitle = displayTitle;
|
||||
if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view' || node.type === 'db-event') {
|
||||
const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || '').trim();
|
||||
@@ -8156,6 +8202,38 @@ const Sidebar: React.FC<{
|
||||
return renderV2TreeTitle(node, hoverTitle, statusBadge);
|
||||
}
|
||||
|
||||
if (dragText) {
|
||||
return (
|
||||
<span
|
||||
title={hoverTitle}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
snapshotTreeSelectionBeforeDrag();
|
||||
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
|
||||
setIsTreeDragging(true);
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('text/plain', dragText);
|
||||
event.dataTransfer.setData(
|
||||
SIDEBAR_SQL_EDITOR_DRAG_MIME,
|
||||
encodeSidebarSqlEditorDragPayload({
|
||||
text: dragText,
|
||||
nodeType: node.type,
|
||||
connectionId: String(node?.dataRef?.id || ''),
|
||||
dbName: String(node?.dataRef?.dbName || ''),
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
restoreTreeSelectionAfterDrag();
|
||||
setIsTreeDragging(false);
|
||||
}}
|
||||
>
|
||||
{statusBadge}{displayTitle}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span title={hoverTitle}>{statusBadge}{displayTitle}</span>;
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,14 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
|
||||
|
||||
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
|
||||
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
|
||||
const parseSchemaAndName = (fullName: string): { schema: string; name: string } => {
|
||||
const raw = String(fullName || '').trim();
|
||||
const idx = raw.lastIndexOf('.');
|
||||
if (idx > 0 && idx < raw.length - 1) {
|
||||
return { schema: raw.substring(0, idx), name: raw.substring(idx + 1) };
|
||||
}
|
||||
return { schema: '', name: raw };
|
||||
};
|
||||
|
||||
const getMetadataDialect = (conn: any): string => {
|
||||
const type = String(conn?.config?.type || '').trim().toLowerCase();
|
||||
@@ -49,13 +57,14 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
|
||||
};
|
||||
|
||||
const buildShowTriggerQueries = (dialect: string, triggerName: string, dbName: string): string[] => {
|
||||
const safeTriggerName = escapeSQLLiteral(triggerName);
|
||||
const { schema, name } = parseSchemaAndName(triggerName);
|
||||
const safeTriggerName = escapeSQLLiteral(name);
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return [
|
||||
`SHOW CREATE TRIGGER \`${triggerName.replace(/`/g, '``')}\``,
|
||||
`SHOW CREATE TRIGGER \`${name.replace(/`/g, '``')}\``,
|
||||
safeDbName
|
||||
? `SELECT ACTION_STATEMENT AS trigger_definition FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' AND trigger_name = '${safeTriggerName}' LIMIT 1`
|
||||
: '',
|
||||
@@ -75,10 +84,13 @@ WHERE t.tgname = '${safeTriggerName}'
|
||||
AND NOT t.tgisinternal
|
||||
LIMIT 1`];
|
||||
case 'sqlserver': {
|
||||
return [`SELECT OBJECT_DEFINITION(OBJECT_ID('${safeTriggerName.replace(/'/g, "''")}')) AS trigger_definition`];
|
||||
return [`SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(triggerName)}')) AS trigger_definition`];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
if (schema) {
|
||||
return [`SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${escapeSQLLiteral(schema).toUpperCase()}' AND TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`];
|
||||
}
|
||||
if (!safeDbName) {
|
||||
return [`SELECT TRIGGER_BODY FROM USER_TRIGGERS WHERE TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`];
|
||||
}
|
||||
|
||||
@@ -420,11 +420,14 @@ export interface TabData {
|
||||
resourceKind?: string;
|
||||
redisDB?: number; // Redis database index for redis tabs
|
||||
triggerName?: string; // Trigger name for trigger tabs
|
||||
triggerTableName?: string; // Trigger target table for trigger tabs
|
||||
viewName?: string; // View name for view definition tabs
|
||||
viewKind?: "view" | "materialized";
|
||||
eventName?: string; // Event name for MySQL event definition tabs
|
||||
routineName?: string; // Routine name for function/procedure definition tabs
|
||||
routineType?: string; // 'FUNCTION' or 'PROCEDURE'
|
||||
schemaName?: string; // Schema / owner name for schema-grouped objects
|
||||
sidebarLocateKey?: string; // Precise sidebar tree key for locating an object node
|
||||
savedQueryId?: string; // Saved query identity for quick-save behavior
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,10 @@ export interface SidebarLocateTabLike {
|
||||
viewName?: string;
|
||||
viewKind?: string;
|
||||
triggerName?: string;
|
||||
triggerTableName?: string;
|
||||
routineName?: string;
|
||||
schemaName?: string;
|
||||
sidebarLocateKey?: string;
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
@@ -154,10 +157,11 @@ export const normalizeSidebarLocateObjectRequestFromTab = (tab: SidebarLocateTab
|
||||
}
|
||||
|
||||
return normalizeSidebarLocateObjectRequest({
|
||||
tabId: tab.id,
|
||||
tabId: toTrimmedString(tab.sidebarLocateKey || tab.id) || undefined,
|
||||
connectionId: tab.connectionId,
|
||||
dbName: tab.dbName,
|
||||
tableName: objectName,
|
||||
schemaName: tab.schemaName,
|
||||
objectGroup: tab.type === 'view-def'
|
||||
? (tab.viewKind === 'materialized' ? 'materializedViews' : 'views')
|
||||
: (tab.type === 'trigger' ? 'triggers' : (tab.type === 'routine-def' ? 'routines' : undefined)),
|
||||
|
||||
32
frontend/src/utils/sidebarSqlDrag.ts
Normal file
32
frontend/src/utils/sidebarSqlDrag.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export const SIDEBAR_SQL_EDITOR_DRAG_MIME = 'application/x-gonavi-sql-object';
|
||||
|
||||
export interface SidebarSqlEditorDragPayload {
|
||||
text: string;
|
||||
nodeType?: string;
|
||||
connectionId?: string;
|
||||
dbName?: string;
|
||||
}
|
||||
|
||||
export const encodeSidebarSqlEditorDragPayload = (payload: SidebarSqlEditorDragPayload): string =>
|
||||
JSON.stringify({
|
||||
text: String(payload.text || '').trim(),
|
||||
nodeType: payload.nodeType ? String(payload.nodeType) : undefined,
|
||||
connectionId: payload.connectionId ? String(payload.connectionId) : undefined,
|
||||
dbName: payload.dbName ? String(payload.dbName) : undefined,
|
||||
});
|
||||
|
||||
export const decodeSidebarSqlEditorDragPayload = (value: string): SidebarSqlEditorDragPayload | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '')) as SidebarSqlEditorDragPayload;
|
||||
const text = String(parsed?.text || '').trim();
|
||||
if (!text) return null;
|
||||
return {
|
||||
text,
|
||||
nodeType: parsed?.nodeType ? String(parsed.nodeType) : undefined,
|
||||
connectionId: parsed?.connectionId ? String(parsed.connectionId) : undefined,
|
||||
dbName: parsed?.dbName ? String(parsed.dbName) : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user