mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-01 18:01:25 +08:00
🐛 fix(query-editor): 修复Monaco搜索框与悬浮提示遮挡
- 移除 QueryEditor 中临时的 find widget DOM 监听与 offset zone 逻辑 - 为 V2 查询编辑器的 Monaco stage 增加搜索框可见态顶部安全区并放开必要溢出 - 调整分栏高度计算与回归测试,确保搜索框间距只作用于 V2 query-editor
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user