@xh/hoist 72.0.0 → 72.1.0

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.
@@ -4,32 +4,62 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+
8
+ import * as Col from '@xh/hoist/admin/columns/Rest';
9
+ import * as AdminCol from '@xh/hoist/admin/columns';
7
10
  import {prefEditorDialog} from '@xh/hoist/admin/tabs/userData/prefs/editor/PrefEditorDialog';
8
11
  import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPreferenceModel';
12
+ import {hframe} from '@xh/hoist/cmp/layout';
9
13
  import {creates, hoistCmp} from '@xh/hoist/core';
10
14
  import {button} from '@xh/hoist/desktop/cmp/button';
15
+ import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearch';
11
16
  import {panel} from '@xh/hoist/desktop/cmp/panel';
12
17
  import {restGrid} from '@xh/hoist/desktop/cmp/rest';
18
+ import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
13
19
  import {Icon} from '@xh/hoist/icon';
14
20
 
15
21
  export const userPreferencePanel = hoistCmp.factory({
16
22
  model: creates(UserPreferenceModel),
17
23
 
18
24
  render({model}) {
19
- return panel({
20
- items: [
21
- restGrid({
22
- extraToolbarItems: () => {
23
- return button({
24
- icon: Icon.gear(),
25
- text: 'Configure',
26
- onClick: () => (model.showEditorDialog = true)
27
- });
28
- }
29
- }),
30
- prefEditorDialog()
31
- ],
32
- mask: 'onLoad'
33
- });
25
+ return hframe(
26
+ panel({
27
+ items: [
28
+ restGrid({
29
+ extraToolbarItems: () => [
30
+ button({
31
+ icon: Icon.gear(),
32
+ text: 'Configure',
33
+ onClick: () => (model.showEditorDialog = true)
34
+ }),
35
+ toolbarSep(),
36
+ jsonSearchButton({
37
+ subjectName: 'User Preference',
38
+ docSearchUrl: 'jsonSearch/searchUserPreferences',
39
+ gridModelConfig: {
40
+ sortBy: ['groupName', 'name', 'owner'],
41
+ columns: [
42
+ {
43
+ field: {name: 'owner', type: 'string'},
44
+ width: 200
45
+ },
46
+ {...AdminCol.groupName},
47
+ {...AdminCol.name},
48
+ {
49
+ field: {name: 'json', type: 'string'},
50
+ hidden: true
51
+ },
52
+ {...Col.lastUpdated}
53
+ ]
54
+ },
55
+ groupByOptions: ['owner', 'groupName', 'name']
56
+ })
57
+ ]
58
+ }),
59
+ prefEditorDialog()
60
+ ],
61
+ mask: 'onLoad'
62
+ })
63
+ );
34
64
  }
35
65
  });
@@ -80,7 +80,7 @@ export class RoleModel extends HoistModel {
80
80
  runInAction(() => {
81
81
  this.allRoles = this.processRolesFromServer(data);
82
82
  });
83
- this.displayRoles();
83
+ this.displayRoles(loadSpec.isRefresh);
84
84
  await this.gridModel.preSelectFirstAsync();
85
85
  } catch (e) {
86
86
  if (loadSpec.isStale) return;
@@ -212,13 +212,13 @@ export class RoleModel extends HoistModel {
212
212
  //------------------
213
213
  // Implementation
214
214
  //------------------
215
- private displayRoles() {
215
+ private displayRoles(isRefresh?: boolean) {
216
216
  const {gridModel} = this,
217
217
  gridData = this.showInGroups
218
218
  ? this.processRolesForTreeGrid(this.allRoles)
219
219
  : this.allRoles;
220
220
  gridModel.loadData(gridData);
221
- gridModel.expandAll();
221
+ if (!isRefresh) gridModel.expandAll();
222
222
  gridModel.autosizeAsync({includeCollapsedChildren: true});
223
223
  }
224
224
 
@@ -46,9 +46,9 @@ export class RoleFormModel extends HoistModel {
46
46
  @computed
47
47
  get hasDirtyMembers(): boolean {
48
48
  return (
49
- this.usersGridModel.store.isModified ||
50
- this.directoryGroupsGridModel.store.isModified ||
51
- this.rolesGridModel.store.isModified
49
+ this.usersGridModel.store.isDirty ||
50
+ this.directoryGroupsGridModel.store.isDirty ||
51
+ this.rolesGridModel.store.isDirty
52
52
  );
53
53
  }
54
54
 
@@ -0,0 +1,17 @@
1
+ import { GridConfig } from '@xh/hoist/cmp/grid';
2
+ import { HoistProps, SelectOption } from '@xh/hoist/core';
3
+ export interface JsonSearchButtonProps extends HoistProps {
4
+ /** Descriptive label for the type of records being searched - will be auto-pluralized. */
5
+ subjectName: string;
6
+ /** Endpoint to search and return matches - Hoist `JsonSearchController` action expected. */
7
+ docSearchUrl: string;
8
+ /** Config for GridModel used to display search results. */
9
+ gridModelConfig: GridConfig;
10
+ /** Field names on returned results to enable for grouping in the search results grid. */
11
+ groupByOptions: Array<SelectOption | any>;
12
+ }
13
+ /**
14
+ * Main entry point component for the JSON search feature. Supported out-of-the-box for a limited
15
+ * set of Hoist artifacts that hold JSON values: JSONBlob, Configs, and User Preferences.
16
+ */
17
+ export declare const jsonSearchButton: import("@xh/hoist/core").ElementFactory<JsonSearchButtonProps>;
@@ -0,0 +1,32 @@
1
+ import { GridConfig, GridModel } from '@xh/hoist/cmp/grid';
2
+ import { HoistModel, TaskObserver } from '@xh/hoist/core';
3
+ /**
4
+ * @internal
5
+ */
6
+ export declare class JsonSearchImplModel extends HoistModel {
7
+ xhImpl: boolean;
8
+ private matchingNodesUrl;
9
+ gridModel: GridModel;
10
+ docLoadTask: TaskObserver;
11
+ nodeLoadTask: TaskObserver;
12
+ groupBy: string;
13
+ isOpen: boolean;
14
+ error: any;
15
+ path: string;
16
+ readerContentType: 'document' | 'matches';
17
+ pathFormat: 'XPath' | 'JSONPath';
18
+ readerContent: string;
19
+ matchingNodeCount: number;
20
+ get subjectName(): string;
21
+ get docSearchUrl(): string;
22
+ get gridModelConfig(): GridConfig;
23
+ get selectedRecord(): import("../../../data").StoreRecord;
24
+ get groupByOptions(): any[];
25
+ toggleSearchIsOpen(): void;
26
+ constructor();
27
+ onLinked(): void;
28
+ loadMatchingDocsAsync(): Promise<void>;
29
+ private loadReaderContentAsync;
30
+ private convertToXPath;
31
+ private setGroupBy;
32
+ }
@@ -41,6 +41,10 @@ export declare abstract class PersistenceProvider<S = any> {
41
41
  * target without thrashing.
42
42
  */
43
43
  static create<S>(cfg: PersistenceProviderConfig<S>): PersistenceProvider<S>;
44
+ /**
45
+ * Merge PersistOptions, respecting provider types, with later options overriding earlier ones.
46
+ */
47
+ static mergePersistOptions(defaults: PersistOptions, ...overrides: PersistOptions[]): PersistOptions;
44
48
  /** Read persisted state at this provider's path. */
45
49
  read(): PersistableState<S>;
46
50
  /** Persist JSON-serializable state to this provider's path. */
@@ -275,6 +275,8 @@ export declare class Store extends HoistBase {
275
275
  /** Records removed locally which have not been committed.*/
276
276
  get removedRecords(): StoreRecord[];
277
277
  /** Records modified locally since they were last loaded. */
278
+ get dirtyRecords(): StoreRecord[];
279
+ /** Alias for {@link Store.dirtyRecords} */
278
280
  get modifiedRecords(): StoreRecord[];
279
281
  /**
280
282
  * Root records in this store, respecting any filter (if applied).
@@ -292,6 +294,8 @@ export declare class Store extends HoistBase {
292
294
  */
293
295
  get summaryRecord(): StoreRecord;
294
296
  /** True if the store has changes which need to be committed. */
297
+ get isDirty(): boolean;
298
+ /** Alias for {@link Store.isDirty} */
295
299
  get isModified(): boolean;
296
300
  /**
297
301
  * Set a filter on this store.
@@ -51,6 +51,8 @@ export declare class StoreRecord {
51
51
  /** True if the StoreRecord has never been committed. */
52
52
  get isAdd(): boolean;
53
53
  /** True if the StoreRecord has been modified since it was last committed. */
54
+ get isDirty(): boolean;
55
+ /** Alias for {@link StoreRecord.isDirty} */
54
56
  get isModified(): boolean;
55
57
  /** False if the StoreRecord has been added or modified. */
56
58
  get isCommitted(): boolean;
@@ -415,7 +415,9 @@ export class FilterChooserModel extends HoistModel {
415
415
  }: FilterChooserPersistOptions) {
416
416
  if (persistValue) {
417
417
  const status = {initialized: false},
418
- persistWith = isObject(persistValue) ? persistValue : rootPersistWith;
418
+ persistWith = isObject(persistValue)
419
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistValue)
420
+ : rootPersistWith;
419
421
  PersistenceProvider.create({
420
422
  persistOptions: {
421
423
  path: `${path}.value`,
@@ -432,7 +434,9 @@ export class FilterChooserModel extends HoistModel {
432
434
  }
433
435
 
434
436
  if (persistFavorites) {
435
- const persistWith = isObject(persistFavorites) ? persistFavorites : rootPersistWith,
437
+ const persistWith = isObject(persistFavorites)
438
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistFavorites)
439
+ : rootPersistWith,
436
440
  provider = PersistenceProvider.create({
437
441
  persistOptions: {
438
442
  path: `${path}.favorites`,
@@ -24,7 +24,9 @@ export function initPersist(
24
24
  }: GridModelPersistOptions
25
25
  ) {
26
26
  if (persistColumns) {
27
- const persistWith = isObject(persistColumns) ? persistColumns : rootPersistWith;
27
+ const persistWith = isObject(persistColumns)
28
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistColumns)
29
+ : rootPersistWith;
28
30
  PersistenceProvider.create({
29
31
  persistOptions: {
30
32
  path: `${path}.columns`,
@@ -39,7 +41,9 @@ export function initPersist(
39
41
  }
40
42
 
41
43
  if (persistSort) {
42
- const persistWith = isObject(persistSort) ? persistSort : rootPersistWith;
44
+ const persistWith = isObject(persistSort)
45
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistSort)
46
+ : rootPersistWith;
43
47
  PersistenceProvider.create({
44
48
  persistOptions: {
45
49
  path: `${path}.sortBy`,
@@ -55,7 +59,9 @@ export function initPersist(
55
59
  }
56
60
 
57
61
  if (persistGrouping) {
58
- const persistWith = isObject(persistSort) ? persistSort : rootPersistWith;
62
+ const persistWith = isObject(persistGrouping)
63
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistGrouping)
64
+ : rootPersistWith;
59
65
  PersistenceProvider.create({
60
66
  persistOptions: {
61
67
  path: `${path}.groupBy`,
@@ -293,7 +293,9 @@ export class GroupingChooserModel extends HoistModel {
293
293
  ...rootPersistWith
294
294
  }: GroupingChooserPersistOptions) {
295
295
  if (persistValue) {
296
- const persistWith = isObject(persistValue) ? persistValue : rootPersistWith;
296
+ const persistWith = isObject(persistValue)
297
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistValue)
298
+ : rootPersistWith;
297
299
  PersistenceProvider.create({
298
300
  persistOptions: {
299
301
  path: `${path}.value`,
@@ -308,7 +310,9 @@ export class GroupingChooserModel extends HoistModel {
308
310
  }
309
311
 
310
312
  if (persistFavorites) {
311
- const persistWith = isObject(persistFavorites) ? persistFavorites : rootPersistWith,
313
+ const persistWith = isObject(persistFavorites)
314
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistFavorites)
315
+ : rootPersistWith,
312
316
  provider = PersistenceProvider.create({
313
317
  persistOptions: {
314
318
  path: `${path}.favorites`,
@@ -24,7 +24,9 @@ export function initPersist(
24
24
  }: ZoneGridModelPersistOptions
25
25
  ) {
26
26
  if (persistMappings) {
27
- const persistWith = isObject(persistMappings) ? persistMappings : rootPersistWith;
27
+ const persistWith = isObject(persistMappings)
28
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistMappings)
29
+ : rootPersistWith;
28
30
  PersistenceProvider.create({
29
31
  persistOptions: {
30
32
  path: `${path}.mappings`,
@@ -39,7 +41,9 @@ export function initPersist(
39
41
  }
40
42
 
41
43
  if (persistGrouping) {
42
- const persistWith = isObject(persistGrouping) ? persistGrouping : rootPersistWith;
44
+ const persistWith = isObject(persistGrouping)
45
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistGrouping)
46
+ : rootPersistWith;
43
47
  PersistenceProvider.create({
44
48
  persistOptions: {
45
49
  path: `${path}.groupBy`,
@@ -54,7 +58,9 @@ export function initPersist(
54
58
  }
55
59
 
56
60
  if (persistSort) {
57
- const persistWith = isObject(persistSort) ? persistSort : rootPersistWith;
61
+ const persistWith = isObject(persistSort)
62
+ ? PersistenceProvider.mergePersistOptions(rootPersistWith, persistSort)
63
+ : rootPersistWith;
58
64
  PersistenceProvider.create({
59
65
  persistOptions: {
60
66
  path: `${path}.sortBy`,
package/core/HoistBase.ts CHANGED
@@ -261,8 +261,7 @@ export abstract class HoistBase {
261
261
  PersistenceProvider.create({
262
262
  persistOptions: {
263
263
  path: property,
264
- ...this.persistWith,
265
- ...options
264
+ ...PersistenceProvider.mergePersistOptions(this.persistWith, options)
266
265
  },
267
266
  owner: this,
268
267
  target: {
@@ -81,7 +81,10 @@ function createPersistDescriptor(
81
81
  // codeValue undefined if no initial in-code value provided, otherwise call to get initial value.
82
82
  ret = codeValue?.call(this);
83
83
 
84
- const persistOptions = {path: property, ...this.persistWith, ...options};
84
+ const persistOptions = {
85
+ path: property,
86
+ ...PersistenceProvider.mergePersistOptions(this.persistWith, options)
87
+ };
85
88
  PersistenceProvider.create({
86
89
  persistOptions,
87
90
  owner: this,
@@ -8,12 +8,14 @@
8
8
  import {logDebug, logError, throwIf} from '@xh/hoist/utils/js';
9
9
  import {
10
10
  cloneDeep,
11
+ compact,
11
12
  debounce as lodashDebounce,
12
13
  get,
13
14
  isEmpty,
14
15
  isNumber,
15
16
  isString,
16
17
  isUndefined,
18
+ omit,
17
19
  set,
18
20
  toPath
19
21
  } from 'lodash';
@@ -91,6 +93,35 @@ export abstract class PersistenceProvider<S = any> {
91
93
  }
92
94
  }
93
95
 
96
+ /**
97
+ * Merge PersistOptions, respecting provider types, with later options overriding earlier ones.
98
+ */
99
+ static mergePersistOptions(
100
+ defaults: PersistOptions,
101
+ ...overrides: PersistOptions[]
102
+ ): PersistOptions {
103
+ const TYPE_RELATED_KEYS = [
104
+ 'type',
105
+ 'prefKey',
106
+ 'localStorageKey',
107
+ 'sessionStorageKey',
108
+ 'dashViewModel',
109
+ 'viewManagerModel',
110
+ 'getData',
111
+ 'setData'
112
+ ];
113
+ return compact(overrides).reduce(
114
+ (ret, override) =>
115
+ TYPE_RELATED_KEYS.some(key => override[key])
116
+ ? {
117
+ ...omit(ret, ...TYPE_RELATED_KEYS),
118
+ ...override
119
+ }
120
+ : {...ret, ...override},
121
+ defaults
122
+ );
123
+ }
124
+
94
125
  /** Read persisted state at this provider's path. */
95
126
  read(): PersistableState<S> {
96
127
  const state = get(this.readRaw(), this.path);
package/data/Store.ts CHANGED
@@ -435,7 +435,7 @@ export class Store extends HoistBase {
435
435
  // sourced from the server / source of record and are coming in as committed.
436
436
  this._committed = this._committed.withTransaction(rsTransaction);
437
437
 
438
- if (this.isModified) {
438
+ if (this.isDirty) {
439
439
  // If this store had pre-existing local modifications, apply the updates over that
440
440
  // local state. This might (or might not) effectively overwrite those local changes,
441
441
  // so we normalize against the newly updated committed state to verify if any local
@@ -662,8 +662,13 @@ export class Store extends HoistBase {
662
662
  }
663
663
 
664
664
  /** Records modified locally since they were last loaded. */
665
+ get dirtyRecords(): StoreRecord[] {
666
+ return this.allRecords.filter(it => it.isDirty);
667
+ }
668
+
669
+ /** Alias for {@link Store.dirtyRecords} */
665
670
  get modifiedRecords(): StoreRecord[] {
666
- return this.allRecords.filter(it => it.isModified);
671
+ return this.dirtyRecords;
667
672
  }
668
673
 
669
674
  /**
@@ -696,10 +701,15 @@ export class Store extends HoistBase {
696
701
 
697
702
  /** True if the store has changes which need to be committed. */
698
703
  @computed
699
- get isModified(): boolean {
704
+ get isDirty(): boolean {
700
705
  return this._current !== this._committed;
701
706
  }
702
707
 
708
+ /** Alias for {@link Store.isDirty} */
709
+ get isModified(): boolean {
710
+ return this.isDirty;
711
+ }
712
+
703
713
  /**
704
714
  * Set a filter on this store.
705
715
  *
@@ -71,10 +71,15 @@ export class StoreRecord {
71
71
  }
72
72
 
73
73
  /** True if the StoreRecord has been modified since it was last committed. */
74
- get isModified(): boolean {
74
+ get isDirty(): boolean {
75
75
  return this.committedData && this.committedData !== this.data;
76
76
  }
77
77
 
78
+ /** Alias for {@link StoreRecord.isDirty} */
79
+ get isModified(): boolean {
80
+ return this.isDirty;
81
+ }
82
+
78
83
  /** False if the StoreRecord has been added or modified. */
79
84
  get isCommitted(): boolean {
80
85
  return this.committedData === this.data;
@@ -7,7 +7,7 @@
7
7
 
8
8
  import {PlainObject, Some} from '@xh/hoist/core';
9
9
  import {BucketSpec} from '@xh/hoist/data/cube/BucketSpec';
10
- import {isEmpty, reduce} from 'lodash';
10
+ import {compact, isEmpty, reduce} from 'lodash';
11
11
  import {View} from '../View';
12
12
  import {RowUpdate} from './RowUpdate';
13
13
 
@@ -97,7 +97,8 @@ export abstract class BaseRow {
97
97
  }
98
98
 
99
99
  // Recurse
100
- return children.flatMap(it => it.getVisibleDatas());
100
+ const ret = compact(children.flatMap(it => it.getVisibleDatas()));
101
+ return !isEmpty(ret) ? ret : null;
101
102
  }
102
103
 
103
104
  private isRedundantChild(parent: any, child: any) {
@@ -149,6 +149,7 @@ export class NavigatorModel extends HoistModel {
149
149
  // to propagate to scrollable elements within the page.
150
150
  swiper.on('touchStart', (s, event: PointerEvent) => {
151
151
  swiper.allowTouchMove = false;
152
+ swiper.params.shortSwipes = true;
152
153
  this._touchStartX = event.pageX;
153
154
  });
154
155
 
@@ -175,6 +176,12 @@ export class NavigatorModel extends HoistModel {
175
176
  swiper.allowTouchMove =
176
177
  swiper.progress < 1 || !isDraggableEl(scrollableParent, 'right');
177
178
 
179
+ // Disable short swipes to prevent accidental navigation when reaching the
180
+ // end of the scrollable parent.
181
+ if (!swiper.allowTouchMove) {
182
+ swiper.params.shortSwipes = false;
183
+ }
184
+
178
185
  // During the swiper transition, undo the scrollable parent's internal scroll
179
186
  // to keep it static.
180
187
  if (swiper.progress < 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "72.0.0",
3
+ "version": "72.1.0",
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",