@xh/hoist 74.0.0-SNAPSHOT.1748626008777 → 74.0.0-SNAPSHOT.1748890930922
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 +13 -0
- package/build/types/cmp/grid/GridModel.d.ts +3 -1
- package/build/types/cmp/grid/columns/Column.d.ts +7 -0
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +20 -16
- package/build/types/core/persist/PersistOptions.d.ts +4 -0
- package/build/types/core/persist/PersistenceProvider.d.ts +3 -0
- package/cmp/grid/GridModel.ts +3 -1
- package/cmp/grid/columns/Column.ts +12 -1
- package/cmp/grid/impl/InitPersist.ts +13 -3
- package/cmp/viewmanager/ViewManagerModel.ts +41 -28
- package/core/persist/PersistOptions.ts +5 -0
- package/core/persist/PersistenceProvider.ts +17 -3
- package/core/persist/provider/ViewManagerProvider.ts +2 -6
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +1 -0
- package/desktop/cmp/dash/container/DashContainerModel.ts +1 -0
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## 74.0.0-SNAPSHOT - unreleased
|
|
4
4
|
|
|
5
|
+
### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - minor change to ViewManagerModel)
|
|
6
|
+
|
|
7
|
+
* Removed `ViewManagerModel.settleTime`. Now set via individual `PersistOptions.settleTime` instead.
|
|
8
|
+
|
|
9
|
+
### 🎁 New Features
|
|
10
|
+
* Added `ViewManagerModel.preserveUnsavedChanges` flag to opt-out of that behaviour.
|
|
11
|
+
* Added `PersistOptions.settleTime` to configure time to wait for state to settle before persisting.
|
|
12
|
+
* Support for gridcolumn level `onCellClicked` events.
|
|
13
|
+
|
|
14
|
+
### 🐞 Bug Fixes
|
|
15
|
+
* Improved `ViewManagerModel.settleTime` by delegating to individual `PersistenceProviders`.
|
|
16
|
+
* Fixed bug where grid column state could become unintentionally dirty when columns were hidden.
|
|
17
|
+
|
|
5
18
|
## v73.0.1 - 2025-05-19
|
|
6
19
|
|
|
7
20
|
### 🐞 Bug Fixes
|
|
@@ -120,7 +120,9 @@ export interface GridConfig {
|
|
|
120
120
|
*/
|
|
121
121
|
onRowDoubleClicked?: (e: RowDoubleClickedEvent) => void;
|
|
122
122
|
/**
|
|
123
|
-
* Callback when
|
|
123
|
+
* Callback when any cell on the grid is clicked - inspect the event to determine the column.
|
|
124
|
+
* Note that {@link ColumnSpec.onCellClicked} is a more targeted handler scoped to a single
|
|
125
|
+
* column, which might be more convenient when clicks on only one column are of interest.
|
|
124
126
|
*/
|
|
125
127
|
onCellClicked?: (e: CellClickedEvent) => void;
|
|
126
128
|
/**
|
|
@@ -4,6 +4,7 @@ import { FunctionComponent, ReactNode } from 'react';
|
|
|
4
4
|
import { GridModel } from '../GridModel';
|
|
5
5
|
import { ColumnCellClassFn, ColumnCellClassRuleFn, ColumnComparator, ColumnEditableFn, ColumnEditorFn, ColumnEditorProps, ColumnExcelFormatFn, ColumnExportValueFn, ColumnGetValueFn, ColumnHeaderClassFn, ColumnHeaderNameFn, ColumnRenderer, ColumnSetValueFn, ColumnSortSpec, ColumnSortValueFn, ColumnTooltipFn } from '../Types';
|
|
6
6
|
import type { ColDef } from '@xh/hoist/kit/ag-grid';
|
|
7
|
+
import { CellClickedEvent } from '@ag-grid-community/core';
|
|
7
8
|
export interface ColumnSpec {
|
|
8
9
|
/**
|
|
9
10
|
* Name of data store field to display within the column, or object containing properties
|
|
@@ -248,6 +249,11 @@ export interface ColumnSpec {
|
|
|
248
249
|
* many rows + multiple actions per row. Defaults to false;
|
|
249
250
|
*/
|
|
250
251
|
actionsShowOnHoverOnly?: boolean;
|
|
252
|
+
/**
|
|
253
|
+
* Callback when a cell within this column clicked.
|
|
254
|
+
* See also {@link GridConfig.onCellClicked}, called when any cell within the grid is clicked.
|
|
255
|
+
*/
|
|
256
|
+
onCellClicked?: (e: CellClickedEvent) => void;
|
|
251
257
|
/**
|
|
252
258
|
* "escape hatch" object to pass directly to Ag-Grid for desktop implementations. Note these
|
|
253
259
|
* options may be used / overwritten by the framework itself, and are not all guaranteed to be
|
|
@@ -337,6 +343,7 @@ export declare class Column {
|
|
|
337
343
|
actionsShowOnHoverOnly?: boolean;
|
|
338
344
|
fieldSpec: FieldSpec;
|
|
339
345
|
omit: Thunkable<boolean>;
|
|
346
|
+
onCellClicked?: (e: CellClickedEvent) => void;
|
|
340
347
|
gridModel: GridModel;
|
|
341
348
|
agOptions: ColDef;
|
|
342
349
|
appData: PlainObject;
|
|
@@ -43,6 +43,11 @@ export interface ViewManagerConfig {
|
|
|
43
43
|
* True (default) to allow users to share their views with other users.
|
|
44
44
|
*/
|
|
45
45
|
enableSharing?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* True (default) to save pending state to SessionStorage so that it can be restored across
|
|
48
|
+
* browser refreshes. Unlike auto-save, this does not write to the database.
|
|
49
|
+
*/
|
|
50
|
+
preserveUnsavedChanges?: boolean;
|
|
46
51
|
/**
|
|
47
52
|
* Function to determine the initial view for a user, when no view has already been persisted.
|
|
48
53
|
* Will be passed a list of views available to the current user. Implementations where
|
|
@@ -52,13 +57,6 @@ export interface ViewManagerConfig {
|
|
|
52
57
|
* Must be set when enableDefault is false.
|
|
53
58
|
*/
|
|
54
59
|
initialViewSpec?: (views: ViewInfo[]) => ViewInfo;
|
|
55
|
-
/**
|
|
56
|
-
* Delay (in ms) to wait after state has been set on associated components before listening for
|
|
57
|
-
* further state changes. The long default wait 1000ms is intended to avoid a false positive
|
|
58
|
-
* dirty indicator when linking to complex components such as dashboards or grids that can
|
|
59
|
-
* report immediate changes to state due to internal processing or rendering.
|
|
60
|
-
*/
|
|
61
|
-
settleTime?: number;
|
|
62
60
|
/**
|
|
63
61
|
* True to allow the user to publish or edit the global views. Apps are expected to
|
|
64
62
|
* commonly set this based on user roles - e.g. `XH.getUser().hasRole('MANAGE_GRID_VIEWS')`.
|
|
@@ -132,8 +130,8 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
132
130
|
readonly enableDefault: boolean;
|
|
133
131
|
readonly enableGlobal: boolean;
|
|
134
132
|
readonly enableSharing: boolean;
|
|
133
|
+
readonly preserveUnsavedChanges: boolean;
|
|
135
134
|
readonly manageGlobal: boolean;
|
|
136
|
-
readonly settleTime: number;
|
|
137
135
|
readonly initialViewSpec: (views: ViewInfo[]) => ViewInfo;
|
|
138
136
|
/** Current view. Will not include uncommitted changes */
|
|
139
137
|
view: View<T>;
|
|
@@ -161,16 +159,12 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
161
159
|
/** Unsaved changes on the current view.*/
|
|
162
160
|
private pendingValue;
|
|
163
161
|
/**
|
|
164
|
-
* Array of {@link ViewManagerProvider} instances bound to this model.
|
|
165
|
-
*
|
|
166
|
-
* proactively push state to the target components when the model's selected `value` changes.
|
|
167
|
-
* @internal
|
|
162
|
+
* Array of {@link ViewManagerProvider} instances bound to this model. Used to proactively push
|
|
163
|
+
* state to the target components when the model's selected `value` changes.
|
|
168
164
|
*/
|
|
169
|
-
providers
|
|
165
|
+
private providers;
|
|
170
166
|
/** Data access for persisting views. */
|
|
171
167
|
private dataAccess;
|
|
172
|
-
/** Last time changes were pushed to linked persistence providers */
|
|
173
|
-
private lastPushed;
|
|
174
168
|
get isValueDirty(): boolean;
|
|
175
169
|
get isViewSavable(): boolean;
|
|
176
170
|
get isViewAutoSavable(): boolean;
|
|
@@ -207,8 +201,18 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
207
201
|
/** Promote a view to global visibility/ownership status. */
|
|
208
202
|
makeViewGlobalAsync(view: ViewInfo): Promise<View<T>>;
|
|
209
203
|
deleteViewsAsync(toDelete: ViewInfo[]): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Called by {@link ViewManagerProvider} to receive state changes from this model.
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
registerProvider(provider: ViewManagerProvider<any>): void;
|
|
209
|
+
/**
|
|
210
|
+
* Called by {@link ViewManagerProvider} to stop receiving state changes.
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
unregisterProvider(provider: ViewManagerProvider<any>): void;
|
|
210
214
|
private initAsync;
|
|
211
|
-
private
|
|
215
|
+
private unsavedChangesReaction;
|
|
212
216
|
private autoSaveReaction;
|
|
213
217
|
private stateReactions;
|
|
214
218
|
private loadViewAsync;
|
|
@@ -11,6 +11,10 @@ export interface PersistOptions {
|
|
|
11
11
|
path?: string;
|
|
12
12
|
/** Debounce interval in ms, or a lodash debounce config. */
|
|
13
13
|
debounce?: DebounceSpec;
|
|
14
|
+
/**
|
|
15
|
+
* Delay (in ms) to wait after state has been read before listening for further state changes.
|
|
16
|
+
*/
|
|
17
|
+
settleTime?: number;
|
|
14
18
|
/**
|
|
15
19
|
* Type of PersistenceProvider to create. Specify as one of the built-in string types,
|
|
16
20
|
* or a subclass of PersistenceProvider.
|
|
@@ -26,9 +26,12 @@ export declare abstract class PersistenceProvider<S = any> {
|
|
|
26
26
|
readonly path: string;
|
|
27
27
|
readonly debounce: DebounceSpec;
|
|
28
28
|
readonly owner: HoistBase;
|
|
29
|
+
readonly settleTime: number;
|
|
29
30
|
protected target: Persistable<S>;
|
|
30
31
|
protected defaultState: PersistableState<S>;
|
|
31
32
|
private disposer;
|
|
33
|
+
private lastReadState;
|
|
34
|
+
private lastReadTime;
|
|
32
35
|
/**
|
|
33
36
|
* Construct an instance of this class.
|
|
34
37
|
*
|
package/cmp/grid/GridModel.ts
CHANGED
|
@@ -255,7 +255,9 @@ export interface GridConfig {
|
|
|
255
255
|
onRowDoubleClicked?: (e: RowDoubleClickedEvent) => void;
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
* Callback when
|
|
258
|
+
* Callback when any cell on the grid is clicked - inspect the event to determine the column.
|
|
259
|
+
* Note that {@link ColumnSpec.onCellClicked} is a more targeted handler scoped to a single
|
|
260
|
+
* column, which might be more convenient when clicks on only one column are of interest.
|
|
259
261
|
*/
|
|
260
262
|
onCellClicked?: (e: CellClickedEvent) => void;
|
|
261
263
|
|
|
@@ -71,6 +71,7 @@ import type {
|
|
|
71
71
|
ValueGetterParams,
|
|
72
72
|
ValueSetterParams
|
|
73
73
|
} from '@xh/hoist/kit/ag-grid';
|
|
74
|
+
import {CellClickedEvent} from '@ag-grid-community/core';
|
|
74
75
|
|
|
75
76
|
export interface ColumnSpec {
|
|
76
77
|
/**
|
|
@@ -374,6 +375,12 @@ export interface ColumnSpec {
|
|
|
374
375
|
*/
|
|
375
376
|
actionsShowOnHoverOnly?: boolean;
|
|
376
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Callback when a cell within this column clicked.
|
|
380
|
+
* See also {@link GridConfig.onCellClicked}, called when any cell within the grid is clicked.
|
|
381
|
+
*/
|
|
382
|
+
onCellClicked?: (e: CellClickedEvent) => void;
|
|
383
|
+
|
|
377
384
|
/**
|
|
378
385
|
* "escape hatch" object to pass directly to Ag-Grid for desktop implementations. Note these
|
|
379
386
|
* options may be used / overwritten by the framework itself, and are not all guaranteed to be
|
|
@@ -479,6 +486,7 @@ export class Column {
|
|
|
479
486
|
actionsShowOnHoverOnly?: boolean;
|
|
480
487
|
fieldSpec: FieldSpec;
|
|
481
488
|
omit: Thunkable<boolean>;
|
|
489
|
+
onCellClicked?: (e: CellClickedEvent) => void;
|
|
482
490
|
|
|
483
491
|
gridModel: GridModel;
|
|
484
492
|
agOptions: ColDef;
|
|
@@ -551,6 +559,7 @@ export class Column {
|
|
|
551
559
|
actionsShowOnHoverOnly,
|
|
552
560
|
actions,
|
|
553
561
|
omit,
|
|
562
|
+
onCellClicked,
|
|
554
563
|
agOptions,
|
|
555
564
|
appData,
|
|
556
565
|
...rest
|
|
@@ -657,6 +666,7 @@ export class Column {
|
|
|
657
666
|
|
|
658
667
|
this.actions = actions;
|
|
659
668
|
this.actionsShowOnHoverOnly = actionsShowOnHoverOnly ?? false;
|
|
669
|
+
this.onCellClicked = onCellClicked;
|
|
660
670
|
|
|
661
671
|
this.gridModel = gridModel;
|
|
662
672
|
this.agOptions = agOptions ? clone(agOptions) : {};
|
|
@@ -758,7 +768,8 @@ export class Column {
|
|
|
758
768
|
if (event.shiftKey && event.key === 'Enter') return true;
|
|
759
769
|
|
|
760
770
|
return false;
|
|
761
|
-
}
|
|
771
|
+
},
|
|
772
|
+
onCellClicked: this.onCellClicked
|
|
762
773
|
};
|
|
763
774
|
|
|
764
775
|
// We will change this setter as needed to install the renderer in the proper location
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {PersistableState, PersistenceProvider} from '@xh/hoist/core';
|
|
8
|
-
import {isObject} from 'lodash';
|
|
8
|
+
import {isEqual, isObject} from 'lodash';
|
|
9
9
|
import {GridModel} from '../GridModel';
|
|
10
|
-
import {GridModelPersistOptions} from '../Types';
|
|
10
|
+
import {ColumnState, GridModelPersistOptions} from '../Types';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Initialize persistence for a {@link GridModel} by applying its `persistWith` config.
|
|
@@ -33,7 +33,8 @@ export function initPersist(
|
|
|
33
33
|
...persistWith
|
|
34
34
|
},
|
|
35
35
|
target: {
|
|
36
|
-
getPersistableState: () =>
|
|
36
|
+
getPersistableState: () =>
|
|
37
|
+
new PersistableColumnState(gridModel.persistableColumnState),
|
|
37
38
|
setPersistableState: ({value}) => gridModel.setColumnState(value)
|
|
38
39
|
},
|
|
39
40
|
owner: gridModel
|
|
@@ -75,3 +76,12 @@ export function initPersist(
|
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
|
|
80
|
+
class PersistableColumnState extends PersistableState<ColumnState[]> {
|
|
81
|
+
override equals(other: PersistableState<ColumnState[]>): boolean {
|
|
82
|
+
return isEqual(
|
|
83
|
+
this.value.filter(it => !it.hidden),
|
|
84
|
+
other.value.filter(it => !it.hidden)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -19,7 +19,7 @@ import type {ViewManagerProvider, ReactionSpec} from '@xh/hoist/core';
|
|
|
19
19
|
import {genDisplayName} from '@xh/hoist/data';
|
|
20
20
|
import {fmtDateTime} from '@xh/hoist/format';
|
|
21
21
|
import {action, bindable, makeObservable, observable, comparer, runInAction} from '@xh/hoist/mobx';
|
|
22
|
-
import {
|
|
22
|
+
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
23
23
|
import {executeIfFunction, pluralize, throwIf} from '@xh/hoist/utils/js';
|
|
24
24
|
import {find, isEqual, isNil, isNull, isObject, isUndefined, lowerCase, uniqBy} from 'lodash';
|
|
25
25
|
import {ReactNode} from 'react';
|
|
@@ -74,6 +74,12 @@ export interface ViewManagerConfig {
|
|
|
74
74
|
*/
|
|
75
75
|
enableSharing?: boolean;
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* True (default) to save pending state to SessionStorage so that it can be restored across
|
|
79
|
+
* browser refreshes. Unlike auto-save, this does not write to the database.
|
|
80
|
+
*/
|
|
81
|
+
preserveUnsavedChanges?: boolean;
|
|
82
|
+
|
|
77
83
|
/**
|
|
78
84
|
* Function to determine the initial view for a user, when no view has already been persisted.
|
|
79
85
|
* Will be passed a list of views available to the current user. Implementations where
|
|
@@ -84,14 +90,6 @@ export interface ViewManagerConfig {
|
|
|
84
90
|
*/
|
|
85
91
|
initialViewSpec?: (views: ViewInfo[]) => ViewInfo;
|
|
86
92
|
|
|
87
|
-
/**
|
|
88
|
-
* Delay (in ms) to wait after state has been set on associated components before listening for
|
|
89
|
-
* further state changes. The long default wait 1000ms is intended to avoid a false positive
|
|
90
|
-
* dirty indicator when linking to complex components such as dashboards or grids that can
|
|
91
|
-
* report immediate changes to state due to internal processing or rendering.
|
|
92
|
-
*/
|
|
93
|
-
settleTime?: number;
|
|
94
|
-
|
|
95
93
|
/**
|
|
96
94
|
* True to allow the user to publish or edit the global views. Apps are expected to
|
|
97
95
|
* commonly set this based on user roles - e.g. `XH.getUser().hasRole('MANAGE_GRID_VIEWS')`.
|
|
@@ -176,8 +174,8 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
176
174
|
readonly enableDefault: boolean;
|
|
177
175
|
readonly enableGlobal: boolean;
|
|
178
176
|
readonly enableSharing: boolean;
|
|
177
|
+
readonly preserveUnsavedChanges: boolean;
|
|
179
178
|
readonly manageGlobal: boolean;
|
|
180
|
-
readonly settleTime: number;
|
|
181
179
|
readonly initialViewSpec: (views: ViewInfo[]) => ViewInfo;
|
|
182
180
|
|
|
183
181
|
/** Current view. Will not include uncommitted changes */
|
|
@@ -216,19 +214,14 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
216
214
|
private pendingValue: PendingValue<T> = null;
|
|
217
215
|
|
|
218
216
|
/**
|
|
219
|
-
* Array of {@link ViewManagerProvider} instances bound to this model.
|
|
220
|
-
*
|
|
221
|
-
* proactively push state to the target components when the model's selected `value` changes.
|
|
222
|
-
* @internal
|
|
217
|
+
* Array of {@link ViewManagerProvider} instances bound to this model. Used to proactively push
|
|
218
|
+
* state to the target components when the model's selected `value` changes.
|
|
223
219
|
*/
|
|
224
|
-
providers: ViewManagerProvider<any>[] = [];
|
|
220
|
+
private providers: ViewManagerProvider<any>[] = [];
|
|
225
221
|
|
|
226
222
|
/** Data access for persisting views. */
|
|
227
223
|
private dataAccess: DataAccess<T>;
|
|
228
224
|
|
|
229
|
-
/** Last time changes were pushed to linked persistence providers */
|
|
230
|
-
private lastPushed: number = null;
|
|
231
|
-
|
|
232
225
|
//---------------
|
|
233
226
|
// Getters
|
|
234
227
|
//---------------
|
|
@@ -296,7 +289,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
296
289
|
enableDefault = true,
|
|
297
290
|
enableGlobal = true,
|
|
298
291
|
enableSharing = true,
|
|
299
|
-
|
|
292
|
+
preserveUnsavedChanges = true,
|
|
300
293
|
initialViewSpec = null
|
|
301
294
|
}: ViewManagerConfig) {
|
|
302
295
|
super();
|
|
@@ -317,7 +310,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
317
310
|
this.enableGlobal = enableGlobal;
|
|
318
311
|
this.enableSharing = enableSharing;
|
|
319
312
|
this.enableAutoSave = enableAutoSave;
|
|
320
|
-
this.
|
|
313
|
+
this.preserveUnsavedChanges = preserveUnsavedChanges;
|
|
321
314
|
this.initialViewSpec = initialViewSpec;
|
|
322
315
|
|
|
323
316
|
this.selectTask = TaskObserver.trackLast({
|
|
@@ -421,10 +414,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
421
414
|
|
|
422
415
|
@action
|
|
423
416
|
setValue(value: Partial<T>) {
|
|
424
|
-
const {view, pendingValue
|
|
425
|
-
if (!pendingValue && settleTime && !olderThan(lastPushed, settleTime)) {
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
417
|
+
const {view, pendingValue} = this;
|
|
428
418
|
|
|
429
419
|
value = this.cleanState(value);
|
|
430
420
|
if (!isEqual(value, view.value)) {
|
|
@@ -500,6 +490,25 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
500
490
|
if (exception) throw exception;
|
|
501
491
|
}
|
|
502
492
|
|
|
493
|
+
//------------------
|
|
494
|
+
// Internal
|
|
495
|
+
//------------------
|
|
496
|
+
/**
|
|
497
|
+
* Called by {@link ViewManagerProvider} to receive state changes from this model.
|
|
498
|
+
* @internal
|
|
499
|
+
*/
|
|
500
|
+
registerProvider(provider: ViewManagerProvider<any>) {
|
|
501
|
+
this.providers.push(provider);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Called by {@link ViewManagerProvider} to stop receiving state changes.
|
|
506
|
+
* @internal
|
|
507
|
+
*/
|
|
508
|
+
unregisterProvider(provider: ViewManagerProvider<any>) {
|
|
509
|
+
this.providers = this.providers.filter(it => it !== provider);
|
|
510
|
+
}
|
|
511
|
+
|
|
503
512
|
//------------------
|
|
504
513
|
// Implementation
|
|
505
514
|
//------------------
|
|
@@ -515,7 +524,9 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
515
524
|
this.views = views;
|
|
516
525
|
this.userPinned = state.userPinned;
|
|
517
526
|
this.autoSave = state.autoSave;
|
|
518
|
-
this.
|
|
527
|
+
if (this.preserveUnsavedChanges) {
|
|
528
|
+
this.pendingValue = XH.sessionStorageService.get(pendingValueStorageKey);
|
|
529
|
+
}
|
|
519
530
|
});
|
|
520
531
|
|
|
521
532
|
// 2) Initialize/choose initial view. Null is ok, and will yield default.
|
|
@@ -537,13 +548,13 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
537
548
|
}
|
|
538
549
|
|
|
539
550
|
this.addReaction(
|
|
540
|
-
this.
|
|
551
|
+
this.preserveUnsavedChanges ? this.unsavedChangesReaction() : null,
|
|
541
552
|
this.autoSaveReaction(),
|
|
542
553
|
...this.stateReactions(initialState)
|
|
543
554
|
);
|
|
544
555
|
}
|
|
545
556
|
|
|
546
|
-
private
|
|
557
|
+
private unsavedChangesReaction(): ReactionSpec {
|
|
547
558
|
return {
|
|
548
559
|
track: () => this.pendingValue,
|
|
549
560
|
run: v => XH.sessionStorageService.set(this.pendingValueStorageKey, v)
|
|
@@ -588,7 +599,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
588
599
|
.thenAction(latest => {
|
|
589
600
|
this.setAsView(latest, pendingValue?.token == token ? pendingValue : null);
|
|
590
601
|
this.providers.forEach(it => it.pushStateToTarget());
|
|
591
|
-
this.lastPushed = Date.now();
|
|
592
602
|
})
|
|
593
603
|
.linkTo(this.selectTask);
|
|
594
604
|
}
|
|
@@ -622,6 +632,9 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
622
632
|
if (!view.isDefault) {
|
|
623
633
|
this.views = uniqBy([view.info, ...this.views], 'token');
|
|
624
634
|
}
|
|
635
|
+
|
|
636
|
+
// Ensure providers have a clean reference of the current view state.
|
|
637
|
+
this.providers.forEach(it => it.read());
|
|
625
638
|
}
|
|
626
639
|
|
|
627
640
|
private handleException(e, opts: ExceptionHandlerOptions = {}) {
|
|
@@ -28,6 +28,11 @@ export interface PersistOptions {
|
|
|
28
28
|
/** Debounce interval in ms, or a lodash debounce config. */
|
|
29
29
|
debounce?: DebounceSpec;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Delay (in ms) to wait after state has been read before listening for further state changes.
|
|
33
|
+
*/
|
|
34
|
+
settleTime?: number;
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Type of PersistenceProvider to create. Specify as one of the built-in string types,
|
|
33
38
|
* or a subclass of PersistenceProvider.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import {olderThan} from '@xh/hoist/utils/datetime';
|
|
8
9
|
import {logDebug, logError, throwIf} from '@xh/hoist/utils/js';
|
|
9
10
|
import {
|
|
10
11
|
cloneDeep,
|
|
@@ -59,11 +60,14 @@ export abstract class PersistenceProvider<S = any> {
|
|
|
59
60
|
readonly path: string;
|
|
60
61
|
readonly debounce: DebounceSpec;
|
|
61
62
|
readonly owner: HoistBase;
|
|
63
|
+
readonly settleTime: number;
|
|
62
64
|
|
|
63
65
|
protected target: Persistable<S>;
|
|
64
66
|
protected defaultState: PersistableState<S>;
|
|
65
67
|
|
|
66
68
|
private disposer: IReactionDisposer;
|
|
69
|
+
private lastReadState: PersistableState<S>;
|
|
70
|
+
private lastReadTime: number;
|
|
67
71
|
|
|
68
72
|
/**
|
|
69
73
|
* Construct an instance of this class.
|
|
@@ -126,7 +130,10 @@ export abstract class PersistenceProvider<S = any> {
|
|
|
126
130
|
read(): PersistableState<S> {
|
|
127
131
|
const state = get(this.readRaw(), this.path);
|
|
128
132
|
logDebug(['Reading state', state], this.owner);
|
|
129
|
-
|
|
133
|
+
const ret = !isUndefined(state) ? new PersistableState(state) : null;
|
|
134
|
+
this.lastReadState = ret;
|
|
135
|
+
this.lastReadTime = Date.now();
|
|
136
|
+
return ret;
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
/** Persist JSON-serializable state to this provider's path. */
|
|
@@ -161,11 +168,12 @@ export abstract class PersistenceProvider<S = any> {
|
|
|
161
168
|
const {owner, persistOptions} = cfg;
|
|
162
169
|
this.owner = owner;
|
|
163
170
|
|
|
164
|
-
const {path, debounce = 250} = persistOptions;
|
|
171
|
+
const {path, debounce = 250, settleTime} = persistOptions;
|
|
165
172
|
throwIf(!path, 'Path not specified in PersistenceProvider.');
|
|
166
173
|
|
|
167
174
|
this.path = path;
|
|
168
175
|
this.debounce = debounce;
|
|
176
|
+
this.settleTime = settleTime;
|
|
169
177
|
this.owner.markManaged(this);
|
|
170
178
|
|
|
171
179
|
if (debounce) {
|
|
@@ -187,8 +195,14 @@ export abstract class PersistenceProvider<S = any> {
|
|
|
187
195
|
this.disposer = reaction(
|
|
188
196
|
() => this.target.getPersistableState(),
|
|
189
197
|
state => {
|
|
190
|
-
if (
|
|
198
|
+
if (this.settleTime && !olderThan(this.lastReadTime, this.settleTime)) {
|
|
199
|
+
return;
|
|
200
|
+
} else if (state.equals(this.defaultState)) {
|
|
191
201
|
this.clear();
|
|
202
|
+
} else if (this.lastReadState && state.equals(this.lastReadState)) {
|
|
203
|
+
// If the last read state is equal to the current state, use the last read state
|
|
204
|
+
// to avoid appearing "dirty"
|
|
205
|
+
this.write(this.lastReadState.value);
|
|
192
206
|
} else {
|
|
193
207
|
this.write(state.value);
|
|
194
208
|
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
9
|
-
import {pull} from 'lodash';
|
|
10
9
|
import {PersistenceProvider, PersistenceProviderConfig} from '../PersistenceProvider';
|
|
11
10
|
import type {ViewManagerModel} from '@xh/hoist/cmp/viewmanager/ViewManagerModel';
|
|
12
11
|
|
|
@@ -18,7 +17,7 @@ export class ViewManagerProvider<S> extends PersistenceProvider<S> {
|
|
|
18
17
|
const {viewManagerModel} = cfg.persistOptions;
|
|
19
18
|
throwIf(!viewManagerModel, `ViewManagerProvider requires a 'viewManagerModel'.`);
|
|
20
19
|
this.viewManagerModel = viewManagerModel;
|
|
21
|
-
viewManagerModel.
|
|
20
|
+
viewManagerModel.registerProvider(this);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
pushStateToTarget() {
|
|
@@ -38,10 +37,7 @@ export class ViewManagerProvider<S> extends PersistenceProvider<S> {
|
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
override destroy() {
|
|
41
|
-
|
|
42
|
-
pull(this.viewManagerModel.providers, this);
|
|
43
|
-
}
|
|
44
|
-
|
|
40
|
+
this.viewManagerModel?.unregisterProvider(this);
|
|
45
41
|
super.destroy();
|
|
46
42
|
}
|
|
47
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "74.0.0-SNAPSHOT.
|
|
3
|
+
"version": "74.0.0-SNAPSHOT.1748890930922",
|
|
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",
|