@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.
- package/CHANGELOG.md +36 -4
- package/build/types/cmp/grouping/GroupingChooserModel.d.ts +8 -4
- package/build/types/cmp/viewmanager/ViewInfo.d.ts +7 -3
- package/build/types/core/types/Interfaces.d.ts +1 -1
- package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +17 -6
- package/build/types/desktop/cmp/panel/Panel.d.ts +1 -1
- package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +26 -10
- package/build/types/mobile/cmp/grouping/GroupingChooser.d.ts +2 -6
- package/build/types/utils/impl/MenuItems.d.ts +13 -0
- package/build/types/utils/impl/index.d.ts +1 -0
- package/cmp/grouping/GroupingChooserModel.ts +25 -12
- package/cmp/viewmanager/ViewInfo.ts +7 -3
- package/core/HoistAppModel.ts +1 -0
- package/core/exception/ExceptionHandler.ts +1 -1
- package/core/types/Interfaces.ts +2 -2
- package/desktop/cmp/button/AppMenuButton.ts +3 -46
- package/desktop/cmp/grouping/GroupingChooser.scss +39 -35
- package/desktop/cmp/grouping/GroupingChooser.ts +157 -89
- package/desktop/cmp/panel/Panel.ts +1 -1
- package/desktop/cmp/viewmanager/ViewManager.ts +58 -16
- package/desktop/cmp/viewmanager/ViewMenu.ts +9 -2
- package/mobile/appcontainer/FeedbackDialog.ts +4 -0
- package/mobile/appcontainer/OptionsDialog.ts +3 -1
- package/mobile/cmp/grid/impl/ColChooser.ts +3 -2
- package/mobile/cmp/grouping/GroupingChooser.scss +41 -20
- package/mobile/cmp/grouping/GroupingChooser.ts +60 -89
- package/mobile/cmp/panel/DialogPanel.scss +5 -0
- package/package.json +1 -1
- package/svc/TrackService.ts +4 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/impl/MenuItems.ts +57 -0
- package/utils/impl/index.ts +1 -0
- 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 {
|
|
9
|
-
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
35
|
-
popoverTitle?:
|
|
57
|
+
/** @deprecated - use `editorTitle` instead */
|
|
58
|
+
popoverTitle?: ReactNode;
|
|
36
59
|
|
|
37
|
-
/**
|
|
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
|
-
|
|
87
|
+
favoritesSide = 'right',
|
|
88
|
+
favoritesTitle = 'Favorites',
|
|
89
|
+
popoverWidth,
|
|
64
90
|
popoverMinHeight,
|
|
65
|
-
popoverTitle
|
|
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,
|
|
75
|
-
isOpen = editorIsOpen
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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:
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
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:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
142
|
-
render({
|
|
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:
|
|
223
|
+
div({className: 'xh-popup__title', item: editorTitle, omit: isNil(editorTitle)}),
|
|
148
224
|
dimensionList({emptyText}),
|
|
149
|
-
addDimensionControl()
|
|
150
|
-
|
|
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
|
|
324
|
-
render({model,
|
|
325
|
-
|
|
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: [
|
|
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-
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
?
|
|
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: [
|
|
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: '
|
|
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: '
|
|
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();
|