mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-18 21:04:16 +08:00
🎨 style(data-grid/query-editor): 统一工具栏图标交互
- 将 V2 数据查看、查询结果和 SQL 编辑器操作改为纯图标按钮 - 统一工具栏操作尺寸,并保留本地化 Tooltip 与无障碍标签 - 使用状态图标精简 SQL 提交模式和提交时机选择器 - 修复事务下拉选项截断及悬浮提示与菜单重叠问题 - 补充图标布局、下拉交互和旧版兼容测试
This commit is contained in:
@@ -218,6 +218,7 @@ vi.mock('@ant-design/icons', () => {
|
||||
CloseOutlined: Icon,
|
||||
BugOutlined: Icon,
|
||||
ConsoleSqlOutlined: Icon,
|
||||
ControlOutlined: Icon,
|
||||
FileTextOutlined: Icon,
|
||||
CopyOutlined: Icon,
|
||||
ClearOutlined: Icon,
|
||||
@@ -508,7 +509,13 @@ const textContent = (node: any): string =>
|
||||
.join('');
|
||||
|
||||
const findButton = (renderer: ReactTestRenderer, text: string) =>
|
||||
renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0];
|
||||
renderer.root.findAll((node) => (
|
||||
node.type === 'button'
|
||||
&& (
|
||||
textContent(node).includes(text)
|
||||
|| String(node.props['aria-label'] || '').includes(text)
|
||||
)
|
||||
))[0];
|
||||
|
||||
const renderHeaderText = (columnKey: string): string => {
|
||||
const column = testRenderState.latestColumns.find((item) => item.key === columnKey);
|
||||
|
||||
@@ -2135,6 +2135,7 @@ describe('DataGrid layout', () => {
|
||||
expect(markup).toContain('gn-v2-toolbar-divider');
|
||||
expect(markup).toContain('gn-v2-commit-button');
|
||||
expect(markup).toContain('gn-v2-ai-insight-button');
|
||||
expect(markup).toContain('gn-v2-data-grid-toolbar-action');
|
||||
expect(markup).toContain('gn-v2-smart-filter-panel');
|
||||
expect(markup).toContain('gn-v2-data-grid-table-shell');
|
||||
expect(markup).toContain('gn-v2-data-grid-table-wrap');
|
||||
@@ -2142,6 +2143,39 @@ describe('DataGrid layout', () => {
|
||||
expect(markup).toContain('提交事务');
|
||||
expect(markup).toContain('手动提交');
|
||||
expect(markup).toContain('AI 洞察');
|
||||
|
||||
const getButtonBody = (label: string) => {
|
||||
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const match = markup.match(new RegExp(`<button(?=[^>]*aria-label="${escapedLabel}")[^>]*>([\\s\\S]*?)<\\/button>`));
|
||||
expect(match, `missing toolbar button: ${label}`).not.toBeNull();
|
||||
return match?.[1] ?? '';
|
||||
};
|
||||
|
||||
[
|
||||
'刷新',
|
||||
'筛选',
|
||||
'新增行',
|
||||
'删除选中',
|
||||
'单元格编辑',
|
||||
'提交事务',
|
||||
'手动提交',
|
||||
'导入',
|
||||
'导出',
|
||||
'AI 洞察',
|
||||
].forEach((label) => {
|
||||
expect(getButtonBody(label)).not.toContain(label);
|
||||
});
|
||||
expect(markup).toContain('aria-haspopup="menu"');
|
||||
expect(markup).toContain('aria-expanded="false"');
|
||||
|
||||
const css = readV2ThemeCss();
|
||||
const iconActionCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-data-grid-toolbar-action {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-data-grid-toolbar-title {'),
|
||||
);
|
||||
expect(iconActionCss).toContain('width: 28px !important;');
|
||||
expect(iconActionCss).toContain('min-width: 28px !important;');
|
||||
expect(iconActionCss).toContain('padding-inline: 0 !important;');
|
||||
});
|
||||
|
||||
it('renders a non-data row number column when enabled', () => {
|
||||
@@ -2470,6 +2504,8 @@ describe('DataGrid layout', () => {
|
||||
expect(markup).toContain('data-grid-query-copy-action="true"');
|
||||
expect(markup).not.toMatch(/data-grid-query-copy-action="true"[^>]*disabled/);
|
||||
expect(markup).toContain('复制');
|
||||
expect(markup).toContain('aria-haspopup="menu"');
|
||||
expect(markup).toContain('aria-expanded="false"');
|
||||
expect(markup.match(/data-grid-query-copy-action="true"/g)?.length).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { AutoComplete, Button, Checkbox, Dropdown, Input, Select, Tooltip } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import type { ButtonProps, MenuProps } from 'antd';
|
||||
import {
|
||||
ClearOutlined,
|
||||
ControlOutlined,
|
||||
CloseOutlined,
|
||||
ConsoleSqlOutlined,
|
||||
CopyOutlined,
|
||||
@@ -37,6 +38,8 @@ type GridSortInfo = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
type ToolbarMenuKey = 'commit-mode' | 'query-copy';
|
||||
|
||||
export interface DataGridToolbarFrameProps {
|
||||
isV2Ui: boolean;
|
||||
tableName?: string;
|
||||
@@ -244,6 +247,10 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
|
||||
(key: string, params?: Record<string, string | number>) => translateProp?.(key, params) ?? key,
|
||||
[translateProp],
|
||||
);
|
||||
const [openToolbarMenu, setOpenToolbarMenu] = React.useState<ToolbarMenuKey | null>(null);
|
||||
const updateToolbarMenuOpen = (menuKey: ToolbarMenuKey, open: boolean) => {
|
||||
setOpenToolbarMenu((current) => (open ? menuKey : current === menuKey ? null : current));
|
||||
};
|
||||
const renderToolbarDivider = () => (
|
||||
<div
|
||||
className={isV2Ui ? 'gn-v2-toolbar-divider' : undefined}
|
||||
@@ -256,6 +263,34 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
|
||||
? translate('data_grid.filter.mongodb_query_placeholder')
|
||||
: translate('data_grid.filter.quick_where_placeholder');
|
||||
const toolbarTitle = tableName || translate('data_grid.table_fallback.query_result');
|
||||
const aiInsightTooltip = aiShortcutLabel !== '-'
|
||||
? `${translate('data_grid.toolbar.ai_insight_tooltip')} · ${aiShortcutLabel}`
|
||||
: translate('data_grid.toolbar.ai_insight_tooltip');
|
||||
const commitModeLabel = translate(`data_grid.toolbar.commit_mode.${dataEditCommitMode}`);
|
||||
const renderToolbarAction = ({
|
||||
label,
|
||||
tooltip = label,
|
||||
legacyContent = label,
|
||||
className,
|
||||
...buttonProps
|
||||
}: Omit<ButtonProps, 'aria-label' | 'children'> & {
|
||||
label: string;
|
||||
tooltip?: React.ReactNode;
|
||||
legacyContent?: React.ReactNode;
|
||||
}) => {
|
||||
const resolvedClassName = [
|
||||
isV2Ui ? 'gn-v2-data-grid-toolbar-action' : undefined,
|
||||
className,
|
||||
].filter(Boolean).join(' ') || undefined;
|
||||
|
||||
return (
|
||||
<Tooltip title={isV2Ui ? tooltip : undefined}>
|
||||
<Button {...buttonProps} className={resolvedClassName} aria-label={label}>
|
||||
{isV2Ui ? null : legacyContent}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -300,99 +335,184 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
|
||||
</>
|
||||
)}
|
||||
{onReload && (
|
||||
<Button icon={<ReloadOutlined />} disabled={loading} onClick={onRefresh}>
|
||||
{translate('data_grid.toolbar.refresh')}
|
||||
</Button>
|
||||
renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.refresh'),
|
||||
icon: <ReloadOutlined />,
|
||||
disabled: loading,
|
||||
onClick: onRefresh,
|
||||
})
|
||||
)}
|
||||
|
||||
{onToggleFilter && (
|
||||
<>
|
||||
{renderToolbarDivider()}
|
||||
<Button icon={<FilterOutlined />} type={showFilter ? 'primary' : 'default'} onClick={onToggleFilterClick}>
|
||||
{translate('data_grid.toolbar.filter')}
|
||||
</Button>
|
||||
{renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.filter'),
|
||||
icon: <FilterOutlined />,
|
||||
type: showFilter ? 'primary' : 'default',
|
||||
onClick: onToggleFilterClick,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{canModifyData && (
|
||||
<>
|
||||
{renderToolbarDivider()}
|
||||
<Button icon={<PlusOutlined />} onClick={onAddRow}>{translate('data_grid.toolbar.add_row')}</Button>
|
||||
{renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.add_row'),
|
||||
icon: <PlusOutlined />,
|
||||
onClick: onAddRow,
|
||||
})}
|
||||
{allSelectedAreDeleted ? (
|
||||
<Button icon={<UndoOutlined />} disabled={deleteTargetRowCount === 0} onClick={onUndoDeleteSelected}>{translate('data_grid.toolbar.undo_delete')}</Button>
|
||||
renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.undo_delete'),
|
||||
tooltip: deleteTargetRowCount > 0
|
||||
? `${translate('data_grid.toolbar.undo_delete')} · ${translate('data_grid.toolbar.selected_count', { count: deleteTargetRowCount })}`
|
||||
: translate('data_grid.toolbar.undo_delete'),
|
||||
icon: <UndoOutlined />,
|
||||
disabled: deleteTargetRowCount === 0,
|
||||
onClick: onUndoDeleteSelected,
|
||||
})
|
||||
) : (
|
||||
<Button icon={<DeleteOutlined />} danger disabled={deleteTargetRowCount === 0} onClick={onDeleteSelected}>{translate('data_grid.toolbar.delete_selected')}</Button>
|
||||
renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.delete_selected'),
|
||||
tooltip: deleteTargetRowCount > 0
|
||||
? `${translate('data_grid.toolbar.delete_selected')} · ${translate('data_grid.toolbar.selected_count', { count: deleteTargetRowCount })}`
|
||||
: translate('data_grid.toolbar.delete_selected'),
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
disabled: deleteTargetRowCount === 0,
|
||||
onClick: onDeleteSelected,
|
||||
})
|
||||
)}
|
||||
{deleteTargetRowCount > 0 && <span style={{ fontSize: '12px', color: '#888' }}>{translate('data_grid.toolbar.selected_count', { count: deleteTargetRowCount })}</span>}
|
||||
{!isV2Ui && deleteTargetRowCount > 0 && <span style={{ fontSize: '12px', color: '#888' }}>{translate('data_grid.toolbar.selected_count', { count: deleteTargetRowCount })}</span>}
|
||||
{renderToolbarDivider()}
|
||||
<Button
|
||||
data-grid-cell-editor-action="true"
|
||||
icon={<EditOutlined />}
|
||||
type={cellEditMode ? 'primary' : 'default'}
|
||||
onClick={onToggleCellEditMode}
|
||||
>
|
||||
{translate('data_grid.toolbar.cell_editor')}
|
||||
</Button>
|
||||
<Tooltip title={isV2Ui ? translate('data_grid.toolbar.cell_editor') : undefined}>
|
||||
<Button
|
||||
data-grid-cell-editor-action="true"
|
||||
className={isV2Ui ? 'gn-v2-data-grid-toolbar-action' : undefined}
|
||||
aria-label={translate('data_grid.toolbar.cell_editor')}
|
||||
icon={<EditOutlined />}
|
||||
type={cellEditMode ? 'primary' : 'default'}
|
||||
onClick={onToggleCellEditMode}
|
||||
>
|
||||
{isV2Ui ? null : translate('data_grid.toolbar.cell_editor')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{cellEditMode && selectedCellsSize > 0 && (
|
||||
<>
|
||||
<Button icon={<CopyOutlined />} onClick={onCopySelectedCellsToClipboard}>
|
||||
{translate('data_grid.toolbar.copy_selection', { count: selectedCellsSize })}
|
||||
</Button>
|
||||
<Button icon={<CopyOutlined />} onClick={onCopySelectedColumnsFromRow}>
|
||||
{translate('data_grid.toolbar.copy_selection_columns', { count: selectedCellsSize })}
|
||||
</Button>
|
||||
<Button type="primary" onClick={onOpenBatchEditModal}>
|
||||
{translate('data_grid.toolbar.batch_fill', { count: selectedCellsSize })}
|
||||
</Button>
|
||||
{renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.copy_selection', { count: selectedCellsSize }),
|
||||
icon: <CopyOutlined />,
|
||||
onClick: onCopySelectedCellsToClipboard,
|
||||
})}
|
||||
{renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.copy_selection_columns', { count: selectedCellsSize }),
|
||||
icon: <CopyOutlined />,
|
||||
onClick: onCopySelectedColumnsFromRow,
|
||||
})}
|
||||
{renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.batch_fill', { count: selectedCellsSize }),
|
||||
icon: <EditOutlined />,
|
||||
type: 'primary',
|
||||
onClick: onOpenBatchEditModal,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{cellEditMode && copiedCellPatchColumnCount > 0 && (
|
||||
<>
|
||||
<Button
|
||||
icon={<VerticalAlignBottomOutlined />}
|
||||
disabled={selectedRowKeysLength === 0}
|
||||
onClick={onPasteCopiedColumnsToSelectedRows}
|
||||
>
|
||||
{translate('data_grid.toolbar.paste_to_selected_rows', { count: selectedRowKeysLength })}
|
||||
</Button>
|
||||
<span style={{ fontSize: '12px', color: '#888' }}>
|
||||
{renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.paste_to_selected_rows', { count: selectedRowKeysLength }),
|
||||
tooltip: `${translate('data_grid.toolbar.paste_to_selected_rows', { count: selectedRowKeysLength })} · ${translate('data_grid.toolbar.copied_columns_count', { count: copiedCellPatchColumnCount })}`,
|
||||
icon: <VerticalAlignBottomOutlined />,
|
||||
disabled: selectedRowKeysLength === 0,
|
||||
onClick: onPasteCopiedColumnsToSelectedRows,
|
||||
})}
|
||||
{!isV2Ui && <span style={{ fontSize: '12px', color: '#888' }}>
|
||||
{translate('data_grid.toolbar.copied_columns_count', { count: copiedCellPatchColumnCount })}
|
||||
</span>
|
||||
</span>}
|
||||
</>
|
||||
)}
|
||||
{renderToolbarDivider()}
|
||||
<Button
|
||||
className={isV2Ui ? 'gn-v2-commit-button' : undefined}
|
||||
icon={<SaveOutlined />}
|
||||
type="primary"
|
||||
disabled={!hasChanges}
|
||||
onClick={onCommit}
|
||||
>
|
||||
{isV2Ui ? (
|
||||
<>
|
||||
<span>{translate('data_grid.toolbar.commit_label')}</span>
|
||||
<span className="gn-v2-toolbar-kbd">{pendingChangeCount}</span>
|
||||
</>
|
||||
) : translate('data_grid.toolbar.commit', { count: pendingChangeCount })}
|
||||
</Button>
|
||||
{hasChanges && (
|
||||
{renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.commit_label'),
|
||||
tooltip: translate('data_grid.toolbar.commit', { count: pendingChangeCount }),
|
||||
legacyContent: translate('data_grid.toolbar.commit', { count: pendingChangeCount }),
|
||||
className: isV2Ui ? 'gn-v2-commit-button' : undefined,
|
||||
icon: <SaveOutlined />,
|
||||
type: 'primary',
|
||||
disabled: !hasChanges,
|
||||
onClick: onCommit,
|
||||
})}
|
||||
{hasChanges && (isV2Ui ? (
|
||||
renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.preview_sql'),
|
||||
icon: <ConsoleSqlOutlined />,
|
||||
onClick: onPreviewChanges,
|
||||
})
|
||||
) : (
|
||||
<Dropdown menu={{ items: [{ key: 'preview-sql', label: translate('data_grid.toolbar.preview_sql_generate'), icon: <ConsoleSqlOutlined />, onClick: onPreviewChanges }] }}>
|
||||
<Button icon={<ConsoleSqlOutlined />}>{translate('data_grid.toolbar.preview_sql')} <DownOutlined /></Button>
|
||||
<Button
|
||||
aria-label={translate('data_grid.toolbar.preview_sql')}
|
||||
icon={<ConsoleSqlOutlined />}
|
||||
>
|
||||
{translate('data_grid.toolbar.preview_sql')} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
))}
|
||||
{hasChanges && renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.rollback'),
|
||||
icon: <UndoOutlined />,
|
||||
onClick: onResetPendingChanges,
|
||||
})}
|
||||
{isV2Ui ? (
|
||||
<Tooltip
|
||||
title={`${commitModeLabel} · ${translate('data_grid.toolbar.commit_mode.tooltip')}`}
|
||||
open={openToolbarMenu === 'commit-mode' ? false : undefined}
|
||||
>
|
||||
<span className="gn-v2-data-grid-toolbar-menu-trigger">
|
||||
<Dropdown
|
||||
autoFocus
|
||||
trigger={['click']}
|
||||
open={openToolbarMenu === 'commit-mode'}
|
||||
onOpenChange={(open) => updateToolbarMenuOpen('commit-mode', open)}
|
||||
menu={{
|
||||
selectable: true,
|
||||
selectedKeys: [dataEditCommitMode],
|
||||
items: [
|
||||
{ key: 'manual', label: translate('data_grid.toolbar.commit_mode.manual') },
|
||||
{ key: 'auto', label: translate('data_grid.toolbar.commit_mode.auto') },
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
setOpenToolbarMenu(null);
|
||||
onDataEditCommitModeChange(key as 'manual' | 'auto');
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="gn-v2-data-grid-toolbar-action"
|
||||
aria-label={commitModeLabel}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={openToolbarMenu === 'commit-mode'}
|
||||
icon={<ControlOutlined />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={translate('data_grid.toolbar.commit_mode.tooltip')}>
|
||||
<Select
|
||||
size="small"
|
||||
value={dataEditCommitMode}
|
||||
onChange={onDataEditCommitModeChange}
|
||||
style={{ width: 118, flex: '0 0 auto' }}
|
||||
options={[
|
||||
{ value: 'manual', label: translate('data_grid.toolbar.commit_mode.manual') },
|
||||
{ value: 'auto', label: translate('data_grid.toolbar.commit_mode.auto') },
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasChanges && <Button icon={<UndoOutlined />} onClick={onResetPendingChanges}>{translate('data_grid.toolbar.rollback')}</Button>}
|
||||
<Tooltip title={translate('data_grid.toolbar.commit_mode.tooltip')}>
|
||||
<Select
|
||||
size="small"
|
||||
value={dataEditCommitMode}
|
||||
onChange={onDataEditCommitModeChange}
|
||||
style={{ width: 118, flex: '0 0 auto' }}
|
||||
options={[
|
||||
{ value: 'manual', label: translate('data_grid.toolbar.commit_mode.manual') },
|
||||
{ value: 'auto', label: translate('data_grid.toolbar.commit_mode.auto') },
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
{dataEditCommitMode === 'auto' && (
|
||||
<Select
|
||||
size="small"
|
||||
@@ -413,45 +533,87 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
|
||||
{(canImport || canExport) && (
|
||||
<>
|
||||
{renderToolbarDivider()}
|
||||
{canImport && <Button icon={<ImportOutlined />} onClick={onImport}>{translate('data_grid.toolbar.import')}</Button>}
|
||||
{canExport && <Button icon={<ExportOutlined />} onClick={onOpenExportModal}>{translate('data_grid.toolbar.export')}</Button>}
|
||||
{canImport && renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.import'),
|
||||
icon: <ImportOutlined />,
|
||||
onClick: onImport,
|
||||
})}
|
||||
{canExport && renderToolbarAction({
|
||||
label: translate('data_grid.toolbar.export'),
|
||||
icon: <ExportOutlined />,
|
||||
onClick: onOpenExportModal,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isQueryResultExport && (
|
||||
<>
|
||||
{renderToolbarDivider()}
|
||||
<Dropdown menu={{ items: queryResultCopyMenu }} disabled={!canCopyQueryResult}>
|
||||
<Button
|
||||
data-grid-query-copy-action="true"
|
||||
icon={<CopyOutlined />}
|
||||
disabled={!canCopyQueryResult}
|
||||
onClick={onCopyQueryResultCsv}
|
||||
{isV2Ui ? (
|
||||
<Tooltip
|
||||
title={translate('data_grid.toolbar.copy')}
|
||||
open={openToolbarMenu === 'query-copy' ? false : undefined}
|
||||
>
|
||||
{translate('data_grid.toolbar.copy')} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<span className="gn-v2-data-grid-toolbar-menu-trigger">
|
||||
<Dropdown
|
||||
autoFocus
|
||||
trigger={['click']}
|
||||
open={openToolbarMenu === 'query-copy'}
|
||||
onOpenChange={(open) => updateToolbarMenuOpen('query-copy', open)}
|
||||
menu={{ items: queryResultCopyMenu }}
|
||||
disabled={!canCopyQueryResult}
|
||||
>
|
||||
<Button
|
||||
data-grid-query-copy-action="true"
|
||||
className="gn-v2-data-grid-toolbar-action"
|
||||
aria-label={translate('data_grid.toolbar.copy')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={openToolbarMenu === 'query-copy'}
|
||||
icon={<CopyOutlined />}
|
||||
disabled={!canCopyQueryResult}
|
||||
/>
|
||||
</Dropdown>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Dropdown menu={{ items: queryResultCopyMenu }} disabled={!canCopyQueryResult}>
|
||||
<Button
|
||||
data-grid-query-copy-action="true"
|
||||
aria-label={translate('data_grid.toolbar.copy')}
|
||||
icon={<CopyOutlined />}
|
||||
disabled={!canCopyQueryResult}
|
||||
onClick={onCopyQueryResultCsv}
|
||||
>
|
||||
{translate('data_grid.toolbar.copy')} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!canModifyData && selectedCellsSize > 0 && (
|
||||
<>
|
||||
{renderToolbarDivider()}
|
||||
<Button
|
||||
data-grid-copy-selection-action="true"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={onCopySelectedCellsToClipboard}
|
||||
>
|
||||
{translate('data_grid.toolbar.copy_selection', { count: selectedCellsSize })}
|
||||
</Button>
|
||||
<Tooltip title={isV2Ui ? translate('data_grid.toolbar.copy_selection', { count: selectedCellsSize }) : undefined}>
|
||||
<Button
|
||||
data-grid-copy-selection-action="true"
|
||||
className={isV2Ui ? 'gn-v2-data-grid-toolbar-action' : undefined}
|
||||
aria-label={translate('data_grid.toolbar.copy_selection', { count: selectedCellsSize })}
|
||||
icon={<CopyOutlined />}
|
||||
onClick={onCopySelectedCellsToClipboard}
|
||||
>
|
||||
{isV2Ui ? null : translate('data_grid.toolbar.copy_selection', { count: selectedCellsSize })}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<>
|
||||
{renderToolbarDivider()}
|
||||
<Tooltip title={translate('data_grid.toolbar.ai_insight_tooltip')}>
|
||||
<Tooltip title={aiInsightTooltip}>
|
||||
<Button
|
||||
className={isV2Ui ? 'gn-v2-ai-insight-button' : undefined}
|
||||
className={isV2Ui ? 'gn-v2-data-grid-toolbar-action gn-v2-ai-insight-button' : undefined}
|
||||
aria-label={isV2Ui ? translate('data_grid.toolbar.ai_insight_short') : translate('data_grid.toolbar.ai_insight')}
|
||||
icon={<RobotOutlined />}
|
||||
style={legacyAiButtonStyle}
|
||||
onMouseEnter={(event) => {
|
||||
@@ -466,8 +628,7 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
|
||||
}}
|
||||
onClick={onRequestAiInsight}
|
||||
>
|
||||
<span>{isV2Ui ? translate('data_grid.toolbar.ai_insight_short') : translate('data_grid.toolbar.ai_insight')}</span>
|
||||
{isV2Ui && aiShortcutLabel !== '-' && <span className="gn-v2-toolbar-kbd">{aiShortcutLabel}</span>}
|
||||
{isV2Ui ? null : <span>{translate('data_grid.toolbar.ai_insight')}</span>}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
@@ -484,10 +645,12 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
|
||||
{renderToolbarDivider()}
|
||||
<Tooltip title={paginationTotalCountLoading ? translate('data_grid.toolbar.cancel_count_tooltip') : translate('data_grid.toolbar.count_total_tooltip')}>
|
||||
<Button
|
||||
className={isV2Ui ? 'gn-v2-data-grid-toolbar-action' : undefined}
|
||||
aria-label={paginationTotalCountLoading ? translate('data_grid.toolbar.cancel_count') : translate('data_grid.toolbar.count_total')}
|
||||
icon={paginationTotalCountLoading ? <CloseOutlined /> : <VerticalAlignBottomOutlined />}
|
||||
onClick={onToggleTotalCount}
|
||||
>
|
||||
{paginationTotalCountLoading ? translate('data_grid.toolbar.cancel_count') : translate('data_grid.toolbar.count_total')}
|
||||
{isV2Ui ? null : (paginationTotalCountLoading ? translate('data_grid.toolbar.cancel_count') : translate('data_grid.toolbar.count_total'))}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -474,6 +474,7 @@ vi.mock('@ant-design/icons', () => {
|
||||
BugOutlined: Icon,
|
||||
ClearOutlined: Icon,
|
||||
CopyOutlined: Icon,
|
||||
DiffOutlined: Icon,
|
||||
PlayCircleOutlined: Icon,
|
||||
SaveOutlined: Icon,
|
||||
FormatPainterOutlined: Icon,
|
||||
@@ -486,6 +487,7 @@ vi.mock('@ant-design/icons', () => {
|
||||
DownOutlined: Icon,
|
||||
EyeOutlined: Icon,
|
||||
EyeInvisibleOutlined: Icon,
|
||||
EllipsisOutlined: Icon,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -620,11 +622,18 @@ const queryResultMessageText = (renderer: ReactTestRenderer): string => {
|
||||
return values.join('\n');
|
||||
};
|
||||
|
||||
const findButton = (renderer: ReactTestRenderer, text: string) =>
|
||||
renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0];
|
||||
const findButtons = (renderer: ReactTestRenderer, text: string) => {
|
||||
const visibleTextMatches = renderer.root.findAll(
|
||||
(node) => node.type === 'button' && textContent(node).includes(text),
|
||||
);
|
||||
return visibleTextMatches.length > 0
|
||||
? visibleTextMatches
|
||||
: renderer.root.findAll((node) => (
|
||||
node.type === 'button' && String(node.props?.['aria-label'] || '').includes(text)
|
||||
));
|
||||
};
|
||||
|
||||
const findButtons = (renderer: ReactTestRenderer, text: string) =>
|
||||
renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text));
|
||||
const findButton = (renderer: ReactTestRenderer, text: string) => findButtons(renderer, text)[0];
|
||||
|
||||
const findExactButton = (renderer: ReactTestRenderer, text: string) =>
|
||||
renderer.root.findAll((node) => node.type === 'button' && textContent(node) === text)[0];
|
||||
@@ -963,6 +972,42 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(textContent(renderer.toJSON())).not.toContain('等待执行 SQL');
|
||||
});
|
||||
|
||||
it('renders the v2 SQL toolbar actions as icon-only buttons', async () => {
|
||||
storeState.appearance.uiVersion = 'v2';
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab()} />);
|
||||
});
|
||||
|
||||
const exactLabels = [
|
||||
'运行',
|
||||
'保存',
|
||||
'AI · 更多',
|
||||
'更多',
|
||||
'搜索',
|
||||
'开启自动换行',
|
||||
'美化 SQL',
|
||||
'美化 SQL · 设置',
|
||||
];
|
||||
const iconOnlyButtons = exactLabels.map((label) => renderer.root.find(
|
||||
(node) => node.type === 'button' && node.props?.['aria-label'] === label,
|
||||
));
|
||||
iconOnlyButtons.push(renderer.root.find(
|
||||
(node) => node.type === 'button'
|
||||
&& String(node.props?.['aria-label'] || '').startsWith('触发 SQL AI 自动补全'),
|
||||
));
|
||||
|
||||
for (const button of iconOnlyButtons) {
|
||||
expect(textContent(button)).toBe('');
|
||||
expect(button.props.className).toContain('gn-v2-query-toolbar-icon-action');
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
renderer.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the empty query results panel after toggling the results button', async () => {
|
||||
storeState.appearance.uiVersion = 'v2';
|
||||
|
||||
@@ -1058,8 +1103,14 @@ describe('QueryEditor external SQL save', () => {
|
||||
|
||||
expect(textContent(renderer.toJSON())).toContain('结果 1');
|
||||
|
||||
const hideButton = renderer.root.find(
|
||||
(node) => node.type === 'button' && node.props['aria-label'] === '隐藏结果区',
|
||||
);
|
||||
expect(textContent(hideButton)).toBe('');
|
||||
expect(hideButton.props.className).toContain('gn-v2-data-grid-toolbar-action');
|
||||
|
||||
await act(async () => {
|
||||
findButton(renderer, '隐藏').props.onClick();
|
||||
hideButton.props.onClick();
|
||||
});
|
||||
|
||||
expect(textContent(renderer.toJSON())).not.toContain('结果 1');
|
||||
@@ -11590,8 +11641,8 @@ describe('QueryEditor external SQL save', () => {
|
||||
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-selects');
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-actions');
|
||||
expect(css).toContain('width: 78px !important;');
|
||||
expect(css).toContain('width: 104px !important;');
|
||||
expect(css).toContain('width: 48px !important;');
|
||||
expect(css).toContain('flex: 0 0 48px !important;');
|
||||
expect(css).toContain('flex: 0 0 auto !important;');
|
||||
expect(css).toContain('justify-content: flex-start;');
|
||||
expect(css).toContain('height: 32px !important;');
|
||||
|
||||
@@ -407,6 +407,7 @@ vi.mock('@ant-design/icons', () => {
|
||||
BugOutlined: Icon,
|
||||
ClearOutlined: Icon,
|
||||
CopyOutlined: Icon,
|
||||
DiffOutlined: Icon,
|
||||
PlayCircleOutlined: Icon,
|
||||
SaveOutlined: Icon,
|
||||
FormatPainterOutlined: Icon,
|
||||
@@ -420,6 +421,7 @@ vi.mock('@ant-design/icons', () => {
|
||||
EyeOutlined: Icon,
|
||||
EyeInvisibleOutlined: Icon,
|
||||
EnterOutlined: Icon,
|
||||
EllipsisOutlined: Icon,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -523,11 +525,18 @@ const textContent = (node: any): string => {
|
||||
.join('');
|
||||
};
|
||||
|
||||
const findButton = (renderer: ReactTestRenderer, text: string) =>
|
||||
renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text))[0];
|
||||
const findButtons = (renderer: ReactTestRenderer, text: string) => {
|
||||
const visibleTextMatches = renderer.root.findAll(
|
||||
(node) => node.type === 'button' && textContent(node).includes(text),
|
||||
);
|
||||
return visibleTextMatches.length > 0
|
||||
? visibleTextMatches
|
||||
: renderer.root.findAll((node) => (
|
||||
node.type === 'button' && String(node.props?.['aria-label'] || '').includes(text)
|
||||
));
|
||||
};
|
||||
|
||||
const findButtons = (renderer: ReactTestRenderer, text: string) =>
|
||||
renderer.root.findAll((node) => node.type === 'button' && textContent(node).includes(text));
|
||||
const findButton = (renderer: ReactTestRenderer, text: string) => findButtons(renderer, text)[0];
|
||||
|
||||
const findExactButton = (renderer: ReactTestRenderer, text: string) =>
|
||||
renderer.root.findAll((node) => node.type === 'button' && textContent(node) === text)[0];
|
||||
@@ -3811,8 +3820,8 @@ describe('QueryEditor external SQL save', () => {
|
||||
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-selects');
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-actions');
|
||||
expect(css).toContain('width: 78px !important;');
|
||||
expect(css).toContain('width: 104px !important;');
|
||||
expect(css).toContain('width: 48px !important;');
|
||||
expect(css).toContain('flex: 0 0 48px !important;');
|
||||
expect(css).toContain('flex: 0 0 auto !important;');
|
||||
expect(css).toContain('justify-content: flex-start;');
|
||||
expect(css).toContain('height: 32px !important;');
|
||||
|
||||
@@ -457,9 +457,13 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
|
||||
|
||||
const toolbarHideButton = (
|
||||
<Tooltip title={hideTooltipTitle}>
|
||||
<Button className={isV2Ui ? 'gn-v2-query-result-toolbar-hide' : undefined} icon={<EyeInvisibleOutlined />} onClick={onHide}>
|
||||
<span>{t('query_editor.results_panel.action.hide')}</span>
|
||||
{isV2Ui && toggleShortcutLabel && <span className="gn-v2-toolbar-kbd">{toggleShortcutLabel}</span>}
|
||||
<Button
|
||||
aria-label={t('query_editor.results_panel.aria.hide')}
|
||||
className={isV2Ui ? 'gn-v2-data-grid-toolbar-action gn-v2-query-result-toolbar-hide' : undefined}
|
||||
icon={<EyeInvisibleOutlined />}
|
||||
onClick={onHide}
|
||||
>
|
||||
{!isV2Ui && <span>{t('query_editor.results_panel.action.hide')}</span>}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,13 @@ describe('QueryEditorToolbar AI trigger affordance', () => {
|
||||
expect(source).toContain('onMouseDown={onCaptureEditorCursorPosition}');
|
||||
expect(source).toContain('onClick={onTriggerSqlAiCompletion}');
|
||||
expect(source).toContain('triggerSqlAiCompletionLabel');
|
||||
expect(source).toMatch(/aria-label="AI more actions"\s+onMouseDown=\{onCaptureEditorCursorPosition\}/);
|
||||
expect(source).toContain('const aiMoreTitle');
|
||||
expect(source).toMatch(/aria-label=\{aiMoreTitle\}[\s\S]*?onMouseDown=\{onCaptureEditorCursorPosition\}/);
|
||||
});
|
||||
|
||||
it('keeps the secondary AI dropdown for other actions', () => {
|
||||
expect(source).toContain('icon={<DownOutlined />}');
|
||||
expect(source).toContain('menu={{ items: aiMenuItems }}');
|
||||
expect(source).toContain('title={isV2Ui ? aiMoreTitle : undefined}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('QueryEditorToolbar layout', () => {
|
||||
expect(css).toContain('gap: 6px;');
|
||||
});
|
||||
|
||||
it('optically aligns the word-wrap icon with its visible label', () => {
|
||||
it('optically centers the word-wrap icon in its v2 icon-only action', () => {
|
||||
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
|
||||
const css = readV2ThemeCss();
|
||||
const wordWrapCss = css.slice(
|
||||
@@ -50,6 +50,7 @@ describe('QueryEditorToolbar layout', () => {
|
||||
|
||||
expect(toolbarSource).toContain('gn-v2-query-toolbar-word-wrap-action');
|
||||
expect(toolbarSource).toContain('gn-query-toolbar-word-wrap-icon');
|
||||
expect(toolbarSource).toContain('{!isV2Ui && t("query_editor.action.word_wrap")}');
|
||||
expect(wordWrapCss).toContain('align-items: center;');
|
||||
expect(wordWrapCss).toContain('justify-content: center;');
|
||||
expect(wordWrapCss).toContain('width: 16px;');
|
||||
@@ -80,7 +81,7 @@ describe('QueryEditorToolbar layout', () => {
|
||||
expect(commitKbdHoverCss).toContain('background:');
|
||||
});
|
||||
|
||||
it('keeps transaction selects wide enough for localized auto-commit labels', () => {
|
||||
it('keeps transaction selectors compact after replacing selected labels with icons', () => {
|
||||
const css = readV2ThemeCss();
|
||||
const transactionModeCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar-transaction-mode-select {'),
|
||||
@@ -91,10 +92,32 @@ describe('QueryEditorToolbar layout', () => {
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar .ant-select-selector {'),
|
||||
);
|
||||
|
||||
expect(transactionModeCss).toContain('width: 78px !important;');
|
||||
expect(transactionModeCss).toContain('flex: 0 0 78px !important;');
|
||||
expect(transactionDelayCss).toContain('width: 104px !important;');
|
||||
expect(transactionDelayCss).toContain('flex: 0 0 104px !important;');
|
||||
expect(transactionModeCss).toContain('width: 48px !important;');
|
||||
expect(transactionModeCss).toContain('flex: 0 0 48px !important;');
|
||||
expect(transactionDelayCss).toContain('width: 48px !important;');
|
||||
expect(transactionDelayCss).toContain('flex: 0 0 48px !important;');
|
||||
expect(css).toContain(
|
||||
'body[data-ui-version="v2"] .gn-v2-query-toolbar .gn-v2-query-toolbar-icon-select .ant-select-selector {',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses a stable icon-button width for every v2 SQL toolbar action', () => {
|
||||
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
|
||||
const css = readV2ThemeCss();
|
||||
const iconActionCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar-icon-action.ant-btn,'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar-run-action.ant-btn {'),
|
||||
);
|
||||
|
||||
expect(toolbarSource).toContain('EllipsisOutlined');
|
||||
expect(toolbarSource).toContain('gn-v2-query-toolbar-menu-trigger');
|
||||
expect(toolbarSource).toContain('aria-haspopup="menu"');
|
||||
expect(toolbarSource).toContain('aria-expanded={isV2Ui ? openToolbarMenu === "ai" : undefined}');
|
||||
expect(toolbarSource).toContain('aria-expanded={isV2Ui ? openToolbarMenu === "more" : undefined}');
|
||||
expect(toolbarSource).toContain('aria-expanded={isV2Ui ? openToolbarMenu === "format" : undefined}');
|
||||
expect(iconActionCss).toContain('width: 34px !important;');
|
||||
expect(iconActionCss).toContain('min-width: 34px !important;');
|
||||
expect(iconActionCss).toContain('padding: 0 !important;');
|
||||
});
|
||||
|
||||
it('keeps editor search action grouped with formatting actions', () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DownOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
EyeOutlined,
|
||||
EllipsisOutlined,
|
||||
FormatPainterOutlined,
|
||||
PlayCircleOutlined,
|
||||
RobotOutlined,
|
||||
@@ -90,6 +91,8 @@ type FullNameSelectOption = {
|
||||
fullName: string;
|
||||
};
|
||||
|
||||
type QueryToolbarMenuKey = "ai" | "more" | "format";
|
||||
|
||||
const renderFullNameSelectTooltip = (fullName: React.ReactNode) => {
|
||||
const fullNameText = String(fullName ?? "");
|
||||
|
||||
@@ -150,6 +153,10 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
}) => {
|
||||
const i18n = useOptionalI18n();
|
||||
const t = i18n?.t ?? defaultTranslate;
|
||||
const [openToolbarMenu, setOpenToolbarMenu] = React.useState<QueryToolbarMenuKey | null>(null);
|
||||
const updateToolbarMenuOpen = (key: QueryToolbarMenuKey, open: boolean) => {
|
||||
setOpenToolbarMenu((current) => open ? key : current === key ? null : current);
|
||||
};
|
||||
const baseMoreMenuItems = saveMoreMenuItems ?? [];
|
||||
const connectionSelectOptions: FullNameSelectOption[] =
|
||||
queryCapableConnections.map((connection) => ({
|
||||
@@ -212,6 +219,8 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
activeShortcutPlatform,
|
||||
)}`
|
||||
: t("app.shortcuts.action.triggerSqlAiCompletion.label");
|
||||
const aiMoreTitle = `AI · ${t("query_editor.action.more")}`;
|
||||
const formatSettingsTitle = `${t("query_editor.action.format_sql")} · ${t("settings.title")}`;
|
||||
const aiMenuItems: MenuProps["items"] = [
|
||||
{
|
||||
key: "ai-inline-completion",
|
||||
@@ -362,36 +371,43 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-run-action" : undefined}
|
||||
aria-label={t("query_editor.action.run")}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action gn-v2-query-toolbar-run-action" : undefined}
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onMouseDown={onCaptureEditorCursorPosition}
|
||||
onClick={onRun}
|
||||
loading={loading}
|
||||
>
|
||||
{t("query_editor.action.run")}
|
||||
{!isV2Ui && t("query_editor.action.run")}
|
||||
</Button>
|
||||
{showViewDataVerify && onViewDataVerify && (
|
||||
<Tooltip title={t("result_diff.view_verify.toolbar.tooltip")}>
|
||||
<Button
|
||||
icon={<DiffOutlined />}
|
||||
disabled={loading}
|
||||
onClick={onViewDataVerify}
|
||||
>
|
||||
{t("result_diff.view_verify.toolbar")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Tooltip>
|
||||
{showViewDataVerify && onViewDataVerify && (
|
||||
<Tooltip title={t("result_diff.view_verify.toolbar.tooltip")}>
|
||||
<Button
|
||||
aria-label={t("result_diff.view_verify.toolbar")}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
icon={<DiffOutlined />}
|
||||
disabled={loading}
|
||||
onClick={onViewDataVerify}
|
||||
>
|
||||
{!isV2Ui && t("result_diff.view_verify.toolbar")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{loading && (
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("query_editor.action.stop")}
|
||||
</Button>
|
||||
<Tooltip title={t("query_editor.action.stop")}>
|
||||
<Button
|
||||
aria-label={t("query_editor.action.stop")}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
type="primary"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{!isV2Ui && t("query_editor.action.stop")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{isV2Ui && pendingTransactionToolbar}
|
||||
@@ -412,46 +428,76 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-save-action" : undefined}
|
||||
aria-label={t("query_editor.action.save")}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action gn-v2-query-toolbar-save-action" : undefined}
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={onQuickSave}
|
||||
>
|
||||
{t("query_editor.action.save")}
|
||||
{!isV2Ui && t("query_editor.action.save")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
|
||||
<Tooltip title={triggerSqlAiCompletionLabel}>
|
||||
<Button
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-ai-action" : undefined}
|
||||
aria-label={triggerSqlAiCompletionLabel}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action gn-v2-query-toolbar-ai-action" : undefined}
|
||||
icon={<RobotOutlined />}
|
||||
style={{ color: "#818cf8" }}
|
||||
onMouseDown={onCaptureEditorCursorPosition}
|
||||
onClick={onTriggerSqlAiCompletion}
|
||||
>
|
||||
AI
|
||||
{!isV2Ui && "AI"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
menu={{ items: aiMenuItems }}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
<Tooltip
|
||||
title={isV2Ui ? aiMoreTitle : undefined}
|
||||
open={isV2Ui && openToolbarMenu === "ai" ? false : undefined}
|
||||
>
|
||||
<Button
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
icon={<DownOutlined />}
|
||||
aria-label="AI more actions"
|
||||
onMouseDown={onCaptureEditorCursorPosition}
|
||||
/>
|
||||
</Dropdown>
|
||||
<span className={isV2Ui ? "gn-v2-query-toolbar-menu-trigger" : undefined}>
|
||||
<Dropdown
|
||||
menu={{ items: aiMenuItems }}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
open={isV2Ui ? openToolbarMenu === "ai" : undefined}
|
||||
onOpenChange={isV2Ui ? (open) => updateToolbarMenuOpen("ai", open) : undefined}
|
||||
>
|
||||
<Button
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
icon={<DownOutlined />}
|
||||
aria-label={aiMoreTitle}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isV2Ui ? openToolbarMenu === "ai" : undefined}
|
||||
onMouseDown={onCaptureEditorCursorPosition}
|
||||
/>
|
||||
</Dropdown>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Dropdown
|
||||
menu={{ items: moreMenuItems }}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
<Tooltip
|
||||
title={isV2Ui ? t("query_editor.action.more") : undefined}
|
||||
open={isV2Ui && openToolbarMenu === "more" ? false : undefined}
|
||||
>
|
||||
<Button>{t("query_editor.action.more")}</Button>
|
||||
</Dropdown>
|
||||
<span className={isV2Ui ? "gn-v2-query-toolbar-menu-trigger" : undefined}>
|
||||
<Dropdown
|
||||
menu={{ items: moreMenuItems }}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
open={isV2Ui ? openToolbarMenu === "more" : undefined}
|
||||
onOpenChange={isV2Ui ? (open) => updateToolbarMenuOpen("more", open) : undefined}
|
||||
>
|
||||
<Button
|
||||
aria-label={t("query_editor.action.more")}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
icon={isV2Ui ? <EllipsisOutlined /> : undefined}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isV2Ui ? openToolbarMenu === "more" : undefined}
|
||||
>
|
||||
{!isV2Ui && t("query_editor.action.more")}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -459,8 +505,13 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
style={{ display: "flex", gap: "8px", alignItems: "center" }}
|
||||
>
|
||||
<Tooltip title={findInEditorTitle}>
|
||||
<Button icon={<SearchOutlined />} onClick={onFindInEditor}>
|
||||
{t("query_editor.action.find_in_editor")}
|
||||
<Button
|
||||
aria-label={t("query_editor.action.find_in_editor")}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
icon={<SearchOutlined />}
|
||||
onClick={onFindInEditor}
|
||||
>
|
||||
{!isV2Ui && t("query_editor.action.find_in_editor")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
@@ -471,7 +522,7 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-word-wrap-action" : undefined}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action gn-v2-query-toolbar-word-wrap-action" : undefined}
|
||||
type={wordWrapEnabled ? "primary" : "default"}
|
||||
icon={<WrapTextIcon />}
|
||||
aria-label={t(
|
||||
@@ -482,24 +533,41 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
|
||||
aria-pressed={wordWrapEnabled}
|
||||
onClick={onToggleWordWrap}
|
||||
>
|
||||
{t("query_editor.action.word_wrap")}
|
||||
{!isV2Ui && t("query_editor.action.word_wrap")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={formatSqlTitle}>
|
||||
<Button icon={<FormatPainterOutlined />} onClick={onFormat}>
|
||||
{t("query_editor.action.format")}
|
||||
<Button
|
||||
aria-label={t("query_editor.action.format_sql")}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
icon={<FormatPainterOutlined />}
|
||||
onClick={onFormat}
|
||||
>
|
||||
{!isV2Ui && t("query_editor.action.format")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
menu={{ items: formatSettingsMenu }}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
<Tooltip
|
||||
title={isV2Ui ? formatSettingsTitle : undefined}
|
||||
open={isV2Ui && openToolbarMenu === "format" ? false : undefined}
|
||||
>
|
||||
<Button
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
icon={<SettingOutlined />}
|
||||
/>
|
||||
</Dropdown>
|
||||
<span className={isV2Ui ? "gn-v2-query-toolbar-menu-trigger" : undefined}>
|
||||
<Dropdown
|
||||
menu={{ items: formatSettingsMenu }}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
open={isV2Ui ? openToolbarMenu === "format" : undefined}
|
||||
onOpenChange={isV2Ui ? (open) => updateToolbarMenuOpen("format", open) : undefined}
|
||||
>
|
||||
<Button
|
||||
aria-label={formatSettingsTitle}
|
||||
className={isV2Ui ? "gn-v2-query-toolbar-icon-action" : undefined}
|
||||
icon={<SettingOutlined />}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isV2Ui ? openToolbarMenu === "format" : undefined}
|
||||
/>
|
||||
</Dropdown>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{!isV2Ui && (
|
||||
|
||||
@@ -26,7 +26,13 @@ vi.mock('../i18n/provider', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const latestTooltipProps = () => antdState.tooltipProps[antdState.tooltipProps.length - 1];
|
||||
const latestSelectProps = (className: string) => [...antdState.selectProps].reverse().find(
|
||||
(props) => String(props.className).includes(className),
|
||||
);
|
||||
|
||||
const latestTooltipProps = (selectClassName: string) => [...antdState.tooltipProps].reverse().find(
|
||||
(props) => String(props.children?.props?.className).includes(selectClassName),
|
||||
);
|
||||
|
||||
describe('QueryEditorTransactionSettings', () => {
|
||||
let renderer: ReactTestRenderer | null = null;
|
||||
@@ -54,9 +60,97 @@ describe('QueryEditorTransactionSettings', () => {
|
||||
);
|
||||
});
|
||||
|
||||
expect(latestTooltipProps().placement).toBe('topLeft');
|
||||
expect(latestTooltipProps()).not.toHaveProperty('autoAdjustOverflow', false);
|
||||
expect(latestTooltipProps()).not.toHaveProperty('open');
|
||||
expect(antdState.selectProps[0]).not.toHaveProperty('onOpenChange');
|
||||
expect(antdState.tooltipProps[0].placement).toBe('topLeft');
|
||||
expect(antdState.tooltipProps[0]).not.toHaveProperty('autoAdjustOverflow', false);
|
||||
expect(antdState.tooltipProps[0].title).toContain('query_editor.transaction.mode.tooltip');
|
||||
expect(antdState.selectProps[0].onOpenChange).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('shows only state icons in the v2 selectors while keeping localized accessible labels', () => {
|
||||
act(() => {
|
||||
renderer = create(
|
||||
<QueryEditorTransactionSettings
|
||||
isV2Ui
|
||||
commitMode="auto"
|
||||
autoCommitDelayMs={0}
|
||||
onCommitModeChange={vi.fn()}
|
||||
onAutoCommitDelayMsChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const [modeSelect, delaySelect] = antdState.selectProps;
|
||||
expect(modeSelect.className).toContain('gn-v2-query-toolbar-icon-select');
|
||||
expect(modeSelect['aria-label']).toBe('query_editor.transaction.mode.auto');
|
||||
expect(modeSelect.popupMatchSelectWidth).toBe(false);
|
||||
expect(typeof modeSelect.labelRender).toBe('function');
|
||||
expect(React.isValidElement(modeSelect.labelRender({ value: 'auto' }))).toBe(true);
|
||||
expect(modeSelect.options.map(({ label }: { label: string }) => label)).toEqual([
|
||||
'query_editor.transaction.mode.manual',
|
||||
'query_editor.transaction.mode.auto',
|
||||
]);
|
||||
|
||||
expect(delaySelect.className).toContain('gn-v2-query-toolbar-icon-select');
|
||||
expect(delaySelect['aria-label']).toBe('query_editor.transaction.delay.immediate_commit');
|
||||
expect(delaySelect.popupMatchSelectWidth).toBe(false);
|
||||
expect(typeof delaySelect.labelRender).toBe('function');
|
||||
expect(React.isValidElement(delaySelect.labelRender({ value: 0 }))).toBe(true);
|
||||
expect(delaySelect.options.map(({ label }: { label: string }) => label)).toEqual([
|
||||
'query_editor.transaction.delay.immediate_commit',
|
||||
'3s后提交',
|
||||
'5s后提交',
|
||||
'10s后提交',
|
||||
'30s后提交',
|
||||
]);
|
||||
|
||||
expect(antdState.tooltipProps[0].title).toContain('query_editor.transaction.mode.auto');
|
||||
expect(antdState.tooltipProps[1].title).toContain('query_editor.transaction.delay.immediate_commit');
|
||||
});
|
||||
|
||||
it('hides each selector tooltip while its v2 dropdown is open', () => {
|
||||
act(() => {
|
||||
renderer = create(
|
||||
<QueryEditorTransactionSettings
|
||||
isV2Ui
|
||||
commitMode="auto"
|
||||
autoCommitDelayMs={0}
|
||||
onCommitModeChange={vi.fn()}
|
||||
onAutoCommitDelayMsChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
latestSelectProps('gn-v2-query-toolbar-transaction-mode-select').onOpenChange(true);
|
||||
});
|
||||
expect(latestTooltipProps('gn-v2-query-toolbar-transaction-mode-select').title).toBeNull();
|
||||
|
||||
act(() => {
|
||||
latestSelectProps('gn-v2-query-toolbar-transaction-mode-select').onOpenChange(false);
|
||||
latestSelectProps('gn-v2-query-toolbar-transaction-delay-select').onOpenChange(true);
|
||||
});
|
||||
expect(latestTooltipProps('gn-v2-query-toolbar-transaction-mode-select').title).toContain(
|
||||
'query_editor.transaction.mode.tooltip',
|
||||
);
|
||||
expect(latestTooltipProps('gn-v2-query-toolbar-transaction-delay-select').title).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps textual select labels in the legacy toolbar', () => {
|
||||
act(() => {
|
||||
renderer = create(
|
||||
<QueryEditorTransactionSettings
|
||||
isV2Ui={false}
|
||||
commitMode="manual"
|
||||
autoCommitDelayMs={0}
|
||||
onCommitModeChange={vi.fn()}
|
||||
onAutoCommitDelayMsChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(antdState.selectProps[0].labelRender).toBeUndefined();
|
||||
expect(antdState.selectProps[0].className).toBeUndefined();
|
||||
expect(antdState.selectProps[0].popupMatchSelectWidth).toBeUndefined();
|
||||
expect(antdState.selectProps[0].onOpenChange).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
ControlOutlined,
|
||||
SyncOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { t as defaultTranslate } from '../i18n';
|
||||
import { useOptionalI18n } from '../i18n/provider';
|
||||
@@ -26,6 +32,8 @@ type QueryEditorTransactionSettingsProps = {
|
||||
onAutoCommitDelayMsChange: (delayMs: number) => void;
|
||||
};
|
||||
|
||||
type OpenTransactionSelect = 'mode' | 'delay' | null;
|
||||
|
||||
const QueryEditorTransactionSettings: React.FC<QueryEditorTransactionSettingsProps> = ({
|
||||
isV2Ui,
|
||||
commitMode,
|
||||
@@ -35,24 +43,45 @@ const QueryEditorTransactionSettings: React.FC<QueryEditorTransactionSettingsPro
|
||||
}) => {
|
||||
const i18n = useOptionalI18n();
|
||||
const t = i18n?.t ?? defaultTranslate;
|
||||
const [openTransactionSelect, setOpenTransactionSelect] = React.useState<OpenTransactionSelect>(null);
|
||||
const updateTransactionSelectOpen = (key: Exclude<OpenTransactionSelect, null>, open: boolean) => {
|
||||
setOpenTransactionSelect((current) => open ? key : current === key ? null : current);
|
||||
};
|
||||
const autoCommitDelayOptions = SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.value === 0
|
||||
? t('query_editor.transaction.delay.immediate_commit')
|
||||
: t('query_editor.transaction.delay.seconds_commit', { seconds: Math.round(option.value / 1000) }),
|
||||
}));
|
||||
const commitModeLabel = t(`query_editor.transaction.mode.${commitMode}`);
|
||||
const autoCommitDelayLabel = autoCommitDelayOptions.find(
|
||||
(option) => option.value === autoCommitDelayMs,
|
||||
)?.label ?? t('query_editor.transaction.delay.immediate_commit');
|
||||
const commitModeTooltip = `${commitModeLabel} · ${t('query_editor.transaction.mode.tooltip')}`;
|
||||
const autoCommitDelayTooltip = `${t('query_editor.transaction.mode.auto')} · ${autoCommitDelayLabel}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
title={t('query_editor.transaction.mode.tooltip')}
|
||||
title={isV2Ui && openTransactionSelect === 'mode' ? null : commitModeTooltip}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-transaction-mode-select' : undefined}
|
||||
aria-label={commitModeLabel}
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-icon-select gn-v2-query-toolbar-transaction-mode-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 78 }}
|
||||
value={commitMode}
|
||||
onChange={(mode) => onCommitModeChange(mode === 'auto' ? 'auto' : 'manual')}
|
||||
popupMatchSelectWidth={isV2Ui ? false : undefined}
|
||||
onOpenChange={isV2Ui ? (open) => updateTransactionSelectOpen('mode', open) : undefined}
|
||||
onChange={(mode) => {
|
||||
setOpenTransactionSelect(null);
|
||||
onCommitModeChange(mode === 'auto' ? 'auto' : 'manual');
|
||||
}}
|
||||
labelRender={isV2Ui ? (option) => (
|
||||
<span className="gn-v2-query-toolbar-select-icon" aria-hidden="true">
|
||||
{option.value === 'auto' ? <SyncOutlined /> : <ControlOutlined />}
|
||||
</span>
|
||||
) : undefined}
|
||||
options={[
|
||||
{ label: t('query_editor.transaction.mode.manual'), value: 'manual' },
|
||||
{ label: t('query_editor.transaction.mode.auto'), value: 'auto' },
|
||||
@@ -60,13 +89,29 @@ const QueryEditorTransactionSettings: React.FC<QueryEditorTransactionSettingsPro
|
||||
/>
|
||||
</Tooltip>
|
||||
{commitMode === 'auto' && (
|
||||
<Select
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-transaction-delay-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 68 }}
|
||||
value={autoCommitDelayMs}
|
||||
onChange={(delayMs) => onAutoCommitDelayMsChange(Number(delayMs))}
|
||||
options={autoCommitDelayOptions}
|
||||
/>
|
||||
<Tooltip
|
||||
title={isV2Ui && openTransactionSelect === 'delay' ? null : autoCommitDelayTooltip}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Select
|
||||
aria-label={autoCommitDelayLabel}
|
||||
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-icon-select gn-v2-query-toolbar-transaction-delay-select' : undefined}
|
||||
style={isV2Ui ? undefined : { width: 68 }}
|
||||
value={autoCommitDelayMs}
|
||||
popupMatchSelectWidth={isV2Ui ? false : undefined}
|
||||
onOpenChange={isV2Ui ? (open) => updateTransactionSelectOpen('delay', open) : undefined}
|
||||
onChange={(delayMs) => {
|
||||
setOpenTransactionSelect(null);
|
||||
onAutoCommitDelayMsChange(Number(delayMs));
|
||||
}}
|
||||
labelRender={isV2Ui ? () => (
|
||||
<span className="gn-v2-query-toolbar-select-icon" aria-hidden="true">
|
||||
{autoCommitDelayMs === 0 ? <ThunderboltOutlined /> : <ClockCircleOutlined />}
|
||||
</span>
|
||||
) : undefined}
|
||||
options={autoCommitDelayOptions}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -162,13 +162,37 @@ body[data-ui-version="v2"] .gn-v2-query-toolbar-max-rows-select {
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-transaction-mode-select {
|
||||
width: 78px !important;
|
||||
flex: 0 0 78px !important;
|
||||
width: 48px !important;
|
||||
flex: 0 0 48px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-transaction-delay-select {
|
||||
width: 104px !important;
|
||||
flex: 0 0 104px !important;
|
||||
width: 48px !important;
|
||||
flex: 0 0 48px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar .gn-v2-query-toolbar-icon-select .ant-select-selector {
|
||||
padding: 0 23px 0 7px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-icon-select .ant-select-selection-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-inline-end: 0 !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-icon-select .ant-select-arrow {
|
||||
inset-inline-end: 7px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-select-icon {
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar .ant-select-selector {
|
||||
@@ -275,12 +299,21 @@ body[data-ui-version="v2"] .gn-v2-query-transaction-commit-button:active .gn-v2-
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-icon-action.ant-btn,
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar .ant-btn-icon-only {
|
||||
width: 34px !important;
|
||||
min-width: 34px !important;
|
||||
padding: 0 !important;
|
||||
flex: 0 0 34px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-menu-trigger {
|
||||
display: inline-flex;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
flex: 0 0 34px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-run-action.ant-btn {
|
||||
min-width: 64px;
|
||||
padding: 0 13px !important;
|
||||
min-width: 34px;
|
||||
padding: 0 !important;
|
||||
font-weight: 650 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -3930,6 +3930,23 @@ body[data-ui-version="v2"] .gn-v2-data-grid .data-grid-toolbar-scroll .ant-btn {
|
||||
border-radius: 7px !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-data-grid-toolbar-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px !important;
|
||||
min-width: 28px !important;
|
||||
padding-inline: 0 !important;
|
||||
flex: 0 0 28px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid .gn-v2-data-grid-toolbar-menu-trigger {
|
||||
display: inline-flex;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
flex: 0 0 28px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-data-grid-toolbar-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user