mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-16 19:49:51 +08:00
✨ feat(datagrid): 为表格编辑增加单元格级撤销能力
- 在 V2 与旧版单元格菜单中增加撤销当前修改入口 - 复用现有改单保存链路回退单元格值与脏标记 - 修复刷新后本地改单状态未完全清理的问题 - 补充相关布局与菜单回归测试 Close #563
This commit is contained in:
@@ -11,6 +11,7 @@ import DataGrid, {
|
||||
resolveNextGridFilterOperatorForColumnChange,
|
||||
} from './DataGrid';
|
||||
import DataGridPaginationBar from './DataGridPaginationBar';
|
||||
import { V2CellContextMenuView } from './V2TableContextMenu';
|
||||
import { cloneShortcutOptions, DEFAULT_SHORTCUT_OPTIONS } from '../utils/shortcuts';
|
||||
|
||||
vi.mock('../store', () => ({
|
||||
@@ -280,6 +281,26 @@ describe('DataGrid layout', () => {
|
||||
expect(markup).toContain('AI 洞察');
|
||||
});
|
||||
|
||||
it('clears modified cell markers when refreshing the grid', () => {
|
||||
const source = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
|
||||
|
||||
expect(source).toMatch(/const handleRefreshGrid = useCallback\(\(\) => \{[\s\S]*setModifiedColumns\(\{\}\);[\s\S]*if \(onReload\) onReload\(\);[\s\S]*\}, \[clearAutoCommitTimer, onReload\]\);/);
|
||||
});
|
||||
|
||||
it('renders a cell-level undo action in the v2 context menu for modified cells', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<V2CellContextMenuView
|
||||
fieldName="status"
|
||||
tableName="orders"
|
||||
rowLabel="row 1"
|
||||
canModifyData
|
||||
canUndoCellChange
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('撤销此单元格修改');
|
||||
});
|
||||
|
||||
it('preserves fractional seconds when rendering datetime values', () => {
|
||||
expect(formatCellDisplayText('2026-05-10T09:12:33.456+08:00')).toBe('2026-05-10 09:12:33.456');
|
||||
});
|
||||
|
||||
@@ -4339,6 +4339,45 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
}, [cellContextMenu, handleCellSave, effectiveEditLocator]);
|
||||
|
||||
const canUndoContextMenuCellChange = useMemo(() => {
|
||||
const record = cellContextMenu.record;
|
||||
const dataIndex = String(cellContextMenu.dataIndex || '').trim();
|
||||
const rowKey = record?.[GONAVI_ROW_KEY];
|
||||
if (!record || !dataIndex || rowKey === undefined || rowKey === null) return false;
|
||||
const keyStr = rowKeyStr(rowKey);
|
||||
if (addedRowKeySet.has(keyStr)) return false;
|
||||
return !!modifiedColumns[keyStr]?.has(dataIndex);
|
||||
}, [addedRowKeySet, cellContextMenu.dataIndex, cellContextMenu.record, modifiedColumns, rowKeyStr]);
|
||||
|
||||
const handleUndoContextMenuCellChange = useCallback(() => {
|
||||
const record = cellContextMenu.record;
|
||||
const dataIndex = String(cellContextMenu.dataIndex || '').trim();
|
||||
const rowKey = record?.[GONAVI_ROW_KEY];
|
||||
if (!record || !dataIndex || rowKey === undefined || rowKey === null) return;
|
||||
|
||||
const keyStr = rowKeyStr(rowKey);
|
||||
if (addedRowKeySet.has(keyStr)) {
|
||||
void message.info('新增行请使用删除选中或整表回滚撤销');
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
return;
|
||||
}
|
||||
if (!modifiedColumns[keyStr]?.has(dataIndex)) {
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
const originalRow = data.find((row) => rowKeyStr(row?.[GONAVI_ROW_KEY]) === keyStr);
|
||||
if (!originalRow) {
|
||||
void message.error('未找到该单元格的原始数据,无法撤销');
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
handleCellSave({ ...record, [dataIndex]: originalRow[dataIndex] });
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
void message.success('已撤销单元格修改');
|
||||
}, [addedRowKeySet, cellContextMenu.dataIndex, cellContextMenu.record, data, handleCellSave, modifiedColumns, rowKeyStr]);
|
||||
|
||||
const handleCellEditorSave = useCallback(() => {
|
||||
if (!cellEditorMeta) return;
|
||||
if (!isWritableResultColumn(cellEditorMeta.dataIndex, effectiveEditLocator)) {
|
||||
@@ -6060,6 +6099,9 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
handleCopyColumnData(cellContextMenu.dataIndex);
|
||||
closeMenu();
|
||||
return;
|
||||
case 'undo-cell-change':
|
||||
handleUndoContextMenuCellChange();
|
||||
return;
|
||||
case 'set-null':
|
||||
handleCellSetNull();
|
||||
return;
|
||||
@@ -6128,6 +6170,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
getTargets,
|
||||
handleBatchFillToSelected,
|
||||
handleCellSetNull,
|
||||
handleUndoContextMenuCellChange,
|
||||
handleCopyContextMenuFieldName,
|
||||
handleCopyCsv,
|
||||
handleCopyDelete,
|
||||
@@ -7465,6 +7508,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setAddedRows([]);
|
||||
setModifiedRows({});
|
||||
setDeletedRowKeys(new Set());
|
||||
setModifiedColumns({});
|
||||
setSelectedRowKeys([]);
|
||||
if (onReload) onReload();
|
||||
}, [clearAutoCommitTimer, onReload]);
|
||||
@@ -7832,6 +7876,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
rowLabel={cellContextMenu.record?.[GONAVI_ROW_KEY] === undefined ? undefined : `row ${String(cellContextMenu.record?.[GONAVI_ROW_KEY])}`}
|
||||
selectedRowCount={selectedRowKeys.length}
|
||||
canModifyData={canModifyData}
|
||||
canUndoCellChange={canUndoContextMenuCellChange}
|
||||
copiedRowCount={copiedRowsForPaste.length}
|
||||
canPasteCopiedColumns={!!copiedCellPatch}
|
||||
supportsCopyInsert={supportsCopyInsert}
|
||||
@@ -7851,6 +7896,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
copiedRowsForPasteLength={copiedRowsForPaste.length}
|
||||
selectedRowKeysLength={selectedRowKeys.length}
|
||||
copiedCellPatchAvailable={!!copiedCellPatch}
|
||||
canUndoCellChange={canUndoContextMenuCellChange}
|
||||
supportsCopyInsert={supportsCopyInsert}
|
||||
onClose={() => setCellContextMenu(prev => ({ ...prev, visible: false }))}
|
||||
onCopyFieldName={handleCopyContextMenuFieldName}
|
||||
@@ -7867,6 +7913,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
copyRowsForPaste([rowKey]);
|
||||
}}
|
||||
onPasteCopiedRowsAsNew={handlePasteCopiedRowsAsNew}
|
||||
onUndoCellChange={handleUndoContextMenuCellChange}
|
||||
onSetNull={handleCellSetNull}
|
||||
onEditRow={handleOpenContextMenuRowEditor}
|
||||
onFillToSelected={() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { CopyOutlined, EditOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons';
|
||||
import { CopyOutlined, EditOutlined, UndoOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons';
|
||||
|
||||
interface CellContextMenuState {
|
||||
visible: boolean;
|
||||
@@ -19,12 +19,14 @@ interface DataGridLegacyCellContextMenuProps {
|
||||
copiedRowsForPasteLength: number;
|
||||
selectedRowKeysLength: number;
|
||||
copiedCellPatchAvailable: boolean;
|
||||
canUndoCellChange: boolean;
|
||||
supportsCopyInsert: boolean;
|
||||
onClose: () => void;
|
||||
onCopyFieldName: () => void;
|
||||
onCopyRowData: () => void;
|
||||
onCopyRowForPaste: () => void;
|
||||
onPasteCopiedRowsAsNew: () => void;
|
||||
onUndoCellChange: () => void;
|
||||
onSetNull: () => void;
|
||||
onEditRow: () => void;
|
||||
onFillToSelected: () => void;
|
||||
@@ -62,12 +64,14 @@ const DataGridLegacyCellContextMenu: React.FC<DataGridLegacyCellContextMenuProps
|
||||
copiedRowsForPasteLength,
|
||||
selectedRowKeysLength,
|
||||
copiedCellPatchAvailable,
|
||||
canUndoCellChange,
|
||||
supportsCopyInsert,
|
||||
onClose,
|
||||
onCopyFieldName,
|
||||
onCopyRowData,
|
||||
onCopyRowForPaste,
|
||||
onPasteCopiedRowsAsNew,
|
||||
onUndoCellChange,
|
||||
onSetNull,
|
||||
onEditRow,
|
||||
onFillToSelected,
|
||||
@@ -131,6 +135,22 @@ const DataGridLegacyCellContextMenu: React.FC<DataGridLegacyCellContextMenuProps
|
||||
<div style={separatorStyle(darkMode)} />
|
||||
{canModifyData && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
...baseItemStyle,
|
||||
cursor: canUndoCellChange ? 'pointer' : 'not-allowed',
|
||||
opacity: canUndoCellChange ? 1 : 0.5,
|
||||
}}
|
||||
{...makeHoverHandlers(canUndoCellChange)}
|
||||
onClick={() => {
|
||||
if (canUndoCellChange) {
|
||||
onUndoCellChange();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<UndoOutlined style={{ marginRight: 8 }} />
|
||||
撤销此单元格修改
|
||||
</div>
|
||||
<div style={baseItemStyle} {...makeHoverHandlers()} onClick={onSetNull}>
|
||||
设置为 NULL
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
FolderAddOutlined,
|
||||
HddOutlined,
|
||||
PushpinOutlined,
|
||||
UndoOutlined,
|
||||
SortAscendingOutlined,
|
||||
SortDescendingOutlined,
|
||||
VerticalAlignBottomOutlined,
|
||||
@@ -543,6 +544,7 @@ export type V2CellContextMenuActionKey =
|
||||
| 'copy-row-for-paste'
|
||||
| 'paste-row-as-new'
|
||||
| 'copy-column-data'
|
||||
| 'undo-cell-change'
|
||||
| 'set-null'
|
||||
| 'edit-row'
|
||||
| 'fill-selected'
|
||||
@@ -652,6 +654,7 @@ export const V2CellContextMenuView: React.FC<{
|
||||
rowLabel?: string;
|
||||
selectedRowCount?: number;
|
||||
canModifyData?: boolean;
|
||||
canUndoCellChange?: boolean;
|
||||
copiedRowCount?: number;
|
||||
canPasteCopiedColumns?: boolean;
|
||||
supportsCopyInsert?: boolean;
|
||||
@@ -663,6 +666,7 @@ export const V2CellContextMenuView: React.FC<{
|
||||
rowLabel,
|
||||
selectedRowCount = 0,
|
||||
canModifyData = false,
|
||||
canUndoCellChange = false,
|
||||
copiedRowCount = 0,
|
||||
canPasteCopiedColumns = false,
|
||||
supportsCopyInsert = true,
|
||||
@@ -694,6 +698,12 @@ export const V2CellContextMenuView: React.FC<{
|
||||
<>
|
||||
<div className="gn-v2-context-menu-section-title">编辑</div>
|
||||
{renderItems([
|
||||
{
|
||||
action: 'undo-cell-change',
|
||||
icon: <UndoOutlined />,
|
||||
title: '撤销此单元格修改',
|
||||
disabled: !canUndoCellChange,
|
||||
},
|
||||
{ action: 'set-null', icon: <ClearOutlined />, title: '设置为 NULL' },
|
||||
{ action: 'edit-row', icon: <EditOutlined />, title: '编辑本行', kbd: '↵' },
|
||||
{ action: 'copy-row-for-paste', icon: <CopyOutlined />, title: '复制本行为新增行' },
|
||||
|
||||
Reference in New Issue
Block a user