Skip to content

[CB] Render data viewer context menu on the root level#4363

Open
sergeyteleshev wants to merge 21 commits into
develfrom
9308-cb-render-data-viewer-context-menu-on-the-root-level
Open

[CB] Render data viewer context menu on the root level#4363
sergeyteleshev wants to merge 21 commits into
develfrom
9308-cb-render-data-viewer-context-menu-on-the-root-level

Conversation

@sergeyteleshev
Copy link
Copy Markdown
Contributor

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 26, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics -2 complexity

Metric Results
Complexity -2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Comment thread webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx Outdated
Comment thread webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves the data viewer context menu rendering to the DataGrid root level so the menu can be positioned globally (rather than per-cell) and re-open reliably on a single right-click (fixing issue #9308).

Changes:

  • Introduces a grid-level TableMenuContext + DataGridMenuContextProvider to manage active cell + menu coordinates.
  • Replaces the per-cell embedded context menu with a lightweight trigger button and a single root-level CellMenu.
  • Adds a new useViewLeave hook to close the menu when the originating cell leaves the viewport, and adds a right-click mousedown propagation blocker to avoid premature dismiss.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Menu/DataGridMenuContextProvider.tsx Adds grid-level provider to centralize context-menu state and rendering.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Menu/CellMenu.tsx Implements a single root-level context menu bound to the active cell via context.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Menu/CellMenu.module.css Hides the menu anchor element since menu is positioned via coordinates.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx Replaces embedded menu with a trigger button that opens the root-level menu.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.module.css Styles the new menu trigger button.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx Wraps the grid in the new menu provider and blocks right-click dismiss behavior.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/TableMenuContext.ts Introduces shared menu state contract for cell/menu integration.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx Routes menu open/close through the shared context and closes on viewport leave.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellContext.ts Extends setMenuVisibility to accept an optional position.
webapp/packages/core-blocks/src/useViewLeave.ts Adds a new hook to detect when an element leaves the viewport.
webapp/packages/core-blocks/src/index.ts Re-exports useViewLeave.
webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx Removes the old per-cell menu implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread webapp/packages/core-blocks/src/useViewLeave.ts Outdated
menuPosition,
openMenu(cellKey: IGridDataKey, x: number, y: number) {
this.activeCellKey = cellKey;
this.menuPosition.position = { x, y };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use menuPosition.open(event)

editionState: DatabaseEditChangeType | null;
isMenuVisible: boolean;
setMenuVisibility(visible: boolean): void;
setMenuVisibility(visible: boolean, position?: { x: number; y: number }): void;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This api should be reworked. There should be openMenu handler that takes event and cell id. This handler should call useContextMenuPosition. Because position is only needed when you open menu and you dont need it when close it

cellContext.setMenuVisibility(visible);
function handleMenuTriggerClick(event: React.MouseEvent<HTMLButtonElement>) {
const rect = event.currentTarget.getBoundingClientRect();
cellContext.setMenuVisibility(true, { x: rect.right - 20, y: rect.bottom });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cell formatter should not make any calculactions with coordinates. openMenu(event) should

focusable={!cellContext.isMenuVisible}
className={s(style, { formatter: true })}
value={cellHolder.value as boolean | null}
focusable
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still should have the way to check whether menu is opened, please keep old behaviour

onStateSwitch={handleCellMenuStateSwitch}
/>
</div>
<button
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please never user native buttons, use our components from ariakit


const { activeCellKey, menuPosition } = tableMenuContext;

useEffect(() => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic should not be inside cellMenu, it should be on MenuContextProvider level


function syncFocus(data: DatabaseDataSelectActionsData<Partial<IGridDataKey>>) {
if (data.type === 'focus') {
const colIdx = data.key?.column ? tableData.getColumnIndexFromColumnKey(data.key.column) : -1;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not modify this method

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you sure? we are going to loose this behavior that we can re-open on right click the new cell (it was before so this is gonna be a regression). the thing is this focus sync steals the focus which causes the bug

Screen.Recording.2026-05-27.at.14.59.13.mov

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this focus is weird. i dont like its defered behavior with setTimeout
something went wrong when we have added it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdym by something went wrong? Is something works incorrectly? As I remember we always had some async logic but in the different place

import type { IGridDataKey } from '@cloudbeaver/plugin-data-viewer';

export interface ITableMenuContext {
activeCellKey: IGridDataKey | null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need activeCellKey? we already have cell and position inside cellContext, we should use them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we moved menu outside of cell context so we need to track active cell id to pass it here:

useDataContextLink(menu.context, (context, id) => {
context.set(DATA_CONTEXT_DV_DDM, dataGridContext.model, id);
context.set(DATA_CONTEXT_DV_DDM_RESULT_INDEX, dataGridContext.resultIndex, id);
context.set(DATA_CONTEXT_DV_SIMPLE, dataGridContext.simple, id);
context.set(DATA_CONTEXT_DV_ACTIONS, dataGridContext.actions, id);
context.set(DATA_CONTEXT_DV_PRESENTATION_ACTIONS, spreadsheetActions, id);
context.set(DATA_CONTEXT_DV_RESULT_KEY, tableMenuContext.activeCell, id);
});

import type { IContextMenuPosition } from '@cloudbeaver/core-blocks';
import type { IGridDataKey } from '@cloudbeaver/plugin-data-viewer';

export interface ITableMenuContext {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably dont need this. You have all 3 fields returned from useContextMenuPosition hook already that lets you open and close menu.

Copy link
Copy Markdown
Contributor Author

@sergeyteleshev sergeyteleshev May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to track active cell key and is opened status

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.


this.isMenuVisible = visibility;
openMenu(event: React.MouseEvent): void {
this.tableMenuContext.openMenu(this.cell, event);
Comment on lines +81 to +85
context.set(DATA_CONTEXT_DV_SIMPLE, dataGridContext.simple, id);
context.set(DATA_CONTEXT_DV_ACTIONS, dataGridContext.actions, id);
context.set(DATA_CONTEXT_DV_PRESENTATION_ACTIONS, spreadsheetActions, id);
context.set(DATA_CONTEXT_DV_RESULT_KEY, tableMenuContext.activeCell, id);
});
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Menu/CellMenu.tsx Outdated
clientY: event.currentTarget.getBoundingClientRect().top + event.currentTarget.getBoundingClientRect().height,
});

this.cellContext.openMenu(event as unknown as React.MouseEvent);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a bit strange that we have event: React.KeyboardEvent as parameter of keyDown, but then we cast it to React.MouseEvent


function handleCellMenuStateSwitch(visible: boolean): void {
cellContext.setMenuVisibility(visible);
function handleMenuTriggerClick(event: React.MouseEvent<HTMLButtonElement>) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk, I expect button is opening when I focus it and press Enter. It's a web standard, I don't think we need to change it.


function syncFocus(data: DatabaseDataSelectActionsData<Partial<IGridDataKey>>) {
if (data.type === 'focus') {
const colIdx = data.key?.column ? tableData.getColumnIndexFromColumnKey(data.key.column) : -1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdym by something went wrong? Is something works incorrectly? As I remember we always had some async logic but in the different place

Comment on lines +53 to +66
useEffect(() => {
const container = dataGridContext.getContainer();

if (!container) {
return;
}

function handleScroll() {
tableMenuState.closeMenu();
}

container.addEventListener('scroll', handleScroll, { capture: true });
return () => container.removeEventListener('scroll', handleScroll, { capture: true });
}, [dataGridContext, tableMenuState]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this? Usually I see that a many stays on it's place no matter if it's on the screen or not, or we could do smth like GitHub does:

Screen.Recording.2026-05-28.at.10.54.57.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants