mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
+5
-3
@@ -36,8 +36,8 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
|
||||
classes.push('rdg-cell-custom-edited');
|
||||
}
|
||||
|
||||
const handleClick = useCallback((event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
selectionContext?.select(column.key, rowIdx, event.ctrlKey, event.shiftKey);
|
||||
const handleMouseDown = useCallback((event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
selectionContext?.select(column.idx, rowIdx, event.ctrlKey, event.shiftKey);
|
||||
}, [column, rowIdx, selectionContext]);
|
||||
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
@@ -49,7 +49,9 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
|
||||
return (
|
||||
<Cell
|
||||
className={classes.join(' ')}
|
||||
onClick={handleClick}
|
||||
data-rowindex={rowIdx}
|
||||
data-columnindex={column.idx}
|
||||
onMouseDown={handleMouseDown}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
{...props}
|
||||
row={row}
|
||||
|
||||
+9
-2
@@ -8,10 +8,17 @@
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { IPosition } from './useGridSelectionContext';
|
||||
|
||||
export interface IDataGridSelectionContext {
|
||||
selectedCells: Map<number, number[]>;
|
||||
select: (key: string, rowIdx: number, multiple: boolean, range: boolean) => void;
|
||||
isSelected: (key: string, rowIdx: number) => boolean;
|
||||
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<IDataGridSelectionContext | null>(null);
|
||||
|
||||
+157
-100
@@ -9,156 +9,213 @@
|
||||
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 type { ITableData } from '../useTableData';
|
||||
import type { IDataGridSelectionContext } from './DataGridSelectionContext';
|
||||
|
||||
interface IPosition {
|
||||
export interface IPosition {
|
||||
idx: number;
|
||||
rowIdx: number;
|
||||
isIndexColumn: boolean;
|
||||
}
|
||||
|
||||
interface IGridSelectionOptions {
|
||||
indexColumnKey: string;
|
||||
}
|
||||
|
||||
export function useGridSelectionContext(modelResultData: IDatabaseDataResult | null, options: IGridSelectionOptions) {
|
||||
export function useGridSelectionContext(tableData: ITableData) {
|
||||
const [selectedCells] = useState(() => observable.map<number, number[]>());
|
||||
const [temporarySelectedCells] = useState(() => observable.map<number, number[]>());
|
||||
const [lastSelectedCell, setLastSelectedCell] = useState<IPosition | null>(null);
|
||||
|
||||
const selectRow = useCallback(action((rowIdx: number, multiple: boolean) => {
|
||||
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;
|
||||
temporarySelectedCells.clear();
|
||||
|
||||
if (!multiple) {
|
||||
selectedCells.clear();
|
||||
}
|
||||
|
||||
const rowSelection = [];
|
||||
for (let i = 0; i < columnsLength; i++) {
|
||||
rowSelection.push(i);
|
||||
}
|
||||
|
||||
const firstRowIdx = Math.min(startPosition, lastPosition);
|
||||
const lastRowIdx = Math.max(startPosition, lastPosition);
|
||||
|
||||
for (let rowIdx = firstRowIdx; rowIdx <= lastRowIdx; rowIdx++) {
|
||||
if (selectedCells.get(rowIdx)?.length === columnsLength) {
|
||||
selectedCells.delete(rowIdx);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (temporary) {
|
||||
temporarySelectedCells.set(rowIdx, rowSelection);
|
||||
} else {
|
||||
selectedCells.set(rowIdx, rowSelection);
|
||||
}
|
||||
}
|
||||
}), [selectedCells, temporarySelectedCells]);
|
||||
|
||||
const selectRange = useCallback(action(
|
||||
(startPosition: number, lastPosition: number, columns: number[], multiple: boolean, temporary = false) => {
|
||||
temporarySelectedCells.clear();
|
||||
|
||||
if (!multiple) {
|
||||
selectedCells.clear();
|
||||
}
|
||||
|
||||
const top = Math.min(startPosition, lastPosition);
|
||||
const bottom = Math.max(startPosition, lastPosition);
|
||||
|
||||
for (let rowIdx = top; rowIdx <= bottom; rowIdx++) {
|
||||
if (temporary) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}), [selectedCells, temporarySelectedCells]);
|
||||
|
||||
const updateMultiSelection = useCallback(
|
||||
(startPosition: IPosition, lastPosition: IPosition, multiple: boolean, temporary: boolean) => {
|
||||
const columnsInRange = tableData.getColumnsInRange(startPosition.idx, lastPosition.idx);
|
||||
const isIndexColumnInRange = tableData.isIndexColumnInRange(columnsInRange);
|
||||
|
||||
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]);
|
||||
|
||||
const isColumnSelected = useCallback((columnIndex: number) => {
|
||||
const rowsLength = tableData.rows?.length || 0;
|
||||
|
||||
for (let rowIdx = 0; rowIdx < rowsLength; rowIdx++) {
|
||||
const rowSelection = selectedCells.get(rowIdx) || [];
|
||||
if (!rowSelection.includes(columnIndex)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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 rowSelection = [];
|
||||
for (let i = 0; i < columnsLength; i++) {
|
||||
rowSelection.push(i);
|
||||
const rowsLength = tableData.rows?.length || 0;
|
||||
|
||||
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]);
|
||||
|
||||
selectedCells.set(rowIdx, rowSelection);
|
||||
}), [selectedCells, modelResultData]);
|
||||
|
||||
const isRowSelected = useCallback((rowIdx: number) => {
|
||||
if (!modelResultData) {
|
||||
throw new Error('Model result data must be provided');
|
||||
}
|
||||
|
||||
const columnsLength = (modelResultData.data as SqlResultSet).columns?.length || 0;
|
||||
|
||||
return selectedCells.get(rowIdx)?.length === columnsLength;
|
||||
}, [modelResultData, selectedCells]);
|
||||
|
||||
const unSelectRow = useCallback((rowIdx: number) => {
|
||||
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');
|
||||
}
|
||||
|
||||
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 selectTable = useCallback(() => {
|
||||
const rowsLength = tableData.rows?.length || 0;
|
||||
const columnsLength = tableData.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]);
|
||||
}, [tableData, selectedCells]);
|
||||
|
||||
const isSelected = useCallback((key: string, rowIdx: number) => {
|
||||
if (!selectedCells.has(rowIdx)) {
|
||||
const isSelected = useCallback((columnKey: string, rowIdx: number) => {
|
||||
if (!selectedCells.has(rowIdx) && !temporarySelectedCells.has(rowIdx)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rowSelection = selectedCells.get(rowIdx)!;
|
||||
const idx = Number.parseInt(key);
|
||||
return rowSelection.includes(idx);
|
||||
const rowSelection = selectedCells.get(rowIdx);
|
||||
const temporaryRowSelection = temporarySelectedCells.get(rowIdx);
|
||||
|
||||
const idx = Number(columnKey);
|
||||
|
||||
return !!(rowSelection?.includes(idx) || temporaryRowSelection?.includes(idx));
|
||||
},
|
||||
[selectedCells]);
|
||||
[selectedCells, temporarySelectedCells]);
|
||||
|
||||
const unSelect = useCallback((idx: number, rowIdx: number) => {
|
||||
if (!selectedCells.has(rowIdx)) {
|
||||
return false;
|
||||
const selectCell = useCallback((columnIdx: number, rowIdx: number) => {
|
||||
const rowSelection = selectedCells.get(rowIdx);
|
||||
if (rowSelection === undefined) {
|
||||
selectedCells.set(rowIdx, [columnIdx]);
|
||||
return;
|
||||
}
|
||||
|
||||
const rowSelection = selectedCells.get(rowIdx)!;
|
||||
const targetIndex = rowSelection.indexOf(idx);
|
||||
const targetIndex = rowSelection.indexOf(columnIdx);
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
rowSelection.splice(targetIndex, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [selectedCells]);
|
||||
|
||||
const selectCell = useCallback((idx: number, rowIdx: number) => {
|
||||
const rowSelection = selectedCells.get(rowIdx);
|
||||
if (rowSelection === undefined) {
|
||||
selectedCells.set(rowIdx, [idx]);
|
||||
return;
|
||||
}
|
||||
|
||||
rowSelection.push(idx);
|
||||
rowSelection.push(columnIdx);
|
||||
}, [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 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 });
|
||||
setLastSelectedCell({ idx: columnIndex, rowIdx });
|
||||
|
||||
if (selectedCells.size > 0 && range) {
|
||||
selectRange(columnIndex, rowIdx, isIndexColumn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isIndexColumn) {
|
||||
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) {
|
||||
selectRows(rowIdx, rowIdx, multiple);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected(key, rowIdx)) {
|
||||
unSelect(columnIndex, rowIdx);
|
||||
return;
|
||||
}
|
||||
if (!multiple) {
|
||||
selectedCells.clear();
|
||||
}
|
||||
|
||||
selectCell(columnIndex, rowIdx);
|
||||
}, [selectedCells, selectRow, selectRange, unSelect, isSelected, selectCell, isRowSelected, unSelectRow]);
|
||||
selectCell(Number(columnKey), rowIdx);
|
||||
}, [
|
||||
tableData,
|
||||
selectedCells,
|
||||
selectRows,
|
||||
selectCell,
|
||||
lastSelectedCell,
|
||||
updateMultiSelection,
|
||||
]);
|
||||
|
||||
const context: IDataGridSelectionContext = useMemo(() => ({
|
||||
selectedCells,
|
||||
select,
|
||||
selectColumn,
|
||||
selectTable,
|
||||
isSelected,
|
||||
}), [selectedCells, select, isSelected]);
|
||||
updateMultiSelection,
|
||||
}), [selectedCells, select, isSelected, selectColumn, selectTable, updateMultiSelection]);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -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 } 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';
|
||||
@@ -30,11 +26,11 @@ 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 { useGridDragging } from './useGridDragging';
|
||||
import { useGridSelectedCellsCopy } from './useGridSelectedCellsCopy';
|
||||
import { useTableData } from './useTableData';
|
||||
|
||||
interface Props {
|
||||
model: IDatabaseDataModel<any>;
|
||||
@@ -47,25 +43,18 @@ function isAtBottom(event: React.UIEvent<HTMLDivElement>): boolean {
|
||||
return target.clientHeight + target.scrollTop === target.scrollHeight;
|
||||
}
|
||||
|
||||
const indexColumn: Column<any[], any> = {
|
||||
key: Number.MAX_SAFE_INTEGER + '',
|
||||
name: '#',
|
||||
minWidth: 60,
|
||||
width: 60,
|
||||
resizable: false,
|
||||
frozen: true,
|
||||
formatter: IndexFormatter,
|
||||
};
|
||||
|
||||
export const DataGridTable: React.FC<Props> = observer(function DataGridTable({ model, resultIndex, className }) {
|
||||
const dataGridRef = useRef<DataGridHandle>(null);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const styles = useStyles(reactGridStyles, baseStyles);
|
||||
const [columnResize] = useState(() => new Executor<IColumnResizeInfo>());
|
||||
|
||||
const modelResultData = model?.getResult(resultIndex);
|
||||
|
||||
const tableData = useTableData(modelResultData);
|
||||
|
||||
const gridSortingContext = useGridSortingContext(model);
|
||||
const gridSelectionContext = useGridSelectionContext(modelResultData, { indexColumnKey: indexColumn.key });
|
||||
const gridSelectionContext = useGridSelectionContext(tableData);
|
||||
const editingContext = useEditing({
|
||||
onEdit: (position, key) => {
|
||||
const editor = model.source.getEditor(resultIndex);
|
||||
@@ -86,6 +75,19 @@ export const DataGridTable: React.FC<Props> = observer(function DataGridTable({
|
||||
},
|
||||
});
|
||||
|
||||
const { onKeydownHandler } = useGridSelectedCellsCopy(modelResultData, gridSelectionContext);
|
||||
const { onMouseDownHandler, onMouseMoveHandler } = useGridDragging({
|
||||
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(() => {
|
||||
if (model.isLoading()) {
|
||||
editingContext.close();
|
||||
@@ -122,62 +124,37 @@ export const DataGridTable: React.FC<Props> = 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<Column<any[], any>>((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<IDataGridContext>(() => ({
|
||||
model,
|
||||
columnResize,
|
||||
resultIndex,
|
||||
getEditorPortal: () => editorRef.current,
|
||||
}), [model, resultIndex, editorRef]);
|
||||
getDataGridApi: () => dataGridRef.current,
|
||||
}), [model, resultIndex, editorRef, dataGridRef]);
|
||||
|
||||
return styled(styles)(
|
||||
<DataGridContext.Provider value={gridContext}>
|
||||
<DataGridSortingContext.Provider value={gridSortingContext}>
|
||||
<DataGridSelectionContext.Provider value={gridSelectionContext}>
|
||||
<EditingContext.Provider value={editingContext}>
|
||||
<DataGridTableContainer modelResultData={modelResultData}>
|
||||
<grid-container
|
||||
as='div'
|
||||
className="cb-react-grid-container"
|
||||
tabIndex={-1}
|
||||
onKeyDown={onKeydownHandler}
|
||||
onMouseDown={onMouseDownHandler}
|
||||
onMouseMove={onMouseMoveHandler}
|
||||
>
|
||||
<DataGrid
|
||||
ref={dataGridRef}
|
||||
className={`cb-react-grid-theme ${className}`}
|
||||
columns={columns}
|
||||
columns={tableData.columns}
|
||||
defaultColumnOptions={{
|
||||
minWidth: 40,
|
||||
resizable: true,
|
||||
formatter: CellFormatter,
|
||||
}}
|
||||
rows={rows}
|
||||
rows={tableData.rows}
|
||||
headerRowHeight={28}
|
||||
rowHeight={24}
|
||||
rowRenderer={RowRenderer}
|
||||
@@ -186,7 +163,7 @@ export const DataGridTable: React.FC<Props> = observer(function DataGridTable({
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
<div ref={editorRef} />
|
||||
</DataGridTableContainer>
|
||||
</grid-container>
|
||||
</EditingContext.Provider>
|
||||
</DataGridSelectionContext.Provider>
|
||||
</DataGridSortingContext.Provider>
|
||||
|
||||
+11
-3
@@ -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<HeaderRendererProps<any>> = 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<HeaderRendererProps<any>> = observer(fu
|
||||
const currentSortMode = gridSortingContext.getSortMode(columnName);
|
||||
|
||||
const handleSort = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
@@ -117,8 +121,12 @@ export const TableColumnHeader: React.FC<HeaderRendererProps<any>> = observer(fu
|
||||
gridSortingContext.setSortMode(columnName, nextSort, e.ctrlKey || e.metaKey);
|
||||
};
|
||||
|
||||
const handleColumnSelection = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
gridSelectionContext.selectColumn(calculatedColumn.key, e.ctrlKey);
|
||||
};
|
||||
|
||||
return styled(headerStyles)(
|
||||
<table-header as="div">
|
||||
<table-header as="div" onClick={handleColumnSelection}>
|
||||
<shrink-container as='div'>
|
||||
<icon as="div">
|
||||
<StaticImage icon={column?.icon} />
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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<HeaderRendererProps<any>> = function TableIndexColumnHeader(props) {
|
||||
const selectionContext = useContext(DataGridSelectionContext);
|
||||
|
||||
return (
|
||||
<div onClick={() => selectionContext?.selectTable()}>{props.column.name}</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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 { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
|
||||
interface IDraggingPosition {
|
||||
idx: number;
|
||||
rowIdx: number;
|
||||
}
|
||||
|
||||
interface IMousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
type DraggingCallback = (
|
||||
startPosition: IDraggingPosition,
|
||||
currentPosition: IDraggingPosition,
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent> | MouseEvent
|
||||
) => void;
|
||||
|
||||
interface IDraggingState {
|
||||
startDraggingCell: IDraggingPosition | null;
|
||||
currentDraggingCell: IDraggingPosition | null;
|
||||
startMousePosition: IMousePosition | null;
|
||||
dragging: boolean;
|
||||
mouseDown: boolean;
|
||||
}
|
||||
|
||||
interface IDraggingCallbacks {
|
||||
onDragStart?: (
|
||||
startPosition: IDraggingPosition,
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent> | MouseEvent
|
||||
) => void;
|
||||
onDragOver?: DraggingCallback;
|
||||
onDragEnd?: DraggingCallback;
|
||||
}
|
||||
|
||||
const THRESHOLD = 10;
|
||||
|
||||
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<HTMLDivElement, 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 columnIdx = cell.getAttribute('data-columnindex');
|
||||
|
||||
if (!rowIdx || !columnIdx) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
rowIdx: Number(rowIdx),
|
||||
colIdx: Number(columnIdx),
|
||||
};
|
||||
}
|
||||
|
||||
function isDraggingStarted(delta: number | null, threshold: number) {
|
||||
if (delta === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return delta > threshold;
|
||||
}
|
||||
|
||||
export function useGridDragging(props: IDraggingCallbacks) {
|
||||
const callbacks = useObjectRef(props);
|
||||
|
||||
const state = useObjectRef<IDraggingState>({
|
||||
startDraggingCell: null,
|
||||
currentDraggingCell: null,
|
||||
startMousePosition: null,
|
||||
dragging: false,
|
||||
mouseDown: false,
|
||||
}, {});
|
||||
|
||||
const onMouseDownHandler = useCallback((event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const position = getCellPositionFromEvent(event);
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
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<HTMLDivElement, MouseEvent>) => {
|
||||
if (!state.mouseDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = getCellPositionFromEvent(event);
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.dragging) {
|
||||
const delta = getDelta(state.startMousePosition, { x: event.pageX, y: event.pageY });
|
||||
if (!isDraggingStarted(delta, THRESHOLD)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (callbacks.onDragStart && state.startDraggingCell) {
|
||||
callbacks.onDragStart(state.startDraggingCell, event);
|
||||
}
|
||||
|
||||
state.dragging = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the new cell is equal to the previous cell
|
||||
if (position.rowIdx === state.currentDraggingCell?.rowIdx
|
||||
&& position.colIdx === state.currentDraggingCell.idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentDraggingCell = { idx: position.colIdx, rowIdx: position.rowIdx };
|
||||
|
||||
if (callbacks.onDragOver) {
|
||||
callbacks.onDragOver(
|
||||
{
|
||||
idx: state.startDraggingCell!.idx,
|
||||
rowIdx: state.startDraggingCell!.rowIdx,
|
||||
},
|
||||
{
|
||||
idx: position.colIdx,
|
||||
rowIdx: position.rowIdx,
|
||||
},
|
||||
event);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onMouseUpHandler = useCallback((event: React.MouseEvent<HTMLDivElement, MouseEvent> | MouseEvent) => {
|
||||
state.mouseDown = false;
|
||||
state.startMousePosition = null;
|
||||
|
||||
if (!state.dragging || !state.startDraggingCell || !state.currentDraggingCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (callbacks.onDragEnd) {
|
||||
callbacks.onDragEnd(
|
||||
{
|
||||
idx: state.startDraggingCell.idx,
|
||||
rowIdx: state.startDraggingCell.rowIdx,
|
||||
},
|
||||
{
|
||||
idx: state.currentDraggingCell.idx,
|
||||
rowIdx: state.currentDraggingCell.rowIdx,
|
||||
},
|
||||
event);
|
||||
}
|
||||
|
||||
state.dragging = false;
|
||||
state.startMousePosition = null;
|
||||
state.currentDraggingCell = null;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', onMouseUpHandler);
|
||||
return () => document.removeEventListener('mouseup', onMouseUpHandler);
|
||||
}, [onMouseUpHandler]);
|
||||
|
||||
return {
|
||||
onMouseDownHandler,
|
||||
onMouseMoveHandler,
|
||||
};
|
||||
}
|
||||
+10
-35
@@ -6,29 +6,13 @@
|
||||
* 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<HTMLDivElement> & KeyboardEvent;
|
||||
|
||||
export const DataGridTableContainer: React.FC<Props> = 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)(
|
||||
<grid-container as='div' tabIndex={-1} className={className} onKeyDown={onKeydownHandler}>
|
||||
{children}
|
||||
</grid-container>
|
||||
);
|
||||
};
|
||||
return { onKeydownHandler };
|
||||
}
|
||||
@@ -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<any[], any> = {
|
||||
key: Number.MAX_SAFE_INTEGER + '',
|
||||
name: '#',
|
||||
minWidth: 60,
|
||||
width: 60,
|
||||
resizable: false,
|
||||
frozen: true,
|
||||
headerRenderer: TableIndexColumnHeader,
|
||||
formatter: IndexFormatter,
|
||||
};
|
||||
|
||||
export interface ITableData {
|
||||
columns: Array<Column<any[], any>>;
|
||||
rows: any[][];
|
||||
getColumnsInRange: (startIndex: number, endIndex: number) => Array<Column<any[], any>>;
|
||||
isIndexColumn: (columnKey: string) => boolean;
|
||||
isIndexColumnInRange: (columnsRange: Array<Column<any[], any>>) => 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<Column<any[], any>>((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<Column<any[], any>>) => 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user