diff --git a/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx b/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx index 224aaba6f5..a986f9d5cb 100644 --- a/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx +++ b/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx @@ -10,6 +10,7 @@ import { observer } from 'mobx-react-lite'; import { useEffect, useRef, useCallback, ChangeEvent } from 'react'; +import { useImperativeHandle } from 'react'; import styled, { use } from 'reshadow'; import { Icon } from '@cloudbeaver/core-blocks'; @@ -35,6 +36,7 @@ export interface InlineEditorProps { edited?: boolean; disabled?: boolean; autofocus?: boolean; + active?: boolean; onChange: (value: string) => void; onSave: () => void; onReject?: () => void; @@ -42,7 +44,7 @@ export interface InlineEditorProps { className?: string; } -export const InlineEditor = observer(function InlineEditor({ +export const InlineEditor = observer(function InlineEditor({ name, value, type = 'text', @@ -55,12 +57,13 @@ export const InlineEditor = observer(function InlineEditor({ edited = false, disabled, autofocus, + active, onChange, onSave, onUndo, onReject, className, -}: InlineEditorProps) { +}, ref) { const commonDialogService = useService(CommonDialogService); const handleChange = useCallback((event: ChangeEvent| string) => { @@ -79,23 +82,28 @@ export const InlineEditor = observer(function InlineEditor({ }, [value, commonDialogService, onSave, onReject, handleChange]); const handleKeyDown = useCallback((event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - onSave(); - } - if (event.key === 'Esc' && onReject) { - onReject(); + switch (event.key) { + case 'Enter': + onSave(); + break; + case 'Escape': + onReject?.(); + break; } }, [onSave, onReject]); const inputRef = useRef(null); + useEffect(() => { if (autofocus) { setTimeout(() => inputRef.current?.focus(), 100); } }, []); + useImperativeHandle(ref, () => inputRef.current!); + return styled(useStyles(InlineEditorStyles))( - + ); -}); +}, { forwardRef: true }); diff --git a/webapp/packages/core-app/src/shared/InlineEditor/styles.ts b/webapp/packages/core-app/src/shared/InlineEditor/styles.ts index 59ee047403..151e372146 100644 --- a/webapp/packages/core-app/src/shared/InlineEditor/styles.ts +++ b/webapp/packages/core-app/src/shared/InlineEditor/styles.ts @@ -38,7 +38,7 @@ export const InlineEditorStyles = composes( border: solid 1px; } - editor:focus-within { + editor:focus-within, editor[|active] { border-color: #01cca3; } @@ -73,7 +73,7 @@ export const InlineEditorStyles = composes( border: solid 1px; } - editor:focus-within { + editor:focus-within, editor[|active] { & editor-actions { border-top-color: #01cca3; border-right-color: #01cca3; @@ -95,7 +95,7 @@ export const InlineEditorStyles = composes( left: auto; } - editor:focus-within { + editor:focus-within, editor[|active] { & editor-actions[|position=bottom], & editor-actions[|position=top] { border-left-color: #01cca3; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx index 973e49a5d3..f23f6c6c2f 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx @@ -7,13 +7,13 @@ */ import { observer } from 'mobx-react-lite'; -import { useContext, useLayoutEffect, useState } from 'react'; +import { useContext, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react'; import type { EditorProps } from 'react-data-grid'; import { createPortal } from 'react-dom'; import { usePopper } from 'react-popper'; import styled, { css } from 'reshadow'; -import { InlineEditor, InlineEditorControls } from '@cloudbeaver/core-app'; +import { InlineEditor } from '@cloudbeaver/core-app'; import { DataGridContext } from '../DataGridContext'; @@ -21,75 +21,94 @@ const styles = css` editor { composes: theme-typography--body2 from global; } + box { + position: absolute; + left: 0; + top: 0; + width: 0; + height: 100%; + } InlineEditor { font-size: 12px; left: -1px; } `; -export const CellEditor: React.FC> = observer(function CellEditor({ +export interface IEditorRef { + focus: () => void; +} + +export const CellEditor = observer, 'rowIdx' | 'row' | 'column' | 'onClose'>, IEditorRef>(function CellEditor({ rowIdx, row, column, - editorPortalTarget, onClose, -}) { +}, ref) { + const context = useContext(DataGridContext); + const inputRef = useRef(null); const [elementRef, setElementRef] = useState(null); const [popperRef, setPopperRef] = useState(null); const popper = usePopper(elementRef, popperRef, { placement: 'right', }); - const context = useContext(DataGridContext); - - useLayoutEffect(() => { - if (elementRef && popperRef) { - const size = elementRef.parentElement?.getBoundingClientRect(); - if (size) { - popperRef.style.width = (size.width + 2) + 'px'; - popperRef.style.height = (size.height + 2) + 'px'; - } - } - }); if (!context) { throw new Error('DataGridContext should be provided'); } - const modelResultData = context.model?.getResult(context.resultIndex); + + useImperativeHandle(ref, () => ({ + focus: () => inputRef.current?.focus(), + })); + + useLayoutEffect(() => { + if (elementRef && popperRef) { + const size = elementRef.parentElement?.parentElement?.getBoundingClientRect(); + + if (size) { + popperRef.style.width = (size.width + 1) + 'px'; + popperRef.style.height = (size.height + 1) + 'px'; + } + } + }); const value = row[column.key]; - const isLastColumn = column.isLastFrozenColumn; // TODO: do not work - const isLastRow = rowIdx > 0 && ((modelResultData?.data.rows.length || 0) - 1 === rowIdx); - let controlsPosition: InlineEditorControls = 'right'; - - if (isLastColumn) { - controlsPosition = isLastRow ? 'top' : 'bottom'; - } - const handleSave = () => onClose(false); - const handleReject = () => onClose(false); - const handleChange = () => {}; - const handleUndo = () => onClose(false); + const handleReject = () => { + context.model.source.getEditor(context.resultIndex) + .revertCell(rowIdx, Number(column.key)); + onClose(false); + }; + const handleChange = (value: string) => { + context.model.source.getEditor(context.resultIndex) + .setCell(rowIdx, Number(column.key), value); + }; + const handleUndo = () => { + context.model.source.getEditor(context.resultIndex) + .revertCell(rowIdx, Number(column.key)); + onClose(false); + }; return styled(styles)( {createPortal(( - ), context.getEditorPortal() || editorPortalTarget)} + ), context.getEditorPortal()!)} ); -}); +}, { forwardRef: true }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx index 14112045bc..747f985b8c 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx @@ -11,27 +11,48 @@ import { useCallback, useContext } from 'react'; import type { CellRendererProps } from 'react-data-grid'; import { Cell } from 'react-data-grid'; +import { EditingContext } from '../../Editing/EditingContext'; +import { DataGridContext } from '../DataGridContext'; import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; export const CellRenderer: React.FC> = observer(function CellRenderer(props) { - const gridSelectionContext = useContext(DataGridSelectionContext); - if (!gridSelectionContext) { - throw new Error('Grid selection context must be provided'); - } + const context = useContext(DataGridContext); + const selectionContext = useContext(DataGridSelectionContext); + const editingContext = useContext(EditingContext); + const editor = context?.model.source.getEditor(context.resultIndex); - let classes = ''; + const classes: string[] = []; const { rowIdx, column } = props; - const { isSelected, select } = gridSelectionContext; - if (isSelected(column.key, rowIdx)) { - classes += 'rdg-cell-custom-selected'; + if (selectionContext?.isSelected(column.key, rowIdx)) { + classes.push('rdg-cell-custom-selected'); } - const onClickHandler = useCallback((event: React.MouseEvent) => { - select(column.key, rowIdx, event.ctrlKey, event.shiftKey); - }, [column, rowIdx, select]); + if (editingContext?.isEditing({ idx: column.idx, rowIdx })) { + classes.push('rdg-cell-custom-editing'); + } + + if (editor?.isCellEdited(rowIdx, Number(column.key))) { + classes.push('rdg-cell-custom-edited'); + } + + const handleClick = useCallback((event: React.MouseEvent) => { + selectionContext?.select(column.key, rowIdx, event.ctrlKey, event.shiftKey); + }, [column, rowIdx, selectionContext]); + + const handleDoubleClick = useCallback(() => { + editingContext?.edit({ idx: column.idx, rowIdx }); + }, [column, rowIdx]); + + const row = editor?.get(rowIdx) || props.row; 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 eeedb9f342..6fb80f2af6 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -11,6 +11,7 @@ import { observer } from 'mobx-react-lite'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import DataGrid from 'react-data-grid'; import type { Column } from 'react-data-grid'; +import type { Position } from 'react-data-grid/lib/types'; import styled from 'reshadow'; import type { SqlResultSet } from '@cloudbeaver/core-sdk'; @@ -18,10 +19,11 @@ import { useStyles } from '@cloudbeaver/core-theming'; import { TextTools } from '@cloudbeaver/core-utils'; import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; +import { EditingContext } from '../Editing/EditingContext'; +import { useEditing } from '../Editing/useEditing'; import { ResultSetTools } from '../ResultSetTools'; import baseStyles from '../styles/base.scss'; import { reactGridStyles } from '../styles/styles'; -import { CellEditor } from './CellEditor/CellEditor'; import { DataGridContext, IDataGridContext } from './DataGridContext'; import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; import { useGridSelectionContext } from './DataGridSelection/useGridSelectionContext'; @@ -62,6 +64,37 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ const gridSortingContext = useGridSortingContext(model); const gridSelectionContext = useGridSelectionContext(modelResultData, { indexColumnKey: indexColumn.key }); + const editingContext = useEditing({ + onEdit: (position, key) => { + const editor = model.source.getEditor(resultIndex); + + // TODO: not works yet + switch (key) { + case 'Delete': + case 'Backspace': + editor.setCell(position.rowIdx, position.idx, ''); + break; + default: + if (key) { + editor.setCell(position.rowIdx, position.idx, key); + } + } + + return true; + }, + }); + + useEffect(() => { + if (model.isLoading()) { + editingContext.close(); + } + }, [model.isLoading()]); + + const handleFocusChange = (position: Position) => { + if (!editingContext.isEditing(position)) { + editingContext.close(); + } + }; const handleScroll = useCallback( async (event: React.UIEvent) => { @@ -110,28 +143,11 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ // TODO: we need some result type specified formatter to common actions with data const rows = (modelResultData.data as SqlResultSet).rows || []; - const columns: Array> = (modelResultData.data as SqlResultSet).columns!.map((col, i) => ({ - key: i + '', + const columns = (modelResultData.data as SqlResultSet).columns!.map>((col, columnIndex) => ({ + key: columnIndex + '', name: col.label!, - width: Math.min(300, measuredCells[i]), - minWidth: 40, - resizable: true, - editable: true, + width: Math.min(300, measuredCells[columnIndex]), headerRenderer: TableColumnHeader, - formatter: CellFormatter, - editor: CellEditor, - editorOptions: { - onCellKeyDown: event => { - event.preventDefault(); - event.stopPropagation(); - }, - onNavigation: event => { - event.preventDefault(); - event.stopPropagation(); - - return false; - }, - }, })); columns.unshift(indexColumn); @@ -148,18 +164,26 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ - - -
- + + + +
+ + diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx index 14ed8b22b2..9e92c373d1 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx @@ -6,8 +6,12 @@ * you may not use this file except in compliance with the License. */ +import { useCallback, useContext, useEffect, useRef } from 'react'; import type { FormatterProps } from 'react-data-grid'; +import { EditingContext } from '../../Editing/EditingContext'; +import { CellEditor, IEditorRef } from '../CellEditor/CellEditor'; + function valueGetter(rawValue: any) { if (rawValue !== null && typeof rawValue === 'object') { return JSON.stringify(rawValue); @@ -38,13 +42,42 @@ function getClasses(rawValue: any) { return classes.join(' '); } -export const CellFormatter: React.FC = function CellFormatter({ row, column }) { +export const CellFormatter: React.FC = function CellFormatter({ rowIdx, row, column, isCellSelected }) { + const editorRef = useRef(null); + const cellRef = useRef(null); + const editingContext = useContext(EditingContext); const rawValue = row[column.key]; const classes = getClasses(rawValue); const value = formatValue(rawValue); + const handleClose = useCallback(() => { + editingContext?.closeEditor({ idx: column.idx, rowIdx }); + }, [column, rowIdx]); + + useEffect(() => { + if (isCellSelected) { + if (editingContext?.isEditing({ idx: column.idx, rowIdx })) { + editorRef.current?.focus(); + } + } + }); + + if (editingContext?.isEditing({ idx: column.idx, rowIdx })) { + return ( +
+ +
+ ); + } + return ( - + {value} ); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx new file mode 100644 index 0000000000..e0c9327ad2 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/EditingContext.tsx @@ -0,0 +1,19 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { createContext } from 'react'; +import type { Position } from 'react-data-grid/lib/types'; + +export interface IEditingContext { + edit: (position: Position, key?: string) => void; + closeEditor: (position: Position) => void; + close: () => void; + isEditing: (position: Position) => boolean; +} + +export const EditingContext = createContext(null); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts new file mode 100644 index 0000000000..8706232878 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/Editing/useEditing.ts @@ -0,0 +1,57 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { observable } from 'mobx'; +import { useState } from 'react'; +import type { Position } from 'react-data-grid/lib/types'; + +import { useObjectRef } from '@cloudbeaver/core-blocks'; + +import type { IEditingContext } from './EditingContext'; + +function findPosition(position: Position): (position: Position) => boolean { + return p => p.idx === position.idx && p.rowIdx === position.rowIdx; +} + +interface IEditingOptions { + onEdit: (position: Position, key?: string) => boolean; +} + +export function useEditing(options: IEditingOptions): IEditingContext { + const optionsRef = useObjectRef(options); + const [editingCells] = useState(() => observable([])); + + const [context] = useState({ + edit(position: Position, key?: string) { + // TODO: not works yet + switch (key) { + case 'Escape': + editingCells.splice(editingCells.findIndex(findPosition(position)), 1); + break; + } + + if (!optionsRef.current.onEdit(position, key)) { + return; + } + + editingCells.clear(); + editingCells.push(position); + }, + closeEditor(position: Position) { + editingCells.splice(editingCells.findIndex(findPosition(position)), 1); + }, + close() { + editingCells.clear(); + }, + isEditing(position: Position) { + return editingCells.some(findPosition(position)); + }, + }); + + return context; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss index af9b7297bc..9928f2397d 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss +++ b/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss @@ -24,7 +24,7 @@ .rdg-cell { border-color: transparent; } - + .rdg-cell-editing { overflow: visible; height: 24px; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss index ecaa17b048..a8e94a19f1 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss +++ b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss @@ -39,13 +39,22 @@ @include mdc-theme-prop(background-color, surface, false); } + .rdg-cell-custom-selected { + background-color: rgba(0, 145, 234, 0.3); + box-shadow: none; + } + .rdg-cell-selected { box-shadow: inset 0 0 0 1px #0091ea; } - .rdg-cell-custom-selected { - background-color: rgba(0, 145, 234, 0.3); - box-shadow: inset 0 0 0 1px #0091ea; + .rdg-cell-custom-editing { + box-shadow: none; + background-color: inherit; + } + + .rdg-cell-custom-edited { + background-color: #ff99006b; } .cell-formatter { diff --git a/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx b/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx index cb79210432..aec0addded 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/Spreadsheet.tsx @@ -19,7 +19,7 @@ export const Spreadsheet: React.FC = observer(function S }) { const deprecated = (model as DataModelWrapper).getOldModel(resultIndex); - if (!deprecated || (model.results.length === 0 && model.isLoading())) { + if (!deprecated || (model.source.results.length === 0 && model.isLoading())) { return null; } diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 078d90775f..e6bbe6f8f5 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -9,12 +9,12 @@ import { observable, makeObservable } from 'mobx'; import type { NotificationService } from '@cloudbeaver/core-events'; -import type { GraphQLService, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; +import type { GraphQLService, SqlDataFilterConstraint, SqlResultSet } from '@cloudbeaver/core-sdk'; import { EDeferredState } from '@cloudbeaver/core-utils'; +import { DatabaseDataEditor } from './DatabaseDataModel/DatabaseDataEditor'; import { DatabaseDataSource } from './DatabaseDataModel/DatabaseDataSource'; import type { IDatabaseDataResult } from './DatabaseDataModel/IDatabaseDataResult'; -import type { DataUpdate } from './DatabaseDataModel/IDatabaseDataSource'; import { FetchTableDataAsyncProcess } from './FetchTableDataAsyncProcess'; import type { IExecutionContext } from './IExecutionContext'; import type { RowDiff } from './TableViewer/TableDataModel/EditedRow'; @@ -29,7 +29,7 @@ export interface IDataContainerOptions { } export interface IDataContainerResult extends IDatabaseDataResult { - + data: SqlResultSet | undefined; } export class ContainerDataSource extends DatabaseDataSource { @@ -51,6 +51,7 @@ export class ContainerDataSource extends DatabaseDataSource { const executionContext = await this.ensureContextCreated(); - const response = await this.graphQLService.sdk.updateResultsDataBatch({ - connectionId: executionContext.connectionId, - contextId: executionContext.contextId, - resultsId: data.data.id, - // updatedRows: this.getRowsDiff(data), - }); + const changes = this.editor?.getChanges(); - this.requestInfo = { - requestDuration: response.result?.duration || 0, - requestMessage: 'Saved successfully', - }; + if (!changes) { + return prevResults; + } - throw new Error('Not implemented'); + for (const update of changes) { + const response = await this.graphQLService.sdk.updateResultsDataBatch({ + connectionId: executionContext.connectionId, + contextId: executionContext.contextId, + resultsId: update.resultId, + updatedRows: Array.from(update.diff.values()).map(diff => ({ + data: diff.source, + updateValues: diff.update.reduce((obj, value, index) => { + if (value !== diff.source[index]) { + obj[index] = value; + } + return obj; + }, {}), + })), + }); + + this.requestInfo = { + requestDuration: response.result?.duration || 0, + requestMessage: 'Saved successfully', + }; + + const result = prevResults.find(result => result.id === update.resultId)!; + const responseResult = response.result?.results.find(result => result.resultSet?.id === update.resultId); + + if (responseResult?.resultSet?.rows && result.data?.rows) { + let i = 0; + for (const row of update.diff.keys()) { + result.data.rows[row] = responseResult.resultSet.rows[i]; + i++; + } + } + + this.editor?.cancelResultChanges(result); + } + + return prevResults; } /** diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts index 33ebd7691a..e72354590c 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts @@ -109,7 +109,7 @@ export class DataViewerTabService { // TODO: used for initial data fetch, but can repeat request each time data tab is selected, // so probably should be refactored and managed by presentation - if (model.results.length === 0) { + if (model.source.results.length === 0) { await model.requestData(); } } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts index 8e6210237d..15b0209c16 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts @@ -12,7 +12,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { GraphQLService } from '@cloudbeaver/core-sdk'; import { ContainerDataSource } from './ContainerDataSource'; -import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataModel'; +import { DatabaseDataAccessMode } from './DatabaseDataModel/IDatabaseDataSource'; import type { DataModelWrapper } from './TableViewer/DataModelWrapper'; import { TableViewerStorageService } from './TableViewer/TableViewerStorageService'; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts new file mode 100644 index 0000000000..d9c0c69aad --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts @@ -0,0 +1,179 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { makeObservable, observable } from 'mobx'; + +import { DataUpdateType, IDatabaseDataEditor, IDatabaseDataResultEditor, IDataUpdate, IResultEditingDiff } from './IDatabaseDataEditor'; +import type { IDatabaseDataResult } from './IDatabaseDataResult'; + +export class DatabaseDataEditor implements IDatabaseDataEditor { + readonly editedResults: Map; + + constructor() { + makeObservable(this, { + editedResults: observable, + }); + + this.editedResults = new Map(); + } + + /** + * @deprecated more universal way should be implemented + */ + isRowEdited(result: TResult, row: number): boolean { + const update = this.editedResults.get(result.id); + const diff = update?.diff.get(row); + + return !!diff?.source.some((value, index) => value !== diff.update[index]); + } + + isResultEdited(result: TResult): boolean { + return this.editedResults.has(result.id); + } + + /** + * @deprecated more universal way should be implemented + */ + isCellEdited(result: TResult, row: number, column: number): boolean { + const update = this.editedResults.get(result.id); + const diff = update?.diff.get(row); + + if (!update || !diff) { + return false; + } + + return diff.source[column] !== diff.update[column]; + } + + getResultEditor(result: TResult): IDatabaseDataResultEditor { + return { + set: this.set.bind(this, result), + setCell: this.setCell.bind(this, result), + get: this.get.bind(this, result), + getCell: this.get.bind(this, result), + isCellEdited: this.isCellEdited.bind(this, result), + isEdited: this.isResultEdited.bind(this, result), + isRowEdited: this.isRowEdited.bind(this, result), + revert: this.revert.bind(this, result), + revertCell: this.revertCell.bind(this, result), + cancelChanges: this.cancelChanges.bind(this, result), + }; + } + + /** + * @deprecated more universal way should be implemented + */ + get(result: TResult, row: number): any { + const update = this.editedResults.get(result.id); + const diff = update?.diff.get(row); + + if (!update || !diff) { + return result.data.rows[row]; + } + + return diff?.update; + } + + /** + * @deprecated more universal way should be implemented + */ + getCell(result: TResult, row: number, column: number): any { + const update = this.editedResults.get(result.id); + const diff = update?.diff.get(row); + + if (!update || !diff) { + return result.data.rows[row][column]; + } + + return diff?.update[column]; + } + + /** + * @deprecated more universal way should be implemented + */ + set(result: TResult, row: number, value: any): void { + const diff = this.getOrCreateDiff(result, row); + + diff.update = value; + } + + /** + * @deprecated more universal way should be implemented + */ + setCell(result: TResult, row: number, column: number, value: any): void { + const diff = this.getOrCreateDiff(result, row); + + diff.update[column] = value; + } + + /** + * @deprecated more universal way should be implemented + */ + revert(result: TResult, row: number): void { + const update = this.editedResults.get(result.id); + + if (update?.diff.has(row)) { + update.diff.delete(row); + } + + if (update?.diff.size === 0) { + this.editedResults.delete(result.id); + } + } + + /** + * @deprecated more universal way should be implemented + */ + revertCell(result: TResult, row: number, column: number): void { + const update = this.editedResults.get(result.id); + const diff = update?.diff.get(row); + + if (diff) { + diff.update[column] = diff?.source[column]; + } + + if (!this.isRowEdited(result, row)) { + this.editedResults.delete(result.id); + } + } + + getChanges(): IDataUpdate[] { + return Array.from(this.editedResults.values()); + } + + cancelResultChanges(result: TResult): void { + this.editedResults.delete(result.id); + } + + cancelChanges(): void { + this.editedResults.clear(); + } + + private getOrCreateUpdate(resultId: string): IDataUpdate { + if (!this.editedResults.has(resultId)) { + this.editedResults.set(resultId, { diff: new Map(), resultId }); + } + + return this.editedResults.get(resultId)!; + } + + private getOrCreateDiff(result: TResult, row: number): IResultEditingDiff { + const update = this.getOrCreateUpdate(result.id); + + if (!update.diff.has(row)) { + const source = result.data.rows[row]; + update.diff.set(row, { + type: DataUpdateType.update, + source, + update: [...source], + }); + } + + return update.diff.get(row)!; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts index 1724168038..705684bc43 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts @@ -11,16 +11,14 @@ import { observable, makeObservable } from 'mobx'; import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { uuid } from '@cloudbeaver/core-utils'; -import { DatabaseDataAccessMode, IDatabaseDataModel } from './IDatabaseDataModel'; +import type { IDatabaseDataModel } from './IDatabaseDataModel'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import type { IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; +import type { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; export class DatabaseDataModel implements IDatabaseDataModel { id: string; - results: TResult[]; source: IDatabaseDataSource; - access: DatabaseDataAccessMode; countGain: number; get requestInfo(): IRequestInfo { @@ -33,16 +31,12 @@ implements IDatabaseDataModel { constructor(source: IDatabaseDataSource) { makeObservable(this, { - results: observable, - access: observable, countGain: observable, }); this.id = uuid(); this.source = source; this.countGain = 0; - this.results = []; - this.access = DatabaseDataAccessMode.Default; } isLoading(): boolean { @@ -53,17 +47,17 @@ implements IDatabaseDataModel { return this.source.offset <= offset && this.source.count >= count; } - async refresh(): Promise { - await this.requestData(); - } - - async reload(): Promise { - this.setSlice(0, this.countGain); - await this.requestData(); + getResult(index: number): TResult | null { + return this.source.getResult(index); } setResults(results: TResult[]): this { - this.results = results; + this.source.setResults(results); + return this; + } + + setAccess(access: DatabaseDataAccessMode): this { + this.source.setAccess(access); return this; } @@ -72,19 +66,6 @@ implements IDatabaseDataModel { return this; } - getResult(index: number): TResult | null { - if (this.results.length > index) { - return this.results[index]; - } - - return null; - } - - setAccess(access: DatabaseDataAccessMode): this { - this.access = access; - return this; - } - setSlice(offset: number, count = this.countGain): this { this.source.setSlice(offset, count); return this; @@ -105,14 +86,23 @@ implements IDatabaseDataModel { return this; } + async refresh(): Promise { + await this.requestData(); + } + + async reload(): Promise { + this.setSlice(0, this.countGain); + await this.requestData(); + } + async requestDataPortion(offset: number, count: number): Promise { if (!this.isDataAvailable(offset, count)) { this.source.setSlice(offset, count); - this.results = await this.source.requestData(this.results); + await this.source.requestData(); } } async requestData(): Promise { - this.results = await this.source.requestData(this.results); + await this.source.requestData(); } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index 60de66c1ef..dfa13ccb4b 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -13,18 +13,22 @@ import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import type { IExecutionContext } from '../IExecutionContext'; import type { RowDiff } from '../TableViewer/TableDataModel/EditedRow'; import type { IRequestDataResult } from '../TableViewer/TableViewerModel'; +import type { IDatabaseDataEditor, IDatabaseDataResultEditor } from './IDatabaseDataEditor'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import type { DataUpdate, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; +import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; export abstract class DatabaseDataSource implements IDatabaseDataSource { + access: DatabaseDataAccessMode; + dataFormat: ResultDataFormat; + supportedDataFormats: ResultDataFormat[]; + editor: IDatabaseDataEditor | null; + results: TResult[]; offset: number; count: number; - dataFormat: ResultDataFormat; options: TOptions | null; requestInfo: IRequestInfo; executionContext: IExecutionContext | null; - supportedDataFormats: ResultDataFormat[]; abstract get canCancel(): boolean; private activeRequest: Promise | null; @@ -32,17 +36,23 @@ implements IDatabaseDataSource { constructor() { makeObservable, 'activeRequest' | 'activeSave'>(this, { + access: observable, + dataFormat: observable, + supportedDataFormats: observable, + editor: observable, + results: observable, offset: observable, count: observable, - dataFormat: observable, options: observable, requestInfo: observable, executionContext: observable, - supportedDataFormats: observable, activeRequest: observable, activeSave: observable, }); + this.access = DatabaseDataAccessMode.Default; + this.results = []; + this.editor = null; this.offset = 0; this.count = 0; this.options = null; @@ -59,10 +69,42 @@ implements IDatabaseDataSource { abstract cancel(): Promise | boolean; + getResult(index: number): TResult | null { + if (this.results.length > index) { + return this.results[index]; + } + + return null; + } + + getEditor(resultIndex: number): IDatabaseDataResultEditor { + if (!this.editor) { + throw new Error('Editor was not provided'); + } + + return this.editor.getResultEditor(this.results[resultIndex]); + } + + setResults(results: TResult[]): this { + this.editor?.cancelChanges(); + this.results = results; + return this; + } + isLoading(): boolean { return !!this.activeRequest || !!this.activeSave; } + setEditor(editor: IDatabaseDataEditor): this { + this.editor = editor; + return this; + } + + setAccess(access: DatabaseDataAccessMode): this { + this.access = access; + return this; + } + setSlice(offset: number, count: number): this { this.offset = offset; this.count = count; @@ -90,7 +132,7 @@ implements IDatabaseDataSource { return this; } - async requestData(prevResults: TResult[]): Promise { + async requestData(): Promise { if (this.activeSave) { try { await this.activeSave; @@ -98,25 +140,24 @@ implements IDatabaseDataSource { } if (this.activeRequest) { - return this.activeRequest; + await this.activeRequest; + return; } try { - const promise = this.request(prevResults); + const promise = this.request(this.results); if (promise instanceof Promise) { this.activeRequest = promise; } - return await promise; + this.editor?.cancelChanges(); + this.results = await promise; } finally { this.activeRequest = null; } } - async saveData( - prevResults: TResult[], - data: DataUpdate - ): Promise { + async saveData(): Promise { if (this.activeRequest) { try { await this.activeRequest; @@ -124,16 +165,17 @@ implements IDatabaseDataSource { } if (this.activeSave) { - return this.activeSave; + await this.activeSave; + return; } try { - const promise = this.save(prevResults, data); + const promise = this.save(this.results); if (promise instanceof Promise) { this.activeSave = promise; } - return await promise; + this.results = await promise; } finally { this.activeSave = null; } @@ -163,7 +205,7 @@ implements IDatabaseDataSource { } abstract request(prevResults: TResult[]): TResult[] | Promise; - abstract save(prevResults: TResult[], data: DataUpdate): Promise | TResult[]; + abstract save(prevResults: TResult[]): Promise | TResult[]; abstract saveDeprecated(resultId: string, rows: RowDiff[]): Promise; abstract dispose(): Promise; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts index a3dccdb8a6..c04a4bf92f 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataEditor.ts @@ -6,9 +6,52 @@ * you may not use this file except in compliance with the License. */ -export interface IDatabaseDataEditor { +import type { IDatabaseDataResult } from './IDatabaseDataResult'; + +export enum DataUpdateType { + delete, + update, + add +} + +export interface IResultEditingDiff { + source: any[]; + update: any[]; + type: DataUpdateType; +} + +export interface IDataUpdate { + resultId: string; + diff: Map; +} + +export interface IDatabaseDataEditor { + isResultEdited: (result: TResult) => boolean; + isRowEdited: (result: TResult, row: number) => boolean; + isCellEdited: (result: TResult, row: number, column: number) => boolean; + get: (result: TResult, row: number) => void; + set: (result: TResult, row: number, value: any) => void; + getCell: (result: TResult, row: number, column: number) => void; + setCell: (result: TResult, row: number, column: number, value: any) => void; + revert: (result: TResult, row: number) => void; + revertCell: (result: TResult, row: number, column: number) => void; + + getResultEditor: (result: TResult) => IDatabaseDataResultEditor; + + getChanges: () => IDataUpdate[]; + cancelChanges: () => void; + cancelResultChanges: (result: TResult) => void; +} + +export interface IDatabaseDataResultEditor { isEdited: () => boolean; - getChanges: () => void; - applyChanges: () => void; - cancelChanges: (skipUpdate?: boolean) => void; + isRowEdited: (row: number) => boolean; + isCellEdited: (row: number, column: number) => boolean; + get: (row: number) => void; + set: (row: number, value: any) => void; + getCell: (row: number, column: number) => void; + setCell: (row: number, column: number, value: any) => void; + revert: (row: number) => void; + revertCell: (row: number, column: number) => void; + cancelChanges: () => void; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts index 63b8d4a693..fd8cfbe2b0 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts @@ -9,18 +9,11 @@ import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; -import type { IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; - -export enum DatabaseDataAccessMode { - Default, - Readonly -} +import type { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDatabaseDataSource'; export interface IDatabaseDataModel { readonly id: string; - readonly results: TResult[]; readonly source: IDatabaseDataSource; - readonly access: DatabaseDataAccessMode; readonly requestInfo: IRequestInfo; readonly supportedDataFormats: ResultDataFormat[]; readonly countGain: number; @@ -30,8 +23,8 @@ export interface IDatabaseDataModel TResult | null; - setCountGain: (count: number) => this; setAccess: (access: DatabaseDataAccessMode) => this; + setCountGain: (count: number) => this; setSlice: (offset: number, count?: number) => this; setOptions: (options: TOptions) => this; setDataFormat: (dataFormat: ResultDataFormat) => this; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index dbbc868a6f..90d1ef5da4 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -6,53 +6,53 @@ * you may not use this file except in compliance with the License. */ -import type { ResultDataFormat, SqlResultSet } from '@cloudbeaver/core-sdk'; +import type { ResultDataFormat } from '@cloudbeaver/core-sdk'; import type { IExecutionContext } from '../IExecutionContext'; import type { RowDiff } from '../TableViewer/TableDataModel/EditedRow'; import type { IRequestDataResult } from '../TableViewer/TableViewerModel'; +import type { IDatabaseDataEditor, IDatabaseDataResultEditor } from './IDatabaseDataEditor'; import type { IDatabaseDataResult } from './IDatabaseDataResult'; -export enum DataUpdateType { - delete, - update, - add -} - export interface IRequestInfo { readonly requestDuration: number; readonly requestMessage: string; } -export interface DataUpdate { - data: SqlResultSet; - dataUpdate: SqlResultSet; - type: DataUpdateType; +export enum DatabaseDataAccessMode { + Default, + Readonly } export interface IDatabaseDataSource { + readonly access: DatabaseDataAccessMode; + readonly dataFormat: ResultDataFormat; + readonly supportedDataFormats: ResultDataFormat[]; + readonly editor: IDatabaseDataEditor | null; + readonly results: TResult[]; readonly offset: number; readonly count: number; readonly options: TOptions | null; readonly requestInfo: IRequestInfo; - readonly dataFormat: ResultDataFormat; readonly executionContext: IExecutionContext | null; - readonly supportedDataFormats: ResultDataFormat[]; readonly canCancel: boolean; isLoading: () => boolean; + + getEditor: (resultIndex: number) => IDatabaseDataResultEditor; + getResult: (index: number) => TResult | null; + + setResults: (results: TResult[]) => this; + setEditor: (editor: IDatabaseDataEditor) => this; + setAccess: (access: DatabaseDataAccessMode) => this; setSlice: (offset: number, count: number) => this; setOptions: (options: TOptions) => this; setDataFormat: (dataFormat: ResultDataFormat) => this; setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this; setExecutionContext: (context: IExecutionContext | null) => this; - requestData: ( - prevResults: TResult[] - ) => Promise | TResult[]; - saveData: ( - prevResults: TResult[], - data: DataUpdate - ) => Promise | TResult[]; + + requestData: () => Promise | void; + saveData: () => Promise | void; /** * @deprecated will be refactored */ diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts index 41da44b63d..031f7a8bbf 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts @@ -14,9 +14,8 @@ import { DetailsError } from '@cloudbeaver/core-sdk'; import type { IDataContainerOptions } from '../ContainerDataSource'; import { DatabaseDataModel } from '../DatabaseDataModel/DatabaseDataModel'; -import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel'; import type { IDatabaseDataResult } from '../DatabaseDataModel/IDatabaseDataResult'; -import type { IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource'; +import { DatabaseDataAccessMode, IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource'; import type { RowDiff } from './TableDataModel/EditedRow'; import { IRequestDataResult, TableViewerModel } from './TableViewerModel'; @@ -101,7 +100,7 @@ export class DataModelWrapper extends DatabaseDataModel { this.clearErrors(); try { - this.results = await this.source.requestData(this.results); + await this.source.requestData(); await this.setDeprecatedModelData(); } catch (exception) { this.showError(exception); @@ -130,7 +129,7 @@ export class DataModelWrapper extends DatabaseDataModel 1 + model.access = this.source.results.length > 1 ? DatabaseDataAccessMode.Readonly - : this.access; + : this.source.access; } } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts index 94e37e72a7..1054344c89 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -35,12 +35,26 @@ export class TableFooterMenuService { if (context.data.model.deprecatedModels.length === 0) { return true; } - return context.data.model.isLoading() || !context.data.model.getOldModel(context.data.resultIndex)?.isEdited(); + const editor = context.data.model.source.getEditor(context.data.resultIndex); + + return context.data.model.isLoading() + || (!context.data.model.getOldModel(context.data.resultIndex)?.isEdited() + && !editor.isEdited()); }, order: 1, title: 'ui_processing_save', icon: 'table-save', - onClick: context => context.data.model.getOldModel(context.data.resultIndex)?.saveChanges(), + onClick: context => { + const editor = context.data.model.source.getEditor(context.data.resultIndex); + + if (context.data.model.getOldModel(context.data.resultIndex)?.isEdited()) { + context.data.model.getOldModel(context.data.resultIndex)?.saveChanges(); + } + + if (editor.isEdited()) { + context.data.model.source.saveData(); + } + }, }); this.registerMenuItem({ id: 'cancel ', @@ -51,12 +65,21 @@ export class TableFooterMenuService { if (context.data.model.deprecatedModels.length === 0) { return true; } - return !context.data.model.getOldModel(context.data.resultIndex)?.isEdited(); + + const editor = context.data.model.source.getEditor(context.data.resultIndex); + + return !context.data.model.getOldModel(context.data.resultIndex)?.isEdited() + && !editor.isEdited(); }, order: 2, title: 'ui_processing_cancel', icon: 'table-cancel', - onClick: context => context.data.model.getOldModel(context.data.resultIndex)?.cancelChanges(), + onClick: context => { + context.data.model.getOldModel(context.data.resultIndex)?.cancelChanges(); + + const editor = context.data.model.source.getEditor(context.data.resultIndex); + editor.cancelChanges(); + }, }); } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx index 52ba9ac39d..1dd7d4721e 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx @@ -76,7 +76,7 @@ export const TableWhereFilter = observer(function TableWhereFilter({ placeholder={translate('table_header_sql_expression')} controlsPosition='inside' edited={!!filterValue} - disabled={context.isLoading() || context.results.length > 1} + disabled={context.isLoading() || context.source.results.length > 1} simple onSave={handleApply} onUndo={resetFilter} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index 4a255aa082..be7c1c7c5a 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -151,7 +151,7 @@ export const TableViewer = observer(function TableViewer({ 0 && presentation.dataFormat === dataFormat} + overlay={dataModel.source.results.length > 0 && presentation.dataFormat === dataFormat} onCancel={() => dataModel.source.cancel()} /> diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts index df42b8f235..bb6d0338cb 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts @@ -14,7 +14,7 @@ import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications'; import { DetailsError, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; -import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel'; +import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataSource'; import { ErrorDialog } from './ErrorDialog'; import type { RowDiff } from './TableDataModel/EditedRow'; import type { TableColumn } from './TableDataModel/TableColumn'; diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 6064812da2..aa513b88ef 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -2,6 +2,7 @@ import { manifest } from './manifest'; export default manifest; +export * from './DatabaseDataModel/DatabaseDataEditor'; export * from './DatabaseDataModel/DatabaseDataFormat'; export * from './DatabaseDataModel/DatabaseDataModel'; export * from './DatabaseDataModel/DatabaseDataSource'; diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 9a526405ff..52dc0af9ed 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -9,10 +9,11 @@ import { observable, makeObservable } from 'mobx'; import type { NotificationService } from '@cloudbeaver/core-events'; -import type { GraphQLService, SqlDataFilterConstraint, SqlExecuteInfo } from '@cloudbeaver/core-sdk'; +import type { GraphQLService, SqlDataFilterConstraint, SqlExecuteInfo, SqlResultSet } from '@cloudbeaver/core-sdk'; import { EDeferredState } from '@cloudbeaver/core-utils'; import { - DatabaseDataSource, DataUpdate, IDatabaseDataResult, IRequestDataResult, RowDiff + DatabaseDataEditor, + DatabaseDataSource, IDatabaseDataResult, IRequestDataResult, RowDiff } from '@cloudbeaver/plugin-data-viewer'; import type { IQueryTabGroup } from './ISqlEditorTabState'; @@ -29,7 +30,7 @@ export interface IDataContainerOptions { } export interface IDataContainerResult extends IDatabaseDataResult { - + data: SqlResultSet | undefined; } export class QueryDataSource extends DatabaseDataSource { @@ -51,6 +52,7 @@ export class QueryDataSource extends DatabaseDataSource - ): IDataContainerResult[] | Promise { - throw new Error('Method not implemented.'); + async save( + prevResults: IDataContainerResult[] + ): Promise { + const params = this.options?.group.sqlQueryParams; + if (!params) { + throw new Error('sqlQueryParams must be provided'); + } + + const changes = this.editor?.getChanges(); + + if (!changes) { + return prevResults; + } + + for (const update of changes) { + const response = await this.graphQLService.sdk.updateResultsDataBatch({ + connectionId: params.connectionId, + contextId: params.contextId, + resultsId: update.resultId, + updatedRows: Array.from(update.diff.values()).map(diff => ({ + data: diff.source, + updateValues: diff.update.reduce((obj, value, index) => { + if (value !== diff.source[index]) { + obj[index] = value; + } + return obj; + }, {}), + })), + }); + + this.requestInfo = { + requestDuration: response.result?.duration || 0, + requestMessage: 'Saved successfully', + }; + + const result = prevResults.find(result => result.id === update.resultId)!; + const responseResult = response.result?.results.find(result => result.resultSet?.id === update.resultId); + + if (responseResult?.resultSet?.rows && result.data?.rows) { + let i = 0; + for (const row of update.diff.keys()) { + result.data.rows[row] = responseResult.resultSet.rows[i]; + i++; + } + } + + this.editor?.cancelResultChanges(result); + } + + return prevResults; } setOptions(options: IDataContainerOptions): this { diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts index 4ee54dfc0b..ccd23a34c9 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts @@ -131,7 +131,7 @@ export class SqlResultTabsService { .setSlice(0, fetchingSettings.fetchDefault) .requestData(); - length = model.results.length; + length = model.source.results.length; if (length === 1) { resultTab.name = this.getTabNameForOrder(tabGroup.order);