mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 17:23:48 +08:00
## 关联 Issue Closes #781 ## 问题根因 表格拖选会在 `currentSelectionRef` 中保存完整目标选区,但剪贴板粘贴逻辑仅使用 `selectionStartRef` 作为锚点,并按剪贴板二维矩阵生成目标坐标。单个复制值解析为 `1 x 1` 矩阵,因此只更新了锚点单元格,没有使用其余已选单元格。 ## 修复方案 - 当剪贴板内容为 `1 x 1` 且当前选区超过一个单元格时,将单值映射到选区中的每个目标坐标。 - 粘贴时根据当前显示行和列重新解析选区坐标,避免排序后使用过期位置。 - 继续复用现有只读列过滤、草稿合并、新增行、删除行和原值比较逻辑。 - 多行或多列剪贴板仍保持原有的锚点二维矩阵粘贴行为,不引入重复铺贴或跨页填充语义。 ## 验证结果 | 验证项 | 命令或步骤 | 结果 | | --- | --- | --- | | 修复前回归 | `npm --prefix frontend test -- src/components/useDataGridBatchActions.test.tsx` | 新增用例按预期失败:实际仅更新 `row-1.id`,期望两行四格 | | 回归测试 | `npm --prefix frontend test -- src/components/useDataGridBatchActions.test.tsx` | 通过,5/5 | | 剪贴板相关测试 | `npm --prefix frontend test -- src/components/dataGridClipboardPaste.test.ts src/components/dataGridSelectionCopy.test.ts src/components/useDataGridBatchActions.test.tsx` | 通过,3 个文件、13/13 | | 类型与生产构建 | `npm --prefix frontend run build` | 通过,`tsc && vite build` 成功 | | 差异检查 | `git diff --cached --check` | 通过 | 前端全量测试 `npm --prefix frontend test` 执行结果为 420 个文件通过、1 个文件失败,3481 项测试通过、1 项失败。唯一失败是 `src/testPolicy.test.ts` 检测到 `upstream/dev` 已存在的 `AIMessageCodeBlock.dependencyBoundary.test.ts` 读取源码文本;本 PR 未修改相关文件。该上游既有错误将通过独立 PR 修复,避免混入本 Issue。 GUI 验证未完成:本地 Vite 服务已正常启动,但浏览器自动化宿主连续返回 `browser guest not attached (webview not ready)`,没有创建可操作页面或截图证据。未将该项记为通过。 ## 风险与兼容性 - 不涉及数据或配置格式变化、数据库迁移或公共 API 变化。 - 仅改变 `1 x 1` 剪贴板内容在多格选区中的行为;现有二维矩阵粘贴由相关回归测试覆盖并保持不变。 - 操作仍只影响当前已加载并选中的可写单元格;只读列、已删除行和未变化值会沿用现有规则跳过。 - 更新规模随当前选区大小线性增长;没有新增网络请求、并发路径或跨页扫描。 - 验证边界:自动化测试覆盖了前端草稿状态与消息计数,未在真实 Wails/MySQL 环境提交数据库变更。 ## 回滚方式 回滚提交 `ec6b4e9e0182a85b862627177c8d373137d1a90b` 即可恢复原有单锚点粘贴逻辑。该提交不改变数据或配置格式,回滚本身不需要数据迁移。
This commit is contained in:
@@ -23,6 +23,7 @@ export const buildDataGridClipboardPasteRows = ({
|
||||
columnNames,
|
||||
startRowIndex,
|
||||
startColumnIndex,
|
||||
targetCells,
|
||||
rowKeyField,
|
||||
addedRowKeys,
|
||||
modifiedRows,
|
||||
@@ -35,6 +36,7 @@ export const buildDataGridClipboardPasteRows = ({
|
||||
columnNames: string[];
|
||||
startRowIndex: number;
|
||||
startColumnIndex: number;
|
||||
targetCells?: Array<{ rowIndex: number; columnIndex: number }>;
|
||||
rowKeyField: string;
|
||||
addedRowKeys: Set<string>;
|
||||
modifiedRows: Record<string, any>;
|
||||
@@ -46,11 +48,28 @@ export const buildDataGridClipboardPasteRows = ({
|
||||
return { rows: [], updatedCellCount: 0 };
|
||||
}
|
||||
|
||||
const valuesByRowIndex = new Map<number, Array<{ columnIndex: number; value: DataGridClipboardValue }>>();
|
||||
const appendValue = (rowIndex: number, columnIndex: number, value: DataGridClipboardValue) => {
|
||||
const rowValues = valuesByRowIndex.get(rowIndex) || [];
|
||||
rowValues.push({ columnIndex, value });
|
||||
valuesByRowIndex.set(rowIndex, rowValues);
|
||||
};
|
||||
|
||||
if (targetCells && matrix.length === 1 && matrix[0]?.length === 1) {
|
||||
targetCells.forEach(({ rowIndex, columnIndex }) => appendValue(rowIndex, columnIndex, matrix[0][0]));
|
||||
} else {
|
||||
matrix.forEach((sourceValues, sourceRowIndex) => {
|
||||
sourceValues.forEach((value, sourceColumnIndex) => {
|
||||
appendValue(startRowIndex + sourceRowIndex, startColumnIndex + sourceColumnIndex, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const pasteRows: DataGridClipboardPasteRow[] = [];
|
||||
let updatedCellCount = 0;
|
||||
|
||||
matrix.forEach((sourceValues, sourceRowIndex) => {
|
||||
const baseRow = rows[startRowIndex + sourceRowIndex];
|
||||
valuesByRowIndex.forEach((targetValues, targetRowIndex) => {
|
||||
const baseRow = rows[targetRowIndex];
|
||||
const rowKeyValue = baseRow?.[rowKeyField];
|
||||
if (rowKeyValue === undefined || rowKeyValue === null) return;
|
||||
|
||||
@@ -61,11 +80,11 @@ export const buildDataGridClipboardPasteRows = ({
|
||||
const currentRow = addedRowKeys.has(rowKey) ? baseRow : { ...baseRow, ...existing };
|
||||
const values: Record<string, DataGridClipboardValue> = {};
|
||||
|
||||
sourceValues.forEach((nextValue, sourceColumnIndex) => {
|
||||
const columnName = columnNames[startColumnIndex + sourceColumnIndex];
|
||||
targetValues.forEach(({ columnIndex, value }) => {
|
||||
const columnName = columnNames[columnIndex];
|
||||
if (!columnName || !isWritableColumn(columnName)) return;
|
||||
if (isValueEqual(currentRow?.[columnName], nextValue)) return;
|
||||
values[columnName] = nextValue;
|
||||
if (isValueEqual(currentRow?.[columnName], value)) return;
|
||||
values[columnName] = value;
|
||||
updatedCellCount += 1;
|
||||
});
|
||||
|
||||
|
||||
@@ -212,6 +212,37 @@ describe('useDataGridBatchActions clipboard paste', () => {
|
||||
expect(messageApi.success).toHaveBeenCalledWith('data_grid.message.pasted_columns_to_rows:{"rows":2,"cells":4}');
|
||||
});
|
||||
|
||||
it('fills the selected cells when pasting a single value', () => {
|
||||
const hook = renderHook();
|
||||
const cell = selectCell(hook.container, 'row-1', 'id');
|
||||
hook.currentSelectionRef.current = new Set([
|
||||
makeCellKey('row-1', 'id'),
|
||||
makeCellKey('row-1', 'name'),
|
||||
makeCellKey('row-2', 'id'),
|
||||
makeCellKey('row-2', 'name'),
|
||||
]);
|
||||
|
||||
const preventDefault = vi.fn();
|
||||
act(() => {
|
||||
(windowTarget.listeners.get('paste') as any)?.({
|
||||
target: cell,
|
||||
clipboardData: { types: ['text/plain'], getData: vi.fn(() => 'filled') },
|
||||
preventDefault,
|
||||
});
|
||||
});
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledOnce();
|
||||
const nextRows = hook.setModifiedRows.mock.calls[0][0]({});
|
||||
expect(nextRows).toEqual({
|
||||
'row-1': { id: 'filled', name: 'filled' },
|
||||
'row-2': { id: 'filled', name: 'filled' },
|
||||
});
|
||||
const nextColumns = hook.setModifiedColumns.mock.calls[0][0]({});
|
||||
expect(nextColumns['row-1']).toEqual(new Set(['id', 'name']));
|
||||
expect(nextColumns['row-2']).toEqual(new Set(['id', 'name']));
|
||||
expect(messageApi.success).toHaveBeenCalledWith('data_grid.message.pasted_columns_to_rows:{"rows":2,"cells":4}');
|
||||
});
|
||||
|
||||
it('resolves the selected row and column again before pasting', () => {
|
||||
const hook = renderHook();
|
||||
const cell = selectCell(hook.container, 'row-2', 'name');
|
||||
|
||||
@@ -531,6 +531,23 @@ const handleBatchFillCells = useCallback(() => {
|
||||
const startColumnIndex = columnIndexMap.get(start.colName) ?? -1;
|
||||
if (startRowIndex === -1 || startColumnIndex === -1) return;
|
||||
|
||||
let targetCells: Array<{ rowIndex: number; columnIndex: number }> | undefined;
|
||||
if (matrix.length === 1 && matrix[0]?.length === 1 && currentSelectionRef.current.size > 1) {
|
||||
const rowIndexes = new Map<string, number>();
|
||||
currentRows.forEach((row, rowIndex) => {
|
||||
const key = row?.[GONAVI_ROW_KEY];
|
||||
if (key !== undefined && key !== null) rowIndexes.set(rowKeyStr(key), rowIndex);
|
||||
});
|
||||
const selectedTargets = Array.from(currentSelectionRef.current).flatMap((cellKey) => {
|
||||
const cell = splitCellKey(cellKey);
|
||||
if (!cell) return [];
|
||||
const rowIndex = rowIndexes.get(cell.rowKey);
|
||||
const columnIndex = columnIndexMap.get(cell.colName);
|
||||
return rowIndex === undefined || columnIndex === undefined ? [] : [{ rowIndex, columnIndex }];
|
||||
});
|
||||
if (selectedTargets.length > 1) targetCells = selectedTargets;
|
||||
}
|
||||
|
||||
const addedRowKeys = new Set<string>();
|
||||
addedRows.forEach((row) => {
|
||||
const key = row?.[GONAVI_ROW_KEY];
|
||||
@@ -542,6 +559,7 @@ const handleBatchFillCells = useCallback(() => {
|
||||
columnNames: displayColumnNames,
|
||||
startRowIndex,
|
||||
startColumnIndex,
|
||||
targetCells,
|
||||
rowKeyField: GONAVI_ROW_KEY,
|
||||
addedRowKeys,
|
||||
modifiedRows,
|
||||
@@ -615,7 +633,7 @@ const handleBatchFillCells = useCallback(() => {
|
||||
cellSelectionPointerRef.current = null;
|
||||
isDraggingRef.current = false;
|
||||
};
|
||||
}, [addedRows, canModifyData, deletedRowKeys, isActive, isTableSurfaceActive, displayColumnNames, columnIndexMap, effectiveEditLocator, isCellValueEqualForDiff, isWritableResultColumn, markCellSelectionDeleteEligible, modifiedRows, rowKeyStr, setAddedRows, setModifiedColumns, setModifiedRows, setSelectedCells, translateDataGrid, updateCellSelection]);
|
||||
}, [addedRows, canModifyData, deletedRowKeys, isActive, isTableSurfaceActive, displayColumnNames, columnIndexMap, effectiveEditLocator, isCellValueEqualForDiff, isWritableResultColumn, markCellSelectionDeleteEligible, modifiedRows, rowKeyStr, setAddedRows, setModifiedColumns, setModifiedRows, setSelectedCells, splitCellKey, translateDataGrid, updateCellSelection]);
|
||||
|
||||
const handleCopySelectedColumnsFromRow = useCallback(() => {
|
||||
const activeSelection = currentSelectionRef.current.size > 0 ? currentSelectionRef.current : selectedCells;
|
||||
|
||||
Reference in New Issue
Block a user