feat: CB-655 table data editing

This commit is contained in:
Wroud
2021-02-09 20:43:54 +03:00
parent 62e83bb4f3
commit df7a7e6a38
28 changed files with 758 additions and 222 deletions
@@ -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<InlineEditorProps, HTMLInputElement | null>(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<HTMLInputElement>| string) => {
@@ -79,23 +82,28 @@ export const InlineEditor = observer(function InlineEditor({
}, [value, commonDialogService, onSave, onReject, handleChange]);
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
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<HTMLInputElement>(null);
useEffect(() => {
if (autofocus) {
setTimeout(() => inputRef.current?.focus(), 100);
}
}, []);
useImperativeHandle(ref, () => inputRef.current!);
return styled(useStyles(InlineEditorStyles))(
<editor as="div" className={className}>
<editor as="div" className={className} {...use({ active })}>
<editor-container as="div">
<input
ref={inputRef}
@@ -118,4 +126,4 @@ export const InlineEditor = observer(function InlineEditor({
</editor-actions>
</editor>
);
});
}, { forwardRef: true });
@@ -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;
@@ -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<EditorProps<any, any>> = observer(function CellEditor({
export interface IEditorRef {
focus: () => void;
}
export const CellEditor = observer<Pick<EditorProps<any, any>, 'rowIdx' | 'row' | 'column' | 'onClose'>, IEditorRef>(function CellEditor({
rowIdx,
row,
column,
editorPortalTarget,
onClose,
}) {
}, ref) {
const context = useContext(DataGridContext);
const inputRef = useRef<HTMLInputElement>(null);
const [elementRef, setElementRef] = useState<HTMLDivElement | null>(null);
const [popperRef, setPopperRef] = useState<HTMLDivElement | null>(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)(
<box ref={setElementRef} as='div'>
{createPortal((
<editor ref={setPopperRef} as="div" style={popper.styles.popper} {...popper.attributes.popper}>
<InlineEditor
ref={inputRef}
type={typeof value === 'number' ? 'number' : 'text'}
value={value}
controlsPosition={controlsPosition}
edited
hideSave
hideCancel
autofocus
active
onSave={handleSave}
onReject={handleReject}
onChange={handleChange}
onUndo={handleUndo}
/>
</editor>
), context.getEditorPortal() || editorPortalTarget)}
), context.getEditorPortal()!)}
</box>
);
});
}, { forwardRef: true });
@@ -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<CellRendererProps<any>> = 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<HTMLDivElement, 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<HTMLDivElement, 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 (
<Cell className={classes} onClick={onClickHandler} {...props} />
<Cell
className={classes.join(' ')}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
{...props}
row={row}
/>
);
});
@@ -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<Props> = 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<HTMLDivElement>) => {
@@ -110,28 +143,11 @@ export const DataGridTable: React.FC<Props> = 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<Column<any[], any>> = (modelResultData.data as SqlResultSet).columns!.map((col, i) => ({
key: i + '',
const columns = (modelResultData.data as SqlResultSet).columns!.map<Column<any[], any>>((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<Props> = observer(function DataGridTable({
<DataGridContext.Provider value={gridContext}>
<DataGridSortingContext.Provider value={gridSortingContext}>
<DataGridSelectionContext.Provider value={gridSelectionContext}>
<DataGridTableContainer modelResultData={modelResultData}>
<DataGrid
className={`cb-react-grid-theme ${className}`}
columns={columns}
rows={rows}
headerRowHeight={28}
rowHeight={24}
rowRenderer={RowRenderer}
onScroll={handleScroll}
/>
<div ref={editorRef} />
</DataGridTableContainer>
<EditingContext.Provider value={editingContext}>
<DataGridTableContainer modelResultData={modelResultData}>
<DataGrid
className={`cb-react-grid-theme ${className}`}
columns={columns}
defaultColumnOptions={{
minWidth: 40,
resizable: true,
formatter: CellFormatter,
}}
rows={rows}
headerRowHeight={28}
rowHeight={24}
rowRenderer={RowRenderer}
onSelectedCellChange={handleFocusChange}
onScroll={handleScroll}
/>
<div ref={editorRef} />
</DataGridTableContainer>
</EditingContext.Provider>
</DataGridSelectionContext.Provider>
</DataGridSortingContext.Provider>
</DataGridContext.Provider>
@@ -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<FormatterProps> = function CellFormatter({ row, column }) {
export const CellFormatter: React.FC<FormatterProps> = function CellFormatter({ rowIdx, row, column, isCellSelected }) {
const editorRef = useRef<IEditorRef>(null);
const cellRef = useRef<HTMLDivElement>(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 (
<div className={`cell-formatter ${classes}`}>
<CellEditor
ref={editorRef}
rowIdx={rowIdx}
row={row}
column={column}
onClose={handleClose}
/>
</div>
);
}
return (
<cell-formatter as='div' className={`cell-formatter ${classes}`}>
<cell-formatter ref={cellRef} as='div' className={`cell-formatter ${classes}`}>
{value}
</cell-formatter>
);
@@ -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<IEditingContext | null>(null);
@@ -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<IEditingOptions>(options);
const [editingCells] = useState(() => observable<Position>([]));
const [context] = useState<IEditingContext>({
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;
}
@@ -24,7 +24,7 @@
.rdg-cell {
border-color: transparent;
}
.rdg-cell-editing {
overflow: visible;
height: 24px;
@@ -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 {
@@ -19,7 +19,7 @@ export const Spreadsheet: React.FC<IDataPresentationProps> = 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;
}
@@ -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<IDataContainerOptions, IDataContainerResult> {
@@ -51,6 +51,7 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
this.currentFetchTableProcess = null;
this.executionContext = null;
this.editor = new DatabaseDataEditor();
}
cancel(): boolean {
@@ -108,24 +109,52 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
}
async save(
prevResults: IDataContainerResult[],
data: DataUpdate
prevResults: IDataContainerResult[]
): Promise<IDataContainerResult[]> {
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;
}
/**
@@ -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();
}
}
@@ -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';
@@ -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<TResult extends IDatabaseDataResult> implements IDatabaseDataEditor<TResult> {
readonly editedResults: Map<string, IDataUpdate>;
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)!;
}
}
@@ -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<TOptions, TResult extends IDatabaseDataResult = IDatabaseDataResult>
implements IDatabaseDataModel<TOptions, TResult> {
id: string;
results: TResult[];
source: IDatabaseDataSource<TOptions, TResult>;
access: DatabaseDataAccessMode;
countGain: number;
get requestInfo(): IRequestInfo {
@@ -33,16 +31,12 @@ implements IDatabaseDataModel<TOptions, TResult> {
constructor(source: IDatabaseDataSource<TOptions, TResult>) {
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<TOptions, TResult> {
return this.source.offset <= offset && this.source.count >= count;
}
async refresh(): Promise<void> {
await this.requestData();
}
async reload(): Promise<void> {
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<TOptions, TResult> {
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<TOptions, TResult> {
return this;
}
async refresh(): Promise<void> {
await this.requestData();
}
async reload(): Promise<void> {
this.setSlice(0, this.countGain);
await this.requestData();
}
async requestDataPortion(offset: number, count: number): Promise<void> {
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<void> {
this.results = await this.source.requestData(this.results);
await this.source.requestData();
}
}
@@ -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<TOptions, TResult extends IDatabaseDataResult>
implements IDatabaseDataSource<TOptions, TResult> {
access: DatabaseDataAccessMode;
dataFormat: ResultDataFormat;
supportedDataFormats: ResultDataFormat[];
editor: IDatabaseDataEditor<TResult> | 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<TResult[]> | null;
@@ -32,17 +36,23 @@ implements IDatabaseDataSource<TOptions, TResult> {
constructor() {
makeObservable<DatabaseDataSource<TOptions, TResult>, '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<TOptions, TResult> {
abstract cancel(): Promise<boolean> | 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<TResult>): 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<TOptions, TResult> {
return this;
}
async requestData(prevResults: TResult[]): Promise<TResult[]> {
async requestData(): Promise<void> {
if (this.activeSave) {
try {
await this.activeSave;
@@ -98,25 +140,24 @@ implements IDatabaseDataSource<TOptions, TResult> {
}
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<TResult[]> {
async saveData(): Promise<void> {
if (this.activeRequest) {
try {
await this.activeRequest;
@@ -124,16 +165,17 @@ implements IDatabaseDataSource<TOptions, TResult> {
}
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<TOptions, TResult> {
}
abstract request(prevResults: TResult[]): TResult[] | Promise<TResult[]>;
abstract save(prevResults: TResult[], data: DataUpdate): Promise<TResult[]> | TResult[];
abstract save(prevResults: TResult[]): Promise<TResult[]> | TResult[];
abstract saveDeprecated(resultId: string, rows: RowDiff[]): Promise<IRequestDataResult>;
abstract dispose(): Promise<void>;
@@ -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<number, IResultEditingDiff>;
}
export interface IDatabaseDataEditor<TResult extends IDatabaseDataResult> {
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;
}
@@ -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<TOptions, TResult extends IDatabaseDataResult = IDatabaseDataResult> {
readonly id: string;
readonly results: TResult[];
readonly source: IDatabaseDataSource<TOptions, TResult>;
readonly access: DatabaseDataAccessMode;
readonly requestInfo: IRequestInfo;
readonly supportedDataFormats: ResultDataFormat[];
readonly countGain: number;
@@ -30,8 +23,8 @@ export interface IDatabaseDataModel<TOptions, TResult extends IDatabaseDataResul
getResult: (index: number) => 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;
@@ -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<T = any> {
data: SqlResultSet;
dataUpdate: SqlResultSet;
type: DataUpdateType;
export enum DatabaseDataAccessMode {
Default,
Readonly
}
export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResult = IDatabaseDataResult> {
readonly access: DatabaseDataAccessMode;
readonly dataFormat: ResultDataFormat;
readonly supportedDataFormats: ResultDataFormat[];
readonly editor: IDatabaseDataEditor<TResult> | 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<TResult>) => 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[]> | TResult[];
saveData: (
prevResults: TResult[],
data: DataUpdate
) => Promise<TResult[]> | TResult[];
requestData: () => Promise<void> | void;
saveData: () => Promise<void> | void;
/**
* @deprecated will be refactored
*/
@@ -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<IDataContainerOptions, I
}
setAccess(access: DatabaseDataAccessMode): this {
this.access = access;
this.source.setAccess(access);
for (const model of this.deprecatedModels) {
model.access = access;
@@ -117,7 +116,7 @@ export class DataModelWrapper extends DatabaseDataModel<IDataContainerOptions, I
async requestData(): Promise<void> {
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<IDataContainerOptions, I
this.source.setSlice(offset, count);
this.clearErrors();
try {
this.results = await this.source.requestData(this.results);
await this.source.requestData();
await this.setDeprecatedModelData();
} catch (exception) {
this.showError(exception);
@@ -164,12 +163,12 @@ export class DataModelWrapper extends DatabaseDataModel<IDataContainerOptions, I
* @deprecated will be removed
*/
private async setDeprecatedModelData() {
if (this.deprecatedModels.length !== this.results.length) {
this.deprecatedModels = this.deprecatedModels.slice(0, this.results.length);
if (this.deprecatedModels.length !== this.source.results.length) {
this.deprecatedModels = this.deprecatedModels.slice(0, this.source.results.length);
for (let i = this.deprecatedModels.length; i < this.results.length; i++) {
for (let i = this.deprecatedModels.length; i < this.source.results.length; i++) {
this.deprecatedModels.push(new TableViewerModel({
access: this.access,
access: this.source.access,
requestDataAsync: async (
model: TableViewerModel,
offset: number,
@@ -209,16 +208,16 @@ export class DataModelWrapper extends DatabaseDataModel<IDataContainerOptions, I
}
}
for (let i = 0; i < this.results.length; i++) {
for (let i = 0; i < this.source.results.length; i++) {
const model = this.getOldModel(i)!;
const result = this.getResult(i)!;
model.refresh();
model.setColumns(result.data.columns);
model.insertRows(0, result.data.rows, !result.loadedFully);
model.access = this.results.length > 1
model.access = this.source.results.length > 1
? DatabaseDataAccessMode.Readonly
: this.access;
: this.source.access;
}
}
@@ -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();
},
});
}
@@ -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}
@@ -151,7 +151,7 @@ export const TableViewer = observer(function TableViewer({
<Loader
loading={dataModel.isLoading()}
cancelDisabled={!dataModel.source.canCancel}
overlay={dataModel.results.length > 0 && presentation.dataFormat === dataFormat}
overlay={dataModel.source.results.length > 0 && presentation.dataFormat === dataFormat}
onCancel={() => dataModel.source.cancel()}
/>
</table-box>
@@ -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';
@@ -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';
@@ -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<IDataContainerOptions, IDataContainerResult> {
@@ -51,6 +52,7 @@ export class QueryDataSource extends DatabaseDataSource<IDataContainerOptions, I
});
this.queryExecutionProcess = null;
this.editor = new DatabaseDataEditor();
}
cancel(): boolean {
@@ -60,11 +62,56 @@ export class QueryDataSource extends DatabaseDataSource<IDataContainerOptions, I
return false;
}
save(
prevResults: IDataContainerResult[],
data: DataUpdate<any>
): IDataContainerResult[] | Promise<IDataContainerResult[]> {
throw new Error('Method not implemented.');
async save(
prevResults: IDataContainerResult[]
): Promise<IDataContainerResult[]> {
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 {
@@ -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);