refactor: CB-1151 grid data presentation

This commit is contained in:
Wroud
2021-08-13 21:09:06 +03:00
parent 3460748758
commit f45346a200
19 changed files with 331 additions and 272 deletions
@@ -40,9 +40,8 @@ export interface IEditorRef {
focus: () => void;
}
export const CellEditor = observer<Pick<EditorProps<any, any>, 'rowIdx' | 'row' | 'column' | 'onClose'>, IEditorRef>(function CellEditor({
export const CellEditor = observer<Pick<EditorProps<any, any>, 'rowIdx' | 'column' | 'onClose'>, IEditorRef>(function CellEditor({
rowIdx,
row,
column,
onClose,
}, ref) {
@@ -51,12 +50,11 @@ export const CellEditor = observer<Pick<EditorProps<any, any>, 'rowIdx' | 'row'
const inputRef = useRef<HTMLInputElement>(null);
const [elementRef, setElementRef] = useState<HTMLDivElement | null>(null);
const [popperRef, setPopperRef] = useState<HTMLDivElement | null>(null);
const formatter = dataGridContext?.model.source.getAction(dataGridContext.resultIndex, ResultSetFormatAction);
const popper = usePopper(elementRef, popperRef, {
placement: 'right',
});
if (!dataGridContext || !tableDataContext) {
if (!dataGridContext || !tableDataContext || column.columnDataIndex === null) {
throw new Error('DataGridContext should be provided');
}
@@ -87,21 +85,28 @@ export const CellEditor = observer<Pick<EditorProps<any, any>, 'rowIdx' | 'row'
}
});
const value = formatter?.getText(row[column.key]) ?? '';
const value = tableDataContext.format
.getText(tableDataContext.getCellValue(rowIdx, column.columnDataIndex)!) ?? '';
const handleSave = () => onClose(false);
const handleReject = () => {
dataGridContext.model.source.getEditor(dataGridContext.resultIndex)
.revertCell(rowIdx, Number(column.key));
if (column.columnDataIndex !== null) {
tableDataContext.editor
.revertCell(rowIdx, column.columnDataIndex);
}
onClose(false);
};
const handleChange = (value: string) => {
dataGridContext.model.source.getEditor(dataGridContext.resultIndex)
.setCell(rowIdx, Number(column.key), value);
if (column.columnDataIndex !== null) {
tableDataContext.editor
.setCell(rowIdx, column.columnDataIndex, value);
}
};
const handleUndo = () => {
dataGridContext.model.source.getEditor(dataGridContext.resultIndex)
.revertCell(rowIdx, Number(column.key));
if (column.columnDataIndex !== null) {
tableDataContext.editor
.revertCell(rowIdx, column.columnDataIndex);
}
onClose(false);
};
@@ -6,7 +6,6 @@
* you may not use this file except in compliance with the License.
*/
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useContext, useMemo } from 'react';
import type { CellRendererProps } from 'react-data-grid';
@@ -14,7 +13,7 @@ import { Cell } from 'react-data-grid';
import { useMouse, useObjectRef } from '@cloudbeaver/core-blocks';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { isBooleanValuePresentationAvailable, ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer';
import { isBooleanValuePresentationAvailable } from '@cloudbeaver/plugin-data-viewer';
import { EditingContext } from '../../Editing/EditingContext';
import { DataGridContext } from '../DataGridContext';
@@ -30,37 +29,29 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
const editingContext = useContext(EditingContext);
const mouse = useMouse<HTMLDivElement>({});
const cellContext = useMemo<ICellContext>(() => ({ mouse }), [mouse]);
const resultColumn = tableDataContext?.getColumnInfo(column.key);
const editor = dataGridContext?.model.source.getEditor(dataGridContext.resultIndex);
const classes: string[] = [];
let classes = '';
if (selectionContext?.isSelected(rowIdx, column.idx)) {
classes.push('rdg-cell-custom-selected');
classes += ' rdg-cell-custom-selected';
}
if (editingContext?.isEditing({ idx: column.idx, rowIdx })) {
classes.push('rdg-cell-custom-editing');
classes += ' rdg-cell-custom-editing';
}
if (!tableDataContext?.isIndexColumn(column.key) && editor?.isCellEdited(rowIdx, Number(column.key))) {
classes.push('rdg-cell-custom-edited');
if (tableDataContext?.isCellEdited(rowIdx, column)) {
classes += ' rdg-cell-custom-edited';
}
const state = useObjectRef({
row: props.row,
column,
rowIdx,
resultColumn,
isCellSelected,
editor,
selectionContext,
dataGridContext,
editingContext,
tableDataContext,
get immutableRow() {
return this.editor?.get(this.rowIdx) || this.row; // performance heavy
},
mouseDown(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
if (EventContext.has(event, EventStopPropagationFlag)) {
return;
@@ -83,11 +74,15 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
);
},
mouseUp(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
if (!this.dataGridContext?.isGridInFocus() || EventContext.has(event, EventStopPropagationFlag)) {
if (
!this.selectionContext
|| !this.dataGridContext?.isGridInFocus()
|| EventContext.has(event, EventStopPropagationFlag)
) {
return;
}
this.selectionContext?.select(
this.selectionContext.select(
{
colIdx: this.column.idx,
rowIdx: this.rowIdx,
@@ -98,25 +93,30 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
);
},
doubleClick(event: React.MouseEvent<HTMLDivElement>) {
if (EventContext.has(event, EventStopPropagationFlag)) {
const columnIndex = this.column.columnDataIndex;
if (
!this.editingContext
|| !this.tableDataContext
|| EventContext.has(event, EventStopPropagationFlag)
|| columnIndex === null
) {
return;
}
const format = this.dataGridContext?.model.source.getAction(
this.dataGridContext.resultIndex,
ResultSetFormatAction
);
const columnIndex = this.tableDataContext?.getDataColumnIndexFromKey(this.column.key) ?? null;
const handleByBooleanFormatter = this.resultColumn && isBooleanValuePresentationAvailable(
this.editor?.getCell(this.rowIdx, Number(this.column.key)),
this.resultColumn
);
const resultColumn = this.tableDataContext.getColumnInfo(columnIndex);
const value = this.tableDataContext.getCellValue(this.rowIdx, columnIndex);
if (!resultColumn || value === undefined) {
return;
}
const handleByBooleanFormatter = isBooleanValuePresentationAvailable(value, resultColumn);
if (
!this.column.editable
|| handleByBooleanFormatter
|| columnIndex === null
|| format?.isReadOnly({
|| this.tableDataContext.format.isReadOnly({
row: this.rowIdx,
column: columnIndex,
})
@@ -124,38 +124,32 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
return;
}
this.editingContext?.edit({ idx: this.column.idx, rowIdx: this.rowIdx });
this.editingContext.edit({
idx: this.column.idx,
rowIdx: this.rowIdx,
});
},
}, {
row: props.row,
column,
rowIdx,
resultColumn,
isCellSelected,
editor,
selectionContext,
dataGridContext,
editingContext,
tableDataContext,
}, {
rowIdx: observable.ref,
row: observable.ref,
editor: observable.ref,
immutableRow: computed,
}, ['doubleClick', 'mouseUp', 'mouseDown']);
}, undefined, ['doubleClick', 'mouseUp', 'mouseDown']);
return (
<CellContext.Provider value={cellContext}>{/** performance super heavy */}
<Cell
ref={mouse.reference}
className={classes.join(' ')}
className={classes}
data-row-index={rowIdx}
data-column-index={column.idx}
onMouseDown={state.mouseDown}
onMouseUp={state.mouseUp}
onDoubleClick={state.doubleClick}
{...props}
row={state.immutableRow}
/>
</CellContext.Provider>
);
@@ -83,7 +83,7 @@ export class DataGridContextMenuCellEditingService {
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const cellValue = data.getCellValue({ column, row });
return data.getColumn(column)?.required || format.isNull(cellValue);
return cellValue === undefined || data.getColumn(column)?.required || format.isNull(cellValue);
},
order: 1,
title: 'data_grid_table_editing_set_to_null',
@@ -216,8 +216,9 @@ export class DataGridContextMenuFilterService {
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const supportedOperations = data.getColumnOperations(column);
const value = data.getCellValue({ column, row });
return supportedOperations.length === 0 || format.isNull(data.getCellValue({ column, row }));
return value === undefined || supportedOperations.length === 0 || format.isNull(value);
},
order: 1,
title: 'data_grid_table_filter_cell_value',
@@ -242,10 +243,12 @@ export class DataGridContextMenuFilterService {
return context.contextType === DataGridContextMenuService.cellContext;
},
isHidden: context => {
const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction);
const supportedOperations = data.getColumnOperations(context.data.column);
const { model, resultIndex, column, row } = context.data;
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const cellValue = data.getCellValue({ column, row });
const supportedOperations = data.getColumnOperations(column);
return supportedOperations.length === 0;
return cellValue === undefined || supportedOperations.length === 0;
},
order: 2,
title: 'data_grid_table_filter_custom_value',
@@ -260,6 +263,10 @@ export class DataGridContextMenuFilterService {
const cellValue = data.getCellValue({ column, row });
const columnLabel = data.getColumn(column)?.label || '';
if (!cellValue) {
return [];
}
return supportedOperations
.filter(operation => !nullOperationsFilter(operation))
.map(operation => {
@@ -34,16 +34,6 @@ export function useGridSelectionContext(
lastSelectedCell: null,
}));
function getRealColumnIndex(colIdx: number) {
let column: number | undefined = props.tableData.getColumnKeyFromColumnIndex(colIdx);
if (props.tableData.isIndexColumn(column)) {
column = undefined;
}
return column;
}
const selectRows = action(function selectRows(
startPosition: number,
lastPosition: number,
@@ -124,7 +114,11 @@ export function useGridSelectionContext(
selectRows(
startPosition.rowIdx,
lastPosition.rowIdx,
isIndexColumnInRange ? undefined : columnsInRange.map(column => Number(column.key)),
isIndexColumnInRange
? undefined
: (columnsInRange
.filter(column => column.columnDataIndex !== null)
.map(column => column.columnDataIndex!)),
multiple,
temporary
);
@@ -134,10 +128,14 @@ export function useGridSelectionContext(
colIdx: number,
multiple: boolean
) {
const { selectionAction, tableData } = props;
state.temporarySelection.clear();
const column = getRealColumnIndex(colIdx);
const column = tableData
.getColumn(colIdx)
.columnDataIndex ?? undefined;
const { selectionAction } = props;
const selected = selectionAction.isElementSelected({ column });
if (!multiple) {
@@ -153,7 +151,9 @@ export function useGridSelectionContext(
}
function isSelected(rowIdx: number, colIdx: number) {
const column = getRealColumnIndex(colIdx);
const column = props.tableData
.getColumn(colIdx)
.columnDataIndex ?? undefined;
const temporaryRowSelection = state.temporarySelection.get(rowIdx);
@@ -205,8 +205,8 @@ export function useGridSelectionContext(
function select(cell: IDraggingPosition, multiple: boolean, range: boolean, temporary: boolean) {
const { lastSelectedCell } = state;
const columnKey = props.tableData.getColumnKeyFromColumnIndex(cell.colIdx);
const isIndexColumn = props.tableData.isIndexColumn(columnKey);
const column = props.tableData.getColumn(cell.colIdx);
const isIndexColumn = props.tableData.isIndexColumn(column.key);
if (!temporary) {
state.lastSelectedCell = cell;
@@ -226,7 +226,9 @@ export function useGridSelectionContext(
return;
}
selectCell(cell.rowIdx, columnKey, multiple, temporary);
if (column.columnDataIndex !== null) {
selectCell(cell.rowIdx, column.columnDataIndex, multiple, temporary);
}
}
return useObjectRef<IDataGridSelectionContext>({
@@ -88,11 +88,7 @@ export const DataGridTable: React.FC<IDataPresentationProps<any, IDatabaseResult
return;
}
const idx = tableData.getColumnIndexFromKey(data.column);
if (idx === null) {
return;
}
const idx = tableData.getColumnIndexFromKey(tableData.getColumnByDataIndex(data.column).key);
editingContext.closeEditor({
rowIdx: data.row,
@@ -106,12 +102,11 @@ export const DataGridTable: React.FC<IDataPresentationProps<any, IDatabaseResult
}, [model.source, resultIndex]);
const handleFocusChange = (position: CellPosition) => {
const key = tableData.getColumnKeyFromColumnIndex(position.idx);
const column = tableData.getColumnInfo(key);
const column = tableData.getColumn(position.idx);
selectionAction.focus({
row: position.rowIdx,
column: column ? key : 0,
column: column.columnDataIndex ?? 0,
});
};
@@ -157,13 +152,13 @@ export const DataGridTable: React.FC<IDataPresentationProps<any, IDatabaseResult
<DataGrid
ref={dataGridRef}
className={`cb-react-grid-theme ${className}`}
columns={tableData.data.columns}
columns={tableData.columns}
defaultColumnOptions={{
minWidth: 50,
resizable: true,
formatter: CellFormatter,
}}
rows={tableData.data.rows}
rows={tableData.dataRows}
headerRowHeight={28}
rowHeight={25}
rowRenderer={RowRenderer}
@@ -44,19 +44,22 @@ export const CellFormatter: React.FC<Props> = observer(function CellFormatter({
const editingContext = useContext(EditingContext);
const [menuVisible, setMenuVisible] = useState(false);
const isEditing = editingContext?.isEditing({ idx: rest.column.idx, rowIdx: rest.rowIdx }) ?? false;
const showCellMenu = !isEditing
&& (rest.isCellSelected || cellContext?.mouse.state.mouseEnter || menuVisible);
const showCellMenu = !isEditing && (
rest.isCellSelected
|| cellContext?.mouse.state.mouseEnter
|| menuVisible
);
const spreadsheetActions = useObjectRef<IDataPresentationActions<IResultSetElementKey>>({
edit(position) {
if (position.column === undefined || position.row === undefined) {
if (position.column === undefined || position.row === undefined || !tableDataContext || !editingContext) {
return;
}
const idx = tableDataContext?.getColumnIndexFromKey(position.column);
const idx = tableDataContext.getColumnIndexFromKey(tableDataContext.getColumnByDataIndex(position.column).key);
if (idx !== undefined && idx !== null) {
editingContext?.edit({ idx, rowIdx: position.row });
if (idx !== -1) {
editingContext.edit({ idx, rowIdx: position.row });
}
},
});
@@ -66,7 +69,7 @@ export const CellFormatter: React.FC<Props> = observer(function CellFormatter({
<formatter-container>
<CellFormatterFactory {...rest} isEditing={isEditing} />
</formatter-container>
{showCellMenu && context && (
{showCellMenu && context && rest.column.columnDataIndex !== null && (
<menu-container>
<CellMenu
model={context.model}
@@ -74,7 +77,7 @@ export const CellFormatter: React.FC<Props> = observer(function CellFormatter({
spreadsheetActions={spreadsheetActions}
resultIndex={context.resultIndex}
row={rest.rowIdx}
column={Number(rest.column.key)}
column={rest.column.columnDataIndex}
onStateSwitch={setMenuVisible}
/>
</menu-container>
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import { useContext, useRef } from 'react';
import type { FormatterProps } from 'react-data-grid';
import { isBooleanValuePresentationAvailable, ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer';
import { isBooleanValuePresentationAvailable } from '@cloudbeaver/plugin-data-viewer';
import { DataGridContext } from '../DataGridContext';
import { TableDataContext } from '../TableDataContext';
@@ -29,10 +29,10 @@ export const CellFormatterFactory: React.FC<IProps> = observer(function CellForm
if (!props.isEditing || formatterRef === null) {
formatterRef.current = TextFormatter;
if (tableDataContext && context) {
const resultColumn = tableDataContext.getColumnInfo(props.column.key);
const formatter = context.model.source.getAction(context.resultIndex, ResultSetFormatAction);
const rawValue = formatter.get(props.row[props.column.key]);
if (tableDataContext && context && props.column.columnDataIndex !== null) {
const resultColumn = tableDataContext.getColumnInfo(props.column.columnDataIndex);
const rawValue = tableDataContext.format
.get(tableDataContext.getCellValue(props.rowIdx, props.column.columnDataIndex)!);
if (resultColumn && isBooleanValuePresentationAvailable(rawValue, resultColumn)) {
formatterRef.current = BooleanFormatter;
@@ -11,8 +11,6 @@ import { useCallback, useContext } from 'react';
import type { FormatterProps } from 'react-data-grid';
import styled, { use, css } from 'reshadow';
import { ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer';
import { EditingContext } from '../../../Editing/EditingContext';
import { DataGridContext } from '../../DataGridContext';
import { TableDataContext } from '../../TableDataContext';
@@ -32,28 +30,41 @@ const styles = css`
}
`;
export const BooleanFormatter: React.FC<FormatterProps> = observer(function BooleanFormatter({ column, row, rowIdx }) {
export const BooleanFormatter: React.FC<FormatterProps> = observer(function BooleanFormatter({ column, rowIdx }) {
const context = useContext(DataGridContext);
const tableDataContext = useContext(TableDataContext);
const editingContext = useContext(EditingContext);
const formatter = context?.model.source.getAction(context.resultIndex, ResultSetFormatAction);
const resultColumn = tableDataContext?.getColumnInfo(column.key);
const rawValue = formatter?.get(row[column.key]) ?? row[column.key];
if (!context || !tableDataContext || !editingContext || column.columnDataIndex === null) {
throw new Error('Contexts required');
}
const formatter = tableDataContext.format;
const rawValue = formatter.get(tableDataContext.getCellValue(rowIdx, column.columnDataIndex)!);
const value = typeof rawValue === 'string' ? rawValue.toLowerCase() === 'true' : rawValue;
const stringifiedValue = formatter?.toDisplayString(value) ?? String(value);
const stringifiedValue = formatter.toDisplayString(value);
const valueRepresentation = value === null ? stringifiedValue : `[${value ? 'v' : ' '}]`;
const disabled = !column.editable || !!editingContext?.readonly;
const disabled = (
!column.editable
|| !!editingContext.readonly
|| formatter.isReadOnly({ row: rowIdx, column: column.columnDataIndex })
);
const toggleValue = useCallback(() => {
if (disabled) {
if (disabled || column.columnDataIndex === null) {
return;
}
const resultColumn = tableDataContext.getColumnInfo(column.columnDataIndex);
if (!resultColumn) {
return;
}
const nextValue = !resultColumn?.required && value === false ? null : !value;
const nextValue = !resultColumn.required && value === false ? null : !value;
context?.model.source.getEditor(context.resultIndex).setCell(rowIdx, Number(column.key), nextValue);
}, [context, resultColumn, column.key, rowIdx, value, disabled]);
tableDataContext.editor
.setCell(rowIdx, column.columnDataIndex, nextValue);
}, [tableDataContext, column, rowIdx, value, disabled]);
return styled(styles)(
<boolean-formatter
@@ -12,11 +12,11 @@ import type { FormatterProps } from 'react-data-grid';
import { IconOrImage } from '@cloudbeaver/core-blocks';
import { isValidUrl } from '@cloudbeaver/core-utils';
import { ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer';
import { EditingContext } from '../../../Editing/EditingContext';
import { CellEditor, IEditorRef } from '../../CellEditor/CellEditor';
import { DataGridContext } from '../../DataGridContext';
import { TableDataContext } from '../../TableDataContext';
function getClasses(rawValue: any) {
const classes = ['text-formatter'];
@@ -26,22 +26,28 @@ function getClasses(rawValue: any) {
return classes.join(' '); // performance heavy
}
export const TextFormatter: React.FC<FormatterProps> = observer(function TextFormatter({ rowIdx, row, column, isCellSelected }) {
export const TextFormatter: React.FC<FormatterProps> = observer(function TextFormatter({ rowIdx, column, isCellSelected }) {
const editorRef = useRef<IEditorRef>(null);
const context = useContext(DataGridContext);
const editingContext = useContext(EditingContext);
const formatter = context?.model.source.getAction(context.resultIndex, ResultSetFormatAction);
const rawValue = formatter?.get(row[column.key]) ?? row[column.key];
const tableDataContext = useContext(TableDataContext);
if (!context || !tableDataContext || !editingContext || column.columnDataIndex === null) {
throw new Error('Contexts required');
}
const formatter = tableDataContext.format;
const rawValue = formatter.get(tableDataContext.getCellValue(rowIdx, column.columnDataIndex)!);
const classes = getClasses(rawValue);
const value = formatter?.toDisplayString(rawValue) ?? String(rawValue);
const value = formatter.toDisplayString(rawValue);
const handleClose = useCallback(() => {
editingContext?.closeEditor({ idx: column.idx, rowIdx });
editingContext.closeEditor({ idx: column.idx, rowIdx });
}, [column, rowIdx]);
useEffect(() => {
if (isCellSelected) {
if (editingContext?.isEditing({ idx: column.idx, rowIdx })) {
if (editingContext.isEditing({ idx: column.idx, rowIdx })) {
editorRef.current?.focus();
}
}
@@ -53,7 +59,6 @@ export const TextFormatter: React.FC<FormatterProps> = observer(function TextFor
<CellEditor
ref={editorRef}
rowIdx={rowIdx}
row={row}
column={column}
onClose={handleClose}
/>
@@ -66,7 +71,7 @@ export const TextFormatter: React.FC<FormatterProps> = observer(function TextFor
return (
<div title={value} className={classes}>
{isUrl && (
<a href={rawValue} target='_blank' rel='noreferrer' draggable={false}>
<a href={rawValue as string} target='_blank' rel='noreferrer' draggable={false}>
<IconOrImage icon='external-link' viewBox='0 0 24 24' />
</a>
)}
@@ -13,7 +13,6 @@ import styled, { css } from 'reshadow';
import { StaticImage } from '@cloudbeaver/core-blocks';
import { useStyles } from '@cloudbeaver/core-theming';
import { ResultSetDataAction } from '@cloudbeaver/plugin-data-viewer';
import { DataGridContext } from '../DataGridContext';
import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext';
@@ -73,16 +72,31 @@ export const TableColumnHeader: React.FC<HeaderRendererProps<any>> = observer(fu
const resultIndex = dataGridContext.resultIndex;
const model = dataGridContext.model;
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const column = data.getColumn(Number(calculatedColumn.key));
const columnName = calculatedColumn.name as string;
let icon = calculatedColumn.icon;
let columnName = calculatedColumn.name as string;
const dataReadonly = tableDataContext.isReadOnly() || model.isReadonly();
let columnReadOnly = !calculatedColumn.editable;
let columnTooltip: string = columnName;
// TODO we want to get "sortable" property from SqlResultColumn data
const sortable = model.source.results.length === 1 && !model.source.isDisabled(resultIndex);
const readOnly = !tableDataContext.isReadOnly() && column?.readOnly;
const readOnlyStatus = column?.readOnlyStatus ? `(Read-only: ${column.readOnlyStatus})` : '';
const columnTooltip = `${columnName}${column?.fullTypeName ? ': ' + column.fullTypeName : ''} ${readOnlyStatus}`;
if (calculatedColumn.columnDataIndex !== null) {
const column = tableDataContext.data.getColumn(calculatedColumn.columnDataIndex);
if (column) {
columnName = column.label!;
icon = column.icon;
columnReadOnly ||= tableDataContext.format.isReadOnly({ column: calculatedColumn.columnDataIndex });
columnTooltip = columnName;
if (column.fullTypeName) {
columnTooltip += `: ${column.fullTypeName}`;
}
if (column.readOnlyStatus) {
columnTooltip += ` (Read-only: ${column.readOnlyStatus})`;
}
}
}
const handleColumnSelection = (e: React.MouseEvent<HTMLDivElement>) => {
gridSelectionContext.selectColumn(calculatedColumn.idx, e.ctrlKey || e.metaKey);
};
@@ -91,13 +105,13 @@ export const TableColumnHeader: React.FC<HeaderRendererProps<any>> = observer(fu
<table-header>
<shrink-container as='div' title={columnTooltip} onClick={handleColumnSelection}>
<icon>
<StaticImage icon={column?.icon} />
{readOnly && <readonly-status className='rdg-table-header__readonly-status' />}
{icon && <StaticImage icon={icon} />}
{!dataReadonly && columnReadOnly && <readonly-status className='rdg-table-header__readonly-status' />}
</icon>
<name>{columnName}</name>
</shrink-container>
{sortable && column?.label && (
<OrderButton model={model} resultIndex={resultIndex} attribute={column.label} />
{!(dataReadonly || columnReadOnly) && calculatedColumn.columnDataIndex !== null && (
<OrderButton model={model} resultIndex={resultIndex} attribute={columnName} />
)}
</table-header>
);
@@ -10,23 +10,32 @@ import { createContext } from 'react';
import type { Column } from 'react-data-grid';
import type { SqlResultColumn } from '@cloudbeaver/core-sdk';
import type { IDatabaseDataResultEditor, IDatabaseResultSet, IResultSetValue, ResultSetDataAction, ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer';
declare module 'react-data-grid' {
interface Column<TRow, TSummaryRow = unknown> {
columnDataIndex: null | number;
icon?: string;
}
}
export interface ITableData {
data: {
columns: Array<Column<any[], any>>;
rows: any[][];
};
format: ResultSetFormatAction;
data: ResultSetDataAction;
editor: IDatabaseDataResultEditor<IDatabaseResultSet>;
columns: Array<Column<any[], any>>;
dataColumns: SqlResultColumn[];
dataRows: any[][];
getCellValue: (rowIndex: number, key: string | number) => any;
getColumnInfo: (key: string | number) => SqlResultColumn | undefined;
dataRows: IResultSetValue[][];
getColumn: (columnIndex: number) => Column<any[], any>;
getColumnByDataIndex: (columnDataIndex: number) => Column<any[], any>;
getCellValue: (rowIndex: number, columnIndex: number) => IResultSetValue | undefined;
getColumnInfo: (columnDataIndex: number) => SqlResultColumn | undefined;
getColumnsInRange: (startIndex: number, endIndex: number) => Array<Column<any[], any>>;
getDataColumnIndexFromKey: (columnKey: string | number) => number | null;
getColumnIndexFromKey: (columnKey: string | number) => number | null;
isIndexColumn: (columnKey: string | number) => boolean;
getColumnIndexFromKey: (columnKey: string) => number;
isCellEdited: (rowIndex: number, column: Column<any[], any>) => boolean;
isIndexColumn: (columnKey: string) => boolean;
isIndexColumnInRange: (columnsRange: Array<Column<any[], any>>) => boolean;
isReadOnly: () => boolean;
getColumnKeyFromColumnIndex: (columnIndex: number) => number;
}
export const TableDataContext = createContext<ITableData | null>(null);
@@ -26,7 +26,6 @@ function copyGridSelectedDataToClipboard(
selectedCells: Map<number, number[]>
) {
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const editor = model.source.getEditor(resultIndex);
const orderedSelectedCells: Map<number, number[]> = new Map([...selectedCells].sort((a, b) => a[0] - b[0]));
@@ -40,15 +39,14 @@ function copyGridSelectedDataToClipboard(
const rowsValues: string[] = [];
for (const [rowIdx, colIndexes] of orderedSelectedCells) {
const rowCellsValues: string[] = [];
for (const column of tableData.data.columns) {
const columnIdx = tableData.getDataColumnIndexFromKey(column.key);
if (columnIdx === null || !selectedColumns.has(columnIdx)) {
for (const column of tableData.columns) {
if (column.columnDataIndex === null || !selectedColumns.has(column.columnDataIndex)) {
continue;
}
if (colIndexes.includes(columnIdx)) {
const cell = editor.getCell(rowIdx, columnIdx);
const cellValue = format.getText(cell);
if (colIndexes.includes(column.columnDataIndex)) {
const cell = tableData.getCellValue(rowIdx, column.columnDataIndex);
const cellValue = cell !== undefined ? format.getText(cell) : undefined;
rowCellsValues.push(cellValue ?? '');
} else {
rowCellsValues.push('');
@@ -11,17 +11,17 @@ import type { Column } from 'react-data-grid';
import { useObjectRef } from '@cloudbeaver/core-blocks';
import type { SqlResultColumn } from '@cloudbeaver/core-sdk';
import { TextTools } from '@cloudbeaver/core-utils';
import { IDatabaseDataModel, IDatabaseResultSet, ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer';
import { TextTools, uuid } from '@cloudbeaver/core-utils';
import { IDatabaseDataModel, IDatabaseResultSet, ResultSetDataAction, ResultSetFormatAction } from '@cloudbeaver/plugin-data-viewer';
import { ResultSetTools } from '../ResultSetTools';
import { IndexFormatter } from './Formatters/IndexFormatter';
import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader';
import { TableIndexColumnHeader } from './TableColumnHeader/TableIndexColumnHeader';
import type { ITableData } from './TableDataContext';
export const indexColumn: Column<any[], any> = {
key: String(Number.MAX_SAFE_INTEGER),
key: '#',
columnDataIndex: null,
name: '#',
minWidth: 60,
width: 60,
@@ -32,36 +32,44 @@ export const indexColumn: Column<any[], any> = {
};
export function useTableData(model: IDatabaseDataModel<any, IDatabaseResultSet>, resultIndex: number): ITableData {
const modelResultData = model.getResult(resultIndex);
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
const data = model.source.getAction(resultIndex, ResultSetDataAction);
const editor = model.source.getEditor(resultIndex);
const props = useObjectRef({
modelResultData,
model,
resultIndex,
format,
data,
editor,
},
undefined,
{
modelResultData: observable.ref,
model: observable.ref,
resultIndex: observable.ref,
format: observable.ref,
data: observable.ref,
editor: observable.ref,
});
return useObjectRef({
get dataColumns() {
return props.modelResultData?.data?.columns || [];
},
get dataRows() {
return props.modelResultData?.data?.rows || [];
get format() {
return props.format;
},
get data() {
if (!props.modelResultData?.data) {
return { columns: [], rows: [] };
return props.data;
},
get editor() {
return props.editor;
},
get dataColumns() {
return this.data.columns;
},
get dataRows() {
return this.data.rows;
},
get columns() {
if (this.dataColumns.length === 0) {
return [];
}
const format = model.source.getAction(resultIndex, ResultSetFormatAction);
// TODO: seems it must be moved to ResultSetFormatAction
const columnNames = ResultSetTools.getHeaders(props.modelResultData.data);
const rowStrings = ResultSetTools.getLongestCells(props.modelResultData.data);
const columnNames = this.format.getHeaders();
const rowStrings = this.format.getLongestCells();
// TODO: seems better to do not measure container size
// for detecting max columns size, better to use configurable variable
@@ -75,54 +83,51 @@ export function useTableData(model: IDatabaseDataModel<any, IDatabaseResultSet>,
}),
}).map(v => v + 16 + 32 + 20);
// TODO: we need some result type specified formatter to common actions with data
const rows = props.modelResultData.data?.rows || [];
const columns = props.modelResultData.data?.columns!.map<Column<any[], any>>((col, columnIndex) => ({
key: columnIndex + '',
const columns: Array<Column<any[], any>> = this.dataColumns.map<Column<any[], any>>((col, columnIndex) => ({
key: uuid(),
columnDataIndex: columnIndex,
name: col.label!,
editable: !format.isReadOnly({ column: columnIndex }),
editable: true,
width: Math.min(300, measuredCells[columnIndex]),
headerRenderer: TableColumnHeader,
})) || [];
}));
columns.unshift(indexColumn);
return { rows, columns };
return columns;
},
getCellValue(rowIndex: number, key: string | number): any {
return this.data.rows[rowIndex][key as number];
getColumn(columnIndex: number) {
return this.columns[columnIndex];
},
getColumnInfo(key: string | number): SqlResultColumn | undefined {
if (this.isIndexColumn(key)) {
return;
}
return this.dataColumns[Number(key)]; // performance heavy
getColumnByDataIndex(columnDataIndex: number) {
return this.columns.find(column => column.columnDataIndex === columnDataIndex)!;
},
getDataColumnIndexFromKey(key: string | number) {
const info = this.getColumnInfo(key);
if (!info) {
return null;
}
return Number(key);
getColumnInfo(columnDataIndex: number): SqlResultColumn | undefined {
return this.data.getColumn(columnDataIndex);
},
getColumnIndexFromKey(key: string | number) {
const index = this.data.columns.findIndex((column: any) => column.key === String(key));
return index === -1 ? null : index;
getCellValue(row: number, column: number) {
return this.editor.getCell(row, column);
// return props.data.getCellValue({ column, row });
},
getColumnIndexFromKey(key: string) {
return this.columns.findIndex(column => column.key === key);
},
getColumnsInRange(startIndex: number, endIndex: number) {
if (startIndex === endIndex) {
return [this.data.columns[startIndex]];
return [this.columns[startIndex]];
}
const firstIndex = Math.min(startIndex, endIndex);
const lastIndex = Math.max(startIndex, endIndex);
return this.data.columns.slice(firstIndex, lastIndex + 1);
return this.columns.slice(firstIndex, lastIndex + 1);
},
isIndexColumn(columnKey: string | number) {
return String(columnKey) === indexColumn.key;
isCellEdited(rowIndex: number, column: Column<any[], any>) {
if (column.columnDataIndex === null) {
return false;
}
return this.editor.isCellEdited(rowIndex, column.columnDataIndex);
},
isIndexColumn(columnKey: string) {
return columnKey === indexColumn.key;
},
isIndexColumnInRange(columnsRange: Array<Column<any[], any>>) {
return columnsRange.some(column => this.isIndexColumn(column.key));
@@ -130,11 +135,8 @@ export function useTableData(model: IDatabaseDataModel<any, IDatabaseResultSet>,
isReadOnly() {
return this.dataColumns.every(column => column.readOnly);
},
getColumnKeyFromColumnIndex(columnIndex: number) {
return Number(this.data.columns[columnIndex].key);
},
}, null, {
data: computed,
columns: computed,
dataColumns: computed,
dataRows: computed,
});
@@ -1,47 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 type { SqlResultSet } from '@cloudbeaver/core-sdk';
import { isResultSetContentValue } from '@cloudbeaver/plugin-data-viewer';
export const ResultSetTools = {
getHeaders(resultSet: SqlResultSet): string[] {
return resultSet.columns?.map(column => column.name!).filter(Boolean) || [];
},
getRows(resultSet: SqlResultSet, offset = 0, count?: number): any[][] {
return resultSet.rows?.slice(offset, count) || [];
},
getLongestCells(resultSet: SqlResultSet, offset = 0, count?: number): string[] {
const rows = this.getRows(resultSet, offset, count);
let cells: string[] = [];
function getStringValue(value: any): string {
if (isResultSetContentValue(value) && value.text !== undefined) {
return value.text;
}
return String(value);
}
for (const row of rows) {
if (cells.length === 0) {
cells = row.map(v => getStringValue(v));
continue;
}
for (let i = 0; i < row.length; i++) {
const value = getStringValue(row[i]);
if (value.length > cells[i].length) {
cells[i] = value;
}
}
}
return cells;
},
};
@@ -6,27 +6,52 @@
* you may not use this file except in compliance with the License.
*/
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { computed, makeObservable } from 'mobx';
import { DataTypeLogicalOperation, ResultDataFormat, SqlResultColumn } from '@cloudbeaver/core-sdk';
import { DatabaseDataAction } from '../../DatabaseDataAction';
import type { IDatabaseDataSource } from '../../IDatabaseDataSource';
import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
import { databaseDataAction } from '../DatabaseDataActionDecorator';
import type { IDatabaseDataResultAction } from '../IDatabaseDataResultAction';
import type { IResultSetContentValue } from './IResultSetContentValue';
import type { IResultSetElementKey } from './IResultSetElementKey';
import { isResultSetContentValue } from './isResultSetContentValue';
import type { IResultSetValue } from './ResultSetFormatAction';
@databaseDataAction()
export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResultSet>
implements IDatabaseDataResultAction<IDatabaseResultSet> {
static dataFormat = ResultDataFormat.Resultset;
getCellValue(cell: IResultSetElementKey): any {
if (cell.row === undefined || cell.column === undefined || !this.result.data?.rows) {
get rows(): IResultSetValue[][] {
return this.result.data?.rows || [];
}
get columns(): SqlResultColumn[] {
return this.result.data?.columns || [];
}
constructor(source: IDatabaseDataSource<any, IDatabaseResultSet>, result: IDatabaseResultSet) {
super(source, result);
makeObservable(this, {
rows: computed,
columns: computed,
});
}
getCellValue(cell: IResultSetElementKey): IResultSetValue | undefined {
if (
cell.row === undefined
|| cell.column === undefined
|| cell.row >= this.rows.length
|| cell.column >= this.columns.length
) {
return undefined;
}
return this.result.data.rows?.[cell.row]?.[cell.column];
return this.rows[cell.row][cell.column];
}
getContent(cell: IResultSetElementKey): IResultSetContentValue | null {
@@ -39,16 +64,17 @@ export class ResultSetDataAction extends DatabaseDataAction<any, IDatabaseResult
return null;
}
getColumn(columnIndex: number) {
if (!this.result.data?.columns) {
getColumn(columnIndex: number): SqlResultColumn | undefined {
if (columnIndex >= this.columns.length) {
return undefined;
}
return this.result.data.columns[columnIndex];
return this.columns[columnIndex];
}
getColumnOperations(columnIndex: number) {
getColumnOperations(columnIndex: number): DataTypeLogicalOperation[] {
const column = this.getColumn(columnIndex);
if (!column) {
return [];
}
@@ -9,6 +9,7 @@
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { DatabaseDataAction } from '../../DatabaseDataAction';
import type { IDatabaseDataSource } from '../../IDatabaseDataSource';
import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
import { databaseDataAction } from '../DatabaseDataActionDecorator';
import type { IDatabaseDataFormatAction } from '../IDatabaseDataFormatAction';
@@ -24,17 +25,48 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
implements IDatabaseDataFormatAction<IResultSetElementKey, IDatabaseResultSet> {
static dataFormat = ResultDataFormat.Resultset;
private data: ResultSetDataAction;
constructor(source: IDatabaseDataSource<any, IDatabaseResultSet>, result: IDatabaseResultSet) {
super(source, result);
this.data = this.getAction(ResultSetDataAction);
}
getHeaders(): string[] {
return this.data.columns.map(column => column.name!).filter(Boolean);
}
getLongestCells(offset = 0, count?: number): string[] {
const rows = this.data.rows.slice(offset, count);
let cells: string[] = [];
for (const row of rows) {
if (cells.length === 0) {
cells = row.map(v => this.toDisplayString(v));
continue;
}
for (let i = 0; i < row.length; i++) {
const value = this.toDisplayString(row[i]);
if (value.length > cells[i].length) {
cells[i] = value;
}
}
}
return cells;
}
isReadOnly(key: IResultSetElementKey): boolean {
let columnReadonly = false;
let cellReadonly = false;
const data = this.getAction(ResultSetDataAction);
if (key.column !== undefined) {
columnReadonly = data.getColumn(key.column)?.readOnly || false;
columnReadonly = this.data.getColumn(key.column)?.readOnly || false;
}
const value = data.getCellValue(key);
const value = this.data.getCellValue(key);
if (isResultSetContentValue(value)) {
cellReadonly = (
@@ -17,6 +17,7 @@ import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
import { databaseDataAction } from '../DatabaseDataActionDecorator';
import type { DatabaseDataSelectActionsData, IDatabaseDataSelectAction } from '../IDatabaseDataSelectAction';
import type { IResultSetElementKey } from './IResultSetElementKey';
import { ResultSetDataAction } from './ResultSetDataAction';
@databaseDataAction()
export class ResultSetSelectAction extends DatabaseDataAction<any, IDatabaseResultSet>
@@ -27,9 +28,11 @@ export class ResultSetSelectAction extends DatabaseDataAction<any, IDatabaseResu
readonly selectedElements: Map<number, number[]>;
private focusedElement: IResultSetElementKey | null;
private data: ResultSetDataAction;
constructor(source: IDatabaseDataSource<any, IDatabaseResultSet>, result: IDatabaseResultSet) {
super(source, result);
this.data = this.getAction(ResultSetDataAction);
this.actions = new Executor();
this.selectedElements = new Map();
this.focusedElement = null;
@@ -46,7 +49,7 @@ export class ResultSetSelectAction extends DatabaseDataAction<any, IDatabaseResu
isElementSelected(key: IResultSetElementKey): boolean {
if (key.row === undefined) {
const rows = this.result.data?.rows?.length || 0;
const rows = this.data.rows.length;
for (let row = 0; row < rows; row++) {
if (!this.isElementSelected({ row, column: key.column })) {
return false;
@@ -66,7 +69,7 @@ export class ResultSetSelectAction extends DatabaseDataAction<any, IDatabaseResu
return row.includes(key.column);
}
return row.length === this.result.data?.columns?.length;
return row.length === this.data.columns.length;
}
getFocusedElement(): IResultSetElementKey | null {
@@ -89,7 +92,7 @@ export class ResultSetSelectAction extends DatabaseDataAction<any, IDatabaseResu
set(key: IResultSetElementKey, selected: boolean): void {
if (key.row === undefined) {
for (let row = 0; row < (this.result.data?.rows?.length || 0); row++) {
for (let row = 0; row < this.data.rows.length; row++) {
this.set({ row, column: key.column }, selected);
}
@@ -97,7 +100,7 @@ export class ResultSetSelectAction extends DatabaseDataAction<any, IDatabaseResu
}
if (key.column === undefined) {
for (let column = 0; column < (this.result.data?.columns?.length || 0); column++) {
for (let column = 0; column < this.data.columns.length; column++) {
this.set({ row: key.row, column }, selected);
}
return;
@@ -34,7 +34,7 @@ export class DatabaseDataEditor<TResult extends IDatabaseDataResult> implements
const update = this.editedResults.get(result.id);
const diff = update?.diff.get(row);
return !!diff?.source.some((value, index) => !this.compareCellValue(value, diff.update[index]));
return diff !== undefined;
}
isResultEdited(result: TResult): boolean {