diff --git a/frontend/src/components/DataGrid.ddl.test.tsx b/frontend/src/components/DataGrid.ddl.test.tsx index 3aa74dd7..1a7338dd 100644 --- a/frontend/src/components/DataGrid.ddl.test.tsx +++ b/frontend/src/components/DataGrid.ddl.test.tsx @@ -217,6 +217,7 @@ vi.mock('@ant-design/icons', () => { FilterOutlined: Icon, CloseOutlined: Icon, BugOutlined: Icon, + CodeOutlined: Icon, ConsoleSqlOutlined: Icon, ControlOutlined: Icon, FileTextOutlined: Icon, @@ -1352,6 +1353,161 @@ describe('DataGrid DDL interactions', () => { renderer!.unmount(); }); + it('navigates and closes the current-page finder from its keyboard controls', async () => { + const onCancel = vi.fn(); + const onNavigatePrevious = vi.fn(); + const onNavigateNext = vi.fn(); + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create( + {}} + onCancel={onCancel} + onNavigatePrevious={onNavigatePrevious} + onNavigateNext={onNavigateNext} + />, + ); + }); + const input = renderer!.root.findByType('input'); + const createKeyEvent = (key: string, shiftKey = false) => ({ + key, + shiftKey, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + }); + + await act(async () => { + input.props.onKeyDown(createKeyEvent('Enter')); + input.props.onKeyDown(createKeyEvent('Enter', true)); + input.props.onKeyDown(createKeyEvent('Escape')); + }); + + expect(onNavigateNext).toHaveBeenCalledTimes(1); + expect(onNavigatePrevious).toHaveBeenCalledTimes(1); + expect(onCancel).toHaveBeenCalledTimes(1); + renderer!.unmount(); + }); + + it('opens the V2 current-page finder with Ctrl+F and closes it without duplicating the widget', async () => { + storeState.appearance.uiVersion = 'v2'; + Object.defineProperty(navigator, 'platform', { + configurable: true, + value: 'Win32', + }); + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create( + , + ); + }); + await waitForEffects(); + + expect(renderer!.root.findAllByType(DataGridPageFind)).toHaveLength(0); + const keydownRegistrations = vi.mocked(window.addEventListener).mock.calls.filter( + ([type, _listener, options]) => type === 'keydown' && options === true, + ); + expect(keydownRegistrations).toHaveLength(1); + const handlePageFindShortcut = keydownRegistrations[0][1] as EventListener; + const createFindShortcutEvent = () => ({ + key: 'f', + code: 'KeyF', + metaKey: false, + ctrlKey: true, + altKey: false, + shiftKey: false, + isComposing: false, + target: document.body, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + stopImmediatePropagation: vi.fn(), + }) as unknown as KeyboardEvent; + + const firstShortcut = createFindShortcutEvent(); + await act(async () => { + handlePageFindShortcut(firstShortcut); + }); + expect(firstShortcut.preventDefault).toHaveBeenCalledTimes(1); + expect(firstShortcut.stopPropagation).toHaveBeenCalledTimes(1); + expect(firstShortcut.stopImmediatePropagation).toHaveBeenCalledTimes(1); + expect(renderer!.root.findAllByType(DataGridPageFind)).toHaveLength(1); + + const secondShortcut = createFindShortcutEvent(); + await act(async () => { + handlePageFindShortcut(secondShortcut); + }); + expect(renderer!.root.findAllByType(DataGridPageFind)).toHaveLength(1); + + await act(async () => { + renderer!.root.findByType(DataGridPageFind).props.onCancel(); + }); + expect(renderer!.root.findAllByType(DataGridPageFind)).toHaveLength(0); + renderer!.unmount(); + }); + + it('does not claim document-level Cmd+F for a query result without DataGrid focus', async () => { + storeState.appearance.uiVersion = 'v2'; + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create( + , + ); + }); + await waitForEffects(); + + const keydownRegistrations = vi.mocked(window.addEventListener).mock.calls.filter( + ([type, _listener, options]) => type === 'keydown' && options === true, + ); + expect(keydownRegistrations).toHaveLength(1); + const event = { + key: 'f', + code: 'KeyF', + metaKey: true, + ctrlKey: false, + altKey: false, + shiftKey: false, + isComposing: false, + target: document.body, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + stopImmediatePropagation: vi.fn(), + } as unknown as KeyboardEvent; + + await act(async () => { + (keydownRegistrations[0][1] as EventListener)(event); + }); + + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(renderer!.root.findAllByType(DataGridPageFind)).toHaveLength(0); + renderer!.unmount(); + }); + it('does not treat page-find highlighting as a deletable cell selection', async () => { messageApi.info.mockResolvedValue(undefined); let renderer: ReactTestRenderer; diff --git a/frontend/src/components/DataGrid.layout.test.tsx b/frontend/src/components/DataGrid.layout.test.tsx index 0bee2122..0298062a 100644 --- a/frontend/src/components/DataGrid.layout.test.tsx +++ b/frontend/src/components/DataGrid.layout.test.tsx @@ -40,6 +40,8 @@ const readDataGridSecondaryActionsSource = (): string => readFileSync(new URL('./DataGridSecondaryActions.tsx', import.meta.url), 'utf8'); const readDataGridShellSource = (): string => readFileSync(new URL('./DataGridShell.tsx', import.meta.url), 'utf8'); +const readQueryEditorSource = (): string => + readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8'); const mockStoreState = vi.hoisted(() => ({ languagePreference: 'system' as LanguagePreference, @@ -213,9 +215,9 @@ describe('DataGrid layout', () => { expect(markup).toContain('跳列'); expect(markup).toContain('日志'); expect(markup).toContain(zhObjectDesignLabel); - expect(markup).toContain('data-grid-page-find="true"'); - expect(markup).toContain('data-grid-page-find-prev="true"'); - expect(markup).toContain('data-grid-page-find-next="true"'); + expect(markup).not.toContain('data-grid-page-find="true"'); + expect(markup).not.toContain('data-grid-page-find-prev="true"'); + expect(markup).not.toContain('data-grid-page-find-next="true"'); expect(markup).toContain('gn-v2-data-grid-status-main'); expect(markup).toContain('gn-v2-data-grid-status-right'); expect(markup).toContain('data-grid-v2-pagination="true"'); @@ -229,7 +231,7 @@ describe('DataGrid layout', () => { expect(markup).toContain('跳转页码'); expect(markup).not.toContain('class="ant-pagination'); expect(markup).not.toContain('class="data-grid-pagination-kicker"'); - expect(markup).toContain('当前页查找...'); + expect(markup).not.toContain('当前页查找...'); }); it('opens the embedded SQL log view from the shared V2 SQL log event in table data tabs', () => { @@ -1077,11 +1079,39 @@ describe('DataGrid layout', () => { expect(shellSource).toMatch(/ { - const source = readDataGridSource(); + it('keeps V2 current-page find hidden until its floating table overlay is opened', () => { + const source = readDataGridShellSource(); + const secondaryActionsSource = readDataGridSecondaryActionsSource(); + const css = readV2ThemeCss(); - expect(source).toContain("const visiblePageFindContent = viewMode === 'table' ? pageFindContent : null;"); - expect(source).toContain('pageFindContent={visiblePageFindContent}'); + expect(source).toContain("const floatingPageFindContent = isV2Ui && pageFindOpen && viewMode === 'table'"); + expect(source).toContain("const legacyPageFindContent = !isV2Ui && viewMode === 'table'"); + expect(source).toContain('data-grid-page-find-overlay="true"'); + expect(source).toContain('pageFindContent={legacyPageFindContent}'); + const v2BranchStart = secondaryActionsSource.indexOf('if (isV2Ui)'); + const legacyBranchStart = secondaryActionsSource.lastIndexOf(' return ('); + expect(v2BranchStart).toBeGreaterThanOrEqual(0); + expect(legacyBranchStart).toBeGreaterThan(v2BranchStart); + expect(secondaryActionsSource.slice(v2BranchStart, legacyBranchStart)).not.toContain('{pageFindContent}'); + expect(css).toMatch(/\.gn-v2-data-grid-page-find-overlay\s*\{[^}]*position:\s*absolute;[^}]*top:\s*8px;[^}]*right:\s*8px;[^}]*z-index:\s*40;[^}]*background:[^;]+;[^}]*box-shadow:/s); + expect(css).toMatch(/\.gn-v2-data-grid-page-find-overlay\s*\{[^}]*width:\s*min\(360px,\s*calc\(100% - 16px\)\);/s); + expect(css).toMatch(/\.gn-v2-data-grid-page-find\s*\{[^}]*width:\s*100%;[^}]*flex:\s*1 1 auto;[^}]*overflow:\s*hidden;/s); + expect(css).toMatch(/\.gn-v2-data-grid-page-find \.ant-input-affix-wrapper\s*\{[^}]*flex:\s*1 1 160px;[^}]*width:\s*auto !important;[^}]*max-width:\s*none !important;/s); + expect(css).not.toMatch(/\.gn-v2-data-grid-page-find\s*\{[^}]*max-width:\s*214px\s*!important;/s); + expect(css).not.toMatch(/\.gn-v2-data-grid-page-find [^{]*\.gn-v2-data-grid-page-find-input[^{]*\{[^}]*width:\s*160px\s*!important;/s); + expect(css).not.toContain('.gn-v2-data-grid-page-find-row'); + }); + + it('routes Ctrl/Cmd+F to the active DataGrid without stealing SQL editor find', () => { + const dataGridSource = readDataGridSource(); + const queryEditorSource = readQueryEditorSource(); + + expect(dataGridSource).toContain("activeShortcutPlatform === 'mac' ? 'Meta+F' : 'Ctrl+F'"); + expect(dataGridSource).toContain('isShortcutMatch(event, pageFindShortcutCombo)'); + expect(dataGridSource).toContain("window.addEventListener('keydown', handlePageFindShortcut, true)"); + expect(dataGridSource).toContain('event.stopImmediatePropagation()'); + expect(queryEditorSource).toContain("targetElement?.closest('.data-grid-root')"); + expect(queryEditorSource).toContain('dataGridHasFocus'); }); it('keeps legacy secondary actions aligned on a shared search-row baseline', () => { @@ -1202,6 +1232,8 @@ describe('DataGrid layout', () => { expect(dataGridSource).toMatch(/ { expect(detachedChromeSource).toContain("translate('data_grid.secondary.er_diagram')"); expect(detachedChromeSource).toContain("translate('data_grid.secondary.column_display')"); expect(detachedChromeSource).toContain("translate('data_grid.secondary.jump_column')"); - expect(detachedChromeSource).toContain("translate('data_grid.secondary.row_count'"); - expect(detachedChromeSource).toContain("translate('data_grid.secondary.pending_changes'"); - expect(detachedChromeSource).toContain("translate('data_grid.secondary.live')"); + expect(secondaryActionsSource).not.toContain("translate('data_grid.secondary.row_count'"); + expect(secondaryActionsSource).not.toContain("translate('data_grid.secondary.pending_changes'"); + expect(secondaryActionsSource).not.toContain("translate('data_grid.secondary.live')"); expect(detachedChromeSource).toContain("translate('data_grid.record_view.empty')"); expect(detachedChromeSource).toContain("translate('data_grid.record_view.json_record_count'"); expect(detachedChromeSource).toContain("translate('data_grid.record_view.edit_json')"); @@ -1769,6 +1801,8 @@ describe('DataGrid layout', () => { const values: Record = { 'data_grid.page_find.tooltip': 'Find only this page', 'data_grid.page_find.placeholder': 'Find current page', + 'data_grid.page_find.previous': 'Previous find match', + 'data_grid.page_find.next': 'Next find match', 'data_grid.page_find.summary': `${params?.occurrences} hits / ${params?.cells} cells`, 'data_grid.pagination.result_set': 'Result set label', 'data_grid.pagination.page_size_aria': 'Rows per page label', @@ -1787,9 +1821,6 @@ describe('DataGrid layout', () => { 'data_grid.secondary.er_diagram': 'ER diagram label', 'data_grid.secondary.column_display': 'Column display label', 'data_grid.secondary.jump_column': 'Jump column label', - 'data_grid.secondary.row_count': `${params?.count} rows label`, - 'data_grid.secondary.pending_changes': `${params?.count} pending label`, - 'data_grid.secondary.live': 'Live label', 'data_grid.record_view.empty': 'No rows label', 'data_grid.record_view.json_record_count': `${params?.count} JSON rows label`, 'data_grid.record_view.edit_json': 'Edit JSON label', @@ -1802,6 +1833,7 @@ describe('DataGrid layout', () => { 'data_grid.preview_panel.no_cell_title': 'Select cell title', 'data_grid.preview_panel.no_cell_description': 'Select cell description', 'data_grid.json_editor.format': 'Format JSON label', + 'common.close': 'Close find', 'common.save': 'Save label', }; return values[key] ?? key; @@ -1809,7 +1841,7 @@ describe('DataGrid layout', () => { const pageFindMarkup = renderToStaticMarkup( { expect(pageFindMarkup).toContain('placeholder="Find current page"'); expect(pageFindMarkup).toContain('1 / 3'); expect(pageFindMarkup).toContain('4 hits / 2 cells'); + expect(pageFindMarkup).toContain('aria-label="Previous find match"'); + expect(pageFindMarkup).toContain('aria-label="Next find match"'); + expect(pageFindMarkup).toContain('aria-label="Close find"'); expect(pageFindMarkup).not.toContain('data_grid.page_find'); const resultViewMarkup = renderToStaticMarkup( @@ -1884,8 +1919,6 @@ describe('DataGrid layout', () => { ddlLoading={false} showColumnComment={false} showColumnType={false} - mergedDisplayCount={3} - pendingChangeCount={2} resultViewSwitcher={view switcher} columnInfoSettingContent={column settings} columnQuickFindContent={quick find} @@ -1905,9 +1938,8 @@ describe('DataGrid layout', () => { expect(secondaryMarkup).toContain('ER diagram label'); expect(secondaryMarkup).toContain('Column display label'); expect(secondaryMarkup).toContain('Jump column label'); - expect(secondaryMarkup).toContain('3 rows label'); - expect(secondaryMarkup).toContain('2 pending label'); - expect(secondaryMarkup).toContain('Live label'); + expect(secondaryMarkup).not.toContain('page find'); + expect(secondaryMarkup).not.toContain('gn-v2-data-grid-status-center'); expect(secondaryMarkup).not.toContain('data_grid.secondary'); const jsonRecordMarkup = renderToStaticMarkup( @@ -2165,9 +2197,41 @@ describe('DataGrid layout', () => { ].forEach((label) => { expect(getButtonBody(label)).not.toContain(label); }); + [ + '数据预览', + zhObjectDesignLabel, + '查看 DDL', + 'ER 图', + '日志', + '字段显示', + ].forEach((label) => { + expect(getButtonBody(label)).not.toContain(label); + }); expect(markup).toContain('aria-haspopup="menu"'); expect(markup).toContain('aria-expanded="false"'); + const resultViewMarkup = renderToStaticMarkup( + zhCnCatalog[key] ?? key} + onViewModeChange={() => {}} + />, + ); + expect(resultViewMarkup).toContain('aria-label="结果视图"'); + expect(resultViewMarkup).not.toContain('>结果视图<'); + expect(resultViewMarkup).toContain('class="gn-v2-data-grid-visually-hidden">表格'); + expect(resultViewMarkup).toContain('class="gn-v2-data-grid-visually-hidden">JSON'); + expect(resultViewMarkup).toContain('class="gn-v2-data-grid-visually-hidden">文本'); + expect(readDataGridSecondaryActionsSource()).toContain(''); + expect(readDataGridSecondaryActionsSource()).toContain( + '', + ); + expect( + readFileSync(new URL('./DataGridResultViewSwitcher.tsx', import.meta.url), 'utf8'), + ).toContain(''); + const css = readV2ThemeCss(); const iconActionCss = css.slice( css.indexOf('body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-data-grid-toolbar-action {'), @@ -2176,6 +2240,9 @@ describe('DataGrid layout', () => { expect(iconActionCss).toContain('width: 28px !important;'); expect(iconActionCss).toContain('min-width: 28px !important;'); expect(iconActionCss).toContain('padding-inline: 0 !important;'); + expect(css).toContain( + 'body[data-ui-version="v2"] .gn-v2-data-grid-statusbar .gn-v2-data-grid-toolbar-action.ant-btn {', + ); }); it('renders a non-data row number column when enabled', () => { @@ -2410,7 +2477,7 @@ describe('DataGrid layout', () => { expect(schemaTableMarkup).toContain('data-grid-ddl-action="true"'); expect(schemaTableMarkup).toContain('查看 DDL'); expect(schemaTableMarkup).toContain(zhObjectDesignLabel); - expect(schemaTableMarkup).toContain('data-grid-page-find="true"'); + expect(schemaTableMarkup).not.toContain('data-grid-page-find="true"'); const queryMarkup = renderDataGridWithI18n( { expect(source).toContain('const handleSubmitColumnQuickFind = useCallback((submittedValue?: string) => {'); expect(source).toContain('const effectiveQuery = String(submittedValue ?? columnQuickFindText);'); expect(source).toContain('resolveDataGridColumnQuickFindTarget(displayColumnNames, query)'); - expect(source).toContain("onCancel={() => setPageFindText('')}"); + expect(source).toContain('onCancel={handleClosePageFind}'); expect(source).toContain('enumerable: true'); expect(source).toContain('resolveDataGridColumnQuickFindScrollLeft({'); const pageFindFocusSource = source.slice( @@ -2835,17 +2902,16 @@ describe('DataGrid layout', () => { expect(columnTitleSource).toContain('data-column-name={normalizedName}'); expect(columnQuickFindSource).toContain('AutoComplete'); expect(columnQuickFindSource).toContain("placeholder={translate('data_grid.column_quick_find.placeholder')}"); - expect(secondaryActionsSource.indexOf('{pageFindContent}')).toBeLessThan(secondaryActionsSource.indexOf('gn-v2-data-grid-status-center')); + expect(secondaryActionsSource).not.toContain('gn-v2-data-grid-status-center'); + expect(secondaryActionsSource).not.toContain('gn-v2-data-grid-live'); expect(css).toContain('width: 66px !important;'); - expect(css).toContain('grid-template-columns: 160px 26px 26px !important;'); expect(css).toContain('container-name: gn-v2-data-grid-statusbar;'); expect(css).toContain('body[data-ui-version="v2"] .gn-v2-data-grid-statusbar::-webkit-scrollbar'); expect(css).toContain('scrollbar-width: thin;'); expect(css).toContain('min-width: max-content;'); expect(css).toContain('flex: 0 0 auto;'); - expect(css).toContain('body[data-ui-version="v2"] .gn-v2-data-grid-status-center {'); - expect(css).not.toContain('.gn-v2-data-grid-status-center > span:last-child {\n display: none;'); - expect(css).not.toContain('.gn-v2-data-grid-status-center > span:nth-child(2) {\n display: none;'); + expect(css).not.toContain('gn-v2-data-grid-status-center'); + expect(css).not.toContain('gn-v2-data-grid-live'); expect(css).toContain('body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap::-webkit-scrollbar'); expect(css).toContain('@container gn-v2-data-grid-statusbar (max-width: 960px)'); expect(css).toContain('@container gn-v2-data-grid-statusbar (max-width: 760px)'); diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 9cb0e0e5..089c8742 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -3,6 +3,7 @@ import Modal from './common/ResizableDraggableModal'; import React, { useState, useEffect, useRef, useContext, useMemo, useCallback, useDeferredValue } from 'react'; import { createPortal } from 'react-dom'; import { Table, message, Input, Button, Dropdown, MenuProps, Form, Pagination, Select, Checkbox, Segmented, Tooltip, Popover, DatePicker, TimePicker } from 'antd'; +import type { InputRef } from 'antd'; import dayjs from 'dayjs'; import type { SortOrder, ColumnType } from 'antd/es/table/interface'; import type { Reference as TableReference } from 'rc-table'; @@ -82,7 +83,13 @@ import { pickRowsForClipboard, } from './dataGridClipboardExport'; import { applyNoAutoCapAttributesWithin, noAutoCapInputProps } from '../utils/inputAutoCap'; -import { DEFAULT_SHORTCUT_OPTIONS, getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts'; +import { + DEFAULT_SHORTCUT_OPTIONS, + getShortcutPlatform, + isEditableElement, + isShortcutMatch, + resolveShortcutDisplay, +} from '../utils/shortcuts'; import { TEMPORAL_FORMATS, formatFromDayjs, @@ -427,6 +434,7 @@ const DataGrid: React.FC = ({ const [columnSearchText, setColumnSearchText] = useState(''); const [columnQuickFindText, setColumnQuickFindText] = useState(''); const [highlightedColumnName, setHighlightedColumnName] = useState(''); + const [pageFindOpen, setPageFindOpen] = useState(false); const [pageFindText, setPageFindText] = useState(''); const [activePageFindMatchIndex, setActivePageFindMatchIndex] = useState(-1); const columnQuickFindHighlightTimerRef = useRef | null>(null); @@ -446,6 +454,7 @@ const DataGrid: React.FC = ({ useEffect(() => { setColumnQuickFindText(''); setHighlightedColumnName(''); + setPageFindOpen(false); setPageFindText(''); setActivePageFindMatchIndex(-1); }, [connectionId, dbName, tableName]); @@ -976,6 +985,7 @@ const DataGrid: React.FC = ({ }); const cellContextMenuPortalRef = useRef(null); const rootRef = useRef(null); + const pageFindInputRef = useRef(null); const containerRef = useRef(null); const tableContainerRef = useRef(null); const tableRef = useRef(null); @@ -1753,6 +1763,97 @@ const DataGrid: React.FC = ({ initialViewModeScope, }); + const pageFindShortcutCombo = activeShortcutPlatform === 'mac' ? 'Meta+F' : 'Ctrl+F'; + + const focusPageFindInput = useCallback(() => { + requestAnimationFrame(() => { + const inputHandle = pageFindInputRef.current as (InputRef & HTMLInputElement) | null; + const input = inputHandle?.input ?? inputHandle; + input?.focus?.({ preventScroll: true }); + input?.select?.(); + }); + }, []); + + const handleOpenPageFind = useCallback(() => { + setPageFindOpen(true); + focusPageFindInput(); + }, [focusPageFindInput]); + + const handleClosePageFind = useCallback(() => { + setPageFindOpen(false); + setPageFindText(''); + setActivePageFindMatchIndex(-1); + requestAnimationFrame(() => { + rootRef.current?.focus({ preventScroll: true }); + }); + }, []); + + const handleDataGridRootPointerDownCapture = useCallback((event: React.PointerEvent) => { + if (isEditableElement(event.target)) return; + event.currentTarget.focus({ preventScroll: true }); + }, []); + + useEffect(() => { + if (!isV2Ui || (isActive && viewMode === 'table')) return; + setPageFindOpen(false); + setPageFindText(''); + setActivePageFindMatchIndex(-1); + }, [isActive, isV2Ui, viewMode]); + + useEffect(() => { + if (!isV2Ui || !isActive || viewMode !== 'table') return; + + const handlePageFindShortcut = (event: KeyboardEvent) => { + if (!isShortcutMatch(event, pageFindShortcutCombo)) return; + + const root = rootRef.current; + const eventTarget = event.target; + const targetNode = typeof Node !== 'undefined' && eventTarget instanceof Node + ? eventTarget + : null; + const targetElement = eventTarget + && typeof (eventTarget as Element).closest === 'function' + ? eventTarget as Element + : null; + const activeElement = document.activeElement; + const eventTargetInGrid = !!(root && targetNode && root.contains(targetNode)); + const activeElementInGrid = !!(root && activeElement && root.contains(activeElement)); + const isDocumentLevelTarget = eventTarget === window + || eventTarget === document + || eventTarget === document.body + || eventTarget === document.documentElement; + const hasGridShortcutContext = eventTargetInGrid + || activeElementInGrid + || (exportScope === 'table' && isDocumentLevelTarget); + if (!hasGridShortcutContext) return; + + const isPageFindInput = !!targetElement?.closest('[data-grid-page-find="true"]'); + if ( + !isPageFindInput + && (isEditableElement(event.target) || isEditableElement(activeElement)) + ) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + handleOpenPageFind(); + }; + + window.addEventListener('keydown', handlePageFindShortcut, true); + return () => { + window.removeEventListener('keydown', handlePageFindShortcut, true); + }; + }, [ + exportScope, + handleOpenPageFind, + isActive, + isV2Ui, + pageFindShortcutCombo, + viewMode, + ]); + useEffect(() => { const handleExternalViewModeChange = (event: Event) => { const detail = (event as CustomEvent)?.detail || {}; @@ -5281,6 +5382,7 @@ const DataGrid: React.FC = ({ handleBatchFillToSelected, handleCellEditorSave, handleCellSetNull, + handleClosePageFind, handleCommit, handleCopyContextMenuFieldName, handleCopyCsv, @@ -5295,6 +5397,7 @@ const DataGrid: React.FC = ({ handleCopyUpdate, handleDataPanelFormatJson, handleDataPanelSave, + handleDataGridRootPointerDownCapture, closeDdlView, handleDdlSidebarResizeStart, handleDeleteSelected, @@ -5360,6 +5463,8 @@ const DataGrid: React.FC = ({ openCurrentViewRowEditor, openRowEditorFieldEditor, pageFindMatches, + pageFindInputRef, + pageFindOpen, pageFindSummary, pageFindText, pagination, diff --git a/frontend/src/components/DataGridPageFind.tsx b/frontend/src/components/DataGridPageFind.tsx index 54ce5852..f069d6d4 100644 --- a/frontend/src/components/DataGridPageFind.tsx +++ b/frontend/src/components/DataGridPageFind.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Button, Input, Tooltip } from 'antd'; -import { LeftOutlined, RightOutlined, SearchOutlined } from '@ant-design/icons'; +import type { InputRef } from 'antd'; +import { CloseOutlined, LeftOutlined, RightOutlined, SearchOutlined } from '@ant-design/icons'; import { t as defaultTranslate, type I18nParams } from '../i18n'; export type DataGridPageFindTranslate = (key: string, params?: I18nParams) => string; @@ -8,6 +9,7 @@ export type DataGridPageFindTranslate = (key: string, params?: I18nParams) => st export interface DataGridPageFindProps { isV2Ui: boolean; darkMode: boolean; + inputRef?: React.Ref; inputProps?: Record; pageFindText: string; normalizedPageFindText: string; @@ -26,6 +28,7 @@ export interface DataGridPageFindProps { const DataGridPageFind: React.FC = ({ isV2Ui, darkMode, + inputRef, inputProps, pageFindText, normalizedPageFindText, @@ -53,6 +56,7 @@ const DataGridPageFind: React.FC = ({ style={isV2Ui ? undefined : { display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, flexWrap: 'nowrap', height: 32 }} > = ({ event.preventDefault(); event.stopPropagation(); onCancel(); + return; + } + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + if (event.shiftKey) { + onNavigatePrevious(); + } else { + onNavigateNext(); + } } }} style={isV2Ui ? undefined : { width: 168, height: 32 }} @@ -74,6 +88,8 @@ const DataGridPageFind: React.FC = ({ - ))} + {viewTabItems.map((item) => { + const isActive = viewMode === item.key + || (item.key === 'table' && (viewMode === 'json' || viewMode === 'text')); + + return ( + + + + + - {pageFindContent} -
- {translate('data_grid.secondary.live')} - {translate('data_grid.secondary.row_count', { count: mergedDisplayCount })} - {translate('data_grid.secondary.pending_changes', { count: pendingChangeCount })} -
{paginationContent} diff --git a/frontend/src/components/DataGridShell.tsx b/frontend/src/components/DataGridShell.tsx index 19148410..97d98044 100644 --- a/frontend/src/components/DataGridShell.tsx +++ b/frontend/src/components/DataGridShell.tsx @@ -156,6 +156,7 @@ const DataGridShell: React.FC = (props) => { handleBatchFillToSelected, handleCellEditorSave, handleCellSetNull, + handleClosePageFind, handleCommit, handleCopyContextMenuFieldName, handleCopyCsv, @@ -170,6 +171,7 @@ const DataGridShell: React.FC = (props) => { handleCopyUpdate, handleDataPanelFormatJson, handleDataPanelSave, + handleDataGridRootPointerDownCapture, handleDdlSidebarResizeStart, handleDeleteSelected, handleDragEnd, @@ -233,7 +235,9 @@ const DataGridShell: React.FC = (props) => { openBatchEditModal, openCurrentViewRowEditor, openRowEditorFieldEditor, + pageFindInputRef, pageFindMatches, + pageFindOpen, pageFindSummary, pageFindText, pagination, @@ -420,6 +424,7 @@ const renderDataTableView = () => ( } pageFindText={pageFindText} normalizedPageFindText={normalizedPageFindText} @@ -429,13 +434,18 @@ const renderDataTableView = () => ( occurrenceCount={pageFindSummary.occurrenceCount} matchedCellCount={pageFindSummary.matchedCellCount} onPageFindTextChange={setPageFindText} - onCancel={() => setPageFindText('')} + onCancel={handleClosePageFind} onNavigatePrevious={() => handleNavigatePageFind('previous')} onNavigateNext={() => handleNavigatePageFind('next')} translate={translateDataGrid} /> ); - const visiblePageFindContent = viewMode === 'table' ? pageFindContent : null; + const floatingPageFindContent = isV2Ui && pageFindOpen && viewMode === 'table' + ? pageFindContent + : null; + const legacyPageFindContent = !isV2Ui && viewMode === 'table' + ? pageFindContent + : null; const columnQuickFindContent = isTableSurfaceActive ? ( ( }, [mergedDisplayData, translateDataGrid]); return ( -
+
( onCloseDdlModal={() => setDdlModalOpen(false)} onCopyDdl={handleCopyDdl} /> + {floatingPageFindContent ? ( +
+ {floatingPageFindContent} +
+ ) : null} {viewMode === 'table' ? ( renderDataTableView() @@ -1011,12 +1035,10 @@ const renderDataTableView = () => ( ddlLoading={ddlLoading} showColumnComment={showColumnComment} showColumnType={showColumnType} - mergedDisplayCount={mergedDisplayData.length} - pendingChangeCount={pendingChangeCount} resultViewSwitcher={resultViewSwitcher} columnInfoSettingContent={columnInfoSettingContent} columnQuickFindContent={columnQuickFindContent} - pageFindContent={visiblePageFindContent} + pageFindContent={legacyPageFindContent} paginationContent={paginationContent} onViewModeChange={handleViewModeChange} dataPanelOpen={dataPanelOpen} diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 0d952174..01c10a7c 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -8463,6 +8463,19 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const editor = editorRef.current; const targetNode = resolveEventTargetNode(event.target); + const targetElement = targetNode + && typeof (targetNode as Element).closest === 'function' + ? targetNode as Element + : null; + const activeElement = document.activeElement; + const dataGridHasFocus = !!( + activeElement + && typeof activeElement.closest === 'function' + && activeElement.closest('.data-grid-root') + ); + if (targetElement?.closest('.data-grid-root') || dataGridHasFocus) { + return; + } const editorHasFocus = !!editor?.hasTextFocus?.(); const inEditorPane = !!(targetNode && editorPaneRef.current?.contains(targetNode)); const inQueryEditor = !!(targetNode && queryEditorRootRef.current?.contains(targetNode)); diff --git a/frontend/src/i18n/catalog.test.ts b/frontend/src/i18n/catalog.test.ts index 2c82787f..c044fdf7 100644 --- a/frontend/src/i18n/catalog.test.ts +++ b/frontend/src/i18n/catalog.test.ts @@ -509,6 +509,8 @@ describe("i18n catalog", () => { const dataGridDetachedChromeKeys = [ "data_grid.page_find.tooltip", "data_grid.page_find.placeholder", + "data_grid.page_find.previous", + "data_grid.page_find.next", "data_grid.page_find.summary", "data_grid.pagination.result_set", "data_grid.pagination.page_size_aria", @@ -536,9 +538,6 @@ describe("i18n catalog", () => { "data_grid.secondary.er_diagram", "data_grid.secondary.column_display", "data_grid.secondary.jump_column", - "data_grid.secondary.row_count", - "data_grid.secondary.pending_changes", - "data_grid.secondary.live", "data_grid.record_view.empty", "data_grid.record_view.json_record_count", "data_grid.record_view.edit_json", @@ -612,8 +611,6 @@ describe("i18n catalog", () => { expect(t("en-US", "data_grid.pagination.page.current", { current: "" })).toContain(""); expect(t("en-US", "data_grid.pagination.page.known", { current: "", totalPages: "" })).toContain(""); expect(t("en-US", "data_grid.pagination.page.known", { current: "", totalPages: "" })).toContain(""); - expect(t("zh-CN", "data_grid.secondary.row_count", { count: "" })).toContain(""); - expect(t("zh-CN", "data_grid.secondary.pending_changes", { count: "" })).toContain(""); expect(t("zh-CN", "data_grid.secondary.view_ddl")).toContain("DDL"); expect(t("ja-JP", "data_grid.secondary.er_diagram")).toContain("ER"); expect(t("zh-CN", "data_grid.record_view.json_record_count", { count: "" })).toContain(""); diff --git a/frontend/src/styles/v2-theme-workbench.css b/frontend/src/styles/v2-theme-workbench.css index 5f256b99..f27fe310 100644 --- a/frontend/src/styles/v2-theme-workbench.css +++ b/frontend/src/styles/v2-theme-workbench.css @@ -1257,48 +1257,3 @@ body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input::placeholder { text-overflow: ellipsis; white-space: nowrap; } - -body[data-ui-version="v2"] .gn-v2-data-grid-page-find { - max-width: 214px !important; - overflow: visible !important; -} - -body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-row { - display: grid !important; - grid-template-columns: 160px 26px 26px !important; - column-gap: 2px !important; - align-items: stretch !important; - height: 26px !important; - min-height: 26px !important; - flex: 0 0 auto !important; -} - -body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-input, -body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-input.ant-input-affix-wrapper { - width: 160px !important; - min-width: 160px !important; - max-width: 160px !important; - height: 26px !important; - min-height: 26px !important; - box-sizing: border-box !important; -} - -body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-input.ant-input-affix-wrapper { - display: flex !important; - align-items: center !important; - padding-inline: 8px !important; - line-height: 26px !important; -} - -body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-prev.ant-btn, -body[data-ui-version="v2"] .gn-v2-data-grid-page-find .gn-v2-data-grid-page-find-next.ant-btn { - width: 26px !important; - min-width: 26px !important; - max-width: 26px !important; - height: 26px !important; - min-height: 26px !important; - margin: 0 !important; - padding: 0 !important; - align-self: stretch !important; - justify-self: stretch !important; -} diff --git a/frontend/src/v2-theme.css b/frontend/src/v2-theme.css index e6ce7934..7cf6cb04 100644 --- a/frontend/src/v2-theme.css +++ b/frontend/src/v2-theme.css @@ -4418,10 +4418,6 @@ body[data-ui-version="v2"] .gn-v2-data-grid-status-right { flex: 0 0 auto; overflow: visible; } - - body[data-ui-version="v2"] .gn-v2-data-grid-status-center { - flex: 0 0 auto; - } } body[data-ui-version="v2"] .gn-v2-data-grid-status-summary { @@ -4451,10 +4447,8 @@ body[data-ui-version="v2"] .gn-v2-data-grid-status-summary span:not(:first-child body[data-ui-version="v2"] .gn-v2-data-grid-view-tabs, body[data-ui-version="v2"] .gn-v2-data-grid-result-switcher, -body[data-ui-version="v2"] .gn-v2-data-grid-status-center, body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap, body[data-ui-version="v2"] .gn-v2-data-grid .data-grid-pagination-shell, -body[data-ui-version="v2"] .gn-v2-data-grid-page-find, body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find { display: inline-flex; align-items: center; @@ -4489,6 +4483,10 @@ body[data-ui-version="v2"] .gn-v2-data-grid-view-tabs .ant-btn-primary { box-shadow: none !important; } +body[data-ui-version="v2"] .gn-v2-data-grid-statusbar .gn-v2-data-grid-toolbar-action.ant-btn { + padding-inline: 0 !important; +} + body[data-ui-version="v2"] .gn-v2-data-grid-result-switcher { height: var(--gn-v2-statusbar-control-height); min-height: var(--gn-v2-statusbar-control-height); @@ -4548,68 +4546,58 @@ body[data-ui-version="v2"] .gn-v2-data-grid-result-switcher .ant-segmented-item- line-height: 20px; } +body[data-ui-version="v2"] .gn-v2-data-grid-result-option { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 20px; + line-height: 1; +} + +body[data-ui-version="v2"] .gn-v2-data-grid-visually-hidden { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + body[data-ui-version="v2"] .gn-v2-data-grid-result-switcher .ant-segmented-item-selected .ant-segmented-item-label { color: var(--gn-fg-1); } -body[data-ui-version="v2"] .gn-v2-data-grid-status-center { - height: var(--gn-v2-statusbar-control-height); - min-height: var(--gn-v2-statusbar-control-height); - gap: 4px; - flex: 0 0 auto; - min-width: max-content; - overflow: visible; - color: var(--gn-fg-4); - font-family: var(--gn-font-mono); - font-size: 10px; - line-height: 26px; - opacity: 0.88; -} - -body[data-ui-version="v2"] .gn-v2-data-grid-status-center > span { - display: inline-flex; - align-items: center; - height: var(--gn-v2-statusbar-control-height); - flex: 0 0 auto; - line-height: 26px; - white-space: nowrap; -} - -body[data-ui-version="v2"] .gn-v2-data-grid-status-center > span:not(:last-child)::after { - content: ''; - display: inline-block; - width: 1px; - height: 10px; - margin-left: 6px; - vertical-align: -1px; - background: var(--gn-br-1); -} - -body[data-ui-version="v2"] .gn-v2-data-grid-live { - color: var(--gn-accent-2) !important; - font-weight: 700; -} - -body[data-ui-version="v2"] .gn-v2-data-grid-live::before { - content: ''; - display: inline-block; - width: 6px; - height: 6px; - margin-right: 4px; - border-radius: 50%; - background: var(--gn-accent); - box-shadow: 0 0 0 3px var(--gn-accent-soft); - vertical-align: 1px; +body[data-ui-version="v2"] .gn-v2-data-grid-page-find-overlay { + position: absolute; + top: 8px; + right: 8px; + z-index: 40; + display: flex; + width: min(360px, calc(100% - 16px)); + max-width: calc(100% - 16px); + padding: 4px; + border: 0.5px solid var(--gn-br-2); + border-radius: 8px; + background: color-mix(in srgb, var(--gn-bg-panel) 96%, transparent); + box-shadow: var(--gn-shadow-md); + box-sizing: border-box; } body[data-ui-version="v2"] .gn-v2-data-grid-page-find { - height: var(--gn-v2-statusbar-control-height); - min-height: var(--gn-v2-statusbar-control-height); + display: inline-flex; + align-items: center; + width: 100%; + min-width: 0; + max-width: 100%; + min-height: 28px; gap: 2px; - flex: 0 0 auto; - max-width: 214px; - margin-left: 0; - overflow: visible; + flex: 1 1 auto; + overflow: hidden; + box-sizing: border-box; } body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find { @@ -4707,12 +4695,13 @@ body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find-submit { } body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-affix-wrapper { - width: 160px !important; - min-width: 160px; - max-width: 160px !important; - height: 26px !important; - min-height: 26px !important; - line-height: 26px !important; + flex: 1 1 160px; + width: auto !important; + min-width: 96px; + max-width: none !important; + height: 28px !important; + min-height: 28px !important; + line-height: 28px !important; border-radius: 6px !important; border: 0.5px solid var(--gn-br-2) !important; background: var(--gn-bg-input) !important; @@ -4774,9 +4763,11 @@ body[data-ui-version="v2"] .gn-v2-data-grid-column-quick-find .ant-select-focuse } body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-btn { - width: 26px !important; - height: 26px !important; - min-height: 26px !important; + flex: 0 0 28px; + width: 28px !important; + min-width: 28px !important; + height: 28px !important; + min-height: 28px !important; padding: 0 !important; display: inline-flex !important; align-items: center !important; @@ -4791,17 +4782,17 @@ body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-btn { } body[data-ui-version="v2"] .gn-v2-data-grid-page-find > span { + flex: 0 1 auto; + min-width: 0; + max-width: 132px; overflow: hidden; color: var(--gn-fg-5); + font-family: var(--gn-font-mono); + font-size: 11px; + white-space: nowrap; text-overflow: ellipsis; } -body[data-ui-version="v2"] .gn-v2-data-grid-statusbar [data-grid-page-find="true"] { - padding: 0; - border: none; - border-radius: 7px; -} - body[data-ui-version="v2"] .gn-v2-data-grid-statusbar [data-grid-column-quick-find="true"] { padding: 0; border: none; @@ -5086,16 +5077,6 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-paginatio } @container gn-v2-data-grid-statusbar (max-width: 960px) { - body[data-ui-version="v2"] .gn-v2-data-grid-page-find { - max-width: 154px; - } - - body[data-ui-version="v2"] .gn-v2-data-grid-page-find .ant-input-affix-wrapper { - width: 112px !important; - min-width: 112px; - max-width: 112px !important; - } - body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-pagination-jump-label { display: none; }