Skip to content

Commit

Permalink
[#13105] Admin managing account requests: use spinners to indicate ac…
Browse files Browse the repository at this point in the history
…tions in progress (#13135)

* Add spinners for approve, reject and reset in account-request-table

* Add spinners to regenerate keys in admin-search-page

* Update spinners in account-request-table to work individually

* Update tests

* Update buttons to be disabled while loading
  • Loading branch information
Respirayson authored Jul 1, 2024
1 parent 015b872 commit b402863
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ exports[`AccountRequestTableComponent should display account requests with no re
<tm-account-request-table
accountRequests={[Function Array]}
accountService={[Function AccountService]}
isApprovingAccount={[Function Array]}
isRejectingAccount={[Function Array]}
isResettingAccount={[Function Array]}
ngbModal={[Function _NgbModal]}
searchString=""
simpleModalService={[Function SimpleModalService]}
Expand Down Expand Up @@ -267,6 +270,9 @@ exports[`AccountRequestTableComponent should display account requests with reset
<tm-account-request-table
accountRequests={[Function Array]}
accountService={[Function AccountService]}
isApprovingAccount={[Function Array]}
isRejectingAccount={[Function Array]}
isResettingAccount={[Function Array]}
ngbModal={[Function _NgbModal]}
searchString={[Function String]}
simpleModalService={[Function SimpleModalService]}
Expand Down Expand Up @@ -592,6 +598,9 @@ exports[`AccountRequestTableComponent should snap with an expanded account reque
<tm-account-request-table
accountRequests={[Function Array]}
accountService={[Function AccountService]}
isApprovingAccount={[Function Array]}
isRejectingAccount={[Function Array]}
isResettingAccount={[Function Array]}
ngbModal={[Function _NgbModal]}
searchString=""
simpleModalService={[Function SimpleModalService]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,16 @@
<i class="fa-solid fa-eye"></i>
</a>
</div>
<button id="approve-account-request-{{i}}" class="btn btn-success" [disabled]="!accountRequest.status || accountRequest.status === 'APPROVED' || accountRequest.status === 'REGISTERED'" (click)="$event.stopPropagation(); approveAccountRequest(accountRequest)">Approve</button>
<button id="approve-account-request-{{i}}" class="btn btn-success" [disabled]="!accountRequest.status || accountRequest.status === 'APPROVED' || accountRequest.status === 'REGISTERED' || isApprovingAccount[i]" (click)="$event.stopPropagation(); approveAccountRequest(accountRequest, i)"> <tm-ajax-loading *ngIf="isApprovingAccount[i]"></tm-ajax-loading>Approve</button>
<span ngbDropdown container="body">
<button id="reject-account-request-{{i}}" type="button" class="btn btn-warning" [disabled]="!accountRequest.status || accountRequest.status === 'REGISTERED' || accountRequest.status === 'APPROVED' || accountRequest.status === 'REJECTED'" ngbDropdownToggle> Reject </button>
<button id="reject-account-request-{{i}}" type="button" class="btn btn-warning" [disabled]="!accountRequest.status || accountRequest.status === 'REGISTERED' || accountRequest.status === 'APPROVED' || accountRequest.status === 'REJECTED' || isRejectingAccount[i]" ngbDropdownToggle> <tm-ajax-loading *ngIf="isRejectingAccount[i]"></tm-ajax-loading> Reject </button>
<div ngbDropdownMenu (click)="$event.stopPropagation()">
<button id="reject-request-{{i}}" class="btn btn-light btn-sm dropdown-item" (click)="$event.stopPropagation(); rejectAccountRequest(accountRequest)"> Reject </button>
<button id="reject-request-with-reason-{{i}}" class="btn btn-light btn-sm dropdown-item" (click)="$event.stopPropagation(); rejectAccountRequestWithReason(accountRequest)"> Reject With Reason </button>
<button id="reject-request-{{i}}" class="btn btn-light btn-sm dropdown-item" (click)="$event.stopPropagation(); rejectAccountRequest(accountRequest, i)"> Reject </button>
<button id="reject-request-with-reason-{{i}}" class="btn btn-light btn-sm dropdown-item" (click)="$event.stopPropagation(); rejectAccountRequestWithReason(accountRequest, i)"> Reject With Reason </button>
</div>
</span>
<div *ngIf="searchString" class="ngb-tooltip-class" [ngbTooltip]="accountRequest.registeredAtText && 'Account requests of registered instructors cannot be deleted'" placement="top">
<button id="reset-account-request-{{i}}" class="btn btn-primary" [disabled]="!accountRequest.registeredAtText" (click)="$event.stopPropagation(); resetAccountRequest(accountRequest);">Reset</button>
<button id="reset-account-request-{{i}}" class="btn btn-primary" [disabled]="!accountRequest.registeredAtText || isResettingAccount[i]" (click)="$event.stopPropagation(); resetAccountRequest(accountRequest, i);"> <tm-ajax-loading *ngIf="isResettingAccount[i]"></tm-ajax-loading>Reset</button>
</div>
</div>
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,15 @@ describe('AccountRequestTableComponent', () => {
component.searchString = 'test';
fixture.detectChanges();

const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => {
return createMockNgbModalRef({});
});
const mockModalRef = {
componentInstance: {},
result: Promise.resolve({}),
dismissed: {
subscribe: jest.fn(),
},
};

const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockReturnValue(mockModalRef as any);

jest.spyOn(accountService, 'resetAccountRequest').mockReturnValue(of({
joinLink: 'joinlink',
Expand Down Expand Up @@ -245,9 +251,15 @@ describe('AccountRequestTableComponent', () => {
component.searchString = 'test';
fixture.detectChanges();

const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => {
return createMockNgbModalRef({});
});
const mockModalRef = {
componentInstance: {},
result: Promise.resolve({}),
dismissed: {
subscribe: jest.fn(),
},
};

const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockReturnValue(mockModalRef as any);

jest.spyOn(accountService, 'resetAccountRequest').mockReturnValue(throwError(() => ({
error: {
Expand Down Expand Up @@ -318,6 +330,9 @@ describe('AccountRequestTableComponent', () => {
const mockModalRef = {
componentInstance: {},
result: Promise.resolve({}),
dismissed: {
subscribe: jest.fn(),
},
};

const modalSpy = jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export class AccountRequestTableComponent {
@Input()
searchString = '';

isRejectingAccount: boolean[] = new Array(this.accountRequests.length).fill(false);
isApprovingAccount: boolean[] = new Array(this.accountRequests.length).fill(false);
isResettingAccount: boolean[] = new Array(this.accountRequests.length).fill(false);

constructor(
private statusMessageService: StatusMessageService,
private simpleModalService: SimpleModalService,
Expand Down Expand Up @@ -94,7 +98,8 @@ export class AccountRequestTableComponent {
}, () => {});
}

approveAccountRequest(accountRequest: AccountRequestTableRowModel): void {
approveAccountRequest(accountRequest: AccountRequestTableRowModel, index: number): void {
this.isApprovingAccount[index] = true;
this.accountService.approveAccountRequest(accountRequest.id, accountRequest.name,
accountRequest.email, accountRequest.instituteAndCountry)
.subscribe({
Expand All @@ -103,31 +108,40 @@ export class AccountRequestTableComponent {
this.statusMessageService.showSuccessToast(
`Account request was successfully approved. Email has been sent to ${accountRequest.email}.`,
);
this.isApprovingAccount[index] = false;
},
error: (resp: ErrorMessageOutput) => {
this.statusMessageService.showErrorToast(resp.error.message);
this.isApprovingAccount[index] = false;
},
});
}

resetAccountRequest(accountRequest: AccountRequestTableRowModel): void {
resetAccountRequest(accountRequest: AccountRequestTableRowModel, index: number): void {
this.isResettingAccount[index] = true;
const modalContent = `Are you sure you want to reset the account request for
<strong>${accountRequest.name}</strong> with email <strong>${accountRequest.email}</strong> from
<strong>${accountRequest.instituteAndCountry}</strong>?
An email with the account registration link will also be sent to the instructor.`;
const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal(
`Reset account request for <strong>${accountRequest.name}</strong>?`, SimpleModalType.WARNING, modalContent);

modalRef.dismissed.subscribe(() => {
this.isResettingAccount[index] = false;
});

modalRef.result.then(() => {
this.accountService.resetAccountRequest(accountRequest.id)
.subscribe({
next: () => {
this.statusMessageService
.showSuccessToast(`Reset successful. An email has been sent to ${accountRequest.email}.`);
accountRequest.registeredAtText = '';
this.isResettingAccount[index] = false;
},
error: (resp: ErrorMessageOutput) => {
this.statusMessageService.showErrorToast(resp.error.message);
this.isResettingAccount[index] = false;
},
});
}, () => {});
Expand Down Expand Up @@ -162,24 +176,32 @@ export class AccountRequestTableComponent {
modalRef.result.then(() => {}, () => {});
}

rejectAccountRequest(accountRequest: AccountRequestTableRowModel): void {
rejectAccountRequest(accountRequest: AccountRequestTableRowModel, index: number): void {
this.isRejectingAccount[index] = true;
this.accountService.rejectAccountRequest(accountRequest.id)
.subscribe({
next: (resp : AccountRequest) => {
accountRequest.status = resp.status;
this.statusMessageService.showSuccessToast('Account request was successfully rejected.');
this.isRejectingAccount[index] = false;
},
error: (resp: ErrorMessageOutput) => {
this.statusMessageService.showErrorToast(resp.error.message);
this.isRejectingAccount[index] = false;
},
});
}

rejectAccountRequestWithReason(accountRequest: AccountRequestTableRowModel): void {
rejectAccountRequestWithReason(accountRequest: AccountRequestTableRowModel, index: number): void {
this.isRejectingAccount[index] = true;
const modalRef: NgbModalRef = this.ngbModal.open(RejectWithReasonModalComponent);
modalRef.componentInstance.accountRequestName = accountRequest.name;
modalRef.componentInstance.accountRequestEmail = accountRequest.email;

modalRef.dismissed.subscribe(() => {
this.isRejectingAccount[index] = false;
});

modalRef.result.then((res: RejectWithReasonModalComponentResult) => {
this.accountService.rejectAccountRequest(accountRequest.id,
res.rejectionReasonTitle, res.rejectionReasonBody)
Expand All @@ -189,9 +211,11 @@ export class AccountRequestTableComponent {
this.statusMessageService.showSuccessToast(
`Account request was successfully rejected. Email has been sent to ${accountRequest.email}.`,
);
this.isRejectingAccount[index] = false;
},
error: (resp: ErrorMessageOutput) => {
this.statusMessageService.showErrorToast(resp.error.message);
this.isRejectingAccount[index] = false;
},
});
}, () => {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
RejectWithReasonModalComponent,
} from './admin-reject-with-reason-modal/admin-reject-with-reason-modal.component';
import { Pipes } from '../../pipes/pipes.module';
import { AjaxLoadingModule } from '../ajax-loading/ajax-loading.module';
import { RichTextEditorModule } from '../rich-text-editor/rich-text-editor.module';

/**
Expand All @@ -29,6 +30,7 @@ import { RichTextEditorModule } from '../rich-text-editor/rich-text-editor.modul
NgbDropdownModule,
Pipes,
RichTextEditorModule,
AjaxLoadingModule,
],
})
export class AccountRequestTableModule { }
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ exports[`AdminSearchPageComponent should snap with a deleted course 1`] = `
emailGenerationService={[Function EmailGenerationService]}
instructorService={[Function InstructorService]}
instructors={[Function Array]}
isRegeneratingInstructorKeys={[Function Array]}
isRegeneratingStudentKeys={[Function Array]}
loadingBarService={[Function LoadingBarService]}
searchQuery=""
searchService={[Function SearchService]}
Expand Down Expand Up @@ -339,6 +341,8 @@ exports[`AdminSearchPageComponent should snap with a search key 1`] = `
emailGenerationService={[Function EmailGenerationService]}
instructorService={[Function InstructorService]}
instructors={[Function Array]}
isRegeneratingInstructorKeys={[Function Array]}
isRegeneratingStudentKeys={[Function Array]}
loadingBarService={[Function LoadingBarService]}
searchQuery={[Function String]}
searchService={[Function SearchService]}
Expand Down Expand Up @@ -392,6 +396,8 @@ exports[`AdminSearchPageComponent should snap with an expanded instructor table
emailGenerationService={[Function EmailGenerationService]}
instructorService={[Function InstructorService]}
instructors={[Function Array]}
isRegeneratingInstructorKeys={[Function Array]}
isRegeneratingStudentKeys={[Function Array]}
loadingBarService={[Function LoadingBarService]}
searchQuery=""
searchService={[Function SearchService]}
Expand Down Expand Up @@ -649,6 +655,8 @@ exports[`AdminSearchPageComponent should snap with an expanded student table 1`]
emailGenerationService={[Function EmailGenerationService]}
instructorService={[Function InstructorService]}
instructors={[Function Array]}
isRegeneratingInstructorKeys={[Function Array]}
isRegeneratingStudentKeys={[Function Array]}
loadingBarService={[Function LoadingBarService]}
searchQuery=""
searchService={[Function SearchService]}
Expand Down Expand Up @@ -955,6 +963,8 @@ exports[`AdminSearchPageComponent should snap with default fields 1`] = `
emailGenerationService={[Function EmailGenerationService]}
instructorService={[Function InstructorService]}
instructors={[Function Array]}
isRegeneratingInstructorKeys={[Function Array]}
isRegeneratingStudentKeys={[Function Array]}
loadingBarService={[Function LoadingBarService]}
searchQuery=""
searchService={[Function SearchService]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
<i class="fas fa-sync"></i> Reset Google ID
</a>
<br>
<button id="regenerate-instructor-key-{{i}}" class="btn btn-danger" (click)="instructor.showLinks = !instructor.showLinks; regenerateInstructorKey(instructor);">Regenerate key</button>
<button id="regenerate-instructor-key-{{i}}" class="btn btn-danger" [disabled]="isRegeneratingInstructorKeys[i]" (click)="instructor.showLinks = !instructor.showLinks; regenerateInstructorKey(instructor, i);"><tm-ajax-loading *ngIf="isRegeneratingInstructorKeys[i]"></tm-ajax-loading>Regenerate key</button>
</td>
</tr>
<tr *ngIf="instructor.showLinks">
Expand Down Expand Up @@ -159,7 +159,7 @@
<i class="fas fa-sync"></i> Reset Google ID
</a>
<br>
<button id="regenerate-student-key-{{i}}" class="btn btn-danger" (click)="student.showLinks = !student.showLinks; regenerateStudentKey(student);">Regenerate key</button>
<button id="regenerate-student-key-{{i}}" class="btn btn-danger" [disabled]="isRegeneratingStudentKeys[i]" (click)="student.showLinks = !student.showLinks; regenerateStudentKey(student, i);"><tm-ajax-loading *ngIf="isRegeneratingStudentKeys[i]"></tm-ajax-loading>Regenerate key</button>
</td>
</tr>
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,9 +607,15 @@ describe('AdminSearchPageComponent', () => {
component.students = [studentResult];
fixture.detectChanges();

jest.spyOn(ngbModal, 'open').mockImplementation(() => {
return createMockNgbModalRef({});
});
const mockModalRef = {
componentInstance: {},
result: Promise.resolve({}),
dismissed: {
subscribe: jest.fn(),
},
};

jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any);

jest.spyOn(studentService, 'regenerateStudentKey').mockReturnValue(of({
message: 'success',
Expand Down Expand Up @@ -669,9 +675,15 @@ describe('AdminSearchPageComponent', () => {
component.students = [studentResult];
fixture.detectChanges();

jest.spyOn(ngbModal, 'open').mockImplementation(() => {
return createMockNgbModalRef({});
});
const mockModalRef = {
componentInstance: {},
result: Promise.resolve({}),
dismissed: {
subscribe: jest.fn(),
},
};

jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any);

jest.spyOn(studentService, 'regenerateStudentKey').mockReturnValue(throwError(() => ({
error: {
Expand All @@ -698,9 +710,15 @@ describe('AdminSearchPageComponent', () => {
component.instructors = [instructorResult];
fixture.detectChanges();

jest.spyOn(ngbModal, 'open').mockImplementation(() => {
return createMockNgbModalRef({});
});
const mockModalRef = {
componentInstance: {},
result: Promise.resolve({}),
dismissed: {
subscribe: jest.fn(),
},
};

jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any);

jest.spyOn(instructorService, 'regenerateInstructorKey').mockReturnValue(of({
message: 'success',
Expand Down Expand Up @@ -728,9 +746,15 @@ describe('AdminSearchPageComponent', () => {
component.instructors = [instructorResult];
fixture.detectChanges();

jest.spyOn(ngbModal, 'open').mockImplementation(() => {
return createMockNgbModalRef({});
});
const mockModalRef = {
componentInstance: {},
result: Promise.resolve({}),
dismissed: {
subscribe: jest.fn(),
},
};

jest.spyOn(ngbModal, 'open').mockReturnValue(mockModalRef as any);

jest.spyOn(instructorService, 'regenerateInstructorKey').mockReturnValue(throwError(() => ({
error: {
Expand Down
Loading

0 comments on commit b402863

Please sign in to comment.