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

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 (41) hide show
  1. package/build/types/cmp/viewmanager/View.d.ts +5 -0
  2. package/build/types/cmp/viewmanager/ViewInfo.d.ts +32 -7
  3. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +34 -31
  4. package/build/types/cmp/viewmanager/ViewToBlobApi.d.ts +28 -6
  5. package/build/types/cmp/viewmanager/index.d.ts +1 -1
  6. package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +0 -4
  7. package/build/types/desktop/cmp/viewmanager/ViewManagerLocalModel.d.ts +10 -0
  8. package/build/types/desktop/cmp/viewmanager/ViewMenu.d.ts +2 -2
  9. package/build/types/desktop/cmp/viewmanager/dialog/ManageDialog.d.ts +4 -3
  10. package/build/types/desktop/cmp/viewmanager/dialog/ManageDialogModel.d.ts +19 -10
  11. package/build/types/desktop/cmp/viewmanager/dialog/SaveAsDialog.d.ts +1 -1
  12. package/build/types/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.d.ts +3 -9
  13. package/build/types/desktop/cmp/viewmanager/dialog/Utils.d.ts +3 -0
  14. package/build/types/desktop/cmp/viewmanager/dialog/ViewMultiPanel.d.ts +1 -0
  15. package/build/types/desktop/cmp/viewmanager/dialog/ViewPanel.d.ts +5 -0
  16. package/build/types/desktop/cmp/viewmanager/dialog/{EditFormModel.d.ts → ViewPanelModel.d.ts} +2 -4
  17. package/build/types/svc/JsonBlobService.d.ts +1 -1
  18. package/cmp/viewmanager/View.ts +21 -1
  19. package/cmp/viewmanager/ViewInfo.ts +58 -11
  20. package/cmp/viewmanager/ViewManagerModel.ts +86 -81
  21. package/cmp/viewmanager/ViewToBlobApi.ts +91 -35
  22. package/cmp/viewmanager/index.ts +1 -1
  23. package/desktop/cmp/viewmanager/ViewManager.scss +25 -28
  24. package/desktop/cmp/viewmanager/ViewManager.ts +28 -26
  25. package/desktop/cmp/viewmanager/ViewManagerLocalModel.ts +28 -0
  26. package/desktop/cmp/viewmanager/ViewMenu.ts +162 -169
  27. package/desktop/cmp/viewmanager/dialog/ManageDialog.ts +67 -40
  28. package/desktop/cmp/viewmanager/dialog/ManageDialogModel.ts +238 -127
  29. package/desktop/cmp/viewmanager/dialog/SaveAsDialog.ts +30 -9
  30. package/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.ts +35 -40
  31. package/desktop/cmp/viewmanager/dialog/Utils.ts +18 -0
  32. package/desktop/cmp/viewmanager/dialog/ViewMultiPanel.ts +70 -0
  33. package/desktop/cmp/viewmanager/dialog/ViewPanel.ts +161 -0
  34. package/desktop/cmp/viewmanager/dialog/ViewPanelModel.ts +116 -0
  35. package/package.json +1 -1
  36. package/svc/JsonBlobService.ts +3 -3
  37. package/svc/storage/BaseStorageService.ts +1 -1
  38. package/tsconfig.tsbuildinfo +1 -1
  39. package/build/types/desktop/cmp/viewmanager/dialog/EditForm.d.ts +0 -5
  40. package/desktop/cmp/viewmanager/dialog/EditForm.ts +0 -126
  41. 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({
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.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,161 @@
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
+
32
+ return panel({
33
+ item: form({
34
+ fieldDefaults: {
35
+ commitOnChange: true,
36
+ minimal: true
37
+ },
38
+ item: vframe({
39
+ className: 'xh-view-manager__manage-dialog__form',
40
+ items: [
41
+ formField({
42
+ field: 'name',
43
+ item: textInput()
44
+ }),
45
+ formField({
46
+ field: 'owner',
47
+ omit: isEditable
48
+ }),
49
+ formField({
50
+ field: 'group',
51
+ item: select({
52
+ enableCreate: true,
53
+ enableClear: true,
54
+ options: getGroupOptions(
55
+ model.parent.viewManagerModel,
56
+ view.isOwned ? 'owned' : 'global'
57
+ )
58
+ }),
59
+ readonlyRenderer: v =>
60
+ v || span({item: 'None provided', className: 'xh-text-color-muted'})
61
+ }),
62
+ formField({
63
+ field: 'description',
64
+ item: textArea({
65
+ selectOnFocus: true,
66
+ height: 70
67
+ }),
68
+ readonlyRenderer: v =>
69
+ v || span({item: 'None provided', className: 'xh-text-color-muted'})
70
+ }),
71
+ formField({
72
+ field: 'isShared',
73
+ label: 'Shared?',
74
+ inline: true,
75
+ item: switchInput(),
76
+ readonlyRenderer: v => (v ? 'Yes' : 'No'),
77
+ omit: isGlobal || !isEditable
78
+ }),
79
+ formField({
80
+ field: 'isDefaultPinned',
81
+ label: 'Pin by default?',
82
+ labelWidth: 110,
83
+ inline: true,
84
+ item: switchInput(),
85
+ omit: !isGlobal || !isEditable
86
+ }),
87
+ vspacer(),
88
+ formButtons(),
89
+ filler(),
90
+ div({
91
+ className: 'xh-view-manager__manage-dialog__metadata',
92
+ item: `Last Updated: ${fmtDateTime(lastUpdated)} by ${lastUpdatedBy === XH.getUsername() ? 'you' : lastUpdatedBy}`
93
+ })
94
+ ]
95
+ })
96
+ })
97
+ });
98
+ }
99
+ });
100
+
101
+ const formButtons = hoistCmp.factory<ViewPanelModel>({
102
+ render({model}) {
103
+ const {formModel, parent, view} = model,
104
+ {readonly} = formModel,
105
+ {isPinned} = view;
106
+
107
+ return formModel.isDirty
108
+ ? hbox({
109
+ justifyContent: 'center',
110
+ items: [
111
+ button({
112
+ text: 'Save Changes',
113
+ icon: Icon.check(),
114
+ intent: 'success',
115
+ minimal: false,
116
+ disabled: !formModel.isValid,
117
+ onClick: () => model.saveAsync()
118
+ }),
119
+ hspacer(),
120
+ button({
121
+ icon: Icon.reset(),
122
+ tooltip: 'Revert changes',
123
+ minimal: false,
124
+ onClick: () => formModel.reset()
125
+ })
126
+ ]
127
+ })
128
+ : vbox({
129
+ style: {gap: 10, alignItems: 'center'},
130
+ items: [
131
+ button({
132
+ text: isPinned ? 'Unpin from your Menu' : 'Pin to your Menu',
133
+ icon: Icon.pin({
134
+ prefix: isPinned ? 'fas' : 'far',
135
+ className: isPinned ? 'xh-yellow' : null
136
+ }),
137
+ width: 200,
138
+ outlined: true,
139
+ onClick: () => parent.togglePinned([view])
140
+ }),
141
+ button({
142
+ text: `Promote to ${capitalize(parent.globalDisplayName)} ${parent.typeDisplayName}`,
143
+ icon: Icon.globe(),
144
+ width: 200,
145
+ outlined: true,
146
+ omit: readonly || view.isGlobal || !parent.manageGlobal,
147
+ onClick: () => parent.makeGlobalAsync(view)
148
+ }),
149
+ button({
150
+ text: 'Delete',
151
+ icon: Icon.delete(),
152
+ width: 200,
153
+ outlined: true,
154
+ intent: 'danger',
155
+ omit: readonly,
156
+ onClick: () => parent.deleteAsync([view])
157
+ })
158
+ ]
159
+ });
160
+ }
161
+ });
@@ -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.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.1734118787755",
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[] {