@xh/hoist 64.0.3 → 64.0.5

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
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 64.0.5 - 2024-06-14
4
+
5
+ ### 🐞 Bug Fixes
6
+
7
+ * Added a workaround for a bug where mobile Safari auto-zooms on orientation change if the user has zoomed the page.
8
+
9
+ ### ⚙️ Technical
10
+
11
+ * Misc. Improvements to logout behavior of `MsalClient`
12
+
13
+ ### 📚 Libraries
14
+
15
+ * @azure/msal-browser `3.14.0 → 3.17.0
16
+
17
+ ## 64.0.4 - 2024-06-05
18
+
19
+ ### ⚙️ Technical
20
+
21
+ * Typescript: Improve `ref` typing in JSX.
22
+
3
23
  ## 64.0.3 - 2024-05-31
4
24
 
5
25
  ### 🐞 Bug Fixes
@@ -8,7 +28,8 @@
8
28
 
9
29
  ### ⚙️ Technical
10
30
 
11
- * Adjustments to API of (beta) `BaseOAuthClient`, `MsaClient`, and `AuthZeroClient`.
31
+ * Adjustments to API of (beta) `BaseOAuthClient`, `MsalClient`, and `AuthZeroClient`.
32
+ `
12
33
 
13
34
  ## 64.0.2 - 2024-05-23
14
35
 
@@ -5,16 +5,16 @@
5
5
  * Copyright © 2024 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {FilterChooserModel} from '@xh/hoist/cmp/filter';
8
- import {GridModel} from '@xh/hoist/cmp/grid';
8
+ import {GridModel, tagsRenderer, TreeStyle} from '@xh/hoist/cmp/grid';
9
9
  import * as Col from '@xh/hoist/cmp/grid/columns';
10
10
  import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
11
11
  import {RecordActionSpec} from '@xh/hoist/data';
12
12
  import {actionCol, calcActionColWidth} from '@xh/hoist/desktop/cmp/grid';
13
13
  import {fmtDate} from '@xh/hoist/format';
14
14
  import {Icon} from '@xh/hoist/icon';
15
- import {action, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
15
+ import {action, bindable, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
16
16
  import {wait} from '@xh/hoist/promise';
17
- import {compact, groupBy, isEmpty, mapValues} from 'lodash';
17
+ import {compact, groupBy, mapValues} from 'lodash';
18
18
  import moment from 'moment/moment';
19
19
  import {RoleEditorModel} from './editor/RoleEditorModel';
20
20
  import {HoistRole, RoleMemberType, RoleModuleConfig} from './Types';
@@ -38,17 +38,33 @@ export class RoleModel extends HoistModel {
38
38
  @observable.ref allRoles: HoistRole[] = [];
39
39
  @observable.ref moduleConfig: RoleModuleConfig;
40
40
 
41
+ @bindable showInGroups = true;
42
+
41
43
  get readonly() {
42
44
  return !XH.getUser().isHoistRoleManager;
43
45
  }
44
46
 
45
47
  get selectedRole(): HoistRole {
46
- return this.gridModel.selectedRecord?.data as HoistRole;
48
+ const selected = this.gridModel.selectedRecord?.data;
49
+ if (selected && !selected.isGroupRow) return selected as HoistRole;
50
+ return null;
47
51
  }
48
52
 
49
53
  constructor() {
50
54
  super();
51
55
  makeObservable(this);
56
+ this.addReaction({
57
+ track: () => this.showInGroups,
58
+ run: showInGroups => {
59
+ const {gridModel} = this;
60
+ if (showInGroups) {
61
+ gridModel.hideColumn('category');
62
+ } else {
63
+ gridModel.showColumn('category');
64
+ }
65
+ this.displayRoles();
66
+ }
67
+ });
52
68
  }
53
69
 
54
70
  override async doLoadAsync(loadSpec: LoadSpec) {
@@ -59,7 +75,8 @@ export class RoleModel extends HoistModel {
59
75
  const {data} = await XH.fetchJson({url: 'roleAdmin/list', loadSpec});
60
76
  if (loadSpec.isStale) return;
61
77
 
62
- this.setRoles(this.processRolesFromServer(data));
78
+ this.allRoles = this.processRolesFromServer(data);
79
+ this.displayRoles();
63
80
  await this.gridModel.preSelectFirstAsync();
64
81
  } catch (e) {
65
82
  if (loadSpec.isStale) return;
@@ -77,12 +94,6 @@ export class RoleModel extends HoistModel {
77
94
  return gridModel.selectAsync(name);
78
95
  }
79
96
 
80
- @action
81
- setRoles(roles: HoistRole[]) {
82
- this.allRoles = roles;
83
- this.gridModel.loadData(roles);
84
- }
85
-
86
97
  @action
87
98
  clear() {
88
99
  this.allRoles = [];
@@ -129,6 +140,9 @@ export class RoleModel extends HoistModel {
129
140
  tooltip: 'Add or remove users from this role.',
130
141
  icon: Icon.edit(),
131
142
  intent: 'primary',
143
+ displayFn: ({record}) => ({
144
+ disabled: !record || record.data.isGroupRow
145
+ }),
132
146
  actionFn: ({record}) => this.editAsync(record.data as HoistRole),
133
147
  recordsRequired: true
134
148
  };
@@ -138,6 +152,9 @@ export class RoleModel extends HoistModel {
138
152
  return {
139
153
  text: 'Clone',
140
154
  icon: Icon.copy(),
155
+ displayFn: ({record}) => ({
156
+ disabled: !record || record.data.isGroupRow
157
+ }),
141
158
  actionFn: ({record}) => this.createAsync(record.data as HoistRole),
142
159
  recordsRequired: true
143
160
  };
@@ -148,6 +165,9 @@ export class RoleModel extends HoistModel {
148
165
  text: 'Delete',
149
166
  icon: Icon.delete(),
150
167
  intent: 'danger',
168
+ displayFn: ({record}) => ({
169
+ disabled: !record || record.data.isGroupRow
170
+ }),
151
171
  actionFn: ({record}) =>
152
172
  this.deleteAsync(record.data as HoistRole)
153
173
  .catchDefault()
@@ -159,18 +179,11 @@ export class RoleModel extends HoistModel {
159
179
  private groupByAction(): RecordActionSpec {
160
180
  return {
161
181
  text: 'Group By Category',
162
- displayFn: ({gridModel}) => ({
163
- icon: isEmpty(gridModel.groupBy) ? Icon.circle() : Icon.checkCircle()
182
+ displayFn: () => ({
183
+ icon: this.showInGroups ? Icon.checkCircle() : Icon.circle()
164
184
  }),
165
- actionFn: ({gridModel}) => {
166
- if (isEmpty(gridModel.groupBy)) {
167
- gridModel.setGroupBy('category');
168
- gridModel.hideColumn('category');
169
- } else {
170
- gridModel.setGroupBy(null);
171
- gridModel.showColumn('category');
172
- gridModel.autosizeAsync();
173
- }
185
+ actionFn: () => {
186
+ this.showInGroups = !this.showInGroups;
174
187
  }
175
188
  };
176
189
  }
@@ -183,6 +196,17 @@ export class RoleModel extends HoistModel {
183
196
  // -------------------------------
184
197
  // Implementation
185
198
  // -------------------------------
199
+
200
+ private displayRoles() {
201
+ const {gridModel} = this,
202
+ gridData = this.showInGroups
203
+ ? this.processRolesForTreeGrid(this.allRoles)
204
+ : this.allRoles;
205
+ gridModel.loadData(gridData);
206
+ gridModel.expandAll();
207
+ gridModel.autosizeAsync({includeCollapsedChildren: true});
208
+ }
209
+
186
210
  private async ensureInitializedAsync() {
187
211
  if (!this.moduleConfig) {
188
212
  const config = await XH.fetchJson({url: 'roleAdmin/config'});
@@ -196,9 +220,7 @@ export class RoleModel extends HoistModel {
196
220
  }
197
221
  }
198
222
 
199
- private processRolesFromServer(
200
- roles: Omit<HoistRole, 'users' | 'directoryGroups' | 'roles'>[]
201
- ): HoistRole[] {
223
+ private processRolesFromServer(roles: Partial<HoistRole>[]): HoistRole[] {
202
224
  return roles.map(role => {
203
225
  const membersByType = mapValues(groupBy(role.members, 'type'), members =>
204
226
  members.map(member => member.name)
@@ -208,8 +230,30 @@ export class RoleModel extends HoistModel {
208
230
  users: membersByType['USER'] ?? [],
209
231
  directoryGroups: membersByType['DIRECTORY_GROUP'] ?? [],
210
232
  roles: membersByType['ROLE'] ?? []
211
- };
233
+ } as HoistRole;
234
+ });
235
+ }
236
+
237
+ private processRolesForTreeGrid(roles: HoistRole[]) {
238
+ const root = [];
239
+ roles.forEach(role => {
240
+ const categories = role.category ? role.category.split('\\') : ['Uncategorized'];
241
+
242
+ let children = root,
243
+ id = '';
244
+ categories.forEach(category => {
245
+ let currCat = children.find(it => it.name === category && it.isGroupRow);
246
+ if (!currCat) {
247
+ currCat = {name: category, children: [], isGroupRow: true};
248
+ currCat.id = `${id}-${currCat.name}`;
249
+ children.push(currCat);
250
+ }
251
+ children = currCat.children;
252
+ id = currCat.id;
253
+ });
254
+ children.push(role);
212
255
  });
256
+ return root;
213
257
  }
214
258
 
215
259
  private async createAsync(roleSpec?: HoistRole): Promise<void> {
@@ -221,25 +265,31 @@ export class RoleModel extends HoistModel {
221
265
 
222
266
  private createGridModel(): GridModel {
223
267
  return new GridModel({
268
+ treeMode: true,
269
+ treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
224
270
  autosizeOptions: {mode: 'managed'},
225
271
  emptyText: 'No roles found.',
226
272
  colChooserModel: true,
227
- sortBy: 'name|asc',
273
+ sortBy: 'name',
228
274
  enableExport: true,
229
275
  exportOptions: {filename: 'roles'},
230
276
  filterModel: true,
231
- groupBy: 'category',
232
- groupRowRenderer: ({value}) => (!value ? 'Uncategorized' : value),
277
+ rowClassRules: {
278
+ 'xh-grid-clear-background-color': ({data}) => !data.data.isGroupRow
279
+ },
233
280
  headerMenuDisplay: 'hover',
234
- onRowDoubleClicked: ({data: record}) =>
235
- !this.readonly &&
236
- record &&
237
- this.roleEditorModel
238
- .editAsync(record.data)
239
- .then(role => role && this.refreshAsync()),
281
+ onRowDoubleClicked: ({data: record}) => {
282
+ if (!this.readonly && record && record.data.isGroupRow) {
283
+ this.roleEditorModel
284
+ .editAsync(record.data)
285
+ .then(role => role && this.refreshAsync());
286
+ }
287
+ },
240
288
  persistWith: {...this.persistWith, path: 'mainGrid'},
241
289
  store: {
242
- idSpec: 'name',
290
+ idSpec: ({id, name}) => {
291
+ return id ?? name;
292
+ },
243
293
  fields: [
244
294
  {name: 'users', displayName: 'Assigned Users', type: 'tags'},
245
295
  {name: 'directoryGroups', displayName: 'Assigned Groups', type: 'tags'},
@@ -250,6 +300,7 @@ export class RoleModel extends HoistModel {
250
300
  {name: 'effectiveRoles', type: 'json'},
251
301
  {name: 'errors', type: 'json'},
252
302
  {name: 'inheritedRoleNames', displayName: 'Inherited Roles', type: 'tags'},
303
+ {name: 'isGroupRow', type: 'bool'},
253
304
  {name: 'effectiveUserNames', displayName: 'Users', type: 'tags'},
254
305
  {
255
306
  name: 'effectiveDirectoryGroupNames',
@@ -261,10 +312,11 @@ export class RoleModel extends HoistModel {
261
312
  ],
262
313
  processRawData: raw => ({
263
314
  ...raw,
264
- effectiveUserNames: raw.effectiveUsers.map(it => it.name),
265
- effectiveDirectoryGroupNames: raw.effectiveDirectoryGroups.map(it => it.name),
266
- effectiveRoleNames: raw.effectiveRoles.map(it => it.name),
267
- inheritedRoleNames: raw.inheritedRoles.map(it => it.name)
315
+ effectiveUserNames: raw.effectiveUsers?.map(it => it.name),
316
+ effectiveDirectoryGroupNames: raw.effectiveDirectoryGroups?.map(it => it.name),
317
+ effectiveRoleNames: raw.effectiveRoles?.map(it => it.name),
318
+ inheritedRoleNames: raw.inheritedRoles?.map(it => it.name),
319
+ isGroupRow: !!raw.isGroupRow
268
320
  })
269
321
  },
270
322
  colDefaults: {
@@ -278,14 +330,18 @@ export class RoleModel extends HoistModel {
278
330
  actions: [this.editAction()],
279
331
  omit: this.readonly
280
332
  },
281
- {field: {name: 'name', type: 'string'}},
282
- {field: {name: 'category', type: 'string'}, hidden: true},
333
+ {field: {name: 'name', type: 'string'}, isTreeColumn: true},
334
+ {
335
+ field: {name: 'category', type: 'string'},
336
+ hidden: true,
337
+ renderer: v => tagsRenderer(v?.split('\\'))
338
+ },
283
339
  {field: {name: 'lastUpdated', type: 'date'}, ...Col.dateTime, hidden: true},
284
340
  {field: {name: 'lastUpdatedBy', type: 'string'}, hidden: true},
285
341
  {field: {name: 'notes', type: 'string'}, filterable: false, flex: 1}
286
342
  ],
287
343
  contextMenu: this.readonly
288
- ? GridModel.defaultContextMenu
344
+ ? [this.groupByAction(), ...GridModel.defaultContextMenu]
289
345
  : [
290
346
  this.addAction(),
291
347
  this.editAction(),
@@ -10,6 +10,7 @@ import {creates, hoistCmp} from '@xh/hoist/core';
10
10
  import {button} from '@xh/hoist/desktop/cmp/button';
11
11
  import {errorMessage} from '@xh/hoist/desktop/cmp/error';
12
12
  import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
13
+ import {switchInput} from '@xh/hoist/desktop/cmp/input';
13
14
  import {panel} from '@xh/hoist/desktop/cmp/panel';
14
15
  import {recordActionBar} from '@xh/hoist/desktop/cmp/record';
15
16
  import {Icon} from '@xh/hoist/icon';
@@ -42,7 +43,8 @@ export const rolePanel = hoistCmp.factory({
42
43
  selModel: gridModel.selModel
43
44
  }),
44
45
  '-',
45
- filterChooser({flex: 1})
46
+ filterChooser({flex: 1}),
47
+ switchInput({bind: 'showInGroups', label: 'Show in Groups', labelSide: 'left'})
46
48
  ],
47
49
  item: hframe(vframe(grid(), roleGraph()), detailsPanel())
48
50
  }),
@@ -5,10 +5,11 @@
5
5
  * Copyright © 2024 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {chart} from '@xh/hoist/cmp/chart';
8
+ import {errorBoundary} from '@xh/hoist/cmp/error';
8
9
  import {div, hspacer, placeholder} from '@xh/hoist/cmp/layout';
9
10
  import {creates, hoistCmp} from '@xh/hoist/core';
10
11
  import {button} from '@xh/hoist/desktop/cmp/button';
11
- import {buttonGroupInput, slider} from '@xh/hoist/desktop/cmp/input';
12
+ import {buttonGroupInput, slider, switchInput} from '@xh/hoist/desktop/cmp/input';
12
13
  import {panel} from '@xh/hoist/desktop/cmp/panel';
13
14
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
14
15
  import {Icon} from '@xh/hoist/icon';
@@ -31,7 +32,7 @@ export const roleGraph = hoistCmp.factory({
31
32
  item: div({
32
33
  item: div({
33
34
  style: {margin: 'auto'},
34
- item: content()
35
+ item: errorBoundary(content())
35
36
  }),
36
37
  style: {
37
38
  display: 'flex',
@@ -78,6 +79,10 @@ export const roleGraph = hoistCmp.factory({
78
79
  max: 2,
79
80
  stepSize: 0.005,
80
81
  labelRenderer: false
82
+ }),
83
+ 'Limit to one level',
84
+ switchInput({
85
+ bind: 'limitToOneLevel'
81
86
  })
82
87
  ],
83
88
  omit: !role
@@ -8,7 +8,7 @@ import {ChartModel} from '@xh/hoist/cmp/chart';
8
8
  import {HoistModel, lookup, managed, PlainObject} from '@xh/hoist/core';
9
9
  import {bindable, computed} from '@xh/hoist/mobx';
10
10
  import {wait} from '@xh/hoist/promise';
11
- import {isEmpty, isMatch, sortBy, sumBy} from 'lodash';
11
+ import {compact, isEmpty, isMatch, sortBy, sumBy} from 'lodash';
12
12
  import {RoleModel} from '../RoleModel';
13
13
  import {EffectiveRoleMember, HoistRole} from '../Types';
14
14
 
@@ -22,6 +22,8 @@ export class RoleGraphModel extends HoistModel {
22
22
 
23
23
  @bindable widthScale: number = 1.0;
24
24
 
25
+ @bindable limitToOneLevel: boolean = true;
26
+
25
27
  get relatedRoles(): EffectiveRoleMember[] {
26
28
  const {role, relationship} = this;
27
29
  if (!role) return [];
@@ -35,7 +37,7 @@ export class RoleGraphModel extends HoistModel {
35
37
 
36
38
  @computed
37
39
  get size() {
38
- const {inverted, maxDepth, leafCount, widthScale} = this;
40
+ const {inverted, leafCount, maxDepth, widthScale} = this;
39
41
  if (inverted) {
40
42
  const AVG_WIDTH = 150,
41
43
  AVG_HEIGHT = 26;
@@ -57,7 +59,7 @@ export class RoleGraphModel extends HoistModel {
57
59
  const {chartModel} = this;
58
60
  this.addReaction(
59
61
  {
60
- track: () => [this.role, this.relationship],
62
+ track: () => [this.role, this.relationship, this.limitToOneLevel],
61
63
  run: async ([role]) => {
62
64
  chartModel.clear(); // avoid HC rendering glitches
63
65
  await wait();
@@ -86,15 +88,15 @@ export class RoleGraphModel extends HoistModel {
86
88
  // Implementation
87
89
  // -------------------------------
88
90
  private getSeriesData(): PlainObject[] {
89
- const {role, relatedRoles} = this,
90
- {name} = role;
91
+ const {role, relatedRoles, limitToOneLevel} = this,
92
+ {name: rootName} = role;
91
93
  if (isEmpty(relatedRoles)) return [];
92
94
  const alreadyAdded = new Set<string>();
93
95
  return [
94
96
  {
95
- id: name,
97
+ id: rootName,
96
98
  // Replace spaces with non-breaking spaces to prevent wrapping.
97
- name: name.replaceAll(' ', '&nbsp'),
99
+ name: rootName.replaceAll(' ', '&nbsp'),
98
100
  dataLabels: {
99
101
  style: {
100
102
  fontWeight: 600
@@ -105,24 +107,28 @@ export class RoleGraphModel extends HoistModel {
105
107
  fillColor: 'var(--xh-bg-alt)'
106
108
  }
107
109
  },
108
- ...sortBy(relatedRoles, 'name').flatMap(({name, sourceRoles}) =>
109
- [...sourceRoles]
110
- .sort((a, b) => {
111
- if (a === role.name) return -1;
112
- if (b === role.name) return 1;
113
- return a > b ? 1 : -1;
114
- })
115
- .map(source => {
116
- // Adds a space to the id to differentiate subsequent nodes from the single expanded one.
117
- const id = alreadyAdded.has(name) ? `${name} ` : name;
118
- alreadyAdded.add(name);
119
- return {
120
- id,
121
- // Replace spaces with non-breaking spaces to prevent wrapping.
122
- name: name.replaceAll(' ', '&nbsp'),
123
- parent: source
124
- };
125
- })
110
+ ...compact(
111
+ sortBy(relatedRoles, 'name').flatMap(({name, sourceRoles}) =>
112
+ [...sourceRoles]
113
+ .sort((a, b) => {
114
+ if (a === role.name) return -1;
115
+ if (b === role.name) return 1;
116
+ return a > b ? 1 : -1;
117
+ })
118
+ .map(source => {
119
+ // Omit all non-root nodes if limitToOneLevel is true
120
+ if (limitToOneLevel && source !== rootName) return null;
121
+ // Adds a space to the id to differentiate subsequent nodes from the single expanded one.
122
+ const id = alreadyAdded.has(name) ? `${name} ` : name;
123
+ alreadyAdded.add(name);
124
+ return {
125
+ id,
126
+ // Replace spaces with non-breaking spaces to prevent wrapping.
127
+ name: name.replaceAll(' ', '&nbsp'),
128
+ parent: source
129
+ };
130
+ })
131
+ )
126
132
  )
127
133
  ];
128
134
  }
@@ -184,7 +190,11 @@ export class RoleGraphModel extends HoistModel {
184
190
 
185
191
  @computed
186
192
  private get leafCount(): number {
187
- const {relatedRoles} = this;
193
+ const {relatedRoles, limitToOneLevel, role} = this;
194
+ // Limit to one level means that we only show the direct children of the root role.
195
+ if (limitToOneLevel)
196
+ return sumBy(relatedRoles, it => (it.sourceRoles.includes(role.name) ? 1 : 0));
197
+
188
198
  return sumBy(relatedRoles, it => {
189
199
  const hasChildren = relatedRoles.some(other => other.sourceRoles.includes(it.name)),
190
200
  parentCount = it.sourceRoles.length;
@@ -195,8 +205,11 @@ export class RoleGraphModel extends HoistModel {
195
205
 
196
206
  @computed
197
207
  private get maxDepth(): number {
198
- const {role: root, relatedRoles} = this;
208
+ const {role: root, relatedRoles, limitToOneLevel} = this;
209
+ // Only the root node.
199
210
  if (isEmpty(relatedRoles)) return 1;
211
+ // Limit to one level means that we only show two levels.
212
+ if (limitToOneLevel) return 2;
200
213
 
201
214
  const maxDepthRecursive = (roleName: string) => {
202
215
  if (roleName === root.name) return 1;
@@ -154,10 +154,20 @@ export class AppContainerModel extends HoistModel {
154
154
  // (e.g. `env(safe-area-inset-top)`). This allows us to avoid overlap with OS-level
155
155
  // controls like the iOS tab switcher, as well as to more easily set the background
156
156
  // color of the (effectively) unusable portions of the screen via
157
- const vp = document.querySelector('meta[name=viewport]'),
158
- content = vp.getAttribute('content');
159
-
160
- vp.setAttribute('content', content + ', viewport-fit=cover');
157
+ this.setViewportContent(this.getViewportContent() + ', viewport-fit=cover');
158
+
159
+ // Temporarily set maximum-scale=1 on orientation change to force reset Safari iOS
160
+ // zoom level, and then remove to restore user zooming. This is a workaround for a bug
161
+ // where Safari full-screen re-zooms on orientation change if user has *ever* zoomed.
162
+ window.addEventListener(
163
+ 'orientationchange',
164
+ () => {
165
+ const content = this.getViewportContent();
166
+ this.setViewportContent(content + ', maximum-scale=1');
167
+ setTimeout(() => this.setViewportContent(content), 0);
168
+ },
169
+ false
170
+ );
161
171
  }
162
172
 
163
173
  try {
@@ -358,4 +368,14 @@ export class AppContainerModel extends HoistModel {
358
368
  loadingPromise = mobxWhen(() => terminalStates.includes(this.appStateModel.state));
359
369
  loadingPromise.linkTo(this.appLoadModel);
360
370
  }
371
+
372
+ private setViewportContent(content: string) {
373
+ const vp = document.querySelector('meta[name=viewport]');
374
+ vp?.setAttribute('content', content);
375
+ }
376
+
377
+ private getViewportContent(): string {
378
+ const vp = document.querySelector('meta[name=viewport]');
379
+ return vp ? vp.getAttribute('content') : '';
380
+ }
361
381
  }
@@ -17,12 +17,12 @@ export declare class RoleModel extends HoistModel {
17
17
  readonly roleEditorModel: RoleEditorModel;
18
18
  allRoles: HoistRole[];
19
19
  moduleConfig: RoleModuleConfig;
20
+ showInGroups: boolean;
20
21
  get readonly(): boolean;
21
22
  get selectedRole(): HoistRole;
22
23
  constructor();
23
24
  doLoadAsync(loadSpec: LoadSpec): Promise<void>;
24
25
  selectRoleAsync(name: string): Promise<void>;
25
- setRoles(roles: HoistRole[]): void;
26
26
  clear(): void;
27
27
  applyMemberFilter(name: string, type: RoleMemberType, includeEffective: boolean): void;
28
28
  deleteAsync(role: HoistRole): Promise<boolean>;
@@ -32,8 +32,10 @@ export declare class RoleModel extends HoistModel {
32
32
  deleteAction(): RecordActionSpec;
33
33
  private groupByAction;
34
34
  editAsync(role: HoistRole): Promise<void>;
35
+ private displayRoles;
35
36
  private ensureInitializedAsync;
36
37
  private processRolesFromServer;
38
+ private processRolesForTreeGrid;
37
39
  private createAsync;
38
40
  private createGridModel;
39
41
  private createFilterChooserModel;
@@ -8,6 +8,7 @@ export declare class RoleGraphModel extends HoistModel {
8
8
  relationship: 'effective' | 'inherited';
9
9
  inverted: boolean;
10
10
  widthScale: number;
11
+ limitToOneLevel: boolean;
11
12
  get relatedRoles(): EffectiveRoleMember[];
12
13
  get role(): HoistRole;
13
14
  get size(): {
@@ -69,4 +69,6 @@ export declare class AppContainerModel extends HoistModel {
69
69
  private startOptionsDialog;
70
70
  private setAppState;
71
71
  private bindInitSequenceToAppLoadModel;
72
+ private setViewportContent;
73
+ private getViewportContent;
72
74
  }
@@ -4,14 +4,17 @@ import { ForwardedRef, FC, ReactNode } from 'react';
4
4
  /**
5
5
  * Type representing props passed to a HoistComponent's render function.
6
6
  *
7
- * This type removes from its base type several props that are used by HoistComponent itself and
8
- * not provided to the render function.
7
+ * This type removes from its base type several properties that are pulled out by the HoistComponent itself and
8
+ * not provided to the render function. `modelConfig` and `modelRef` are resolved into the `model` property.
9
+ * `ref` is passed as the second argument to the render function.
9
10
  */
10
11
  export type RenderPropsOf<P extends HoistProps> = P & {
11
12
  /** Pre-processed by HoistComponent internals into a mounted model. Never passed to render. */
12
13
  modelConfig: never;
13
14
  /** Pre-processed by HoistComponent internals and attached to model. Never passed to render. */
14
15
  modelRef: never;
16
+ /** Pre-processed by HoistComponent internals and passed as second argument to render. */
17
+ ref: never;
15
18
  };
16
19
  /**
17
20
  * Configuration for creating a Component. May be specified either as a render function,
@@ -1,6 +1,6 @@
1
1
  import { HoistModel } from '@xh/hoist/core';
2
2
  import { Property } from 'csstype';
3
- import { CSSProperties, HTMLAttributes, ReactNode, Ref } from 'react';
3
+ import { CSSProperties, HTMLAttributes, LegacyRef, ReactNode, Ref } from 'react';
4
4
  /**
5
5
  * Props interface for Hoist Components.
6
6
  *
@@ -32,6 +32,8 @@ export interface HoistProps<M extends HoistModel = HoistModel> {
32
32
  className?: string;
33
33
  /** React children. */
34
34
  children?: ReactNode;
35
+ /** React Ref for this component. */
36
+ ref?: LegacyRef<any>;
35
37
  }
36
38
  /**
37
39
  * A version of Hoist props that allows dynamic keys/properties. This is the interface that
@@ -1,5 +1,5 @@
1
1
  import { TEST_ID } from '@xh/hoist/utils/js';
2
- import { ForwardedRef, Key, ReactElement, ReactNode } from 'react';
2
+ import { Key, ReactElement, ReactNode } from 'react';
3
3
  import { Some, Thunkable } from './types/Types';
4
4
  /**
5
5
  * Alternative format for specifying React Elements in render functions. This type is designed to
@@ -31,8 +31,6 @@ export type ElementSpec<P> = P & {
31
31
  item?: Some<ReactNode>;
32
32
  /** True to exclude the Element. */
33
33
  omit?: Thunkable<boolean>;
34
- /** React Ref for this component. */
35
- ref?: ForwardedRef<any>;
36
34
  /** React key for this component. */
37
35
  key?: Key;
38
36
  /**
@@ -36,6 +36,7 @@ export declare const autoRefreshAppOption: ({ formFieldProps, inputProps }?: Aut
36
36
  modelRef?: import("react").Ref<import("../../../cmp/form").FieldModel>;
37
37
  className?: string;
38
38
  children?: import("react").ReactNode;
39
+ ref?: import("react").LegacyRef<any>;
39
40
  margin?: string | number;
40
41
  marginTop?: string | number;
41
42
  marginRight?: string | number;
@@ -34,6 +34,7 @@ export declare const themeAppOption: ({ formFieldProps, inputProps }?: ThemeAppO
34
34
  modelRef?: import("react").Ref<import("../../../cmp/form").FieldModel>;
35
35
  className?: string;
36
36
  children?: import("react").ReactNode;
37
+ ref?: import("react").LegacyRef<any>;
37
38
  margin?: string | number;
38
39
  marginTop?: string | number;
39
40
  marginRight?: string | number;
@@ -21,7 +21,7 @@ export interface FileChooserProps extends HoistProps<FileChooserModel>, BoxProps
21
21
  * True (default) to display the selected file(s) in a grid alongside the dropzone. Note
22
22
  * that, if false, the component will not provide any built-in indication of its selection.
23
23
  */
24
- showFileGrid: boolean;
24
+ showFileGrid?: boolean;
25
25
  /** Intro/help text to display within the dropzone target. */
26
26
  targetText?: ReactNode;
27
27
  }