@xh/hoist 75.0.0-SNAPSHOT.1751313309623 → 75.0.0-SNAPSHOT.1751426116456

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
@@ -1,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## v75.0.0-SNAPSHOT - unreleased
3
+ ## v74.1.1 - 2025-07-02
4
+
5
+ ### 🎁 New Features
6
+
7
+ * Further refinements to the `GroupingChooser` desktop UI.
8
+ * Added new props `favoritesSide` and `favoritesTitle`.
9
+ * Deprecated `popoverTitle` prop - use `editorTitle` instead.
10
+ * Moved "Save as Favorite" button to a new compact toolbar within the popover.
4
11
 
5
12
  ## v74.1.0 - 2025-06-30
6
13
 
@@ -76,6 +76,7 @@ export declare class GroupingChooserModel extends HoistModel {
76
76
  value: string[];
77
77
  label: string;
78
78
  }[];
79
+ get hasFavorites(): boolean;
79
80
  setFavorites(favorites: string[][]): void;
80
81
  addFavorite(value: string[]): void;
81
82
  addPendingAsFavorite(): void;
@@ -1,19 +1,30 @@
1
1
  import { GroupingChooserModel } from '@xh/hoist/cmp/grouping';
2
+ import { Side } from '@xh/hoist/core';
2
3
  import { ButtonProps } from '@xh/hoist/desktop/cmp/button';
3
4
  import '@xh/hoist/desktop/register';
4
5
  import './GroupingChooser.scss';
6
+ import { ReactNode } from 'react';
5
7
  export interface GroupingChooserProps extends ButtonProps<GroupingChooserModel> {
8
+ /** Title for value-editing portion of popover, or null to suppress. */
9
+ editorTitle?: ReactNode;
6
10
  /** Text to represent empty state (i.e. value = null or []) */
7
11
  emptyText?: string;
12
+ /**
13
+ * Side of the popover, relative to the value-editing controls, on which the Favorites list
14
+ * should be rendered, if enabled.
15
+ */
16
+ favoritesSide?: Side;
17
+ /** Title for favorites-list portion of popover, or null to suppress. */
18
+ favoritesTitle?: ReactNode;
8
19
  /** Min height in pixels of the popover menu itself. */
9
20
  popoverMinHeight?: number;
10
21
  /** Position of popover relative to target button. */
11
22
  popoverPosition?: 'bottom' | 'top';
12
- /** Title for popover (default "GROUP BY") or null to suppress. */
13
- popoverTitle?: string;
23
+ /** @deprecated - use `editorTitle` instead */
24
+ popoverTitle?: ReactNode;
14
25
  /**
15
26
  * Width in pixels of the popover menu itself.
16
- * If unspecified, will default based on whether favorites are enabled.
27
+ * If unspecified, will default based on favorites enabled status + side.
17
28
  */
18
29
  popoverWidth?: number;
19
30
  /** True (default) to style target button as an input field - blends better in toolbars. */
@@ -266,6 +266,11 @@ export class GroupingChooserModel extends HoistModel {
266
266
  );
267
267
  }
268
268
 
269
+ @computed
270
+ get hasFavorites() {
271
+ return !isEmpty(this.favorites);
272
+ }
273
+
269
274
  @action
270
275
  setFavorites(favorites: string[][]) {
271
276
  this.favorites = favorites.filter(v => this.validateValue(v));
@@ -202,7 +202,7 @@ export class ExceptionHandler {
202
202
  XH.track({
203
203
  category: 'Client Error',
204
204
  severity: exception.isRoutine ? 'INFO' : 'ERROR',
205
- message: exception.message ?? 'Client Error',
205
+ message: exception.message || 'Client Error',
206
206
  correlationId: exception.correlationId,
207
207
  data,
208
208
  logData: ['userAlerted']
@@ -121,8 +121,23 @@
121
121
  }
122
122
 
123
123
  &__favorites {
124
- flex: 1;
125
- border-left: 1px solid var(--xh-popup-border-color);
124
+ &--top {
125
+ border-bottom: 1px solid var(--xh-popup-border-color);
126
+ }
127
+
128
+ &--right {
129
+ flex: 1;
130
+ border-left: 1px solid var(--xh-popup-border-color);
131
+ }
132
+
133
+ &--bottom {
134
+ border-top: 1px solid var(--xh-popup-border-color);
135
+ }
136
+
137
+ &--left {
138
+ flex: 1;
139
+ border-right: 1px solid var(--xh-popup-border-color);
140
+ }
126
141
 
127
142
  --xh-menu-border: none;
128
143
 
@@ -130,14 +145,8 @@
130
145
  padding: 0;
131
146
  }
132
147
 
133
- &__add-btn {
134
- flex: none;
135
- margin: var(--xh-pad-px) auto;
136
- }
137
-
138
148
  &__favorite {
139
149
  align-items: center;
140
- max-width: 50vw;
141
150
 
142
151
  .xh-button {
143
152
  padding: 0 !important;
@@ -5,37 +5,61 @@
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, hframe, 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
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} 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
30
  import classNames from 'classnames';
20
- import {compact, isEmpty, sortBy} from 'lodash';
31
+ import {compact, isEmpty, isNil, isUndefined, sortBy} from 'lodash';
21
32
  import './GroupingChooser.scss';
33
+ import {ReactNode} from 'react';
22
34
 
23
35
  export interface GroupingChooserProps extends ButtonProps<GroupingChooserModel> {
36
+ /** Title for value-editing portion of popover, or null to suppress. */
37
+ editorTitle?: ReactNode;
38
+
24
39
  /** Text to represent empty state (i.e. value = null or []) */
25
40
  emptyText?: string;
26
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
+
27
51
  /** Min height in pixels of the popover menu itself. */
28
52
  popoverMinHeight?: number;
29
53
 
30
54
  /** Position of popover relative to target button. */
31
55
  popoverPosition?: 'bottom' | 'top';
32
56
 
33
- /** Title for popover (default "GROUP BY") or null to suppress. */
34
- popoverTitle?: string;
57
+ /** @deprecated - use `editorTitle` instead */
58
+ popoverTitle?: ReactNode;
35
59
 
36
60
  /**
37
61
  * Width in pixels of the popover menu itself.
38
- * If unspecified, will default based on whether favorites are enabled.
62
+ * If unspecified, will default based on favorites enabled status + side.
39
63
  */
40
64
  popoverWidth?: number;
41
65
 
@@ -58,10 +82,13 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
58
82
  {
59
83
  model,
60
84
  className,
85
+ editorTitle = 'Group By',
61
86
  emptyText = 'Ungrouped',
87
+ favoritesSide = 'right',
88
+ favoritesTitle = 'Favorites',
62
89
  popoverWidth,
63
90
  popoverMinHeight,
64
- popoverTitle = 'Group By',
91
+ popoverTitle,
65
92
  popoverPosition = 'bottom',
66
93
  styleButtonAsInput = true,
67
94
  testId,
@@ -72,9 +99,18 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
72
99
  const {editorIsOpen, value, allowEmpty, persistFavorites} = model,
73
100
  isOpen = editorIsOpen,
74
101
  label = isEmpty(value) && allowEmpty ? emptyText : model.getValueLabel(value),
75
- [layoutProps, buttonProps] = splitLayoutProps(rest);
102
+ [layoutProps, buttonProps] = splitLayoutProps(rest),
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
+ }
76
112
 
77
- popoverWidth = popoverWidth || (persistFavorites ? 500 : 250);
113
+ popoverWidth = popoverWidth || (persistFavorites && !favesTB ? 500 : 250);
78
114
 
79
115
  return box({
80
116
  ref,
@@ -83,7 +119,7 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
83
119
  item: popover({
84
120
  isOpen,
85
121
  popoverRef: model.popoverRef,
86
- popoverClassName: 'xh-grouping-chooser-popover xh-popup--framed',
122
+ popoverClassName: `xh-grouping-chooser-popover xh-grouping-chooser-popover--${favesClassNameMod} xh-popup--framed`,
87
123
  // Left align editor to keep in place when button changing size when commitOnChange: true
88
124
  position: `${popoverPosition}-left`,
89
125
  minimal: false,
@@ -103,10 +139,12 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
103
139
  })
104
140
  ),
105
141
  content: popoverCmp({
142
+ editorTitle,
143
+ emptyText,
144
+ favoritesSide,
145
+ favoritesTitle,
106
146
  popoverWidth,
107
147
  popoverMinHeight,
108
- popoverTitle,
109
- emptyText,
110
148
  testId
111
149
  }),
112
150
  onInteraction: (nextOpenState, e) => {
@@ -127,35 +165,62 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
127
165
  //------------------
128
166
  // Editor
129
167
  //------------------
130
- const popoverCmp = hoistCmp.factory<GroupingChooserModel>({
131
- render({model, 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
+
132
203
  return panel({
204
+ className: 'xh-grouping-chooser-popover__inner',
133
205
  width: popoverWidth,
134
206
  minHeight: popoverMinHeight,
135
- items: hframe({
136
- items: [
137
- editor({
138
- popoverTitle,
139
- emptyText,
140
- testId: getTestId(testId, 'editor')
141
- }),
142
- favoritesChooser({
143
- omit: !model.persistFavorites,
144
- testId: getTestId(testId, 'favorites')
145
- })
146
- ]
207
+ items: itemsContainer({items}),
208
+ bbar: toolbar({
209
+ compact: true,
210
+ omit: !model.persistFavorites,
211
+ items: [filler(), favoritesAddBtn({testId})]
147
212
  })
148
213
  });
149
214
  }
150
215
  });
151
216
 
152
217
  const editor = hoistCmp.factory<GroupingChooserModel>({
153
- render({popoverTitle, emptyText, testId}) {
218
+ render({editorTitle, emptyText, testId}) {
154
219
  return vbox({
155
220
  className: 'xh-grouping-chooser__editor',
156
221
  testId,
157
222
  items: [
158
- div({className: 'xh-popup__title', item: popoverTitle, omit: !popoverTitle}),
223
+ div({className: 'xh-popup__title', item: editorTitle, omit: isNil(editorTitle)}),
159
224
  dimensionList({emptyText}),
160
225
  addDimensionControl()
161
226
  ]
@@ -330,26 +395,23 @@ function getDimOptions(dims, model) {
330
395
  // Favorites
331
396
  //------------------
332
397
  const favoritesChooser = hoistCmp.factory<GroupingChooserModel>({
333
- render({model, testId}) {
334
- const {favoritesOptions: options, isAddFavoriteEnabled} = model,
335
- items = isEmpty(options)
336
- ? [menuItem({text: 'No favorites saved.', disabled: true})]
337
- : options.map(it => favoriteMenuItem(it));
398
+ render({model, favoritesSide, favoritesTitle, testId}) {
399
+ const {favoritesOptions: options, hasFavorites} = model;
338
400
 
339
401
  return vbox({
340
- className: 'xh-grouping-chooser__favorites',
402
+ className: `xh-grouping-chooser__favorites xh-grouping-chooser__favorites--${favoritesSide}`,
341
403
  testId,
342
404
  items: [
343
- div({className: 'xh-popup__title', item: 'Favorites'}),
344
- menu({items}),
345
- button({
346
- text: 'Add current',
347
- icon: Icon.add({intent: 'success'}),
348
- className: 'xh-grouping-chooser__favorites__add-btn',
349
- outlined: true,
350
- omit: !isAddFavoriteEnabled,
351
- onClick: () => model.addPendingAsFavorite()
352
- })
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.')
353
415
  ]
354
416
  });
355
417
  }
@@ -372,3 +434,19 @@ const favoriteMenuItem = hoistCmp.factory<GroupingChooserModel>({
372
434
  });
373
435
  }
374
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "75.0.0-SNAPSHOT.1751313309623",
3
+ "version": "75.0.0-SNAPSHOT.1751426116456",
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",