Skip to content

Commit ef9bdc5

Browse files
HejdaJakubbodnara
authored andcommitted
feat(admin): added dialog to copy sponsored members from one sponsor of vo to another
* added copy-sponsored-members-dialog component * update openapi
1 parent 25a20b5 commit ef9bdc5

28 files changed

+822
-635
lines changed

apps/admin-gui/src/app/shared/components/choose-sponsor/choose-sponsor.component.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
<div class="mt-4">
2-
<h6>{{'DIALOGS.CREATE_SPONSORED_MEMBER.SELECT_SPONSOR' | translate}}</h6>
2+
<h6>
3+
{{(copy ? 'DIALOGS.CREATE_SPONSORED_MEMBER.SELECT_SPONSOR_COPY' : 'DIALOGS.CREATE_SPONSORED_MEMBER.SELECT_SPONSOR') | translate }}
4+
</h6>
35
<mat-radio-group
46
[(ngModel)]="sponsorType"
57
(ngModelChange)="emitSponsorType()"
68
class="dialog-container">
79
<span
8-
matTooltip="{{'DIALOGS.CREATE_SPONSORED_MEMBER.SELECT_SELF_DISABLED' | translate}}"
9-
[matTooltipDisabled]="isSponsor"
10+
matTooltip="{{selfTooltip | translate}}"
11+
[matTooltipDisabled]="isSelfEnabled()"
1012
matTooltipPosition="before">
11-
<mat-radio-button [disabled]="!isSponsor" (click)="selectedSponsor = null" value="self">
13+
<mat-radio-button [disabled]="!isSelfEnabled()" (click)="selectedSponsor = null" value="self">
1214
{{'DIALOGS.CREATE_SPONSORED_MEMBER.SELECT_SELF' | translate}}
1315
</mat-radio-button>
1416
</span>

apps/admin-gui/src/app/shared/components/choose-sponsor/choose-sponsor.component.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
1+
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
22
import { Role } from '@perun-web-apps/perun/models';
33
import { GuiAuthResolver } from '@perun-web-apps/perun/services';
44
import { RichUser, User } from '@perun-web-apps/perun/openapi';
@@ -8,22 +8,36 @@ import { RichUser, User } from '@perun-web-apps/perun/openapi';
88
templateUrl: './choose-sponsor.component.html',
99
styleUrls: ['./choose-sponsor.component.scss'],
1010
})
11-
export class ChooseSponsorComponent implements OnInit {
11+
export class ChooseSponsorComponent implements OnInit, OnChanges {
1212
@Input() voId: number;
1313
@Input() voSponsors: RichUser[] = [];
14+
@Input() copy = false;
15+
@Input() disableSelf = false;
1416
@Output() sponsorTypeSelected: EventEmitter<string> = new EventEmitter<string>();
1517
@Output() sponsorSelected: EventEmitter<User> = new EventEmitter<User>();
1618
sponsorType = 'self';
1719
isSponsor = false;
1820
isPerunAdmin = false;
1921
selectedSponsor: User = null;
22+
selfTooltip = 'DIALOGS.CREATE_SPONSORED_MEMBER.SELECT_SELF_DISABLED';
2023

2124
constructor(private guiAuthResolver: GuiAuthResolver) {}
2225

2326
ngOnInit(): void {
2427
this.isSponsor = this.guiAuthResolver.principalHasRole(Role.SPONSOR, 'Vo', this.voId);
2528
this.isPerunAdmin = this.guiAuthResolver.isPerunAdmin();
26-
this.sponsorType = this.isSponsor ? 'self' : 'other';
29+
if (this.isSelfEnabled()) {
30+
this.selfTooltip = 'DIALOGS.CREATE_SPONSORED_MEMBER.SELECT_SELF_DISABLED_COPY';
31+
}
32+
this.updateSponsorType();
33+
}
34+
35+
ngOnChanges(): void {
36+
this.updateSponsorType();
37+
}
38+
39+
updateSponsorType(): void {
40+
this.sponsorType = this.isSelfEnabled() ? 'self' : 'other';
2741
this.emitSponsorType();
2842
}
2943

@@ -38,4 +52,8 @@ export class ChooseSponsorComponent implements OnInit {
3852
this.selectedSponsor = sponsor;
3953
this.sponsorSelected.emit(sponsor);
4054
}
55+
56+
isSelfEnabled(): boolean {
57+
return this.isSponsor && !this.disableSelf;
58+
}
4159
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<ng-template #spinner>
2+
<perun-web-apps-loading-dialog></perun-web-apps-loading-dialog>
3+
</ng-template>
4+
<div class="{{theme}} position-relative">
5+
<div *perunWebAppsLoader="loading; indicator: spinner">
6+
<h1 mat-dialog-title>{{'DIALOGS.COPY_SPONSORED_MEMBERS.TITLE' | translate}}</h1>
7+
<div class="dialog-container" mat-dialog-content>
8+
<h6>{{'DIALOGS.COPY_SPONSORED_MEMBERS.SPONSOR_FROM' | translate}}</h6>
9+
<perun-web-apps-user-search-select
10+
(userSelected)="sourceSponsorChanged($event)"
11+
[disableAutoSelect]="true"
12+
[users]="voSponsorsSource">
13+
</perun-web-apps-user-search-select>
14+
<ng-template #spinner>
15+
<perun-web-apps-loading-table></perun-web-apps-loading-table>
16+
</ng-template>
17+
<div class="position-relative" *ngIf="sourceSponsorSelected">
18+
<h6>{{'DIALOGS.COPY_SPONSORED_MEMBERS.SPONSORED_MEMBERS' | translate}}</h6>
19+
<perun-web-apps-alert alert_type="info">
20+
{{'DIALOGS.COPY_SPONSORED_MEMBERS.SPONSORED_MEMBERS_INFO' | translate}}
21+
</perun-web-apps-alert>
22+
<perun-web-apps-immediate-filter
23+
(filter)="filter = $event"
24+
[placeholder]="'VO_DETAIL.SETTINGS.SPONSORED_MEMBERS.FILTER'">
25+
</perun-web-apps-immediate-filter>
26+
<app-sponsored-members-list
27+
*perunWebAppsLoader="tableLoading; indicator: spinner"
28+
[disableRouting]="true"
29+
[sponsoredMembers]="filteredSponsoredMembers"
30+
[selection]="selection"
31+
[filterValue]="filter"
32+
[displayedColumns]="['select', 'id', 'name', 'email', 'logins', 'expiration']"
33+
[selectedSponsor]="sourceSponsor"
34+
[tableId]="tableId">
35+
</app-sponsored-members-list>
36+
</div>
37+
<div *ngIf="isPerunAdmin">
38+
<app-choose-sponsor
39+
*ngIf="!tableLoading"
40+
[voId]="data.voId"
41+
[copy]="true"
42+
[disableSelf]="disableSelf"
43+
[voSponsors]="voSponsorsTarget"
44+
(sponsorTypeSelected)="sponsorType = $event"
45+
(sponsorSelected)="targetSponsor = $event">
46+
</app-choose-sponsor>
47+
</div>
48+
<div>
49+
<perun-web-apps-alert alert_type="info">
50+
{{'DIALOGS.COPY_SPONSORED_MEMBERS.EXP_INFO' | translate}}
51+
</perun-web-apps-alert>
52+
<h6>{{'DIALOGS.COPY_SPONSORED_MEMBERS.EXPIRATION' | translate}}</h6>
53+
<mat-checkbox [(ngModel)]="pickExpiration">Pick new expiration date</mat-checkbox>
54+
<perun-web-apps-expiration-select *ngIf="pickExpiration" (datePicker)="expiration = $event">
55+
</perun-web-apps-expiration-select>
56+
</div>
57+
</div>
58+
<div mat-dialog-actions>
59+
<button (click)="cancel()" class="ms-auto" mat-flat-button>
60+
{{'DIALOGS.COPY_SPONSORED_MEMBERS.CANCEL' | translate}}
61+
</button>
62+
<button
63+
data-cy="sponsor-member-button"
64+
(click)="submit()"
65+
class="ms-2"
66+
color="accent"
67+
[disabled]="loading || selection.selected.length === 0 || (sponsorType === 'other' && !targetSponsor)"
68+
mat-flat-button>
69+
{{'DIALOGS.COPY_SPONSORED_MEMBERS.SPONSOR' | translate}}
70+
</button>
71+
</div>
72+
</div>
73+
</div>

apps/admin-gui/src/app/shared/components/dialogs/copy-sponsored-members-dialog/copy-sponsored-members-dialog.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { Component, Inject, OnInit } from '@angular/core';
2+
import {
3+
MembersManagerService,
4+
MemberWithSponsors,
5+
RichUser,
6+
User,
7+
} from '@perun-web-apps/perun/openapi';
8+
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
9+
import {
10+
GuiAuthResolver,
11+
NotificatorService,
12+
PerunTranslateService,
13+
StoreService,
14+
} from '@perun-web-apps/perun/services';
15+
import { SelectionModel } from '@angular/cdk/collections';
16+
import { TABLE_ADD_SPONSORED_MEMBERS } from '@perun-web-apps/config/table-config';
17+
import { Urns } from '@perun-web-apps/perun/urns';
18+
19+
export interface CopySponsoredMembersDialogData {
20+
voId: number;
21+
theme: string;
22+
voSponsors: RichUser[];
23+
findSponsorsAuth: boolean;
24+
}
25+
@Component({
26+
selector: 'app-copy-sponsored-members-dialog',
27+
templateUrl: './copy-sponsored-members-dialog.component.html',
28+
styleUrls: ['./copy-sponsored-members-dialog.component.scss'],
29+
})
30+
export class CopySponsoredMembersDialogComponent implements OnInit {
31+
loading = false;
32+
tableLoading = false;
33+
theme: string;
34+
sponsorType: 'self' | 'other' = 'self';
35+
tableId = TABLE_ADD_SPONSORED_MEMBERS;
36+
voSponsorsTarget: RichUser[];
37+
voSponsorsSource: RichUser[];
38+
sourceSponsor: User;
39+
targetSponsor: User;
40+
sponsoredMembers: MemberWithSponsors[];
41+
filteredSponsoredMembers: MemberWithSponsors[];
42+
selection: SelectionModel<MemberWithSponsors> = new SelectionModel<MemberWithSponsors>(true, []);
43+
sourceSponsorSelected = false;
44+
filter: string;
45+
expiration = 'never';
46+
pickExpiration = false;
47+
disableSelf = false;
48+
isPerunAdmin = false;
49+
private attrNames: string[] = [Urns.USER_DEF_PREFERRED_MAIL];
50+
constructor(
51+
private dialogRef: MatDialogRef<CopySponsoredMembersDialogComponent>,
52+
@Inject(MAT_DIALOG_DATA) public data: CopySponsoredMembersDialogData,
53+
private store: StoreService,
54+
private membersService: MembersManagerService,
55+
private notificator: NotificatorService,
56+
private translate: PerunTranslateService,
57+
private auth: GuiAuthResolver
58+
) {}
59+
60+
ngOnInit(): void {
61+
this.attrNames = this.attrNames.concat(this.store.getLoginAttributeNames());
62+
this.membersService
63+
.getAllSponsoredMembersAndTheirSponsors(this.data.voId, this.attrNames)
64+
.subscribe({
65+
next: (members) => {
66+
this.sponsoredMembers = members;
67+
},
68+
});
69+
this.voSponsorsTarget = this.data.voSponsors;
70+
this.voSponsorsSource = this.data.voSponsors;
71+
this.isPerunAdmin = this.auth.isPerunAdmin();
72+
// if not PERUNADMIN user cannot copy to anyone else than self, so do not allow selecting self as source sponsor
73+
if (!this.isPerunAdmin) {
74+
this.voSponsorsSource = this.voSponsorsSource.filter(
75+
(voSponsor) => voSponsor.id !== this.store.getPerunPrincipal().user.id
76+
);
77+
}
78+
this.theme = this.data.theme;
79+
}
80+
81+
sourceSponsorChanged(user: User): void {
82+
this.tableLoading = true;
83+
this.sourceSponsor = user;
84+
this.voSponsorsTarget = this.data.voSponsors.filter(
85+
(sponsor) => sponsor.id !== this.sourceSponsor.id
86+
);
87+
// disable copying to self only if PERUNADMIN (it is not possible to choose self as source sponsor without PERUNADMIN anyway)
88+
if (this.isPerunAdmin) {
89+
this.disableSelf = this.store.getPerunPrincipal().user.id === this.sourceSponsor.id;
90+
}
91+
this.sourceSponsorSelected = true;
92+
this.selection.clear();
93+
this.filteredSponsoredMembers = this.sponsoredMembers.filter((member) =>
94+
member.sponsors.map((sponsor) => sponsor.user.id).includes(this.sourceSponsor.id)
95+
);
96+
this.tableLoading = false;
97+
}
98+
99+
sponsor(members: MemberWithSponsors[]): void {
100+
const sponsor =
101+
this.sponsorType === 'self' ? this.store.getPerunPrincipal().user : this.targetSponsor;
102+
103+
const memberIds = members.map((member) => member.member.id);
104+
105+
this.membersService
106+
.copySponsoredMembers(
107+
memberIds,
108+
this.sourceSponsor.id,
109+
sponsor.id,
110+
!this.pickExpiration,
111+
this.expiration
112+
)
113+
.subscribe({
114+
next: () => {
115+
this.notificator.showSuccess(
116+
this.translate.instant('DIALOGS.COPY_SPONSORED_MEMBERS.SUCCESS')
117+
);
118+
this.loading = false;
119+
this.dialogRef.close(true);
120+
},
121+
error: () => (this.loading = false),
122+
});
123+
}
124+
125+
submit(): void {
126+
this.loading = true;
127+
this.expiration = this.expiration === 'never' ? null : this.expiration;
128+
129+
this.sponsor(this.selection.selected);
130+
}
131+
132+
cancel(): void {
133+
this.dialogRef.close(false);
134+
}
135+
}

apps/admin-gui/src/app/shared/components/dialogs/edit-member-sponsors-dialog/edit-member-sponsors-dialog.component.html

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ <h1 mat-dialog-title>{{'DIALOGS.EDIT_MEMBER_SPONSORS.TITLE' | translate}}</h1>
2222
<th *matHeaderCellDef mat-header-cell>
2323
{{'DIALOGS.EDIT_MEMBER_SPONSORS.TABLE_EXPIRATION' | translate}}
2424
</th>
25-
<td *matCellDef="let sponsor" class="align-elements" mat-cell>
26-
<span>{{parseDate(sponsor.validityTo)}}</span>
25+
<td
26+
*matCellDef="let sponsor"
27+
[class.align-elements]="[sponsor.user, vo] | isAuthorized: 'updateSponsorshipValidity_Member_User_LocalDate'"
28+
mat-cell>
29+
<span>{{sponsor.validityTo | parseDate}}</span>
2730
<button
28-
*ngIf="isExpirationAuthorized(sponsor)"
31+
*ngIf="[sponsor.user, vo] | isAuthorized: 'updateSponsorshipValidity_Member_User_LocalDate'"
2932
(click)="changeExpiration(sponsor)"
3033
mat-icon-button>
3134
<mat-icon> today </mat-icon>
@@ -36,14 +39,14 @@ <h1 mat-dialog-title>{{'DIALOGS.EDIT_MEMBER_SPONSORS.TITLE' | translate}}</h1>
3639
<th *matHeaderCellDef mat-header-cell></th>
3740
<td *matCellDef="let sponsor" mat-cell>
3841
<div
39-
[matTooltipDisabled]="isRemoveAuthorized(sponsor)"
42+
[matTooltipDisabled]="([member] | isAuthorized: 'sponsored-removeSponsor_Member_User_policy') && ([sponsor.user] | isAuthorized: 'sponsor-removeSponsor_Member_User_policy')"
4043
[matTooltipPosition]="'above'"
4144
matTooltip="{{'DIALOGS.EDIT_MEMBER_SPONSORS.REMOVE_SPONSOR_DISABLED' | translate}}">
4245
<button
4346
attr.data-cy="{{sponsor.user.firstName}}-unsponsor-mark-button"
4447
(click)="markSponsor(sponsor)"
4548
class="btn-delete"
46-
[disabled]="!isRemoveAuthorized(sponsor)"
49+
[disabled]="!([member] | isAuthorized: 'sponsored-removeSponsor_Member_User_policy') || !([sponsor.user] | isAuthorized: 'sponsor-removeSponsor_Member_User_policy')"
4750
mat-icon-button>
4851
<mat-icon
4952
*ngIf="!sponsorsToRemove.has(sponsor.user.id)"

apps/admin-gui/src/app/shared/components/dialogs/edit-member-sponsors-dialog/edit-member-sponsors-dialog.component.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import {
1515
import { MatTableDataSource } from '@angular/material/table';
1616
import { getDefaultDialogConfig } from '@perun-web-apps/perun/utils';
1717
import { ChangeSponsorshipExpirationDialogComponent } from '@perun-web-apps/perun/dialogs';
18-
import { formatDate } from '@angular/common';
19-
2018
export interface EditMemberSponsorsDialogComponent {
2119
theme: string;
2220
sponsors: Sponsor[];
@@ -35,7 +33,8 @@ export class EditMemberSponsorsDialogComponent implements OnInit {
3533
dataSource: MatTableDataSource<Sponsor>;
3634
loading = false;
3735
sponsorsToRemove: Set<number> = new Set<number>();
38-
private vo: Vo;
36+
member: Member;
37+
vo: Vo;
3938
private expirationChanged = false;
4039

4140
constructor(
@@ -52,6 +51,7 @@ export class EditMemberSponsorsDialogComponent implements OnInit {
5251
ngOnInit(): void {
5352
this.theme = this.data.theme;
5453
this.sponsors = this.data.sponsors;
54+
this.member = this.data.member;
5555
this.dataSource = new MatTableDataSource<Sponsor>(this.data.sponsors);
5656
this.vo = {
5757
beanName: 'Vo',
@@ -77,29 +77,6 @@ export class EditMemberSponsorsDialogComponent implements OnInit {
7777
this.dialogRef.close(this.expirationChanged);
7878
}
7979

80-
isRemoveAuthorized(sponsor: Sponsor): boolean {
81-
return (
82-
this.authResolver.isAuthorized('sponsored-removeSponsor_Member_User_policy', [
83-
this.data.member,
84-
]) &&
85-
this.authResolver.isAuthorized('sponsor-removeSponsor_Member_User_policy', [sponsor.user])
86-
);
87-
}
88-
89-
isExpirationAuthorized(sponsor: Sponsor): boolean {
90-
return this.authResolver.isAuthorized('updateSponsorshipValidity_Member_User_LocalDate', [
91-
sponsor.user,
92-
this.vo,
93-
]);
94-
}
95-
96-
parseDate(date: string): string {
97-
if (date === null) {
98-
return 'Never expire';
99-
}
100-
return formatDate(date, 'd.M.y', 'en');
101-
}
102-
10380
changeExpiration(sponsor: Sponsor): void {
10481
const config = getDefaultDialogConfig();
10582
config.width = '400px';

apps/admin-gui/src/app/shared/components/sponsored-members-list/sponsored-members-list.component.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,16 @@
7878
</span>
7979
</td>
8080
</ng-container>
81+
<ng-container matColumnDef="expiration">
82+
<th *matHeaderCellDef mat-header-cell>
83+
{{'SHARED.COMPONENTS.SPONSORED_MEMBERS_LIST.EXPIRATION' | translate}}
84+
</th>
85+
<td *matCellDef="let sponsoredMember" mat-cell>
86+
{{(sponsoredMember.sponsors | selectedSponsor : selectedSponsor).validityTo | parseDate }}
87+
</td>
88+
</ng-container>
8189
<ng-container matColumnDef="menu">
82-
<th *matHeaderCellDef mat-header-cell mat-sort-header></th>
90+
<th *matHeaderCellDef mat-header-cell></th>
8391
<td *matCellDef="let sponsoredMember" mat-cell>
8492
<button
8593
attr.data-cy="{{sponsoredMember.member.user.firstName}}-edit-sponsors-button"

0 commit comments

Comments
 (0)