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 5ac7d449d5..198e149057 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 @@ -40,9 +40,8 @@ export interface IEditorRef { focus: () => void; } -export const CellEditor = observer, 'rowIdx' | 'row' | 'column' | 'onClose'>, IEditorRef>(function CellEditor({ +export const CellEditor = observer, 'rowIdx' | 'column' | 'onClose'>, IEditorRef>(function CellEditor({ rowIdx, - row, column, onClose, }, ref) { @@ -51,12 +50,11 @@ export const CellEditor = observer, 'rowIdx' | 'row' const inputRef = useRef(null); const [elementRef, setElementRef] = useState(null); const [popperRef, setPopperRef] = useState(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, '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); }; 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 ede41b2db0..ecedd42460 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 @@ -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> = observer(function const editingContext = useContext(EditingContext); const mouse = useMouse({}); const cellContext = useMemo(() => ({ 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) { if (EventContext.has(event, EventStopPropagationFlag)) { return; @@ -83,11 +74,15 @@ export const CellRenderer: React.FC> = observer(function ); }, mouseUp(event: React.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> = observer(function ); }, doubleClick(event: React.MouseEvent) { - 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> = 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 ( {/** performance super heavy */} ); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts index f5526f5190..00b7b0b70e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts @@ -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', diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts index e0b8250cbf..9be8c45b46 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts @@ -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 => { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx index 48a3a5af70..67f6d7ae3a 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/useGridSelectionContext.tsx @@ -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({ 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 e8e77fdbc3..58a58c4f94 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -88,11 +88,7 @@ export const DataGridTable: React.FC { - 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 = 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>({ 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 = observer(function CellFormatter({ - {showCellMenu && context && ( + {showCellMenu && context && rest.column.columnDataIndex !== null && ( = observer(function CellFormatter({ spreadsheetActions={spreadsheetActions} resultIndex={context.resultIndex} row={rest.rowIdx} - column={Number(rest.column.key)} + column={rest.column.columnDataIndex} onStateSwitch={setMenuVisible} /> diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx index 1749238d54..2b5ad03718 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatterFactory.tsx @@ -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 = 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; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx index 69e0889244..28cad3e220 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatters/BooleanFormatter.tsx @@ -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 = observer(function BooleanFormatter({ column, row, rowIdx }) { +export const BooleanFormatter: React.FC = 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)( = observer(function TextFormatter({ rowIdx, row, column, isCellSelected }) { +export const TextFormatter: React.FC = observer(function TextFormatter({ rowIdx, column, isCellSelected }) { const editorRef = useRef(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 = observer(function TextFor @@ -66,7 +71,7 @@ export const TextFormatter: React.FC = observer(function TextFor return (
{isUrl && ( - + )} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index 6a216d3641..74802f766c 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -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> = 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) => { gridSelectionContext.selectColumn(calculatedColumn.idx, e.ctrlKey || e.metaKey); }; @@ -91,13 +105,13 @@ export const TableColumnHeader: React.FC> = observer(fu - - {readOnly && } + {icon && } + {!dataReadonly && columnReadOnly && } {columnName} - {sortable && column?.label && ( - + {!(dataReadonly || columnReadOnly) && calculatedColumn.columnDataIndex !== null && ( + )} ); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts index a965c0a784..5fc1efbc55 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableDataContext.ts @@ -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 { + columnDataIndex: null | number; + icon?: string; + } +} export interface ITableData { - data: { - columns: Array>; - rows: any[][]; - }; + format: ResultSetFormatAction; + data: ResultSetDataAction; + editor: IDatabaseDataResultEditor; + columns: Array>; dataColumns: SqlResultColumn[]; - dataRows: any[][]; - getCellValue: (rowIndex: number, key: string | number) => any; - getColumnInfo: (key: string | number) => SqlResultColumn | undefined; + dataRows: IResultSetValue[][]; + getColumn: (columnIndex: number) => Column; + getColumnByDataIndex: (columnDataIndex: number) => Column; + getCellValue: (rowIndex: number, columnIndex: number) => IResultSetValue | undefined; + getColumnInfo: (columnDataIndex: number) => SqlResultColumn | undefined; getColumnsInRange: (startIndex: number, endIndex: number) => Array>; - 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) => boolean; + isIndexColumn: (columnKey: string) => boolean; isIndexColumnInRange: (columnsRange: Array>) => boolean; isReadOnly: () => boolean; - getColumnKeyFromColumnIndex: (columnIndex: number) => number; } export const TableDataContext = createContext(null); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts index aea483db1c..d22de6e6eb 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -26,7 +26,6 @@ function copyGridSelectedDataToClipboard( selectedCells: Map ) { const format = model.source.getAction(resultIndex, ResultSetFormatAction); - const editor = model.source.getEditor(resultIndex); const orderedSelectedCells: Map = 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(''); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts index 46a02c1ace..95782a2f53 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts @@ -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 = { - key: String(Number.MAX_SAFE_INTEGER), + key: '#', + columnDataIndex: null, name: '#', minWidth: 60, width: 60, @@ -32,36 +32,44 @@ export const indexColumn: Column = { }; export function useTableData(model: IDatabaseDataModel, 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, }), }).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>((col, columnIndex) => ({ - key: columnIndex + '', + const columns: Array> = this.dataColumns.map>((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) { + if (column.columnDataIndex === null) { + return false; + } + return this.editor.isCellEdited(rowIndex, column.columnDataIndex); + }, + isIndexColumn(columnKey: string) { + return columnKey === indexColumn.key; }, isIndexColumnInRange(columnsRange: Array>) { return columnsRange.some(column => this.isIndexColumn(column.key)); @@ -130,11 +135,8 @@ export function useTableData(model: IDatabaseDataModel, 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, }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/ResultSetTools.ts b/webapp/packages/plugin-data-spreadsheet-new/src/ResultSetTools.ts deleted file mode 100644 index 31e279bfde..0000000000 --- a/webapp/packages/plugin-data-spreadsheet-new/src/ResultSetTools.ts +++ /dev/null @@ -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; - }, -}; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index 8c39554bba..b082e00853 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -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 implements IDatabaseDataResultAction { 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, 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= 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 []; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts index 26e1080a38..1628a4cf93 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts @@ -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 { static dataFormat = ResultDataFormat.Resultset; + private data: ResultSetDataAction; + + constructor(source: IDatabaseDataSource, 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 = ( diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts index 3793d06d46..b097104cfc 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction.ts @@ -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 @@ -27,9 +28,11 @@ export class ResultSetSelectAction extends DatabaseDataAction; private focusedElement: IResultSetElementKey | null; + private data: ResultSetDataAction; constructor(source: IDatabaseDataSource, 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 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 {