diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index bed8925..7396e24 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -0295a42fd931778d85157816d79d29e5 \ No newline at end of file +d0464f9da25e9356e61652e638c99ffe \ No newline at end of file diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 7f5384c..2d4df89 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -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 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(); + }); + + 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, diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index eaa1918..663f8d4 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -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(); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 34146ba..49fda2c 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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 | null | undefined): || node?.type === 'routine'; }; +const resolveSidebarObjectDragText = (node: Pick | 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: , 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 ( { + 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} {displayTitle} @@ -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 ( + { + 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} + + ); + } + return {statusBadge}{displayTitle}; }; diff --git a/frontend/src/components/TriggerViewer.tsx b/frontend/src/components/TriggerViewer.tsx index ef59bd2..3029580 100644 --- a/frontend/src/components/TriggerViewer.tsx +++ b/frontend/src/components/TriggerViewer.tsx @@ -24,6 +24,14 @@ const TriggerViewer: React.FC = ({ 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 = ({ 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()}'`]; } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 75b5898..8b8d2b6 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -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 } diff --git a/frontend/src/utils/sidebarLocate.ts b/frontend/src/utils/sidebarLocate.ts index d5f7a26..2f33287 100644 --- a/frontend/src/utils/sidebarLocate.ts +++ b/frontend/src/utils/sidebarLocate.ts @@ -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)), diff --git a/frontend/src/utils/sidebarSqlDrag.ts b/frontend/src/utils/sidebarSqlDrag.ts new file mode 100644 index 0000000..613c7e6 --- /dev/null +++ b/frontend/src/utils/sidebarSqlDrag.ts @@ -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; + } +};