Skip to content

Conversation

@SychevAndrey
Copy link
Contributor

@SychevAndrey SychevAndrey commented Jan 26, 2026

Screen.Recording.2026-01-28.at.15.37.39.mov

@SychevAndrey SychevAndrey self-assigned this Jan 27, 2026
@SychevAndrey SychevAndrey marked this pull request as ready for review January 28, 2026 11:45
SychevAndrey and others added 6 commits January 28, 2026 12:48
That functionality comes in handy when we want to use Undo and Replace All functions together:
Before that commit we used to perform N operations where N is a number of replaced cells. And history would record them as atomic operations, so pressing Undo would revert only the last replace.
setMany enables batch editing and batch reverting, so now we can Replace All and then revert it by using one Undo operation
Copy link
Contributor

@sergeyteleshev sergeyteleshev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All in all feature looks awesome! Nice integration with undo/redo btw

Please also consider cases with readonly tables (probably block replace functionality with disabled state) - Session Manager, readonly tables, tables where we cannot edit cells, but can add/delete rows

Also, keep in mind that if we replace 1 item. We want to scroll and focus this item because it may be outside the visible view, and we need to show the user that this value has changed. For "replace all" the view should remain the same (no selection, no scroll to cell)

Some of the issues can be resolved in the next tickets. We need to decide what we can do now and what later

Comment on lines +63 to +73
for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
for (let colIdx = 0; colIdx < columnCount; colIdx++) {
const cellText = getCellText(rowIdx, colIdx);
if (searchPattern.test(cellText)) {
matches.push({ rowIdx, colIdx });
if (searchPattern.global) {
searchPattern.lastIndex = 0;
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well for big tables this would just crash the app due to performance issues in the worst case scenario. for most cases it would be like a freeze

we need something tricky here. maybe batch search in new thread (service worker)

I suppose not a bad idea to sync with the team how should we handle that so we do not dig into this real hard

also we have react-minisearch lib for searching. maybe it can offer something for us. we need to check the API for ideas

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image this simple solution still shows decent results even on huge tables. I would take it to separate ticket for further exploration.

I tried to use Web Worker for that calculations, but the bottleneck here is the getCellText which can't be called somewhere else. Batching, serializing/deserializing, orchestration adds too much overhead and didn't show any significant improvements. Actually, for me it even for worse

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We’ll need to come up with a solution anyway. Users with large datasets will definitely run into issues, but we can address performance problems on large datasets in a separate ticket. The main thing for now is that the new functionality should not affect users who don’t use the search and replace feature

Comment on lines 183 to 204
@@ -189,13 +199,20 @@ export const DataGridTable = observer<IDataPresentationProps>(function DataGridT
return;
}

if (dataGridRef.current?.isReplacing()) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should revert that new behavior

As user I want to see all changes focused. For example we have huge table and our change is out of the screen right now. Currently if I replace 1 value I wont see what was replaced cause it was not scrolled or selected this cell. so we can miss something important

Comment on lines +63 to +73
for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
for (let colIdx = 0; colIdx < columnCount; colIdx++) {
const cellText = getCellText(rowIdx, colIdx);
if (searchPattern.test(cellText)) {
matches.push({ rowIdx, colIdx });
if (searchPattern.global) {
searchPattern.lastIndex = 0;
}
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We’ll need to come up with a solution anyway. Users with large datasets will definitely run into issues, but we can address performance problems on large datasets in a separate ticket. The main thing for now is that the new functionality should not affect users who don’t use the search and replace feature

}

/** Replace pattern in cell text. Returns new text and whether pattern still matches. */
static replaceInCell(cellText: string, pattern: RegExp, replaceValue: string): { newText: string; stillMatches: boolean } {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we choose to use static methods and class approach in general here? Seems like it could be 3 functions instead

});

useEffect(() => {
onCellClassNameChange(getCellClassName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is confusing for me. We are reacting to the fn reference change here? Could you describe what its used for please

onCellKeyDown={handleCellKeyDown}
onColumnWidthsChange={setColumnWidths}
/>
{searchOpen && (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use <Activity /> here to preserve state and improve perfomance

return () => el.removeEventListener('keydown', handleKeyDown);
}, []);

function handleSearchOpen() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move all search related logic to a separate hook? so we dont implement it all in the entry file?

Comment on lines +82 to +84
if (pattern.global) {
pattern.lastIndex = 0;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it safe to mutate object from the argument?

const cached = searchStorage?.get();
if (cached) {
searchStorage?.set({ ...cached, open: false });
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move to the internal hook

if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
setSearchOpen(true);
setTimeout(() => searchPanelRef.current?.focus(), 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setTimeout(() => searchPanelRef.current?.focus(), 0);
isn't it better to handle this logic inside search panel component in render layout hook?


function handleSearchOpen() {
setSearchOpen(true);
setTimeout(() => searchPanelRef.current?.focus(), 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

setSearchOpen(false);
const cached = searchStorage?.get();
if (cached) {
searchStorage?.set({ ...cached, open: false });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can make update method that will accept partial state for update?

Comment on lines +26 to +27
replaceCellValue: (rowIdx: number, colIdx: number, value: string) => void;
replaceCellValues?: (updates: ICellValueUpdate[]) => void;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onReplace
let's keep only one and rename it

Comment on lines +57 to +60
useEffect(() => {
onCellClassNameChange(getCellClassName);
return () => onCellClassNameChange(undefined);
}, [getCellClassName, onCellClassNameChange]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you better to expose getCellClassName via useImperativeHandle

onEditorOpen?: (position: ICellPosition) => void;
onCellKeyDown?: (position: ICellPosition, event: DataGridCellKeyboardEvent) => void;
className?: string;
searchReadOnly?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need searchReadOnly? we have getCellEditable

Comment on lines +48 to +56
recordBatchCellEdit(data: IGridHistoryBatchCellUpdateData<TKey, TCell>): void {
this.compressLastEditedCellHistory();

this.history.add({
source: GRID_HISTORY_SOURCE.BATCH_EDIT_CELLS,
data,
});
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's rework GRID_HISTORY_SOURCE.EDIT_CELL to accept array of changes, there is no need in the multiple event types for every type of change

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moreover, batch is not an a special event, any event can be batched, so it usually solved on the event structure side or event emmiter

Comment on lines +77 to +92
const transformedUpdates = updates.map(({ key, value }) => {
const [update] = this.getOrCreateUpdate(key.row, DatabaseEditChangeType.update);
const prevValue = update.source?.[key.column.index] as any;

if (isResultSetContentValue(prevValue) && !isResultSetComplexValue(value)) {
if ('text' in prevValue && !isNull(value)) {
value = createResultSetContentValue({
text: String(value),
contentLength: String(value).length,
contentType: prevValue.contentType ?? 'text/plain',
});
}
}

return { key, value };
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please avoid logic duplication, this is very important to have same code for set and setMany, make a private method to transform data

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants