From 422e4ced7f9edccb258e9c25bf406a1c34b08719 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 15 Feb 2021 13:42:46 +0300 Subject: [PATCH 1/7] feat(plugin-data-spreadsheet-new): CB-696 add multiselection --- .../DataGrid/CellRenderer/CellRenderer.tsx | 6 +- .../DataGridSelectionContext.ts | 5 + .../useGridSelectionContext.tsx | 169 +++++++++++++--- .../src/DataGrid/DataGridTable.tsx | 38 +++- .../TableColumnHeader/TableColumnHeader.tsx | 14 +- .../TableIndexColumnHeader.tsx | 28 +++ .../src/DataGrid/useGridDragging.ts | 181 ++++++++++++++++++ ...tainer.tsx => useGridSelectedCellsCopy.ts} | 49 ++--- .../src/styles/base.scss | 7 + 9 files changed, 419 insertions(+), 78 deletions(-) create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts rename webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/{DataGridTableContainer.tsx => useGridSelectedCellsCopy.ts} (60%) 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 a0fb9e90a6..8ee32f6231 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 @@ -36,7 +36,7 @@ export const CellRenderer: React.FC> = observer(function classes.push('rdg-cell-custom-edited'); } - const handleClick = useCallback((event: React.MouseEvent) => { + const handleMouseDown = useCallback((event: React.MouseEvent) => { selectionContext?.select(column.key, rowIdx, event.ctrlKey, event.shiftKey); }, [column, rowIdx, selectionContext]); @@ -49,7 +49,9 @@ export const CellRenderer: React.FC> = observer(function return ( ; select: (key: string, rowIdx: number, multiple: boolean, range: boolean) => void; + selectRange: (startPosition: IPosition, lastPosition: IPosition, multiple: boolean, temporary: boolean) => void; + selectColumn: (colKey: string, multiple: boolean) => void; + selectTable: () => void; isSelected: (key: string, rowIdx: number) => boolean; } 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 59d5e31290..12da5ff790 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 @@ -12,23 +12,33 @@ import { useCallback, useMemo, useState } from 'react'; import type { SqlResultSet } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataResult } from '@cloudbeaver/plugin-data-viewer'; +import { indexColumn } from '../DataGridTable'; import type { IDataGridSelectionContext } from './DataGridSelectionContext'; -interface IPosition { +export interface IPosition { idx: number; rowIdx: number; isIndexColumn: boolean; } interface IGridSelectionOptions { - indexColumnKey: string; + select: (position: Pick) => void; +} + +export function isIndexCol(columnKey: string): boolean { + return columnKey === indexColumn.key; +} + +export function getColIdxFromColKey(columnKey: string): number { + return isIndexCol(columnKey) ? 0 : Number(columnKey); } export function useGridSelectionContext(modelResultData: IDatabaseDataResult | null, options: IGridSelectionOptions) { const [selectedCells] = useState(() => observable.map()); + const [temporarySelectedCells] = useState(() => observable.map()); const [lastSelectedCell, setLastSelectedCell] = useState(null); - const selectRow = useCallback(action((rowIdx: number, multiple: boolean) => { + const selectRow = useCallback(action((rowIdx: number, multiple: boolean, temporary = false) => { if (!modelResultData) { throw new Error('Model result data must be provided'); } @@ -44,6 +54,11 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n rowSelection.push(i); } + if (temporary) { + temporarySelectedCells.set(rowIdx, rowSelection); + return; + } + selectedCells.set(rowIdx, rowSelection); }), [selectedCells, modelResultData]); @@ -52,7 +67,7 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n throw new Error('Model result data must be provided'); } - const columnsLength = (modelResultData.data as SqlResultSet).columns?.length || 0; + const columnsLength = (modelResultData.data as SqlResultSet).columns?.length; return selectedCells.get(rowIdx)?.length === columnsLength; }, [modelResultData, selectedCells]); @@ -61,41 +76,117 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n selectedCells.delete(rowIdx); }, [selectedCells]); - const selectRange = useCallback(action((idx: number, rowIdx: number, isIndexCol: boolean) => { - if (!lastSelectedCell) { - throw new Error('Last selected cell must be provided'); + const selectRange = useCallback(action( + (startPosition: IPosition, lastPosition: IPosition, multiple: boolean, temporary = false) => { + temporarySelectedCells.clear(); + + if (!multiple) { + selectedCells.clear(); + } + + const left = Math.min(startPosition.idx, lastPosition.idx); + const right = Math.max(startPosition.idx, lastPosition.idx); + const top = Math.min(startPosition.rowIdx, lastPosition.rowIdx); + const bottom = Math.max(startPosition.rowIdx, lastPosition.rowIdx); + + const rowSelection = []; + for (let colIdx = left; colIdx <= right; colIdx++) { + rowSelection.push(colIdx); + } + + for (let rowIdx = top; rowIdx <= bottom; rowIdx++) { + if (startPosition.isIndexColumn || lastPosition.isIndexColumn) { + selectRow(rowIdx, true, temporary); + continue; + } + if (temporary) { + temporarySelectedCells.set(rowIdx, [...rowSelection]); + continue; + } + + const currentRowSelection = selectedCells.get(rowIdx) || []; + selectedCells.set(rowIdx, [...currentRowSelection, ...rowSelection]); + } + + options.select({ idx: lastPosition.idx, rowIdx: lastPosition.rowIdx }); + }), [selectedCells, temporarySelectedCells]); + + const isColumnSelected = useCallback((columnIndex: number, rowsLength: number) => { + let result = true; + + for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { + const rowSelection = selectedCells.get(rowIdx) || []; + if (!rowSelection.includes(columnIndex)) { + result = false; + break; + } + } + return result; + }, [selectedCells]); + + const unSelectColumn = useCallback((columnIndex: number, rowsLength: number) => { + for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { + const rowSelection = selectedCells.get(rowIdx) || []; + selectedCells.set(rowIdx, [...rowSelection.filter(colIdx => colIdx !== columnIndex)]); + } + }, [selectedCells]); + + const selectColumn = useCallback(action((columnKey: string, multiple: boolean) => { + if (!modelResultData) { + throw new Error('Model result data must be provided'); } - selectedCells.clear(); + if (!multiple) { + selectedCells.clear(); + } - const left = Math.min(idx, lastSelectedCell.idx); - const right = Math.max(idx, lastSelectedCell.idx); - const top = Math.min(rowIdx, lastSelectedCell.rowIdx); - const bottom = Math.max(rowIdx, lastSelectedCell.rowIdx); + const rowsLength = (modelResultData.data as SqlResultSet).rows?.length || 0; + const columnIndex = getColIdxFromColKey(columnKey); + + if (isColumnSelected(columnIndex, rowsLength)) { + unSelectColumn(columnIndex, rowsLength); + } else { + for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { + const rowSelection = selectedCells.get(rowIdx) || []; + + selectedCells.set(rowIdx, [...rowSelection, columnIndex]); + } + } + }), [modelResultData, selectedCells, isColumnSelected, unSelectColumn]); + + const selectTable = useCallback(() => { + if (!modelResultData) { + throw new Error('Model result data must be provided'); + } + + const rowsLength = (modelResultData.data as SqlResultSet).rows?.length || 0; + const columnsLength = (modelResultData.data as SqlResultSet).columns?.length || 0; const rowSelection = []; - for (let colIdx = left; colIdx <= right; colIdx++) { + for (let colIdx = 0; colIdx < columnsLength; colIdx++) { rowSelection.push(colIdx); } - for (let rowIdx = top; rowIdx <= bottom; rowIdx++) { - if (isIndexCol || lastSelectedCell.isIndexColumn) { - selectRow(rowIdx, true); - continue; - } + + for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { selectedCells.set(rowIdx, [...rowSelection]); } - }), [selectedCells, lastSelectedCell]); + + options.select({ idx: -1, rowIdx: 0 }); + }, [modelResultData, selectedCells, options]); const isSelected = useCallback((key: string, rowIdx: number) => { - if (!selectedCells.has(rowIdx)) { + if (!selectedCells.has(rowIdx) && !temporarySelectedCells.has(rowIdx)) { return false; } - const rowSelection = selectedCells.get(rowIdx)!; + const rowSelection = selectedCells.get(rowIdx); + const temporaryRowSelection = temporarySelectedCells.get(rowIdx); + const idx = Number.parseInt(key); - return rowSelection.includes(idx); + + return !!(rowSelection?.includes(idx) || temporaryRowSelection?.includes(idx)); }, - [selectedCells]); + [selectedCells, temporarySelectedCells]); const unSelect = useCallback((idx: number, rowIdx: number) => { if (!selectedCells.has(rowIdx)) { @@ -123,13 +214,20 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n }, [selectedCells]); const select = useCallback((key: string, rowIdx: number, multiple: boolean, range: boolean) => { - const isIndexColumn = key === options.indexColumnKey; - const columnIndex = isIndexColumn ? 0 : Number.parseInt(key); + const isIndexColumn = isIndexCol(key); + const columnIndex = getColIdxFromColKey(key); setLastSelectedCell({ idx: columnIndex, rowIdx, isIndexColumn }); - if (selectedCells.size > 0 && range) { - selectRange(columnIndex, rowIdx, isIndexColumn); + if (selectedCells.size > 0 && range && lastSelectedCell) { + selectRange( + { + idx: lastSelectedCell.idx, + rowIdx: lastSelectedCell.rowIdx, + isIndexColumn: lastSelectedCell.isIndexColumn, + }, + { idx: columnIndex, rowIdx, isIndexColumn }, + multiple); return; } @@ -152,13 +250,26 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n } selectCell(columnIndex, rowIdx); - }, [selectedCells, selectRow, selectRange, unSelect, isSelected, selectCell, isRowSelected, unSelectRow]); + }, [ + selectedCells, + selectRow, + selectRange, + unSelect, + isSelected, + selectCell, + isRowSelected, + unSelectRow, + lastSelectedCell, + ]); const context: IDataGridSelectionContext = useMemo(() => ({ selectedCells, select, + selectRange, + selectColumn, + selectTable, isSelected, - }), [selectedCells, select, isSelected]); + }), [selectedCells, select, isSelected, selectRange, selectColumn, selectTable]); return context; } 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 f93dd18b33..a5ae8c9ba4 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -10,7 +10,7 @@ import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import DataGrid from 'react-data-grid'; -import type { Column } from 'react-data-grid'; +import type { Column, DataGridHandle } from 'react-data-grid'; import type { Position } from 'react-data-grid/lib/types'; import styled from 'reshadow'; @@ -30,11 +30,13 @@ import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionC import { useGridSelectionContext } from './DataGridSelection/useGridSelectionContext'; import { DataGridSortingContext } from './DataGridSorting/DataGridSortingContext'; import { useGridSortingContext } from './DataGridSorting/useGridSortingContext'; -import { DataGridTableContainer } from './DataGridTableContainer'; import { CellFormatter } from './Formatters/CellFormatter'; import { IndexFormatter } from './Formatters/IndexFormatter'; import { RowRenderer } from './RowRenderer/RowRenderer'; import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader'; +import { TableIndexColumnHeader } from './TableColumnHeader/TableIndexColumnHeader'; +import { useGridDragging } from './useGridDragging'; +import { useGridSelectedCellsCopy } from './useGridSelectedCellsCopy'; interface Props { model: IDatabaseDataModel; @@ -47,17 +49,19 @@ function isAtBottom(event: React.UIEvent): boolean { return target.clientHeight + target.scrollTop === target.scrollHeight; } -const indexColumn: Column = { +export const indexColumn: Column = { key: Number.MAX_SAFE_INTEGER + '', name: '#', minWidth: 60, width: 60, resizable: false, frozen: true, + headerRenderer: TableIndexColumnHeader, formatter: IndexFormatter, }; export const DataGridTable: React.FC = observer(function DataGridTable({ model, resultIndex, className }) { + const dataGridRef = useRef(null); const editorRef = useRef(null); const styles = useStyles(reactGridStyles, baseStyles); const [columnResize] = useState(() => new Executor()); @@ -65,7 +69,10 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ const modelResultData = model?.getResult(resultIndex); const gridSortingContext = useGridSortingContext(model); - const gridSelectionContext = useGridSelectionContext(modelResultData, { indexColumnKey: indexColumn.key }); + const gridSelectionContext = useGridSelectionContext(modelResultData, { + // we need idx + 1 to keep our api in sync with react data grid api + select: ({ idx, rowIdx }) => dataGridRef.current?.selectCell({ idx: idx + 1, rowIdx }), + }); const editingContext = useEditing({ onEdit: (position, key) => { const editor = model.source.getEditor(resultIndex); @@ -86,6 +93,14 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ }, }); + const { onKeydownHandler } = useGridSelectedCellsCopy(modelResultData, gridSelectionContext); + const { onMouseDownHandler, onMouseMoveHandler } = useGridDragging({ + onDragOver: (startPosition, currentPosiition, event) => + gridSelectionContext.selectRange(startPosition, currentPosiition, event.ctrlKey, true), + onDragEnd: (startPosition, currentPosiition, event) => + gridSelectionContext.selectRange(startPosition, currentPosiition, event.ctrlKey, false), + }); + useEffect(() => { if (model.isLoading()) { editingContext.close(); @@ -161,15 +176,24 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ columnResize, resultIndex, getEditorPortal: () => editorRef.current, - }), [model, resultIndex, editorRef]); + getDataGridApi: () => dataGridRef.current, + }), [model, resultIndex, editorRef, dataGridRef]); return styled(styles)( - + = observer(function DataGridTable({ onScroll={handleScroll} />
- + 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 5711c245d6..4f8131426c 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 @@ -16,6 +16,7 @@ import type { SqlResultSet } from '@cloudbeaver/core-sdk'; import type { SortMode } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext } from '../DataGridContext'; +import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; import { DataGridSortingContext } from '../DataGridSorting/DataGridSortingContext'; const headerStyles = css` @@ -24,6 +25,7 @@ const headerStyles = css` align-items: center; align-content: center; width: 100%; + cursor: pointer; } shrink-container { display: flex; @@ -83,9 +85,10 @@ export const TableColumnHeader: React.FC> = observer(fu }) { const dataGridContext = useContext(DataGridContext); const gridSortingContext = useContext(DataGridSortingContext); + const gridSelectionContext = useContext(DataGridSelectionContext); - if (!dataGridContext || !gridSortingContext) { - throw new Error('Data grid context or sorting context are missed'); + if (!dataGridContext || !gridSortingContext || !gridSelectionContext) { + throw new Error('One of the following contexts are missed(data grid context, grid sorting context, grid selection context)'); } const model = dataGridContext.model; @@ -99,6 +102,7 @@ export const TableColumnHeader: React.FC> = observer(fu const currentSortMode = gridSortingContext.getSortMode(columnName); const handleSort = (e: React.MouseEvent) => { + e.stopPropagation(); if (loading) { return; } @@ -117,8 +121,12 @@ export const TableColumnHeader: React.FC> = observer(fu gridSortingContext.setSortMode(columnName, nextSort, e.ctrlKey || e.metaKey); }; + const handleColumnSelection = (e: React.MouseEvent) => { + gridSelectionContext.selectColumn(calculatedColumn.key, e.ctrlKey); + }; + return styled(headerStyles)( - + diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx new file mode 100644 index 0000000000..add32b31aa --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx @@ -0,0 +1,28 @@ +/* + * 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 { useContext } from 'react'; +import type { HeaderRendererProps } from 'react-data-grid'; + +import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; + +export const TableIndexColumnHeader: React.FC> = function TableIndexColumnHeader(props) { + const selectionContext = useContext(DataGridSelectionContext); + + if (!selectionContext) { + throw new Error('Selection context must be provided'); + } + + const indexClickHandler = (event: React.MouseEvent) => { + selectionContext.selectTable(); + }; + + return ( +
{props.column.name}
+ ); +}; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts new file mode 100644 index 0000000000..60e0c5de90 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts @@ -0,0 +1,181 @@ +/* + * 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 { useState, useCallback, useEffect } from 'react'; + +import { getColIdxFromColKey, IPosition, isIndexCol } from './DataGridSelection/useGridSelectionContext'; + +interface IDraggingPosition { + idx: number; + rowIdx: number; + isIndexColumn: boolean; +} + +interface IMousePosition { + x: number; + y: number; +} + +type DraggingCallback = ( + startPosition: IPosition, currentPosition: IPosition, event: React.MouseEvent | MouseEvent +) => void; + +interface IDraggingCallbacks { + onDragStart?: (startPosition: IPosition, event: React.MouseEvent | MouseEvent) => void; + onDragOver?: DraggingCallback; + onDragEnd?: DraggingCallback; +} + +const THRESHOLD = 20; + +function getDelta(startPosition: IMousePosition | null, currentPosition: IMousePosition | null) { + if (!startPosition || !currentPosition) { + return null; + } + + const xDelta = Math.abs(startPosition.x - currentPosition.x); + const yDelta = Math.abs(startPosition.y - currentPosition.y); + + return Math.max(xDelta, yDelta); +} + +function getCellPositionFromEvent(event: React.MouseEvent) { + const target = event.target as HTMLElement; + const cell = target.closest('[role="gridcell"]') as HTMLElement | null; + + if (!cell) { + return; + } + + const rowIdx = cell.getAttribute('data-rowindex'); + const colKey = cell.getAttribute('data-columnkey'); + + if (!rowIdx || !colKey) { + return; + } + + return { rowIdx: Number(rowIdx), colIdx: getColIdxFromColKey(colKey), isIndexColumn: isIndexCol(colKey) }; +} + +function isDraggingStarted(delta: number | null, threshold: number) { + if (delta === null) { + return false; + } + + return delta > threshold; +} + +export function useGridDragging(callbacks: IDraggingCallbacks) { + const { onDragStart, onDragOver, onDragEnd } = callbacks; + + const [startDraggingCell, setStartDraggingCell] = useState(null); + const [currentDraggingCell, setCurrentDraggingCell] = useState(null); + const [startMousePosition, setStartMousePosition] = useState(null); + const [isDragging, setDragging] = useState(false); + const [mouseDown, setMouseDown] = useState(false); + + const onMouseDownHandler = (event: React.MouseEvent) => { + const position = getCellPositionFromEvent(event); + + if (!position) { + return; + } + + setMouseDown(true); + setStartMousePosition({ ...startMousePosition, x: event.pageX, y: event.pageY }); + setStartDraggingCell({ idx: position.colIdx, rowIdx: position.rowIdx, isIndexColumn: position.isIndexColumn }); + }; + + const onMouseMoveHandler = (event: React.MouseEvent) => { + if (!mouseDown) { + return; + } + + const position = getCellPositionFromEvent(event); + + if (!position) { + return; + } + + if (!isDragging) { + const delta = getDelta(startMousePosition, { x: event.pageX, y: event.pageY }); + if (!isDraggingStarted(delta, THRESHOLD)) { + return; + } + + if (onDragStart && startDraggingCell) { + onDragStart(startDraggingCell, event); + } + + setDragging(true); + return; + } + + // check if the new cell is equal to the previous cell + if (position.rowIdx === currentDraggingCell?.rowIdx + && position.colIdx === currentDraggingCell.idx + && position.isIndexColumn === currentDraggingCell.isIndexColumn) { + return; + } + + setCurrentDraggingCell({ idx: position.colIdx, rowIdx: position.rowIdx, isIndexColumn: position.isIndexColumn }); + + if (onDragOver) { + onDragOver( + { + idx: startDraggingCell!.idx, + isIndexColumn: startDraggingCell!.isIndexColumn, + rowIdx: startDraggingCell!.rowIdx, + }, + { + idx: position.colIdx, + isIndexColumn: position.isIndexColumn, + rowIdx: position.rowIdx, + }, + event); + } + }; + + const onMouseUpHandler = useCallback((event: React.MouseEvent | MouseEvent) => { + setMouseDown(false); + setStartMousePosition(null); + + if (!isDragging || !startDraggingCell || !currentDraggingCell) { + return; + } + + if (onDragEnd) { + onDragEnd( + { + idx: startDraggingCell.idx, + isIndexColumn: startDraggingCell.isIndexColumn, + rowIdx: startDraggingCell.rowIdx, + }, + { + idx: currentDraggingCell.idx, + isIndexColumn: currentDraggingCell.isIndexColumn, + rowIdx: currentDraggingCell.rowIdx, + }, + event); + } + + setDragging(false); + setStartDraggingCell(null); + setCurrentDraggingCell(null); + }, [isDragging, currentDraggingCell, startDraggingCell, onDragEnd]); + + useEffect(() => { + document.addEventListener('mouseup', onMouseUpHandler); + return () => document.removeEventListener('mouseup', onMouseUpHandler); + }, [onMouseUpHandler]); + + return { + onMouseDownHandler, + onMouseMoveHandler, + }; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTableContainer.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts similarity index 60% rename from webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTableContainer.tsx rename to webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts index 354e55dfc0..8c45a41b6d 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTableContainer.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -1,34 +1,18 @@ /* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 DBeaver Corp and others + * 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 { useCallback, useContext } from 'react'; -import styled, { css } from 'reshadow'; +import { useCallback } from 'react'; import type { SqlResultSet } from '@cloudbeaver/core-sdk'; import { copyToClipboard } from '@cloudbeaver/core-utils'; import type { IDatabaseDataResult } from '@cloudbeaver/plugin-data-viewer'; -import { DataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; - -const styles = css` - grid-container { - outline: 0; - display: flex; - flex-direction: column; - flex: 1; - } -`; - -interface Props { - children: React.ReactNode; - modelResultData: IDatabaseDataResult | null; - className?: string; -} +import type { IDataGridSelectionContext } from './DataGridSelection/DataGridSelectionContext'; const EVENT_KEY_CODE = { C: 'KeyC', @@ -72,28 +56,19 @@ function copyGridSelectedDataToClipboard(modelData: SqlResultSet, selectedCells: // needed for event.code type IKeyboardEvent = React.KeyboardEvent & KeyboardEvent; -export const DataGridTableContainer: React.FC = function DataGridTableContainer({ - modelResultData, children, className, -}) { - const selectionContext = useContext(DataGridSelectionContext); - - if (!selectionContext) { - throw new Error('Selection context must be provided'); - } - +export function useGridSelectedCellsCopy( + modelResultData: IDatabaseDataResult | null, + selectionContext: IDataGridSelectionContext | null +) { const onKeydownHandler = useCallback((event: IKeyboardEvent) => { - if (!modelResultData) { + if (!modelResultData || !selectionContext) { return; } if ((event.ctrlKey || event.metaKey) && event.code === EVENT_KEY_CODE.C) { copyGridSelectedDataToClipboard(modelResultData.data, selectionContext.selectedCells); } - }, [modelResultData, selectionContext.selectedCells]); + }, [modelResultData, selectionContext]); - return styled(styles)( - - {children} - - ); -}; + return { onKeydownHandler }; +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss index af2491586e..7e1cca6ad8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss +++ b/webapp/packages/plugin-data-spreadsheet-new/src/styles/base.scss @@ -8,6 +8,13 @@ @import "react-data-grid/dist/react-data-grid.css"; +.cb-react-grid-container { + outline: 0; + display: flex; + flex-direction: column; + flex: 1; +} + .cb-react-grid-theme { font-family: inherit; outline: 0; From 2c2e54f7295541d98e5d65190df95a99fa2675c6 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 15 Feb 2021 13:49:58 +0300 Subject: [PATCH 2/7] feat(plugin-data-spreadsheet-new): CB-696 copyright --- .../src/DataGrid/useGridDragging.ts | 4 ++-- .../src/DataGrid/useGridSelectedCellsCopy.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts index 60e0c5de90..491503a646 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts @@ -1,6 +1,6 @@ /* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 DBeaver Corp and others + * 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. 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 8c45a41b6d..fa3c2fb112 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridSelectedCellsCopy.ts @@ -1,6 +1,6 @@ /* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 DBeaver Corp and others + * 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. From 8be33a62a6f4c2a6238c97dd32926c994e323cb2 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 15 Feb 2021 14:37:58 +0300 Subject: [PATCH 3/7] feat(plugin-data-spreadsheet-new): CB-696 naming --- .../DataGridSelectionContext.ts | 6 ++-- .../useGridSelectionContext.tsx | 29 ++++++++++--------- .../src/DataGrid/useGridDragging.ts | 12 +++++--- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts index 993b337386..1bc9f74348 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridSelection/DataGridSelectionContext.ts @@ -12,11 +12,11 @@ import type { IPosition } from './useGridSelectionContext'; export interface IDataGridSelectionContext { selectedCells: Map; - select: (key: string, rowIdx: number, multiple: boolean, range: boolean) => void; + select: (columnKey: string, rowIdx: number, multiple: boolean, range: boolean) => void; selectRange: (startPosition: IPosition, lastPosition: IPosition, multiple: boolean, temporary: boolean) => void; - selectColumn: (colKey: string, multiple: boolean) => void; + selectColumn: (columnKey: string, multiple: boolean) => void; selectTable: () => void; - isSelected: (key: string, rowIdx: number) => boolean; + isSelected: (columnKey: string, rowIdx: number) => boolean; } export const DataGridSelectionContext = createContext(null); 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 12da5ff790..ec077f68a6 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 @@ -25,12 +25,12 @@ interface IGridSelectionOptions { select: (position: Pick) => void; } -export function isIndexCol(columnKey: string): boolean { +export function isIndexColumn(columnKey: string): boolean { return columnKey === indexColumn.key; } -export function getColIdxFromColKey(columnKey: string): number { - return isIndexCol(columnKey) ? 0 : Number(columnKey); +export function getColumnIdxFromColumnKey(columnKey: string): number { + return isIndexColumn(columnKey) ? 0 : Number(columnKey); } export function useGridSelectionContext(modelResultData: IDatabaseDataResult | null, options: IGridSelectionOptions) { @@ -105,7 +105,7 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n } const currentRowSelection = selectedCells.get(rowIdx) || []; - selectedCells.set(rowIdx, [...currentRowSelection, ...rowSelection]); + selectedCells.set(rowIdx, [...new Set([...currentRowSelection, ...rowSelection])]); } options.select({ idx: lastPosition.idx, rowIdx: lastPosition.rowIdx }); @@ -141,7 +141,7 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n } const rowsLength = (modelResultData.data as SqlResultSet).rows?.length || 0; - const columnIndex = getColIdxFromColKey(columnKey); + const columnIndex = getColumnIdxFromColumnKey(columnKey); if (isColumnSelected(columnIndex, rowsLength)) { unSelectColumn(columnIndex, rowsLength); @@ -174,7 +174,7 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n options.select({ idx: -1, rowIdx: 0 }); }, [modelResultData, selectedCells, options]); - const isSelected = useCallback((key: string, rowIdx: number) => { + const isSelected = useCallback((columnKey: string, rowIdx: number) => { if (!selectedCells.has(rowIdx) && !temporarySelectedCells.has(rowIdx)) { return false; } @@ -182,7 +182,7 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n const rowSelection = selectedCells.get(rowIdx); const temporaryRowSelection = temporarySelectedCells.get(rowIdx); - const idx = Number.parseInt(key); + const idx = Number(columnKey); return !!(rowSelection?.includes(idx) || temporaryRowSelection?.includes(idx)); }, @@ -200,6 +200,7 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n rowSelection.splice(targetIndex, 1); return true; } + return false; }, [selectedCells]); @@ -213,11 +214,11 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n rowSelection.push(idx); }, [selectedCells]); - const select = useCallback((key: string, rowIdx: number, multiple: boolean, range: boolean) => { - const isIndexColumn = isIndexCol(key); - const columnIndex = getColIdxFromColKey(key); + const select = useCallback((columnKey: string, rowIdx: number, multiple: boolean, range: boolean) => { + const isIndexCol = isIndexColumn(columnKey); + const columnIndex = getColumnIdxFromColumnKey(columnKey); - setLastSelectedCell({ idx: columnIndex, rowIdx, isIndexColumn }); + setLastSelectedCell({ idx: columnIndex, rowIdx, isIndexColumn: isIndexCol }); if (selectedCells.size > 0 && range && lastSelectedCell) { selectRange( @@ -226,12 +227,12 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n rowIdx: lastSelectedCell.rowIdx, isIndexColumn: lastSelectedCell.isIndexColumn, }, - { idx: columnIndex, rowIdx, isIndexColumn }, + { idx: columnIndex, rowIdx, isIndexColumn: isIndexCol }, multiple); return; } - if (isIndexColumn) { + if (isIndexCol) { if (multiple && isRowSelected(rowIdx)) { unSelectRow(rowIdx); } else { @@ -244,7 +245,7 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n selectedCells.clear(); } - if (isSelected(key, rowIdx)) { + if (isSelected(columnKey, rowIdx)) { unSelect(columnIndex, rowIdx); return; } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts index 491503a646..ec58008695 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts @@ -8,7 +8,7 @@ import { useState, useCallback, useEffect } from 'react'; -import { getColIdxFromColKey, IPosition, isIndexCol } from './DataGridSelection/useGridSelectionContext'; +import { getColumnIdxFromColumnKey, IPosition, isIndexColumn } from './DataGridSelection/useGridSelectionContext'; interface IDraggingPosition { idx: number; @@ -53,13 +53,17 @@ function getCellPositionFromEvent(event: React.MouseEvent Date: Wed, 17 Feb 2021 11:46:11 +0300 Subject: [PATCH 4/7] feat(plugin-data-spreadsheet-new): CB-696 add useTableData hook --- .../DataGrid/CellRenderer/CellRenderer.tsx | 4 +- .../DataGridSelectionContext.ts | 6 +- .../useGridSelectionContext.tsx | 252 ++++++++---------- .../src/DataGrid/DataGridTable.tsx | 79 ++---- .../TableIndexColumnHeader.tsx | 6 +- .../src/DataGrid/useGridDragging.ts | 32 +-- .../src/DataGrid/useTableData.ts | 98 +++++++ 7 files changed, 253 insertions(+), 224 deletions(-) create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts 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 8ee32f6231..bb61a26b0f 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 @@ -37,7 +37,7 @@ export const CellRenderer: React.FC> = observer(function } const handleMouseDown = useCallback((event: React.MouseEvent) => { - selectionContext?.select(column.key, rowIdx, event.ctrlKey, event.shiftKey); + selectionContext?.select(column.idx, rowIdx, event.ctrlKey, event.shiftKey); }, [column, rowIdx, selectionContext]); const handleDoubleClick = useCallback(() => { @@ -50,7 +50,7 @@ export const CellRenderer: React.FC> = observer(function ; - select: (columnKey: string, rowIdx: number, multiple: boolean, range: boolean) => void; - selectRange: (startPosition: IPosition, lastPosition: IPosition, multiple: boolean, temporary: boolean) => void; + select: (columnIndex: number, rowIdx: number, multiple: boolean, range: boolean) => void; selectColumn: (columnKey: string, multiple: boolean) => void; selectTable: () => void; isSelected: (columnKey: string, rowIdx: number) => boolean; + updateMultiSelection: ( + startPosition: IPosition, lastPosition: IPosition, multiple: boolean, temporary: boolean + ) => void; } export const DataGridSelectionContext = createContext(null); 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 ec077f68a6..36a3b1628e 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 @@ -9,111 +9,105 @@ import { action, observable } from 'mobx'; import { useCallback, useMemo, useState } from 'react'; -import type { SqlResultSet } from '@cloudbeaver/core-sdk'; -import type { IDatabaseDataResult } from '@cloudbeaver/plugin-data-viewer'; - -import { indexColumn } from '../DataGridTable'; +import type { ITableData } from '../useTableData'; import type { IDataGridSelectionContext } from './DataGridSelectionContext'; export interface IPosition { idx: number; rowIdx: number; - isIndexColumn: boolean; } -interface IGridSelectionOptions { - select: (position: Pick) => void; -} - -export function isIndexColumn(columnKey: string): boolean { - return columnKey === indexColumn.key; -} - -export function getColumnIdxFromColumnKey(columnKey: string): number { - return isIndexColumn(columnKey) ? 0 : Number(columnKey); -} - -export function useGridSelectionContext(modelResultData: IDatabaseDataResult | null, options: IGridSelectionOptions) { +export function useGridSelectionContext(tableData: ITableData) { const [selectedCells] = useState(() => observable.map()); const [temporarySelectedCells] = useState(() => observable.map()); const [lastSelectedCell, setLastSelectedCell] = useState(null); - const selectRow = useCallback(action((rowIdx: number, multiple: boolean, temporary = false) => { - if (!modelResultData) { - throw new Error('Model result data must be provided'); - } + const selectRows = useCallback(action( + (startPosition: number, lastPosition: number, multiple: boolean, temporary = false) => { + const columnsLength = tableData.columns?.length || 0; - const columnsLength = (modelResultData.data as SqlResultSet).columns?.length || 0; + if (!multiple) { + selectedCells.clear(); + } - if (!multiple) { - selectedCells.clear(); - } + const rowSelection = []; + for (let i = 0; i < columnsLength; i++) { + rowSelection.push(i); + } - const rowSelection = []; - for (let i = 0; i < columnsLength; i++) { - rowSelection.push(i); - } + const firstRowIdx = Math.min(startPosition, lastPosition); + const lastRowIdx = Math.max(startPosition, lastPosition); - if (temporary) { - temporarySelectedCells.set(rowIdx, rowSelection); - return; - } - - selectedCells.set(rowIdx, rowSelection); - }), [selectedCells, modelResultData]); + for (let rowIdx = firstRowIdx; rowIdx <= lastRowIdx; rowIdx++) { + if (temporary) { + temporarySelectedCells.set(rowIdx, rowSelection); + } else { + selectedCells.set(rowIdx, rowSelection); + } + } + }), [selectedCells, temporarySelectedCells]); const isRowSelected = useCallback((rowIdx: number) => { - if (!modelResultData) { - throw new Error('Model result data must be provided'); + if (!tableData) { + throw new Error('Table data must be provided'); } - const columnsLength = (modelResultData.data as SqlResultSet).columns?.length; + const columnsLength = tableData.columns?.length; return selectedCells.get(rowIdx)?.length === columnsLength; - }, [modelResultData, selectedCells]); + }, [tableData, selectedCells]); const unSelectRow = useCallback((rowIdx: number) => { selectedCells.delete(rowIdx); }, [selectedCells]); const selectRange = useCallback(action( - (startPosition: IPosition, lastPosition: IPosition, multiple: boolean, temporary = false) => { - temporarySelectedCells.clear(); - + (startPosition: number, lastPosition: number, columns: number[], multiple: boolean, temporary = false) => { if (!multiple) { selectedCells.clear(); } - const left = Math.min(startPosition.idx, lastPosition.idx); - const right = Math.max(startPosition.idx, lastPosition.idx); - const top = Math.min(startPosition.rowIdx, lastPosition.rowIdx); - const bottom = Math.max(startPosition.rowIdx, lastPosition.rowIdx); - - const rowSelection = []; - for (let colIdx = left; colIdx <= right; colIdx++) { - rowSelection.push(colIdx); - } + const top = Math.min(startPosition, lastPosition); + const bottom = Math.max(startPosition, lastPosition); for (let rowIdx = top; rowIdx <= bottom; rowIdx++) { - if (startPosition.isIndexColumn || lastPosition.isIndexColumn) { - selectRow(rowIdx, true, temporary); - continue; - } if (temporary) { - temporarySelectedCells.set(rowIdx, [...rowSelection]); - continue; + temporarySelectedCells.set(rowIdx, [...columns]); + } else { + const currentRowSelection = selectedCells.get(rowIdx) || []; + const newRowSelection = [...currentRowSelection, ...columns] + .filter((columnIdx, idx, arr) => arr.indexOf(columnIdx) === idx); + + selectedCells.set(rowIdx, newRowSelection); } - - const currentRowSelection = selectedCells.get(rowIdx) || []; - selectedCells.set(rowIdx, [...new Set([...currentRowSelection, ...rowSelection])]); } - - options.select({ idx: lastPosition.idx, rowIdx: lastPosition.rowIdx }); }), [selectedCells, temporarySelectedCells]); - const isColumnSelected = useCallback((columnIndex: number, rowsLength: number) => { + const updateMultiSelection = useCallback( + (startPosition: IPosition, lastPosition: IPosition, multiple: boolean, temporary: boolean) => { + const columnsInRange = tableData.getColumnsInRange(startPosition.idx, lastPosition.idx); + const isIndexColumnInRange = tableData.isIndexColumnInRange(columnsInRange); + + temporarySelectedCells.clear(); + + if (isIndexColumnInRange) { + selectRows(startPosition.rowIdx, lastPosition.rowIdx, multiple, temporary); + } else { + selectRange( + startPosition.rowIdx, + lastPosition.rowIdx, + columnsInRange.map(column => Number(column.key)), + multiple, + temporary + ); + } + }, [selectRange, selectRows, tableData, temporarySelectedCells]); + + const isColumnSelected = useCallback((columnIndex: number) => { let result = true; + const rowsLength = tableData.rows?.length || 0; + for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { const rowSelection = selectedCells.get(rowIdx) || []; if (!rowSelection.includes(columnIndex)) { @@ -122,45 +116,38 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n } } return result; - }, [selectedCells]); + }, [selectedCells, tableData]); + + const unSelectColumn = useCallback((columnIndex: number) => { + const rowsLength = tableData.rows?.length || 0; - const unSelectColumn = useCallback((columnIndex: number, rowsLength: number) => { for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { const rowSelection = selectedCells.get(rowIdx) || []; selectedCells.set(rowIdx, [...rowSelection.filter(colIdx => colIdx !== columnIndex)]); } - }, [selectedCells]); + }, [selectedCells, tableData]); const selectColumn = useCallback(action((columnKey: string, multiple: boolean) => { - if (!modelResultData) { - throw new Error('Model result data must be provided'); - } - if (!multiple) { selectedCells.clear(); } - const rowsLength = (modelResultData.data as SqlResultSet).rows?.length || 0; - const columnIndex = getColumnIdxFromColumnKey(columnKey); + const rowsLength = tableData.rows?.length || 0; + const columnIndex = Number(columnKey); - if (isColumnSelected(columnIndex, rowsLength)) { - unSelectColumn(columnIndex, rowsLength); + if (isColumnSelected(columnIndex)) { + unSelectColumn(columnIndex); } else { for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { const rowSelection = selectedCells.get(rowIdx) || []; - selectedCells.set(rowIdx, [...rowSelection, columnIndex]); } } - }), [modelResultData, selectedCells, isColumnSelected, unSelectColumn]); + }), [tableData, selectedCells, isColumnSelected, unSelectColumn]); const selectTable = useCallback(() => { - if (!modelResultData) { - throw new Error('Model result data must be provided'); - } - - const rowsLength = (modelResultData.data as SqlResultSet).rows?.length || 0; - const columnsLength = (modelResultData.data as SqlResultSet).columns?.length || 0; + const rowsLength = tableData.rows?.length || 0; + const columnsLength = tableData.columns?.length || 0; const rowSelection = []; for (let colIdx = 0; colIdx < columnsLength; colIdx++) { @@ -170,9 +157,7 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { selectedCells.set(rowIdx, [...rowSelection]); } - - options.select({ idx: -1, rowIdx: 0 }); - }, [modelResultData, selectedCells, options]); + }, [tableData, selectedCells]); const isSelected = useCallback((columnKey: string, rowIdx: number) => { if (!selectedCells.has(rowIdx) && !temporarySelectedCells.has(rowIdx)) { @@ -204,73 +189,72 @@ export function useGridSelectionContext(modelResultData: IDatabaseDataResult | n return false; }, [selectedCells]); - const selectCell = useCallback((idx: number, rowIdx: number) => { + const selectCell = useCallback((columnIdx: number, rowIdx: number) => { const rowSelection = selectedCells.get(rowIdx); if (rowSelection === undefined) { - selectedCells.set(rowIdx, [idx]); + selectedCells.set(rowIdx, [columnIdx]); return; } - rowSelection.push(idx); + rowSelection.push(columnIdx); }, [selectedCells]); - const select = useCallback((columnKey: string, rowIdx: number, multiple: boolean, range: boolean) => { - const isIndexCol = isIndexColumn(columnKey); - const columnIndex = getColumnIdxFromColumnKey(columnKey); + const select = useCallback( + (columnIndex: number, rowIdx: number, multiple: boolean, range: boolean) => { + const columnKey = tableData.getColumnKeyFromColumnIndex(columnIndex); + const isIndexColumn = tableData.isIndexColumn(columnKey); - setLastSelectedCell({ idx: columnIndex, rowIdx, isIndexColumn: isIndexCol }); + setLastSelectedCell({ idx: columnIndex, rowIdx }); - if (selectedCells.size > 0 && range && lastSelectedCell) { - selectRange( - { - idx: lastSelectedCell.idx, - rowIdx: lastSelectedCell.rowIdx, - isIndexColumn: lastSelectedCell.isIndexColumn, - }, - { idx: columnIndex, rowIdx, isIndexColumn: isIndexCol }, - multiple); - return; - } - - if (isIndexCol) { - if (multiple && isRowSelected(rowIdx)) { - unSelectRow(rowIdx); - } else { - selectRow(rowIdx, multiple); + if (selectedCells.size > 0 && range && lastSelectedCell) { + updateMultiSelection( + { idx: lastSelectedCell.idx, rowIdx: lastSelectedCell.rowIdx }, + { idx: columnIndex, rowIdx }, + multiple, + false); + return; } - return; - } - if (!multiple) { - selectedCells.clear(); - } + if (isIndexColumn) { + if (multiple && isRowSelected(rowIdx)) { + unSelectRow(rowIdx); + } else { + selectRows(rowIdx, rowIdx, multiple); + } + return; + } - if (isSelected(columnKey, rowIdx)) { - unSelect(columnIndex, rowIdx); - return; - } + if (!multiple) { + selectedCells.clear(); + } - selectCell(columnIndex, rowIdx); - }, [ - selectedCells, - selectRow, - selectRange, - unSelect, - isSelected, - selectCell, - isRowSelected, - unSelectRow, - lastSelectedCell, - ]); + if (isSelected(columnKey, rowIdx)) { + unSelect(Number(columnKey), rowIdx); + return; + } + + selectCell(Number(columnKey), rowIdx); + }, [ + tableData, + selectedCells, + unSelect, + isSelected, + selectRows, + selectCell, + isRowSelected, + unSelectRow, + lastSelectedCell, + updateMultiSelection, + ]); const context: IDataGridSelectionContext = useMemo(() => ({ selectedCells, select, - selectRange, selectColumn, selectTable, isSelected, - }), [selectedCells, select, isSelected, selectRange, selectColumn, selectTable]); + updateMultiSelection, + }), [selectedCells, select, isSelected, selectColumn, selectTable, updateMultiSelection]); return context; } 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 a5ae8c9ba4..1623e2f813 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -6,23 +6,19 @@ * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import DataGrid from 'react-data-grid'; -import type { Column, DataGridHandle } from 'react-data-grid'; +import type { DataGridHandle } from 'react-data-grid'; import type { Position } from 'react-data-grid/lib/types'; import styled from 'reshadow'; import { Executor } from '@cloudbeaver/core-executor'; -import type { SqlResultSet } from '@cloudbeaver/core-sdk'; 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 { DataGridContext, IColumnResizeInfo, IDataGridContext } from './DataGridContext'; @@ -31,12 +27,10 @@ import { useGridSelectionContext } from './DataGridSelection/useGridSelectionCon import { DataGridSortingContext } from './DataGridSorting/DataGridSortingContext'; import { useGridSortingContext } from './DataGridSorting/useGridSortingContext'; import { CellFormatter } from './Formatters/CellFormatter'; -import { IndexFormatter } from './Formatters/IndexFormatter'; import { RowRenderer } from './RowRenderer/RowRenderer'; -import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader'; -import { TableIndexColumnHeader } from './TableColumnHeader/TableIndexColumnHeader'; import { useGridDragging } from './useGridDragging'; import { useGridSelectedCellsCopy } from './useGridSelectedCellsCopy'; +import { useTableData } from './useTableData'; interface Props { model: IDatabaseDataModel; @@ -49,17 +43,6 @@ function isAtBottom(event: React.UIEvent): boolean { return target.clientHeight + target.scrollTop === target.scrollHeight; } -export const indexColumn: Column = { - key: Number.MAX_SAFE_INTEGER + '', - name: '#', - minWidth: 60, - width: 60, - resizable: false, - frozen: true, - headerRenderer: TableIndexColumnHeader, - formatter: IndexFormatter, -}; - export const DataGridTable: React.FC = observer(function DataGridTable({ model, resultIndex, className }) { const dataGridRef = useRef(null); const editorRef = useRef(null); @@ -68,11 +51,10 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ const modelResultData = model?.getResult(resultIndex); + const tableData = useTableData(modelResultData); + const gridSortingContext = useGridSortingContext(model); - const gridSelectionContext = useGridSelectionContext(modelResultData, { - // we need idx + 1 to keep our api in sync with react data grid api - select: ({ idx, rowIdx }) => dataGridRef.current?.selectCell({ idx: idx + 1, rowIdx }), - }); + const gridSelectionContext = useGridSelectionContext(tableData); const editingContext = useEditing({ onEdit: (position, key) => { const editor = model.source.getEditor(resultIndex); @@ -95,10 +77,15 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ const { onKeydownHandler } = useGridSelectedCellsCopy(modelResultData, gridSelectionContext); const { onMouseDownHandler, onMouseMoveHandler } = useGridDragging({ - onDragOver: (startPosition, currentPosiition, event) => - gridSelectionContext.selectRange(startPosition, currentPosiition, event.ctrlKey, true), - onDragEnd: (startPosition, currentPosiition, event) => - gridSelectionContext.selectRange(startPosition, currentPosiition, event.ctrlKey, false), + onDragStart: startPosition => { + dataGridRef.current?.selectCell({ idx: startPosition.idx, rowIdx: startPosition.rowIdx }); + }, + onDragOver: (startPosition, currentPosition, event) => { + gridSelectionContext.updateMultiSelection(startPosition, currentPosition, event.ctrlKey, true); + }, + onDragEnd: (startPosition, currentPosition, event) => { + gridSelectionContext.updateMultiSelection(startPosition, currentPosition, event.ctrlKey, false); + }, }); useEffect(() => { @@ -137,40 +124,6 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ } }, [model, modelResultData]); - const { columns, rows } = useMemo(() => computed(() => { - if (!modelResultData) { - return { columns: [], rows: [] }; - } - - const columnNames = ResultSetTools.getHeaders(modelResultData.data as SqlResultSet); - const rowStrings = ResultSetTools.getLongestCells(modelResultData.data as SqlResultSet); - - // TODO: seems better to do not measure container size - // for detecting max columns size, better to use configurable variable - const measuredCells = TextTools.getWidth({ - font: '400 14px Roboto', - text: columnNames.map((cell, i) => { - if (cell.length > (rowStrings[i] || '').length) { - return cell; - } - return rowStrings[i]; - }), - }).map(v => v + 16 + 32 + 20); - - // TODO: we need some result type specified formatter to common actions with data - const rows = (modelResultData.data as SqlResultSet).rows || []; - - const columns = (modelResultData.data as SqlResultSet).columns!.map>((col, columnIndex) => ({ - key: columnIndex + '', - name: col.label!, - width: Math.min(300, measuredCells[columnIndex]), - headerRenderer: TableColumnHeader, - })); - columns.unshift(indexColumn); - - return { rows, columns }; - }), [modelResultData]).get(); - const gridContext = useMemo(() => ({ model, columnResize, @@ -195,13 +148,13 @@ export const DataGridTable: React.FC = observer(function DataGridTable({ > = function TableIndexColumnHeader(props) { const selectionContext = useContext(DataGridSelectionContext); - if (!selectionContext) { - throw new Error('Selection context must be provided'); - } - const indexClickHandler = (event: React.MouseEvent) => { - selectionContext.selectTable(); + selectionContext?.selectTable(); }; return ( diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts index ec58008695..5031221d29 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts @@ -8,12 +8,9 @@ import { useState, useCallback, useEffect } from 'react'; -import { getColumnIdxFromColumnKey, IPosition, isIndexColumn } from './DataGridSelection/useGridSelectionContext'; - interface IDraggingPosition { idx: number; rowIdx: number; - isIndexColumn: boolean; } interface IMousePosition { @@ -22,16 +19,21 @@ interface IMousePosition { } type DraggingCallback = ( - startPosition: IPosition, currentPosition: IPosition, event: React.MouseEvent | MouseEvent + startPosition: IDraggingPosition, + currentPosition: IDraggingPosition, + event: React.MouseEvent | MouseEvent ) => void; interface IDraggingCallbacks { - onDragStart?: (startPosition: IPosition, event: React.MouseEvent | MouseEvent) => void; + onDragStart?: ( + startPosition: IDraggingPosition, + event: React.MouseEvent | MouseEvent + ) => void; onDragOver?: DraggingCallback; onDragEnd?: DraggingCallback; } -const THRESHOLD = 20; +const THRESHOLD = 10; function getDelta(startPosition: IMousePosition | null, currentPosition: IMousePosition | null) { if (!startPosition || !currentPosition) { @@ -53,16 +55,15 @@ function getCellPositionFromEvent(event: React.MouseEvent) => { @@ -122,23 +123,20 @@ export function useGridDragging(callbacks: IDraggingCallbacks) { // check if the new cell is equal to the previous cell if (position.rowIdx === currentDraggingCell?.rowIdx - && position.colIdx === currentDraggingCell.idx - && position.isIndexColumn === currentDraggingCell.isIndexColumn) { + && position.colIdx === currentDraggingCell.idx) { return; } - setCurrentDraggingCell({ idx: position.colIdx, rowIdx: position.rowIdx, isIndexColumn: position.isIndexColumn }); + setCurrentDraggingCell({ idx: position.colIdx, rowIdx: position.rowIdx }); if (onDragOver) { onDragOver( { idx: startDraggingCell!.idx, - isIndexColumn: startDraggingCell!.isIndexColumn, rowIdx: startDraggingCell!.rowIdx, }, { idx: position.colIdx, - isIndexColumn: position.isIndexColumn, rowIdx: position.rowIdx, }, event); @@ -157,12 +155,10 @@ export function useGridDragging(callbacks: IDraggingCallbacks) { onDragEnd( { idx: startDraggingCell.idx, - isIndexColumn: startDraggingCell.isIndexColumn, rowIdx: startDraggingCell.rowIdx, }, { idx: currentDraggingCell.idx, - isIndexColumn: currentDraggingCell.isIndexColumn, rowIdx: currentDraggingCell.rowIdx, }, event); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts new file mode 100644 index 0000000000..a2b8ed51d7 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useTableData.ts @@ -0,0 +1,98 @@ +/* + * 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 { computed } from 'mobx'; +import { useCallback, useMemo } from 'react'; +import type { Column } from 'react-data-grid'; + +import type { SqlResultSet } from '@cloudbeaver/core-sdk'; +import { TextTools } from '@cloudbeaver/core-utils'; +import type { IDatabaseDataResult } from '@cloudbeaver/plugin-data-viewer'; + +import { ResultSetTools } from '../ResultSetTools'; +import { IndexFormatter } from './Formatters/IndexFormatter'; +import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader'; +import { TableIndexColumnHeader } from './TableColumnHeader/TableIndexColumnHeader'; + +export const indexColumn: Column = { + key: Number.MAX_SAFE_INTEGER + '', + name: '#', + minWidth: 60, + width: 60, + resizable: false, + frozen: true, + headerRenderer: TableIndexColumnHeader, + formatter: IndexFormatter, +}; + +export interface ITableData { + columns: Array>; + rows: any[][]; + getColumnsInRange: (startIndex: number, endIndex: number) => Array>; + isIndexColumn: (columnKey: string) => boolean; + isIndexColumnInRange: (columnsRange: Array>) => boolean; + getColumnKeyFromColumnIndex: (columnIndex: number) => string; +} + +export function useTableData(modelResultData: IDatabaseDataResult | null): ITableData { + const { columns, rows } = useMemo(() => computed(() => { + if (!modelResultData) { + return { columns: [], rows: [] }; + } + + const columnNames = ResultSetTools.getHeaders(modelResultData.data as SqlResultSet); + const rowStrings = ResultSetTools.getLongestCells(modelResultData.data as SqlResultSet); + + // TODO: seems better to do not measure container size + // for detecting max columns size, better to use configurable variable + const measuredCells = TextTools.getWidth({ + font: '400 14px Roboto', + text: columnNames.map((cell, i) => { + if (cell.length > (rowStrings[i] || '').length) { + return cell; + } + return rowStrings[i]; + }), + }).map(v => v + 16 + 32 + 20); + + // TODO: we need some result type specified formatter to common actions with data + const rows = (modelResultData.data as SqlResultSet).rows || []; + + const columns = (modelResultData.data as SqlResultSet).columns!.map>((col, columnIndex) => ({ + key: columnIndex + '', + name: col.label!, + width: Math.min(300, measuredCells[columnIndex]), + headerRenderer: TableColumnHeader, + })); + columns.unshift(indexColumn); + + return { rows, columns }; + }), [modelResultData]).get(); + + const isIndexColumn = useCallback((columnKey: string) => columnKey === indexColumn.key, []); + + const isIndexColumnInRange = useCallback( + (columnsRange: Array>) => columnsRange.some(column => isIndexColumn(column.key)), [isIndexColumn] + ); + + const getColumnKeyFromColumnIndex = useCallback((columnIndex: number) => columns[columnIndex].key, [columns]); + + const getColumnsInRange = useCallback((startIndex: number, endIndex: number) => { + if (startIndex === endIndex) { + return [columns[startIndex]]; + } + + const firstIndex = Math.min(startIndex, endIndex); + const lastIndex = Math.max(startIndex, endIndex); + return columns.slice(firstIndex, lastIndex + 1); + }, [columns]); + + return { + columns, rows, getColumnsInRange, isIndexColumn, isIndexColumnInRange, getColumnKeyFromColumnIndex, + }; +} From 172ba715325fcd93a16e6ef779e28152ff284246 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 17 Feb 2021 17:50:46 +0300 Subject: [PATCH 5/7] feat(plugin-data-spreadsheet-new): CB-696 replaced react state with ref object --- .../useGridSelectionContext.tsx | 45 +++------ .../src/DataGrid/useGridDragging.ts | 94 +++++++++++-------- 2 files changed, 68 insertions(+), 71 deletions(-) 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 36a3b1628e..62ecc5e931 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 @@ -39,6 +39,11 @@ export function useGridSelectionContext(tableData: ITableData) { const lastRowIdx = Math.max(startPosition, lastPosition); for (let rowIdx = firstRowIdx; rowIdx <= lastRowIdx; rowIdx++) { + if (isRowSelected(rowIdx)) { + selectedCells.delete(rowIdx); + continue; + } + if (temporary) { temporarySelectedCells.set(rowIdx, rowSelection); } else { @@ -48,19 +53,11 @@ export function useGridSelectionContext(tableData: ITableData) { }), [selectedCells, temporarySelectedCells]); const isRowSelected = useCallback((rowIdx: number) => { - if (!tableData) { - throw new Error('Table data must be provided'); - } - const columnsLength = tableData.columns?.length; return selectedCells.get(rowIdx)?.length === columnsLength; }, [tableData, selectedCells]); - const unSelectRow = useCallback((rowIdx: number) => { - selectedCells.delete(rowIdx); - }, [selectedCells]); - const selectRange = useCallback(action( (startPosition: number, lastPosition: number, columns: number[], multiple: boolean, temporary = false) => { if (!multiple) { @@ -118,15 +115,6 @@ export function useGridSelectionContext(tableData: ITableData) { return result; }, [selectedCells, tableData]); - const unSelectColumn = useCallback((columnIndex: number) => { - const rowsLength = tableData.rows?.length || 0; - - for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { - const rowSelection = selectedCells.get(rowIdx) || []; - selectedCells.set(rowIdx, [...rowSelection.filter(colIdx => colIdx !== columnIndex)]); - } - }, [selectedCells, tableData]); - const selectColumn = useCallback(action((columnKey: string, multiple: boolean) => { if (!multiple) { selectedCells.clear(); @@ -135,15 +123,17 @@ export function useGridSelectionContext(tableData: ITableData) { const rowsLength = tableData.rows?.length || 0; const columnIndex = Number(columnKey); - if (isColumnSelected(columnIndex)) { - unSelectColumn(columnIndex); - } else { - for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { - const rowSelection = selectedCells.get(rowIdx) || []; + const isSelected = isColumnSelected(columnIndex); + + for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { + const rowSelection = selectedCells.get(rowIdx) || []; + if (isSelected) { + selectedCells.set(rowIdx, [...rowSelection.filter(colIdx => colIdx !== columnIndex)]); + } else { selectedCells.set(rowIdx, [...rowSelection, columnIndex]); } } - }), [tableData, selectedCells, isColumnSelected, unSelectColumn]); + }), [tableData, selectedCells, isColumnSelected]); const selectTable = useCallback(() => { const rowsLength = tableData.rows?.length || 0; @@ -216,11 +206,8 @@ export function useGridSelectionContext(tableData: ITableData) { } if (isIndexColumn) { - if (multiple && isRowSelected(rowIdx)) { - unSelectRow(rowIdx); - } else { - selectRows(rowIdx, rowIdx, multiple); - } + selectRows(rowIdx, rowIdx, multiple); + return; } @@ -241,8 +228,6 @@ export function useGridSelectionContext(tableData: ITableData) { isSelected, selectRows, selectCell, - isRowSelected, - unSelectRow, lastSelectedCell, updateMultiSelection, ]); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts index 5031221d29..0ef45002f2 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts @@ -6,7 +6,9 @@ * you may not use this file except in compliance with the License. */ -import { useState, useCallback, useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; + +import { useObjectRef } from '@cloudbeaver/core-blocks'; interface IDraggingPosition { idx: number; @@ -24,6 +26,14 @@ type DraggingCallback = ( event: React.MouseEvent | MouseEvent ) => void; +interface IDraggingState { + startDraggingCell: IDraggingPosition | null; + currentDraggingCell: IDraggingPosition | null; + startMousePosition: IMousePosition | null; + dragging: boolean; + mouseDown: boolean; +} + interface IDraggingCallbacks { onDragStart?: ( startPosition: IDraggingPosition, @@ -75,29 +85,31 @@ function isDraggingStarted(delta: number | null, threshold: number) { return delta > threshold; } -export function useGridDragging(callbacks: IDraggingCallbacks) { - const { onDragStart, onDragOver, onDragEnd } = callbacks; +export function useGridDragging(props: IDraggingCallbacks) { + const callbacks = useObjectRef(props); - const [startDraggingCell, setStartDraggingCell] = useState(null); - const [currentDraggingCell, setCurrentDraggingCell] = useState(null); - const [startMousePosition, setStartMousePosition] = useState(null); - const [isDragging, setDragging] = useState(false); - const [mouseDown, setMouseDown] = useState(false); + const state = useObjectRef({ + startDraggingCell: null, + currentDraggingCell: null, + startMousePosition: null, + dragging: false, + mouseDown: false, + }, {}); - const onMouseDownHandler = (event: React.MouseEvent) => { + const onMouseDownHandler = useCallback((event: React.MouseEvent) => { const position = getCellPositionFromEvent(event); if (!position) { return; } - setMouseDown(true); - setStartMousePosition({ ...startMousePosition, x: event.pageX, y: event.pageY }); - setStartDraggingCell({ idx: position.colIdx, rowIdx: position.rowIdx }); - }; + state.current.mouseDown = true; + state.current.startMousePosition = { x: event.pageX, y: event.pageY }; + state.current.startDraggingCell = { idx: position.colIdx, rowIdx: position.rowIdx }; + }, []); - const onMouseMoveHandler = (event: React.MouseEvent) => { - if (!mouseDown) { + const onMouseMoveHandler = useCallback((event: React.MouseEvent) => { + if (!state.current.mouseDown) { return; } @@ -107,33 +119,33 @@ export function useGridDragging(callbacks: IDraggingCallbacks) { return; } - if (!isDragging) { - const delta = getDelta(startMousePosition, { x: event.pageX, y: event.pageY }); + if (!state.current.dragging) { + const delta = getDelta(state.current.startMousePosition, { x: event.pageX, y: event.pageY }); if (!isDraggingStarted(delta, THRESHOLD)) { return; } - if (onDragStart && startDraggingCell) { - onDragStart(startDraggingCell, event); + if (callbacks.current.onDragStart && state.current.startDraggingCell) { + callbacks.current.onDragStart(state.current.startDraggingCell, event); } - setDragging(true); + state.current.dragging = true; return; } // check if the new cell is equal to the previous cell - if (position.rowIdx === currentDraggingCell?.rowIdx - && position.colIdx === currentDraggingCell.idx) { + if (position.rowIdx === state.current.currentDraggingCell?.rowIdx + && position.colIdx === state.current.currentDraggingCell.idx) { return; } - setCurrentDraggingCell({ idx: position.colIdx, rowIdx: position.rowIdx }); + state.current.currentDraggingCell = { idx: position.colIdx, rowIdx: position.rowIdx }; - if (onDragOver) { - onDragOver( + if (callbacks.current.onDragOver) { + callbacks.current.onDragOver( { - idx: startDraggingCell!.idx, - rowIdx: startDraggingCell!.rowIdx, + idx: state.current.startDraggingCell!.idx, + rowIdx: state.current.startDraggingCell!.rowIdx, }, { idx: position.colIdx, @@ -141,33 +153,33 @@ export function useGridDragging(callbacks: IDraggingCallbacks) { }, event); } - }; + }, []); const onMouseUpHandler = useCallback((event: React.MouseEvent | MouseEvent) => { - setMouseDown(false); - setStartMousePosition(null); + state.current.mouseDown = false; + state.current.startMousePosition = null; - if (!isDragging || !startDraggingCell || !currentDraggingCell) { + if (!state.current.dragging || !state.current.startDraggingCell || !state.current.currentDraggingCell) { return; } - if (onDragEnd) { - onDragEnd( + if (callbacks.current.onDragEnd) { + callbacks.current.onDragEnd( { - idx: startDraggingCell.idx, - rowIdx: startDraggingCell.rowIdx, + idx: state.current.startDraggingCell.idx, + rowIdx: state.current.startDraggingCell.rowIdx, }, { - idx: currentDraggingCell.idx, - rowIdx: currentDraggingCell.rowIdx, + idx: state.current.currentDraggingCell.idx, + rowIdx: state.current.currentDraggingCell.rowIdx, }, event); } - setDragging(false); - setStartDraggingCell(null); - setCurrentDraggingCell(null); - }, [isDragging, currentDraggingCell, startDraggingCell, onDragEnd]); + state.current.dragging = false; + state.current.startMousePosition = null; + state.current.currentDraggingCell = null; + }, []); useEffect(() => { document.addEventListener('mouseup', onMouseUpHandler); From 45037a6f7fb81ad045ed3607d2b91290e6229085 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 17 Feb 2021 19:07:52 +0300 Subject: [PATCH 6/7] feat(plugin-data-spreadsheet-new): CB-696 remove unSelect --- .../useGridSelectionContext.tsx | 44 ++++----------- .../src/DataGrid/useGridDragging.ts | 56 +++++++++---------- 2 files changed, 40 insertions(+), 60 deletions(-) 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 62ecc5e931..5ad491d0ba 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 @@ -101,29 +101,26 @@ export function useGridSelectionContext(tableData: ITableData) { }, [selectRange, selectRows, tableData, temporarySelectedCells]); const isColumnSelected = useCallback((columnIndex: number) => { - let result = true; - const rowsLength = tableData.rows?.length || 0; for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { const rowSelection = selectedCells.get(rowIdx) || []; if (!rowSelection.includes(columnIndex)) { - result = false; - break; + return false; } } - return result; + return true; }, [selectedCells, tableData]); const selectColumn = useCallback(action((columnKey: string, multiple: boolean) => { + const columnIndex = Number(columnKey); + const isSelected = isColumnSelected(columnIndex); + if (!multiple) { selectedCells.clear(); } const rowsLength = tableData.rows?.length || 0; - const columnIndex = Number(columnKey); - - const isSelected = isColumnSelected(columnIndex); for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) { const rowSelection = selectedCells.get(rowIdx) || []; @@ -163,22 +160,6 @@ export function useGridSelectionContext(tableData: ITableData) { }, [selectedCells, temporarySelectedCells]); - const unSelect = useCallback((idx: number, rowIdx: number) => { - if (!selectedCells.has(rowIdx)) { - return false; - } - - const rowSelection = selectedCells.get(rowIdx)!; - const targetIndex = rowSelection.indexOf(idx); - - if (targetIndex !== -1) { - rowSelection.splice(targetIndex, 1); - return true; - } - - return false; - }, [selectedCells]); - const selectCell = useCallback((columnIdx: number, rowIdx: number) => { const rowSelection = selectedCells.get(rowIdx); if (rowSelection === undefined) { @@ -186,6 +167,13 @@ export function useGridSelectionContext(tableData: ITableData) { return; } + const targetIndex = rowSelection.indexOf(columnIdx); + + if (targetIndex !== -1) { + rowSelection.splice(targetIndex, 1); + return; + } + rowSelection.push(columnIdx); }, [selectedCells]); @@ -207,7 +195,6 @@ export function useGridSelectionContext(tableData: ITableData) { if (isIndexColumn) { selectRows(rowIdx, rowIdx, multiple); - return; } @@ -215,17 +202,10 @@ export function useGridSelectionContext(tableData: ITableData) { selectedCells.clear(); } - if (isSelected(columnKey, rowIdx)) { - unSelect(Number(columnKey), rowIdx); - return; - } - selectCell(Number(columnKey), rowIdx); }, [ tableData, selectedCells, - unSelect, - isSelected, selectRows, selectCell, lastSelectedCell, diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts index 0ef45002f2..e86f9f467b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/useGridDragging.ts @@ -103,13 +103,13 @@ export function useGridDragging(props: IDraggingCallbacks) { return; } - state.current.mouseDown = true; - state.current.startMousePosition = { x: event.pageX, y: event.pageY }; - state.current.startDraggingCell = { idx: position.colIdx, rowIdx: position.rowIdx }; + state.mouseDown = true; + state.startMousePosition = { x: event.pageX, y: event.pageY }; + state.startDraggingCell = { idx: position.colIdx, rowIdx: position.rowIdx }; }, []); const onMouseMoveHandler = useCallback((event: React.MouseEvent) => { - if (!state.current.mouseDown) { + if (!state.mouseDown) { return; } @@ -119,33 +119,33 @@ export function useGridDragging(props: IDraggingCallbacks) { return; } - if (!state.current.dragging) { - const delta = getDelta(state.current.startMousePosition, { x: event.pageX, y: event.pageY }); + if (!state.dragging) { + const delta = getDelta(state.startMousePosition, { x: event.pageX, y: event.pageY }); if (!isDraggingStarted(delta, THRESHOLD)) { return; } - if (callbacks.current.onDragStart && state.current.startDraggingCell) { - callbacks.current.onDragStart(state.current.startDraggingCell, event); + if (callbacks.onDragStart && state.startDraggingCell) { + callbacks.onDragStart(state.startDraggingCell, event); } - state.current.dragging = true; + state.dragging = true; return; } // check if the new cell is equal to the previous cell - if (position.rowIdx === state.current.currentDraggingCell?.rowIdx - && position.colIdx === state.current.currentDraggingCell.idx) { + if (position.rowIdx === state.currentDraggingCell?.rowIdx + && position.colIdx === state.currentDraggingCell.idx) { return; } - state.current.currentDraggingCell = { idx: position.colIdx, rowIdx: position.rowIdx }; + state.currentDraggingCell = { idx: position.colIdx, rowIdx: position.rowIdx }; - if (callbacks.current.onDragOver) { - callbacks.current.onDragOver( + if (callbacks.onDragOver) { + callbacks.onDragOver( { - idx: state.current.startDraggingCell!.idx, - rowIdx: state.current.startDraggingCell!.rowIdx, + idx: state.startDraggingCell!.idx, + rowIdx: state.startDraggingCell!.rowIdx, }, { idx: position.colIdx, @@ -156,29 +156,29 @@ export function useGridDragging(props: IDraggingCallbacks) { }, []); const onMouseUpHandler = useCallback((event: React.MouseEvent | MouseEvent) => { - state.current.mouseDown = false; - state.current.startMousePosition = null; + state.mouseDown = false; + state.startMousePosition = null; - if (!state.current.dragging || !state.current.startDraggingCell || !state.current.currentDraggingCell) { + if (!state.dragging || !state.startDraggingCell || !state.currentDraggingCell) { return; } - if (callbacks.current.onDragEnd) { - callbacks.current.onDragEnd( + if (callbacks.onDragEnd) { + callbacks.onDragEnd( { - idx: state.current.startDraggingCell.idx, - rowIdx: state.current.startDraggingCell.rowIdx, + idx: state.startDraggingCell.idx, + rowIdx: state.startDraggingCell.rowIdx, }, { - idx: state.current.currentDraggingCell.idx, - rowIdx: state.current.currentDraggingCell.rowIdx, + idx: state.currentDraggingCell.idx, + rowIdx: state.currentDraggingCell.rowIdx, }, event); } - state.current.dragging = false; - state.current.startMousePosition = null; - state.current.currentDraggingCell = null; + state.dragging = false; + state.startMousePosition = null; + state.currentDraggingCell = null; }, []); useEffect(() => { From 73c4688c3aa085a8d9f06c5530dd68d45a618913 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Wed, 17 Feb 2021 19:14:25 +0300 Subject: [PATCH 7/7] feat(plugin-data-spreadsheet-new): CB-696 inline isRowSelected --- .../useGridSelectionContext.tsx | 16 ++++++---------- .../TableColumnHeader/TableIndexColumnHeader.tsx | 6 +----- 2 files changed, 7 insertions(+), 15 deletions(-) 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 5ad491d0ba..744f35e20b 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 @@ -26,6 +26,8 @@ export function useGridSelectionContext(tableData: ITableData) { (startPosition: number, lastPosition: number, multiple: boolean, temporary = false) => { const columnsLength = tableData.columns?.length || 0; + temporarySelectedCells.clear(); + if (!multiple) { selectedCells.clear(); } @@ -39,7 +41,7 @@ export function useGridSelectionContext(tableData: ITableData) { const lastRowIdx = Math.max(startPosition, lastPosition); for (let rowIdx = firstRowIdx; rowIdx <= lastRowIdx; rowIdx++) { - if (isRowSelected(rowIdx)) { + if (selectedCells.get(rowIdx)?.length === columnsLength) { selectedCells.delete(rowIdx); continue; } @@ -52,14 +54,10 @@ export function useGridSelectionContext(tableData: ITableData) { } }), [selectedCells, temporarySelectedCells]); - const isRowSelected = useCallback((rowIdx: number) => { - const columnsLength = tableData.columns?.length; - - return selectedCells.get(rowIdx)?.length === columnsLength; - }, [tableData, selectedCells]); - const selectRange = useCallback(action( (startPosition: number, lastPosition: number, columns: number[], multiple: boolean, temporary = false) => { + temporarySelectedCells.clear(); + if (!multiple) { selectedCells.clear(); } @@ -85,8 +83,6 @@ export function useGridSelectionContext(tableData: ITableData) { const columnsInRange = tableData.getColumnsInRange(startPosition.idx, lastPosition.idx); const isIndexColumnInRange = tableData.isIndexColumnInRange(columnsInRange); - temporarySelectedCells.clear(); - if (isIndexColumnInRange) { selectRows(startPosition.rowIdx, lastPosition.rowIdx, multiple, temporary); } else { @@ -98,7 +94,7 @@ export function useGridSelectionContext(tableData: ITableData) { temporary ); } - }, [selectRange, selectRows, tableData, temporarySelectedCells]); + }, [selectRange, selectRows, tableData]); const isColumnSelected = useCallback((columnIndex: number) => { const rowsLength = tableData.rows?.length || 0; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx index 6d38fdf80e..f9be7b6f5c 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx @@ -14,11 +14,7 @@ import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelection export const TableIndexColumnHeader: React.FC> = function TableIndexColumnHeader(props) { const selectionContext = useContext(DataGridSelectionContext); - const indexClickHandler = (event: React.MouseEvent) => { - selectionContext?.selectTable(); - }; - return ( -
{props.column.name}
+
selectionContext?.selectTable()}>{props.column.name}
); };