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:
Syngnat
2026-08-02 00:50:33 +08:00
committed by GitHub
15 changed files with 454 additions and 47 deletions

View File

@@ -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);

View File

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

View 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();
});
});

View File

@@ -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>
);

View File

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

View File

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

View File

@@ -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);
});
});

View File

@@ -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);
}
}
}
}

View File

@@ -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",