@xh/hoist 78.0.0-SNAPSHOT.1762557528853 → 78.0.0-SNAPSHOT.1762557871581

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 CHANGED
@@ -33,6 +33,11 @@
33
33
  * Fixed `AgGridModel.getExpandState()` not returning a full representation of expanded groups -
34
34
  an issue that primarily affected linked tree map visualizations.
35
35
 
36
+ ### ⚙️ Technical
37
+
38
+ * Support Grails 7 service name conventions in admin client (backward compatible)
39
+
40
+
36
41
  ## 76.2.0 - 2025-10-22
37
42
 
38
43
  ### ⚙️ Technical
@@ -152,7 +152,13 @@ export class ServiceModel extends BaseInstanceModel {
152
152
  }
153
153
 
154
154
  private processRawData(r: PlainObject) {
155
- const provider = r.name && r.name.startsWith('hoistCore') ? 'Hoist' : 'App';
155
+ // For Grails <=6, plugin is prefix in name.
156
+ // For Grails >7, we provide class to determine provider
157
+ // TODO: simplify when Hoist v34+ required.
158
+ const provider =
159
+ r.name.startsWith('hoistCore') || r.className?.startsWith('io.xh.hoist')
160
+ ? 'Hoist'
161
+ : 'App';
156
162
  const displayName = lowerFirst(r.name.replace('hoistCore', ''));
157
163
  return {provider, displayName, ...r};
158
164
  }
@@ -7,7 +7,7 @@
7
7
  import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
8
8
  import {AppModel} from '@xh/hoist/admin/AppModel';
9
9
  import * as Col from '@xh/hoist/admin/columns';
10
- import {hbox, hspacer} from '@xh/hoist/cmp/layout';
10
+ import {br, fragment, hbox, hspacer} from '@xh/hoist/cmp/layout';
11
11
  import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
12
12
  import {FieldSpec} from '@xh/hoist/data';
13
13
  import {defaultReadonlyRenderer} from '@xh/hoist/desktop/cmp/form';
@@ -17,14 +17,14 @@ import {
17
17
  cloneAction,
18
18
  deleteAction,
19
19
  editAction,
20
- RestGridModel,
21
- RestStore
20
+ RestGridModel
22
21
  } from '@xh/hoist/desktop/cmp/rest';
22
+ import {Icon} from '@xh/hoist/icon';
23
23
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
24
+ import {pluralize} from '@xh/hoist/utils/js';
24
25
  import {isNil, truncate} from 'lodash';
25
26
  import {DifferModel} from '../../../differ/DifferModel';
26
27
  import {RegroupDialogModel} from '../../../regroup/RegroupDialogModel';
27
- import {Icon} from '@xh/hoist/icon';
28
28
 
29
29
  export class ConfigPanelModel extends HoistModel {
30
30
  override persistWith = {localStorageKey: 'xhAdminConfigState'};
@@ -43,18 +43,27 @@ export class ConfigPanelModel extends HoistModel {
43
43
  super();
44
44
  makeObservable(this);
45
45
 
46
- const required = true,
46
+ const {regroupAction} = this.regroupDialogModel,
47
+ required = true,
47
48
  enableCreate = true,
48
49
  hidden = true;
49
50
 
50
51
  this.gridModel = new RestGridModel({
51
- readonly: AppModel.readonly,
52
- persistWith: this.persistWith,
52
+ // Core config
53
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
53
54
  colChooserModel: true,
54
55
  enableExport: true,
55
56
  exportOptions: {filename: exportFilenameWithDate('configs')},
57
+ filterFields: ['name', 'value', 'groupName', 'note'],
58
+ groupBy: 'groupName',
59
+ persistWith: this.persistWith,
60
+ prepareCloneFn: ({clone}) => (clone.name = `${clone.name}_CLONE`),
61
+ readonly: AppModel.readonly,
56
62
  selModel: 'multiple',
57
- store: new RestStore({
63
+ sortBy: 'name',
64
+ unit: 'config',
65
+ // Store + fields
66
+ store: {
58
67
  url: 'rest/configAdmin',
59
68
  reloadLookupsOnLoad: true,
60
69
  fieldDefaults: {enableXssProtection: false},
@@ -83,25 +92,8 @@ export class ConfigPanelModel extends HoistModel {
83
92
  editable: false
84
93
  }
85
94
  ]
86
- }),
87
- actionWarning: {
88
- del: records =>
89
- `Are you sure you want to delete ${records.length} config(s)? Deleting configs can break running apps.`
90
95
  },
91
- toolbarActions: [addAction, editAction, cloneAction, deleteAction],
92
- menuActions: [
93
- addAction,
94
- editAction,
95
- cloneAction,
96
- deleteAction,
97
- '-',
98
- this.regroupDialogModel.regroupAction
99
- ],
100
- prepareCloneFn: ({clone}) => (clone.name = `${clone.name}_CLONE`),
101
- unit: 'config',
102
- filterFields: ['name', 'value', 'groupName', 'note'],
103
- sortBy: 'name',
104
- groupBy: 'groupName',
96
+ // Cols + editors
105
97
  columns: [
106
98
  {...Col.groupName, hidden},
107
99
  {...Col.name},
@@ -138,7 +130,19 @@ export class ConfigPanelModel extends HoistModel {
138
130
  {field: 'clientVisible'},
139
131
  {field: 'lastUpdated'},
140
132
  {field: 'lastUpdatedBy'}
141
- ]
133
+ ],
134
+ // Actions
135
+ actionWarning: {
136
+ del: records =>
137
+ fragment(
138
+ `Are you sure you want to delete ${pluralize('selected config', records.length, true)}?`,
139
+ br(),
140
+ br(),
141
+ `Deleting configs can break running apps.`
142
+ )
143
+ },
144
+ menuActions: [addAction, editAction, cloneAction, deleteAction, '-', regroupAction],
145
+ toolbarActions: [addAction, editAction, cloneAction, deleteAction]
142
146
  });
143
147
  }
144
148
 
@@ -25,12 +25,19 @@ export class UserPreferenceModel extends HoistModel {
25
25
  hidden = true;
26
26
 
27
27
  this.gridModel = new RestGridModel({
28
- readonly: AppModel.readonly,
29
- persistWith: {localStorageKey: 'xhAdminUserPreferenceState'},
28
+ // Core config
29
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
30
30
  colChooserModel: true,
31
31
  enableExport: true,
32
32
  exportOptions: {filename: exportFilenameWithDate('user-prefs')},
33
+ filterFields: ['name', 'username'],
34
+ groupBy: 'groupName',
35
+ persistWith: {localStorageKey: 'xhAdminUserPreferenceState'},
36
+ readonly: AppModel.readonly,
33
37
  selModel: 'multiple',
38
+ sortBy: 'name',
39
+ unit: 'user preference',
40
+ // Store + fields
34
41
  store: {
35
42
  url: 'rest/userPreferenceAdmin',
36
43
  reloadLookupsOnLoad: true,
@@ -55,10 +62,7 @@ export class UserPreferenceModel extends HoistModel {
55
62
  {...(Col.lastUpdatedBy.field as FieldSpec), editable: false}
56
63
  ]
57
64
  },
58
- sortBy: 'name',
59
- groupBy: 'groupName',
60
- unit: 'user preference',
61
- filterFields: ['name', 'username'],
65
+ // Cols + editors
62
66
  columns: [
63
67
  {...Col.name},
64
68
  {...Col.type},
@@ -7,11 +7,13 @@
7
7
  import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
8
8
  import {AppModel} from '@xh/hoist/admin/AppModel';
9
9
  import * as Col from '@xh/hoist/admin/columns';
10
+ import {br, fragment} from '@xh/hoist/cmp/layout';
10
11
  import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
11
12
  import {FieldSpec} from '@xh/hoist/data';
12
13
  import {textArea} from '@xh/hoist/desktop/cmp/input';
13
14
  import {addAction, deleteAction, editAction, RestGridModel} from '@xh/hoist/desktop/cmp/rest';
14
15
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
16
+ import {pluralize} from '@xh/hoist/utils/js';
15
17
  import {DifferModel} from '../../../../differ/DifferModel';
16
18
  import {RegroupDialogModel} from '../../../../regroup/RegroupDialogModel';
17
19
 
@@ -32,18 +34,26 @@ export class PrefEditorModel extends HoistModel {
32
34
  super();
33
35
  makeObservable(this);
34
36
 
35
- const required = true,
37
+ const {regroupAction} = this.regroupDialogModel,
38
+ required = true,
36
39
  enableCreate = true,
37
40
  hidden = true;
38
41
 
39
42
  this.gridModel = new RestGridModel({
40
- readonly: AppModel.readonly,
41
- persistWith: this.persistWith,
43
+ // Core config
44
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
42
45
  colChooserModel: true,
43
46
  enableExport: true,
44
47
  exportOptions: {filename: exportFilenameWithDate('prefs')},
48
+ filterFields: ['name', 'groupName'],
49
+ groupBy: 'groupName',
50
+ persistWith: this.persistWith,
51
+ readonly: AppModel.readonly,
45
52
  selModel: 'multiple',
46
53
  showRefreshButton: true,
54
+ sortBy: 'name',
55
+ unit: 'preference',
56
+ // Store + fields
47
57
  store: {
48
58
  url: 'rest/preferenceAdmin',
49
59
  reloadLookupsOnLoad: true,
@@ -68,21 +78,7 @@ export class PrefEditorModel extends HoistModel {
68
78
  {...(Col.lastUpdatedBy.field as FieldSpec), editable: false}
69
79
  ]
70
80
  },
71
- sortBy: 'name',
72
- groupBy: 'groupName',
73
- unit: 'preference',
74
- filterFields: ['name', 'groupName'],
75
- actionWarning: {
76
- del: records =>
77
- `Are you sure you want to delete ${records.length} preference(s)? Deleting preferences can break running apps.`
78
- },
79
- menuActions: [
80
- addAction,
81
- editAction,
82
- deleteAction,
83
- '-',
84
- this.regroupDialogModel.regroupAction
85
- ],
81
+ // Cols + Editors
86
82
  columns: [
87
83
  {...Col.name},
88
84
  {...Col.type},
@@ -100,7 +96,18 @@ export class PrefEditorModel extends HoistModel {
100
96
  {field: 'notes', formField: {item: textArea({height: 100})}},
101
97
  {field: 'lastUpdated'},
102
98
  {field: 'lastUpdatedBy'}
103
- ]
99
+ ],
100
+ // Actions
101
+ actionWarning: {
102
+ del: records =>
103
+ fragment(
104
+ `Are you sure you want to delete ${pluralize('selected preference', records.length, true)}?`,
105
+ br(),
106
+ br(),
107
+ `Deleting preference definitions can break running apps.`
108
+ )
109
+ },
110
+ menuActions: [addAction, editAction, deleteAction, '-', regroupAction]
104
111
  });
105
112
  }
106
113
 
@@ -20,11 +20,6 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
20
20
  maxRows?: number;
21
21
  /** Padding inside the container [x, y] in pixels. Defaults to same as `margin`. */
22
22
  containerPadding?: [number, number];
23
- /**
24
- * Whether an overlay with an Add View button should be rendered
25
- * when the canvas is empty. Default true.
26
- */
27
- showAddViewButtonWhenEmpty?: boolean;
28
23
  }
29
24
  export interface DashCanvasItemState {
30
25
  layout: DashCanvasItemLayout;
@@ -50,9 +45,7 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
50
45
  compact: boolean;
51
46
  margin: [number, number];
52
47
  containerPadding: [number, number];
53
- DROPPING_ELEM_ID: string;
54
48
  maxRows: number;
55
- showAddViewButtonWhenEmpty: boolean;
56
49
  /** Current number of rows in canvas */
57
50
  get rows(): number;
58
51
  get isEmpty(): boolean;
@@ -61,7 +54,7 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
61
54
  isResizing: boolean;
62
55
  private isLoadingState;
63
56
  get rglLayout(): any[];
64
- constructor({ viewSpecs, viewSpecDefaults, initialState, layoutLocked, contentLocked, renameLocked, persistWith, emptyText, addViewButtonText, columns, rowHeight, compact, margin, maxRows, containerPadding, extraMenuItems, showAddViewButtonWhenEmpty }: DashCanvasConfig);
57
+ constructor({ viewSpecs, viewSpecDefaults, initialState, layoutLocked, contentLocked, renameLocked, persistWith, emptyText, addViewButtonText, columns, rowHeight, compact, margin, maxRows, containerPadding, extraMenuItems }: DashCanvasConfig);
65
58
  /** Removes all views from the canvas */
66
59
  clear(): void;
67
60
  /**
@@ -84,16 +77,6 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
84
77
  width?: number;
85
78
  height?: number;
86
79
  }): DashCanvasViewModel;
87
- dropViewIntoCanvas(specId: string, opts: {
88
- title: string;
89
- state: any;
90
- layout: {
91
- x: number;
92
- y: number;
93
- w: number;
94
- h: number;
95
- };
96
- }, rglLayout: any[]): DashCanvasViewModel;
97
80
  /**
98
81
  * Remove a view from the DashCanvas
99
82
  * @param id - DashCanvasViewModel id to remove from the container
@@ -1,10 +1,11 @@
1
- import { RowDoubleClickedEvent } from '@xh/hoist/kit/ag-grid';
2
1
  import { BaseFieldConfig } from '@xh/hoist/cmp/form';
3
2
  import { GridConfig, GridModel } from '@xh/hoist/cmp/grid';
4
3
  import { ElementSpec, HoistModel, PlainObject } from '@xh/hoist/core';
5
4
  import '@xh/hoist/desktop/register';
6
5
  import { RecordAction, RecordActionSpec, StoreRecord } from '@xh/hoist/data';
6
+ import { RowDoubleClickedEvent } from '@xh/hoist/kit/ag-grid';
7
7
  import { ExportOptions } from '@xh/hoist/svc';
8
+ import { ReactNode } from 'react';
8
9
  import { FormFieldProps } from '../form';
9
10
  import { RestStore, RestStoreConfig } from './data/RestStore';
10
11
  import { RestFormModel } from './impl/RestFormModel';
@@ -22,9 +23,9 @@ export interface RestGridConfig extends GridConfig {
22
23
  showRefreshButton?: boolean;
23
24
  /** Warning to display before actions on a selection of records. */
24
25
  actionWarning?: {
25
- add?: string | ((recs: StoreRecord[]) => string);
26
- del?: string | ((recs: StoreRecord[]) => string);
27
- edit?: string | ((recs: StoreRecord[]) => string);
26
+ add?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
27
+ del?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
28
+ edit?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
28
29
  };
29
30
  /** Name that describes records in this grid. */
30
31
  unit?: string;
@@ -16,9 +16,9 @@ export declare class RestFormModel extends HoistModel {
16
16
  dialogRef: import("react").RefObject<HTMLElement>;
17
17
  get unit(): string;
18
18
  get actionWarning(): {
19
- add?: string | ((recs: import("@xh/hoist/data").StoreRecord[]) => string);
20
- del?: string | ((recs: import("@xh/hoist/data").StoreRecord[]) => string);
21
- edit?: string | ((recs: import("@xh/hoist/data").StoreRecord[]) => string);
19
+ add?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
20
+ del?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
21
+ edit?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
22
22
  };
23
23
  get actions(): (import("@xh/hoist/data").RecordActionSpec | import("@xh/hoist/data").RecordAction)[];
24
24
  get editors(): RestGridEditor[];
@@ -98,7 +98,7 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
98
98
  ),
99
99
  ...rglOptions
100
100
  }),
101
- emptyContainerOverlay({omit: !model.showAddViewButtonWhenEmpty})
101
+ emptyContainerOverlay()
102
102
  ],
103
103
  [TEST_ID]: testId
104
104
  })
@@ -50,12 +50,6 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
50
50
 
51
51
  /** Padding inside the container [x, y] in pixels. Defaults to same as `margin`. */
52
52
  containerPadding?: [number, number];
53
-
54
- /**
55
- * Whether an overlay with an Add View button should be rendered
56
- * when the canvas is empty. Default true.
57
- */
58
- showAddViewButtonWhenEmpty?: boolean;
59
53
  }
60
54
 
61
55
  export interface DashCanvasItemState {
@@ -92,9 +86,7 @@ export class DashCanvasModel
92
86
  //-----------------------------
93
87
  // Public properties
94
88
  //-----------------------------
95
- DROPPING_ELEM_ID = '__dropping-elem__';
96
89
  maxRows: number;
97
- showAddViewButtonWhenEmpty: boolean;
98
90
 
99
91
  /** Current number of rows in canvas */
100
92
  get rows(): number {
@@ -114,25 +106,21 @@ export class DashCanvasModel
114
106
  private isLoadingState: boolean;
115
107
 
116
108
  get rglLayout() {
117
- return this.layout
118
- .map(it => {
119
- const dashCanvasView = this.getView(it.i);
120
- if (!dashCanvasView) return null;
121
-
122
- const {autoHeight, viewSpec} = dashCanvasView;
123
-
124
- return {
125
- ...it,
126
- resizeHandles: autoHeight
127
- ? ['w', 'e']
128
- : ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'],
129
- maxH: viewSpec.maxHeight,
130
- minH: viewSpec.minHeight,
131
- maxW: viewSpec.maxWidth,
132
- minW: viewSpec.minWidth
133
- };
134
- })
135
- .filter(Boolean);
109
+ return this.layout.map(it => {
110
+ const dashCanvasView = this.getView(it.i),
111
+ {autoHeight, viewSpec} = dashCanvasView;
112
+
113
+ return {
114
+ ...it,
115
+ resizeHandles: autoHeight
116
+ ? ['w', 'e']
117
+ : ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'],
118
+ maxH: viewSpec.maxHeight,
119
+ minH: viewSpec.minHeight,
120
+ maxW: viewSpec.maxWidth,
121
+ minW: viewSpec.minWidth
122
+ };
123
+ });
136
124
  }
137
125
 
138
126
  constructor({
@@ -151,8 +139,7 @@ export class DashCanvasModel
151
139
  margin = [10, 10],
152
140
  maxRows = Infinity,
153
141
  containerPadding = margin,
154
- extraMenuItems,
155
- showAddViewButtonWhenEmpty = true
142
+ extraMenuItems
156
143
  }: DashCanvasConfig) {
157
144
  super();
158
145
  makeObservable(this);
@@ -200,7 +187,6 @@ export class DashCanvasModel
200
187
  this.emptyText = emptyText;
201
188
  this.addViewButtonText = addViewButtonText;
202
189
  this.extraMenuItems = extraMenuItems;
203
- this.showAddViewButtonWhenEmpty = showAddViewButtonWhenEmpty;
204
190
 
205
191
  this.loadState(initialState);
206
192
  this.state = this.buildState();
@@ -275,27 +261,6 @@ export class DashCanvasModel
275
261
  return this.addViewInternal(specId, {title, layout, state});
276
262
  }
277
263
 
278
- @action dropViewIntoCanvas(
279
- specId: string,
280
- opts: {
281
- title: string;
282
- state: any;
283
- layout: {
284
- x: number;
285
- y: number;
286
- w: number;
287
- h: number;
288
- };
289
- },
290
- rglLayout: any[]
291
- ): DashCanvasViewModel {
292
- const newViewModel: DashCanvasViewModel = this.addViewInternal(specId, opts),
293
- droppingItem: any = rglLayout.find(it => it.i === this.DROPPING_ELEM_ID);
294
- droppingItem.i = newViewModel.id;
295
- this.onRglLayoutChange(rglLayout);
296
- return newViewModel;
297
- }
298
-
299
264
  /**
300
265
  * Remove a view from the DashCanvas
301
266
  * @param id - DashCanvasViewModel id to remove from the container
@@ -421,8 +386,6 @@ export class DashCanvasModel
421
386
 
422
387
  onRglLayoutChange(rglLayout) {
423
388
  rglLayout = rglLayout.map(it => pick(it, ['i', 'x', 'y', 'w', 'h']));
424
- if (rglLayout.some(it => it.i === this.DROPPING_ELEM_ID)) return;
425
-
426
389
  this.setLayout(rglLayout);
427
390
  }
428
391
 
@@ -5,15 +5,16 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {RowDoubleClickedEvent} from '@xh/hoist/kit/ag-grid';
9
8
  import {BaseFieldConfig} from '@xh/hoist/cmp/form';
10
9
  import {GridConfig, GridModel} from '@xh/hoist/cmp/grid';
11
10
  import {ElementSpec, HoistModel, managed, PlainObject, XH} from '@xh/hoist/core';
12
11
  import '@xh/hoist/desktop/register';
13
12
  import {RecordAction, RecordActionSpec, StoreRecord} from '@xh/hoist/data';
13
+ import {RowDoubleClickedEvent} from '@xh/hoist/kit/ag-grid';
14
14
  import {ExportOptions} from '@xh/hoist/svc';
15
15
  import {pluralize, throwIf, withDefault} from '@xh/hoist/utils/js';
16
16
  import {isFunction} from 'lodash';
17
+ import {ReactNode} from 'react';
17
18
  import {FormFieldProps} from '../form';
18
19
  import {addAction, deleteAction, editAction, viewAction} from './Actions';
19
20
  import {RestStore, RestStoreConfig} from './data/RestStore';
@@ -39,9 +40,9 @@ export interface RestGridConfig extends GridConfig {
39
40
 
40
41
  /** Warning to display before actions on a selection of records. */
41
42
  actionWarning?: {
42
- add?: string | ((recs: StoreRecord[]) => string);
43
- del?: string | ((recs: StoreRecord[]) => string);
44
- edit?: string | ((recs: StoreRecord[]) => string);
43
+ add?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
44
+ del?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
45
+ edit?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
45
46
  };
46
47
 
47
48
  /** Name that describes records in this grid. */
@@ -109,11 +110,7 @@ export class RestGridModel extends HoistModel {
109
110
  add: null,
110
111
  edit: null,
111
112
  del: recs =>
112
- recs.length > 1
113
- ? `Are you sure you want to delete the selected ${recs.length} ${pluralize(
114
- this.unit
115
- )}?`
116
- : `Are you sure you want to delete the selected ${this.unit}?`
113
+ `Are you sure you want to delete ${pluralize(`selected ${this.unit}`, recs.length, true)}?`
117
114
  };
118
115
 
119
116
  @managed gridModel: GridModel = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "78.0.0-SNAPSHOT.1762557528853",
3
+ "version": "78.0.0-SNAPSHOT.1762557871581",
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",