@xh/hoist 70.0.0 → 71.0.0-SNAPSHOT.1731971865033

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.
@@ -5,19 +5,36 @@ import {ViewTree} from '@xh/hoist/core/persist/viewmanager';
5
5
  import {ViewManagerModel} from '@xh/hoist/core/persist/viewmanager/ViewManagerModel';
6
6
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
7
7
  import {switchInput} from '@xh/hoist/desktop/cmp/input';
8
- import {manageDialog} from '@xh/hoist/desktop/cmp/viewmanager/cmp/ManageDialog';
9
- import {saveDialog} from '@xh/hoist/desktop/cmp/viewmanager/cmp/SaveDialog';
8
+ import {manageDialog} from './impl/ManageDialog';
9
+ import {saveDialog} from './impl/SaveDialog';
10
10
  import {Icon} from '@xh/hoist/icon';
11
11
  import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
12
12
  import {consumeEvent, pluralize} from '@xh/hoist/utils/js';
13
13
  import {isEmpty} from 'lodash';
14
14
  import {ReactNode} from 'react';
15
15
 
16
+ /**
17
+ * Visibility options for save/revert button.
18
+ *
19
+ * 'never' to hide button.
20
+ * 'whenDirty' to only show when persistence state is dirty and button is therefore enabled.
21
+ * 'always' will always show button, unless autoSave is active.
22
+ *
23
+ * Note that we never show the button when 'autoSave' is active because it would never be enabled
24
+ * for more than a flash.
25
+ */
26
+ export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
27
+
16
28
  export interface ViewManagerProps extends HoistProps<ViewManagerModel> {
17
29
  menuButtonProps?: Partial<ButtonProps>;
18
30
  saveButtonProps?: Partial<ButtonProps>;
19
- /** 'whenDirty' to only show saveButton when persistence state is dirty. (Default 'whenDirty') */
20
- showSaveButton?: 'whenDirty' | 'always' | 'never';
31
+ revertButtonProps?: Partial<ButtonProps>;
32
+
33
+ /** Default 'whenDirty' */
34
+ showSaveButton?: ViewManagerStateButtonMode;
35
+ /** Default 'never' */
36
+ showRevertButton?: ViewManagerStateButtonMode;
37
+
21
38
  /** True to render private views in sub-menu (Default false)*/
22
39
  showPrivateViewsInSubMenu?: boolean;
23
40
  /** True to render shared views in sub-menu (Default false)*/
@@ -40,7 +57,9 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
40
57
  className,
41
58
  menuButtonProps,
42
59
  saveButtonProps,
60
+ revertButtonProps,
43
61
  showSaveButton = 'whenDirty',
62
+ showRevertButton = 'never',
44
63
  showPrivateViewsInSubMenu = false,
45
64
  showSharedViewsInSubMenu = false
46
65
  }: ViewManagerProps) {
@@ -55,8 +74,12 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
55
74
  popoverClassName: 'xh-view-manager__popover'
56
75
  }),
57
76
  saveButton({
58
- showSaveButton,
77
+ mode: showSaveButton,
59
78
  ...saveButtonProps
79
+ }),
80
+ revertButton({
81
+ mode: showRevertButton,
82
+ ...revertButtonProps
60
83
  })
61
84
  ]
62
85
  }),
@@ -74,7 +97,7 @@ const menuButton = hoistCmp.factory<ViewManagerModel>({
74
97
  const {selectedView, DisplayName} = model;
75
98
  return button({
76
99
  className: 'xh-view-manager__menu-button',
77
- text: model.getHierarchyDisplayName(selectedView?.name) ?? `Default ${DisplayName}`,
100
+ text: selectedView?.shortName ?? `Default ${DisplayName}`,
78
101
  icon: Icon.bookmark(),
79
102
  rightIcon: Icon.chevronDown(),
80
103
  outlined: true,
@@ -84,86 +107,109 @@ const menuButton = hoistCmp.factory<ViewManagerModel>({
84
107
  });
85
108
 
86
109
  const saveButton = hoistCmp.factory<ViewManagerModel>({
87
- render({model, showSaveButton, ...rest}) {
88
- if (
89
- !model.canShowSaveButton ||
90
- showSaveButton === 'never' ||
91
- (showSaveButton === 'whenDirty' && !model.isDirty)
92
- ) {
93
- return null;
94
- }
95
-
110
+ render({model, mode, ...rest}) {
111
+ if (hideStateButton(model, mode)) return null;
96
112
  return button({
97
113
  className: 'xh-view-manager__save-button',
98
114
  icon: Icon.save(),
99
115
  tooltip: `Save changes to this ${model.displayName}`,
100
116
  intent: 'primary',
101
- disabled: !model.canSave,
102
- onClick: () => model.saveAsync(false).linkTo(model.loadModel),
117
+ disabled: !model.isDirty,
118
+ onClick: () => {
119
+ model.canSave ? model.saveAsync() : model.saveAsAsync();
120
+ },
121
+ ...rest
122
+ });
123
+ }
124
+ });
125
+
126
+ const revertButton = hoistCmp.factory<ViewManagerModel>({
127
+ render({model, mode, ...rest}) {
128
+ if (hideStateButton(model, mode)) return null;
129
+ return button({
130
+ className: 'xh-view-manager__revert-button',
131
+ icon: Icon.reset(),
132
+ tooltip: `Revert changes to this ${model.displayName}`,
133
+ intent: 'danger',
134
+ disabled: !model.isDirty,
135
+ onClick: () => model.resetAsync(),
103
136
  ...rest
104
137
  });
105
138
  }
106
139
  });
107
140
 
141
+ function hideStateButton(model: ViewManagerModel, mode: ViewManagerStateButtonMode): boolean {
142
+ return mode === 'never' || (mode === 'whenDirty' && !model.isDirty) || model.canAutoSave;
143
+ }
144
+
108
145
  const viewMenu = hoistCmp.factory<ViewManagerProps>({
109
146
  render({model, showPrivateViewsInSubMenu, showSharedViewsInSubMenu}) {
110
- const {DisplayName} = model,
111
- pluralDisp = pluralize(DisplayName),
112
- items = [];
147
+ const {
148
+ autoSaveUnavailableReason,
149
+ enableDefault,
150
+ canSave,
151
+ selectedToken,
152
+ enableAutoSave,
153
+ DisplayName,
154
+ autoSave,
155
+ privateViewTree,
156
+ sharedViewTree,
157
+ favoriteViews,
158
+ views,
159
+ isDirty
160
+ } = model;
113
161
 
114
- if (!isEmpty(model.favoriteViews)) {
162
+ const pluralDisp = pluralize(DisplayName),
163
+ items = [];
164
+ if (!isEmpty(favoriteViews)) {
115
165
  items.push(
116
166
  menuDivider({title: 'Favorites'}),
117
- ...model.favoriteViews.map(it => {
167
+ ...favoriteViews.map(it => {
118
168
  return menuItem({
119
169
  key: `${it.token}-favorite`,
120
170
  icon: model.selectedToken === it.token ? Icon.check() : Icon.placeholder(),
121
171
  text: menuItemTextAndFaveToggle({
122
- view: {...it, text: model.getHierarchyDisplayName(it.name)}
172
+ view: {...it, text: it.shortName}
123
173
  }),
124
- onClick: () => model.selectViewAsync(it.token).linkTo(model.loadModel),
174
+ onClick: () => model.selectViewAsync(it.token),
125
175
  title: it.description
126
176
  });
127
177
  })
128
178
  );
129
179
  }
130
180
 
131
- if (!isEmpty(model.privateViewTree)) {
181
+ if (!isEmpty(privateViewTree)) {
132
182
  if (showPrivateViewsInSubMenu) {
133
183
  items.push(
134
184
  menuDivider({omit: isEmpty(items)}),
135
185
  menuItem({
136
186
  text: `My ${pluralDisp}`,
137
187
  shouldDismissPopover: false,
138
- children: model.privateViewTree.map(it => {
139
- return buildMenuItem(it, model);
140
- })
188
+ items: privateViewTree.map(it => buildMenuItem(it, model))
141
189
  })
142
190
  );
143
191
  } else {
144
192
  items.push(
145
193
  menuDivider({title: `My ${pluralDisp}`}),
146
- ...model.privateViewTree.map(it => buildMenuItem(it, model))
194
+ ...privateViewTree.map(it => buildMenuItem(it, model))
147
195
  );
148
196
  }
149
197
  }
150
198
 
151
- if (!isEmpty(model.sharedViewTree)) {
199
+ if (!isEmpty(sharedViewTree)) {
152
200
  if (showSharedViewsInSubMenu) {
153
201
  items.push(
154
202
  menuDivider({omit: isEmpty(items)}),
155
203
  menuItem({
156
204
  text: `Shared ${pluralDisp}`,
157
205
  shouldDismissPopover: false,
158
- children: model.sharedViewTree.map(it => {
159
- return buildMenuItem(it, model);
160
- })
206
+ items: sharedViewTree.map(it => buildMenuItem(it, model))
161
207
  })
162
208
  );
163
209
  } else {
164
210
  items.push(
165
211
  menuDivider({title: `Shared ${pluralDisp}`}),
166
- ...model.sharedViewTree.map(it => buildMenuItem(it, model))
212
+ ...sharedViewTree.map(it => buildMenuItem(it, model))
167
213
  );
168
214
  }
169
215
  }
@@ -172,19 +218,19 @@ const viewMenu = hoistCmp.factory<ViewManagerProps>({
172
218
  className: 'xh-view-manager__menu',
173
219
  items: [
174
220
  ...items,
175
- menuDivider({omit: !model.enableDefault || isEmpty(items)}),
221
+ menuDivider({omit: !enableDefault || isEmpty(items)}),
176
222
  menuItem({
177
- icon: model.selectedToken ? Icon.placeholder() : Icon.check(),
223
+ icon: selectedToken ? Icon.placeholder() : Icon.check(),
178
224
  text: `Default ${DisplayName}`,
179
- omit: !model.enableDefault,
225
+ omit: !enableDefault,
180
226
  onClick: () => model.selectViewAsync(null)
181
227
  }),
182
228
  menuDivider(),
183
229
  menuItem({
184
230
  icon: Icon.save(),
185
231
  text: 'Save',
186
- disabled: !model.canSave,
187
- onClick: () => model.saveAsync(false)
232
+ disabled: !canSave || !isDirty,
233
+ onClick: () => model.saveAsync()
188
234
  }),
189
235
  menuItem({
190
236
  icon: Icon.copy(),
@@ -193,26 +239,27 @@ const viewMenu = hoistCmp.factory<ViewManagerProps>({
193
239
  }),
194
240
  menuItem({
195
241
  icon: Icon.reset(),
196
- text: `Revert ${DisplayName}`,
197
- disabled: !model.isDirty,
242
+ text: `Revert`,
243
+ disabled: !isDirty,
198
244
  onClick: () => model.resetAsync()
199
245
  }),
200
- menuDivider({omit: !model.enableAutoSave}),
246
+ menuDivider({omit: !enableAutoSave}),
201
247
  menuItem({
202
- omit: !model.enableAutoSave,
248
+ omit: !enableAutoSave,
203
249
  text: switchInput({
204
250
  label: 'Auto Save',
205
- bind: 'autoSaveActive',
206
- inline: true,
207
- disabled: !model.enableAutoSaveToggle
251
+ value: !autoSaveUnavailableReason && autoSave,
252
+ disabled: !!autoSaveUnavailableReason,
253
+ onChange: v => (model.autoSave = v),
254
+ inline: true
208
255
  }),
209
- title: model.disabledAutoSaveReason,
256
+ title: autoSaveUnavailableReason,
210
257
  shouldDismissPopover: false
211
258
  }),
212
259
  menuDivider(),
213
260
  menuItem({
214
261
  icon: Icon.gear(),
215
- disabled: isEmpty(model.views),
262
+ disabled: isEmpty(views),
216
263
  text: `Manage ${pluralDisp}...`,
217
264
  onClick: () => model.openManageDialog()
218
265
  })
@@ -231,7 +278,7 @@ function buildMenuItem(viewOrFolder: ViewTree, model: ViewManagerModel): ReactNo
231
278
  text,
232
279
  icon,
233
280
  shouldDismissPopover: false,
234
- children: viewOrFolder.items
281
+ items: viewOrFolder.items
235
282
  ? viewOrFolder.items.map(child => buildMenuItem(child, model))
236
283
  : []
237
284
  });
@@ -242,7 +289,7 @@ function buildMenuItem(viewOrFolder: ViewTree, model: ViewManagerModel): ReactNo
242
289
  icon,
243
290
  text: menuItemTextAndFaveToggle({model, view: viewOrFolder}),
244
291
  title: viewOrFolder.description,
245
- onClick: () => model.selectViewAsync(viewOrFolder.token).linkTo(model.loadModel)
292
+ onClick: () => model.selectViewAsync(viewOrFolder.token)
246
293
  });
247
294
  }
248
295
  }
@@ -59,7 +59,7 @@ const formPanel = hoistCmp.factory<ManageDialogProps>({
59
59
  isOwnView = values.owner === XH.getUsername();
60
60
 
61
61
  if (model.hasMultiSelection) {
62
- return multiSelectionPanel();
62
+ return multiSelectionPanel({onClose});
63
63
  }
64
64
 
65
65
  if (!model.selectedId)
@@ -1,3 +1 @@
1
1
  export * from './ViewManager';
2
- export * from './cmp/ManageDialog';
3
- export * from './cmp/SaveDialog';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "70.0.0",
3
+ "version": "71.0.0-SNAPSHOT.1731971865033",
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",
@@ -58,15 +58,12 @@ export class JsonBlobService extends HoistService {
58
58
  }
59
59
 
60
60
  /** Retrieve all blobs of a particular type that are visible to the current user. */
61
- async listAsync({
62
- type,
63
- includeValue,
64
- loadSpec
65
- }: {
61
+ async listAsync(spec: {
66
62
  type: string;
67
63
  includeValue?: boolean;
68
64
  loadSpec?: LoadSpec;
69
- }) {
65
+ }): Promise<JsonBlob[]> {
66
+ const {type, includeValue, loadSpec} = spec;
70
67
  return XH.fetchJson({
71
68
  url: 'xh/listJsonBlobs',
72
69
  params: {type, includeValue},