@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 +22 -1
- package/admin/tabs/userData/roles/RoleModel.ts +99 -43
- package/admin/tabs/userData/roles/RolePanel.ts +3 -1
- package/admin/tabs/userData/roles/graph/RoleGraph.ts +7 -2
- package/admin/tabs/userData/roles/graph/RoleGraphModel.ts +40 -27
- package/appcontainer/AppContainerModel.ts +24 -4
- package/build/types/admin/tabs/userData/roles/RoleModel.d.ts +3 -1
- package/build/types/admin/tabs/userData/roles/graph/RoleGraphModel.d.ts +1 -0
- package/build/types/appcontainer/AppContainerModel.d.ts +2 -0
- package/build/types/core/HoistComponent.d.ts +5 -2
- package/build/types/core/HoistProps.d.ts +3 -1
- package/build/types/core/elem.d.ts +1 -3
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +1 -0
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +1 -0
- package/build/types/desktop/cmp/filechooser/FileChooser.d.ts +1 -1
- package/build/types/icon/Icon.d.ts +7 -4
- package/build/types/utils/react/LayoutPropUtils.d.ts +4 -4
- package/cmp/grid/Grid.scss +5 -0
- package/core/HoistComponent.ts +7 -2
- package/core/HoistProps.ts +4 -1
- package/core/elem.ts +0 -4
- package/desktop/cmp/filechooser/FileChooser.ts +1 -1
- package/desktop/cmp/input/ButtonGroupInput.ts +4 -3
- package/icon/Icon.ts +7 -4
- package/mobile/appcontainer/ToastSource.ts +2 -1
- package/package.json +6 -5
- package/security/BaseOAuthClient.ts +1 -1
- package/security/msal/MsalClient.ts +8 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/react/LayoutPropUtils.ts +4 -4
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`, `
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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: (
|
|
163
|
-
icon:
|
|
182
|
+
displayFn: () => ({
|
|
183
|
+
icon: this.showInGroups ? Icon.checkCircle() : Icon.circle()
|
|
164
184
|
}),
|
|
165
|
-
actionFn: (
|
|
166
|
-
|
|
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
|
|
273
|
+
sortBy: 'name',
|
|
228
274
|
enableExport: true,
|
|
229
275
|
exportOptions: {filename: 'roles'},
|
|
230
276
|
filterModel: true,
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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:
|
|
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
|
|
265
|
-
effectiveDirectoryGroupNames: raw.effectiveDirectoryGroups
|
|
266
|
-
effectiveRoleNames: raw.effectiveRoles
|
|
267
|
-
inheritedRoleNames: raw.inheritedRoles
|
|
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
|
-
{
|
|
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,
|
|
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:
|
|
97
|
+
id: rootName,
|
|
96
98
|
// Replace spaces with non-breaking spaces to prevent wrapping.
|
|
97
|
-
name:
|
|
99
|
+
name: rootName.replaceAll(' ', ' '),
|
|
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
|
-
...
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
id
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(' ', ' '),
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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;
|
|
@@ -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
|
|
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 {
|
|
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
|
|
24
|
+
showFileGrid?: boolean;
|
|
25
25
|
/** Intro/help text to display within the dropzone target. */
|
|
26
26
|
targetText?: ReactNode;
|
|
27
27
|
}
|