@xh/hoist 77.0.0-SNAPSHOT.1761672695220 → 77.0.0-SNAPSHOT.1761763880882

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## 77.0.0-SNAPSHOT - unreleased
4
4
 
5
+ ### 🐞 Bug Fixes
6
+ * Fixes regressions in grid context menu for filtering and copy/paste introduced by agGrid v34.
7
+ * Fixes `getExpandState` in `AgGridModel`
8
+
9
+ * Note: AgGrid no longer supports html markup in context menus. Applications setting the
10
+ `RecordGridAction` `text` or `secondaryText` property to markup should be sure to use React nodes
11
+ instead.
12
+
5
13
  ### 💥 Breaking Changes
6
14
 
7
15
  * The `disableXssProtection` flag supported by `AppSpec` and `FieldSpec` has been removed and
@@ -14,7 +22,7 @@
14
22
  * Apps that were previously opting-out via `disableXssProtection` should simply remove that
15
23
  flag. Apps for which this protection remains important should enable at either the app level
16
24
  or for selected Fields and/or Stores.
17
-
25
+
18
26
  ## 76.2.0 - 2025-10-22
19
27
 
20
28
  ### ⚙️ Technical
@@ -1,6 +1,6 @@
1
1
  import { GridModel } from '@xh/hoist/cmp/grid';
2
- import { GridContextMenuSpec } from '../GridContextMenu';
3
- import type { GetContextMenuItemsParams, MenuItemDef } from '@xh/hoist/kit/ag-grid';
2
+ import { type GetContextMenuItemsParams, type MenuItemDef } from '@xh/hoist/kit/ag-grid';
3
+ import type { GridContextMenuSpec } from '../GridContextMenu';
4
4
  /**
5
5
  * @internal
6
6
  */
@@ -1,12 +1,12 @@
1
- import { ReactElement } from 'react';
2
- import { Intent, PlainObject, TestSupportProps } from '../core';
1
+ import { ReactElement, ReactNode } from 'react';
2
+ import { Intent, TestSupportProps } from '../core';
3
3
  import { StoreRecord } from './StoreRecord';
4
4
  import { Column, GridModel } from '../cmp/grid';
5
5
  export interface RecordActionSpec extends TestSupportProps {
6
6
  /** Label to be displayed. */
7
- text?: string;
7
+ text?: ReactNode;
8
8
  /** Additional label to be displayed, usually in a minimal fashion.*/
9
- secondaryText?: string;
9
+ secondaryText?: ReactNode;
10
10
  /** Icon to be displayed.*/
11
11
  icon?: ReactElement;
12
12
  /** Intent to be used for rendering the action.*/
@@ -72,15 +72,15 @@ export interface ActionFnData {
72
72
  * @see GridContextMenuSpec
73
73
  */
74
74
  export declare class RecordAction {
75
- text: string;
76
- secondaryText: string;
75
+ text: ReactNode;
76
+ secondaryText: ReactNode;
77
77
  icon: ReactElement;
78
78
  intent: Intent;
79
79
  className: string;
80
80
  tooltip: string;
81
81
  actionFn: (data: ActionFnData) => void;
82
- displayFn: (data: ActionFnData) => PlainObject;
83
- items: Array<RecordAction | string>;
82
+ displayFn: (data: ActionFnData) => RecordActionSpec;
83
+ items: RecordActionLike[];
84
84
  disabled: boolean;
85
85
  hidden: boolean;
86
86
  recordsRequired: boolean | number;
@@ -90,17 +90,7 @@ export declare class RecordAction {
90
90
  * Called by UI elements to get the display configuration for rendering the action.
91
91
  * @internal
92
92
  */
93
- getDisplaySpec({ record, selectedRecords, gridModel, column, ...rest }: ActionFnData): {
94
- icon: ReactElement<any, string | import("react").JSXElementConstructor<any>>;
95
- text: string;
96
- secondaryText: string;
97
- intent: Intent;
98
- className: string;
99
- tooltip: string;
100
- items: (string | RecordAction)[];
101
- hidden: boolean;
102
- disabled: boolean;
103
- };
93
+ getDisplaySpec({ record, selectedRecords, gridModel, column, ...rest }: ActionFnData): RecordActionSpec;
104
94
  /**
105
95
  * Called by UI elements to trigger the action.
106
96
  * @internal
@@ -13,8 +13,8 @@ export declare let agGridVersion: any;
13
13
  * implementations.
14
14
  */
15
15
  export type { GridOptions, GridApi, SortDirection, ColDef, ColGroupDef, GetContextMenuItemsParams, GridReadyEvent, IHeaderGroupParams, IHeaderParams, ProcessCellForExportParams, CellClassParams, HeaderClassParams, HeaderValueGetterParams, ICellRendererParams, ITooltipParams, IRowNode, RowClassParams, ValueGetterParams, ValueSetterParams, MenuItemDef, CellPosition, NavigateToNextCellParams, ColumnEvent, ColumnState as AgColumnState, Column as AgColumn, ColumnGroup as AgColumnGroup, AgProvidedColumnGroup, RowDoubleClickedEvent, RowClickedEvent, RowHeightParams, CellClickedEvent, CellContextMenuEvent, CellDoubleClickedEvent, CellEditingStartedEvent, CellEditingStoppedEvent } from 'ag-grid-community';
16
- export type { CustomCellEditorProps } from 'ag-grid-react';
17
- export { useGridCellEditor } from 'ag-grid-react';
16
+ export type { CustomCellEditorProps, CustomMenuItemProps } from 'ag-grid-react';
17
+ export { useGridCellEditor, useGridMenuItem } from 'ag-grid-react';
18
18
  /**
19
19
  * Expose application versions of ag-Grid to Hoist.
20
20
  * Typically called in the Bootstrap.js. of the application.
@@ -18,6 +18,7 @@ import {
18
18
  isEmpty,
19
19
  isEqual,
20
20
  isNil,
21
+ isObject,
21
22
  partition,
22
23
  setWith,
23
24
  startCase
@@ -415,27 +416,23 @@ export class AgGridModel extends HoistModel {
415
416
 
416
417
  const expandState = {};
417
418
  this.agApi.forEachNode(node => {
418
- if (!node.allChildrenCount) return;
419
-
420
- if (node.expanded) {
421
- // Skip if parent is collapsed. Parents are visited before children,
422
- // so should already be in expandState if expanded.
423
- const parent = node.parent;
424
- if (
425
- parent &&
426
- parent.id !== 'ROOT_NODE_ID' &&
427
- !has(expandState, this.getGroupNodePath(parent))
428
- ) {
429
- return;
430
- }
431
-
432
- // Note use of setWith + customizer - required to ensure that nested nodes are
433
- // serialized as objects - see https://github.com/xh/hoist-react/issues/3550.
434
- const path = this.getGroupNodePath(node);
435
- setWith(expandState, path, true, () => ({}));
419
+ if (!node.allChildrenCount || !node.expanded) return;
420
+ // Skip if parent is collapsed. Parents are visited before children,
421
+ // so should already be in expandState if expanded.
422
+ const parent = node.parent;
423
+ if (
424
+ parent &&
425
+ parent.id !== 'ROOT_NODE_ID' &&
426
+ !has(expandState, this.getGroupNodePath(parent))
427
+ ) {
428
+ return;
436
429
  }
437
- });
438
430
 
431
+ const path = this.getGroupNodePath(node);
432
+ // Note use of setWith + customizer - required to ensure that nested nodes are
433
+ // serialized as objects - see https://github.com/xh/hoist-react/issues/3550.
434
+ setWith(expandState, path, true, nsValue => (isObject(nsValue) ? nsValue : {}));
435
+ });
439
436
  return expandState;
440
437
  }
441
438
 
package/cmp/grid/Grid.ts CHANGED
@@ -273,6 +273,7 @@ export class GridLocalModel extends HoistModel {
273
273
  mode: selModel.mode == 'single' ? 'singleRow' : 'multiRow',
274
274
  enableClickSelection: selModel.isEnabled,
275
275
  isRowSelectable: () => selModel.isEnabled,
276
+ copySelectedRows: selModel.isEnabled,
276
277
  checkboxes: false,
277
278
  headerCheckbox: false
278
279
  };
@@ -321,7 +322,7 @@ export class GridLocalModel extends HoistModel {
321
322
  getContextMenuItems = (params: GetContextMenuItemsParams) => {
322
323
  const {model} = this,
323
324
  {contextMenu} = model;
324
- if (!contextMenu || XH.isMobileApp || model.isEditing) return null;
325
+ if (!contextMenu || XH.isMobileApp || model.isEditing) return [];
325
326
 
326
327
  // Manipulate selection if needed.
327
328
  if (model.selModel.isEnabled) {
@@ -336,7 +337,7 @@ export class GridLocalModel extends HoistModel {
336
337
  }
337
338
 
338
339
  const ret = getAgGridMenuItems(params, model, contextMenu);
339
- if (isEmpty(ret)) return null;
340
+ if (isEmpty(ret)) return [];
340
341
 
341
342
  return ret;
342
343
  };
@@ -4,19 +4,22 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {Some, XH} from '@xh/hoist/core';
7
+ import {isEmpty, isFunction, isNil, isString, uniq} from 'lodash';
8
+ import copy from 'clipboard-copy';
9
+ import {hoistCmp, type HoistProps, type Some, XH} from '@xh/hoist/core';
8
10
  import {Column, GridModel} from '@xh/hoist/cmp/grid';
9
- import {RecordAction, Store, StoreRecord} from '@xh/hoist/data';
10
- import {convertIconToHtml, Icon} from '@xh/hoist/icon';
11
+ import {RecordAction, type RecordActionSpec, Store, StoreRecord} from '@xh/hoist/data';
12
+ import {Icon} from '@xh/hoist/icon';
11
13
  import {filterConsecutiveMenuSeparators} from '@xh/hoist/utils/impl';
12
- import copy from 'clipboard-copy';
13
- import {isEmpty, isFunction, isNil, isString, uniq} from 'lodash';
14
- import {isValidElement} from 'react';
15
- import {renderToStaticMarkup} from '@xh/hoist/utils/react';
16
- import {GridContextMenuItemLike, GridContextMenuSpec} from '../GridContextMenu';
17
-
18
- import type {GetContextMenuItemsParams, MenuItemDef} from '@xh/hoist/kit/ag-grid';
19
14
  import {wait} from '@xh/hoist/promise';
15
+ import {div, span} from '@xh/hoist/cmp/layout';
16
+ import {
17
+ useGridMenuItem,
18
+ type GetContextMenuItemsParams,
19
+ type MenuItemDef,
20
+ type CustomMenuItemProps
21
+ } from '@xh/hoist/kit/ag-grid';
22
+ import type {GridContextMenuItemLike, GridContextMenuSpec} from '../GridContextMenu';
20
23
 
21
24
  /**
22
25
  * @internal
@@ -76,22 +79,22 @@ function buildMenuItems(
76
79
  subMenu = buildMenuItems(displaySpec.items, record, gridModel, column, agParams);
77
80
  }
78
81
 
79
- const icon = isValidElement(displaySpec.icon) ? convertIconToHtml(displaySpec.icon) : null;
80
-
81
82
  const cssClasses = ['xh-grid-menu-option'];
82
83
  if (displaySpec.intent)
83
84
  cssClasses.push(`xh-grid-menu-option--intent-${displaySpec.intent}`);
84
85
  if (displaySpec.className) cssClasses.push(displaySpec.className);
85
86
 
86
87
  ret.push({
87
- name: displaySpec.text,
88
- shortcut: displaySpec.secondaryText,
89
- icon,
88
+ menuItem: RecordActionMenuItem,
89
+ menuItemParams: {
90
+ displaySpec
91
+ },
92
+ // Standard MenuActionProps
90
93
  cssClasses,
91
94
  subMenu,
92
95
  tooltip: displaySpec.tooltip,
93
96
  disabled: displaySpec.disabled,
94
- // Avoid specifying action if no handler, allows submenus to remain open if accidentally clicked
97
+ // Don't specify action if no handler, allows submenus to remain open if clicked
95
98
  action: action.actionFn ? () => action.call(actionParams) : undefined
96
99
  });
97
100
  });
@@ -186,14 +189,9 @@ function replaceHoistToken(token: string, gridModel: GridModel): Some<RecordActi
186
189
  column,
187
190
  gridModel
188
191
  })
189
- : (values[0] ?? '[blank]'),
190
- // Grid col renderers will very typically return elements, but we need this to be a string.
191
- // That's the contract for `RecordAction.text`, but even more importantly, we end up piping
192
- // those actions into Ag-Grid context menus, which *only* accept strings / HTML markup
193
- // and *not* ReactElements (as of AG v28.2).
194
- text = isValidElement(elem) ? renderToStaticMarkup(elem) : elem;
195
-
196
- return {text};
192
+ : (values[0] ?? '[blank]');
193
+
194
+ return {text: elem};
197
195
  };
198
196
 
199
197
  return new RecordAction({
@@ -302,3 +300,43 @@ function levelExpandAction(gridModel: GridModel): RecordAction {
302
300
  }
303
301
  });
304
302
  }
303
+
304
+ /**
305
+ * A MenuItem for a Hoist RecordAction.
306
+ *
307
+ * A variant of the standard ag-Grid Context menu. Unlike built-in ag-Grid menu item,
308
+ * provides support for specifying 'text' and 'shortcut' display as react elements.
309
+ *
310
+ * @internal
311
+ */
312
+
313
+ interface RecordActionMenuItemProps extends HoistProps, CustomMenuItemProps {
314
+ displaySpec: RecordActionSpec;
315
+ }
316
+
317
+ const RecordActionMenuItem = hoistCmp<RecordActionMenuItemProps>({
318
+ render({displaySpec, subMenu}: RecordActionMenuItemProps) {
319
+ useGridMenuItem({
320
+ configureDefaults: () => true
321
+ });
322
+
323
+ return div(
324
+ span({className: 'ag-menu-option-part ag-menu-option-icon', item: displaySpec.icon}),
325
+ span({className: 'ag-menu-option-part ag-menu-option-text', item: displaySpec.text}),
326
+ span({
327
+ className: 'ag-menu-option-part ag-menu-option-shortcut',
328
+ item: displaySpec.secondaryText
329
+ }),
330
+ span({
331
+ className: 'ag-menu-option-part ag-menu-option-popup-pointer',
332
+ item: subMenu
333
+ ? span({
334
+ className: 'ag-icon ag-icon-small-right',
335
+ unselectable: 'on',
336
+ role: 'presentation'
337
+ })
338
+ : ''
339
+ })
340
+ );
341
+ }
342
+ });
@@ -6,17 +6,17 @@
6
6
  */
7
7
 
8
8
  import {isBoolean, isEmpty, isNil, isNumber, isString} from 'lodash';
9
- import {ReactElement} from 'react';
10
- import {Intent, PlainObject, TestSupportProps} from '../core';
9
+ import {ReactElement, ReactNode} from 'react';
10
+ import {Intent, TestSupportProps} from '../core';
11
11
  import {StoreRecord} from './StoreRecord';
12
12
  import {Column, GridModel} from '../cmp/grid';
13
13
 
14
14
  export interface RecordActionSpec extends TestSupportProps {
15
15
  /** Label to be displayed. */
16
- text?: string;
16
+ text?: ReactNode;
17
17
 
18
18
  /** Additional label to be displayed, usually in a minimal fashion.*/
19
- secondaryText?: string;
19
+ secondaryText?: ReactNode;
20
20
 
21
21
  /** Icon to be displayed.*/
22
22
  icon?: ReactElement;
@@ -100,15 +100,15 @@ export interface ActionFnData {
100
100
  * @see GridContextMenuSpec
101
101
  */
102
102
  export class RecordAction {
103
- text: string;
104
- secondaryText: string;
103
+ text: ReactNode;
104
+ secondaryText: ReactNode;
105
105
  icon: ReactElement;
106
106
  intent: Intent;
107
107
  className: string;
108
108
  tooltip: string;
109
109
  actionFn: (data: ActionFnData) => void;
110
- displayFn: (data: ActionFnData) => PlainObject;
111
- items: Array<RecordAction | string>;
110
+ displayFn: (data: ActionFnData) => RecordActionSpec;
111
+ items: RecordActionLike[];
112
112
  disabled: boolean;
113
113
  hidden: boolean;
114
114
  recordsRequired: boolean | number;
@@ -152,11 +152,17 @@ export class RecordAction {
152
152
  * Called by UI elements to get the display configuration for rendering the action.
153
153
  * @internal
154
154
  */
155
- getDisplaySpec({record, selectedRecords, gridModel, column, ...rest}: ActionFnData) {
155
+ getDisplaySpec({
156
+ record,
157
+ selectedRecords,
158
+ gridModel,
159
+ column,
160
+ ...rest
161
+ }: ActionFnData): RecordActionSpec {
156
162
  const recordCount =
157
163
  record && isEmpty(selectedRecords) ? 1 : selectedRecords ? selectedRecords.length : 0;
158
164
 
159
- const defaultDisplay = {
165
+ const defaultDisplay: RecordActionSpec = {
160
166
  icon: this.icon,
161
167
  text: this.text,
162
168
  secondaryText: this.secondaryText,
@@ -60,8 +60,8 @@ export type {
60
60
  CellEditingStoppedEvent
61
61
  } from 'ag-grid-community';
62
62
 
63
- export type {CustomCellEditorProps} from 'ag-grid-react';
64
- export {useGridCellEditor} from 'ag-grid-react';
63
+ export type {CustomCellEditorProps, CustomMenuItemProps} from 'ag-grid-react';
64
+ export {useGridCellEditor, useGridMenuItem} from 'ag-grid-react';
65
65
 
66
66
  const MIN_VERSION = '34.2.0';
67
67
  const MAX_VERSION = '34.*.*';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "77.0.0-SNAPSHOT.1761672695220",
3
+ "version": "77.0.0-SNAPSHOT.1761763880882",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",