@xh/hoist 74.0.0 → 74.1.1

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +36 -4
  2. package/build/types/cmp/grouping/GroupingChooserModel.d.ts +8 -4
  3. package/build/types/cmp/viewmanager/ViewInfo.d.ts +7 -3
  4. package/build/types/core/types/Interfaces.d.ts +1 -1
  5. package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +17 -6
  6. package/build/types/desktop/cmp/panel/Panel.d.ts +1 -1
  7. package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +26 -10
  8. package/build/types/mobile/cmp/grouping/GroupingChooser.d.ts +2 -6
  9. package/build/types/utils/impl/MenuItems.d.ts +13 -0
  10. package/build/types/utils/impl/index.d.ts +1 -0
  11. package/cmp/grouping/GroupingChooserModel.ts +25 -12
  12. package/cmp/viewmanager/ViewInfo.ts +7 -3
  13. package/core/HoistAppModel.ts +1 -0
  14. package/core/exception/ExceptionHandler.ts +1 -1
  15. package/core/types/Interfaces.ts +2 -2
  16. package/desktop/cmp/button/AppMenuButton.ts +3 -46
  17. package/desktop/cmp/grouping/GroupingChooser.scss +39 -35
  18. package/desktop/cmp/grouping/GroupingChooser.ts +157 -89
  19. package/desktop/cmp/panel/Panel.ts +1 -1
  20. package/desktop/cmp/viewmanager/ViewManager.ts +58 -16
  21. package/desktop/cmp/viewmanager/ViewMenu.ts +9 -2
  22. package/mobile/appcontainer/FeedbackDialog.ts +4 -0
  23. package/mobile/appcontainer/OptionsDialog.ts +3 -1
  24. package/mobile/cmp/grid/impl/ColChooser.ts +3 -2
  25. package/mobile/cmp/grouping/GroupingChooser.scss +41 -20
  26. package/mobile/cmp/grouping/GroupingChooser.ts +60 -89
  27. package/mobile/cmp/panel/DialogPanel.scss +5 -0
  28. package/package.json +1 -1
  29. package/svc/TrackService.ts +4 -3
  30. package/tsconfig.tsbuildinfo +1 -1
  31. package/utils/impl/MenuItems.ts +57 -0
  32. package/utils/impl/index.ts +1 -0
  33. package/utils/js/LangUtils.ts +1 -1
@@ -5,43 +5,66 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
8
- import {box, div, filler, fragment, hbox, vbox} from '@xh/hoist/cmp/layout';
9
- import {hoistCmp, uses} from '@xh/hoist/core';
8
+ import {
9
+ box,
10
+ div,
11
+ filler,
12
+ fragment,
13
+ hbox,
14
+ hframe,
15
+ placeholder,
16
+ vbox,
17
+ vframe
18
+ } from '@xh/hoist/cmp/layout';
19
+ import {hoistCmp, Side, uses} from '@xh/hoist/core';
10
20
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
11
21
  import {select} from '@xh/hoist/desktop/cmp/input';
12
22
  import {panel} from '@xh/hoist/desktop/cmp/panel';
13
23
  import '@xh/hoist/desktop/register';
24
+ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
14
25
  import {Icon} from '@xh/hoist/icon';
15
- import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
26
+ import {menu, menuItem, popover} from '@xh/hoist/kit/blueprint';
16
27
  import {dragDropContext, draggable, droppable} from '@xh/hoist/kit/react-beautiful-dnd';
17
- import {elemWithin, getTestId, TEST_ID} from '@xh/hoist/utils/js';
28
+ import {apiDeprecated, elemWithin, getTestId} from '@xh/hoist/utils/js';
18
29
  import {splitLayoutProps} from '@xh/hoist/utils/react';
19
- import {ReactElement} from 'react';
20
30
  import classNames from 'classnames';
21
- import {compact, isEmpty, sortBy} from 'lodash';
31
+ import {compact, isEmpty, isNil, isUndefined, sortBy} from 'lodash';
22
32
  import './GroupingChooser.scss';
33
+ import {ReactNode} from 'react';
23
34
 
24
35
  export interface GroupingChooserProps extends ButtonProps<GroupingChooserModel> {
36
+ /** Title for value-editing portion of popover, or null to suppress. */
37
+ editorTitle?: ReactNode;
38
+
25
39
  /** Text to represent empty state (i.e. value = null or []) */
26
40
  emptyText?: string;
27
41
 
42
+ /**
43
+ * Side of the popover, relative to the value-editing controls, on which the Favorites list
44
+ * should be rendered, if enabled.
45
+ */
46
+ favoritesSide?: Side;
47
+
48
+ /** Title for favorites-list portion of popover, or null to suppress. */
49
+ favoritesTitle?: ReactNode;
50
+
28
51
  /** Min height in pixels of the popover menu itself. */
29
52
  popoverMinHeight?: number;
30
53
 
31
54
  /** Position of popover relative to target button. */
32
55
  popoverPosition?: 'bottom' | 'top';
33
56
 
34
- /** Title for popover (default "GROUP BY") or null to suppress. */
35
- popoverTitle?: string;
57
+ /** @deprecated - use `editorTitle` instead */
58
+ popoverTitle?: ReactNode;
36
59
 
37
- /** Width in pixels of the popover menu itself. */
60
+ /**
61
+ * Width in pixels of the popover menu itself.
62
+ * If unspecified, will default based on favorites enabled status + side.
63
+ */
38
64
  popoverWidth?: number;
39
65
 
40
66
  /** True (default) to style target button as an input field - blends better in toolbars. */
41
67
  styleButtonAsInput?: boolean;
42
-
43
- /** Icon clicked to launch favorites menu. Defaults to Icon.favorite() */
44
- favoritesIcon?: ReactElement;
45
68
  }
46
69
 
47
70
  /**
@@ -59,25 +82,35 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
59
82
  {
60
83
  model,
61
84
  className,
85
+ editorTitle = 'Group By',
62
86
  emptyText = 'Ungrouped',
63
- popoverWidth = 250,
87
+ favoritesSide = 'right',
88
+ favoritesTitle = 'Favorites',
89
+ popoverWidth,
64
90
  popoverMinHeight,
65
- popoverTitle = 'Group By',
91
+ popoverTitle,
66
92
  popoverPosition = 'bottom',
67
93
  styleButtonAsInput = true,
68
94
  testId,
69
- favoritesIcon,
70
95
  ...rest
71
96
  },
72
97
  ref
73
98
  ) {
74
- const {editorIsOpen, favoritesIsOpen, persistFavorites, value, allowEmpty} = model,
75
- isOpen = editorIsOpen || favoritesIsOpen,
99
+ const {editorIsOpen, value, allowEmpty, persistFavorites} = model,
100
+ isOpen = editorIsOpen,
76
101
  label = isEmpty(value) && allowEmpty ? emptyText : model.getValueLabel(value),
77
102
  [layoutProps, buttonProps] = splitLayoutProps(rest),
78
- favoritesMenuTestId = getTestId(testId, 'favorites-menu'),
79
- favoritesIconTestId = getTestId(testId, 'favorites-icon'),
80
- editorTestId = getTestId(testId, 'editor');
103
+ favesClassNameMod = `faves-${persistFavorites ? favoritesSide : 'disabled'}`,
104
+ favesTB = isTB(favoritesSide);
105
+
106
+ if (!isUndefined(popoverTitle)) {
107
+ apiDeprecated('GroupingChooser.popoverTitle', {
108
+ msg: `Update to use 'editorTitle' instead`
109
+ });
110
+ editorTitle = popoverTitle;
111
+ }
112
+
113
+ popoverWidth = popoverWidth || (persistFavorites && !favesTB ? 500 : 250);
81
114
 
82
115
  return box({
83
116
  ref,
@@ -86,11 +119,10 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
86
119
  item: popover({
87
120
  isOpen,
88
121
  popoverRef: model.popoverRef,
89
- popoverClassName: 'xh-grouping-chooser-popover xh-popup--framed',
90
- // right align favorites popover to match star icon
91
- // left align editor to keep in place when button changing size when commitOnChange: true
92
- position: favoritesIsOpen ? `${popoverPosition}-right` : `${popoverPosition}-left`,
93
- minimal: styleButtonAsInput,
122
+ popoverClassName: `xh-grouping-chooser-popover xh-grouping-chooser-popover--${favesClassNameMod} xh-popup--framed`,
123
+ // Left align editor to keep in place when button changing size when commitOnChange: true
124
+ position: `${popoverPosition}-left`,
125
+ minimal: false,
94
126
  item: fragment(
95
127
  button({
96
128
  text: label,
@@ -98,34 +130,29 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
98
130
  tabIndex: -1,
99
131
  className: classNames(
100
132
  'xh-grouping-chooser-button',
101
- styleButtonAsInput ? 'xh-grouping-chooser-button--as-input' : null,
102
- persistFavorites ? 'xh-grouping-chooser-button--with-favorites' : null
133
+ styleButtonAsInput ? 'xh-grouping-chooser-button--as-input' : null
103
134
  ),
104
135
  minimal: styleButtonAsInput,
105
136
  ...buttonProps,
106
137
  onClick: () => model.toggleEditor(),
107
138
  testId
108
- }),
109
- favoritesIconCmp({testId: favoritesIconTestId, favoritesIcon})
139
+ })
110
140
  ),
111
- content: favoritesIsOpen
112
- ? favoritesMenu({testId: favoritesMenuTestId})
113
- : editor({
114
- popoverWidth,
115
- popoverMinHeight,
116
- popoverTitle,
117
- emptyText,
118
- testId: editorTestId
119
- }),
141
+ content: popoverCmp({
142
+ editorTitle,
143
+ emptyText,
144
+ favoritesSide,
145
+ favoritesTitle,
146
+ popoverWidth,
147
+ popoverMinHeight,
148
+ testId
149
+ }),
120
150
  onInteraction: (nextOpenState, e) => {
121
151
  if (
122
152
  isOpen &&
123
153
  nextOpenState === false &&
124
154
  e?.target &&
125
- !elemWithin(
126
- e.target as HTMLElement,
127
- 'xh-grouping-chooser-button--with-favorites'
128
- )
155
+ !elemWithin(e.target as HTMLElement, 'xh-grouping-chooser-button')
129
156
  ) {
130
157
  model.commitPendingValueAndClose();
131
158
  }
@@ -138,18 +165,65 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
138
165
  //------------------
139
166
  // Editor
140
167
  //------------------
141
- const editor = hoistCmp.factory<GroupingChooserModel>({
142
- render({popoverWidth, popoverMinHeight, popoverTitle, emptyText, testId}) {
168
+ const popoverCmp = hoistCmp.factory<Partial<GroupingChooserProps>>({
169
+ render({
170
+ model,
171
+ editorTitle,
172
+ emptyText,
173
+ favoritesSide,
174
+ favoritesTitle,
175
+ popoverWidth,
176
+ popoverMinHeight,
177
+ testId
178
+ }) {
179
+ const favesTB = isTB(favoritesSide),
180
+ isFavesFirst = favoritesSide === 'left' || favoritesSide === 'top',
181
+ items = [
182
+ editor({
183
+ editorTitle,
184
+ emptyText,
185
+ testId: getTestId(testId, 'editor')
186
+ }),
187
+ favoritesChooser({
188
+ // Omit if favorites generally disabled, or if none saved yet AND in top/bottom
189
+ // orientation - the empty state looks clumsy in that case. Show when empty in
190
+ // left/right orientation to avoid large jump in popover width.
191
+ omit: !model.persistFavorites || (!model.hasFavorites && favesTB),
192
+ favoritesSide,
193
+ favoritesTitle,
194
+ testId: getTestId(testId, 'favorites')
195
+ })
196
+ ],
197
+ itemsContainer = favesTB ? vframe : hframe;
198
+
199
+ if (isFavesFirst) {
200
+ items.reverse();
201
+ }
202
+
143
203
  return panel({
204
+ className: 'xh-grouping-chooser-popover__inner',
144
205
  width: popoverWidth,
145
206
  minHeight: popoverMinHeight,
207
+ items: itemsContainer({items}),
208
+ bbar: toolbar({
209
+ compact: true,
210
+ omit: !model.persistFavorites,
211
+ items: [filler(), favoritesAddBtn({testId})]
212
+ })
213
+ });
214
+ }
215
+ });
216
+
217
+ const editor = hoistCmp.factory<GroupingChooserModel>({
218
+ render({editorTitle, emptyText, testId}) {
219
+ return vbox({
220
+ className: 'xh-grouping-chooser__editor',
221
+ testId,
146
222
  items: [
147
- div({className: 'xh-popup__title', item: popoverTitle, omit: !popoverTitle}),
223
+ div({className: 'xh-popup__title', item: editorTitle, omit: isNil(editorTitle)}),
148
224
  dimensionList({emptyText}),
149
- addDimensionControl(),
150
- filler()
151
- ],
152
- testId
225
+ addDimensionControl()
226
+ ]
153
227
  });
154
228
  }
155
229
  });
@@ -283,7 +357,7 @@ const addDimensionControl = hoistCmp.factory<GroupingChooserModel>({
283
357
  // ensure the Select loses its internal input state.
284
358
  key: JSON.stringify(options),
285
359
  options,
286
- placeholder: 'Add...',
360
+ placeholder: 'Add level...',
287
361
  flex: 1,
288
362
  width: null,
289
363
  hideDropdownIndicator: true,
@@ -320,47 +394,25 @@ function getDimOptions(dims, model) {
320
394
  //------------------
321
395
  // Favorites
322
396
  //------------------
323
- const favoritesIconCmp = hoistCmp.factory<GroupingChooserModel>({
324
- render({model, testId, favoritesIcon}) {
325
- if (!model.persistFavorites) return null;
326
- return div({
327
- item: favoritesIcon ?? Icon.favorite(),
328
- className: 'xh-grouping-chooser__favorite-icon',
329
- [TEST_ID]: testId,
330
- onClick: e => {
331
- model.toggleFavoritesMenu();
332
- e.stopPropagation();
333
- }
334
- });
335
- }
336
- });
337
-
338
- const favoritesMenu = hoistCmp.factory<GroupingChooserModel>({
339
- render({model, testId}) {
340
- const options = model.favoritesOptions,
341
- isFavorite = model.isFavorite(model.value),
342
- omitAdd = isEmpty(model.value) || isFavorite,
343
- items = [];
344
-
345
- if (isEmpty(options)) {
346
- items.push(menuItem({text: 'No favorites saved...', disabled: true}));
347
- } else {
348
- items.push(...options.map(it => favoriteMenuItem(it)));
349
- }
350
-
351
- items.push(
352
- menuDivider({omit: omitAdd}),
353
- menuItem({
354
- icon: Icon.add({intent: 'success'}),
355
- text: 'Add current',
356
- omit: omitAdd,
357
- onClick: () => model.addFavorite(model.value)
358
- })
359
- );
397
+ const favoritesChooser = hoistCmp.factory<GroupingChooserModel>({
398
+ render({model, favoritesSide, favoritesTitle, testId}) {
399
+ const {favoritesOptions: options, hasFavorites} = model;
360
400
 
361
401
  return vbox({
402
+ className: `xh-grouping-chooser__favorites xh-grouping-chooser__favorites--${favoritesSide}`,
362
403
  testId,
363
- items: [div({className: 'xh-popup__title', item: 'Favorites'}), menu({items})]
404
+ items: [
405
+ div({
406
+ className: 'xh-popup__title',
407
+ item: favoritesTitle,
408
+ omit: isNil(favoritesTitle)
409
+ }),
410
+ hasFavorites
411
+ ? menu({
412
+ items: options.map(it => favoriteMenuItem(it))
413
+ })
414
+ : placeholder('No favorites saved.')
415
+ ]
364
416
  });
365
417
  }
366
418
  });
@@ -369,7 +421,7 @@ const favoriteMenuItem = hoistCmp.factory<GroupingChooserModel>({
369
421
  render({model, value, label}) {
370
422
  return menuItem({
371
423
  text: label,
372
- className: 'xh-grouping-chooser__favorite',
424
+ className: 'xh-grouping-chooser__favorites__favorite',
373
425
  onClick: () => model.setValue(value),
374
426
  labelElement: button({
375
427
  icon: Icon.delete(),
@@ -382,3 +434,19 @@ const favoriteMenuItem = hoistCmp.factory<GroupingChooserModel>({
382
434
  });
383
435
  }
384
436
  });
437
+
438
+ const favoritesAddBtn = hoistCmp.factory<GroupingChooserModel>({
439
+ render({model, testId}) {
440
+ return button({
441
+ text: 'Save as Favorite',
442
+ icon: Icon.favorite(),
443
+ className: 'xh-grouping-chooser__favorites__add-btn',
444
+ testId: getTestId(testId, 'favorites-add-btn'),
445
+ omit: !model.persistFavorites,
446
+ disabled: !model.isAddFavoriteEnabled,
447
+ onClick: () => model.addPendingAsFavorite()
448
+ });
449
+ }
450
+ });
451
+
452
+ const isTB = (favoritesSide: Side) => favoritesSide === 'top' || favoritesSide === 'bottom';
@@ -84,7 +84,7 @@ export interface PanelProps extends HoistProps<PanelModel>, Omit<BoxProps, 'titl
84
84
  tbar?: Some<ReactNode>;
85
85
 
86
86
  /**
87
- * A toolbar to be docked at the top of the panel.
87
+ * A toolbar to be docked at the bottom of the panel.
88
88
  * If specified as an array, items will be passed as children to a Toolbar component.
89
89
  */
90
90
  bbar?: Some<ReactNode>;
@@ -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
  });
@@ -7,6 +7,7 @@
7
7
  import {FeedbackDialogModel} from '@xh/hoist/appcontainer/FeedbackDialogModel';
8
8
  import {filler} from '@xh/hoist/cmp/layout';
9
9
  import {hoistCmp, uses} from '@xh/hoist/core';
10
+ import {Icon} from '@xh/hoist/icon';
10
11
  import {button} from '@xh/hoist/mobile/cmp/button';
11
12
  import {dialog} from '@xh/hoist/mobile/cmp/dialog';
12
13
  import {textArea} from '@xh/hoist/mobile/cmp/input';
@@ -41,6 +42,9 @@ export const feedbackDialog = hoistCmp.factory({
41
42
  }),
42
43
  button({
43
44
  text: 'Send',
45
+ icon: Icon.mail(),
46
+ intent: 'primary',
47
+ outlined: true,
44
48
  onClick: () => model.submitAsync()
45
49
  })
46
50
  ]
@@ -57,8 +57,10 @@ export const optionsDialog = hoistCmp.factory({
57
57
  onClick: () => model.hide()
58
58
  }),
59
59
  button({
60
- text: 'Save',
60
+ text: 'Apply',
61
61
  icon: reloadRequired ? Icon.refresh() : Icon.check(),
62
+ intent: 'primary',
63
+ outlined: true,
62
64
  disabled: !formModel.isDirty,
63
65
  onClick: () => model.saveAsync()
64
66
  })
@@ -60,7 +60,6 @@ export const [ColChooser, colChooser] = hoistCmp.withFactory<ColChooserProps>({
60
60
  onDragEnd: impl.onDragEnd,
61
61
  items: [
62
62
  panel({
63
- title: 'Visible Columns',
64
63
  className: 'xh-col-chooser__section',
65
64
  scrollable: true,
66
65
  items: [
@@ -119,8 +118,10 @@ export const [ColChooser, colChooser] = hoistCmp.withFactory<ColChooserProps>({
119
118
  onClick: () => model.close()
120
119
  }),
121
120
  button({
122
- text: 'Save',
121
+ text: 'Apply',
123
122
  icon: Icon.check(),
123
+ intent: 'primary',
124
+ outlined: true,
124
125
  onClick: () => {
125
126
  model.commit();
126
127
  model.close();