Skip to content

Commit c420f08

Browse files
authored
Bulk role category update (#3730)
1 parent f0df6f8 commit c420f08

File tree

7 files changed

+182
-27
lines changed

7 files changed

+182
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Use this prop to customize markdown rendering.
99
* New `mergeDeep` method provided in `@xh/hoist/utils/js` as an alternative to `lodash.merge`,
1010
without lodash's surprising deep-merging of array-based properties.
11+
* Enhanced Roles Admin UI to support bulk category reassignment.
1112

1213
### 🐞 Bug Fixes
1314

admin/regroup/RegroupDialog.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import {filler} from '@xh/hoist/cmp/layout';
88
import {hoistCmp, uses} from '@xh/hoist/core';
99
import {button} from '@xh/hoist/desktop/cmp/button';
10+
import {select} from '@xh/hoist/desktop/cmp/input';
1011
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
1112
import {Icon} from '@xh/hoist/icon';
1213
import {dialog, dialogBody} from '@xh/hoist/kit/blueprint';
13-
import {select} from '@xh/hoist/desktop/cmp/input';
1414

1515
import {RegroupDialogModel} from './RegroupDialogModel';
1616

@@ -23,7 +23,7 @@ export const regroupDialog = hoistCmp.factory({
2323

2424
return dialog({
2525
title: 'Change Group',
26-
icon: Icon.grip(),
26+
icon: Icon.folder(),
2727
style: {width: 300},
2828
isOpen: true,
2929
isCloseButtonShown: false,

admin/regroup/RegroupDialogModel.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,38 @@
44
*
55
* Copyright © 2024 Extremely Heavy Industries Inc.
66
*/
7-
import {uniq} from 'lodash';
87
import {HoistModel, XH} from '@xh/hoist/core';
9-
import {action, bindable, observable, makeObservable} from '@xh/hoist/mobx';
108
import {Icon} from '@xh/hoist/icon/Icon';
9+
import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
10+
import {uniq} from 'lodash';
1111

1212
export class RegroupDialogModel extends HoistModel {
13-
_parent;
13+
private parent;
1414

1515
@bindable groupName = null;
1616
@observable isOpen = false;
1717

1818
regroupAction = {
1919
text: 'Change Group',
20-
icon: Icon.grip(),
20+
icon: Icon.folder(),
2121
recordsRequired: true,
2222
actionFn: () => this.open(),
23-
displayFn: () => ({hidden: this._parent.gridModel.readonly})
23+
displayFn: () => ({hidden: this.parent.gridModel.readonly})
2424
};
2525

2626
get options() {
27-
return uniq(this._parent.gridModel.store.allRecords.map(it => it.data.groupName)).sort();
27+
return uniq(this.parent.gridModel.store.allRecords.map(it => it.data.groupName)).sort();
2828
}
2929

3030
constructor(parent) {
3131
super();
3232
makeObservable(this);
33-
this._parent = parent;
33+
this.parent = parent;
3434
}
3535

3636
async saveAsync() {
37-
const {_parent, groupName} = this,
38-
{selectedRecords, store} = _parent.gridModel,
37+
const {parent, groupName} = this,
38+
{selectedRecords, store} = parent.gridModel,
3939
ids = selectedRecords.map(it => it.id),
4040
resp = await store.bulkUpdateRecordsAsync(ids, {groupName}),
4141
failuresPresent = resp.fail > 0,

admin/tabs/userData/roles/RoleModel.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*
55
* Copyright © 2024 Extremely Heavy Industries Inc.
66
*/
7+
import {RecategorizeDialogModel} from '@xh/hoist/admin/tabs/userData/roles/recategorize/RecategorizeDialogModel';
78
import {FilterChooserModel} from '@xh/hoist/cmp/filter';
89
import {GridModel, tagsRenderer, TreeStyle} from '@xh/hoist/cmp/grid';
910
import * as Col from '@xh/hoist/cmp/grid/columns';
@@ -34,6 +35,7 @@ export class RoleModel extends HoistModel {
3435
@managed gridModel: GridModel;
3536
@managed filterChooserModel: FilterChooserModel;
3637
@managed readonly roleEditorModel = new RoleEditorModel(this);
38+
@managed recategorizeDialogModel = new RecategorizeDialogModel(this);
3739

3840
@observable.ref allRoles: HoistRole[] = [];
3941
@observable.ref moduleConfig: RoleModuleConfig;
@@ -75,7 +77,9 @@ export class RoleModel extends HoistModel {
7577
const {data} = await XH.fetchJson({url: 'roleAdmin/list', loadSpec});
7678
if (loadSpec.isStale) return;
7779

78-
runInAction(() => (this.allRoles = this.processRolesFromServer(data)));
80+
runInAction(() => {
81+
this.allRoles = this.processRolesFromServer(data);
82+
});
7983
this.displayRoles();
8084
await this.gridModel.preSelectFirstAsync();
8185
} catch (e) {
@@ -158,7 +162,7 @@ export class RoleModel extends HoistModel {
158162
disabled: !record || record.data.isGroupRow
159163
}),
160164
actionFn: ({record}) => this.editAsync(record.data as HoistRole),
161-
recordsRequired: true
165+
recordsRequired: 1
162166
};
163167
}
164168

@@ -170,7 +174,7 @@ export class RoleModel extends HoistModel {
170174
disabled: !record || record.data.isGroupRow
171175
}),
172176
actionFn: ({record}) => this.createAsync(record.data as HoistRole),
173-
recordsRequired: true
177+
recordsRequired: 1
174178
};
175179
}
176180

@@ -186,7 +190,7 @@ export class RoleModel extends HoistModel {
186190
this.deleteAsync(record.data as HoistRole)
187191
.catchDefault()
188192
.linkTo(this.loadModel),
189-
recordsRequired: true
193+
recordsRequired: 1
190194
};
191195
}
192196

@@ -269,6 +273,7 @@ export class RoleModel extends HoistModel {
269273
treeMode: true,
270274
treeStyle: TreeStyle.HIGHLIGHTS_AND_BORDERS,
271275
autosizeOptions: {mode: 'managed'},
276+
selModel: 'multiple',
272277
emptyText: 'No roles found.',
273278
colChooserModel: true,
274279
sortBy: 'name',
@@ -334,17 +339,7 @@ export class RoleModel extends HoistModel {
334339
{field: {name: 'lastUpdatedBy', type: 'string'}, hidden: true},
335340
{field: {name: 'notes', type: 'string'}, filterable: false, flex: 1}
336341
],
337-
contextMenu: this.readonly
338-
? [this.groupByAction(), ...GridModel.defaultContextMenu]
339-
: [
340-
this.addAction(),
341-
this.editAction(),
342-
this.cloneAction(),
343-
this.deleteAction(),
344-
'-',
345-
this.groupByAction(),
346-
...GridModel.defaultContextMenu
347-
],
342+
contextMenu: () => this.getContextMenuItems(),
348343
onRowDoubleClicked: ({data: record}) => {
349344
if (record && !record.data.isGroupRow) {
350345
this.editAsync(record.data as HoistRole);
@@ -353,6 +348,21 @@ export class RoleModel extends HoistModel {
353348
});
354349
}
355350

351+
private getContextMenuItems() {
352+
return this.readonly
353+
? [this.groupByAction(), ...GridModel.defaultContextMenu]
354+
: [
355+
this.addAction(),
356+
this.editAction(),
357+
this.cloneAction(),
358+
this.deleteAction(),
359+
this.recategorizeDialogModel.recategorizeAction(),
360+
'-',
361+
this.groupByAction(),
362+
...GridModel.defaultContextMenu
363+
];
364+
}
365+
356366
private createFilterChooserModel(): FilterChooserModel {
357367
const config = this.moduleConfig;
358368
return new FilterChooserModel({

admin/tabs/userData/roles/RolePanel.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*
55
* Copyright © 2024 Extremely Heavy Industries Inc.
66
*/
7+
import {recategorizeDialog} from '@xh/hoist/admin/tabs/userData/roles/recategorize/RecategorizeDialog';
78
import {grid} from '@xh/hoist/cmp/grid';
89
import {fragment, hframe, vframe} from '@xh/hoist/cmp/layout';
910
import {creates, hoistCmp} from '@xh/hoist/core';
@@ -48,7 +49,8 @@ export const rolePanel = hoistCmp.factory({
4849
],
4950
item: hframe(vframe(grid(), roleGraph()), detailsPanel())
5051
}),
51-
roleEditor()
52+
roleEditor(),
53+
recategorizeDialog()
5254
);
5355
}
5456
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* This file belongs to Hoist, an application development toolkit
3+
* developed by Extremely Heavy Industries (www.xh.io | [email protected])
4+
*
5+
* Copyright © 2024 Extremely Heavy Industries Inc.
6+
*/
7+
import {RecategorizeDialogModel} from '@xh/hoist/admin/tabs/userData/roles/recategorize/RecategorizeDialogModel';
8+
import {filler} from '@xh/hoist/cmp/layout';
9+
import {hoistCmp, uses} from '@xh/hoist/core';
10+
import {button} from '@xh/hoist/desktop/cmp/button';
11+
import {select} from '@xh/hoist/desktop/cmp/input';
12+
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
13+
import {Icon} from '@xh/hoist/icon';
14+
import {dialog, dialogBody} from '@xh/hoist/kit/blueprint';
15+
16+
export const recategorizeDialog = hoistCmp.factory({
17+
model: uses(RecategorizeDialogModel),
18+
19+
render({model}) {
20+
const {isOpen} = model;
21+
if (!isOpen) return null;
22+
23+
return dialog({
24+
title: `Change Category (${model.selectedRecords.length} roles)`,
25+
icon: Icon.folder(),
26+
style: {width: 300},
27+
isOpen: true,
28+
isCloseButtonShown: false,
29+
items: [
30+
dialogBody(
31+
select({
32+
bind: 'categoryName',
33+
enableCreate: true,
34+
options: model.options,
35+
width: 260
36+
})
37+
),
38+
tbar()
39+
]
40+
});
41+
}
42+
});
43+
44+
const tbar = hoistCmp.factory<RecategorizeDialogModel>(({model}) => {
45+
return toolbar(
46+
filler(),
47+
button({
48+
text: 'Cancel',
49+
onClick: () => model.close()
50+
}),
51+
button({
52+
text: 'Save',
53+
icon: Icon.check(),
54+
intent: 'success',
55+
disabled: model.categoryName == null,
56+
onClick: () => model.saveAsync()
57+
})
58+
);
59+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* This file belongs to Hoist, an application development toolkit
3+
* developed by Extremely Heavy Industries (www.xh.io | [email protected])
4+
*
5+
* Copyright © 2024 Extremely Heavy Industries Inc.
6+
*/
7+
import {RoleModel} from '@xh/hoist/admin/tabs/userData/roles/RoleModel';
8+
import {HoistModel, XH} from '@xh/hoist/core';
9+
import {StoreRecord} from '@xh/hoist/data';
10+
import {Icon} from '@xh/hoist/icon/Icon';
11+
import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
12+
import {compact, every, filter, map, uniq} from 'lodash';
13+
14+
export class RecategorizeDialogModel extends HoistModel {
15+
private parent: RoleModel;
16+
selectedRecords: StoreRecord[];
17+
18+
@bindable categoryName = null;
19+
@observable isOpen = false;
20+
21+
recategorizeAction() {
22+
return {
23+
text: 'Change Category',
24+
icon: Icon.folder(),
25+
recordsRequired: true,
26+
actionFn: ({selectedRecords}) => this.open(selectedRecords),
27+
displayFn: ({selectedRecords}) => {
28+
return {
29+
hidden: this.parent.readonly,
30+
disabled: every(selectedRecords, it => it.data?.isGroupRow)
31+
};
32+
}
33+
};
34+
}
35+
36+
get options() {
37+
return [
38+
...compact(uniq(this.parent.allRoles.map(it => it.category))).sort(),
39+
{value: '_CLEAR_ROLES_', label: '[Clear Existing Category]'}
40+
];
41+
}
42+
43+
constructor(parent) {
44+
super();
45+
makeObservable(this);
46+
this.parent = parent;
47+
}
48+
49+
async saveAsync() {
50+
if (this.parent.readonly) return;
51+
const roleSpec = filter(
52+
this.selectedRecords.map(it => it.data),
53+
it => !it.isGroupRow
54+
);
55+
const roles: string[] = map(roleSpec, it => it.name);
56+
await XH.fetchService
57+
.postJson({
58+
url: 'roleAdmin/bulkCategoryUpdate',
59+
body: {
60+
roles,
61+
category: this.categoryName === '_CLEAR_ROLES_' ? null : this.categoryName
62+
}
63+
})
64+
.catchDefault();
65+
await this.parent.refreshAsync();
66+
this.close();
67+
}
68+
69+
//-----------------
70+
// Actions
71+
//-----------------
72+
@action
73+
close() {
74+
this.categoryName = null;
75+
this.isOpen = false;
76+
}
77+
78+
@action
79+
open(selectedRecords) {
80+
this.selectedRecords = selectedRecords;
81+
this.isOpen = true;
82+
}
83+
}

0 commit comments

Comments
 (0)