Files
cloudbeaver/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridEditAction.ts
T
296e03ec74 7023-cb-find-and-replace-new (#4085)
* dbeaver/pro#7023 feat: add SearchPanel component and related styles to UI-kit
add chevron Icon to UI-kit (used in Search Panel)

* dbeaver/pro#7023 feat: add SearchPanel story

* dbeaver/pro#7023 feat: update SearchPanel integration in CodeMirror

* dbeaver/pro#7023 feat: add onReplaceToggle prop to SearchPanel for toggle functionality

* dbeaver/pro#7023 fix: replace-input styles

* dbeaver/pro#7023 feat: implement search functionality in DataGrid v2

* dbeaver/pro#7023 fix: update react-data-grid workspace reference in yarn.lock

* dbeaver/pro#7023 feat: don't focus cell on replace

* dbeaver/pro#7023 feat: add searchReadOnly prop to DataGrid and GridSearchPanel components

* dbeaver/pro#7023 fix: update styles for search panel

* dbeaver/pro#7023 feat: add theme variables for improved styling in storybook

* dbeaver/pro#7023 feat: update search panel styles in datagrid

* dbeaver/pro#7023 feat: improve search functionality by resetting active match index and triggering search on find actions

* dbeaver/pro#7023 refactor: useGridSearch

* dbeaver/pro#7023 fix: search panel styles

* dbeaver/pro#7023 feat: implement grid search state management

* dbeaver/pro#7023 feat: add refresh functionality to search components

* dbeaver/pro#7023 feat: add history action to refresh search results on undo/redo

* dbeaver/pro#7023 feat: add batch cell update functionality to data grid

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

* dbeaver/pro#7023 refactor: remove unnecessary eslint disable comment in useGridSearch

* dbeaver/pro#7023 feat: select affected cell on history change

* dbeaver/pro#7023 feat: add searchReadOnly prop to DataGridTable for conditional read-only state

* dbeaver/pro#7023 refactor: simplify isReadOnly logic in GridSearchPanel

* dbeaver/pro#7023 refactor: use constants for flags

* dbeaver/pro#7023 refactor: update focus and refresh methods in GridSearchPanel

* dbeaver/pro#7023 refactor: consolidate grid search state update logic

* dbeaver/pro#7023 refactor: update scrollToCell method signature and related usages

* dbeaver/pro#7023 refactor: update handleCellChangeBatch to use ICellChange type

* dbeaver/pro#7023 refactor: remove unwanted code

* dbeaver/pro#7023 feat: refactor search functionality to use standalone functions

* dbeaver/pro#7023 feat: implement useDataGridSearch hook to avoid bloating the  DataGrid component with the search-related logic

* dbeaver/pro#7023 feat: update search functionality to use onReplace for cell value updates

* dbeaver/pro#7023 feat: use Activity component for search panel visibility in DataGrid

* dbeaver/pro#7023 refactor: use common value transformation fn in set and setMany methods

* dbeaver/pro#7023 refactor: rename methods for "unnkeyed" cache

* dbeaver/pro#7023 refactor: unify cell edit history handling and remove batch edit references

* dbeaver/pro#7023 fix: update searchReadOnly logic

* dbeaver/pro#7023 refactor: replace rowsCountRef with useState (Error: Cannot access refs during render)

* dbeaver/pro#7023 refactor: expose getCellClassName via useImperativeHandle

Replaced onCellClassNameChange prop + useEffect with useImperativeHandle as suggested in code review. Using useState setter directly as callback ref either caused infinite re-renders (without deps) or lost context synchronization entirely (with stable deps). Settled on useCallback ref that sets both useRef (for method access) and useState (for context propagation).

* dbeaver/pro#7023 fix: Cmd+F not re-focusing the search panel when already open

* dbeaver/pro#7023 refactor:  cell edit history compression

* dbeaver/pro#7023 fix: refresh search state on revert

* dbeaver/pro#7023 refactor: move focus blocking during replace into grid component

Previously DataGridTable checked isReplacing() before syncing
focus/selection, leaking grid internals
Now the grid's selectCell blocks during replacing internally and
supports deferred mode (setTimeout + rAF) for post-render focus sync.

* dbeaver/pro#7023 refactor: restructure grid search logic with useReducer and improve state management

* dbeaver/pro#7023 refactor: improve grid search

moved ref synchronization out of render,
unified search+scroll into one shared path,
added a SET_MATCHES no-op guard to reduce unnecessary updates,
removed duplicate open=false persistence from useDataGridSearch (single owner now in useGridSearch).

* dbeaver/pro#7023 refactor: update search state types

* dbeaver/pro#7023 refactor: restructure grid search state interfaces for improved clarity

* dbeaver/pro#7023 refactor: optimize match retrieval logic after removal in grid search

* dbeaver/pro#7023 refactor: sync refs and computed props in one effect

* dbeaver/pro#7023 fix: Esc double handling between SearchPanel and CaptureView hotkeys

* dbeaver/pro#7023 refactor: fix review comments in useGridSearch

* dbeaver/pro#7023 refactor: unify hotkey stop propagation via shared EventContext/EventStopPropagationFlag

* dbeaver/pro#7023 refactor: pass reactive cellText to useGridSearch

* dbeaver/pro#7023 refactor: update grid search reducer actions and navigation logic

reuse SET_ACTIVE_MATCH and computeActiveMatchIdx for consistency

* dbeaver/pro#7023 refactor: improve equality check for cell matches

* dbeaver/pro#7023 feat: implement cell change tracking subscriptions and cleanup logic

* dbeaver/pro#7023 refactor: remove redundant clearTrackedCellSubscriptions

Cleanup implemented in useTrackedGridSearchCellSubscriptions hook

* dbeaver/pro#7023 fix: cancel stale deferred selectCell to prevent infinite focus loop

When multiple rows are deleted, competing deferred selectCell timeouts
cause focusSyncRef mismatch and endless focus-change cycle.

* dbeaver/pro#7023 fix: update keyboard shortcut for search to use 'KeyF'

* dbeaver/pro#7023 feat: make search disabled by default

* dbeaver/pro#7023 fix: layout

* dbeaver/pro#7023 fix: update scrollBox selection method in Table component

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Viktor Kirst <viktor.kirst@dbeaver.com>
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
2026-03-17 17:28:02 +01:00

762 lines
22 KiB
TypeScript

/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { action, makeObservable, observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { IDatabaseDataSource } from '../../IDatabaseDataSource.js';
import { DatabaseEditAction } from '../DatabaseEditAction.js';
import {
DatabaseEditChangeType,
type IDatabaseDataEditActionData,
type IDatabaseDataEditActionValue,
type IDatabaseDataEditApplyActionData,
type IDatabaseDataEditApplyActionUpdate,
} from '../IDatabaseDataEditAction.js';
import { GridDataResultAction } from './GridDataResultAction.js';
import { IDatabaseDataResult } from '../../IDatabaseDataResult.js';
import type { IGridColumnKey, IGridDataKey, IGridRowKey } from './IGridDataKey.js';
import { GridDataKeysUtils } from './GridDataKeysUtils.js';
import { compareGridRowKeys } from './compareGridRowKeys.js';
import { IDatabaseDataResultAction } from '../IDatabaseDataResultAction.js';
import { GridHistoryAction } from './GridHistoryAction.js';
import { GridEditHistoryManager } from './GridEditHistoryManager.js';
import type { IGridHistoryData, IGridHistoryRevertData, IGridHistoryRow } from './GridHistoryTypes.js';
export interface IGridUpdate<TCell> {
row: IGridRowKey;
type: DatabaseEditChangeType;
update: TCell[];
source?: TCell[];
}
export interface IGridEditApplyActionUpdate extends IDatabaseDataEditApplyActionUpdate {
type?: DatabaseEditChangeType;
row: IGridRowKey;
newRow: IGridRowKey;
}
export interface IGridEditApplyActionData extends IDatabaseDataEditApplyActionData {
resultId: string | null;
updates: Array<IGridEditApplyActionUpdate>;
}
export type IGridEditActionData<TKey extends IGridDataKey = IGridDataKey, TCell = unknown> = IDatabaseDataEditActionData<TKey, TCell>;
@injectable(() => [IDatabaseDataSource, IDatabaseDataResult, IDatabaseDataResultAction, GridHistoryAction])
export class GridEditAction<
TColumn = unknown,
TRow = unknown,
TKey extends IGridDataKey = IGridDataKey,
TCell = unknown,
TResult extends IDatabaseDataResult = IDatabaseDataResult,
> extends DatabaseEditAction<TKey, TCell, IGridEditApplyActionData, TResult> {
protected readonly editorData: Map<string, IGridUpdate<TCell>>;
protected readonly data: GridDataResultAction<TColumn, TRow, TKey, TCell, TResult>;
protected readonly historyManager: GridEditHistoryManager<TKey, TCell>;
constructor(
source: IDatabaseDataSource<any, TResult>,
result: TResult,
data: IDatabaseDataResultAction<TKey, TResult>,
history: GridHistoryAction<any, TResult>,
) {
super(source, result);
this.editorData = new Map();
this.data = data as GridDataResultAction<TColumn, TRow, TKey, TCell, TResult>;
this.historyManager = new GridEditHistoryManager<TKey, TCell>(history as GridHistoryAction<IGridHistoryData<TKey, TCell>, TResult>);
makeObservable<this, 'editorData' | '_setRows' | '_addRows' | '_deleteRows' | '_revertChanges' | '_setCells'>(this, {
editorData: observable,
set: action,
add: action,
addRow: action,
delete: action,
deleteRow: action,
revert: action,
applyUpdate: action,
applyPartialUpdate: action,
_setCells: action,
_setRows: action,
_addRows: action,
_deleteRows: action,
_revertChanges: action,
});
this.historyManager.setupHandlers({
setCells: this._setCells.bind(this),
setRows: this._setRows.bind(this),
addRows: this._addRows.bind(this),
deleteRows: this._deleteRows.bind(this),
revertChanges: this._revertChanges.bind(this),
});
}
get addRows(): IGridRowKey[] {
return Array.from(this.editorData.values())
.filter(update => update.type === DatabaseEditChangeType.add)
.map(update => update.row);
}
get updates(): IGridUpdate<TCell>[] {
return Array.from(this.editorData.values()).sort((a, b) => {
if (a.type !== b.type) {
return a.type - b.type;
}
return a.row.index - b.row.index;
});
}
isEdited(): boolean {
return this.editorData.size > 0;
}
isElementEdited(key: TKey): boolean {
const update = this.editorData.get(GridDataKeysUtils.serialize(key.row));
if (!update) {
return false;
}
if (update.source === undefined || update.type === DatabaseEditChangeType.delete) {
return true;
}
return !this.compareCellValue(update.source[key.column.index], update.update[key.column.index]);
}
isRowEdited(key: IGridRowKey): boolean {
const update = this.editorData.get(GridDataKeysUtils.serialize(key));
if (!update) {
return false;
}
return true;
}
getElementState(key: TKey): DatabaseEditChangeType | null {
const update = this.editorData.get(GridDataKeysUtils.serialize(key.row));
if (!update) {
return null;
}
if (update.source === undefined || update.type !== DatabaseEditChangeType.update) {
return update.type;
}
if (!this.compareCellValue(update.source[key.column.index], update.update[key.column.index])) {
return update.type;
}
return null;
}
get(key: TKey): TCell | undefined {
return this.editorData.get(GridDataKeysUtils.serialize(key.row))?.update[key.column.index];
}
getRow(key: IGridRowKey): TCell[] | undefined {
return this.editorData.get(GridDataKeysUtils.serialize(key))?.update;
}
set(key: TKey, value: TCell): void {
const [update] = this.getOrCreateUpdate(key.row, DatabaseEditChangeType.update);
const prevValue = update.update[key.column.index] as TCell;
this.historyManager.recordCellEdit({
updates: [{ key, value, prevValue }],
});
update.update[key.column.index] = value;
this.action.execute({
resultId: this.result.id,
type: update.type,
revert: false,
value: [
{
key,
prevValue,
value,
},
],
});
this.removeEmptyUpdate(update);
}
setMany(updates: Array<{ key: TKey; value: TCell }>): void {
if (updates.length === 0) {
return;
}
const historyUpdates: Array<{ key: TKey; prevValue: TCell; value: TCell }> = [];
const actionValues: Array<{ key: TKey; prevValue: TCell; value: TCell }> = [];
for (const { key, value } of updates) {
const [update] = this.getOrCreateUpdate(key.row, DatabaseEditChangeType.update);
const prevValue = update.update[key.column.index] as TCell;
historyUpdates.push({ key, prevValue, value });
actionValues.push({ key, prevValue, value });
update.update[key.column.index] = value;
this.removeEmptyUpdate(update);
}
this.historyManager.recordCellEdit({ updates: historyUpdates });
this.action.execute({
resultId: this.result.id,
type: DatabaseEditChangeType.update,
revert: false,
value: actionValues,
});
}
add(...keys: TKey[]): void {
const result: TKey[] = [];
const rowKeys = new Set<string>();
for (const key of keys) {
const serialized = GridDataKeysUtils.serialize(key.row);
if (!rowKeys.has(serialized)) {
result.push(key);
rowKeys.add(serialized);
}
}
if (result.length <= 1) {
this.addRow(result[0]?.row, undefined, result[0]?.column);
return;
}
const addedKeys: Array<IGridHistoryRow<TKey, TCell>> = [];
for (const key of result) {
const newKey = this.addRow(key.row, undefined, key.column, true);
const update = this.editorData.get(GridDataKeysUtils.serialize(newKey.row));
if (update) {
addedKeys.push({ key: newKey, value: update.update });
}
}
if (addedKeys.length > 0) {
this.historyManager.recordAddRows({
rowEntries: addedKeys,
});
}
}
addRow(row?: IGridRowKey, value?: TCell[], column?: IGridColumnKey, ignoreHistory = false): TKey {
if (!row) {
row = this.data.getDefaultKey().row;
}
if (value === undefined) {
value = this.data.columns.map(() => null) as TCell[];
}
row = this.getNextRowAdd(row);
if (!column) {
column = this.data.getDefaultKey().column;
}
const [update, created] = this.getOrCreateUpdate(row, DatabaseEditChangeType.add, value);
const key = { column, row } as TKey;
if (created) {
if (!ignoreHistory) {
this.historyManager.recordAddRows({
rowEntries: [{ key, value: update.update }],
});
}
this.action.execute({
resultId: this.result.id,
type: update.type,
revert: false,
value: [{ key }],
});
}
return key;
}
duplicate(...keys: TKey[]): void {
const result: TKey[] = [];
const rowKeys = new Set<string>();
for (const key of keys) {
const serialized = GridDataKeysUtils.serialize(key.row);
if (!rowKeys.has(serialized)) {
result.push(key);
rowKeys.add(serialized);
}
}
this.duplicateRow(...result);
}
duplicateRow(...keys: TKey[]): void {
const duplicatedKeys: Array<IGridHistoryRow<TKey, TCell>> = [];
for (const key of keys) {
let value = this.data.getRowValue(key.row);
const editedValue = this.editorData.get(GridDataKeysUtils.serialize(key.row));
if (editedValue) {
value = editedValue.update;
}
const clonedValue = JSON.parse(JSON.stringify(value)) as TCell[];
const newKey = this.addRow(key.row, clonedValue, key.column, true);
duplicatedKeys.push({ key: newKey, value: clonedValue });
}
if (duplicatedKeys.length > 0) {
this.historyManager.recordAddRows({
rowEntries: duplicatedKeys,
});
}
}
delete(...keys: TKey[]): void {
const reverted: Array<IDatabaseDataEditActionValue<TKey, TCell>> = [];
const deleted: Array<IDatabaseDataEditActionValue<TKey, TCell>> = [];
const rowEntries: Array<IGridHistoryRow<TKey, TCell>> = [];
for (const key of keys) {
const serializedKey = GridDataKeysUtils.serialize(key.row);
const update = this.editorData.get(serializedKey);
const value = update?.update || this.data.getRowValue(key.row);
if (update?.type === DatabaseEditChangeType.add) {
reverted.push({ key });
this._revertChanges([{ row: key.row }]);
} else {
this._deleteRow(key.row);
deleted.push({ key });
}
if (value) {
rowEntries.push({ key, value });
}
}
if (rowEntries.length > 0) {
this.historyManager.recordDeleteRows({
rowEntries,
});
}
if (reverted.length > 0) {
this.action.execute({
resultId: this.result.id,
type: DatabaseEditChangeType.add,
revert: true,
value: reverted,
});
}
if (deleted.length > 0) {
this.action.execute({
resultId: this.result.id,
type: DatabaseEditChangeType.delete,
revert: false,
value: deleted,
});
}
}
deleteRow(key: IGridRowKey, column?: IGridColumnKey): void {
if (!column) {
column = this.data.getDefaultKey().column;
}
this.delete({ row: key, column } as TKey);
}
applyPartialUpdate(resultId: string | null, rows: TCell[][]): void {
if (rows.length !== this.updates.length) {
console.warn('GridEditAction: returned data differs from performed update');
}
const applyUpdate: Array<IGridEditApplyActionUpdate> = [];
const tempUpdates = this.updates
.map((update, i) => ({
rowIndex: update.type === DatabaseEditChangeType.delete ? -1 : i,
update,
}))
.sort((a, b) => compareGridRowKeys(b.update.row, a.update.row));
let offset = tempUpdates.reduce((offset, { update }) => {
if (update.type === DatabaseEditChangeType.add) {
return offset + 1;
}
if (update.type === DatabaseEditChangeType.delete) {
return offset - 1;
}
return offset;
}, 0);
for (const update of tempUpdates) {
const value = rows?.[update.rowIndex];
const row = update.update.row;
const type = update.update.type;
switch (update.update.type) {
case DatabaseEditChangeType.update: {
if (value) {
this.data.setRowValue(update.update.row, value);
}
this.applyResultToUpdate(update.update, value);
this.shiftRow(update.update.row, offset);
this.removeEmptyUpdate(update.update);
break;
}
case DatabaseEditChangeType.add: {
if (value) {
this.data.insertRow(update.update.row, value, 1);
}
this.applyResultToUpdate(update.update, value);
this.shiftRow(update.update.row, offset);
this.removeEmptyUpdate(update.update);
offset--;
break;
}
case DatabaseEditChangeType.delete: {
this.revert({ row: update.update.row, column: { index: 0 } } as TKey);
this.data.removeRow(update.update.row);
offset++;
break;
}
}
applyUpdate.push({
type,
row,
newRow: update.update.row,
});
}
if (applyUpdate.length > 0) {
this.applyAction.execute({
resultId: resultId,
updates: applyUpdate,
});
}
}
applyUpdate(resultId: string | null, rows: TCell[][]): void {
this.applyPartialUpdate(resultId, rows);
this.clear();
}
revert(...keys: TKey[]): void {
const revertedUpdates: Array<IDatabaseDataEditActionValue<TKey, TCell>> = [];
const revertedDeletions: Array<IDatabaseDataEditActionValue<TKey, TCell>> = [];
const revertedAdditions: Array<IDatabaseDataEditActionValue<TKey, TCell>> = [];
const { updates, deletions, additions } = this._getAllChanges();
const allKeys = new Set(keys.map(key => GridDataKeysUtils.serialize(key.row)));
const historyUpdates: IGridHistoryRevertData<TKey, TCell>['updates'] = updates.filter(({ key }) =>
allKeys.has(GridDataKeysUtils.serialize(key.row)),
);
const historyDeletions: IGridHistoryRevertData<TKey, TCell>['deletions'] = deletions.filter(({ key }) =>
allKeys.has(GridDataKeysUtils.serialize(key.row)),
);
const historyAdditions: IGridHistoryRevertData<TKey, TCell>['additions'] = additions.filter(({ key }) =>
allKeys.has(GridDataKeysUtils.serialize(key.row)),
);
for (const key of keys) {
const row = GridDataKeysUtils.serialize(key.row);
const update = this.editorData.get(row);
if (!update) {
continue;
}
let prevValue: TCell | undefined;
let value: TCell | undefined;
if (update.type === DatabaseEditChangeType.delete) {
revertedDeletions.push({ key });
this.editorData.delete(row);
} else {
prevValue = update.update[key.column.index];
value = update.source?.[key.column.index] ?? (null as TCell);
update.update[key.column.index] = value;
if (update.type === DatabaseEditChangeType.add) {
revertedAdditions.push({ key, prevValue, value });
} else {
revertedUpdates.push({ key, prevValue, value });
}
}
this.removeEmptyUpdate(update);
}
if (historyUpdates.length > 0 || historyDeletions.length > 0 || historyAdditions.length > 0) {
this.historyManager.recordRevert({
updates: historyUpdates,
deletions: historyDeletions,
additions: historyAdditions,
});
}
if (revertedUpdates.length > 0) {
this.action.execute({
resultId: this.result.id,
type: DatabaseEditChangeType.update,
revert: true,
value: revertedUpdates,
});
}
if (revertedDeletions.length > 0) {
this.action.execute({
resultId: this.result.id,
type: DatabaseEditChangeType.delete,
revert: true,
value: revertedDeletions,
});
}
if (revertedAdditions.length > 0) {
this.action.execute({
resultId: this.result.id,
type: DatabaseEditChangeType.add,
revert: true,
value: revertedAdditions,
});
}
}
clear(): void {
if (this.editorData.size === 0) {
return;
}
const { updates, deletions, additions } = this._getAllChanges();
if (updates.length > 0 || deletions.length > 0 || additions.length > 0) {
this.historyManager.recordRevert({
updates,
deletions,
additions,
});
}
this.editorData.clear();
this.action.execute({
resultId: this.result.id,
revert: true,
});
}
private _getAllChanges() {
const historyUpdates: IGridHistoryRevertData<TKey, TCell>['updates'] = [];
const historyDeletions: IGridHistoryRevertData<TKey, TCell>['deletions'] = [];
const historyAdditions: IGridHistoryRevertData<TKey, TCell>['additions'] = [];
for (const [, update] of this.editorData) {
const key = { row: update.row, column: { index: 0 } } as TKey;
if (update.type === DatabaseEditChangeType.delete) {
if (update.source) {
historyDeletions.push({ key, value: [...update.source] });
}
} else if (update.type === DatabaseEditChangeType.add) {
if (update.update) {
historyAdditions.push({ key, value: [...update.update] });
}
} else if (update.source) {
historyUpdates.push({
key,
prevValue: [...update.update],
value: [...update.source],
});
}
}
return {
updates: historyUpdates,
deletions: historyDeletions,
additions: historyAdditions,
};
}
private _deleteRow(row: IGridRowKey): void {
const serializedKey = GridDataKeysUtils.serialize(row);
const update = this.editorData.get(serializedKey);
if (row.subIndex !== 0 && !update) {
return;
}
if (update && update.type !== DatabaseEditChangeType.delete) {
this.editorData.delete(serializedKey);
}
if (update?.type !== DatabaseEditChangeType.add) {
this.getOrCreateUpdate(row, DatabaseEditChangeType.delete);
}
}
private _setCells(cells: Array<{ key: TKey; value: TCell }>): void {
for (const { key, value } of cells) {
const [update] = this.getOrCreateUpdate(key.row, DatabaseEditChangeType.update);
update.update[key.column.index] = value;
this.removeEmptyUpdate(update);
}
}
private _setRows(rows: Array<{ key: TKey; value: TCell[] }>): void {
for (const { key, value } of rows) {
const [update] = this.getOrCreateUpdate(key.row, DatabaseEditChangeType.update);
for (let i = 0; i < value.length; i++) {
update.update[i] = value[i]!;
}
this.removeEmptyUpdate(update);
}
}
private _addRows(rows: Array<{ row: IGridRowKey; value: TCell[] | undefined; column: IGridColumnKey }>): void {
for (const { row, value } of rows) {
let rowValue = value;
if (rowValue === undefined) {
rowValue = this.data.columns.map(() => null) as TCell[];
}
this.getOrCreateUpdate(row, DatabaseEditChangeType.add, rowValue);
}
}
private _deleteRows(rows: Array<{ row: IGridRowKey; column: IGridColumnKey }>): void {
for (const { row } of rows) {
const serializedKey = GridDataKeysUtils.serialize(row);
const update = this.editorData.get(serializedKey);
if (update) {
this.editorData.delete(serializedKey);
}
if (update?.type !== DatabaseEditChangeType.add) {
this.getOrCreateUpdate(row, DatabaseEditChangeType.delete);
}
}
}
private _revertChanges(rows: Array<{ row: IGridRowKey }>): void {
for (const { row } of rows) {
const serializedKey = GridDataKeysUtils.serialize(row);
this.editorData.delete(serializedKey);
}
}
private getNextRowAdd(row: IGridRowKey): IGridRowKey {
let i = row.subIndex + 1;
while (this.editorData.has(GridDataKeysUtils.serialize({ ...row, subIndex: i }))) {
i++;
}
return { ...row, subIndex: i };
}
private shiftRow(row: IGridRowKey, shift: number) {
const key = GridDataKeysUtils.serialize(row);
const update = this.editorData.get(GridDataKeysUtils.serialize(row));
if (update) {
update.row = {
index: update.row.index + shift,
subIndex: 0,
};
this.editorData.delete(key);
this.editorData.set(GridDataKeysUtils.serialize(update.row), update);
}
}
private removeEmptyUpdate(update: IGridUpdate<TCell>) {
if (update.type === DatabaseEditChangeType.add) {
return;
}
if (update.source && !update.source.some((value, i) => !this.compareCellValue(value, update.update[i]))) {
this.editorData.delete(GridDataKeysUtils.serialize(update.row));
}
}
protected getOrCreateUpdate(row: IGridRowKey, type: DatabaseEditChangeType, update?: TCell[]): [IGridUpdate<TCell>, boolean] {
const key = GridDataKeysUtils.serialize(row);
let created = false;
if (!this.editorData.has(key)) {
let source: TCell[] | undefined;
if (type !== DatabaseEditChangeType.add) {
source = this.data.getRowValue(row);
} else {
source = [...(update || [])];
}
this.editorData.set(key, {
row,
type,
source,
update: observable([...(source || update || [])]),
});
created = true;
}
return [this.editorData.get(key)!, created];
}
protected compareCellValue(valueA: TCell | undefined, valueB: TCell | undefined): boolean {
const castedValueA = valueA === undefined ? '' : valueA;
const castedValueB = valueB === undefined ? '' : valueB;
if (typeof castedValueA === 'number' || typeof castedValueB === 'number') {
return String(castedValueA) === String(castedValueB);
}
if (typeof castedValueA === 'boolean' || typeof castedValueB === 'boolean') {
return String(castedValueA).toLowerCase() === String(castedValueB).toLowerCase();
}
return castedValueA === castedValueB;
}
protected applyResultToUpdate(update: IGridUpdate<TCell>, result?: TCell[]): void {
if (result) {
update.source = result;
}
if (update.type === DatabaseEditChangeType.add) {
update.type = DatabaseEditChangeType.update;
}
}
}