Skip to content

Commit

Permalink
Add support for dynamic context menu items
Browse files Browse the repository at this point in the history
  • Loading branch information
mattrunyon committed Jun 24, 2024
1 parent 7e24fce commit 8bc2799
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 32 deletions.
26 changes: 25 additions & 1 deletion plugins/ui/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1291,14 +1291,16 @@ te = ui.table(

### ui.table Context Menu

Items can be added to the bottom of the `ui.table` context menu (right-click menu) by using the `context_menu` or `context_header_menu` props. The `context_menu` prop adds items for each cell, while the `context_header_menu` prop adds items for the header cells.
Items can be added to the bottom of the `ui.table` context menu (right-click menu) by using the `context_menu` or `context_header_menu` props. The `context_menu` prop adds items to the cell context menu, while the `context_header_menu` prop adds items to the column header context menu.

Menu items must have a `title` and either an `action` or `actions` prop. They may have an `icon` which is the name of the icon that will be passed to `ui.icon`.

The `action` prop is a callback that is called when the item is clicked and receives info about the cell that was clicked when the menu was opened.

The `actions` prop is an array of menu items that will be displayed in a sub-menu. Sub-menus can contain other sub-menus for a nested menu.

Menu items can be dynamically created by instead passing a function as the context item. The function will be called with the data of the cell that was clicked when the menu was opened, and must return the menu items or None.

```py
from deephaven import ui
import deephaven.plot.express as dx
Expand Down Expand Up @@ -1335,6 +1337,28 @@ t = ui.table(
)
```

The following example shows creating context menu items dynamically so that the item only appears on the `sym` column. Note the function is still passed as part of a list. If multiple functions are passed, each will be called and any items they return will be added to the context menu.

```py
from deephaven import ui
import deephaven.plot.express as dx

def create_context_menu(data):
if data["column_name"] == "sym":
return [
{
"title": f"Print {data['value']}",
"action": lambda d: print(d['value'])
},
]
return None

t = ui.table(
dx.data.stocks(),
context_menu=[create_context_menu]
)
```

## Re-using components

In a previous example, we created a text_filter_table component. We can re-use that component, and display two tables with an input filter side-by-side:
Expand Down
14 changes: 9 additions & 5 deletions plugins/ui/src/deephaven/ui/components/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
ColumnPressCallback,
QuickFilterExpression,
RowPressCallback,
ContextMenuItem,
ResolvableContextMenuItem,
)


Expand All @@ -24,8 +24,8 @@ def table(
quick_filters: dict[ColumnName, QuickFilterExpression] | None = None,
show_quick_filters: bool = False,
show_search: bool = False,
context_menu: list[ContextMenuItem] | None = None,
context_header_menu: list[ContextMenuItem] | None = None,
context_menu: list[ResolvableContextMenuItem] | None = None,
context_header_menu: list[ResolvableContextMenuItem] | None = None,
) -> UITable:
"""
Customization to how a table is displayed, how it behaves, and listen to UI events.
Expand All @@ -51,8 +51,12 @@ def table(
quick_filters: The quick filters to apply to the table. Dictionary of column name to filter value.
show_quick_filters: Whether to show the quick filter bar by default.
show_search: Whether to show the search bar by default.
context_menu: The context menu items to show when a cell is right clicked. May contain action items or submenu items.
context_header_menu: The context menu items to show when a column header is right clicked. May contain action items or submenu items.
context_menu: The context menu items to show when a cell is right clicked.
May contain action items or submenu items or None.
May also be a function that receives the cell data and returns the context menu items.
context_header_menu: The context menu items to show when a column header is right clicked.
May contain action items or submenu items.
May also be a function that receives the column header data and returns the context menu items or None.
"""
props = locals()
del props["table"]
Expand Down
11 changes: 10 additions & 1 deletion plugins/ui/src/deephaven/ui/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class ContextMenuSubmenuItem(ContextMenuItemBase):
An item that contains a submenu for a context menu.
"""

actions: List[Union[ContextMenuActionItem, "ContextMenuSubmenuItem"]]
actions: List["ResolvableContextMenuItem"]
"""
A list of actions that will form the submenu for the item.
"""
Expand All @@ -148,6 +148,15 @@ class ContextMenuSubmenuItem(ContextMenuItemBase):
May contain an action item or a submenu item.
"""

ResolvableContextMenuItem = Union[
ContextMenuItem,
Callable[[ContextMenuActionParams], Union[List[ContextMenuItem], None]],
]
"""
A context menu item or a function that returns a list of context menu items or None.
This can be used to dynamically generate context menu items based on the cell the menu is opened on.
"""


class SliderChange(TypedDict):
"""
Expand Down
2 changes: 1 addition & 1 deletion plugins/ui/src/js/src/elements/UITable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { getSettings, RootState } from '@deephaven/redux';
import { GridMouseHandler } from '@deephaven/grid';
import { UITableProps, wrapContextActions } from './utils/UITableUtils';
import UITableMouseHandler from './utils/UITableMouseHandler';
import UITableContextMenuHandler from './UITableContextMenuHandler';
import UITableContextMenuHandler from './utils/UITableContextMenuHandler';

const log = Log.module('@deephaven/js-plugin-ui/UITable');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ class UITableContextMenuHandler extends IrisGridContextMenuHandler {

private contextMenuItems: UITableProps['contextMenu'];

private contextColumnHeaderItems: UITableProps['contextColumnMenu'];
private contextColumnHeaderItems: UITableProps['contextHeaderMenu'];

constructor(
dh: typeof DhType,
irisGrid: IrisGridType,
model: IrisGridModel,
contextMenuItems: UITableProps['contextMenu'],
contextColumnHeaderItems: UITableProps['contextColumnMenu']
contextColumnHeaderItems: UITableProps['contextHeaderMenu']
) {
super(irisGrid, dh);
this.irisGrid = irisGrid;
Expand Down
84 changes: 62 additions & 22 deletions plugins/ui/src/js/src/elements/utils/UITableUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import type {
IrisGridContextMenuData,
RowIndex,
} from '@deephaven/iris-grid';
import type { ContextAction } from '@deephaven/components';
import type {
ContextAction,
ResolvableContextAction,
} from '@deephaven/components';
import { ELEMENT_KEY, ElementNode, isElementNode } from './ElementUtils';
import { getIcon } from './IconElementUtils';
import {
Expand All @@ -30,17 +33,24 @@ export type ColumnIndex = number;

export type RowDataMap = Record<ColumnName, RowDataValue>;

export interface UIContextItemParams {
value: unknown;
text_value: string | null;
column_name: string;
is_column_header: boolean;
is_row_header: boolean;
}

export type UIContextItem = Omit<ContextAction, 'action' | 'actions'> & {
action?: (params: {
value: unknown;
text_value: string | null;
column_name: string;
is_column_header: boolean;
is_row_header: boolean;
}) => void;
action?: (params: UIContextItemParams) => void;

actions?: UIContextItem[];
actions?: ResolvableUIContextItem[];
};

type ResolvableUIContextItem =
| UIContextItem
| ((params: UIContextItemParams) => Promise<UIContextItem[] | null>);

export interface UITableProps {
table: dh.WidgetExportedObject;
onCellPress?: (cellIndex: [ColumnIndex, RowIndex], data: CellData) => void;
Expand All @@ -57,8 +67,8 @@ export interface UITableProps {
sorts?: DehydratedSort[];
showSearch: boolean;
showQuickFilters: boolean;
contextMenu?: UIContextItem[];
contextHeaderMenu?: UIContextItem[];
contextMenu?: ResolvableUIContextItem[];
contextHeaderMenu?: ResolvableUIContextItem[];
[key: string]: unknown;
}

Expand All @@ -73,17 +83,11 @@ export function isUITable(obj: unknown): obj is UITableNode {
);
}

/**
* Wraps context item actions from the server so they are called with the cell info.
* @param items The context items from the server
* @param data The context menu data to use for the context items
* @returns Context items with the UI actions wrapped so they receive the cell info
*/
export function wrapContextActions(
items: UIContextItem[],
function wrapUIContextItem(
item: UIContextItem,
data: Omit<IrisGridContextMenuData, 'model' | 'modelRow' | 'modelColumn'>
): ContextAction[] {
return items.map(item => ({
): ContextAction {
return {
group: 999999, // Default to the end of the menu
...item,
icon: item.icon
Expand All @@ -101,5 +105,41 @@ export function wrapContextActions(
}
: undefined,
actions: item.actions ? wrapContextActions(item.actions, data) : undefined,
}));
} satisfies ContextAction;
}

function wrapUIContextItems(
items: UIContextItem[],
data: Omit<IrisGridContextMenuData, 'model' | 'modelRow' | 'modelColumn'>
): ContextAction[] {
return items.map(item => wrapUIContextItem(item, data));
}

/**
* Wraps context item actions from the server so they are called with the cell info.
* @param items The context items from the server
* @param data The context menu data to use for the context items
* @returns Context items with the UI actions wrapped so they receive the cell info
*/
export function wrapContextActions(
items: ResolvableUIContextItem[],
data: Omit<IrisGridContextMenuData, 'model' | 'modelRow' | 'modelColumn'>
): ResolvableContextAction[] {
return items.map(item => {
if (typeof item === 'function') {
return async () =>
wrapUIContextItems(
(await item({
value: data.value,
text_value: data.valueText,
column_name: data.column.name,
is_column_header: data.rowIndex == null,
is_row_header: data.columnIndex == null,
})) ?? [],
data
);
}

return wrapUIContextItem(item, data);
});
}

0 comments on commit 8bc2799

Please sign in to comment.