@xh/hoist 75.0.0-SNAPSHOT.1749666833237 → 75.0.0-SNAPSHOT.1750966316712

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,23 +2,32 @@
2
2
 
3
3
  ## v75.0.0-SNAPSHOT - unreleased
4
4
 
5
+ ### 🎁 New Features
6
+
7
+ * Added props to `ViewManager` to customize icons used for different types of views, and modified
8
+ default icons for Global and Shared views.
9
+ * Added `ViewManager.extraMenuItems` prop to allow insertion of custom, app-specific items into the
10
+ component's standard menu.
11
+
5
12
  ## v74.0.0 - 2025-06-11
6
13
 
7
14
  ### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - minor changes to ViewManagerModel, ChartModel)
8
15
 
9
16
  * Removed `ViewManagerModel.settleTime`. Now set via individual `PersistOptions.settleTime` instead.
10
- * ️Removed `ChartModel.showContextMenu`. Use a setting of `false` for the new `ChartModel.contextMenu`
11
- property instead.
17
+ * ️Removed `ChartModel.showContextMenu`. Use a setting of `false` for the new
18
+ `ChartModel.contextMenu` property instead.
12
19
 
13
20
  ### 🎁 New Features
21
+
14
22
  * Added `ViewManagerModel.preserveUnsavedChanges` flag to opt-out of that behaviour.
15
23
  * Added `PersistOptions.settleTime` to configure time to wait for state to settle before persisting.
16
24
  * Support for grid column level `onCellClicked` events.
17
25
  * General improvements to `MenuItem` api
18
- * New `MenuContext` object now sent as 2nd arg to `actionFn` and `prepareFn`.
19
- * New `ChartModel.contextMenu` property provides a fully customizable context menu for charts.
26
+ * New `MenuContext` object now sent as 2nd arg to `actionFn` and `prepareFn`.
27
+ * New `ChartModel.contextMenu` property provides a fully customizable context menu for charts.
20
28
 
21
29
  ### 🐞 Bug Fixes
30
+
22
31
  * Improved `ViewManagerModel.settleTime` by delegating to individual `PersistenceProviders`.
23
32
  * Fixed bug where grid column state could become unintentionally dirty when columns were hidden.
24
33
  * Improved `WebsocketService` heartbeat detection to auto-reconnect when the socket reports as open
@@ -15,15 +15,19 @@ export declare class ViewInfo {
15
15
  readonly description: string;
16
16
  /** User owning this view. Null if the view is global.*/
17
17
  readonly owner: string;
18
- /** Is the owner making this view accessible to others? Always true for global views. */
18
+ /**
19
+ * True if the owner (which can be the current user) has made this view accessible to all other
20
+ * users. Note always `false` for global views, to better distinguish the two sharing models.
21
+ */
19
22
  readonly isShared: boolean;
20
23
  /** True if this view is global and visible to all users. */
21
24
  readonly isGlobal: boolean;
22
25
  /** Optional group name used for bucketing this view in display. */
23
26
  readonly group: string;
24
27
  /**
25
- * Should this view be pinned by users by default?
26
- * This value is intended to be used for global views only.
28
+ * True if this view should be pinned by default to all users' menus, where it will appear
29
+ * unless the user has explicitly unpinned it. Only applicable for global views, can be enabled
30
+ * by view managers to promote especially important global views and ensure users see them.
27
31
  */
28
32
  readonly isDefaultPinned: boolean;
29
33
  /**
@@ -1,26 +1,42 @@
1
- import { HoistProps } from '@xh/hoist/core';
1
+ import { HoistProps, MenuItemLike } from '@xh/hoist/core';
2
2
  import { ViewManagerModel } from '@xh/hoist/cmp/viewmanager';
3
3
  import { ButtonProps } from '@xh/hoist/desktop/cmp/button';
4
+ import { ReactElement } from 'react';
4
5
  import './ViewManager.scss';
5
- /**
6
- * Visibility options for save/revert button.
7
- *
8
- * 'never' to hide button.
9
- * 'whenDirty' to only show when persistence state is dirty and button is therefore enabled.
10
- * 'always' will always show button.
11
- */
12
- export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
13
6
  export interface ViewManagerProps extends HoistProps<ViewManagerModel> {
14
7
  menuButtonProps?: Partial<ButtonProps>;
15
8
  saveButtonProps?: Partial<ButtonProps>;
16
9
  revertButtonProps?: Partial<ButtonProps>;
10
+ /** Button icon when on the default (in-code state) view. Default `Icon.bookmark`. */
11
+ defaultViewIcon?: ReactElement;
12
+ /** Button icon when the selected view is owned by the current user. Default `Icon.bookmark`. */
13
+ ownedViewIcon?: ReactElement;
14
+ /** Button icon when the selected view is shared by another user. Default `Icon.users`. */
15
+ sharedViewIcon?: ReactElement;
16
+ /** Button icon when the selected view is globally shared. Default `Icon.globe`. */
17
+ globalViewIcon?: ReactElement;
17
18
  /** Default 'whenDirty' */
18
19
  showSaveButton?: ViewManagerStateButtonMode;
19
20
  /** Default 'never' */
20
21
  showRevertButton?: ViewManagerStateButtonMode;
21
- /** Side the save and revert buttons should appear on (default 'right') */
22
+ /** Side relative to the menu on which save/revert buttons should render. Default 'right'. */
22
23
  buttonSide?: 'left' | 'right';
24
+ /**
25
+ * Array of extra menu items. Can contain:
26
+ * + `MenuItems` or configs to create them.
27
+ * + `MenuDividers` or the special string token '-'.
28
+ * + React Elements or strings, which will be interpreted as the `text` property for a MenuItem.
29
+ */
30
+ extraMenuItems?: MenuItemLike[];
23
31
  }
32
+ /**
33
+ * Visibility options for save/revert buttons inlined next to the ViewManager menu:
34
+ * 'never' to always hide - user must save/revert via menu.
35
+ * 'whenDirty' (default) to show only when view state is dirty and the button is enabled.
36
+ * 'always' to always show, including when view not dirty and the button is disabled.
37
+ * Useful to avoid jumpiness in toolbar layouts.
38
+ */
39
+ export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
24
40
  /**
25
41
  * Desktop ViewManager component - a button-based menu for saving and swapping between named
26
42
  * bundles of persisted component state (e.g. grid views, dashboards, and similar).
@@ -0,0 +1,13 @@
1
+ import { MenuItemLike } from '@xh/hoist/core';
2
+ import { ReactNode } from 'react';
3
+ /**
4
+ * Parse MenuItem configs into Blueprint MenuItems.
5
+ *
6
+ * Note this is currently used in a few limited places and is not generally applied to all menu-
7
+ * like components in Hoist. In particular, it is not used by the `menu` component re-exported from
8
+ * Blueprint. See https://github.com/xh/hoist-react/issues/2400 covering TBD work to more fully
9
+ * standardize a Hoist menu component that might then incorporate this processing.
10
+ *
11
+ * @internal
12
+ */
13
+ export declare function parseMenuItems(items: MenuItemLike[]): ReactNode[];
@@ -2,3 +2,4 @@ export * from './Separators';
2
2
  export * from './TimeZone';
3
3
  export * from './Equals';
4
4
  export * from './IsOmitted';
5
+ export * from './MenuItems';
@@ -29,7 +29,10 @@ export class ViewInfo {
29
29
  /** User owning this view. Null if the view is global.*/
30
30
  readonly owner: string;
31
31
 
32
- /** Is the owner making this view accessible to others? Always true for global views. */
32
+ /**
33
+ * True if the owner (which can be the current user) has made this view accessible to all other
34
+ * users. Note always `false` for global views, to better distinguish the two sharing models.
35
+ */
33
36
  readonly isShared: boolean;
34
37
 
35
38
  /** True if this view is global and visible to all users. */
@@ -39,8 +42,9 @@ export class ViewInfo {
39
42
  readonly group: string;
40
43
 
41
44
  /**
42
- * Should this view be pinned by users by default?
43
- * This value is intended to be used for global views only.
45
+ * True if this view should be pinned by default to all users' menus, where it will appear
46
+ * unless the user has explicitly unpinned it. Only applicable for global views, can be enabled
47
+ * by view managers to promote especially important global views and ensure users see them.
44
48
  */
45
49
  readonly isDefaultPinned: boolean;
46
50
 
@@ -4,17 +4,13 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {MenuItemProps} from '@blueprintjs/core';
8
- import {hoistCmp, isMenuItem, MenuItemLike, XH} from '@xh/hoist/core';
7
+ import {hoistCmp, MenuItemLike, XH} from '@xh/hoist/core';
9
8
  import {ButtonProps, button} from '@xh/hoist/desktop/cmp/button';
10
9
  import '@xh/hoist/desktop/register';
11
10
  import {Icon} from '@xh/hoist/icon';
12
- import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
13
- import {wait} from '@xh/hoist/promise';
14
- import {filterConsecutiveMenuSeparators, isOmitted} from '@xh/hoist/utils/impl';
11
+ import {menu, popover} from '@xh/hoist/kit/blueprint';
12
+ import {parseMenuItems} from '@xh/hoist/utils/impl';
15
13
  import {withDefault} from '@xh/hoist/utils/js';
16
- import {clone, isEmpty} from 'lodash';
17
- import {ReactNode} from 'react';
18
14
 
19
15
  export interface AppMenuButtonProps extends ButtonProps {
20
16
  /**
@@ -176,42 +172,3 @@ function buildMenuItems(props: AppMenuButtonProps) {
176
172
 
177
173
  return parseMenuItems([...extraItems, '-', ...defaultItems]);
178
174
  }
179
-
180
- function parseMenuItems(items: MenuItemLike[]): ReactNode[] {
181
- items = items.map(item => {
182
- if (!isMenuItem(item)) return item;
183
-
184
- item = clone(item);
185
- item.items = clone(item.items);
186
- item.prepareFn?.(item);
187
- return item;
188
- });
189
-
190
- return items
191
- .filter(it => !isMenuItem(it) || (!it.hidden && !isOmitted(it)))
192
- .filter(filterConsecutiveMenuSeparators())
193
- .map(item => {
194
- if (item === '-') return menuDivider();
195
- if (!isMenuItem(item)) return item;
196
-
197
- const {actionFn} = item;
198
-
199
- // Create menuItem from config
200
- const cfg: MenuItemProps = {
201
- text: item.text,
202
- icon: item.icon,
203
- intent: item.intent,
204
- className: item.className,
205
- onClick: actionFn ? e => wait().then(() => actionFn(e)) : null, // do async to allow menu to close
206
- disabled: item.disabled
207
- };
208
-
209
- // Recursively parse any submenus
210
- if (!isEmpty(item.items)) {
211
- cfg.children = parseMenuItems(item.items);
212
- cfg.popoverProps = {openOnTargetFocus: false};
213
- }
214
-
215
- return menuItem(cfg);
216
- });
217
- }
@@ -7,13 +7,14 @@
7
7
 
8
8
  import {box, fragment, hbox} from '@xh/hoist/cmp/layout';
9
9
  import {spinner} from '@xh/hoist/cmp/spinner';
10
- import {hoistCmp, HoistProps, useLocalModel, uses} from '@xh/hoist/core';
10
+ import {hoistCmp, HoistProps, MenuItemLike, useLocalModel, uses} from '@xh/hoist/core';
11
11
  import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
12
12
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {popover} from '@xh/hoist/kit/blueprint';
15
15
  import {useOnVisibleChange} from '@xh/hoist/utils/react';
16
16
  import {startCase} from 'lodash';
17
+ import {ReactElement} from 'react';
17
18
  import {viewMenu} from './ViewMenu';
18
19
  import {ViewManagerLocalModel} from './ViewManagerLocalModel';
19
20
  import {manageDialog} from './dialog/ManageDialog';
@@ -21,28 +22,44 @@ import {saveAsDialog} from './dialog/SaveAsDialog';
21
22
 
22
23
  import './ViewManager.scss';
23
24
 
24
- /**
25
- * Visibility options for save/revert button.
26
- *
27
- * 'never' to hide button.
28
- * 'whenDirty' to only show when persistence state is dirty and button is therefore enabled.
29
- * 'always' will always show button.
30
- */
31
- export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
32
-
33
25
  export interface ViewManagerProps extends HoistProps<ViewManagerModel> {
34
26
  menuButtonProps?: Partial<ButtonProps>;
35
27
  saveButtonProps?: Partial<ButtonProps>;
36
28
  revertButtonProps?: Partial<ButtonProps>;
37
29
 
30
+ /** Button icon when on the default (in-code state) view. Default `Icon.bookmark`. */
31
+ defaultViewIcon?: ReactElement;
32
+ /** Button icon when the selected view is owned by the current user. Default `Icon.bookmark`. */
33
+ ownedViewIcon?: ReactElement;
34
+ /** Button icon when the selected view is shared by another user. Default `Icon.users`. */
35
+ sharedViewIcon?: ReactElement;
36
+ /** Button icon when the selected view is globally shared. Default `Icon.globe`. */
37
+ globalViewIcon?: ReactElement;
38
+
38
39
  /** Default 'whenDirty' */
39
40
  showSaveButton?: ViewManagerStateButtonMode;
40
41
  /** Default 'never' */
41
42
  showRevertButton?: ViewManagerStateButtonMode;
42
- /** Side the save and revert buttons should appear on (default 'right') */
43
+ /** Side relative to the menu on which save/revert buttons should render. Default 'right'. */
43
44
  buttonSide?: 'left' | 'right';
45
+ /**
46
+ * Array of extra menu items. Can contain:
47
+ * + `MenuItems` or configs to create them.
48
+ * + `MenuDividers` or the special string token '-'.
49
+ * + React Elements or strings, which will be interpreted as the `text` property for a MenuItem.
50
+ */
51
+ extraMenuItems?: MenuItemLike[];
44
52
  }
45
53
 
54
+ /**
55
+ * Visibility options for save/revert buttons inlined next to the ViewManager menu:
56
+ * 'never' to always hide - user must save/revert via menu.
57
+ * 'whenDirty' (default) to show only when view state is dirty and the button is enabled.
58
+ * 'always' to always show, including when view not dirty and the button is disabled.
59
+ * Useful to avoid jumpiness in toolbar layouts.
60
+ */
61
+ export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
62
+
46
63
  /**
47
64
  * Desktop ViewManager component - a button-based menu for saving and swapping between named
48
65
  * bundles of persisted component state (e.g. grid views, dashboards, and similar).
@@ -60,9 +77,14 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
60
77
  menuButtonProps,
61
78
  saveButtonProps,
62
79
  revertButtonProps,
80
+ defaultViewIcon = Icon.bookmark(),
81
+ ownedViewIcon = Icon.bookmark(),
82
+ sharedViewIcon = Icon.users(),
83
+ globalViewIcon = Icon.globe(),
63
84
  showSaveButton = 'whenDirty',
64
85
  showRevertButton = 'never',
65
- buttonSide = 'right'
86
+ buttonSide = 'right',
87
+ extraMenuItems = []
66
88
  }: ViewManagerProps) {
67
89
  const {loadModel} = model,
68
90
  locModel = useLocalModel(() => new ViewManagerLocalModel(model)),
@@ -70,7 +92,17 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
70
92
  revert = revertButton({model: locModel, mode: showRevertButton, ...revertButtonProps}),
71
93
  menu = popover({
72
94
  disabled: !locModel.isVisible, // Prevent orphaned popover menu
73
- item: menuButton({model: locModel, ...menuButtonProps}),
95
+ item: menuButton({
96
+ model: locModel,
97
+ icon: buttonIcon({
98
+ model: locModel,
99
+ defaultViewIcon,
100
+ ownedViewIcon,
101
+ sharedViewIcon,
102
+ globalViewIcon
103
+ }),
104
+ ...menuButtonProps
105
+ }),
74
106
  content: loadModel.isPending
75
107
  ? box({
76
108
  item: spinner({compact: true}),
@@ -79,7 +111,7 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
79
111
  height: 30,
80
112
  width: 30
81
113
  })
82
- : viewMenu({model: locModel}),
114
+ : viewMenu({model: locModel, extraMenuItems}),
83
115
  onOpening: () => model.refreshAsync(),
84
116
  placement: 'bottom',
85
117
  popoverClassName: 'xh-view-manager__popover'
@@ -97,13 +129,13 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
97
129
  });
98
130
 
99
131
  const menuButton = hoistCmp.factory<ViewManagerLocalModel>({
100
- render({model, ...rest}) {
132
+ render({model, icon, ...rest}) {
101
133
  const {view, typeDisplayName, isLoading} = model.parent;
102
134
  return button({
103
135
  className: 'xh-view-manager__menu-button',
104
136
  text: view.isDefault ? `Default ${startCase(typeDisplayName)}` : view.name,
105
137
  icon: !isLoading
106
- ? Icon.bookmark()
138
+ ? icon
107
139
  : box({
108
140
  item: spinner({width: 13, height: 13, style: {margin: 'auto'}}),
109
141
  width: 16.25
@@ -115,6 +147,16 @@ const menuButton = hoistCmp.factory<ViewManagerLocalModel>({
115
147
  }
116
148
  });
117
149
 
150
+ const buttonIcon = hoistCmp.factory<ViewManagerLocalModel>({
151
+ render({model, ownedViewIcon, sharedViewIcon, globalViewIcon, defaultViewIcon}) {
152
+ const {view} = model.parent;
153
+ if (view.isOwned) return ownedViewIcon;
154
+ if (view.isShared) return sharedViewIcon;
155
+ if (view.isGlobal) return globalViewIcon;
156
+ return defaultViewIcon;
157
+ }
158
+ });
159
+
118
160
  const saveButton = hoistCmp.factory<ViewManagerLocalModel>({
119
161
  render({model, mode, ...rest}) {
120
162
  if (hideStateButton(model, mode)) return null;
@@ -11,6 +11,7 @@ import {switchInput} from '@xh/hoist/desktop/cmp/input';
11
11
  import {Icon} from '@xh/hoist/icon';
12
12
  import {menu, menuDivider, menuItem} from '@xh/hoist/kit/blueprint';
13
13
  import {pluralize} from '@xh/hoist/utils/js';
14
+ import {filterConsecutiveMenuSeparators, parseMenuItems} from '@xh/hoist/utils/impl';
14
15
  import {Dictionary} from 'express-serve-static-core';
15
16
  import {each, filter, groupBy, isEmpty, isFunction, orderBy, some, startCase} from 'lodash';
16
17
  import {ReactNode} from 'react';
@@ -20,10 +21,16 @@ import {ViewManagerLocalModel} from './ViewManagerLocalModel';
20
21
  * Default Menu used by ViewManager.
21
22
  */
22
23
  export const viewMenu = hoistCmp.factory<ViewManagerLocalModel>({
23
- render({model}) {
24
+ render({model, extraMenuItems}) {
24
25
  return menu({
25
26
  className: 'xh-view-manager__menu',
26
- items: [...getNavMenuItems(model.parent), menuDivider(), ...getOtherMenuItems(model)]
27
+ items: [
28
+ ...getNavMenuItems(model.parent),
29
+ menuDivider(),
30
+ ...parseMenuItems(extraMenuItems),
31
+ menuDivider(),
32
+ ...getOtherMenuItems(model)
33
+ ].filter(filterConsecutiveMenuSeparators())
27
34
  });
28
35
  }
29
36
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "75.0.0-SNAPSHOT.1749666833237",
3
+ "version": "75.0.0-SNAPSHOT.1750966316712",
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",