@xh/hoist 71.0.0-SNAPSHOT.1735311286067 → 71.0.0-SNAPSHOT.1735324944017
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 +1 -0
- package/build/types/cmp/viewmanager/{ViewToBlobApi.d.ts → DataAccess.d.ts} +13 -4
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +14 -13
- package/build/types/cmp/viewmanager/index.d.ts +1 -1
- package/build/types/svc/JsonBlobService.d.ts +5 -1
- package/cmp/viewmanager/DataAccess.ts +179 -0
- package/cmp/viewmanager/ViewManagerModel.ts +111 -140
- package/cmp/viewmanager/index.ts +1 -1
- package/desktop/cmp/button/Button.scss +48 -21
- package/package.json +1 -1
- package/svc/JsonBlobService.ts +25 -10
- package/tsconfig.tsbuildinfo +1 -1
- package/cmp/viewmanager/ViewToBlobApi.ts +0 -199
|
@@ -10,26 +10,22 @@ import {
|
|
|
10
10
|
ExceptionHandlerOptions,
|
|
11
11
|
HoistModel,
|
|
12
12
|
LoadSpec,
|
|
13
|
-
PersistableState,
|
|
14
|
-
PersistenceProvider,
|
|
15
|
-
PersistOptions,
|
|
16
13
|
PlainObject,
|
|
17
14
|
TaskObserver,
|
|
18
15
|
Thunkable,
|
|
19
16
|
XH
|
|
20
17
|
} from '@xh/hoist/core';
|
|
21
|
-
import type {ViewManagerProvider} from '@xh/hoist/core';
|
|
18
|
+
import type {ViewManagerProvider, ReactionSpec} from '@xh/hoist/core';
|
|
22
19
|
import {genDisplayName} from '@xh/hoist/data';
|
|
23
20
|
import {fmtDateTime} from '@xh/hoist/format';
|
|
24
|
-
import {action, bindable, makeObservable, observable,
|
|
21
|
+
import {action, bindable, makeObservable, observable, comparer, runInAction} from '@xh/hoist/mobx';
|
|
25
22
|
import {olderThan, SECONDS} from '@xh/hoist/utils/datetime';
|
|
26
23
|
import {executeIfFunction, pluralize, throwIf} from '@xh/hoist/utils/js';
|
|
27
|
-
import {find,
|
|
28
|
-
import {runInAction} from 'mobx';
|
|
24
|
+
import {find, isEqual, isNil, isNull, isUndefined, lowerCase} from 'lodash';
|
|
29
25
|
import {ReactNode} from 'react';
|
|
30
26
|
import {ViewInfo} from './ViewInfo';
|
|
31
27
|
import {View} from './View';
|
|
32
|
-
import {
|
|
28
|
+
import {DataAccess, ViewCreateSpec, ViewUpdateSpec, ViewUserState} from './DataAccess';
|
|
33
29
|
|
|
34
30
|
export interface ViewManagerConfig {
|
|
35
31
|
/**
|
|
@@ -79,9 +75,6 @@ export interface ViewManagerConfig {
|
|
|
79
75
|
*/
|
|
80
76
|
manageGlobal?: Thunkable<boolean>;
|
|
81
77
|
|
|
82
|
-
/** Used to persist the user's state. */
|
|
83
|
-
persistWith?: ViewManagerPersistOptions;
|
|
84
|
-
|
|
85
78
|
/**
|
|
86
79
|
* Required discriminator for the particular class of views to be loaded and managed by this
|
|
87
80
|
* model. Set to something descriptive and specific enough to be identifiable and allow for
|
|
@@ -90,6 +83,13 @@ export interface ViewManagerConfig {
|
|
|
90
83
|
*/
|
|
91
84
|
type: string;
|
|
92
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Optional sub-discriminator for the particular location in your app this instance of the
|
|
88
|
+
* view manager appears in. A particular currentView and pendingValue will be maintained by
|
|
89
|
+
* instance, but all other options, and the available library of views will be shared by type.
|
|
90
|
+
*/
|
|
91
|
+
instance?: string;
|
|
92
|
+
|
|
93
93
|
/**
|
|
94
94
|
* Optional user-facing display name for the view type, displayed in the ViewManager menu
|
|
95
95
|
* and associated management dialogs and prompts. Defaulted from `type` if not provided.
|
|
@@ -102,14 +102,6 @@ export interface ViewManagerConfig {
|
|
|
102
102
|
globalDisplayName?: string;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
export interface ViewManagerPersistOptions extends PersistOptions {
|
|
106
|
-
/** True to persist pinning preferences or provide specific PersistOptions. (Default true) */
|
|
107
|
-
persistPinning?: boolean | PersistOptions;
|
|
108
|
-
|
|
109
|
-
/** True to include pending value or provide specific PersistOptions. (Default false) */
|
|
110
|
-
persistPendingValue?: boolean | PersistOptions;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
105
|
/**
|
|
114
106
|
* ViewManagerModel coordinates the loading, saving, and management of user-defined bundles of
|
|
115
107
|
* {@link Persistable} component/model state.
|
|
@@ -146,8 +138,8 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
146
138
|
}
|
|
147
139
|
|
|
148
140
|
/** Immutable configuration for this model. */
|
|
149
|
-
declare persistWith: ViewManagerPersistOptions;
|
|
150
141
|
readonly type: string;
|
|
142
|
+
readonly instance: string;
|
|
151
143
|
readonly typeDisplayName: string;
|
|
152
144
|
readonly globalDisplayName: string;
|
|
153
145
|
readonly enableAutoSave: boolean;
|
|
@@ -175,7 +167,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
175
167
|
* True if user has opted-in to automatically saving changes to personal views (if auto-save
|
|
176
168
|
* generally available as per `enableAutoSave`).
|
|
177
169
|
*/
|
|
178
|
-
@bindable autoSave =
|
|
170
|
+
@bindable autoSave = false;
|
|
179
171
|
|
|
180
172
|
/**
|
|
181
173
|
* TaskObserver linked to {@link selectViewAsync}. If a change to the active view is likely to
|
|
@@ -202,7 +194,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
202
194
|
providers: ViewManagerProvider<any>[] = [];
|
|
203
195
|
|
|
204
196
|
/** Data access for persisting views. */
|
|
205
|
-
private
|
|
197
|
+
private dataAccess: DataAccess<T>;
|
|
206
198
|
|
|
207
199
|
/** Last time changes were pushed to linked persistence providers */
|
|
208
200
|
private lastPushed: number = null;
|
|
@@ -265,7 +257,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
265
257
|
*/
|
|
266
258
|
private constructor({
|
|
267
259
|
type,
|
|
268
|
-
|
|
260
|
+
instance = 'default',
|
|
269
261
|
typeDisplayName,
|
|
270
262
|
globalDisplayName = 'global',
|
|
271
263
|
manageGlobal = false,
|
|
@@ -285,9 +277,9 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
285
277
|
);
|
|
286
278
|
|
|
287
279
|
this.type = type;
|
|
280
|
+
this.instance = instance;
|
|
288
281
|
this.typeDisplayName = lowerCase(typeDisplayName ?? genDisplayName(type));
|
|
289
282
|
this.globalDisplayName = globalDisplayName;
|
|
290
|
-
this.persistWith = persistWith;
|
|
291
283
|
this.manageGlobal = executeIfFunction(manageGlobal) ?? false;
|
|
292
284
|
this.enableDefault = enableDefault;
|
|
293
285
|
this.enableGlobal = enableGlobal;
|
|
@@ -303,20 +295,23 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
303
295
|
message: `Saving ${this.typeDisplayName}...`
|
|
304
296
|
});
|
|
305
297
|
|
|
306
|
-
this.
|
|
298
|
+
this.dataAccess = new DataAccess(this);
|
|
307
299
|
}
|
|
308
300
|
|
|
309
301
|
override async doLoadAsync(loadSpec: LoadSpec) {
|
|
302
|
+
const {dataAccess, view} = this;
|
|
310
303
|
try {
|
|
311
|
-
// 1) Update
|
|
312
|
-
const views = await
|
|
304
|
+
// 1) Update views and related state
|
|
305
|
+
const {views, state} = await dataAccess.fetchDataAsync();
|
|
313
306
|
if (loadSpec.isStale) return;
|
|
314
|
-
runInAction(() =>
|
|
307
|
+
runInAction(() => {
|
|
308
|
+
this.views = views;
|
|
309
|
+
this.userPinned = state.userPinned;
|
|
310
|
+
this.autoSave = state.autoSave;
|
|
311
|
+
});
|
|
315
312
|
|
|
316
|
-
//
|
|
317
|
-
const {view} = this;
|
|
313
|
+
// potentially fast-forward current view.
|
|
318
314
|
if (!view.isDefault) {
|
|
319
|
-
// Reload view if can be fast-forwarded. Otherwise, leave as is for save/saveAs.
|
|
320
315
|
const latestInfo = find(views, {token: view.token});
|
|
321
316
|
if (latestInfo && latestInfo.lastUpdated > view.lastUpdated) {
|
|
322
317
|
this.loadViewAsync(latestInfo, this.pendingValue);
|
|
@@ -329,20 +324,24 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
329
324
|
}
|
|
330
325
|
|
|
331
326
|
async selectViewAsync(info: ViewInfo): Promise<void> {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
327
|
+
// ensure any pending auto-save gets completed
|
|
328
|
+
if (this.isValueDirty && this.isViewAutoSavable) {
|
|
329
|
+
await this.maybeAutoSaveAsync();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// if still dirty, require confirm
|
|
333
|
+
if (this.isValueDirty && this.view.isOwned && !(await this.confirmDiscardChangesAsync())) {
|
|
334
|
+
return;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
await this.loadViewAsync(info).catch(e => this.handleException(e));
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
async saveAsAsync(spec: ViewCreateSpec): Promise<void> {
|
|
341
|
-
const view = await this.
|
|
341
|
+
const view = await this.dataAccess.createViewAsync({...spec, value: this.getValue()});
|
|
342
342
|
this.noteSuccess(`Created ${view.typedName}`);
|
|
343
343
|
this.userPin(view.info);
|
|
344
344
|
this.setAsView(view);
|
|
345
|
-
this.refreshAsync();
|
|
346
345
|
}
|
|
347
346
|
|
|
348
347
|
//------------------------
|
|
@@ -353,12 +352,12 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
353
352
|
this.logError('Unexpected conditions for call to save, skipping');
|
|
354
353
|
return;
|
|
355
354
|
}
|
|
356
|
-
const {pendingValue, view,
|
|
355
|
+
const {pendingValue, view, dataAccess} = this;
|
|
357
356
|
try {
|
|
358
357
|
if (!(await this.maybeConfirmSaveAsync(view, pendingValue))) {
|
|
359
358
|
return;
|
|
360
359
|
}
|
|
361
|
-
const updated = await
|
|
360
|
+
const updated = await dataAccess
|
|
362
361
|
.updateViewValueAsync(view, pendingValue.value)
|
|
363
362
|
.linkTo(this.saveTask);
|
|
364
363
|
|
|
@@ -441,18 +440,18 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
441
440
|
|
|
442
441
|
/** Update all aspects of a view's metadata.*/
|
|
443
442
|
async updateViewInfoAsync(view: ViewInfo, updates: ViewUpdateSpec): Promise<View<T>> {
|
|
444
|
-
return this.
|
|
443
|
+
return this.dataAccess.updateViewInfoAsync(view, updates);
|
|
445
444
|
}
|
|
446
445
|
|
|
447
446
|
/** Promote a view to global visibility/ownership status. */
|
|
448
447
|
async makeViewGlobalAsync(view: ViewInfo): Promise<View<T>> {
|
|
449
|
-
return this.
|
|
448
|
+
return this.dataAccess.makeViewGlobalAsync(view);
|
|
450
449
|
}
|
|
451
450
|
|
|
452
451
|
async deleteViewsAsync(toDelete: ViewInfo[]): Promise<void> {
|
|
453
452
|
let exception;
|
|
454
453
|
try {
|
|
455
|
-
await this.
|
|
454
|
+
await this.dataAccess.deleteViewsAsync(toDelete);
|
|
456
455
|
} catch (e) {
|
|
457
456
|
exception = e;
|
|
458
457
|
}
|
|
@@ -471,38 +470,86 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
471
470
|
// Implementation
|
|
472
471
|
//------------------
|
|
473
472
|
private async initAsync() {
|
|
473
|
+
let {dataAccess, pendingValueStorageKey} = this,
|
|
474
|
+
initialState;
|
|
475
|
+
|
|
474
476
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
+
// 1) Initialize views and related state
|
|
478
|
+
const {views, state} = await dataAccess.fetchDataAsync();
|
|
479
|
+
initialState = state;
|
|
480
|
+
runInAction(() => {
|
|
481
|
+
this.views = views;
|
|
482
|
+
this.userPinned = state.userPinned;
|
|
483
|
+
this.autoSave = state.autoSave;
|
|
484
|
+
this.pendingValue = XH.sessionStorageService.get(pendingValueStorageKey);
|
|
485
|
+
});
|
|
477
486
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
487
|
+
// 2) Initialize/choose initial view. Null is ok, and will yield default.
|
|
488
|
+
let initialView,
|
|
489
|
+
initialTkn = initialState.currentView;
|
|
490
|
+
if (isUndefined(initialTkn)) {
|
|
491
|
+
initialView = this.initialViewSpec?.(views);
|
|
492
|
+
} else if (!isNull(initialTkn)) {
|
|
493
|
+
initialView = find(views, {token: initialTkn}) ?? this.initialViewSpec?.(views);
|
|
494
|
+
} else {
|
|
495
|
+
initialView = null;
|
|
481
496
|
}
|
|
482
497
|
|
|
483
|
-
|
|
484
|
-
if (!this.view) {
|
|
485
|
-
await this.loadViewAsync(this.initialViewSpec?.(views), this.pendingValue);
|
|
486
|
-
}
|
|
498
|
+
await this.loadViewAsync(initialView, this.pendingValue);
|
|
487
499
|
} catch (e) {
|
|
488
|
-
// Always ensure at least default view is installed
|
|
489
|
-
|
|
490
|
-
|
|
500
|
+
// Always ensure at least default view is installed (other state defaults are fine)
|
|
501
|
+
this.loadViewAsync(null, this.pendingValue);
|
|
491
502
|
this.handleException(e, {showAlert: false, logOnServer: true});
|
|
492
503
|
}
|
|
493
504
|
|
|
494
|
-
this.addReaction(
|
|
505
|
+
this.addReaction(
|
|
506
|
+
this.pendingValueReaction(),
|
|
507
|
+
this.autoSaveReaction(),
|
|
508
|
+
...this.stateReactions(initialState)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private pendingValueReaction(): ReactionSpec {
|
|
513
|
+
return {
|
|
514
|
+
track: () => this.pendingValue,
|
|
515
|
+
run: v => XH.sessionStorageService.set(this.pendingValueStorageKey, v)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private autoSaveReaction(): ReactionSpec {
|
|
520
|
+
return {
|
|
495
521
|
track: () => [this.pendingValue, this.autoSave],
|
|
496
522
|
run: () => this.maybeAutoSaveAsync(),
|
|
497
|
-
debounce:
|
|
498
|
-
}
|
|
523
|
+
debounce: 2 * SECONDS
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private stateReactions(initialState: ViewUserState): ReactionSpec[] {
|
|
528
|
+
const {dataAccess} = this;
|
|
529
|
+
return [
|
|
530
|
+
{
|
|
531
|
+
track: () => this.userPinned,
|
|
532
|
+
run: userPinned => dataAccess.updateStateAsync({userPinned}),
|
|
533
|
+
equals: comparer.structural,
|
|
534
|
+
debounce: 2 * SECONDS
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
track: () => this.autoSave,
|
|
538
|
+
run: autoSave => dataAccess.updateStateAsync({autoSave})
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
track: () => this.view?.token,
|
|
542
|
+
run: tkn => dataAccess.updateStateAsync({currentView: tkn}),
|
|
543
|
+
fireImmediately: this.view?.token !== initialState.currentView
|
|
544
|
+
}
|
|
545
|
+
];
|
|
499
546
|
}
|
|
500
547
|
|
|
501
548
|
private async loadViewAsync(
|
|
502
549
|
info: ViewInfo,
|
|
503
550
|
pendingValue: PendingValue<T> = null
|
|
504
551
|
): Promise<void> {
|
|
505
|
-
return this.
|
|
552
|
+
return this.dataAccess
|
|
506
553
|
.fetchViewAsync(info)
|
|
507
554
|
.thenAction(latest => {
|
|
508
555
|
this.setAsView(latest, pendingValue?.token == info?.token ? pendingValue : null);
|
|
@@ -513,10 +560,10 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
513
560
|
}
|
|
514
561
|
|
|
515
562
|
private async maybeAutoSaveAsync() {
|
|
516
|
-
const {pendingValue, isViewAutoSavable, view,
|
|
563
|
+
const {pendingValue, isViewAutoSavable, view, dataAccess} = this;
|
|
517
564
|
if (isViewAutoSavable && pendingValue) {
|
|
518
565
|
try {
|
|
519
|
-
const updated = await
|
|
566
|
+
const updated = await dataAccess
|
|
520
567
|
.updateViewValueAsync(view, pendingValue.value)
|
|
521
568
|
.linkTo(this.saveTask);
|
|
522
569
|
|
|
@@ -551,6 +598,10 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
551
598
|
XH.successToast(msg);
|
|
552
599
|
}
|
|
553
600
|
|
|
601
|
+
private get pendingValueStorageKey(): string {
|
|
602
|
+
return `${this.type}_${this.instance}`;
|
|
603
|
+
}
|
|
604
|
+
|
|
554
605
|
/**
|
|
555
606
|
* Stringify and parse to ensure that any value set here is valid, serializable JSON.
|
|
556
607
|
*/
|
|
@@ -575,7 +626,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
575
626
|
|
|
576
627
|
private async maybeConfirmSaveAsync(view: View, pendingValue: PendingValue<T>) {
|
|
577
628
|
// Get latest from server for reference
|
|
578
|
-
const latest = await this.
|
|
629
|
+
const latest = await this.dataAccess.fetchViewAsync(view.info),
|
|
579
630
|
isGlobal = latest.isGlobal,
|
|
580
631
|
isStale = latest.lastUpdated > pendingValue.baseUpdated;
|
|
581
632
|
if (!isStale && !isGlobal) return true;
|
|
@@ -613,86 +664,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
613
664
|
}
|
|
614
665
|
});
|
|
615
666
|
}
|
|
616
|
-
|
|
617
|
-
//------------------
|
|
618
|
-
// Persistence
|
|
619
|
-
//------------------
|
|
620
|
-
private initPersist(options: ViewManagerPersistOptions) {
|
|
621
|
-
const {
|
|
622
|
-
persistPinning = true,
|
|
623
|
-
persistPendingValue = false,
|
|
624
|
-
path = 'viewManager',
|
|
625
|
-
...rootPersistWith
|
|
626
|
-
} = options;
|
|
627
|
-
|
|
628
|
-
// Pinning potentially in dedicated location
|
|
629
|
-
if (persistPinning) {
|
|
630
|
-
const opts = isObject(persistPinning) ? persistPinning : rootPersistWith;
|
|
631
|
-
PersistenceProvider.create({
|
|
632
|
-
persistOptions: {path: `${path}.pinning`, ...opts},
|
|
633
|
-
target: {
|
|
634
|
-
getPersistableState: () => new PersistableState(this.userPinned),
|
|
635
|
-
setPersistableState: ({value}) => {
|
|
636
|
-
const {views} = this;
|
|
637
|
-
this.userPinned = !isEmpty(views) // Clean state iff views loaded!
|
|
638
|
-
? pickBy(value, (_, tkn) => views.some(v => v.token === tkn))
|
|
639
|
-
: value;
|
|
640
|
-
}
|
|
641
|
-
},
|
|
642
|
-
owner: this
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// AutoSave, potentially in core location.
|
|
647
|
-
if (this.enableAutoSave) {
|
|
648
|
-
PersistenceProvider.create({
|
|
649
|
-
persistOptions: {path: `${path}.autoSave`, ...rootPersistWith},
|
|
650
|
-
target: {
|
|
651
|
-
getPersistableState: () => new PersistableState(this.autoSave),
|
|
652
|
-
setPersistableState: ({value}) => (this.autoSave = value)
|
|
653
|
-
},
|
|
654
|
-
owner: this
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Pending Value, potentially in dedicated location
|
|
659
|
-
// On hydration, stash away for one time use when hydrating view itself below
|
|
660
|
-
if (persistPendingValue) {
|
|
661
|
-
const opts = isObject(persistPendingValue) ? persistPendingValue : rootPersistWith;
|
|
662
|
-
PersistenceProvider.create({
|
|
663
|
-
persistOptions: {path: `${path}.pendingValue`, ...opts},
|
|
664
|
-
target: {
|
|
665
|
-
getPersistableState: () => new PersistableState(this.pendingValue),
|
|
666
|
-
setPersistableState: ({value}) => {
|
|
667
|
-
// Only accept this during initialization!
|
|
668
|
-
if (!this.view) this.pendingValue = value;
|
|
669
|
-
}
|
|
670
|
-
},
|
|
671
|
-
owner: this
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// View, in core location
|
|
676
|
-
PersistenceProvider.create({
|
|
677
|
-
persistOptions: {path: `${path}.view`, ...rootPersistWith},
|
|
678
|
-
target: {
|
|
679
|
-
// View could be null, just before initialization.
|
|
680
|
-
getPersistableState: () => new PersistableState(this.view?.token),
|
|
681
|
-
setPersistableState: async ({value: token}) => {
|
|
682
|
-
// Requesting available view -- load it with any init pending val.
|
|
683
|
-
const viewInfo = token ? find(this.views, {token}) : null;
|
|
684
|
-
if (viewInfo || !token) {
|
|
685
|
-
try {
|
|
686
|
-
await this.loadViewAsync(viewInfo, this.pendingValue);
|
|
687
|
-
} catch (e) {
|
|
688
|
-
this.logError('Failure loading persisted view', e);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
},
|
|
693
|
-
owner: this
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
667
|
}
|
|
697
668
|
|
|
698
669
|
interface PendingValue<T> {
|
package/cmp/viewmanager/index.ts
CHANGED
|
@@ -56,33 +56,12 @@
|
|
|
56
56
|
padding: var(--xh-pad-half-px) var(--xh-pad-px);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// Standard and outlined have borders - remove standard radius in between adjacent buttons so
|
|
60
|
-
// they seamlessly combine their inner borders.
|
|
61
|
-
border-radius: 0;
|
|
62
|
-
&.xh-button--standard,
|
|
63
|
-
&.xh-button--outlined {
|
|
64
|
-
&:first-child {
|
|
65
|
-
border-bottom-left-radius: var(--xh-button-border-radius-px);
|
|
66
|
-
border-top-left-radius: var(--xh-button-border-radius-px);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
&:last-child {
|
|
70
|
-
border-bottom-right-radius: var(--xh-button-border-radius-px);
|
|
71
|
-
border-top-right-radius: var(--xh-button-border-radius-px);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
59
|
// Minimal has no borders - radius is made visible by background shading when hovered or
|
|
76
60
|
// pressed, and looks more natural without any radius.
|
|
77
61
|
&.xh-button--minimal {
|
|
78
62
|
border-radius: 0;
|
|
79
63
|
}
|
|
80
64
|
|
|
81
|
-
// Outlined buttons should avoid doubled-up inner borders.
|
|
82
|
-
&.xh-button--outlined:not(:last-child) {
|
|
83
|
-
border-right: none;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
65
|
&.xh-button--enabled {
|
|
87
66
|
// Hoist calls FocusStyleManager.onlyShowFocusOnTabs(), which by default hides focus indicators
|
|
88
67
|
// unless a component has been tabbed into via keyboard. Override here to always show a focus
|
|
@@ -93,6 +72,54 @@
|
|
|
93
72
|
}
|
|
94
73
|
}
|
|
95
74
|
}
|
|
75
|
+
|
|
76
|
+
// Horizontal mode only styles
|
|
77
|
+
&:not(.bp5-vertical) {
|
|
78
|
+
.xh-button.bp5-button {
|
|
79
|
+
// Standard and outlined have borders - remove standard radius in between adjacent buttons so
|
|
80
|
+
// they seamlessly combine their inner borders.
|
|
81
|
+
border-radius: 0;
|
|
82
|
+
&.xh-button--standard,
|
|
83
|
+
&.xh-button--outlined {
|
|
84
|
+
&:first-child {
|
|
85
|
+
border-radius: var(--xh-button-border-radius-px) 0 0 var(--xh-button-border-radius-px);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&:last-child {
|
|
89
|
+
border-radius: 0 var(--xh-button-border-radius-px) var(--xh-button-border-radius-px) 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Outlined buttons should avoid doubled-up inner borders.
|
|
94
|
+
&.xh-button--outlined:not(:last-child) {
|
|
95
|
+
border-right: none;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Vertical mode only styles
|
|
101
|
+
&.bp5-vertical {
|
|
102
|
+
.xh-button.bp5-button {
|
|
103
|
+
// Standard and outlined have borders - remove standard radius in between adjacent buttons so
|
|
104
|
+
// they seamlessly combine their inner borders.
|
|
105
|
+
border-radius: 0;
|
|
106
|
+
&.xh-button--standard,
|
|
107
|
+
&.xh-button--outlined {
|
|
108
|
+
&:first-child {
|
|
109
|
+
border-radius: var(--xh-button-border-radius-px) var(--xh-button-border-radius-px) 0 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&:last-child {
|
|
113
|
+
border-radius: 0 0 var(--xh-button-border-radius-px) var(--xh-button-border-radius-px);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Outlined buttons should avoid doubled-up inner borders.
|
|
118
|
+
&.xh-button--outlined:not(:last-child) {
|
|
119
|
+
border-bottom: none;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
125
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "71.0.0-SNAPSHOT.
|
|
3
|
+
"version": "71.0.0-SNAPSHOT.1735324944017",
|
|
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/svc/JsonBlobService.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Copyright © 2024 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {HoistService, LoadSpec, PlainObject, XH} from '@xh/hoist/core';
|
|
8
|
-
import {
|
|
8
|
+
import {pick} from 'lodash';
|
|
9
9
|
|
|
10
10
|
export interface JsonBlob {
|
|
11
11
|
/** Either null for private blobs or special token "*" for globally shared blobs. */
|
|
@@ -89,17 +89,32 @@ export class JsonBlobService extends HoistService {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** Modify mutable properties of an existing JSONBlob, as identified by its unique token. */
|
|
92
|
-
async updateAsync(
|
|
93
|
-
|
|
94
|
-
{acl, description, meta, name, owner, value}: Partial<JsonBlob>
|
|
95
|
-
): Promise<JsonBlob> {
|
|
96
|
-
const update = omitBy({acl, description, meta, name, owner, value}, isUndefined);
|
|
92
|
+
async updateAsync(token: string, update: Partial<JsonBlob>): Promise<JsonBlob> {
|
|
93
|
+
update = pick(update, ['acl', 'description', 'meta', 'name', 'owner', 'value']);
|
|
97
94
|
return XH.fetchJson({
|
|
98
95
|
url: 'xh/updateJsonBlob',
|
|
99
|
-
params: {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
params: {token, update: JSON.stringify(update)}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Create or update a blob for a user with the existing type and name. */
|
|
101
|
+
async createOrUpdateAsync(
|
|
102
|
+
type: string,
|
|
103
|
+
name: string,
|
|
104
|
+
data: Partial<JsonBlob>
|
|
105
|
+
): Promise<JsonBlob> {
|
|
106
|
+
const update = pick(data, ['acl', 'description', 'meta', 'value']);
|
|
107
|
+
return XH.fetchJson({
|
|
108
|
+
url: 'xh/createOrUpdateJsonBlob',
|
|
109
|
+
params: {type, name, update: JSON.stringify(update)}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Find a blob owned by this user with a specific type and name. If none exists, return null. */
|
|
114
|
+
async findAsync(type: string, name: string): Promise<JsonBlob> {
|
|
115
|
+
return XH.fetchJson({
|
|
116
|
+
url: 'xh/findJsonBlob',
|
|
117
|
+
params: {type, name}
|
|
103
118
|
});
|
|
104
119
|
}
|
|
105
120
|
|