@xh/hoist 71.0.0-SNAPSHOT.1733854822950 → 71.0.0-SNAPSHOT.1734551243081

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/build/types/cmp/viewmanager/View.d.ts +5 -0
  3. package/build/types/cmp/viewmanager/ViewInfo.d.ts +32 -7
  4. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +41 -31
  5. package/build/types/cmp/viewmanager/ViewToBlobApi.d.ts +28 -7
  6. package/build/types/cmp/viewmanager/index.d.ts +1 -1
  7. package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +0 -4
  8. package/build/types/desktop/cmp/viewmanager/ViewManagerLocalModel.d.ts +11 -0
  9. package/build/types/desktop/cmp/viewmanager/ViewMenu.d.ts +2 -2
  10. package/build/types/desktop/cmp/viewmanager/dialog/ManageDialog.d.ts +3 -1
  11. package/build/types/desktop/cmp/viewmanager/dialog/ManageDialogModel.d.ts +18 -13
  12. package/build/types/desktop/cmp/viewmanager/dialog/SaveAsDialog.d.ts +1 -1
  13. package/build/types/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.d.ts +3 -9
  14. package/build/types/desktop/cmp/viewmanager/dialog/Utils.d.ts +3 -0
  15. package/build/types/desktop/cmp/viewmanager/dialog/ViewMultiPanel.d.ts +2 -0
  16. package/build/types/desktop/cmp/viewmanager/dialog/ViewPanel.d.ts +5 -0
  17. package/build/types/desktop/cmp/viewmanager/dialog/{EditFormModel.d.ts → ViewPanelModel.d.ts} +2 -4
  18. package/build/types/svc/JsonBlobService.d.ts +1 -1
  19. package/cmp/viewmanager/View.ts +21 -1
  20. package/cmp/viewmanager/ViewInfo.ts +58 -11
  21. package/cmp/viewmanager/ViewManagerModel.ts +109 -82
  22. package/cmp/viewmanager/ViewToBlobApi.ts +114 -42
  23. package/cmp/viewmanager/index.ts +1 -1
  24. package/desktop/cmp/viewmanager/ViewManager.scss +25 -28
  25. package/desktop/cmp/viewmanager/ViewManager.ts +32 -27
  26. package/desktop/cmp/viewmanager/ViewManagerLocalModel.ts +33 -0
  27. package/desktop/cmp/viewmanager/ViewMenu.ts +162 -169
  28. package/desktop/cmp/viewmanager/dialog/ManageDialog.ts +69 -42
  29. package/desktop/cmp/viewmanager/dialog/ManageDialogModel.ts +267 -150
  30. package/desktop/cmp/viewmanager/dialog/SaveAsDialog.ts +30 -9
  31. package/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.ts +35 -40
  32. package/desktop/cmp/viewmanager/dialog/Utils.ts +18 -0
  33. package/desktop/cmp/viewmanager/dialog/ViewMultiPanel.ts +70 -0
  34. package/desktop/cmp/viewmanager/dialog/ViewPanel.ts +166 -0
  35. package/desktop/cmp/viewmanager/dialog/ViewPanelModel.ts +116 -0
  36. package/package.json +1 -1
  37. package/svc/JsonBlobService.ts +3 -3
  38. package/svc/storage/BaseStorageService.ts +1 -1
  39. package/tsconfig.tsbuildinfo +1 -1
  40. package/build/types/desktop/cmp/viewmanager/dialog/EditForm.d.ts +0 -5
  41. package/desktop/cmp/viewmanager/dialog/EditForm.ts +0 -126
  42. package/desktop/cmp/viewmanager/dialog/EditFormModel.ts +0 -125
@@ -8,8 +8,7 @@
8
8
  import {FormModel} from '@xh/hoist/cmp/form';
9
9
  import {HoistModel, managed, XH} from '@xh/hoist/core';
10
10
  import {makeObservable, action, observable} from '@xh/hoist/mobx';
11
- import {View} from './View';
12
- import {ViewManagerModel} from './ViewManagerModel';
11
+ import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
13
12
 
14
13
  /**
15
14
  * Backing model for ViewManagerModel's SaveAs
@@ -20,20 +19,6 @@ export class SaveAsDialogModel extends HoistModel {
20
19
  @managed readonly formModel: FormModel;
21
20
  @observable isOpen: boolean = false;
22
21
 
23
- private resolveOpen: (value: View) => void;
24
-
25
- get type(): string {
26
- return this.parent.type;
27
- }
28
-
29
- get typeDisplayName(): string {
30
- return this.parent.typeDisplayName;
31
- }
32
-
33
- get globalDisplayName(): string {
34
- return this.parent.globalDisplayName;
35
- }
36
-
37
22
  constructor(parent: ViewManagerModel) {
38
23
  super();
39
24
  makeObservable(this);
@@ -42,20 +27,35 @@ export class SaveAsDialogModel extends HoistModel {
42
27
  }
43
28
 
44
29
  @action
45
- openAsync(): Promise<View> {
46
- this.formModel.init(this.parent.view.info ?? {});
47
- this.isOpen = true;
30
+ open() {
31
+ const {parent, formModel} = this,
32
+ src = parent.view,
33
+ name = parent.ownedViews.some(it => it.name === src.name)
34
+ ? `Copy of ${src.name}`
35
+ : src.name;
36
+
37
+ formModel.init({
38
+ name,
39
+ group: src.group,
40
+ description: src.description,
41
+ isShared: false
42
+ });
48
43
 
49
- return new Promise(resolve => (this.resolveOpen = resolve));
44
+ this.isOpen = true;
50
45
  }
51
46
 
52
- cancel() {
53
- this.close();
54
- this.resolveOpen(null);
47
+ @action
48
+ close() {
49
+ this.isOpen = false;
55
50
  }
56
51
 
57
52
  async saveAsAsync() {
58
- return this.doSaveAsAsync().linkTo(this.parent.saveTask);
53
+ try {
54
+ await this.doSaveAsAsync().linkTo(this.parent.saveTask);
55
+ this.close();
56
+ } catch (e) {
57
+ XH.handleException(e);
58
+ }
59
59
  }
60
60
 
61
61
  //------------------------
@@ -68,30 +68,25 @@ export class SaveAsDialogModel extends HoistModel {
68
68
  name: 'name',
69
69
  rules: [({value}) => this.parent.validateViewNameAsync(value)]
70
70
  },
71
- {name: 'description'}
71
+ {name: 'group'},
72
+ {name: 'description'},
73
+ {name: 'isShared'}
72
74
  ]
73
75
  });
74
76
  }
75
77
 
76
78
  private async doSaveAsAsync() {
77
- const {formModel, parent} = this,
78
- {name, description} = formModel.getData(),
79
+ let {formModel, parent} = this,
80
+ {name, group, description, isShared} = formModel.getData(),
79
81
  isValid = await formModel.validateAsync();
80
82
 
81
83
  if (!isValid) return;
82
84
 
83
- try {
84
- const ret = await this.parent.api.createViewAsync(name, description, parent.getValue());
85
- this.close();
86
- this.resolveOpen(ret);
87
- } catch (e) {
88
- XH.handleException(e);
89
- }
90
- }
91
-
92
- @action
93
- private close() {
94
- this.isOpen = false;
95
- this.formModel.init();
85
+ return parent.saveAsAsync({
86
+ name: name.trim(),
87
+ group: group?.trim(),
88
+ description: description?.trim(),
89
+ isShared
90
+ });
96
91
  }
97
92
  }
@@ -0,0 +1,18 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2024 Extremely Heavy Industries Inc.
6
+ */
7
+
8
+ import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
9
+ import {SelectOption} from '@xh/hoist/core';
10
+ import {map, uniq} from 'lodash';
11
+
12
+ export function getGroupOptions(model: ViewManagerModel, type: 'owned' | 'global'): SelectOption[] {
13
+ const views = type == 'owned' ? model.ownedViews : model.globalViews;
14
+ return uniq(map(views, 'group'))
15
+ .sort()
16
+ .filter(g => g != null)
17
+ .map(g => ({label: g, value: g}));
18
+ }
@@ -0,0 +1,70 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2024 Extremely Heavy Industries Inc.
6
+ */
7
+
8
+ import {placeholder, vbox, vframe, vspacer} from '@xh/hoist/cmp/layout';
9
+ import {hoistCmp, uses} from '@xh/hoist/core';
10
+ import {button} from '@xh/hoist/desktop/cmp/button';
11
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
12
+ import {Icon} from '@xh/hoist/icon';
13
+ import {pluralize} from '@xh/hoist/utils/js';
14
+ import {every, isEmpty, some} from 'lodash';
15
+ import {ManageDialogModel} from './ManageDialogModel';
16
+
17
+ export const viewMultiPanel = hoistCmp.factory<ManageDialogModel>({
18
+ model: uses(() => ManageDialogModel),
19
+ render({model}) {
20
+ const views = model.selectedViews;
21
+ if (isEmpty(views)) return null;
22
+
23
+ return panel({
24
+ item: vframe({
25
+ className: 'xh-view-manager__manage-dialog__form',
26
+ item: placeholder(
27
+ Icon.gears(),
28
+ `${views.length} selected ${pluralize(model.viewManagerModel.typeDisplayName)}`,
29
+ vspacer(),
30
+ buttons()
31
+ )
32
+ })
33
+ });
34
+ }
35
+ });
36
+
37
+ const buttons = hoistCmp.factory<ManageDialogModel>({
38
+ render({model}) {
39
+ const views = model.selectedViews,
40
+ allEditable = every(views, 'isEditable'),
41
+ allPinned = every(views, 'isPinned'),
42
+ allUnpinned = !some(views, 'isPinned');
43
+
44
+ return vbox({
45
+ style: {gap: 10, alignItems: 'center'},
46
+ items: [
47
+ button({
48
+ text: allPinned ? 'Unpin from your Menu' : 'Pin to your Menu',
49
+ icon: Icon.pin({
50
+ prefix: allPinned ? 'fas' : 'far',
51
+ className: allPinned ? 'xh-yellow' : ''
52
+ }),
53
+ width: 200,
54
+ outlined: true,
55
+ omit: !(allPinned || allUnpinned),
56
+ onClick: () => model.togglePinned(views)
57
+ }),
58
+ button({
59
+ text: 'Delete',
60
+ icon: Icon.delete(),
61
+ width: 200,
62
+ outlined: true,
63
+ intent: 'danger',
64
+ omit: !allEditable,
65
+ onClick: () => model.deleteAsync(views)
66
+ })
67
+ ]
68
+ });
69
+ }
70
+ });
@@ -0,0 +1,166 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2024 Extremely Heavy Industries Inc.
6
+ */
7
+
8
+ import {form} from '@xh/hoist/cmp/form';
9
+ import {div, filler, hbox, hspacer, span, vbox, vframe, vspacer} from '@xh/hoist/cmp/layout';
10
+ import {hoistCmp, uses, XH} from '@xh/hoist/core';
11
+ import {button} from '@xh/hoist/desktop/cmp/button';
12
+ import {formField} from '@xh/hoist/desktop/cmp/form';
13
+ import {select, switchInput, textArea, textInput} from '@xh/hoist/desktop/cmp/input';
14
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
15
+ import {ViewPanelModel} from '@xh/hoist/desktop/cmp/viewmanager/dialog/ViewPanelModel';
16
+ import {getGroupOptions} from '@xh/hoist/desktop/cmp/viewmanager/dialog/Utils';
17
+ import {fmtDateTime} from '@xh/hoist/format';
18
+ import {Icon} from '@xh/hoist/icon';
19
+ import {capitalize} from 'lodash';
20
+
21
+ /**
22
+ * Form to edit or view details on a single saved view within the ViewManager manage dialog.
23
+ */
24
+ export const viewPanel = hoistCmp.factory({
25
+ model: uses(ViewPanelModel),
26
+ render({model}) {
27
+ const {view} = model;
28
+ if (!view) return null;
29
+
30
+ const {isGlobal, lastUpdated, lastUpdatedBy, isEditable} = view,
31
+ {enableSharing} = model.parent.viewManagerModel;
32
+
33
+ return panel({
34
+ item: form({
35
+ fieldDefaults: {
36
+ commitOnChange: true,
37
+ minimal: true
38
+ },
39
+ item: vframe({
40
+ className: 'xh-view-manager__manage-dialog__form',
41
+ items: [
42
+ formField({
43
+ field: 'name',
44
+ item: textInput()
45
+ }),
46
+ formField({
47
+ field: 'owner',
48
+ omit: isEditable
49
+ }),
50
+ formField({
51
+ field: 'group',
52
+ item: select({
53
+ enableCreate: true,
54
+ enableClear: true,
55
+ options: getGroupOptions(
56
+ model.parent.viewManagerModel,
57
+ view.isOwned ? 'owned' : 'global'
58
+ )
59
+ }),
60
+ readonlyRenderer: v =>
61
+ v || span({item: 'None provided', className: 'xh-text-color-muted'})
62
+ }),
63
+ formField({
64
+ field: 'description',
65
+ item: textArea({
66
+ selectOnFocus: true,
67
+ height: 70
68
+ }),
69
+ readonlyRenderer: v =>
70
+ v || span({item: 'None provided', className: 'xh-text-color-muted'})
71
+ }),
72
+ formField({
73
+ field: 'isShared',
74
+ label: 'Shared?',
75
+ inline: true,
76
+ item: switchInput(),
77
+ readonlyRenderer: v => (v ? 'Yes' : 'No'),
78
+ omit: !enableSharing || isGlobal || !isEditable
79
+ }),
80
+ formField({
81
+ field: 'isDefaultPinned',
82
+ label: 'Pin by default?',
83
+ labelWidth: 110,
84
+ inline: true,
85
+ item: switchInput(),
86
+ omit: !isGlobal || !isEditable
87
+ }),
88
+ vspacer(),
89
+ formButtons(),
90
+ filler(),
91
+ div({
92
+ className: 'xh-view-manager__manage-dialog__metadata',
93
+ item: `Last Updated: ${fmtDateTime(lastUpdated)} by ${lastUpdatedBy === XH.getUsername() ? 'you' : lastUpdatedBy}`
94
+ })
95
+ ]
96
+ })
97
+ })
98
+ });
99
+ }
100
+ });
101
+
102
+ const formButtons = hoistCmp.factory<ViewPanelModel>({
103
+ render({model}) {
104
+ const {formModel, parent, view} = model,
105
+ {readonly} = formModel,
106
+ {isPinned} = view;
107
+
108
+ if (formModel.isDirty) {
109
+ return hbox({
110
+ justifyContent: 'center',
111
+ items: [
112
+ button({
113
+ text: 'Save Changes',
114
+ icon: Icon.check(),
115
+ intent: 'success',
116
+ minimal: false,
117
+ disabled: !formModel.isValid,
118
+ onClick: () => model.saveAsync()
119
+ }),
120
+ hspacer(),
121
+ button({
122
+ icon: Icon.reset(),
123
+ tooltip: 'Revert changes',
124
+ minimal: false,
125
+ onClick: () => formModel.reset()
126
+ })
127
+ ]
128
+ });
129
+ }
130
+
131
+ const {enableGlobal, globalDisplayName, manageGlobal, typeDisplayName} =
132
+ parent.viewManagerModel;
133
+ return vbox({
134
+ style: {gap: 10, alignItems: 'center'},
135
+ items: [
136
+ button({
137
+ text: isPinned ? 'Unpin from your Menu' : 'Pin to your Menu',
138
+ icon: Icon.pin({
139
+ prefix: isPinned ? 'fas' : 'far',
140
+ className: isPinned ? 'xh-yellow' : null
141
+ }),
142
+ width: 200,
143
+ outlined: true,
144
+ onClick: () => parent.togglePinned([view])
145
+ }),
146
+ button({
147
+ text: `Promote to ${capitalize(globalDisplayName)} ${typeDisplayName}`,
148
+ icon: Icon.globe(),
149
+ width: 200,
150
+ outlined: true,
151
+ omit: readonly || view.isGlobal || !enableGlobal || !manageGlobal,
152
+ onClick: () => parent.makeGlobalAsync(view)
153
+ }),
154
+ button({
155
+ text: 'Delete',
156
+ icon: Icon.delete(),
157
+ width: 200,
158
+ outlined: true,
159
+ intent: 'danger',
160
+ omit: readonly,
161
+ onClick: () => parent.deleteAsync([view])
162
+ })
163
+ ]
164
+ });
165
+ }
166
+ });
@@ -0,0 +1,116 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2024 Extremely Heavy Industries Inc.
6
+ */
7
+
8
+ import {FormModel} from '@xh/hoist/cmp/form';
9
+ import {fragment, p, strong} from '@xh/hoist/cmp/layout';
10
+ import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
11
+ import {capitalize} from 'lodash';
12
+ import {ManageDialogModel} from './ManageDialogModel';
13
+ import {makeObservable} from '@xh/hoist/mobx';
14
+ import {ViewInfo} from '@xh/hoist/cmp/viewmanager';
15
+ import {ReactNode} from 'react';
16
+
17
+ /**
18
+ * Backing model for EditForm
19
+ */
20
+ export class ViewPanelModel extends HoistModel {
21
+ parent: ManageDialogModel;
22
+
23
+ @managed formModel: FormModel;
24
+
25
+ get view(): ViewInfo {
26
+ return this.parent.selectedView;
27
+ }
28
+
29
+ get loadTask(): TaskObserver {
30
+ return this.parent.loadModel;
31
+ }
32
+
33
+ constructor(parent: ManageDialogModel) {
34
+ super();
35
+ makeObservable(this);
36
+
37
+ this.parent = parent;
38
+ this.formModel = this.createFormModel();
39
+
40
+ this.addReaction({
41
+ track: () => this.view,
42
+ run: view => {
43
+ if (view) {
44
+ const {formModel} = this;
45
+ formModel.init({
46
+ ...view,
47
+ owner: view.owner ?? capitalize(parent.viewManagerModel.globalDisplayName)
48
+ });
49
+ formModel.readonly = !view.isEditable;
50
+ }
51
+ },
52
+ fireImmediately: true
53
+ });
54
+ }
55
+
56
+ async saveAsync() {
57
+ const {parent, view, formModel} = this,
58
+ {name, group, description, isDefaultPinned, isShared} = formModel.getData(),
59
+ isValid = await formModel.validateAsync(),
60
+ isDirty = formModel.isDirty;
61
+
62
+ if (!isValid || !isDirty) return;
63
+
64
+ if (view.isOwned && view.isShared != isShared) {
65
+ const msg: ReactNode = !isShared
66
+ ? `Your ${view.typedName} will no longer be visible to all other ${XH.appName} users.`
67
+ : `Your ${view.typedName} will become visible to all other ${XH.appName} users.`;
68
+ const msgs = [msg, strong('Are you sure you want to proceed?')];
69
+
70
+ const confirmed = await XH.confirm({
71
+ message: fragment(msgs.map(m => p(m))),
72
+ confirmProps: {
73
+ text: 'Yes, update sharing',
74
+ outlined: true,
75
+ autoFocus: false,
76
+ intent: 'primary'
77
+ }
78
+ });
79
+ if (!confirmed) return;
80
+ }
81
+
82
+ await parent.updateAsync(view, {
83
+ name,
84
+ group,
85
+ description,
86
+ isShared,
87
+ isDefaultPinned
88
+ });
89
+ }
90
+
91
+ //------------------------
92
+ // Implementation
93
+ //------------------------
94
+ private createFormModel(): FormModel {
95
+ return new FormModel({
96
+ fields: [
97
+ {
98
+ name: 'name',
99
+ rules: [
100
+ async ({value}) => {
101
+ return this.parent.viewManagerModel.validateViewNameAsync(
102
+ value,
103
+ this.view
104
+ );
105
+ }
106
+ ]
107
+ },
108
+ {name: 'owner'},
109
+ {name: 'group'},
110
+ {name: 'description'},
111
+ {name: 'isShared'},
112
+ {name: 'isDefaultPinned'}
113
+ ]
114
+ });
115
+ }
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "71.0.0-SNAPSHOT.1733854822950",
3
+ "version": "71.0.0-SNAPSHOT.1734551243081",
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",
@@ -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 {pickBy} from 'lodash';
8
+ import {isUndefined, omitBy} from 'lodash';
9
9
 
10
10
  export interface JsonBlob {
11
11
  /** Either null for private blobs or special token "*" for globally shared blobs. */
@@ -91,9 +91,9 @@ export class JsonBlobService extends HoistService {
91
91
  /** Modify mutable properties of an existing JSONBlob, as identified by its unique token. */
92
92
  async updateAsync(
93
93
  token: string,
94
- {acl, description, meta, name, value}: Partial<JsonBlob>
94
+ {acl, description, meta, name, owner, value}: Partial<JsonBlob>
95
95
  ): Promise<JsonBlob> {
96
- const update = pickBy({acl, description, meta, name, value}, (v, k) => v !== undefined);
96
+ const update = omitBy({acl, description, meta, name, owner, value}, isUndefined);
97
97
  return XH.fetchJson({
98
98
  url: 'xh/updateJsonBlob',
99
99
  params: {
@@ -59,7 +59,7 @@ export abstract class BaseStorageService extends HoistService {
59
59
  }
60
60
 
61
61
  clear() {
62
- this.storeInstance().clear();
62
+ this.storeInstance.clear();
63
63
  }
64
64
 
65
65
  keys(): string[] {