🐛 fix(sql-editor): 修复对象超链接定位并支持侧栏拖拽插入

- 修复视图、触发器、过程超链接打开后的左树定位失败
- 修正触发器对象内容不显示及错误提示文案
- 支持左侧对象名拖拽插入 SQL 编辑器
This commit is contained in:
Syngnat
2026-06-02 20:29:42 +08:00
parent dd8af73887
commit 8fba42adbf
8 changed files with 281 additions and 13 deletions

View File

@@ -1 +1 @@
0295a42fd931778d85157816d79d29e5
d0464f9da25e9356e61652e638c99ffe

View File

@@ -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,

View File

@@ -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();

View File

@@ -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>;
};

View File

@@ -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()}'`];
}

View File

@@ -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
}

View File

@@ -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)),

View 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;
}
};