mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-21 16:34:21 +08:00
✨ feat(data-grid): 优化行文本查看体验 (#816)
## 背景 字段较多时,查看同一行数据需要横向滚动,影响快速定位和比较。 ## 变更 - 支持点击行号整列或双击行号切换到文本视图。 - 文本视图改为字段、类型、注释、值四列;前三列固定宽度,值列使用剩余空间。 - 值列仅在实际截断时显示完整内容 Tooltip,并支持点击或键盘操作复制。 - 保留文本/JSON 编辑后的返回表格入口及多语言文案。 - 增加布局和复制交互回归测试。 ## 验证 - pm --prefix frontend test -- --run src/components/DataGrid.layout.test.tsx src/components/DataGridRecordViews.interaction.test.tsx src/i18n/catalog.test.ts(90/90 通过) - pm --prefix frontend run build 通过。 - Wails 生产包已构建并人工测试通过。 ## 说明 TypeScript 全量检查仍报告上游 QueryEditor.tsx 中 Monaco hover.enabled 的 2 个既有类型错误,与本 PR 改动无关。 Closes #796
This commit is contained in:
@@ -933,6 +933,12 @@ describe('DataGrid layout', () => {
|
||||
'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',
|
||||
'data_grid.record_view.back_to_table': 'Back to table label',
|
||||
'data_grid.record_view.field': 'Field label',
|
||||
'data_grid.record_view.value': 'Value label',
|
||||
'data_grid.record_view.comment': 'Comment label',
|
||||
'data_grid.record_view.type': 'Type label',
|
||||
'data_grid.record_view.copy_value': 'Copy value label',
|
||||
'data_grid.record_view.previous': 'Previous label',
|
||||
'data_grid.record_view.next': 'Next label',
|
||||
'data_grid.record_view.record_position': `Record label ${params?.current} of ${params?.total}`,
|
||||
@@ -1054,10 +1060,12 @@ describe('DataGrid layout', () => {
|
||||
jsonViewText="[]"
|
||||
translate={translate}
|
||||
onOpenJsonEditor={() => {}}
|
||||
onReturnToTable={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(jsonRecordMarkup).toContain('5 JSON rows label');
|
||||
expect(jsonRecordMarkup).toContain('Edit JSON label');
|
||||
expect(jsonRecordMarkup).toContain('Back to table label');
|
||||
expect(jsonRecordMarkup).not.toContain('data_grid.record_view');
|
||||
|
||||
const textRecordMarkup = renderToStaticMarkup(
|
||||
@@ -1076,6 +1084,7 @@ describe('DataGrid layout', () => {
|
||||
onPrev={() => {}}
|
||||
onNext={() => {}}
|
||||
onEditCurrent={() => {}}
|
||||
onReturnToTable={() => {}}
|
||||
formatTextViewValue={(value) => String(value)}
|
||||
/>,
|
||||
);
|
||||
@@ -1083,9 +1092,26 @@ describe('DataGrid layout', () => {
|
||||
expect(textRecordMarkup).toContain('Next label');
|
||||
expect(textRecordMarkup).toContain('Record label 1 of 2');
|
||||
expect(textRecordMarkup).toContain('Edit current label');
|
||||
expect(textRecordMarkup).toContain('Back to table label');
|
||||
expect(textRecordMarkup).toContain('Field label');
|
||||
expect(textRecordMarkup).toContain('Value label');
|
||||
expect(textRecordMarkup).toContain('Comment label');
|
||||
expect(textRecordMarkup).toContain('Type label');
|
||||
expect(textRecordMarkup).toContain('data-grid-text-view-header="field"');
|
||||
expect(textRecordMarkup).toContain('data-grid-text-view-header="value"');
|
||||
expect(textRecordMarkup.indexOf('data-grid-text-view-header="field"'))
|
||||
.toBeLessThan(textRecordMarkup.indexOf('data-grid-text-view-header="type"'));
|
||||
expect(textRecordMarkup.indexOf('data-grid-text-view-header="type"'))
|
||||
.toBeLessThan(textRecordMarkup.indexOf('data-grid-text-view-header="comment"'));
|
||||
expect(textRecordMarkup.indexOf('data-grid-text-view-header="comment"'))
|
||||
.toBeLessThan(textRecordMarkup.indexOf('data-grid-text-view-header="value"'));
|
||||
expect(textRecordMarkup).toContain('data-grid-text-value-copy="true"');
|
||||
expect(textRecordMarkup).toContain('aria-label="Copy value label"');
|
||||
expect(textRecordMarkup).toContain('grid-template-columns:180px 140px 240px minmax(260px, 1fr)');
|
||||
expect(textRecordMarkup).toContain('text-overflow:ellipsis');
|
||||
expect(textRecordMarkup).toContain('raw_sql');
|
||||
expect(textRecordMarkup).toContain('TYPE varchar(128)');
|
||||
expect(textRecordMarkup).toContain('COMMENT SQL text payload');
|
||||
expect(textRecordMarkup).toContain('varchar(128)');
|
||||
expect(textRecordMarkup).toContain('SQL text payload');
|
||||
expect(textRecordMarkup).toContain('GitHub release HTTP 500 checksum abc123');
|
||||
expect(textRecordMarkup).not.toContain('data_grid.record_view');
|
||||
|
||||
@@ -1105,6 +1131,7 @@ describe('DataGrid layout', () => {
|
||||
onPrev={() => {}}
|
||||
onNext={() => {}}
|
||||
onEditCurrent={() => {}}
|
||||
onReturnToTable={() => {}}
|
||||
formatTextViewValue={(value) => String(value)}
|
||||
/>,
|
||||
);
|
||||
@@ -1415,9 +1442,13 @@ describe('DataGrid layout', () => {
|
||||
expect(markup).toContain('align-items:center');
|
||||
expect(markup).toContain('min-height:var(--gonavi-header-min-height, 40px)');
|
||||
expect(markup).toContain('text-align:center');
|
||||
expect(markup).toContain('padding-inline:2');
|
||||
expect(markup).toContain('padding:0');
|
||||
expect(markup).toContain('vertical-align:middle');
|
||||
expect(markup).toContain('data-grid-row-number="true"');
|
||||
expect(markup).toContain('data-grid-row-number-action="true"');
|
||||
expect(markup).toContain('display:flex');
|
||||
expect(markup).toContain('width:100%');
|
||||
expect(markup).toContain('height:100%');
|
||||
expect(markup).toContain('width:36');
|
||||
// ant Table fixed 列会渲染 fix 相关 class
|
||||
expect(markup.includes('ant-table-cell-fix') || markup.includes('fixed')).toBe(true);
|
||||
|
||||
@@ -3322,6 +3322,10 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
return ROW_NUMBER_COLUMN_WIDTH;
|
||||
}, [columnWidths]);
|
||||
|
||||
const handleRowNumberDoubleClick = useCallback((index: number) => {
|
||||
handleViewModeChange('text', { textRecordIndex: index });
|
||||
}, [handleViewModeChange]);
|
||||
|
||||
const rowNumberColumn = useMemo<ColumnType<any>>(() => ({
|
||||
title: (
|
||||
<div
|
||||
@@ -3368,12 +3372,18 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
minWidth: 28,
|
||||
},
|
||||
}),
|
||||
onCell: () => ({
|
||||
onCell: (_record: Item, index?: number) => ({
|
||||
'data-grid-row-number-action': 'true',
|
||||
style: {
|
||||
width: rowNumberColumnWidth,
|
||||
minWidth: 28,
|
||||
paddingInline: 2,
|
||||
padding: 0,
|
||||
textAlign: 'center' as const,
|
||||
},
|
||||
onDoubleClick: (event: React.MouseEvent<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRowNumberDoubleClick(index ?? 0);
|
||||
},
|
||||
}),
|
||||
render: (_value: unknown, _record: Item, index: number) => {
|
||||
@@ -3381,12 +3391,31 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const pageSize = Math.max(1, Number(pagination?.pageSize) || 0);
|
||||
const offset = pageSize > 0 ? (currentPage - 1) * pageSize : 0;
|
||||
return (
|
||||
<span className="data-grid-row-number" data-grid-row-number="true">
|
||||
{offset + index + 1}
|
||||
</span>
|
||||
<Tooltip title={translateDataGrid('data_grid.row_number.double_click_to_view')}>
|
||||
<span
|
||||
className="data-grid-row-number"
|
||||
data-grid-row-number="true"
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRowNumberDoubleClick(index);
|
||||
}}
|
||||
>
|
||||
{offset + index + 1}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
}), [handleResizeAutoFit, handleResizeStart, pagination?.current, pagination?.pageSize, rowNumberColumnWidth]);
|
||||
}), [handleResizeAutoFit, handleResizeStart, handleRowNumberDoubleClick, pagination?.current, pagination?.pageSize, rowNumberColumnWidth, translateDataGrid]);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
const baseColumns = resolvedShowRowNumberColumn
|
||||
|
||||
105
frontend/src/components/DataGridRecordViews.interaction.test.tsx
Normal file
105
frontend/src/components/DataGridRecordViews.interaction.test.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { act, create } from 'react-test-renderer';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const messageApi = vi.hoisted(() => ({
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
Button: ({ children, ...props }: { children?: React.ReactNode }) => <button {...props}>{children}</button>,
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
message: messageApi,
|
||||
}));
|
||||
|
||||
vi.mock('./MonacoEditor', () => ({
|
||||
default: () => <div data-testid="record-view-editor" />,
|
||||
}));
|
||||
|
||||
import { DataGridTextView } from './DataGridRecordViews';
|
||||
|
||||
const translate = (key: string): string => ({
|
||||
'data_grid.record_view.empty': 'No rows',
|
||||
'data_grid.record_view.previous': 'Previous',
|
||||
'data_grid.record_view.next': 'Next',
|
||||
'data_grid.record_view.record_position': 'Record position',
|
||||
'data_grid.record_view.edit_current': 'Edit current',
|
||||
'data_grid.record_view.back_to_table': 'Back to table',
|
||||
'data_grid.record_view.field': 'Field',
|
||||
'data_grid.record_view.value': 'Value',
|
||||
'data_grid.record_view.comment': 'Comment',
|
||||
'data_grid.record_view.type': 'Type',
|
||||
'data_grid.record_view.copy_value': 'Copy value',
|
||||
'data_grid.message.copied_to_clipboard': 'Copied',
|
||||
'connection_modal.message.copy_failed': 'Copy failed',
|
||||
}[key] ?? key);
|
||||
|
||||
describe('DataGridTextView value copy', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('navigator', {
|
||||
clipboard: { writeText: vi.fn(() => Promise.resolve()) },
|
||||
});
|
||||
messageApi.error.mockReset();
|
||||
messageApi.success.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const renderView = () => create(
|
||||
<DataGridTextView
|
||||
darkMode={false}
|
||||
rowCount={1}
|
||||
textRecordIndex={0}
|
||||
canModifyData={false}
|
||||
currentTextRow={{ description: 'formatted value' }}
|
||||
displayOutputColumnNames={['description']}
|
||||
columnMetaMap={{ description: { type: 'text', comment: 'A long description' } }}
|
||||
columnMetaMapByLowerName={{}}
|
||||
translate={translate}
|
||||
onPrev={() => {}}
|
||||
onNext={() => {}}
|
||||
onEditCurrent={() => {}}
|
||||
onReturnToTable={() => {}}
|
||||
formatTextViewValue={(value) => String(value)}
|
||||
/>,
|
||||
);
|
||||
|
||||
it('copies a formatted value on click and keyboard activation', async () => {
|
||||
const renderer = renderView();
|
||||
const valueCell = renderer.root.find((node) => node.props['data-grid-text-value-copy'] === 'true');
|
||||
const writeText = navigator.clipboard.writeText as ReturnType<typeof vi.fn>;
|
||||
|
||||
await act(async () => {
|
||||
valueCell.props.onClick();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(writeText).toHaveBeenCalledWith('formatted value');
|
||||
expect(messageApi.success).toHaveBeenCalledWith('Copied');
|
||||
|
||||
const preventDefault = vi.fn();
|
||||
await act(async () => {
|
||||
valueCell.props.onKeyDown({ key: 'Enter', preventDefault });
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(writeText).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('shows the existing copy failure message when the clipboard rejects', async () => {
|
||||
const renderer = renderView();
|
||||
const valueCell = renderer.root.find((node) => node.props['data-grid-text-value-copy'] === 'true');
|
||||
const writeText = navigator.clipboard.writeText as ReturnType<typeof vi.fn>;
|
||||
writeText.mockRejectedValueOnce(new Error('clipboard unavailable'));
|
||||
|
||||
await act(async () => {
|
||||
valueCell.props.onClick();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(messageApi.error).toHaveBeenCalledWith('Copy failed');
|
||||
expect(messageApi.success).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { Button, message, Tooltip } from 'antd';
|
||||
import Editor from './MonacoEditor';
|
||||
import { t as defaultTranslate, type I18nParams } from '../i18n';
|
||||
|
||||
@@ -12,6 +12,7 @@ interface DataGridJsonViewProps {
|
||||
jsonViewText: string;
|
||||
translate?: DataGridRecordViewTranslate;
|
||||
onOpenJsonEditor: () => void;
|
||||
onReturnToTable: () => void;
|
||||
}
|
||||
|
||||
export const DataGridJsonView: React.FC<DataGridJsonViewProps> = ({
|
||||
@@ -21,6 +22,7 @@ export const DataGridJsonView: React.FC<DataGridJsonViewProps> = ({
|
||||
jsonViewText,
|
||||
translate = defaultTranslate,
|
||||
onOpenJsonEditor,
|
||||
onReturnToTable,
|
||||
}) => (
|
||||
<div style={{ height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ padding: '8px 10px', borderBottom: darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(0,0,0,0.08)', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
@@ -34,6 +36,9 @@ export const DataGridJsonView: React.FC<DataGridJsonViewProps> = ({
|
||||
{translate('data_grid.record_view.edit_json')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="small" onClick={onReturnToTable}>
|
||||
{translate('data_grid.record_view.back_to_table')}
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ flex: 1, minHeight: 0, padding: '8px 10px 10px 10px' }}>
|
||||
<Editor
|
||||
@@ -72,9 +77,72 @@ interface DataGridTextViewProps {
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
onEditCurrent: () => void;
|
||||
onReturnToTable: () => void;
|
||||
formatTextViewValue: (value: any, columnName?: string) => string;
|
||||
}
|
||||
|
||||
interface DataGridTextCellProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
'data-grid-text-view-cell'?: string;
|
||||
'data-grid-text-value-copy'?: string;
|
||||
}
|
||||
|
||||
interface DataGridTextValueCellProps extends DataGridTextCellProps {
|
||||
value: string;
|
||||
cellBaseStyle: React.CSSProperties;
|
||||
tooltipInnerStyle: React.CSSProperties;
|
||||
}
|
||||
|
||||
const DataGridTextValueCell: React.FC<DataGridTextValueCellProps> = ({
|
||||
value,
|
||||
cellBaseStyle,
|
||||
tooltipInnerStyle,
|
||||
...props
|
||||
}) => {
|
||||
const cellRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isTruncated, setIsTruncated] = React.useState(false);
|
||||
|
||||
const measureTruncation = React.useCallback(() => {
|
||||
const cell = cellRef.current;
|
||||
const nextIsTruncated = Boolean(value)
|
||||
&& Boolean(cell?.clientWidth)
|
||||
&& Boolean(cell && cell.scrollWidth > cell.clientWidth);
|
||||
setIsTruncated((previous) => (previous === nextIsTruncated ? previous : nextIsTruncated));
|
||||
}, [value]);
|
||||
|
||||
React.useEffect(() => {
|
||||
measureTruncation();
|
||||
const cell = cellRef.current;
|
||||
if (!cell || typeof ResizeObserver === 'undefined') return undefined;
|
||||
|
||||
const observer = new ResizeObserver(measureTruncation);
|
||||
observer.observe(cell);
|
||||
return () => observer.disconnect();
|
||||
}, [measureTruncation]);
|
||||
|
||||
const cell = (
|
||||
<div
|
||||
{...props}
|
||||
ref={cellRef}
|
||||
style={{ ...cellBaseStyle, ...props.style }}
|
||||
onMouseEnter={(event) => {
|
||||
measureTruncation();
|
||||
props.onMouseEnter?.(event);
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={isTruncated && value ? <span style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{value}</span> : undefined}
|
||||
overlayInnerStyle={tooltipInnerStyle}
|
||||
>
|
||||
{cell}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const DataGridTextView: React.FC<DataGridTextViewProps> = ({
|
||||
darkMode,
|
||||
rowCount,
|
||||
@@ -90,9 +158,51 @@ export const DataGridTextView: React.FC<DataGridTextViewProps> = ({
|
||||
onPrev,
|
||||
onNext,
|
||||
onEditCurrent,
|
||||
onReturnToTable,
|
||||
formatTextViewValue,
|
||||
}) => {
|
||||
const metaTextColor = darkMode ? 'rgba(255,255,255,0.52)' : 'rgba(0,0,0,0.48)';
|
||||
const primaryTextColor = darkMode ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.88)';
|
||||
const valueTextColor = darkMode ? 'rgba(255,255,255,0.88)' : 'rgba(0,0,0,0.88)';
|
||||
const gridTemplateColumns = '180px 140px 240px minmax(260px, 1fr)';
|
||||
const gridMinWidth = 820;
|
||||
const cellBaseStyle: React.CSSProperties = {
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
padding: '8px 10px',
|
||||
lineHeight: '20px',
|
||||
};
|
||||
const tooltipInnerStyle: React.CSSProperties = {
|
||||
maxWidth: 560,
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
};
|
||||
|
||||
const copyValue = React.useCallback(async (value: string) => {
|
||||
try {
|
||||
if (!navigator.clipboard?.writeText) throw new Error('Clipboard API unavailable');
|
||||
await navigator.clipboard.writeText(value);
|
||||
void message.success(translate('data_grid.message.copied_to_clipboard'));
|
||||
} catch {
|
||||
void message.error(translate('connection_modal.message.copy_failed'));
|
||||
}
|
||||
}, [translate]);
|
||||
|
||||
const renderCell = (
|
||||
value: string,
|
||||
style: React.CSSProperties,
|
||||
props?: DataGridTextCellProps,
|
||||
) => {
|
||||
return (
|
||||
<div {...props} style={{ ...cellBaseStyle, ...style }}>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
@@ -113,40 +223,89 @@ export const DataGridTextView: React.FC<DataGridTextViewProps> = ({
|
||||
{translate('data_grid.record_view.edit_current')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="small" onClick={onReturnToTable}>
|
||||
{translate('data_grid.record_view.back_to_table')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="custom-scrollbar" style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '8px 12px' }}>
|
||||
{currentTextRow ? displayOutputColumnNames.map((col) => {
|
||||
const columnMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
|
||||
const columnType = String(columnMeta?.type || '').trim();
|
||||
const columnComment = String(columnMeta?.comment || '').trim();
|
||||
|
||||
return (
|
||||
<div key={col} style={{ display: 'grid', gridTemplateColumns: '240px 1fr', gap: 10, padding: '6px 0', borderBottom: darkMode ? '1px solid rgba(255,255,255,0.06)' : '1px solid rgba(0,0,0,0.06)', alignItems: 'start' }}>
|
||||
<div style={{ wordBreak: 'break-all' }}>
|
||||
<div style={{ fontWeight: 600, color: darkMode ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.88)' }}>
|
||||
{col} :
|
||||
</div>
|
||||
{showColumnType && columnType && (
|
||||
<div style={{ marginTop: 3, fontSize: 11, lineHeight: 1.35, color: metaTextColor }}>
|
||||
{translate('data_grid.column.type_tooltip', { type: columnType })}
|
||||
</div>
|
||||
)}
|
||||
{showColumnComment && columnComment && (
|
||||
<div style={{ marginTop: 2, fontSize: 11, lineHeight: 1.35, color: metaTextColor }}>
|
||||
{translate('data_grid.column.comment_tooltip', { comment: columnComment })}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ minWidth: gridMinWidth }}>
|
||||
<div
|
||||
role="row"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns,
|
||||
borderBottom: darkMode ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.12)',
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.025)',
|
||||
}}
|
||||
>
|
||||
{[
|
||||
['field', 'data_grid.record_view.field'],
|
||||
['type', 'data_grid.record_view.type'],
|
||||
['comment', 'data_grid.record_view.comment'],
|
||||
['value', 'data_grid.record_view.value'],
|
||||
].map(([key, label]) => (
|
||||
<div
|
||||
key={key}
|
||||
role="columnheader"
|
||||
data-grid-text-view-header={key}
|
||||
style={{ ...cellBaseStyle, color: metaTextColor, fontWeight: 600 }}
|
||||
>
|
||||
{translate(label)}
|
||||
</div>
|
||||
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: darkMode ? 'rgba(255,255,255,0.88)' : 'rgba(0,0,0,0.88)' }}>
|
||||
{formatTextViewValue(currentTextRow[col], col)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}) : (
|
||||
<div style={{ fontSize: 12, color: darkMode ? '#999' : '#666', paddingTop: 4 }}>
|
||||
{translate('data_grid.record_view.empty')}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{currentTextRow ? displayOutputColumnNames.map((col) => {
|
||||
const columnMeta = columnMetaMap[col] || columnMetaMapByLowerName[col.toLowerCase()];
|
||||
const columnType = String(columnMeta?.type || '').trim();
|
||||
const columnComment = String(columnMeta?.comment || '').trim();
|
||||
const formattedValue = formatTextViewValue(currentTextRow[col], col);
|
||||
const borderBottom = darkMode ? '1px solid rgba(255,255,255,0.06)' : '1px solid rgba(0,0,0,0.06)';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={col}
|
||||
role="row"
|
||||
style={{ display: 'grid', gridTemplateColumns, borderBottom }}
|
||||
>
|
||||
{renderCell(col, { fontWeight: 600, color: primaryTextColor }, { 'data-grid-text-view-cell': 'field' })}
|
||||
{renderCell(
|
||||
showColumnType ? columnType : '',
|
||||
{ color: metaTextColor },
|
||||
{ 'data-grid-text-view-cell': 'type' },
|
||||
)}
|
||||
{renderCell(
|
||||
showColumnComment ? columnComment : '',
|
||||
{ color: metaTextColor },
|
||||
{ 'data-grid-text-view-cell': 'comment' },
|
||||
)}
|
||||
<DataGridTextValueCell
|
||||
value={formattedValue}
|
||||
cellBaseStyle={cellBaseStyle}
|
||||
tooltipInnerStyle={tooltipInnerStyle}
|
||||
style={{ color: valueTextColor, fontWeight: 400, cursor: 'copy' }}
|
||||
data-grid-text-view-cell="value"
|
||||
data-grid-text-value-copy="true"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={translate('data_grid.record_view.copy_value')}
|
||||
onClick={() => { void copyValue(formattedValue); }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
void copyValue(formattedValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}) : (
|
||||
<div
|
||||
style={{ ...cellBaseStyle, gridColumn: '1 / -1', color: darkMode ? '#999' : '#666' }}
|
||||
>
|
||||
{translate('data_grid.record_view.empty')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -852,6 +852,7 @@ const renderDataTableView = () => (
|
||||
canModifyData={canModifyData}
|
||||
jsonViewText={jsonViewText}
|
||||
translate={translateDataGrid}
|
||||
onReturnToTable={() => handleViewModeChange('table')}
|
||||
onOpenJsonEditor={handleOpenJsonEditor}
|
||||
/>
|
||||
) : (
|
||||
@@ -867,6 +868,7 @@ const renderDataTableView = () => (
|
||||
showColumnType={showColumnType}
|
||||
showColumnComment={showColumnComment}
|
||||
translate={translateDataGrid}
|
||||
onReturnToTable={() => handleViewModeChange('table')}
|
||||
onPrev={() => setTextRecordIndex((i: number) => Math.max(0, i - 1))}
|
||||
onNext={() => setTextRecordIndex((i: number) => Math.min(textViewRows.length - 1, i + 1))}
|
||||
onEditCurrent={openCurrentViewRowEditor}
|
||||
|
||||
@@ -386,6 +386,14 @@ export const buildDataGridCssText = ({
|
||||
.${gridId} .data-grid-row-number-cell {
|
||||
text-align: center !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.data-grid-row-number-cell,
|
||||
.${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell.data-grid-row-number-cell,
|
||||
.${gridId} .ant-table-header th.data-grid-row-number-cell,
|
||||
body[data-ui-version="v2"] .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.data-grid-row-number-cell,
|
||||
body[data-ui-version="v2"] .${gridId}.gn-v2-data-grid .ant-table-tbody .ant-table-row > .ant-table-cell.data-grid-row-number-cell {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.${gridId} .ant-table-thead > tr:first-child > th:first-child,
|
||||
|
||||
|
||||
@@ -99,4 +99,24 @@ describe('useDataGridDdlView i18n', () => {
|
||||
|
||||
expect(messageApi.error).toHaveBeenCalledWith('ORA-31603: object "USERS" not found');
|
||||
});
|
||||
|
||||
it('honors an explicit text row when opening from the row number', () => {
|
||||
const setTextRecordIndex = vi.fn();
|
||||
renderHook({
|
||||
selectedRowKeys: ['row-10'],
|
||||
mergedDisplayDataRef: {
|
||||
current: [
|
||||
{ __gonavi_row_key__: 'row-8' },
|
||||
{ __gonavi_row_key__: 'row-10' },
|
||||
],
|
||||
},
|
||||
setTextRecordIndex,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
controller?.handleViewModeChange('text', { textRecordIndex: 0 });
|
||||
});
|
||||
|
||||
expect(setTextRecordIndex).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@ export interface UseDataGridDdlViewResult {
|
||||
ddlRequestSeqRef: React.MutableRefObject<number>;
|
||||
isTableSurfaceActive: boolean;
|
||||
handleOpenTableDdl: (options?: { asView?: boolean }) => Promise<void>;
|
||||
handleViewModeChange: (nextMode: GridViewMode) => void;
|
||||
handleViewModeChange: (nextMode: GridViewMode, options?: { textRecordIndex?: number }) => void;
|
||||
handleDdlSidebarResizeStart: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
resetDdlViewState: () => void;
|
||||
closeDdlView: () => void;
|
||||
@@ -291,7 +291,7 @@ export const useDataGridDdlView = ({
|
||||
void handleOpenTableDdl({ asView: true });
|
||||
}, [canRestoreSharedDdlView, ddlContextKey, handleOpenTableDdl, hasPendingLocalTableViewRequest, isActive]);
|
||||
|
||||
const handleViewModeChange = React.useCallback((nextMode: GridViewMode) => {
|
||||
const handleViewModeChange = React.useCallback((nextMode: GridViewMode, options?: { textRecordIndex?: number }) => {
|
||||
setSuppressSharedDdlView(false);
|
||||
if ((nextMode === 'fields' || nextMode === 'ddl' || nextMode === 'er' || nextMode === 'sqlLog') && !isV2Ui) {
|
||||
setSharedDdlViewOpen(false);
|
||||
@@ -317,11 +317,17 @@ export const useDataGridDdlView = ({
|
||||
}
|
||||
|
||||
if (nextMode === 'text') {
|
||||
const selectedKey = selectedRowKeys[0];
|
||||
if (selectedKey !== undefined) {
|
||||
const idx = mergedDisplayDataRef.current.findIndex((row) => rowKeyStr(row?.__gonavi_row_key__) === rowKeyStr(selectedKey));
|
||||
if (idx >= 0) {
|
||||
setTextRecordIndex(idx);
|
||||
const explicitIndex = options?.textRecordIndex;
|
||||
if (explicitIndex !== undefined && Number.isFinite(explicitIndex)) {
|
||||
const maxIndex = Math.max(0, mergedDisplayDataRef.current.length - 1);
|
||||
setTextRecordIndex(Math.max(0, Math.min(Math.trunc(explicitIndex), maxIndex)));
|
||||
} else {
|
||||
const selectedKey = selectedRowKeys[0];
|
||||
if (selectedKey !== undefined) {
|
||||
const idx = mergedDisplayDataRef.current.findIndex((row) => rowKeyStr(row?.__gonavi_row_key__) === rowKeyStr(selectedKey));
|
||||
if (idx >= 0) {
|
||||
setTextRecordIndex(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +558,11 @@ describe("i18n catalog", () => {
|
||||
"data_grid.record_view.empty",
|
||||
"data_grid.record_view.json_record_count",
|
||||
"data_grid.record_view.edit_json",
|
||||
"data_grid.record_view.field",
|
||||
"data_grid.record_view.value",
|
||||
"data_grid.record_view.comment",
|
||||
"data_grid.record_view.type",
|
||||
"data_grid.record_view.copy_value",
|
||||
"data_grid.record_view.previous",
|
||||
"data_grid.record_view.next",
|
||||
"data_grid.record_view.record_position",
|
||||
|
||||
Reference in New Issue
Block a user