mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 01:03:51 +08:00
✨ feat(datagrid): 支持按单元格选区删除记录
- 统一单元格选区与复选框行的删除目标和计数,按记录行去重处理 - 保留复选框优先级,新增行直接移除,已有记录进入待提交删除 - 隔离页内查找高亮,并在刷新、数据重载或上下文切换时清理旧选区 - 补充选区删除、刷新保护、查找隔离和新增行回归测试
This commit is contained in:
@@ -6,10 +6,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import DataGrid, {
|
||||
attachDataGridVirtualEditRenderVersion,
|
||||
buildDataGridCommitChangeSet,
|
||||
collectDataGridCellSelectionRowKeys,
|
||||
GONAVI_ROW_KEY,
|
||||
hasDataGridVirtualEditRenderVersionChanged,
|
||||
} from './DataGrid';
|
||||
import { resetDataGridDdlViewSharedStateForTests } from './useDataGridDdlView';
|
||||
import DataGridPageFind from './DataGridPageFind';
|
||||
import DataGridToolbarFrame from './DataGridToolbarFrame';
|
||||
import { V2CellContextMenuView, V2ColumnHeaderContextMenuView, V2TableGroupContextMenuView } from './V2TableContextMenu';
|
||||
import { setCurrentLanguage, t } from '../i18n';
|
||||
@@ -512,6 +514,21 @@ const commitColumnGuard = (columnName: string) => (
|
||||
columnName !== GONAVI_ROW_KEY && columnName !== ORACLE_ROWID_LOCATOR_COLUMN
|
||||
);
|
||||
|
||||
describe('DataGrid cell selection row keys', () => {
|
||||
it('deduplicates every record covered by a rectangular cell selection', () => {
|
||||
const cellKeys = Array.from({ length: 6 }, (_, rowIndex) => (
|
||||
['id', 'user_id', 'app_key'].map((columnName) => `row-${rowIndex + 1}\u0001${columnName}`)
|
||||
)).flat();
|
||||
|
||||
expect(collectDataGridCellSelectionRowKeys([
|
||||
...cellKeys,
|
||||
'row-3\u0001id',
|
||||
'malformed-cell-key',
|
||||
'\u0001empty-row-key',
|
||||
])).toEqual(['row-1', 'row-2', 'row-3', 'row-4', 'row-5', 'row-6']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataGrid commit change set', () => {
|
||||
it('uses unique locator values instead of falling back to the whole row', () => {
|
||||
const result = buildDataGridCommitChangeSet({
|
||||
@@ -1136,6 +1153,250 @@ describe('DataGrid DDL interactions', () => {
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('deletes every record represented by a cell-only column selection', async () => {
|
||||
messageApi.info.mockResolvedValue(undefined);
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
<DataGrid
|
||||
data={[
|
||||
{ __gonavi_row_key__: 'row-1', id: 1, name: 'Ada' },
|
||||
{ __gonavi_row_key__: 'row-2', id: 2, name: 'Linus' },
|
||||
]}
|
||||
columnNames={['id', 'name']}
|
||||
loading={false}
|
||||
tableName="users"
|
||||
dbName="main"
|
||||
connectionId="conn-1"
|
||||
pkColumns={['id']}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
await act(async () => {
|
||||
renderer!.root.findByType(DataGridToolbarFrame).props.onToggleCellEditMode();
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
const nameColumn = testRenderState.latestColumns.find((column) => column.key === 'name');
|
||||
expect(nameColumn?.editable).toBe(true);
|
||||
const headerProps = nameColumn.onHeaderCell(nameColumn);
|
||||
await act(async () => {
|
||||
headerProps.onClickCapture({
|
||||
target: { closest: vi.fn(() => null) },
|
||||
currentTarget: { querySelector: vi.fn(() => null) },
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
let toolbar = renderer!.root.findByType(DataGridToolbarFrame);
|
||||
expect(toolbar.props.selectedRowKeysLength).toBe(0);
|
||||
expect(toolbar.props.selectedCellsSize).toBe(2);
|
||||
expect(toolbar.props.deleteTargetRowCount).toBe(2);
|
||||
expect(findButton(renderer!, t('data_grid.toolbar.delete_selected')).props.disabled).toBeFalsy();
|
||||
|
||||
await act(async () => {
|
||||
toolbar.props.onRefresh();
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
toolbar = renderer!.root.findByType(DataGridToolbarFrame);
|
||||
expect(toolbar.props.selectedCellsSize).toBe(0);
|
||||
expect(toolbar.props.deleteTargetRowCount).toBe(0);
|
||||
expect(findButton(renderer!, t('data_grid.toolbar.delete_selected')).props.disabled).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
headerProps.onClickCapture({
|
||||
target: { closest: vi.fn(() => null) },
|
||||
currentTarget: { querySelector: vi.fn(() => null) },
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
findButton(renderer!, t('data_grid.toolbar.delete_selected')).props.onClick();
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
toolbar = renderer!.root.findByType(DataGridToolbarFrame);
|
||||
expect(toolbar.props.pendingChangeCount).toBe(2);
|
||||
expect(toolbar.props.selectedCellsSize).toBe(0);
|
||||
expect(toolbar.props.deleteTargetRowCount).toBe(0);
|
||||
expect(findButton(renderer!, t('data_grid.toolbar.delete_selected')).props.disabled).toBe(true);
|
||||
expect(
|
||||
testRenderState.latestTableProps.dataSource.map((row: Record<string, unknown>) => (
|
||||
testRenderState.latestTableProps.rowClassName(row)
|
||||
)),
|
||||
).toEqual(['row-deleted', 'row-deleted']);
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('does not treat page-find highlighting as a deletable cell selection', async () => {
|
||||
messageApi.info.mockResolvedValue(undefined);
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
<DataGrid
|
||||
data={[
|
||||
{ __gonavi_row_key__: 'row-1', id: 1, name: 'Ada' },
|
||||
{ __gonavi_row_key__: 'row-2', id: 2, name: 'Linus' },
|
||||
]}
|
||||
columnNames={['id', 'name']}
|
||||
loading={false}
|
||||
tableName="users"
|
||||
dbName="main"
|
||||
connectionId="conn-1"
|
||||
pkColumns={['id']}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
await act(async () => {
|
||||
renderer!.root.findByType(DataGridToolbarFrame).props.onToggleCellEditMode();
|
||||
});
|
||||
await act(async () => {
|
||||
renderer!.root.findByType(DataGridPageFind).props.onPageFindTextChange('Ada');
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
const pageFind = renderer!.root.findByType(DataGridPageFind);
|
||||
expect(pageFind.props.matchCount).toBeGreaterThan(0);
|
||||
await act(async () => {
|
||||
pageFind.props.onNavigateNext();
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
const toolbar = renderer!.root.findByType(DataGridToolbarFrame);
|
||||
expect(toolbar.props.cellEditMode).toBe(true);
|
||||
expect(toolbar.props.selectedCellsSize).toBe(1);
|
||||
expect(toolbar.props.deleteTargetRowCount).toBe(0);
|
||||
expect(findButton(renderer!, t('data_grid.toolbar.delete_selected')).props.disabled).toBe(true);
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('keeps checkbox-selected rows as the delete target when a cell selection also exists', async () => {
|
||||
messageApi.info.mockResolvedValue(undefined);
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
<DataGrid
|
||||
data={[
|
||||
{ __gonavi_row_key__: 'row-1', id: 1, name: 'Ada' },
|
||||
{ __gonavi_row_key__: 'row-2', id: 2, name: 'Linus' },
|
||||
]}
|
||||
columnNames={['id', 'name']}
|
||||
loading={false}
|
||||
tableName="users"
|
||||
dbName="main"
|
||||
connectionId="conn-1"
|
||||
pkColumns={['id']}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
await act(async () => {
|
||||
renderer!.root.findByType(DataGridToolbarFrame).props.onToggleCellEditMode();
|
||||
});
|
||||
const nameColumn = testRenderState.latestColumns.find((column) => column.key === 'name');
|
||||
const headerProps = nameColumn.onHeaderCell(nameColumn);
|
||||
await act(async () => {
|
||||
headerProps.onClickCapture({
|
||||
target: { closest: vi.fn(() => null) },
|
||||
currentTarget: { querySelector: vi.fn(() => null) },
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
testRenderState.latestTableProps.rowSelection.onChange(['row-1']);
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
let toolbar = renderer!.root.findByType(DataGridToolbarFrame);
|
||||
expect(toolbar.props.selectedCellsSize).toBe(2);
|
||||
expect(toolbar.props.selectedRowKeysLength).toBe(1);
|
||||
expect(toolbar.props.deleteTargetRowCount).toBe(1);
|
||||
|
||||
await act(async () => {
|
||||
toolbar.props.onDeleteSelected();
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
toolbar = renderer!.root.findByType(DataGridToolbarFrame);
|
||||
expect(toolbar.props.pendingChangeCount).toBe(1);
|
||||
expect(toolbar.props.selectedCellsSize).toBe(0);
|
||||
expect(
|
||||
testRenderState.latestTableProps.dataSource.map((row: Record<string, unknown>) => (
|
||||
testRenderState.latestTableProps.rowClassName(row)
|
||||
)),
|
||||
).toEqual(['row-deleted', '']);
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('removes a newly added record selected only through its cells without leaving a pending delete', async () => {
|
||||
messageApi.info.mockResolvedValue(undefined);
|
||||
let renderer: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
<DataGrid
|
||||
data={[]}
|
||||
columnNames={['id', 'name']}
|
||||
loading={false}
|
||||
tableName="users"
|
||||
dbName="main"
|
||||
connectionId="conn-1"
|
||||
pkColumns={['id']}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
await act(async () => {
|
||||
renderer!.root.findByType(DataGridToolbarFrame).props.onAddRow();
|
||||
});
|
||||
await waitForEffects();
|
||||
expect(testRenderState.latestTableProps.dataSource).toHaveLength(1);
|
||||
expect(renderer!.root.findByType(DataGridToolbarFrame).props.pendingChangeCount).toBe(1);
|
||||
|
||||
await act(async () => {
|
||||
renderer!.root.findByType(DataGridToolbarFrame).props.onToggleCellEditMode();
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
const nameColumn = testRenderState.latestColumns.find((column) => column.key === 'name');
|
||||
expect(nameColumn?.editable).toBe(true);
|
||||
const headerProps = nameColumn.onHeaderCell(nameColumn);
|
||||
await act(async () => {
|
||||
headerProps.onClickCapture({
|
||||
target: { closest: vi.fn(() => null) },
|
||||
currentTarget: { querySelector: vi.fn(() => null) },
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
let toolbar = renderer!.root.findByType(DataGridToolbarFrame);
|
||||
expect(toolbar.props.selectedRowKeysLength).toBe(0);
|
||||
expect(toolbar.props.selectedCellsSize).toBe(1);
|
||||
expect(toolbar.props.deleteTargetRowCount).toBe(1);
|
||||
|
||||
await act(async () => {
|
||||
findButton(renderer!, t('data_grid.toolbar.delete_selected')).props.onClick();
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
toolbar = renderer!.root.findByType(DataGridToolbarFrame);
|
||||
expect(testRenderState.latestTableProps.dataSource).toHaveLength(0);
|
||||
expect(toolbar.props.pendingChangeCount).toBe(0);
|
||||
expect(toolbar.props.selectedCellsSize).toBe(0);
|
||||
expect(toolbar.props.deleteTargetRowCount).toBe(0);
|
||||
expect(findButton(renderer!, t('data_grid.toolbar.delete_selected')).props.disabled).toBe(true);
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
it('allows sorter arrow clicks through while cell edit mode is active', async () => {
|
||||
messageApi.info.mockResolvedValue(undefined);
|
||||
const onSort = vi.fn();
|
||||
|
||||
@@ -1299,7 +1299,7 @@ describe('DataGrid layout', () => {
|
||||
expect(toolbarFrameSource).toContain(`translate('${key}`);
|
||||
});
|
||||
[
|
||||
/translate\('data_grid\.toolbar\.selected_count', \{ count: selectedRowKeysLength \}\)/,
|
||||
/translate\('data_grid\.toolbar\.selected_count', \{ count: deleteTargetRowCount \}\)/,
|
||||
/translate\('data_grid\.toolbar\.copy_selection', \{ count: selectedCellsSize \}\)/,
|
||||
/translate\('data_grid\.toolbar\.copy_selection_columns', \{ count: selectedCellsSize \}\)/,
|
||||
/translate\('data_grid\.toolbar\.batch_fill', \{ count: selectedCellsSize \}\)/,
|
||||
|
||||
@@ -190,6 +190,7 @@ import {
|
||||
useDataGridI18nLanguage,
|
||||
makeCellKey,
|
||||
splitCellKey,
|
||||
collectDataGridCellSelectionRowKeys,
|
||||
resolveContextMenuFieldName,
|
||||
trimSimpleCache,
|
||||
looksLikeDateTimeText,
|
||||
@@ -289,6 +290,7 @@ export {
|
||||
resolveNextGridFilterOperatorForColumnChange,
|
||||
buildGridFieldSelectOptions,
|
||||
buildDataGridCommitChangeSet,
|
||||
collectDataGridCellSelectionRowKeys,
|
||||
} from './DataGridCore';
|
||||
|
||||
// Native scroll events can outlive a pointer gesture on macOS. Wait for a brief
|
||||
@@ -875,6 +877,8 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
// 批量编辑模式状态
|
||||
const [cellEditMode, setCellEditMode] = useState(false);
|
||||
const [selectedCells, setSelectedCells] = useState<Set<string>>(new Set());
|
||||
const [cellSelectionDeleteEligible, setCellSelectionDeleteEligible] = useState(false);
|
||||
const cellSelectionSourceDataRef = useRef<Item[] | null>(null);
|
||||
const [copiedCellPatch, setCopiedCellPatch] = useState<{ sourceRowKey: string; values: Record<string, any> } | null>(null);
|
||||
const [copiedRowsForPaste, setCopiedRowsForPaste] = useState<Array<Record<string, any>>>([]);
|
||||
|
||||
@@ -1664,10 +1668,16 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const markCellSelectionDeleteEligible = useCallback((eligible: boolean) => {
|
||||
cellSelectionSourceDataRef.current = eligible ? data : null;
|
||||
setCellSelectionDeleteEligible(eligible);
|
||||
}, [data]);
|
||||
|
||||
const resetCellSelection = useCallback((clearState: boolean = true) => {
|
||||
if (clearState) {
|
||||
setSelectedCells(new Set());
|
||||
}
|
||||
markCellSelectionDeleteEligible(false);
|
||||
currentSelectionRef.current = new Set();
|
||||
selectionStartRef.current = null;
|
||||
pendingCellSelectionStartRef.current = null;
|
||||
@@ -1686,7 +1696,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
cellSelectionAutoScrollRafRef.current = null;
|
||||
}
|
||||
updateCellSelection(new Set());
|
||||
}, [updateCellSelection]);
|
||||
}, [markCellSelectionDeleteEligible, updateCellSelection]);
|
||||
|
||||
const closeCellEditMode = useCallback(() => {
|
||||
setCellEditMode(false);
|
||||
@@ -1713,8 +1723,17 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
});
|
||||
currentSelectionRef.current = nextSelection;
|
||||
setSelectedCells(nextSelection);
|
||||
markCellSelectionDeleteEligible(true);
|
||||
updateCellSelection(nextSelection);
|
||||
}, [canModifyData, effectiveEditLocator, makeCellKey, resetCellSelection, rowKeyStr, updateCellSelection]);
|
||||
}, [canModifyData, effectiveEditLocator, makeCellKey, markCellSelectionDeleteEligible, resetCellSelection, rowKeyStr, updateCellSelection]);
|
||||
|
||||
const previousSelectionSourceDataRef = useRef(data);
|
||||
useEffect(() => {
|
||||
if (previousSelectionSourceDataRef.current === data) return;
|
||||
previousSelectionSourceDataRef.current = data;
|
||||
setSelectedRowKeys([]);
|
||||
resetCellSelection();
|
||||
}, [data, resetCellSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
closeCellEditModeRef.current = closeCellEditMode;
|
||||
@@ -1766,6 +1785,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setCopiedCellPatch,
|
||||
setModifiedRows,
|
||||
setSelectedCells,
|
||||
markCellSelectionDeleteEligible,
|
||||
splitCellKey,
|
||||
suppressCellSelectionClickRef,
|
||||
translateDataGrid,
|
||||
@@ -1812,10 +1832,25 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setAutoCommitRemainingSeconds(null);
|
||||
}, []);
|
||||
|
||||
const selectedCellRowKeys = useMemo(
|
||||
() => cellEditMode
|
||||
&& cellSelectionDeleteEligible
|
||||
&& cellSelectionSourceDataRef.current === data
|
||||
? collectDataGridCellSelectionRowKeys(selectedCells)
|
||||
: [],
|
||||
[cellEditMode, cellSelectionDeleteEligible, data, selectedCells],
|
||||
);
|
||||
const deleteTargetRowKeys = useMemo(
|
||||
() => selectedRowKeys.length > 0
|
||||
? selectedRowKeys.map(key => rowKeyStr(key))
|
||||
: selectedCellRowKeys,
|
||||
[rowKeyStr, selectedCellRowKeys, selectedRowKeys],
|
||||
);
|
||||
const deleteTargetRowCount = deleteTargetRowKeys.length;
|
||||
const allSelectedAreDeleted = useMemo(() => {
|
||||
if (selectedRowKeys.length === 0) return false;
|
||||
return selectedRowKeys.every(key => deletedRowKeys.has(rowKeyStr(key)));
|
||||
}, [selectedRowKeys, deletedRowKeys, rowKeyStr]);
|
||||
if (deleteTargetRowKeys.length === 0) return false;
|
||||
return deleteTargetRowKeys.every(key => deletedRowKeys.has(key));
|
||||
}, [deleteTargetRowKeys, deletedRowKeys]);
|
||||
|
||||
const addedRowKeySet = useMemo(() => {
|
||||
const next = new Set<string>();
|
||||
@@ -2136,6 +2171,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setDeletedRowKeys(new Set());
|
||||
setModifiedColumns({});
|
||||
setSelectedRowKeys([]);
|
||||
resetCellSelection();
|
||||
setCopiedCellPatch(null);
|
||||
setCopiedRowsForPaste([]);
|
||||
closeRowEditor();
|
||||
@@ -2159,6 +2195,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
currentConnConfig,
|
||||
dataSourceContextKey,
|
||||
isV2Ui,
|
||||
resetCellSelection,
|
||||
resetDdlViewState,
|
||||
resolvedDdlTableName,
|
||||
viewMode,
|
||||
@@ -3055,8 +3092,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const handleDeleteSelected = () => {
|
||||
const addedKeysToRemove: string[] = [];
|
||||
const baseKeysToDelete: string[] = [];
|
||||
for (const key of selectedRowKeys) {
|
||||
const keyStr = rowKeyStr(key);
|
||||
for (const keyStr of deleteTargetRowKeys) {
|
||||
if (addedRowKeySet.has(keyStr)) {
|
||||
addedKeysToRemove.push(keyStr);
|
||||
} else if (!deletedRowKeys.has(keyStr)) {
|
||||
@@ -3079,15 +3115,17 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
});
|
||||
}
|
||||
setSelectedRowKeys([]);
|
||||
if (cellEditMode) resetCellSelection();
|
||||
};
|
||||
|
||||
const handleUndoDeleteSelected = () => {
|
||||
setDeletedRowKeys(prev => {
|
||||
const newDeleted = new Set(prev);
|
||||
selectedRowKeys.forEach(key => newDeleted.delete(rowKeyStr(key)));
|
||||
deleteTargetRowKeys.forEach(key => newDeleted.delete(key));
|
||||
return newDeleted;
|
||||
});
|
||||
setSelectedRowKeys([]);
|
||||
if (cellEditMode) resetCellSelection();
|
||||
};
|
||||
|
||||
const handlePreviewChanges = useCallback(async () => {
|
||||
@@ -3914,6 +3952,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const focusPageFindMatch = useCallback((match: DataGridFindMatch) => {
|
||||
if (!match) return;
|
||||
const nextSelection = new Set([makeCellKey(match.rowKey, match.columnName)]);
|
||||
markCellSelectionDeleteEligible(false);
|
||||
setSelectedCells(nextSelection);
|
||||
currentSelectionRef.current = nextSelection;
|
||||
selectionStartRef.current = {
|
||||
@@ -4003,7 +4042,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
applyVisibleFocus();
|
||||
});
|
||||
});
|
||||
}, [applyVirtualHorizontalOffset, enableVirtual, mergedDisplayData, pickVerticalScrollTarget, readVirtualHorizontalOffset, rowKeyStr, updateCellSelection, updateFocusedCell]);
|
||||
}, [applyVirtualHorizontalOffset, enableVirtual, markCellSelectionDeleteEligible, mergedDisplayData, pickVerticalScrollTarget, readVirtualHorizontalOffset, rowKeyStr, updateCellSelection, updateFocusedCell]);
|
||||
|
||||
const handleNavigatePageFind = useCallback((direction: DataGridFindNavigationDirection) => {
|
||||
const nextIndex = resolveDataGridFindNavigationIndex(activePageFindMatchIndex, pageFindMatches.length, direction);
|
||||
@@ -4952,6 +4991,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
dataContextValue,
|
||||
dataEditAutoCommitDelayMs,
|
||||
dataEditCommitMode,
|
||||
deleteTargetRowCount,
|
||||
dataPanelDirtyRef,
|
||||
dataPanelIsJson,
|
||||
dataPanelOpen,
|
||||
|
||||
@@ -241,6 +241,15 @@ const splitCellKey = (cellKey: string): { rowKey: string; colName: string } | nu
|
||||
colName: cellKey.slice(sepIndex + CELL_KEY_SEP.length),
|
||||
};
|
||||
};
|
||||
const collectDataGridCellSelectionRowKeys = (cellKeys: Iterable<string>): string[] => {
|
||||
const rowKeys = new Set<string>();
|
||||
for (const cellKey of cellKeys) {
|
||||
const parsed = splitCellKey(cellKey);
|
||||
if (!parsed || !parsed.rowKey) continue;
|
||||
rowKeys.add(parsed.rowKey);
|
||||
}
|
||||
return Array.from(rowKeys);
|
||||
};
|
||||
export const resolveContextMenuFieldName = (dataIndex: string, title?: string): string => {
|
||||
const name = String(dataIndex || title || '').trim();
|
||||
return name;
|
||||
@@ -1661,6 +1670,7 @@ export {
|
||||
useDataGridI18nLanguage,
|
||||
makeCellKey,
|
||||
splitCellKey,
|
||||
collectDataGridCellSelectionRowKeys,
|
||||
trimSimpleCache,
|
||||
looksLikeDateTimeText,
|
||||
normalizeDateTimeString,
|
||||
|
||||
@@ -111,6 +111,7 @@ const DataGridShell: React.FC<DataGridShellProps> = (props) => {
|
||||
dataContextValue,
|
||||
dataEditAutoCommitDelayMs,
|
||||
dataEditCommitMode,
|
||||
deleteTargetRowCount,
|
||||
dataPanelDirtyRef,
|
||||
dataPanelIsJson,
|
||||
dataPanelOpen,
|
||||
@@ -508,6 +509,7 @@ const renderDataTableView = () => (
|
||||
|
||||
const handleRefreshGrid = useCallback(() => {
|
||||
setSelectedRowKeys([]);
|
||||
resetCellSelection();
|
||||
const normalizedTableName = String(tableName || '').trim();
|
||||
const normalizedDbName = String(dbName || '').trim();
|
||||
if (connectionId && normalizedTableName) {
|
||||
@@ -518,7 +520,7 @@ const renderDataTableView = () => (
|
||||
setMetadataReloadVersion((value: number) => value + 1);
|
||||
}
|
||||
if (onReload) onReload();
|
||||
}, [connectionId, dbName, onReload, tableName]);
|
||||
}, [connectionId, dbName, onReload, resetCellSelection, tableName]);
|
||||
|
||||
const handleResetPendingChanges = useCallback(() => {
|
||||
clearAutoCommitTimer();
|
||||
@@ -588,6 +590,7 @@ const renderDataTableView = () => (
|
||||
onToggleFilter={onToggleFilter}
|
||||
canModifyData={canModifyData}
|
||||
selectedRowKeysLength={selectedRowKeys.length}
|
||||
deleteTargetRowCount={deleteTargetRowCount}
|
||||
allSelectedAreDeleted={allSelectedAreDeleted}
|
||||
cellEditMode={cellEditMode}
|
||||
selectedCellsSize={selectedCells.size}
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface DataGridToolbarFrameProps {
|
||||
onToggleFilter?: () => void;
|
||||
canModifyData: boolean;
|
||||
selectedRowKeysLength: number;
|
||||
deleteTargetRowCount: number;
|
||||
allSelectedAreDeleted: boolean;
|
||||
cellEditMode: boolean;
|
||||
selectedCellsSize: number;
|
||||
@@ -161,6 +162,7 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
|
||||
onToggleFilter,
|
||||
canModifyData,
|
||||
selectedRowKeysLength,
|
||||
deleteTargetRowCount,
|
||||
allSelectedAreDeleted,
|
||||
cellEditMode,
|
||||
selectedCellsSize,
|
||||
@@ -317,11 +319,11 @@ const DataGridToolbarFrame: React.FC<DataGridToolbarFrameProps> = ({
|
||||
{renderToolbarDivider()}
|
||||
<Button icon={<PlusOutlined />} onClick={onAddRow}>{translate('data_grid.toolbar.add_row')}</Button>
|
||||
{allSelectedAreDeleted ? (
|
||||
<Button icon={<UndoOutlined />} disabled={selectedRowKeysLength === 0} onClick={onUndoDeleteSelected}>{translate('data_grid.toolbar.undo_delete')}</Button>
|
||||
<Button icon={<UndoOutlined />} disabled={deleteTargetRowCount === 0} onClick={onUndoDeleteSelected}>{translate('data_grid.toolbar.undo_delete')}</Button>
|
||||
) : (
|
||||
<Button icon={<DeleteOutlined />} danger disabled={selectedRowKeysLength === 0} onClick={onDeleteSelected}>{translate('data_grid.toolbar.delete_selected')}</Button>
|
||||
<Button icon={<DeleteOutlined />} danger disabled={deleteTargetRowCount === 0} onClick={onDeleteSelected}>{translate('data_grid.toolbar.delete_selected')}</Button>
|
||||
)}
|
||||
{selectedRowKeysLength > 0 && <span style={{ fontSize: '12px', color: '#888' }}>{translate('data_grid.toolbar.selected_count', { count: selectedRowKeysLength })}</span>}
|
||||
{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"
|
||||
|
||||
@@ -43,6 +43,7 @@ type DataGridBatchActionsContext = Record<string, any> & {
|
||||
>;
|
||||
setModifiedRows: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
setSelectedCells: React.Dispatch<React.SetStateAction<Set<string>>>;
|
||||
markCellSelectionDeleteEligible: (eligible: boolean) => void;
|
||||
rowKeyStr: (key: React.Key) => string;
|
||||
makeCellKey: (rowKey: string, colName: string) => string;
|
||||
splitCellKey: (cellKey: string) => { rowKey: string; colName: string } | null;
|
||||
@@ -93,6 +94,7 @@ export const useDataGridBatchActions = (ctx: DataGridBatchActionsContext) => {
|
||||
setCopiedCellPatch,
|
||||
setModifiedRows,
|
||||
setSelectedCells,
|
||||
markCellSelectionDeleteEligible,
|
||||
splitCellKey,
|
||||
suppressCellSelectionClickRef,
|
||||
translateDataGrid,
|
||||
@@ -188,6 +190,7 @@ const handleBatchFillCells = useCallback(() => {
|
||||
|
||||
// 清除选中状态
|
||||
setSelectedCells(new Set());
|
||||
markCellSelectionDeleteEligible(false);
|
||||
currentSelectionRef.current = new Set();
|
||||
selectionStartRef.current = null;
|
||||
isDraggingRef.current = false;
|
||||
@@ -197,7 +200,7 @@ const handleBatchFillCells = useCallback(() => {
|
||||
cellSelectionAutoScrollRafRef.current = null;
|
||||
}
|
||||
updateCellSelection(new Set());
|
||||
}, [batchEditValue, batchEditSetNull, addedRows, modifiedRows, rowKeyStr, updateCellSelection, closeBatchEditModal, translateDataGrid]);
|
||||
}, [batchEditValue, batchEditSetNull, addedRows, modifiedRows, rowKeyStr, updateCellSelection, closeBatchEditModal, markCellSelectionDeleteEligible, translateDataGrid]);
|
||||
|
||||
// 事件委托:在容器级别处理单元格拖选;未开启模式时,拖拽超过阈值会自动进入单元格编辑模式。
|
||||
useEffect(() => {
|
||||
@@ -443,6 +446,7 @@ const handleBatchFillCells = useCallback(() => {
|
||||
|
||||
if (currentSelectionRef.current.size > 0) {
|
||||
setSelectedCells(new Set(currentSelectionRef.current));
|
||||
markCellSelectionDeleteEligible(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -489,7 +493,7 @@ const handleBatchFillCells = useCallback(() => {
|
||||
cellSelectionPointerRef.current = null;
|
||||
isDraggingRef.current = false;
|
||||
};
|
||||
}, [canModifyData, isTableSurfaceActive, displayColumnNames, columnIndexMap, effectiveEditLocator, updateCellSelection]);
|
||||
}, [canModifyData, isTableSurfaceActive, displayColumnNames, columnIndexMap, effectiveEditLocator, markCellSelectionDeleteEligible, updateCellSelection]);
|
||||
|
||||
const handleCopySelectedColumnsFromRow = useCallback(() => {
|
||||
const activeSelection = currentSelectionRef.current.size > 0 ? currentSelectionRef.current : selectedCells;
|
||||
|
||||
Reference in New Issue
Block a user