@xh/hoist 71.0.0-SNAPSHOT.1733854822950 → 71.0.0-SNAPSHOT.1734551243081

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 (42) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/build/types/cmp/viewmanager/View.d.ts +5 -0
  3. package/build/types/cmp/viewmanager/ViewInfo.d.ts +32 -7
  4. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +41 -31
  5. package/build/types/cmp/viewmanager/ViewToBlobApi.d.ts +28 -7
  6. package/build/types/cmp/viewmanager/index.d.ts +1 -1
  7. package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +0 -4
  8. package/build/types/desktop/cmp/viewmanager/ViewManagerLocalModel.d.ts +11 -0
  9. package/build/types/desktop/cmp/viewmanager/ViewMenu.d.ts +2 -2
  10. package/build/types/desktop/cmp/viewmanager/dialog/ManageDialog.d.ts +3 -1
  11. package/build/types/desktop/cmp/viewmanager/dialog/ManageDialogModel.d.ts +18 -13
  12. package/build/types/desktop/cmp/viewmanager/dialog/SaveAsDialog.d.ts +1 -1
  13. package/build/types/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.d.ts +3 -9
  14. package/build/types/desktop/cmp/viewmanager/dialog/Utils.d.ts +3 -0
  15. package/build/types/desktop/cmp/viewmanager/dialog/ViewMultiPanel.d.ts +2 -0
  16. package/build/types/desktop/cmp/viewmanager/dialog/ViewPanel.d.ts +5 -0
  17. package/build/types/desktop/cmp/viewmanager/dialog/{EditFormModel.d.ts → ViewPanelModel.d.ts} +2 -4
  18. package/build/types/svc/JsonBlobService.d.ts +1 -1
  19. package/cmp/viewmanager/View.ts +21 -1
  20. package/cmp/viewmanager/ViewInfo.ts +58 -11
  21. package/cmp/viewmanager/ViewManagerModel.ts +109 -82
  22. package/cmp/viewmanager/ViewToBlobApi.ts +114 -42
  23. package/cmp/viewmanager/index.ts +1 -1
  24. package/desktop/cmp/viewmanager/ViewManager.scss +25 -28
  25. package/desktop/cmp/viewmanager/ViewManager.ts +32 -27
  26. package/desktop/cmp/viewmanager/ViewManagerLocalModel.ts +33 -0
  27. package/desktop/cmp/viewmanager/ViewMenu.ts +162 -169
  28. package/desktop/cmp/viewmanager/dialog/ManageDialog.ts +69 -42
  29. package/desktop/cmp/viewmanager/dialog/ManageDialogModel.ts +267 -150
  30. package/desktop/cmp/viewmanager/dialog/SaveAsDialog.ts +30 -9
  31. package/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.ts +35 -40
  32. package/desktop/cmp/viewmanager/dialog/Utils.ts +18 -0
  33. package/desktop/cmp/viewmanager/dialog/ViewMultiPanel.ts +70 -0
  34. package/desktop/cmp/viewmanager/dialog/ViewPanel.ts +166 -0
  35. package/desktop/cmp/viewmanager/dialog/ViewPanelModel.ts +116 -0
  36. package/package.json +1 -1
  37. package/svc/JsonBlobService.ts +3 -3
  38. package/svc/storage/BaseStorageService.ts +1 -1
  39. package/tsconfig.tsbuildinfo +1 -1
  40. package/build/types/desktop/cmp/viewmanager/dialog/EditForm.d.ts +0 -5
  41. package/desktop/cmp/viewmanager/dialog/EditForm.ts +0 -126
  42. package/desktop/cmp/viewmanager/dialog/EditFormModel.ts +0 -125
@@ -1,32 +1,6 @@
1
1
  .xh-view-manager {
2
2
  align-items: center;
3
3
 
4
- &__menu-item {
5
- &:hover {
6
- .xh-view-manager__menu-item__fave-toggle {
7
- opacity: 1;
8
- color: var(--xh-yellow);
9
- }
10
- }
11
-
12
- &__fave-toggle {
13
- opacity: 0;
14
-
15
- &:hover {
16
- color: var(--xh-yellow-light) !important;
17
- }
18
-
19
- &--active {
20
- opacity: 1;
21
- color: var(--xh-yellow);
22
- }
23
-
24
- &--active:hover {
25
- color: var(--xh-yellow-light) !important;
26
- }
27
- }
28
- }
29
-
30
4
  //------------------------
31
5
  // Dialogs
32
6
  //------------------------
@@ -35,8 +9,16 @@
35
9
  &__form {
36
10
  padding: var(--xh-pad-px);
37
11
 
38
- .xh-form-field.xh-form-field-readonly .xh-form-field-readonly-display {
39
- padding: 0;
12
+ .xh-form-field.xh-form-field-readonly {
13
+ &:not(.xh-form-field-inline) {
14
+ .xh-form-field-label {
15
+ border-bottom: var(--xh-border-solid);
16
+ }
17
+ }
18
+
19
+ .xh-form-field-readonly-display {
20
+ padding: 0;
21
+ }
40
22
  }
41
23
 
42
24
  .xh-form-field .xh-form-field-info {
@@ -54,5 +36,20 @@
54
36
  color: var(--xh-text-color-muted);
55
37
  font-size: var(--xh-font-size-small-px);
56
38
  }
39
+
40
+ &__help-text {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: var(--xh-pad-px);
44
+ margin: var(--xh-pad-px);
45
+ padding: var(--xh-pad-px);
46
+ border: var(--xh-border-solid);
47
+ border-radius: var(--xh-border-radius-px);
48
+ background-color: var(--xh-bg-alt);
49
+
50
+ .xh-icon {
51
+ font-size: 1.2em;
52
+ }
53
+ }
57
54
  }
58
55
  }
@@ -7,13 +7,15 @@
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, uses} from '@xh/hoist/core';
10
+ import {hoistCmp, HoistProps, 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
+ import {useOnVisibleChange} from '@xh/hoist/utils/react';
15
16
  import {startCase} from 'lodash';
16
17
  import {viewMenu} from './ViewMenu';
18
+ import {ViewManagerLocalModel} from './ViewManagerLocalModel';
17
19
  import {manageDialog} from './dialog/ManageDialog';
18
20
  import {saveAsDialog} from './dialog/SaveAsDialog';
19
21
 
@@ -39,10 +41,6 @@ export interface ViewManagerProps extends HoistProps<ViewManagerModel> {
39
41
  showRevertButton?: ViewManagerStateButtonMode;
40
42
  /** Side the save and revert buttons should appear on (default 'right') */
41
43
  buttonSide?: 'left' | 'right';
42
- /** True to render private views in sub-menu (Default false) */
43
- showPrivateViewsInSubMenu?: boolean;
44
- /** True to render global views in sub-menu (Default false) */
45
- showGlobalViewsInSubMenu?: boolean;
46
44
  }
47
45
 
48
46
  /**
@@ -64,32 +62,33 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
64
62
  revertButtonProps,
65
63
  showSaveButton = 'whenDirty',
66
64
  showRevertButton = 'never',
67
- buttonSide = 'right',
68
- showPrivateViewsInSubMenu = false,
69
- showGlobalViewsInSubMenu = false
65
+ buttonSide = 'right'
70
66
  }: ViewManagerProps) {
71
- const save = saveButton({mode: showSaveButton, ...saveButtonProps}),
72
- revert = revertButton({mode: showRevertButton, ...revertButtonProps}),
67
+ const locModel = useLocalModel(() => new ViewManagerLocalModel(model)),
68
+ save = saveButton({model: locModel, mode: showSaveButton, ...saveButtonProps}),
69
+ revert = revertButton({model: locModel, mode: showRevertButton, ...revertButtonProps}),
73
70
  menu = popover({
74
- item: menuButton(menuButtonProps),
75
- content: viewMenu({showPrivateViewsInSubMenu, showGlobalViewsInSubMenu}),
71
+ disabled: !locModel.isVisible, // Prevent orphaned popover menu
72
+ item: menuButton({model: locModel, ...menuButtonProps}),
73
+ content: viewMenu({model: locModel}),
76
74
  placement: 'bottom-start',
77
75
  popoverClassName: 'xh-view-manager__popover'
78
76
  });
79
77
  return fragment(
80
78
  hbox({
81
79
  className,
82
- items: buttonSide == 'left' ? [revert, save, menu] : [menu, save, revert]
80
+ items: buttonSide == 'left' ? [revert, save, menu] : [menu, save, revert],
81
+ ref: useOnVisibleChange(isVisible => (locModel.isVisible = isVisible))
83
82
  }),
84
- manageDialog({omit: !model.manageDialogOpen}),
85
- saveAsDialog()
83
+ manageDialog({model: locModel.manageDialogModel}),
84
+ saveAsDialog({model: locModel.saveAsDialogModel})
86
85
  );
87
86
  }
88
87
  });
89
88
 
90
- const menuButton = hoistCmp.factory<ViewManagerModel>({
89
+ const menuButton = hoistCmp.factory<ViewManagerLocalModel>({
91
90
  render({model, ...rest}) {
92
- const {view, typeDisplayName, isLoading} = model;
91
+ const {view, typeDisplayName, isLoading} = model.parent;
93
92
  return button({
94
93
  className: 'xh-view-manager__menu-button',
95
94
  text: view.isDefault ? `Default ${startCase(typeDisplayName)}` : view.name,
@@ -106,40 +105,46 @@ const menuButton = hoistCmp.factory<ViewManagerModel>({
106
105
  }
107
106
  });
108
107
 
109
- const saveButton = hoistCmp.factory<ViewManagerModel>({
108
+ const saveButton = hoistCmp.factory<ViewManagerLocalModel>({
110
109
  render({model, mode, ...rest}) {
111
110
  if (hideStateButton(model, mode)) return null;
111
+ const {parent, saveAsDialogModel} = model,
112
+ {typeDisplayName, isLoading, isValueDirty} = parent;
112
113
  return button({
113
114
  className: 'xh-view-manager__save-button',
114
115
  icon: Icon.save(),
115
- tooltip: `Save changes to this ${model.typeDisplayName}`,
116
+ tooltip: `Save changes to this ${typeDisplayName}`,
116
117
  intent: 'primary',
117
- disabled: !model.isValueDirty || model.isLoading,
118
+ disabled: !isValueDirty || isLoading,
118
119
  onClick: () => {
119
- model.isViewSavable ? model.saveAsync() : model.saveAsAsync();
120
+ parent.isViewSavable ? parent.saveAsync() : saveAsDialogModel.open();
120
121
  },
121
122
  ...rest
122
123
  });
123
124
  }
124
125
  });
125
126
 
126
- const revertButton = hoistCmp.factory<ViewManagerModel>({
127
+ const revertButton = hoistCmp.factory<ViewManagerLocalModel>({
127
128
  render({model, mode, ...rest}) {
128
129
  if (hideStateButton(model, mode)) return null;
130
+ const {typeDisplayName, isLoading, isValueDirty} = model.parent;
129
131
  return button({
130
132
  className: 'xh-view-manager__revert-button',
131
133
  icon: Icon.reset(),
132
- tooltip: `Revert changes to this ${model.typeDisplayName}`,
134
+ tooltip: `Revert changes to this ${typeDisplayName}`,
133
135
  intent: 'danger',
134
- disabled: !model.isValueDirty || model.isLoading,
135
- onClick: () => model.resetAsync(),
136
+ disabled: !isValueDirty || isLoading,
137
+ onClick: () => model.parent.resetAsync(),
136
138
  ...rest
137
139
  });
138
140
  }
139
141
  });
140
142
 
141
- function hideStateButton(model: ViewManagerModel, mode: ViewManagerStateButtonMode): boolean {
143
+ function hideStateButton(model: ViewManagerLocalModel, mode: ViewManagerStateButtonMode): boolean {
144
+ const {parent} = model;
142
145
  return (
143
- mode === 'never' || (mode === 'whenDirty' && !model.isValueDirty) || model.isViewAutoSavable
146
+ mode === 'never' ||
147
+ (mode === 'whenDirty' && !parent.isValueDirty) ||
148
+ parent.isViewAutoSavable
144
149
  );
145
150
  }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2024 Extremely Heavy Industries Inc.
6
+ */
7
+
8
+ import {HoistModel, managed} from '@xh/hoist/core';
9
+ import {bindable, makeObservable} from '@xh/hoist/mobx';
10
+ import {ManageDialogModel} from './dialog/ManageDialogModel';
11
+ import {SaveAsDialogModel} from './dialog/SaveAsDialogModel';
12
+ import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
13
+
14
+ export class ViewManagerLocalModel extends HoistModel {
15
+ readonly parent: ViewManagerModel;
16
+
17
+ @managed
18
+ readonly manageDialogModel: ManageDialogModel;
19
+
20
+ @managed
21
+ readonly saveAsDialogModel: SaveAsDialogModel;
22
+
23
+ @bindable
24
+ isVisible = true;
25
+
26
+ constructor(parent: ViewManagerModel) {
27
+ super();
28
+ makeObservable(this);
29
+ this.parent = parent;
30
+ this.manageDialogModel = new ManageDialogModel(parent);
31
+ this.saveAsDialogModel = new SaveAsDialogModel(parent);
32
+ }
33
+ }
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2024 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {box, div, filler, fragment, hbox, span} from '@xh/hoist/cmp/layout';
8
+ import {box} from '@xh/hoist/cmp/layout';
9
9
  import {spinner} from '@xh/hoist/cmp/spinner';
10
10
  import {hoistCmp} from '@xh/hoist/core';
11
11
  import {ViewManagerModel, ViewInfo} from '@xh/hoist/cmp/viewmanager';
@@ -14,188 +14,181 @@ import {Icon} from '@xh/hoist/icon';
14
14
  import {menu, menuDivider, menuItem} from '@xh/hoist/kit/blueprint';
15
15
  import {wait} from '@xh/hoist/promise';
16
16
  import {consumeEvent, pluralize} from '@xh/hoist/utils/js';
17
- import {isEmpty, startCase} from 'lodash';
17
+ import {Dictionary} from 'express-serve-static-core';
18
+ import {each, filter, groupBy, isEmpty, orderBy, some, startCase} from 'lodash';
18
19
  import {ReactNode} from 'react';
19
- import {ViewManagerProps} from './ViewManager';
20
+ import {ViewManagerLocalModel} from './ViewManagerLocalModel';
20
21
 
21
22
  /**
22
23
  * Default Menu used by ViewManager.
23
24
  */
24
- export const viewMenu = hoistCmp.factory<ViewManagerProps>({
25
- render({model, showPrivateViewsInSubMenu, showGlobalViewsInSubMenu}) {
26
- const {
27
- enableAutoSave,
28
- autoSaveUnavailableReason,
29
- autoSave,
30
- enableDefault,
31
- isViewSavable,
32
- view,
33
- typeDisplayName,
34
- globalDisplayName,
35
- favoriteViews,
36
- views,
37
- isValueDirty,
38
- privateViews,
39
- globalViews,
40
- loadModel
41
- } = model;
42
-
43
- const pluralName = pluralize(startCase(typeDisplayName)),
44
- myPluralName = `My ${pluralName}`,
45
- globalPluralName = `${startCase(globalDisplayName)} ${pluralName}`,
46
- items = [];
47
- if (!isEmpty(favoriteViews)) {
48
- items.push(
49
- menuDivider({title: 'Favorites'}),
50
- ...favoriteViews.map(info => {
51
- return menuItem({
52
- key: `${info.token}-favorite`,
53
- icon: view.token === info.token ? Icon.check() : Icon.placeholder(),
54
- text: textAndFaveToggle({info}),
55
- onClick: () => model.selectViewAsync(info),
56
- title: info.description
57
- });
58
- })
59
- );
60
- }
61
-
62
- if (!isEmpty(privateViews)) {
63
- const privateItems = privateViews.map(it => buildMenuItem(it, model));
64
- if (showPrivateViewsInSubMenu) {
65
- items.push(
66
- menuDivider({omit: isEmpty(items)}),
67
- menuItem({
68
- text: myPluralName,
69
- shouldDismissPopover: false,
70
- items: privateItems
71
- })
72
- );
73
- } else {
74
- items.push(menuDivider({title: myPluralName}), ...privateItems);
75
- }
76
- }
77
-
78
- if (!isEmpty(globalViews)) {
79
- const globalItems = globalViews.map(it => buildMenuItem(it, model));
80
- if (showGlobalViewsInSubMenu) {
81
- items.push(
82
- menuDivider({omit: isEmpty(items)}),
83
- menuItem({
84
- text: globalPluralName,
85
- shouldDismissPopover: false,
86
- items: globalItems
87
- })
88
- );
89
- } else {
90
- items.push(menuDivider({title: globalPluralName}), ...globalItems);
91
- }
92
- }
93
-
25
+ export const viewMenu = hoistCmp.factory<ViewManagerLocalModel>({
26
+ render({model}) {
94
27
  return menu({
95
28
  className: 'xh-view-manager__menu',
96
- items: [
97
- ...items,
98
- menuDivider({omit: !enableDefault || isEmpty(items)}),
99
- menuItem({
100
- icon: view.isDefault ? Icon.check() : Icon.placeholder(),
101
- text: `Default ${startCase(typeDisplayName)}`,
102
- omit: !enableDefault,
103
- onClick: () => model.selectViewAsync(null)
104
- }),
105
- menuDivider(),
106
- menuItem({
107
- icon: Icon.save(),
108
- text: 'Save',
109
- disabled: !isViewSavable || !isValueDirty,
110
- onClick: () => model.saveAsync()
111
- }),
112
- menuItem({
113
- icon: Icon.placeholder(),
114
- text: 'Save As...',
115
- onClick: () => model.saveAsAsync()
116
- }),
117
- menuItem({
118
- icon: Icon.reset(),
119
- text: `Revert`,
120
- disabled: !isValueDirty,
121
- onClick: () => model.resetAsync()
122
- }),
123
- menuDivider({omit: !enableAutoSave}),
124
- menuItem({
125
- omit: !enableAutoSave,
126
- text: switchInput({
127
- label: 'Auto Save',
128
- value: !autoSaveUnavailableReason && autoSave,
129
- disabled: !!autoSaveUnavailableReason,
130
- onChange: v => (model.autoSave = v),
131
- inline: true
132
- }),
133
- title: autoSaveUnavailableReason,
134
- shouldDismissPopover: false
135
- }),
136
- menuDivider(),
137
- menuItem({
138
- icon: Icon.gear(),
139
- disabled: isEmpty(views),
140
- text: `Manage ${pluralName}...`,
141
- onClick: () => model.openManageDialog()
142
- }),
143
- menuItem({
144
- icon: !loadModel.isPending
145
- ? Icon.refresh()
146
- : box({
147
- height: 20,
148
- item: spinner({width: 16.25, height: 16.25})
149
- }),
150
- disabled: loadModel.isPending,
151
- text: `Refresh ${pluralName}`,
152
- onClick: e => {
153
- // Ensure at least 100ms delay to render spinner
154
- Promise.all([wait(100), model.refreshAsync()]).linkTo(loadModel);
155
- consumeEvent(e);
156
- }
157
- })
158
- ]
29
+ items: [...getNavMenuItems(model.parent), menuDivider(), ...getOtherMenuItems(model)]
159
30
  });
160
31
  }
161
32
  });
162
33
 
163
- function buildMenuItem(data: ViewInfo, model: ViewManagerModel): ReactNode {
164
- const selected = data.token === model.view.token,
165
- icon = selected ? Icon.check() : Icon.placeholder();
34
+ function getNavMenuItems(model: ViewManagerModel): ReactNode[] {
35
+ const {enableDefault, view, typeDisplayName, globalDisplayName} = model,
36
+ ownedViews = groupBy(filter(model.ownedViews, 'isPinned'), 'group'),
37
+ globalViews = groupBy(filter(model.globalViews, 'isPinned'), 'group'),
38
+ sharedViews = groupBy(filter(model.sharedViews, 'isPinned'), 'owner'),
39
+ pluralName = pluralize(startCase(typeDisplayName)),
40
+ ret = [];
41
+
42
+ // Main Views items by type
43
+ if (!isEmpty(ownedViews)) {
44
+ ret.push(
45
+ menuDivider({title: `My ${pluralName}`}),
46
+ ...getGroupedMenuItems(ownedViews, model)
47
+ );
48
+ }
49
+ if (!isEmpty(globalViews)) {
50
+ ret.push(
51
+ menuDivider({title: `${startCase(globalDisplayName)} ${pluralName}`}),
52
+ ...getGroupedMenuItems(globalViews, model)
53
+ );
54
+ }
55
+ if (!isEmpty(sharedViews)) {
56
+ ret.push(
57
+ menuDivider({title: `Shared ${pluralName}`}),
58
+ ...getGroupedMenuItems(sharedViews, model)
59
+ );
60
+ }
61
+
62
+ if (enableDefault) {
63
+ ret.push(
64
+ menuDivider({omit: isEmpty(ret)}),
65
+ menuItem({
66
+ className: 'xh-view-manager__menu-item',
67
+ icon: view.isDefault ? Icon.check() : Icon.placeholder(),
68
+ text: `Default ${startCase(typeDisplayName)}`,
69
+ onClick: () => model.selectViewAsync(null)
70
+ })
71
+ );
72
+ }
73
+
74
+ return ret;
75
+ }
76
+
77
+ function getOtherMenuItems(model: ViewManagerLocalModel): ReactNode[] {
78
+ const {parent} = model;
79
+ const {
80
+ enableAutoSave,
81
+ autoSaveUnavailableReason,
82
+ autoSave,
83
+ isViewSavable,
84
+ views,
85
+ isValueDirty,
86
+ loadModel,
87
+ typeDisplayName
88
+ } = parent;
89
+
90
+ const pluralName = pluralize(startCase(typeDisplayName));
91
+
92
+ return [
93
+ menuItem({
94
+ icon: Icon.save(),
95
+ text: 'Save',
96
+ disabled: !isViewSavable || !isValueDirty,
97
+ onClick: () => parent.saveAsync()
98
+ }),
99
+ menuItem({
100
+ icon: Icon.placeholder(),
101
+ text: 'Save As...',
102
+ onClick: () => model.saveAsDialogModel.open()
103
+ }),
104
+ menuItem({
105
+ icon: Icon.reset(),
106
+ text: `Revert`,
107
+ disabled: !isValueDirty,
108
+ onClick: () => parent.resetAsync()
109
+ }),
110
+ menuDivider({omit: !enableAutoSave}),
111
+ menuItem({
112
+ omit: !enableAutoSave,
113
+ text: switchInput({
114
+ label: 'Auto Save',
115
+ value: !autoSaveUnavailableReason && autoSave,
116
+ disabled: !!autoSaveUnavailableReason,
117
+ onChange: v => (parent.autoSave = v),
118
+ inline: true
119
+ }),
120
+ title: autoSaveUnavailableReason,
121
+ shouldDismissPopover: false
122
+ }),
123
+ menuDivider(),
124
+ menuItem({
125
+ icon: Icon.gear(),
126
+ disabled: isEmpty(views),
127
+ text: `Manage ${pluralName}...`,
128
+ onClick: () => model.manageDialogModel.open()
129
+ }),
130
+ menuItem({
131
+ icon: !loadModel.isPending
132
+ ? Icon.refresh()
133
+ : box({
134
+ height: 20,
135
+ item: spinner({width: 16.25, height: 16.25})
136
+ }),
137
+ disabled: loadModel.isPending,
138
+ text: `Refresh ${pluralName}`,
139
+ onClick: e => {
140
+ // Ensure at least 100ms delay to render spinner
141
+ Promise.all([wait(100), parent.refreshAsync()]).linkTo(loadModel);
142
+ consumeEvent(e);
143
+ }
144
+ })
145
+ ];
146
+ }
147
+
148
+ function getGroupedMenuItems(
149
+ byGroup: Dictionary<ViewInfo[]>,
150
+ model: ViewManagerModel
151
+ ): ReactNode[] {
152
+ // Create grouped tree...
153
+ let nodes: (ViewInfo | {name: string; groupViews: ViewInfo[]; isSelected: boolean})[] = [],
154
+ selectedToken = model.view.token;
155
+
156
+ each(byGroup, (groupViews, name) => {
157
+ if (name != 'null') {
158
+ nodes.push({name, groupViews, isSelected: some(groupViews, {token: selectedToken})});
159
+ } else {
160
+ nodes.push(...groupViews);
161
+ }
162
+ });
163
+
164
+ // ...sort groups first, then alpha by name. But could easily intersperse instead
165
+ nodes = orderBy(nodes, [v => v instanceof ViewInfo, 'name']);
166
+
167
+ return nodes.map(n => {
168
+ return n instanceof ViewInfo
169
+ ? viewMenuItem(n, model)
170
+ : menuItem({
171
+ text: n.name,
172
+ icon: n.isSelected ? Icon.check() : Icon.placeholder(),
173
+ shouldDismissPopover: false,
174
+ items: n.groupViews.map(v => viewMenuItem(v, model))
175
+ });
176
+ });
177
+ }
178
+
179
+ function viewMenuItem(view: ViewInfo, model: ViewManagerModel): ReactNode {
180
+ const icon = view.isCurrentView ? Icon.check() : Icon.placeholder(),
181
+ title = [];
182
+
183
+ if (!view.isOwned && view.owner) title.push(view.owner);
184
+ if (view.description) title.push(view.description);
166
185
 
167
186
  return menuItem({
168
187
  className: 'xh-view-manager__menu-item',
169
- key: data.token,
188
+ key: view.token,
189
+ text: view.name,
190
+ title: title.join(' | '),
170
191
  icon,
171
- text: textAndFaveToggle({info: data}),
172
- title: data.description,
173
- onClick: () => model.selectViewAsync(data)
192
+ onClick: () => model.selectViewAsync(view)
174
193
  });
175
194
  }
176
-
177
- const textAndFaveToggle = hoistCmp.factory<ViewManagerModel>({
178
- render({model, info}) {
179
- const {isFavorite, name} = info;
180
- return hbox({
181
- alignItems: 'center',
182
- items: [
183
- span({style: {paddingRight: 5}, item: name}),
184
- fragment({
185
- omit: !model.enableFavorites,
186
- items: [
187
- filler(),
188
- div({
189
- className: `xh-view-manager__menu-item__fave-toggle ${isFavorite ? 'xh-view-manager__menu-item__fave-toggle--active' : ''}`,
190
- item: Icon.favorite({prefix: isFavorite ? 'fas' : 'far'}),
191
- onClick: e => {
192
- consumeEvent(e);
193
- model.toggleFavorite(info.token);
194
- }
195
- })
196
- ]
197
- })
198
- ]
199
- });
200
- }
201
- });