diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx index b99b5a9299..1dade1e740 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx @@ -196,6 +196,7 @@ export function useGridSelectionContext( if (!multiple) { selectionAction.clear(); + return; } if (temporary) { @@ -229,14 +230,14 @@ export function useGridSelectionContext( return; } - const column = props.tableData.getColumn(cell.colIdx); - if (state.range) { return; } - const isIndexColumn = props.tableData.isIndexColumn(column.key); + const column = props.tableData.getColumn(cell.colIdx); + const isIndexColumn = props.tableData.isIndexColumn(column.key); const row = props.tableData.getRow(cell.rowIdx); + if (isIndexColumn) { selectRows(row, row, undefined, multiple, temporary); return; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index 457c943d2b..8889585612 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -81,6 +81,19 @@ export const DataGridTable = observer { dataGridRef.current?.selectCell({ idx: startPosition.colIdx, rowIdx: startPosition.rowIdx }); @@ -154,17 +167,19 @@ export const DataGridTable = observer) { - if (data.type === 'focus') { - if (!data.key?.column || !data.key.row) { - return; + setTimeout(() => { // TODO: update focus after render rows update + if (data.type === 'focus') { + if (!data.key?.column || !data.key.row) { + return; + } + + const idx = tableData.getColumnIndexFromColumnKey(data.key.column); + const rowIdx = tableData.getRowIndexFromKey(data.key.row); + + focusSyncRef.current = { idx, rowIdx }; + dataGridRef.current?.selectCell({ idx, rowIdx }); } - - const idx = tableData.getColumnIndexFromColumnKey(data.key.column); - const rowIdx = tableData.getRowIndexFromKey(data.key.row); - - focusSyncRef.current = { idx, rowIdx }; - dataGridRef.current?.selectCell({ idx, rowIdx }); - } + }, 1); } selectionAction.actions.addHandler(syncFocus); @@ -229,10 +244,10 @@ export const DataGridTable = observer gridContainerRef.current === document.activeElement, + isGridInFocus, getEditorPortal: () => editorRef.current, getDataGridApi: () => dataGridRef.current, - }), [model, actions, resultIndex, editorRef, dataGridRef]); + }), [model, actions, resultIndex, editorRef, dataGridRef, gridContainerRef]); return styled(styles)( diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts index 0a2b89fce1..38ada8d52a 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -10,7 +10,7 @@ import { useCallback } from 'react'; import { useObjectRef } from '@cloudbeaver/core-blocks'; import { copyToClipboard } from '@cloudbeaver/core-utils'; -import { IResultSetColumnKey, IResultSetElementKey, ResultSetDataKeysUtils } from '@cloudbeaver/plugin-data-viewer'; +import { IResultSetColumnKey, IResultSetElementKey, ResultSetDataKeysUtils, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer'; import type { IDataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; import type { ITableData } from './TableDataContext'; @@ -19,7 +19,13 @@ const EVENT_KEY_CODE = { C: 'KeyC', }; -function copyGridSelectedDataToClipboard( +function getCellCopyValue(tableData: ITableData, key: IResultSetElementKey): string { + const cell = tableData.getCellValue(key); + const cellValue = cell !== undefined ? tableData.format.getText(cell) : undefined; + return cellValue ?? ''; +} + +function getSelectedCellsValue( tableData: ITableData, selectedCells: Map ) { @@ -48,9 +54,7 @@ function copyGridSelectedDataToClipboard( const cellKey = rowSelection.find(key => ResultSetDataKeysUtils.isEqual(key.column, column)); if (cellKey) { - const cell = tableData.getCellValue(cellKey); - const cellValue = cell !== undefined ? tableData.format.getText(cell) : undefined; - rowCellsValues.push(cellValue ?? ''); + rowCellsValues.push(getCellCopyValue(tableData, cellKey)); } else { rowCellsValues.push(''); } @@ -58,7 +62,7 @@ function copyGridSelectedDataToClipboard( rowsValues.push(rowCellsValues.join('\t')); } - copyToClipboard(rowsValues.join('\r\n')); + return rowsValues.join('\r\n'); } // needed for event.code @@ -66,16 +70,28 @@ type IKeyboardEvent = React.KeyboardEvent & KeyboardEvent; export function useGridSelectedCellsCopy( tableData: ITableData, + resultSetSelectAction: ResultSetSelectAction, selectionContext: IDataGridSelectionContext ) { - const props = useObjectRef({ tableData, selectionContext }); + const props = useObjectRef({ tableData, selectionContext, resultSetSelectAction }); const onKeydownHandler = useCallback((event: IKeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.code === EVENT_KEY_CODE.C) { - copyGridSelectedDataToClipboard( - props.tableData, - props.selectionContext.selectedCells - ); + const focusedElement = props.resultSetSelectAction.getFocusedElement(); + let value: string | null = null; + + if (Array.from(props.selectionContext.selectedCells.keys()).length > 0) { + value = getSelectedCellsValue( + props.tableData, + props.selectionContext.selectedCells + ); + } else if (focusedElement) { + value = getCellCopyValue(tableData, focusedElement); + } + + if (value !== null) { + copyToClipboard(value); + } } }, []); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts index b54fbf560c..7d94d7e3a8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts @@ -40,9 +40,9 @@ export function useTableData( const editor = model.source.getAction(resultIndex, ResultSetEditAction); const view = model.source.getAction(resultIndex, ResultSetViewAction); - return useObservableRef(() => ({ + return useObservableRef }>(() => ({ get gridDiv(): HTMLDivElement | null { - return gridDIVElement.current; + return this.gridDIVElement.current; }, get columnKeys(): IResultSetColumnKey[] { return this.view.columnKeys; @@ -160,10 +160,12 @@ export function useTableData( data: observable.ref, editor: observable.ref, view: observable.ref, + gridDIVElement: observable.ref, }, { format, data, editor, view, + gridDIVElement, }); } diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 7aec18c5a6..2639eebeec 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -149,6 +149,13 @@ export class ContainerDataSource extends DatabaseDataSource newResult.id === result.id); @@ -156,13 +163,6 @@ export class ContainerDataSource extends DatabaseDataSource @@ -21,12 +21,14 @@ export abstract class DatabaseEditAction { static dataFormat: ResultDataFormat | null = null; - readonly action: IExecutor>; + readonly action: ISyncExecutor>; + readonly applyAction: ISyncExecutor>; protected features: Array; constructor(source: IDatabaseDataSource, result: TResult) { super(source, result); - this.action = new Executor(); + this.action = new SyncExecutor(); + this.applyAction = new SyncExecutor(); this.features = []; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseSelectAction.ts index 3ce04d3239..4196c20cf2 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseSelectAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/DatabaseSelectAction.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DatabaseDataAction } from '../DatabaseDataAction'; @@ -20,11 +20,11 @@ export abstract class DatabaseSelectAction implements IDatabaseDataSelectAction { static dataFormat: ResultDataFormat | null = null; - readonly actions: IExecutor>; + readonly actions: ISyncExecutor>; constructor(source: IDatabaseDataSource, result: TResult) { super(source, result); - this.actions = new Executor(); + this.actions = new SyncExecutor(); } abstract isSelected(): boolean; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataEditAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataEditAction.ts index 2eba6f66b0..ca63a76f2f 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataEditAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataEditAction.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { IExecutor } from '@cloudbeaver/core-executor'; +import type { ISyncExecutor } from '@cloudbeaver/core-executor'; import type { IDatabaseDataAction } from '../IDatabaseDataAction'; import type { IDatabaseDataResult } from '../IDatabaseDataResult'; @@ -25,6 +25,17 @@ export interface IDatabaseDataEditActionValue { prevValue?: TValue; } +export interface IDatabaseDataEditApplyActionUpdate { + type?: DatabaseEditChangeType; + row: TKey; + newRow: TKey; +} + +export interface IDatabaseDataEditApplyActionData { + resultId: string | null; + updates: Array>; +} + export interface IDatabaseDataEditActionData { revert: boolean; type?: DatabaseEditChangeType; @@ -34,7 +45,8 @@ export interface IDatabaseDataEditActionData { export interface IDatabaseDataEditAction extends IDatabaseDataAction { - readonly action: IExecutor>; + readonly action: ISyncExecutor>; + readonly applyAction: ISyncExecutor>; isEdited: () => boolean; isElementEdited: (key: TKey) => boolean; hasFeature: (feature: keyof this) => boolean; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSelectAction.ts index 354ce002b3..370fcca8c2 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSelectAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSelectAction.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { IExecutor } from '@cloudbeaver/core-executor'; +import type { ISyncExecutor } from '@cloudbeaver/core-executor'; import type { IDatabaseDataAction } from '../IDatabaseDataAction'; import type { IDatabaseDataResult } from '../IDatabaseDataResult'; @@ -27,7 +27,7 @@ export type DatabaseDataSelectActionsData = { export interface IDatabaseDataSelectAction extends IDatabaseDataAction { - readonly actions: IExecutor>; + readonly actions: ISyncExecutor>; isSelected: () => boolean; isElementSelected: (key: TKey) => boolean; getFocusedElement: () => TKey | null; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index 106630ba88..2671253255 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -52,16 +52,25 @@ export class ResultSetDataAction extends DatabaseDataAction { static dataFormat = ResultDataFormat.Resultset; + readonly applyAction: ISyncExecutor>; private editorData: Map; private data: ResultSetDataAction; @@ -45,6 +47,7 @@ export class ResultSetEditAction data: ResultSetDataAction ) { super(source, result); + this.applyAction = new SyncExecutor(); this.editorData = new Map(); this.data = data; this.features = ['add', 'delete']; @@ -286,6 +289,7 @@ export class ResultSetEditAction } applyUpdate(result: IDatabaseResultSet): void { + const applyUpdate: Array> = []; let rowIndex = 0; let addShift = 0; let deleteShift = 0; @@ -303,6 +307,11 @@ export class ResultSetEditAction if (value !== undefined) { this.data.setRowValue(update.row, value); + applyUpdate.push({ + type: DatabaseEditChangeType.update, + row: update.row, + newRow: update.row, + }); } rowIndex++; @@ -313,7 +322,15 @@ export class ResultSetEditAction const value = result.data?.rows?.[rowIndex]; if (value !== undefined) { - this.data.insertRow(update.row, value, addShift); + const newRow = this.data.insertRow(update.row, value, addShift); + + if (newRow) { + applyUpdate.push({ + type: DatabaseEditChangeType.add, + row: update.row, + newRow, + }); + } } insertedRows.push(update.row); @@ -324,12 +341,29 @@ export class ResultSetEditAction case DatabaseEditChangeType.delete: { const insertShift = insertedRows.filter(row => row.index < update.row.index).length; - this.data.removeRow(update.row, deleteShift + insertShift); + const newRow = this.data.removeRow(update.row, deleteShift + insertShift); + + if (newRow) { + applyUpdate.push({ + type: DatabaseEditChangeType.delete, + row: update.row, + newRow, + }); + } + deleteShift--; break; } } } + + if (applyUpdate.length > 0) { + this.applyAction.execute({ + resultId: result.id, + updates: applyUpdate, + }); + } + this.clear(); } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts index 19aa07782f..44da4dc329 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts @@ -8,16 +8,17 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; -import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataSource } from '../../IDatabaseDataSource'; import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; import { databaseDataAction } from '../DatabaseDataActionDecorator'; import { DatabaseSelectAction } from '../DatabaseSelectAction'; -import { DatabaseEditChangeType, IDatabaseDataEditActionData } from '../IDatabaseDataEditAction'; +import { DatabaseEditChangeType, IDatabaseDataEditActionData, IDatabaseDataEditApplyActionData } from '../IDatabaseDataEditAction'; import type { DatabaseDataSelectActionsData } from '../IDatabaseDataSelectAction'; import type { IResultSetColumnKey, IResultSetElementKey, IResultSetPartialKey, IResultSetRowKey } from './IResultSetDataKey'; +import { ResultSetDataAction } from './ResultSetDataAction'; import { ResultSetDataKeysUtils } from './ResultSetDataKeysUtils'; import { ResultSetEditAction } from './ResultSetEditAction'; import type { IResultSetValue } from './ResultSetFormatAction'; @@ -31,24 +32,27 @@ export class ResultSetSelectAction extends DatabaseSelectAction>; + readonly actions: ISyncExecutor>; readonly selectedElements: Map; private focusedElement: IResultSetElementKey | null; private view: ResultSetViewAction; private edit: ResultSetEditAction; + private data: ResultSetDataAction; private validationDisposer: IReactionDisposer; constructor( source: IDatabaseDataSource, result: IDatabaseResultSet, view: ResultSetViewAction, - edit: ResultSetEditAction + edit: ResultSetEditAction, + data: ResultSetDataAction ) { super(source, result); this.view = view; this.edit = edit; - this.actions = new Executor(); + this.data = data; + this.actions = new SyncExecutor(); this.selectedElements = new Map(); this.focusedElement = null; @@ -64,8 +68,14 @@ export class ResultSetSelectAction extends DatabaseSelectAction this.view.rowKeys, (current, previous) => { if (this.focusedElement) { const focus = this.focusedElement; + const currentIndex = current.findIndex(key => ResultSetDataKeysUtils.isEqual(key, focus.row)); + const focusIndex = previous.findIndex(key => ResultSetDataKeysUtils.isEqual(key, focus.row)); + if (currentIndex >= 0 && focusIndex === -1) { + return; + } + if (focusIndex === -1 || current.length === 0) { this.focus(null); return; @@ -97,6 +107,7 @@ export class ResultSetSelectAction extends DatabaseSelectAction) { + let nextFocus = { + ...this.data.getDefaultKey(), + ...this.focusedElement, + }; + + for (const update of data.updates) { + switch (update.type) { + case DatabaseEditChangeType.add: + if (nextFocus === null || ResultSetDataKeysUtils.isEqual(update.row, nextFocus.row)) { + nextFocus = { ...nextFocus, row: update.newRow }; + } + break; + + case DatabaseEditChangeType.delete: + if (nextFocus === null || ResultSetDataKeysUtils.isEqual(update.row, nextFocus.row)) { + nextFocus = { ...nextFocus, row: update.newRow }; + } + this.set({ row: update.row }, false, true); + break; + } + } + + this.focus(nextFocus); + } + private syncFocus(data: IDatabaseDataEditActionData) { switch (data.type) { case DatabaseEditChangeType.add: diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts index cc446da3d1..2aa6ffabf7 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetViewAction.ts @@ -73,7 +73,7 @@ export class ResultSetViewAction extends DatabaseDataAction { this.result = result; } + afterResultUpdate(): void { } + tryGetAction>( action: IDatabaseDataActionClass ): T | undefined { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts index a4816b4c7c..18d8bb8618 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts @@ -87,7 +87,7 @@ implements IDatabaseDataActions { } updateResults(results: TResult[]): void { - const actionsMap = Array.from(this.actions.entries()); + let actionsMap = Array.from(this.actions.entries()); for (const [key, actions] of actionsMap) { const result = results.find(result => result.uniqueResultId === key); @@ -103,6 +103,14 @@ implements IDatabaseDataActions { } } } + + actionsMap = Array.from(this.actions.entries()); + + for (const [, actions] of actionsMap) { + for (const action of actions) { + action.afterResultUpdate(); + } + } } private addActionToList( diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index e7ac828e87..f16a239ec5 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -18,7 +18,7 @@ import type { IDatabaseDataResult } from './IDatabaseDataResult'; import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; export abstract class DatabaseDataSource - implements IDatabaseDataSource { +implements IDatabaseDataSource { access: DatabaseDataAccessMode; dataFormat: ResultDataFormat; supportedDataFormats: ResultDataFormat[]; @@ -162,8 +162,8 @@ export abstract class DatabaseDataSource void; + afterResultUpdate: () => void; tryGetAction: >( action: IDatabaseDataActionClass ) => T | undefined; diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 1d98bf5423..fdbe2212c8 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -96,6 +96,13 @@ export class QueryDataSource extends DatabaseDataSource newResult.id === result.id); @@ -103,13 +110,6 @@ export class QueryDataSource extends DatabaseDataSource