@xh/hoist 77.0.0-SNAPSHOT.1760631134539 → 77.0.0-SNAPSHOT.1760707577281

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
@@ -4,60 +4,53 @@
4
4
 
5
5
  ### 🎁 New Features
6
6
 
7
- * `DashCanvasView` and `DashContainerView` components now have a public `@bindable` `titleDetails`
8
- property on their models to support displaying additional information in the title bar of these
9
- components. `titleDetails` is not persisted, and is expected to be set programmatically by the
10
- application as needed.
7
+ * Added a public `@bindable titleDetails` config to `DashViewModel` to support displaying additional
8
+ information in the title bar of dashboard widgets. The new property is not persisted, allowing
9
+ apps to programmatically show dynamic info in a widget header without perturbing its saved state.
10
+ * Enhanced grid column filtering to support sorting the list of available values.
11
11
 
12
12
  ## 76.0.0 - 2025-09-26
13
13
 
14
14
  ### 💥 Breaking Changes (upgrade difficulty: 🟠 MEDIUM - AG Grid update, Hoist React upgrade)
15
15
 
16
- * Hoist v76 upgrades AG Grid to v34 (from v31), covering three major AG Grid releases with their own
17
- potentially breaking changes. Fortunately, internal Hoist updates to our managed API wrappers mean
18
- that most apps will see very minimal changes, although there are required adjustments to app-level
19
- `package.json` to install updated grid dependencies and `Bootstrap.ts` to import and register
20
- your licensed grid modules at their new import paths.
21
-
22
- Applications implementing `groupRowRenderer` should note that the `value` property passed to this
23
- function is no longer stringified, but is instead the raw field value for the group.
24
-
25
- See AG's upgrade guides for more details:
26
- ** [Upgrade to v32](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-32/)
27
- ** [Upgrade to v33](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-33/)
28
- ** [Upgrade to v34](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-34/)
29
-
30
- * The constructor for `TabModel` has changed to take its owning container as a second argument.
31
- (Most applications do not create `TabModels` directly, but it is possible.)
32
- * The `Exception` class and `HoistException` type have been moved from `@xh\hoist\core` to a new
33
- lower level package `@xh\hoist\exception`. This new structure is not expected to effect most
34
- applications, and was put in place to reduce the risk of circular dependencies between internal
35
- hoist packages.
36
-
37
- ### 🎁 New Features
38
-
39
- * Added new `extraConfirmText`, `extraConfirmLabel` properties to `MessageOptions`. Use this option
16
+ * Hoist v76 **upgrades AG Grid to v34** (from v31), covering three major AG Grid releases with their
17
+ own potentially breaking changes.
18
+ * Fortunately, internal Hoist updates to our managed API wrappers mean that most apps will see
19
+ very minimal changes, although there are required adjustments to app-level `package.json` to
20
+ install updated grid dependencies and `Bootstrap.ts` to import and register your licensed grid
21
+ modules at their new import paths.
22
+ * Applications implementing `groupRowRenderer` should note that the `value` property passed
23
+ to this function is no longer stringified, but is instead the raw field value for the group.
24
+ * See AG's upgrade guides for more details:
25
+ * [Upgrade to v32](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-32/)
26
+ * [Upgrade to v33](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-33/)
27
+ * [Upgrade to v34](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-34/)
28
+ * Modified the `TabModel` constructor to take its owning container as a second argument.
29
+ * Apps very rarely create `TabModels` directly, so this unlikely to require changes.
30
+ * Moved the `Exception` class and `HoistException` type from `@xh\hoist\core` to a new lower-level
31
+ package `@xh\hoist\exception` to reduce the risk of circule dependencies within Hoist.
32
+ * Apps rarely interact with these directly, so also unlikely to require changes.
33
+
34
+ ### 🎁 New Features
35
+
36
+ * Added `extraConfirmText` + `extraConfirmLabel` configs to `MessageOptions`. Use these new options
40
37
  to require the specified text to be re-typed by a user when confirming a potentially destructive
41
- or disruptive action.
42
- * Updated grid column filters to apply on `Enter` / dismiss on `Esc` and tweaked the filter popup
38
+ or disruptive action. Note their usage within Hoist's Admin Console when deleting a role.
39
+ * Updated grid column filters to apply on `Enter` / dismiss on `Esc`. Tweaked the filter popup
43
40
  toolbar for clarity.
44
- * Added new ability to specify nested tab containers in a single declarative config. Apps may now
41
+ * Added new ability to specify nested tab containers in a single declarative config. Apps may now
45
42
  provide a spec for a nested tab container directly to the `TabConfig.content` property.
46
- * Improvements to View Management:
47
- ** Allow users to create 'Global' views directly in 'Save/Save As' Dialog.
48
- ** Simplify presentation/edit of view visibility to new "Visibility" control
49
- ** Support for the 'isDefaultPinned' attribute on global views has been removed. All global
50
- views will be pinned by default. This feature was deemed too confusing, and not useful in
51
- practice. App maintainers should ensure that all global views are appropriate and well
52
- organized enough to be shown immediately to new users in the view menu.
53
- * New constraint rule: `validEmails` - to validate one or more email addresses in an input field.
54
- * `DashCanvas` accepts a new prop `rglOptions` to pass additional options to the underlying
55
- `react-grid-layout`.
56
- * Experimental grid feature `enableFullWidthScroll` has been promoted to a first-class property
57
- on `GridModel`. Set to true to ensure that the grid will have a single horizontal scrollbar
58
- spanning the width of all columns, including any pinned columns.
59
- * New `@sharePendingPromise` decorator for returning a shared Promise across concurrent async calls.
60
-
43
+ * Improved `ViewManager` features:
44
+ * Enabled globally sharing a new view directly from the 'Save/Save As' dialog.
45
+ * Simplified presentation and management of view visibility via new "Visibility" control.
46
+ * Removed support for the `isDefaultPinned` attribute on global views. All global views will be
47
+ pinned (i.e. show up in user menus) by default. Users can still explicitly "unpin" any global
48
+ views to remove them from their menus.
49
+ * Added a `validEmails` constraint rule to validate one or more email addresses in an input field.
50
+ * Added `DashCanvas.rglOptions` prop - passed through to the underlying `react-grid-layout`.
51
+ * Promoted experimental grid feature `enableFullWidthScroll` to a first-class `GridModel` config.
52
+ Set to true to ensure that the grid will have a single horizontal scrollbar spanning the width of
53
+ all columns, including any pinned columns.
61
54
 
62
55
  ### 🐞 Bug Fixes
63
56
 
@@ -73,6 +66,9 @@
73
66
 
74
67
  ### ⚙️ Technical
75
68
 
69
+ * Added a new `@sharePendingPromise` decorator for returning a shared Promise across concurrent
70
+ async calls. Calls made to a decorated method while a prior call with the same args is still
71
+ pending won't kick off a new call, but will instead receive the same Promise as the first call.
76
72
  * Added `XH.logLevel` to define a minimum logging severity threshold for Hoist's client-side logging
77
73
  utilities. Defaulted to 'info' to prevent possible memory and performance impacts of verbose
78
74
  logging on 'debug'. Change at runtime via new `XH.setLogLevel()` when troubleshooting. See
@@ -1,10 +1,10 @@
1
+ import { Column, GridFilterFieldSpec, GridFilterModel, GridModel } from '@xh/hoist/cmp/grid';
1
2
  import { TabContainerModel } from '@xh/hoist/cmp/tab';
2
3
  import { HoistModel } from '@xh/hoist/core';
3
4
  import { CompoundFilter, FieldFilter, FieldType, Filter, FilterLike, Store } from '@xh/hoist/data';
4
- import { GridFilterFieldSpec, GridFilterModel } from '@xh/hoist/cmp/grid';
5
+ import { ColumnHeaderFilterModel } from '../ColumnHeaderFilterModel';
5
6
  import { CustomTabModel } from './custom/CustomTabModel';
6
7
  import { ValuesTabModel } from './values/ValuesTabModel';
7
- import { ColumnHeaderFilterModel } from '../ColumnHeaderFilterModel';
8
8
  export declare class HeaderFilterModel extends HoistModel {
9
9
  xhImpl: boolean;
10
10
  fieldSpec: GridFilterFieldSpec;
@@ -14,7 +14,9 @@ export declare class HeaderFilterModel extends HoistModel {
14
14
  customTabModel: CustomTabModel;
15
15
  get filterModel(): GridFilterModel;
16
16
  get field(): string;
17
+ get gridModel(): GridModel;
17
18
  get store(): Store;
19
+ get column(): Column;
18
20
  get fieldType(): FieldType;
19
21
  get currentGridFilter(): Filter;
20
22
  get columnFilters(): FieldFilter[];
@@ -5,12 +5,16 @@ import { HeaderFilterModel } from '../HeaderFilterModel';
5
5
  export declare class ValuesTabModel extends HoistModel {
6
6
  xhImpl: boolean;
7
7
  headerFilterModel: HeaderFilterModel;
8
- /** Checkbox grid to display enumerated set of values */
8
+ /** Checkbox grid to display enumerated set of values. */
9
9
  gridModel: GridModel;
10
- /** List of currently checked values in the list*/
10
+ /** List of currently checked values. */
11
11
  pendingValues: any[];
12
- /** Bound search term for `StoreFilterField` */
12
+ /** Bound search term for `StoreFilterField`. */
13
13
  filterText: string;
14
+ /**
15
+ * Merge current filter with pendingValues on commit.
16
+ * Used when commitOnChange is false.
17
+ */
14
18
  combineCurrentFilters: boolean;
15
19
  /** FieldFilter output by this model. */
16
20
  get filter(): FieldFilterSpec;
@@ -22,15 +26,18 @@ export declare class ValuesTabModel extends HoistModel {
22
26
  get values(): any[];
23
27
  get valueCount(): number;
24
28
  get hasHiddenValues(): boolean;
29
+ get sortIcon(): any;
25
30
  constructor(headerFilterModel: HeaderFilterModel);
26
31
  syncWithFilter(): void;
27
32
  reset(): void;
28
33
  setRecsChecked(isChecked: boolean, values: any[]): void;
34
+ toggleSort(): void;
29
35
  toggleAllRecsChecked(): void;
30
36
  private onFilterTextChange;
31
37
  private onCombineCurrentFiltersToggle;
32
38
  private getFilter;
33
39
  private doSyncWithFilter;
34
40
  private syncGrid;
41
+ private initGridSortBy;
35
42
  private createGridModel;
36
43
  }
@@ -54,7 +54,6 @@ const bbar = hoistCmp.factory<HeaderFilterModel>({
54
54
  render({model}) {
55
55
  const {commitOnChange, hasFilter, isDirty} = model;
56
56
  return toolbar({
57
- compact: true,
58
57
  items: [
59
58
  button({
60
59
  icon: Icon.delete(),
@@ -5,8 +5,9 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
+ import {Column, GridFilterFieldSpec, GridFilterModel, GridModel} from '@xh/hoist/cmp/grid';
8
9
  import {TabContainerModel} from '@xh/hoist/cmp/tab';
9
- import {HoistModel, managed, lookup} from '@xh/hoist/core';
10
+ import {HoistModel, lookup, managed} from '@xh/hoist/core';
10
11
  import {
11
12
  CompoundFilter,
12
13
  FieldFilter,
@@ -19,12 +20,11 @@ import {
19
20
  import {action, computed} from '@xh/hoist/mobx';
20
21
  import {wait} from '@xh/hoist/promise';
21
22
  import {isEmpty} from 'lodash';
22
- import {GridFilterFieldSpec, GridFilterModel} from '@xh/hoist/cmp/grid';
23
+ import {ColumnHeaderFilterModel} from '../ColumnHeaderFilterModel';
23
24
  import {customTab} from './custom/CustomTab';
24
25
  import {CustomTabModel} from './custom/CustomTabModel';
25
26
  import {valuesTab} from './values/ValuesTab';
26
27
  import {ValuesTabModel} from './values/ValuesTabModel';
27
- import {ColumnHeaderFilterModel} from '../ColumnHeaderFilterModel';
28
28
 
29
29
  export class HeaderFilterModel extends HoistModel {
30
30
  override xhImpl = true;
@@ -46,8 +46,16 @@ export class HeaderFilterModel extends HoistModel {
46
46
  return this.fieldSpec.field;
47
47
  }
48
48
 
49
+ get gridModel(): GridModel {
50
+ return this.filterModel.gridModel;
51
+ }
52
+
49
53
  get store(): Store {
50
- return this.filterModel.gridModel.store;
54
+ return this.gridModel.store;
55
+ }
56
+
57
+ get column(): Column {
58
+ return this.parent.column;
51
59
  }
52
60
 
53
61
  get fieldType(): FieldType {
@@ -1,24 +1,32 @@
1
1
  .xh-values-filter-tab {
2
- .store-filter-header {
3
- padding: 5px 7px;
2
+ &__filter-controls {
4
3
  border-bottom: 1px solid var(--xh-grid-header-border-color);
4
+ padding: 5px 7px;
5
5
  row-gap: 5px;
6
+
6
7
  .bp5-control-indicator {
7
8
  font-size: 1em;
8
9
  }
10
+
9
11
  label {
10
12
  font-size: var(--xh-grid-compact-header-font-size-px);
11
13
  color: var(--xh-grid-header-text-color);
12
14
  cursor: pointer;
13
15
  }
16
+
17
+ &__sort-icon {
18
+ border-left: var(--xh-menu-border);
19
+ padding-left: var(--xh-pad-half-px);
20
+ color: var(--xh-grid-header-text-color);
21
+ }
14
22
  }
15
23
 
16
24
  &__hidden-values-message {
17
- display: flex;
18
- padding: var(--xh-pad-half-px);
19
25
  background-color: var(--xh-bg-alt);
20
26
  border-top: var(--xh-border-solid);
21
27
  color: var(--xh-text-color-muted);
28
+ display: flex;
29
+ padding: var(--xh-pad-half-px);
22
30
 
23
31
  .xh-icon {
24
32
  margin-right: var(--xh-pad-half-px);
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {isEmpty} from 'lodash';
8
8
  import {grid} from '@xh/hoist/cmp/grid';
9
- import {div, hframe, placeholder, label, vbox, vframe} from '@xh/hoist/cmp/layout';
9
+ import {div, hframe, placeholder, label, vbox, vframe, filler} from '@xh/hoist/cmp/layout';
10
10
  import {storeFilterField} from '@xh/hoist/cmp/store';
11
11
  import {XH, hoistCmp, uses} from '@xh/hoist/core';
12
12
  import {button} from '@xh/hoist/desktop/cmp/button';
@@ -60,7 +60,7 @@ const storeFilterSelect = hoistCmp.factory<ValuesTabModel>(({model}) => {
60
60
  addToFilterId = XH.genId();
61
61
 
62
62
  return vbox({
63
- className: 'store-filter-header',
63
+ className: 'xh-values-filter-tab__filter-controls',
64
64
  items: [
65
65
  hframe(
66
66
  checkbox({
@@ -73,6 +73,12 @@ const storeFilterSelect = hoistCmp.factory<ValuesTabModel>(({model}) => {
73
73
  label({
74
74
  htmlFor: selectAllId,
75
75
  item: `(Select All${filterText ? ' Search Results' : ''})`
76
+ }),
77
+ filler(),
78
+ div({
79
+ className: 'xh-values-filter-tab__filter-controls__sort-icon',
80
+ item: model.sortIcon,
81
+ onClick: () => model.toggleSort()
76
82
  })
77
83
  ),
78
84
  hframe({
@@ -7,26 +7,27 @@
7
7
  import {GridFilterModel, GridModel} from '@xh/hoist/cmp/grid';
8
8
  import {HoistModel, managed} from '@xh/hoist/core';
9
9
  import {FieldFilterSpec} from '@xh/hoist/data';
10
- import {HeaderFilterModel} from '../HeaderFilterModel';
11
10
  import {checkbox} from '@xh/hoist/desktop/cmp/input';
11
+ import {Icon} from '@xh/hoist/icon';
12
12
  import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
13
13
  import {castArray, difference, flatten, isEmpty, map, partition, uniq, without} from 'lodash';
14
+ import {HeaderFilterModel} from '../HeaderFilterModel';
14
15
 
15
16
  export class ValuesTabModel extends HoistModel {
16
17
  override xhImpl = true;
17
18
 
18
19
  headerFilterModel: HeaderFilterModel;
19
20
 
20
- /** Checkbox grid to display enumerated set of values */
21
- @managed @observable.ref gridModel: GridModel;
21
+ /** Checkbox grid to display enumerated set of values. */
22
+ @managed gridModel: GridModel;
22
23
 
23
- /** List of currently checked values in the list*/
24
+ /** List of currently checked values. */
24
25
  @observable.ref pendingValues: any[] = [];
25
26
 
26
- /** Bound search term for `StoreFilterField` */
27
+ /** Bound search term for `StoreFilterField`. */
27
28
  @bindable filterText: string = null;
28
29
 
29
- /*
30
+ /**
30
31
  * Merge current filter with pendingValues on commit.
31
32
  * Used when commitOnChange is false.
32
33
  */
@@ -80,12 +81,26 @@ export class ValuesTabModel extends HoistModel {
80
81
  return this.values.length < this.valueCount;
81
82
  }
82
83
 
84
+ get sortIcon() {
85
+ const {sort, abs} = this.gridModel.sortBy[0];
86
+ if (sort === 'asc') {
87
+ if (abs) return Icon.sortAbsAsc();
88
+ return Icon.sortAsc();
89
+ }
90
+ if (sort === 'desc') {
91
+ if (abs) return Icon.sortAbsDesc();
92
+ return Icon.sortDesc();
93
+ }
94
+ return null;
95
+ }
96
+
83
97
  constructor(headerFilterModel: HeaderFilterModel) {
84
98
  super();
85
99
  makeObservable(this);
86
100
 
87
101
  this.headerFilterModel = headerFilterModel;
88
102
  this.gridModel = this.createGridModel();
103
+ this.initGridSortBy();
89
104
 
90
105
  this.addReaction(
91
106
  {
@@ -125,6 +140,13 @@ export class ValuesTabModel extends HoistModel {
125
140
  : without(this.pendingValues, ...values);
126
141
  }
127
142
 
143
+ @action
144
+ toggleSort() {
145
+ const {colId, sort, abs} = this.gridModel.sortBy.find(it => it.colId === 'value'),
146
+ newSort = sort === 'asc' ? 'desc' : 'asc';
147
+ this.gridModel.setSortBy({colId, sort: newSort, abs});
148
+ }
149
+
128
150
  toggleAllRecsChecked() {
129
151
  const setAllToChecked = !this.allVisibleRecsChecked,
130
152
  values = this.gridModel.store.records.map(it => it.get('value'));
@@ -244,13 +266,22 @@ export class ValuesTabModel extends HoistModel {
244
266
  this.gridModel.loadData(data);
245
267
  }
246
268
 
269
+ private initGridSortBy() {
270
+ const {gridModel: srcGridModel, column} = this.headerFilterModel,
271
+ srcColGridSorter = srcGridModel.sortBy.find(it => it.colId === column.colId);
272
+
273
+ this.gridModel.setSortBy({
274
+ colId: 'value',
275
+ sort: srcColGridSorter?.sort ?? 'asc',
276
+ abs: srcColGridSorter?.abs ?? false
277
+ });
278
+ }
279
+
247
280
  private createGridModel() {
248
281
  const {BLANK_PLACEHOLDER} = GridFilterModel,
249
282
  {headerFilterModel, fieldSpec} = this,
250
- {fieldType} = headerFilterModel,
251
- renderer =
252
- fieldSpec.renderer ??
253
- (fieldType !== 'tags' ? this.headerFilterModel.parent.column.renderer : null);
283
+ {fieldType, column} = headerFilterModel,
284
+ renderer = fieldSpec.renderer ?? (fieldType !== 'tags' ? column.renderer : null);
254
285
 
255
286
  return new GridModel({
256
287
  store: {
@@ -301,6 +332,7 @@ export class ValuesTabModel extends HoistModel {
301
332
  {
302
333
  field: 'value',
303
334
  align: 'left',
335
+ tooltip: true,
304
336
  comparator: (v1, v2, sortDir, abs, {defaultComparator}) => {
305
337
  const mul = sortDir === 'desc' ? -1 : 1;
306
338
  if (v1 === BLANK_PLACEHOLDER) return 1 * mul;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "77.0.0-SNAPSHOT.1760631134539",
3
+ "version": "77.0.0-SNAPSHOT.1760707577281",
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",
package/styles/vars.scss CHANGED
@@ -522,8 +522,8 @@ body {
522
522
  --xh-zone-grid-label-color: var(--zone-grid-label-color, inherit);
523
523
 
524
524
  // Grid column-header-based filter popover (desktop only)
525
- --xh-grid-filter-popover-height-px: var(--grid-filter-popover-height-px, 350px);
526
- --xh-grid-filter-popover-width-px: var(--grid-filter-popover-width-px, 240px);
525
+ --xh-grid-filter-popover-height-px: var(--grid-filter-popover-height-px, 400px);
526
+ --xh-grid-filter-popover-width-px: var(--grid-filter-popover-width-px, 280px);
527
527
 
528
528
  // Dark Grid
529
529
  &.xh-dark {