From 784d7d60e2cd43c1ddb53c9f8ddce6d1967cf2e6 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 30 Jun 2026 17:25:48 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DMonaco=E6=90=9C=E7=B4=A2=E6=A1=86=E4=B8=8E=E6=82=AC?= =?UTF-8?q?=E6=B5=AE=E6=8F=90=E7=A4=BA=E9=81=AE=E6=8C=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 QueryEditor 中临时的 find widget DOM 监听与 offset zone 逻辑 - 为 V2 查询编辑器的 Monaco stage 增加搜索框可见态顶部安全区并放开必要溢出 - 调整分栏高度计算与回归测试,确保搜索框间距只作用于 V2 query-editor --- .../QueryEditor.external-sql-save.test.tsx | 26 +- frontend/src/components/QueryEditor.tsx | 254 ++++-------------- frontend/src/styles/v2-theme-workbench.css | 16 +- 3 files changed, 84 insertions(+), 212 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 6062699..15bb041 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -612,6 +612,12 @@ const createDefaultConnections = () => ([ const createQueryEditorSplitNodeMock = (element: any) => { const className = String(element?.props?.className || ''); + if (className.includes('gn-v2-query-monaco-stage')) { + return { + style: {}, + getBoundingClientRect: () => ({ height: 300 }), + }; + } if (className.includes('gn-v2-query-monaco-shell')) { return { style: {}, @@ -8503,17 +8509,15 @@ describe('QueryEditor external SQL save', () => { expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-text {'); }); - it('keeps Monaco find widget offset styles scoped to the v2 query editor shell', () => { + it('keeps Monaco find widget spacing scoped to the v2 query editor shell', () => { const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8'); const css = readV2ThemeCss(); - expect(source).toContain('QUERY_EDITOR_MONACO_FIND_WIDGET_OFFSET_PX = 10'); - expect(source).toContain("editor.contrib.findController"); - expect(source).toContain('MutationObserver'); - expect(source).toContain('gn-v2-query-monaco-shell-find-visible'); - expect(source).toContain('heightInPx: QUERY_EDITOR_MONACO_FIND_WIDGET_OFFSET_PX'); - expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-monaco-shell .monaco-editor .find-widget.visible {'); - expect(css).toContain('top: 10px !important;'); + expect(source).toContain('addExtraSpaceOnTop: true'); + expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-monaco-stage:has(.monaco-editor .find-widget.visible:not(.hiddenEditor)) {'); + expect(css).toContain('padding-top: 24px;'); + expect(css).toContain('overflow: visible;'); + expect(css).not.toContain('body[data-ui-version="v2"] .gn-v2-query-monaco-stage .monaco-editor .find-widget {'); }); it('keeps the v2 query editor toolbar grouped and compact', () => { @@ -8715,11 +8719,11 @@ describe('QueryEditor external SQL save', () => { ); }); - const editorShell = renderer.root.find((node) => { + const editorStage = renderer.root.find((node) => { const className = String(node.props?.className || ''); - return className.includes('gn-v2-query-monaco-shell'); + return className.includes('gn-v2-query-monaco-stage'); }); - expect(editorShell.props.style.height).toBe(525); + expect(editorStage.props.style.height).toBe(525); }); it('inserts sidebar object text when dropped into the SQL editor', async () => { diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 16a4143..049c368 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -148,27 +148,11 @@ export { const buildQueryEditorMonacoActionLabel = (key: string): string => `GoNavi: ${translate(key)}`; -const QUERY_EDITOR_MONACO_FIND_WIDGET_OFFSET_PX = 10; const QUERY_EDITOR_MONACO_FIND_OPTIONS = { addExtraSpaceOnTop: true, } as const; const QUERY_EDITOR_NATIVE_SELECT_CURRENT_LINE_EVENT = 'gonavi:native-select-current-line'; -type QueryEditorMonacoFindStateChangeEvent = { - isRevealed?: boolean; -}; - -type QueryEditorMonacoFindState = { - isRevealed?: boolean; - onFindReplaceStateChange?: ( - listener: (event: QueryEditorMonacoFindStateChangeEvent) => void, - ) => { dispose?: () => void } | void; -}; - -type QueryEditorMonacoFindController = { - getState?: () => QueryEditorMonacoFindState | null; -}; - const QUERY_EDITOR_SQL_PROMPT_PLACEHOLDER = '{SQL}'; const escapeQueryEditorObjectEditSqlLiteral = (value: unknown): string => ( @@ -800,13 +784,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc // Resizing state const [editorHeight, setEditorHeight] = useState(300); + const editorStageRef = useRef(null); const editorShellRef = useRef(null); const editorRef = useRef(null); const monacoRef = useRef(null); - const findWidgetOffsetZoneIdRef = useRef(null); - const findWidgetOffsetVisibleRef = useRef(false); - const findWidgetStateDisposableRef = useRef<{ dispose?: () => void } | null>(null); - const findWidgetDomObserverRef = useRef(null); const runQueryActionRef = useRef(null); const selectCurrentStatementActionRef = useRef(null); const duplicateCurrentLineActionRef = useRef(null); @@ -1006,131 +987,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc editor.trigger?.('keyboard', 'actions.find', null); }, []); - const disconnectQueryEditorFindWidgetObserver = useCallback(() => { - findWidgetDomObserverRef.current?.disconnect?.(); - findWidgetDomObserverRef.current = null; - }, []); - const clearQueryEditorFindWidgetOffset = useCallback((editorInstance?: any) => { - editorShellRef.current?.classList?.remove?.('gn-v2-query-monaco-shell-find-visible'); - findWidgetOffsetVisibleRef.current = false; - const editor = editorInstance || editorRef.current; - const currentZoneId = findWidgetOffsetZoneIdRef.current; - if (!currentZoneId || !editor?.changeViewZones) { - findWidgetOffsetZoneIdRef.current = null; - return; - } - editor.changeViewZones((accessor: any) => { - accessor.removeZone(currentZoneId); - }); - findWidgetOffsetZoneIdRef.current = null; - }, []); - const syncQueryEditorFindWidgetOffset = useCallback((editorInstance: any, visible: boolean) => { - const editor = editorInstance || editorRef.current; - const shouldOffset = isV2Ui && visible; - editorShellRef.current?.classList?.toggle?.('gn-v2-query-monaco-shell-find-visible', shouldOffset); - const currentVisible = findWidgetOffsetVisibleRef.current; - const hasZone = Boolean(findWidgetOffsetZoneIdRef.current); - - if (currentVisible === shouldOffset && (!shouldOffset || hasZone)) { - return; - } - findWidgetOffsetVisibleRef.current = shouldOffset; - - if (!editor?.changeViewZones) { - return; - } - - const currentZoneId = findWidgetOffsetZoneIdRef.current; - if (!shouldOffset && !currentZoneId) { - return; - } - - editor.changeViewZones((accessor: any) => { - if (currentZoneId) { - accessor.removeZone(currentZoneId); - findWidgetOffsetZoneIdRef.current = null; - } - if (!shouldOffset || typeof document === 'undefined' || typeof document.createElement !== 'function') { - return; - } - const domNode = document.createElement('div'); - domNode.className = 'gn-v2-query-find-widget-offset-zone'; - domNode.setAttribute('aria-hidden', 'true'); - findWidgetOffsetZoneIdRef.current = accessor.addZone({ - afterLineNumber: 0, - heightInPx: QUERY_EDITOR_MONACO_FIND_WIDGET_OFFSET_PX, - domNode, - suppressMouseDown: true, - }); - }); - }, [isV2Ui]); - const bindQueryEditorFindWidgetDomObserver = useCallback((editorInstance?: any) => { - const editor = editorInstance || editorRef.current; - disconnectQueryEditorFindWidgetObserver(); - if (!editor || !isV2Ui) { - return; - } - - const resolveFindWidgetVisible = () => { - const shell = editorShellRef.current; - const findWidget = shell?.querySelector?.('.monaco-editor .find-widget'); - if (!findWidget || !('classList' in findWidget)) { - return false; - } - return findWidget.classList.contains('visible') && !findWidget.classList.contains('hiddenEditor'); - }; - - const syncFromDom = () => { - syncQueryEditorFindWidgetOffset(editor, resolveFindWidgetVisible()); - }; - - syncFromDom(); - - if (typeof MutationObserver !== 'function' || !editorShellRef.current) { - return; - } - - const observer = new MutationObserver(() => { - syncFromDom(); - }); - observer.observe(editorShellRef.current, { - subtree: true, - childList: true, - attributes: true, - attributeFilter: ['class'], - }); - findWidgetDomObserverRef.current = observer; - - if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') { - window.requestAnimationFrame(() => { - syncFromDom(); - }); - } - }, [disconnectQueryEditorFindWidgetObserver, isV2Ui, syncQueryEditorFindWidgetOffset]); - const bindQueryEditorFindWidgetOffset = useCallback((editorInstance?: any) => { - const editor = editorInstance || editorRef.current; - findWidgetStateDisposableRef.current?.dispose?.(); - findWidgetStateDisposableRef.current = null; - clearQueryEditorFindWidgetOffset(editor); - if (!editor || !isV2Ui) { - return; - } - - const findController = editor.getContribution?.('editor.contrib.findController') as QueryEditorMonacoFindController | null; - const findState = findController?.getState?.(); - bindQueryEditorFindWidgetDomObserver(editor); - if (!findState) { - return; - } - - syncQueryEditorFindWidgetOffset(editor, Boolean(findState.isRevealed)); - findWidgetStateDisposableRef.current = findState.onFindReplaceStateChange?.((event) => { - if (!event?.isRevealed) { - return; - } - syncQueryEditorFindWidgetOffset(editor, Boolean(findState.isRevealed)); - }) || null; - }, [bindQueryEditorFindWidgetDomObserver, clearQueryEditorFindWidgetOffset, isV2Ui, syncQueryEditorFindWidgetOffset]); const handleShowSqlExecutionLog = useCallback((mode: 'open' | 'toggle' = 'toggle') => { if (!isActive) { return; @@ -1158,20 +1014,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }); const autoFetchVisible = useAutoFetchVisibility(); - useEffect(() => { - const editor = editorRef.current; - if (!editor) { - return; - } - bindQueryEditorFindWidgetOffset(editor); - return () => { - disconnectQueryEditorFindWidgetObserver(); - findWidgetStateDisposableRef.current?.dispose?.(); - findWidgetStateDisposableRef.current = null; - clearQueryEditorFindWidgetOffset(editor); - }; - }, [bindQueryEditorFindWidgetOffset, clearQueryEditorFindWidgetOffset, disconnectQueryEditorFindWidgetObserver]); - useEffect(() => { const nextContextKey = [ String(currentConnectionId || '').trim(), @@ -2141,15 +1983,15 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const resolveEditorSplitAvailableHeight = useCallback(() => { const rootRect = queryEditorRootRef.current?.getBoundingClientRect?.(); const paneRect = editorPaneRef.current?.getBoundingClientRect?.(); - const shellRect = editorShellRef.current?.getBoundingClientRect?.(); + const editorContainerRect = (editorStageRef.current || editorShellRef.current)?.getBoundingClientRect?.(); const rootHeight = Number(rootRect?.height || 0); const paneHeight = Number(paneRect?.height || 0); - const shellHeight = Number(shellRect?.height || 0); + const editorContainerHeight = Number(editorContainerRect?.height || 0); if (!Number.isFinite(rootHeight) || rootHeight <= 0) { return 0; } - const nonEditorPaneHeight = paneHeight > 0 && shellHeight > 0 - ? Math.max(0, paneHeight - shellHeight) + const nonEditorPaneHeight = paneHeight > 0 && editorContainerHeight > 0 + ? Math.max(0, paneHeight - editorContainerHeight) : 0; const availableHeight = rootHeight - nonEditorPaneHeight; return Number.isFinite(availableHeight) && availableHeight > 0 ? availableHeight : 0; @@ -2213,8 +2055,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const applyEditorHeightToDom = useCallback(() => { const nextHeight = pendingEditorHeightRef.current; - if (editorShellRef.current) { - editorShellRef.current.style.height = `${nextHeight}px`; + const editorContainer = editorStageRef.current || editorShellRef.current; + if (editorContainer) { + editorContainer.style.height = `${nextHeight}px`; } editorRef.current?.layout?.(); }, []); @@ -2276,7 +2119,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault(); - const currentEditorHeight = Number(editorShellRef.current?.getBoundingClientRect?.().height || editorHeight); + const currentEditorHeight = Number((editorStageRef.current || editorShellRef.current)?.getBoundingClientRect?.().height || editorHeight); const startHeight = Number.isFinite(currentEditorHeight) && currentEditorHeight > 0 ? currentEditorHeight : editorHeight; dragRef.current = { startY: e.clientY, startHeight, currentHeight: startHeight }; pendingEditorHeightRef.current = startHeight; @@ -2584,7 +2427,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc above: false, }, }); - bindQueryEditorFindWidgetOffset(editor); const applyNavigationHoverStateAtPosition = (targetPosition: { lineNumber: number; column: number } | null) => { if (!ctrlMetaPressedRef.current) { @@ -6092,6 +5934,20 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc onFinish={(action) => void finishPendingSqlTransaction(action, 'manual')} /> ); + const queryEditorStageStyle: React.CSSProperties = isResultPanelVisible + ? { + height: editorHeight, + minHeight: '100px', + } + : { + flex: '1 1 auto', + minHeight: 0, + }; + const resolvedQueryEditorStageStyle: React.CSSProperties = isV2Ui + ? { + ...queryEditorStageStyle, + } as React.CSSProperties + : queryEditorStageStyle; return (
@@ -6142,36 +5998,42 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc />
- { - const nextValue = val || ''; - syncQueryDraft(nextValue); - }} - onMount={handleEditorDidMount} - options={{ - minimap: { enabled: false }, - automaticLayout: true, - fixedOverflowWidgets: true, - find: QUERY_EDITOR_MONACO_FIND_OPTIONS, - hover: { - enabled: true, - delay: QUERY_EDITOR_HOVER_DELAY_MS, - above: false, - }, - scrollBeyondLastLine: false, - quickSuggestions: { other: true, comments: false, strings: false }, - suggestOnTriggerCharacters: true, - }} - /> +
+ { + const nextValue = val || ''; + syncQueryDraft(nextValue); + }} + onMount={handleEditorDidMount} + options={{ + minimap: { enabled: false }, + automaticLayout: true, + fixedOverflowWidgets: true, + find: QUERY_EDITOR_MONACO_FIND_OPTIONS, + hover: { + enabled: true, + delay: QUERY_EDITOR_HOVER_DELAY_MS, + above: false, + }, + scrollBeyondLastLine: false, + quickSuggestions: { other: true, comments: false, strings: false }, + suggestOnTriggerCharacters: true, + }} + /> +
{isResultPanelVisible && ( diff --git a/frontend/src/styles/v2-theme-workbench.css b/frontend/src/styles/v2-theme-workbench.css index 84cd650..13e7a72 100644 --- a/frontend/src/styles/v2-theme-workbench.css +++ b/frontend/src/styles/v2-theme-workbench.css @@ -285,19 +285,25 @@ body[data-ui-version="v2"] .gn-v2-query-toolbar-ai-action.ant-btn { } } -body[data-ui-version="v2"] .gn-v2-query-monaco-shell { +body[data-ui-version="v2"] .gn-v2-query-monaco-stage { + position: relative; + display: flex; + flex-direction: column; + min-height: 0; overflow: hidden; border-top: 0.5px solid var(--gn-br-1); border-bottom: 0.5px solid var(--gn-br-1); background: var(--gn-bg-panel); } -body[data-ui-version="v2"] .gn-v2-query-monaco-shell .monaco-editor .find-widget.visible { - top: 10px !important; +body[data-ui-version="v2"] .gn-v2-query-monaco-stage:has(.monaco-editor .find-widget.visible:not(.hiddenEditor)) { + padding-top: 24px; + overflow: visible; } -body[data-ui-version="v2"] .gn-v2-query-monaco-shell .gn-v2-query-find-widget-offset-zone { - pointer-events: none; +body[data-ui-version="v2"] .gn-v2-query-monaco-shell { + min-height: 0; + overflow: visible; } body[data-ui-version="v2"] .gn-v2-query-resizer {