🐛 fix(query-editor): 修复Monaco搜索框与悬浮提示遮挡

- 移除 QueryEditor 中临时的 find widget DOM 监听与 offset zone 逻辑
- 为 V2 查询编辑器的 Monaco stage 增加搜索框可见态顶部安全区并放开必要溢出
- 调整分栏高度计算与回归测试,确保搜索框间距只作用于 V2 query-editor
This commit is contained in:
Syngnat
2026-06-30 17:25:48 +08:00
parent ece1119c31
commit 784d7d60e2
3 changed files with 84 additions and 212 deletions

View File

@@ -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 () => {

View File

@@ -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<HTMLDivElement | null>(null);
const editorShellRef = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<any>(null);
const monacoRef = useRef<any>(null);
const findWidgetOffsetZoneIdRef = useRef<string | null>(null);
const findWidgetOffsetVisibleRef = useRef(false);
const findWidgetStateDisposableRef = useRef<{ dispose?: () => void } | null>(null);
const findWidgetDomObserverRef = useRef<MutationObserver | null>(null);
const runQueryActionRef = useRef<any>(null);
const selectCurrentStatementActionRef = useRef<any>(null);
const duplicateCurrentLineActionRef = useRef<any>(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 (
<div ref={queryEditorRootRef} className={isV2Ui ? 'gn-v2-query-editor' : undefined} style={{ flex: '1 1 auto', minHeight: 0, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
@@ -6142,36 +5998,42 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
/>
<div
ref={editorShellRef}
className={isV2Ui ? 'gn-v2-query-monaco-shell' : undefined}
style={isResultPanelVisible ? { height: editorHeight, minHeight: '100px' } : { flex: '1 1 auto', minHeight: 0 }}
ref={editorStageRef}
className={isV2Ui ? 'gn-v2-query-monaco-stage' : undefined}
style={resolvedQueryEditorStageStyle}
>
<Editor
height="100%"
gonaviTypography="code"
defaultLanguage="sql"
theme={darkMode ? "transparent-dark" : "transparent-light"}
defaultValue={query}
onChange={(val) => {
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,
}}
/>
<div
ref={editorShellRef}
className={isV2Ui ? 'gn-v2-query-monaco-shell' : undefined}
style={isV2Ui ? { flex: '1 1 auto', minHeight: 0 } : undefined}
>
<Editor
height="100%"
gonaviTypography="code"
defaultLanguage="sql"
theme={darkMode ? "transparent-dark" : "transparent-light"}
defaultValue={query}
onChange={(val) => {
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,
}}
/>
</div>
</div>
{isResultPanelVisible && (

View File

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