diff --git a/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx b/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx index 4de5295d951..15c4097b0a0 100644 --- a/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx +++ b/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import { useGridReactiveValue } from './useGridReactiveValue.js'; import { mapCellContentRenderer } from './mapCellContentRenderer.js'; import { mapRenderHeaderCell } from './mapRenderHeaderCell.js'; import { mapEditCellRenderer } from './mapEditCellRenderer.js'; +import { mapSummaryCellRenderer } from './mapSummaryCellRenderer.js'; import { DataGridRowContext, type IDataGridRowContext } from './DataGridRowContext.js'; import './DataGrid.css'; import { HeaderDnDContext, isColumn, useHeaderDnD } from './useHeaderDnD.js'; @@ -71,6 +72,7 @@ export const DataGrid = forwardRef(function DataGrid getHeaderResizable, columnSortable, getHeaderHeight, + getSummaryRowHeight, getHeaderPinned, columnSortingState, getHeaderDnD, @@ -87,6 +89,8 @@ export const DataGrid = forwardRef(function DataGrid getRowId, getRowHeight, getRowClass, + getRowPinnedTop, + getRowPinnedBottom, onHeaderReorder, onScroll, onScrollToBottom, @@ -121,6 +125,7 @@ export const DataGrid = forwardRef(function DataGrid renderHeaderCell: mapRenderHeaderCell(i), renderCell: mapCellContentRenderer(i), renderEditCell: mapEditCellRenderer(i), + renderSummaryCell: mapSummaryCellRenderer(i), }; }); @@ -180,13 +185,31 @@ export const DataGrid = forwardRef(function DataGrid } } - const rows = useMemo( - () => - new Array(rowsCount).fill({ idx: 0 }).map((_, i) => ({ - idx: i, - })), - [rowsCount], - ); + const { rows, topPinnedRows, bottomPinnedRows } = useMemo(() => { + const allRows = new Array(rowsCount).fill({ idx: 0 }).map((_, i) => ({ + idx: i, + })); + + const topPinned: IInnerRow[] = []; + const bottomPinned: IInnerRow[] = []; + + for (const row of allRows) { + const isPinnedTop = getRowPinnedTop?.(row.idx) ?? false; + const isPinnedBottom = getRowPinnedBottom?.(row.idx) ?? false; + + if (isPinnedTop) { + topPinned.push(row); + } else if (isPinnedBottom) { + bottomPinned.push(row); + } + } + + return { + rows: allRows, + topPinnedRows: topPinned, + bottomPinnedRows: bottomPinned, + }; + }, [rowsCount, getRowPinnedTop, getRowPinnedBottom]); function handleCellFocus(args: CellSelectArgs) { onFocus?.({ colIdx: dndHeaderContext.getDataColIdxByKey(args.column.key), rowIdx: args.rowIdx }); @@ -215,7 +238,7 @@ export const DataGrid = forwardRef(function DataGrid return ( - + (function DataGrid rows={rows} className={className} headerRowHeight={getHeaderHeight?.()} + summaryRowHeight={getSummaryRowHeight?.()} rowHeight={getRowHeight ? row => getRowHeight(row.idx) : undefined} rowKeyGetter={getRowId ? row => getRowId(row.idx) : undefined} rowClass={getRowClass ? row => getRowClass(row.idx) : undefined} @@ -244,6 +268,8 @@ export const DataGrid = forwardRef(function DataGrid renderCell: cellRenderer, noRowsFallback: children, }} + topSummaryRows={topPinnedRows} + bottomSummaryRows={bottomPinnedRows} onScroll={onScroll} onSelectedCellChange={handleCellFocus} onCellKeyDown={handleCellKeyDown} diff --git a/webapp/common-react/@dbeaver/react-data-grid/src/DataGridHeaderCellContext.ts b/webapp/common-react/@dbeaver/react-data-grid/src/DataGridHeaderCellContext.ts index d167471bda0..024f54056c9 100644 --- a/webapp/common-react/@dbeaver/react-data-grid/src/DataGridHeaderCellContext.ts +++ b/webapp/common-react/@dbeaver/react-data-grid/src/DataGridHeaderCellContext.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ export interface IDataGridHeaderCellContext { getHeaderWidth?: (colIdx: number) => number | string | null; getHeaderResizable?: (colIdx: number) => boolean; getHeaderHeight?: () => number; + getSummaryRowHeight?: () => number; getHeaderPinned?: (colIdx: number) => boolean; getHeaderDnD?: (colIdx: number) => boolean; onHeaderReorder?: (from: number, to: number) => void; diff --git a/webapp/common-react/@dbeaver/react-data-grid/src/DataGridRowContext.ts b/webapp/common-react/@dbeaver/react-data-grid/src/DataGridRowContext.ts index 4e75224cb5e..459d201461d 100644 --- a/webapp/common-react/@dbeaver/react-data-grid/src/DataGridRowContext.ts +++ b/webapp/common-react/@dbeaver/react-data-grid/src/DataGridRowContext.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ export interface IDataGridRowContext { rowElement?: IGridReactiveValue; rowCount: IGridReactiveValue; onScrollToBottom?: () => void; + getRowPinnedTop?: (rowIdx: number) => boolean; + getRowPinnedBottom?: (rowIdx: number) => boolean; } export const DataGridRowContext = createContext(null); diff --git a/webapp/common-react/@dbeaver/react-data-grid/src/mapSummaryCellRenderer.tsx b/webapp/common-react/@dbeaver/react-data-grid/src/mapSummaryCellRenderer.tsx new file mode 100644 index 00000000000..0dd76ad99b6 --- /dev/null +++ b/webapp/common-react/@dbeaver/react-data-grid/src/mapSummaryCellRenderer.tsx @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { RenderSummaryCellProps } from 'react-data-grid'; +import type { IInnerRow } from './IInnerRow.js'; +import { SummaryCellRenderer } from './renderers/SummaryCellRenderer.js'; + +export function mapSummaryCellRenderer(colIdx: number) { + return function RenderSummaryCell({ row, column }: RenderSummaryCellProps) { + const rowIdx = (row as IInnerRow).idx; + return ; + }; +} diff --git a/webapp/common-react/@dbeaver/react-data-grid/src/renderers/SummaryCellRenderer.tsx b/webapp/common-react/@dbeaver/react-data-grid/src/renderers/SummaryCellRenderer.tsx new file mode 100644 index 00000000000..1507264f7a3 --- /dev/null +++ b/webapp/common-react/@dbeaver/react-data-grid/src/renderers/SummaryCellRenderer.tsx @@ -0,0 +1,40 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 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 { use, useMemo } from 'react'; +import { HeaderDnDContext } from '../useHeaderDnD.js'; +import { DataGridCellContext, type IDataGridCellRenderer, type IDataGridCellProps } from '../DataGridCellContext.js'; +import { useGridReactiveValue } from '../useGridReactiveValue.js'; +import type { CalculatedColumn } from 'react-data-grid'; +import type { IInnerRow } from '../IInnerRow.js'; +import { CellContentRenderer } from './CellContentRenderer.js'; + +export interface Props { + rowIdx: number; + column: CalculatedColumn; +} + +export function SummaryCellRenderer({ rowIdx, column }: Props): React.ReactNode { + const cellContext = use(DataGridCellContext); + const dndContext = use(HeaderDnDContext)!; + const dataColIdx = dndContext.getDataColIdxByKey(column.key); + + const mappedProps = useMemo( + () => ({ + isFocused: false, + }), + [], + ); + + const renderDefaultCell = useMemo( + () => () => , + [rowIdx, dataColIdx], + ); + + return useGridReactiveValue(cellContext?.cellElement, rowIdx, dataColIdx, mappedProps, renderDefaultCell); +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_PIN_ROW_BOTTOM.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_PIN_ROW_BOTTOM.ts new file mode 100644 index 00000000000..c50134b81ec --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_PIN_ROW_BOTTOM.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_PIN_ROW_BOTTOM = createAction('data-grid-pin-row-bottom', { + label: 'plugin_data_spreadsheet_new_pin_row_bottom', + icon: 'pin-row-bottom', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_PIN_ROW_TOP.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_PIN_ROW_TOP.ts new file mode 100644 index 00000000000..991b3f31f79 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_PIN_ROW_TOP.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_PIN_ROW_TOP = createAction('data-grid-pin-row-top', { + label: 'plugin_data_spreadsheet_new_pin_row_top', + icon: 'pin-row-top', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_UNPIN_ALL_ROWS.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_UNPIN_ALL_ROWS.ts new file mode 100644 index 00000000000..c6e32e49e6b --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_UNPIN_ALL_ROWS.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_UNPIN_ALL_ROWS = createAction('data-grid-unpin-all-rows', { + label: 'plugin_data_spreadsheet_new_unpin_all_rows', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_UNPIN_ROW.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_UNPIN_ROW.ts new file mode 100644 index 00000000000..753aae6750f --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Actions/Pin/ACTION_DATA_GRID_UNPIN_ROW.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DATA_GRID_UNPIN_ROW = createAction('data-grid-unpin-row', { + label: 'plugin_data_spreadsheet_new_unpin_row', + icon: 'unpin-row', +}); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuPinService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuPinService.ts new file mode 100644 index 00000000000..b6383cf6d0c --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuPinService.ts @@ -0,0 +1,203 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 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 { injectable } from '@cloudbeaver/core-di'; +import { ActionService, MenuService } from '@cloudbeaver/core-view'; +import { + DATA_CONTEXT_DV_DDM, + DATA_CONTEXT_DV_DDM_RESULT_INDEX, + DATA_CONTEXT_DV_PRESENTATION_ACTIONS, + DATA_CONTEXT_DV_RESULT_KEY, + GridViewAction, + IDatabaseDataViewAction, + isResultSetDataSource, + MENU_DV_CONTEXT_MENU, +} from '@cloudbeaver/plugin-data-viewer'; + +import { ACTION_DATA_GRID_PIN_COLUMN } from '../Actions/Pin/ACTION_DATA_GRID_PIN_COLUMN.js'; +import { ACTION_DATA_GRID_PIN_ROW_BOTTOM } from '../Actions/Pin/ACTION_DATA_GRID_PIN_ROW_BOTTOM.js'; +import { ACTION_DATA_GRID_PIN_ROW_TOP } from '../Actions/Pin/ACTION_DATA_GRID_PIN_ROW_TOP.js'; +import { ACTION_DATA_GRID_UNPIN_ALL_COLUMNS } from '../Actions/Pin/ACTION_DATA_GRID_UNPIN_ALL_COLUMNS.js'; +import { ACTION_DATA_GRID_UNPIN_ALL_ROWS } from '../Actions/Pin/ACTION_DATA_GRID_UNPIN_ALL_ROWS.js'; +import { ACTION_DATA_GRID_UNPIN_COLUMN } from '../Actions/Pin/ACTION_DATA_GRID_UNPIN_COLUMN.js'; +import { ACTION_DATA_GRID_UNPIN_ROW } from '../Actions/Pin/ACTION_DATA_GRID_UNPIN_ROW.js'; + +@injectable(() => [ActionService, MenuService]) +export class DataGridContextMenuPinService { + constructor( + private readonly actionService: ActionService, + private readonly menuService: MenuService, + ) {} + + register(): void { + this.menuService.addCreator({ + root: true, + menus: [MENU_DV_CONTEXT_MENU], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], + getItems: (context, items) => [ + ...items, + ACTION_DATA_GRID_PIN_COLUMN, + ACTION_DATA_GRID_UNPIN_COLUMN, + ACTION_DATA_GRID_UNPIN_ALL_COLUMNS, + ACTION_DATA_GRID_PIN_ROW_TOP, + ACTION_DATA_GRID_PIN_ROW_BOTTOM, + ACTION_DATA_GRID_UNPIN_ROW, + ACTION_DATA_GRID_UNPIN_ALL_ROWS, + ], + }); + + this.actionService.addHandler({ + id: 'data-grid-pin-handler', + actions: [ + ACTION_DATA_GRID_PIN_COLUMN, + ACTION_DATA_GRID_UNPIN_COLUMN, + ACTION_DATA_GRID_UNPIN_ALL_COLUMNS, + ACTION_DATA_GRID_PIN_ROW_TOP, + ACTION_DATA_GRID_PIN_ROW_BOTTOM, + ACTION_DATA_GRID_UNPIN_ROW, + ACTION_DATA_GRID_UNPIN_ALL_ROWS, + ], + contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], + isHidden(context, action) { + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY); + const presentationActions = context.has(DATA_CONTEXT_DV_PRESENTATION_ACTIONS) ? context.get(DATA_CONTEXT_DV_PRESENTATION_ACTIONS) : null; + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + + if (!key) { + return false; + } + + if (action === ACTION_DATA_GRID_PIN_COLUMN) { + return presentationActions?.isColumnPinned(key) === true; + } + + if (action === ACTION_DATA_GRID_UNPIN_COLUMN) { + return presentationActions?.isColumnPinned(key) === false; + } + + if (action === ACTION_DATA_GRID_UNPIN_ALL_COLUMNS) { + return !presentationActions?.hasPinnedColumns(); + } + + if (action === ACTION_DATA_GRID_PIN_ROW_TOP || action === ACTION_DATA_GRID_PIN_ROW_BOTTOM || action === ACTION_DATA_GRID_UNPIN_ROW) { + if (key.row === undefined) { + return false; + } + const view = model.source.getAction(resultIndex, IDatabaseDataViewAction, GridViewAction); + + if (action === ACTION_DATA_GRID_PIN_ROW_TOP) { + return view.isRowPinnedTop(key.row); + } + if (action === ACTION_DATA_GRID_PIN_ROW_BOTTOM) { + return view.isRowPinnedBottom(key.row); + } + if (action === ACTION_DATA_GRID_UNPIN_ROW) { + return !view.isRowPinned(key.row); + } + } + + if (action === ACTION_DATA_GRID_UNPIN_ALL_ROWS) { + const view = model.source.getAction(resultIndex, IDatabaseDataViewAction, GridViewAction); + return !view.hasPinnedRows(); + } + + return false; + }, + isActionApplicable(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + if (!isResultSetDataSource(model.source) || model.isDisabled(resultIndex)) { + return false; + } + + const view = model.source.getAction(resultIndex, IDatabaseDataViewAction, GridViewAction); + + if (action === ACTION_DATA_GRID_PIN_COLUMN) { + return key.column !== undefined && !view.isColumnPinned(key.column); + } + + if (action === ACTION_DATA_GRID_UNPIN_COLUMN) { + return key.column !== undefined && view.isColumnPinned(key.column); + } + + if (action === ACTION_DATA_GRID_UNPIN_ALL_COLUMNS) { + return view.hasPinnedColumns(); + } + + if (action === ACTION_DATA_GRID_PIN_ROW_TOP || action === ACTION_DATA_GRID_PIN_ROW_BOTTOM || action === ACTION_DATA_GRID_UNPIN_ROW) { + if (key.row === undefined) { + return false; + } + const isPinned = view.isRowPinned(key.row); + const isPinnedTop = view.isRowPinnedTop(key.row); + const isPinnedBottom = view.isRowPinnedBottom(key.row); + + if (action === ACTION_DATA_GRID_PIN_ROW_TOP) { + return !isPinnedTop; + } + if (action === ACTION_DATA_GRID_PIN_ROW_BOTTOM) { + return !isPinnedBottom; + } + if (action === ACTION_DATA_GRID_UNPIN_ROW) { + return isPinned; + } + } + + if (action === ACTION_DATA_GRID_UNPIN_ALL_ROWS) { + return view.hasPinnedRows(); + } + + return false; + }, + handler(context, action) { + const model = context.get(DATA_CONTEXT_DV_DDM)!; + const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; + const key = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; + + const view = model.source.getAction(resultIndex, IDatabaseDataViewAction, GridViewAction); + + switch (action) { + case ACTION_DATA_GRID_PIN_COLUMN: + if (key.column) { + view.pinColumn(key.column); + } + break; + case ACTION_DATA_GRID_UNPIN_COLUMN: + if (key.column) { + view.unpinColumn(key.column); + } + break; + case ACTION_DATA_GRID_UNPIN_ALL_COLUMNS: + view.unpinAllColumns(); + break; + case ACTION_DATA_GRID_PIN_ROW_TOP: + if (key.row) { + view.pinRowTop(key.row); + } + break; + case ACTION_DATA_GRID_PIN_ROW_BOTTOM: + if (key.row) { + view.pinRowBottom(key.row); + } + break; + case ACTION_DATA_GRID_UNPIN_ROW: + if (key.row) { + view.unpinRow(key.row); + } + break; + case ACTION_DATA_GRID_UNPIN_ALL_ROWS: + view.unpinAllRows(); + break; + } + }, + }); + } +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.css index 9de688c1c93..30382d2be07 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.css +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.css @@ -21,7 +21,7 @@ .data-grid__container { --data-grid-order-button-unordered: #c4c4c4; --data-grid-readonly-status-color: #e28835; - --data-grid-cell-selection-background-color: rgba(150, 150, 150, 0.2); + --data-grid-cell-selection-background-color: rgba(150, 150, 150, 0.4); --data-grid-cell-selection-background-color-focus: rgba(0, 145, 234, 0.2); --data-grid-index-cell-border-color: var(--theme-primary); --data-grid-selected-row-color: var(--theme-secondary) !important; @@ -30,9 +30,25 @@ user-select: none; -webkit-user-select: none; + & .rdg-top-summary-row { + &:nth-last-child(1 of &) { + & .rdg-cell { + border-block-end: solid 3px var(--data-grid-cell-selection-background-color); + } + } + } + + & .rdg-bottom-summary-row { + &:nth-child(1 of &) { + & .rdg-cell { + border-block-start: solid 3px var(--data-grid-cell-selection-background-color); + } + } + } + & .rdg-cell-frozen { &:nth-last-child(1 of &) { - border-inline-end-width: 3px; + border-inline-end: solid 3px var(--data-grid-cell-selection-background-color); } } 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 e0888c6dd70..f57c208f272 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -1,10 +1,11 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 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 { observer } from 'mobx-react-lite'; import { useCallback, useLayoutEffect, useMemo, useRef, type HTMLAttributes } from 'react'; import { reaction } from 'mobx'; @@ -303,6 +304,9 @@ export const DataGridTable = observer(function DataGridT // Track pinnedColumns.size to trigger re-render when columns are pinned/unpinned // This ensures the component re-renders when columns are pinned/unpinned getComputed(() => viewAction.pinnedColumns.size); + // Track pinnedRowsTop and pinnedRowsBottom to trigger re-render when rows are pinned/unpinned + getComputed(() => viewAction.pinnedRowsTop.size); + getComputed(() => viewAction.pinnedRowsBottom.size); function getHeaderPinned(colIdx: number) { if (colIdx === 0) { @@ -318,6 +322,22 @@ export const DataGridTable = observer(function DataGridT return viewAction.isColumnPinned(column.key); } + function getRowPinnedTop(rowIdx: number) { + const row = tableData.rows[rowIdx]; + if (!row) { + return false; + } + return viewAction.isRowPinnedTop(row); + } + + function getRowPinnedBottom(rowIdx: number) { + const row = tableData.rows[rowIdx]; + if (!row) { + return false; + } + return viewAction.isRowPinnedBottom(row); + } + function getHeaderResizable(colIdx: number) { return colIdx !== 0; } @@ -518,6 +538,9 @@ export const DataGridTable = observer(function DataGridT getHeaderWidth={getHeaderWidth} getHeaderPinned={getHeaderPinned} getHeaderResizable={getHeaderResizable} + getRowPinnedTop={getRowPinnedTop} + getRowPinnedBottom={getRowPinnedBottom} + getSummaryRowHeight={() => ROW_HEIGHT} getRowHeight={() => ROW_HEIGHT} getColumnKey={getColumnKey} columnCount={columnsCount} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts index a928e7aae11..2af0f11a62e 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ import { DATA_CONTEXT_DV_ACTIONS, DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, - DATA_CONTEXT_DV_PRESENTATION_ACTIONS, DATA_CONTEXT_DV_RESULT_KEY, DATA_CONTEXT_DV_SIMPLE, DataPresentationService, @@ -27,10 +26,8 @@ import { DataGridContextMenuCellEditingService } from './DataGrid/DataGridContex import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.js'; import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.js'; import { DataGridContextMenuSaveContentService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSaveContentService.js'; +import { DataGridContextMenuPinService } from './DataGrid/DataGridContextMenu/DataGridContextMenuPinService.js'; import { DataGridSettingsService } from './DataGridSettingsService.js'; -import { ACTION_DATA_GRID_PIN_COLUMN } from './DataGrid/Actions/Pin/ACTION_DATA_GRID_PIN_COLUMN.js'; -import { ACTION_DATA_GRID_UNPIN_COLUMN } from './DataGrid/Actions/Pin/ACTION_DATA_GRID_UNPIN_COLUMN.js'; -import { ACTION_DATA_GRID_UNPIN_ALL_COLUMNS } from './DataGrid/Actions/Pin/ACTION_DATA_GRID_UNPIN_ALL_COLUMNS.js'; import { ACTION_DATA_GRID_FILTERS_RESET_OR_SORTING } from './DataGrid/Actions/Filters/ACTION_DATA_GRID_FILTERS_RESET_OR_SORTING.js'; const VALUE_TEXT_PRESENTATION_ID = 'value-text-presentation'; @@ -44,6 +41,7 @@ const SpreadsheetGrid = importLazyComponent(() => import('./SpreadsheetGrid.js') DataGridContextMenuFilterService, DataGridContextMenuCellEditingService, DataGridContextMenuSaveContentService, + DataGridContextMenuPinService, ActionService, MenuService, ExceptionsCatcherService, @@ -56,6 +54,7 @@ export class SpreadsheetBootstrap extends Bootstrap { private readonly dataGridContextMenuFilterService: DataGridContextMenuFilterService, private readonly dataGridContextMenuCellEditingService: DataGridContextMenuCellEditingService, private readonly dataGridContextMenuSaveContentService: DataGridContextMenuSaveContentService, + private readonly dataGridContextMenuPinService: DataGridContextMenuPinService, private readonly actionService: ActionService, private readonly menuService: MenuService, exceptionsCatcherService: ExceptionsCatcherService, @@ -79,32 +78,19 @@ export class SpreadsheetBootstrap extends Bootstrap { this.dataGridContextMenuFilterService.register(); this.dataGridContextMenuCellEditingService.register(); this.dataGridContextMenuSaveContentService.register(); + this.dataGridContextMenuPinService.register(); this.menuService.addCreator({ root: true, menus: [MENU_DV_CONTEXT_MENU], contexts: [DATA_CONTEXT_DV_SIMPLE, DATA_CONTEXT_DV_ACTIONS, DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX], - getItems: (context, items) => [ - ACTION_OPEN, - ...items, - ACTION_DATA_GRID_FILTERS_RESET_OR_SORTING, - ACTION_DATA_GRID_PIN_COLUMN, - ACTION_DATA_GRID_UNPIN_COLUMN, - ACTION_DATA_GRID_UNPIN_ALL_COLUMNS, - ], + getItems: (context, items) => [ACTION_OPEN, ...items, ACTION_DATA_GRID_FILTERS_RESET_OR_SORTING], }); this.actionService.addHandler({ id: 'data-grid-key-base-handler', menus: [MENU_DV_CONTEXT_MENU], - contexts: [ - DATA_CONTEXT_DV_SIMPLE, - DATA_CONTEXT_DV_ACTIONS, - DATA_CONTEXT_DV_DDM, - DATA_CONTEXT_DV_DDM_RESULT_INDEX, - DATA_CONTEXT_DV_PRESENTATION_ACTIONS, - DATA_CONTEXT_DV_RESULT_KEY, - ], + contexts: [DATA_CONTEXT_DV_SIMPLE, DATA_CONTEXT_DV_ACTIONS, DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX, DATA_CONTEXT_DV_RESULT_KEY], getActionInfo: (context, action) => { if (action === ACTION_OPEN) { return { ...action.info, label: 'data_grid_table_open_value_panel', icon: 'value-panel' }; @@ -112,24 +98,6 @@ export class SpreadsheetBootstrap extends Bootstrap { return action.info; }, - isHidden: (context, action) => { - const dataContextResultKey = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; - const presentationActions = context.get(DATA_CONTEXT_DV_PRESENTATION_ACTIONS)!; - - if (action === ACTION_DATA_GRID_PIN_COLUMN && dataContextResultKey) { - return presentationActions.isColumnPinned(dataContextResultKey) === true; - } - - if (action === ACTION_DATA_GRID_UNPIN_COLUMN && dataContextResultKey) { - return presentationActions.isColumnPinned(dataContextResultKey) === false; - } - - if (action === ACTION_DATA_GRID_UNPIN_ALL_COLUMNS) { - return !presentationActions.hasPinnedColumns(); - } - - return false; - }, isActionApplicable: (context, action): boolean => { const model = context.get(DATA_CONTEXT_DV_DDM)!; const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!; @@ -150,13 +118,7 @@ export class SpreadsheetBootstrap extends Bootstrap { return constraints.orderConstraints.length > 0 || constraints.filterConstraints.length > 0; } - return [ - ACTION_OPEN, - ACTION_DATA_GRID_FILTERS_RESET_OR_SORTING, - ACTION_DATA_GRID_PIN_COLUMN, - ACTION_DATA_GRID_UNPIN_COLUMN, - ACTION_DATA_GRID_UNPIN_ALL_COLUMNS, - ].includes(action); + return [ACTION_OPEN, ACTION_DATA_GRID_FILTERS_RESET_OR_SORTING].includes(action); }, handler: async (context, action) => { if (action === ACTION_OPEN) { @@ -177,29 +139,6 @@ export class SpreadsheetBootstrap extends Bootstrap { constraints.deleteData(); }); } - - if (action === ACTION_DATA_GRID_PIN_COLUMN) { - const dataContextResultKey = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; - const presentationActions = context.get(DATA_CONTEXT_DV_PRESENTATION_ACTIONS)!; - - if (dataContextResultKey.column) { - presentationActions.pinColumn(dataContextResultKey); - } - } - - if (action === ACTION_DATA_GRID_UNPIN_COLUMN) { - const dataContextResultKey = context.get(DATA_CONTEXT_DV_RESULT_KEY)!; - const presentationActions = context.get(DATA_CONTEXT_DV_PRESENTATION_ACTIONS)!; - - if (dataContextResultKey.column) { - presentationActions.unpinColumn(dataContextResultKey); - } - } - - if (action === ACTION_DATA_GRID_UNPIN_ALL_COLUMNS) { - const presentationActions = context.get(DATA_CONTEXT_DV_PRESENTATION_ACTIONS)!; - presentationActions.unpinAllColumns(); - } }, }); } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts index 1a10bfb9a47..b88e3502afc 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/de.ts @@ -10,6 +10,10 @@ export default [ ['plugin_data_spreadsheet_new_pin_column', 'Spalte anheften'], ['plugin_data_spreadsheet_new_unpin_column', 'Spalte lösen'], ['plugin_data_spreadsheet_new_unpin_all_columns', 'Alle Spalten lösen'], + ['plugin_data_spreadsheet_new_pin_row_top', 'Zeile oben anheften'], + ['plugin_data_spreadsheet_new_pin_row_bottom', 'Zeile unten anheften'], + ['plugin_data_spreadsheet_new_unpin_row', 'Zeile lösen'], + ['plugin_data_spreadsheet_new_unpin_all_rows', 'Alle Zeilen lösen'], ['data_grid_table_filter_cell_value', 'Zellwert'], ['data_grid_table_filter_reset_all_filters', 'Alle Filter zurücksetzen'], ['data_grid_table_filter_delete_for_column', 'Filter für "{arg:column}" löschen'], diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts index 0e82354bb6c..369351af3ae 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts @@ -11,6 +11,10 @@ export default [ ['plugin_data_spreadsheet_new_pin_column', 'Pin Column'], ['plugin_data_spreadsheet_new_unpin_column', 'Unpin Column'], ['plugin_data_spreadsheet_new_unpin_all_columns', 'Unpin All Columns'], + ['plugin_data_spreadsheet_new_pin_row_top', 'Pin Row to Top'], + ['plugin_data_spreadsheet_new_pin_row_bottom', 'Pin Row to Bottom'], + ['plugin_data_spreadsheet_new_unpin_row', 'Unpin Row'], + ['plugin_data_spreadsheet_new_unpin_all_rows', 'Unpin All Rows'], ['data_grid_table_open_value_panel', 'Show in value panel'], ['data_grid_table_filter', 'Filters'], ['data_grid_table_filter_cell_value', 'Cell value'], diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts index e47ec4f1f1d..dfced2116d1 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/fr.ts @@ -11,6 +11,10 @@ export default [ ['plugin_data_spreadsheet_new_pin_column', 'Épingler la colonne'], ['plugin_data_spreadsheet_new_unpin_column', 'Désépingler la colonne'], ['plugin_data_spreadsheet_new_unpin_all_columns', 'Désépingler toutes les colonnes'], + ['plugin_data_spreadsheet_new_pin_row_top', 'Épingler la ligne en haut'], + ['plugin_data_spreadsheet_new_pin_row_bottom', 'Épingler la ligne en bas'], + ['plugin_data_spreadsheet_new_unpin_row', 'Désépingler la ligne'], + ['plugin_data_spreadsheet_new_unpin_all_rows', 'Désépingler toutes les lignes'], ['data_grid_table_open_value_panel', 'Afficher dans le panneau de valeurs'], ['data_grid_table_filter', 'Filtres'], ['data_grid_table_filter_cell_value', 'Valeur de la cellule'], diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts index bc1d6cb6798..875b16c7b33 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/it.ts @@ -6,6 +6,10 @@ export default [ ['plugin_data_spreadsheet_new_pin_column', 'Blocca colonna'], ['plugin_data_spreadsheet_new_unpin_column', 'Sblocca colonna'], ['plugin_data_spreadsheet_new_unpin_all_columns', 'Sblocca tutte le colonne'], + ['plugin_data_spreadsheet_new_pin_row_top', 'Blocca riga in alto'], + ['plugin_data_spreadsheet_new_pin_row_bottom', 'Blocca riga in basso'], + ['plugin_data_spreadsheet_new_unpin_row', 'Sblocca riga'], + ['plugin_data_spreadsheet_new_unpin_all_rows', 'Sblocca tutte le righe'], ['data_grid_table_open_value_panel', 'Mostra nel pannello dei valori'], ['data_grid_table_filter', 'Filtri'], ['data_grid_table_filter_cell_value', 'Valore della cella'], diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts index bd2601d5182..6bb8644a930 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts @@ -11,6 +11,10 @@ export default [ ['plugin_data_spreadsheet_new_pin_column', 'Закрепить колонку'], ['plugin_data_spreadsheet_new_unpin_column', 'Открепить колонку'], ['plugin_data_spreadsheet_new_unpin_all_columns', 'Открепить все колонки'], + ['plugin_data_spreadsheet_new_pin_row_top', 'Закрепить строку сверху'], + ['plugin_data_spreadsheet_new_pin_row_bottom', 'Закрепить строку снизу'], + ['plugin_data_spreadsheet_new_unpin_row', 'Открепить строку'], + ['plugin_data_spreadsheet_new_unpin_all_rows', 'Открепить все строки'], ['data_grid_table_open_value_panel', 'Показать в панели значений'], ['data_grid_table_filter', 'Фильтры'], ['data_grid_table_filter_cell_value', 'Значение ячейки'], diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts index ec58fe7aba2..77acc73c7b7 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/vi.ts @@ -11,6 +11,10 @@ export default [ ['plugin_data_spreadsheet_new_pin_column', 'Ghim cột'], ['plugin_data_spreadsheet_new_unpin_column', 'Bỏ ghim cột'], ['plugin_data_spreadsheet_new_unpin_all_columns', 'Bỏ ghim tất cả cột'], + ['plugin_data_spreadsheet_new_pin_row_top', 'Ghim hàng lên trên'], + ['plugin_data_spreadsheet_new_pin_row_bottom', 'Ghim hàng xuống dưới'], + ['plugin_data_spreadsheet_new_unpin_row', 'Bỏ ghim hàng'], + ['plugin_data_spreadsheet_new_unpin_all_rows', 'Bỏ ghim tất cả hàng'], ['data_grid_table_open_value_panel', 'Hiển thị trong bảng giá trị'], ['data_grid_table_filter', 'Bộ lọc'], ['data_grid_table_filter_cell_value', 'Giá trị ô'], diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts index 2174c28363b..bd598aa060b 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/zh.ts @@ -11,6 +11,10 @@ export default [ ['plugin_data_spreadsheet_new_pin_column', '固定列'], ['plugin_data_spreadsheet_new_unpin_column', '取消固定列'], ['plugin_data_spreadsheet_new_unpin_all_columns', '取消固定所有列'], + ['plugin_data_spreadsheet_new_pin_row_top', '固定行到顶部'], + ['plugin_data_spreadsheet_new_pin_row_bottom', '固定行到底部'], + ['plugin_data_spreadsheet_new_unpin_row', '取消固定行'], + ['plugin_data_spreadsheet_new_unpin_all_rows', '取消固定所有行'], ['data_grid_table_open_value_panel', '在值面板中查看'], ['data_grid_table_filter', '过滤器'], ['data_grid_table_filter_cell_value', '单元格值'], diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/module.ts b/webapp/packages/plugin-data-spreadsheet-new/src/module.ts index e622cfd5a93..e0973083272 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/module.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/module.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ import { DataGridContextMenuSaveContentService } from './DataGrid/DataGridContex import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.js'; import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.js'; import { DataGridContextMenuCellEditingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.js'; +import { DataGridContextMenuPinService } from './DataGrid/DataGridContextMenu/DataGridContextMenuPinService.js'; export default ModuleRegistry.add({ name: '@cloudbeaver/plugin-data-spreadsheet-new', @@ -27,6 +28,7 @@ export default ModuleRegistry.add({ .addSingleton(DataGridSettingsService) .addSingleton(DataGridContextMenuSaveContentService) .addSingleton(DataGridContextMenuOrderService) - .addSingleton(DataGridContextMenuFilterService); + .addSingleton(DataGridContextMenuFilterService) + .addSingleton(DataGridContextMenuPinService); }, }); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridViewAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridViewAction.ts index 8ce368f91f3..02aeae3a947 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridViewAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/Grid/GridViewAction.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -53,6 +53,8 @@ export class GridViewAction< private columnsOrder: number[]; readonly pinnedColumns: ObservableSet; + readonly pinnedRowsTop: ObservableSet; + readonly pinnedRowsBottom: ObservableSet; protected readonly data: GridDataResultAction; protected readonly editor?: GridEditAction; @@ -67,14 +69,22 @@ export class GridViewAction< this.editor = editor as GridEditAction | undefined; this.columnsOrder = this.data.columns.map((key, index) => index); this.pinnedColumns = observable.set(); + this.pinnedRowsTop = observable.set(); + this.pinnedRowsBottom = observable.set(); - makeObservable(this, { + makeObservable(this, { columnsOrder: observable, pinnedColumns: observable, + pinnedRowsTop: observable, + pinnedRowsBottom: observable, setColumnOrder: action, pinColumn: action, unpinColumn: action, unpinAllColumns: action, + pinRowTop: action, + pinRowBottom: action, + unpinRow: action, + unpinAllRows: action, rows: computed, rowKeys: computed, columns: computed, @@ -201,6 +211,47 @@ export class GridViewAction< return this.pinnedColumns.size > 0; } + pinRowTop(key: IGridRowKey): void { + const serializedKey = GridDataKeysUtils.serialize(key); + this.pinnedRowsTop.add(serializedKey); + this.pinnedRowsBottom.delete(serializedKey); + } + + pinRowBottom(key: IGridRowKey): void { + const serializedKey = GridDataKeysUtils.serialize(key); + this.pinnedRowsBottom.add(serializedKey); + this.pinnedRowsTop.delete(serializedKey); + } + + unpinRow(key: IGridRowKey): void { + const serializedKey = GridDataKeysUtils.serialize(key); + this.pinnedRowsTop.delete(serializedKey); + this.pinnedRowsBottom.delete(serializedKey); + } + + unpinAllRows(): void { + this.pinnedRowsTop.clear(); + this.pinnedRowsBottom.clear(); + } + + isRowPinnedTop(key: IGridRowKey): boolean { + const serializedKey = GridDataKeysUtils.serialize(key); + return this.pinnedRowsTop.has(serializedKey); + } + + isRowPinnedBottom(key: IGridRowKey): boolean { + const serializedKey = GridDataKeysUtils.serialize(key); + return this.pinnedRowsBottom.has(serializedKey); + } + + isRowPinned(key: IGridRowKey): boolean { + return this.isRowPinnedTop(key) || this.isRowPinnedBottom(key); + } + + hasPinnedRows(): boolean { + return this.pinnedRowsTop.size > 0 || this.pinnedRowsBottom.size > 0; + } + protected mapRow(row: IGridRowKey): TCell[] { const edited = this.editor?.getRow(row);