Skip to content

Commit bbe5629

Browse files
committed
feat(admin): add option to copy members from one group to other groups in the same vo
* added dialog to copy members * updated openapi
1 parent 87ed74d commit bbe5629

File tree

8 files changed

+340
-1
lines changed

8 files changed

+340
-1
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<div class="{{data.theme}}">
2+
<h1 mat-dialog-title>{{'DIALOGS.COPY_MEMBERS.TITLE' | translate}}</h1>
3+
<mat-spinner *ngIf="loading" class="mr-auto ml-auto"></mat-spinner>
4+
<div *ngIf="!loading" class="dialog-container" mat-dialog-content>
5+
<mat-radio-group [(ngModel)]="copyType" class="flex-container">
6+
<span
7+
matTooltip="{{'DIALOGS.COPY_MEMBERS.DISABLED_COPY_SELECTION' | translate}}"
8+
[matTooltipDisabled]="data.members.length > 0"
9+
matTooltipPosition="above">
10+
<mat-radio-button [disabled]="data.members.length === 0" value="selection">
11+
{{'DIALOGS.COPY_MEMBERS.COPY_SELECTION' | translate}}
12+
</mat-radio-button>
13+
</span>
14+
<mat-radio-button value="all">
15+
{{'DIALOGS.COPY_MEMBERS.COPY_ALL' | translate}}
16+
</mat-radio-button>
17+
</mat-radio-group>
18+
<perun-web-apps-alert class="mt-4" alert_type="warn">
19+
{{'DIALOGS.COPY_MEMBERS.ATTRIBUTES_WARN' | translate}}
20+
</perun-web-apps-alert>
21+
<h5 class="mt-4">{{'DIALOGS.COPY_MEMBERS.SELECT_GROUPS' | translate}}</h5>
22+
<perun-web-apps-immediate-filter
23+
(filter)="applyFilter($event)"
24+
[placeholder]="'SHARED_LIB.PERUN.COMPONENTS.RESOURCES_LIST.TABLE_SEARCH'">
25+
</perun-web-apps-immediate-filter>
26+
<perun-web-apps-groups-list
27+
[groups]="assignableGroups"
28+
[selection]="selection"
29+
[disableRouting]="true"
30+
[displayedColumns]="['select', 'id', 'name', 'description']"
31+
[filter]="filterValue"
32+
[tableId]="tableId">
33+
</perun-web-apps-groups-list>
34+
</div>
35+
<div mat-dialog-actions>
36+
<button (click)="onCancel()" class="ml-auto" mat-flat-button>
37+
{{'DIALOGS.COPY_MEMBERS.CANCEL' | translate}}
38+
</button>
39+
<button
40+
(click)="onSubmit()"
41+
[disabled]="loading || selection.isEmpty()"
42+
class="ml-2"
43+
color="accent"
44+
mat-flat-button>
45+
{{'DIALOGS.COPY_MEMBERS.COPY' | translate}}
46+
</button>
47+
</div>
48+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.flex-container {
2+
display: flex;
3+
flex-direction: column;
4+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Component, Inject, OnInit } from '@angular/core';
2+
import { Group, GroupsManagerService, RichGroup, RichMember } from '@perun-web-apps/perun/openapi';
3+
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
4+
import { TABLE_VO_GROUPS } from '@perun-web-apps/config/table-config';
5+
import { Urns } from '@perun-web-apps/perun/urns';
6+
import { hasBooleanAttributeEnabled, isGroupSynchronized } from '@perun-web-apps/perun/utils';
7+
import {
8+
GuiAuthResolver,
9+
NotificatorService,
10+
PerunTranslateService,
11+
} from '@perun-web-apps/perun/services';
12+
import { SelectionModel } from '@angular/cdk/collections';
13+
14+
export interface CopyMembersDialogData {
15+
theme: string;
16+
members: RichMember[];
17+
groupId: number;
18+
voId: number;
19+
}
20+
@Component({
21+
selector: 'app-copy-members-dialog',
22+
templateUrl: './copy-members-dialog-component.html',
23+
styleUrls: ['./copy-members-dialog-component.scss'],
24+
})
25+
export class CopyMembersDialogComponent implements OnInit {
26+
loading = false;
27+
copyType: 'all' | 'selection' = 'all';
28+
filterValue = '';
29+
tableId = TABLE_VO_GROUPS;
30+
assignableGroups: Group[] = [];
31+
selection = new SelectionModel<Group>(true, []);
32+
33+
private groupAttrNames = [Urns.GROUP_SYNC_ENABLED, Urns.GROUP_BLOCK_MANUAL_MEMBER_ADDING];
34+
35+
constructor(
36+
public dialogRef: MatDialogRef<CopyMembersDialogData>,
37+
@Inject(MAT_DIALOG_DATA) public data: CopyMembersDialogData,
38+
private groupsService: GroupsManagerService,
39+
private guiAuthResolver: GuiAuthResolver,
40+
private translate: PerunTranslateService,
41+
private notificator: NotificatorService
42+
) {}
43+
44+
ngOnInit(): void {
45+
this.loading = true;
46+
if (this.data.members.length > 0) {
47+
this.copyType = 'selection';
48+
}
49+
50+
this.groupsService
51+
.getAllRichGroupsWithAttributesByNames(this.data.voId, this.groupAttrNames)
52+
.subscribe((groups) => {
53+
this.assignableGroups = this.filterAssignableGroups(groups);
54+
this.loading = false;
55+
});
56+
}
57+
58+
onCancel(): void {
59+
this.dialogRef.close();
60+
}
61+
62+
onSubmit(): void {
63+
this.loading = true;
64+
const memberIds =
65+
this.copyType === 'selection' ? this.data.members.map((member) => member.id) : [];
66+
this.groupsService
67+
.copyMembers(
68+
this.data.groupId,
69+
this.selection.selected.map((group) => group.id),
70+
memberIds
71+
)
72+
.subscribe({
73+
next: () => {
74+
this.notificator.showSuccess(this.translate.instant('DIALOGS.COPY_MEMBERS.SUCCESS'));
75+
this.dialogRef.close(true);
76+
},
77+
error: () => {
78+
this.notificator.showError(this.translate.instant('DIALOGS.COPY_MEMBERS.ERROR'));
79+
this.loading = false;
80+
},
81+
});
82+
}
83+
applyFilter(filterValue: string): void {
84+
this.filterValue = filterValue;
85+
}
86+
87+
private filterAssignableGroups(groups: RichGroup[]): RichGroup[] {
88+
const assignableGroups: RichGroup[] = [];
89+
for (const grp of groups) {
90+
if (
91+
grp.name !== 'members' &&
92+
grp.id !== this.data.groupId &&
93+
!(
94+
isGroupSynchronized(grp) ||
95+
hasBooleanAttributeEnabled(grp.attributes, Urns.GROUP_BLOCK_MANUAL_MEMBER_ADDING)
96+
) &&
97+
this.guiAuthResolver.isAuthorized(
98+
'dest-copyMembers_Group_List<Group>_List<Member>_boolean_policy',
99+
[grp]
100+
)
101+
) {
102+
assignableGroups.push(grp);
103+
}
104+
}
105+
return assignableGroups;
106+
}
107+
}

apps/admin-gui/src/app/shared/shared.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ import { UpdateBanDialogComponent } from './components/dialogs/update-ban-dialog
186186
import { UpdateVoBanDialogComponent } from './components/dialogs/update-vo-ban-dialog/update-vo-ban-dialog.component';
187187
import { UpdateResourceBanDialogComponent } from './components/dialogs/update-resource-ban-dialog/update-resource-ban-dialog.component';
188188
import { UpdateFacilityBanDialogComponent } from './components/dialogs/update-facility-ban-dialog/update-facility-ban-dialog.component';
189+
import { CopyMembersDialogComponent } from './components/dialogs/copy-members-dialog/copy-members-dialog-component';
189190

190191
@NgModule({
191192
imports: [
@@ -463,6 +464,7 @@ import { UpdateFacilityBanDialogComponent } from './components/dialogs/update-fa
463464
UpdateVoBanDialogComponent,
464465
UpdateResourceBanDialogComponent,
465466
UpdateFacilityBanDialogComponent,
467+
CopyMembersDialogComponent,
466468
],
467469
providers: [AnyToStringPipe, ExtSourceTypePipe, ConsentRelatedAttributePipe],
468470
})

apps/admin-gui/src/app/vos/pages/group-detail-page/group-members/group-members.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ <h1 class="page-subtitle">{{'GROUP_DETAIL.MEMBERS.TITLE' | translate}}</h1>
5151
</button>
5252
</mat-menu>
5353
</span>
54+
<span>
55+
<button (click)="onCopyMembers()" *ngIf="copyAuth" class="mr-2" color="accent" mat-flat-button>
56+
{{'GROUP_DETAIL.MEMBERS.COPY_MEMBERS' | translate}}
57+
</button>
58+
</span>
5459
<span
5560
[matTooltipDisabled]="!(group | groupMembersActionButtonDisabled)"
5661
[matTooltipPosition]="'above'"

apps/admin-gui/src/app/vos/pages/group-detail-page/group-members/group-members.component.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { UntypedFormControl } from '@angular/forms';
2525
import { RPCError } from '@perun-web-apps/perun/models';
2626
import { GroupAddMemberDialogComponent } from '../../../components/group-add-member-dialog/group-add-member-dialog.component';
2727
import { BulkInviteMembersDialogComponent } from '../../../../shared/components/dialogs/bulk-invite-members-dialog/bulk-invite-members-dialog.component';
28+
import { CopyMembersDialogComponent } from '../../../../shared/components/dialogs/copy-members-dialog/copy-members-dialog-component';
2829

2930
@Component({
3031
selector: 'app-group-members',
@@ -55,6 +56,7 @@ export class GroupMembersComponent implements OnInit {
5556
addAuth: boolean;
5657
removeAuth: boolean;
5758
inviteAuth: boolean;
59+
copyAuth: boolean;
5860
blockManualMemberAdding: boolean;
5961
displayedColumns = [
6062
'checkbox',
@@ -129,6 +131,10 @@ export class GroupMembersComponent implements OnInit {
129131
'group-sendInvitation_Vo_Group_String_String_String_policy',
130132
[this.group]
131133
);
134+
this.copyAuth = this.guiAuthResolver.isAuthorized(
135+
'source-copyMembers_Group_List<Group>_List<Member>_boolean_policy',
136+
[this.group]
137+
);
132138
}
133139

134140
onSearchByString(filter: string): void {
@@ -193,6 +199,25 @@ export class GroupMembersComponent implements OnInit {
193199
this.dialog.open(BulkInviteMembersDialogComponent, config);
194200
}
195201

202+
onCopyMembers(): void {
203+
const config = getDefaultDialogConfig();
204+
config.width = '650px';
205+
config.data = {
206+
voId: this.group.voId,
207+
groupId: this.group.id,
208+
theme: 'group-theme',
209+
members: this.selection.selected,
210+
};
211+
212+
const dialogRef = this.dialog.open(CopyMembersDialogComponent, config);
213+
214+
dialogRef.afterClosed().subscribe((success) => {
215+
if (success) {
216+
this.selection.clear();
217+
}
218+
});
219+
}
220+
196221
displaySelectedStatuses(): string {
197222
if (this.selectedStatuses.length === this.statusList.length) {
198223
return 'ALL';

apps/admin-gui/src/assets/i18n/en.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,7 @@
664664
"ADD_MEMBER": "Add",
665665
"ADD_MEMBER_DISABLED": "Manual adding of members is blocked for this group.",
666666
"REMOVE_MEMBERS": "Remove",
667+
"COPY_MEMBERS": "Copy",
667668
"SEARCH_DESCRIPTION": "Search by name, login, email or UUID",
668669
"LIST_ALL": "List all members",
669670
"INVITE": "Invite",
@@ -1175,6 +1176,19 @@
11751176
"NAME_COLUMN": "Name",
11761177
"ERROR_COLUMN": "Error"
11771178
},
1179+
"COPY_MEMBERS": {
1180+
"TITLE": "Copy members",
1181+
"DISABLED_COPY_SELECTION": "No members selected",
1182+
"COPY_SELECTION": "Copy selected members of this group",
1183+
"COPY_ALL": "Copy all direct members of this group",
1184+
"SELECT_GROUPS": "Select groups to which members will be copied",
1185+
"ATTRIBUTES_WARN": "Member-group attributes of the members will not be copied!",
1186+
"CANCEL": "Cancel",
1187+
"COPY": "Copy",
1188+
"SUCCESS": "Members were successfully copied.",
1189+
"ERROR": "There was an error copying members, the operation was cancelled and none of the members were copied."
1190+
1191+
},
11781192
"ADD_GROUPS": {
11791193
"TITLE": "Add group",
11801194
"SEARCH": "Search",
@@ -2298,7 +2312,7 @@
22982312
"NO_MEMBERS_ALERT": "No members present",
22992313
"NO_FILTER_RESULTS_ALERT": "No members are matching your query",
23002314
"INDIRECT_MEMBER": "Indirect member",
2301-
"CHECKBOX_TOOLTIP_INDIRECT": "Indirect members cannot be removed",
2315+
"CHECKBOX_TOOLTIP_INDIRECT": "Indirect members cannot be removed or copied",
23022316
"CHECKBOX_TOOLTIP_UNALTERABLE": "Members from member organizations cannot be directly removed",
23032317
"STATUS_TOOLTIP_INDIRECT": "Member status from member organization cannot be directly changed",
23042318
"STATUS_TOOLTIP_GROUP_INDIRECT": "Indirect member status cannot be changed",

0 commit comments

Comments
 (0)