diff --git a/src/main/java/teammates/logic/api/EmailGenerator.java b/src/main/java/teammates/logic/api/EmailGenerator.java index e72708aa267..d7a6165b599 100644 --- a/src/main/java/teammates/logic/api/EmailGenerator.java +++ b/src/main/java/teammates/logic/api/EmailGenerator.java @@ -823,7 +823,7 @@ private EmailWrapper generateFeedbackSessionEmailBaseForNotifiedInstructors( "${feedbackSessionName}", SanitizationHelper.sanitizeForHtml(session.getFeedbackSessionName()), "${deadline}", SanitizationHelper.sanitizeForHtml( TimeHelper.formatInstant(endTime, session.getTimeZone(), DATETIME_DISPLAY_FORMAT)), - "${instructorPreamble}", fillUpInstructorPreamble(course), + "${instructorPreamble}", fillUpInstructorPreamble(course, session), "${sessionInstructions}", session.getInstructionsString(), "${submitUrl}", "{in the actual email sent to the students, this will be the unique link}", "${reportUrl}", "{in the actual email sent to the students, this will be the unique link}", @@ -1020,10 +1020,14 @@ private String fillUpInstructorRejoinAfterGoogleIdResetFragment(InstructorAttrib "${supportEmail}", Config.SUPPORT_EMAIL); } - private String fillUpInstructorPreamble(CourseAttributes course) { + private String fillUpInstructorPreamble(CourseAttributes course, FeedbackSessionAttributes session) { + var recoveryUrl = Config.getFrontEndAppUrl(Const.WebPageURIs.SESSIONS_LINK_RECOVERY_PAGE).toAbsoluteString(); return Templates.populateTemplate(EmailTemplates.FRAGMENT_INSTRUCTOR_COPY_PREAMBLE, "${courseId}", SanitizationHelper.sanitizeForHtml(course.getId()), - "${courseName}", SanitizationHelper.sanitizeForHtml(course.getName())); + "${courseName}", SanitizationHelper.sanitizeForHtml(course.getName()), + "${feedbackSessionName}", + SanitizationHelper.sanitizeForHtml(session.getFeedbackSessionName()), + "${sessionsRecoveryLink}", recoveryUrl); } /** diff --git a/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java b/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java index 6e9683f5213..322a4459c9c 100644 --- a/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java +++ b/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java @@ -851,7 +851,7 @@ private EmailWrapper generateFeedbackSessionEmailBaseForNotifiedInstructors( "${feedbackSessionName}", SanitizationHelper.sanitizeForHtml(session.getName()), "${deadline}", SanitizationHelper.sanitizeForHtml( TimeHelper.formatInstant(endTime, session.getCourse().getTimeZone(), DATETIME_DISPLAY_FORMAT)), - "${instructorPreamble}", fillUpInstructorPreamble(course), + "${instructorPreamble}", fillUpInstructorPreamble(course, session), "${sessionInstructions}", session.getInstructionsString(), "${submitUrl}", "{in the actual email sent to the students, this will be the unique link}", "${reportUrl}", "{in the actual email sent to the students, this will be the unique link}", @@ -1098,10 +1098,14 @@ private String fillUpInstructorRejoinAfterGoogleIdResetFragment(Instructor instr "${supportEmail}", Config.SUPPORT_EMAIL); } - private String fillUpInstructorPreamble(Course course) { + private String fillUpInstructorPreamble(Course course, FeedbackSession session) { + var recoveryUrl = Config.getFrontEndAppUrl(Const.WebPageURIs.SESSIONS_LINK_RECOVERY_PAGE).toAbsoluteString(); + return Templates.populateTemplate(EmailTemplates.FRAGMENT_INSTRUCTOR_COPY_PREAMBLE, "${courseId}", SanitizationHelper.sanitizeForHtml(course.getId()), - "${courseName}", SanitizationHelper.sanitizeForHtml(course.getName())); + "${courseName}", SanitizationHelper.sanitizeForHtml(course.getName()), + "${feedbackSessionName}", SanitizationHelper.sanitizeForHtml(session.getName()), + "${sessionsRecoveryLink}", recoveryUrl); } /** diff --git a/src/main/resources/instructorEmailFragment-instructorCopyPreamble.html b/src/main/resources/instructorEmailFragment-instructorCopyPreamble.html index 416b2d0329d..82708830ce5 100644 --- a/src/main/resources/instructorEmailFragment-instructorCopyPreamble.html +++ b/src/main/resources/instructorEmailFragment-instructorCopyPreamble.html @@ -1,4 +1,12 @@

+ Kindly note that this email simply serves as a preview of how the email will appear to the + students, and the link is not the actual link that the students will receive. + As such, please do not forward this email to students. + We recommend that you make the following announcement (edit content as you see fit) using an alternative means (e.g., your course announcements) to alert students: +


+ The TEAMMATES session ${feedbackSessionName} in course [${courseId}] ${courseName} is now open. + If you did not receive the unique access link via email, and you can't find it in your spam box either, go to this link to recover the access link. +
The email below has been sent to students of course: [${courseId}] ${courseName}.

=== Email message as seen by the students === diff --git a/src/test/resources/emails/sessionClosingEmailCopyToInstructor.html b/src/test/resources/emails/sessionClosingEmailCopyToInstructor.html index 28e230525fe..f9416466212 100644 --- a/src/test/resources/emails/sessionClosingEmailCopyToInstructor.html +++ b/src/test/resources/emails/sessionClosingEmailCopyToInstructor.html @@ -1,6 +1,14 @@

Hello Instructor1 Course1,

+ Kindly note that this email simply serves as a preview of how the email will appear to the + students, and the link is not the actual link that the students will receive. + As such, please do not forward this email to students. + We recommend that you make the following announcement (edit content as you see fit) using an alternative means (e.g., your course announcements) to alert students: +


+ The TEAMMATES session First feedback session in course [idOfTypicalCourse1] Typical Course 1 with 2 Evals is now open. + If you did not receive the unique access link via email, and you can't find it in your spam box either, go to this link to recover the access link. +
The email below has been sent to students of course: [idOfTypicalCourse1] Typical Course 1 with 2 Evals.

=== Email message as seen by the students === diff --git a/src/test/resources/emails/sessionClosingEmailTestingSanitizationCopyToInstructor.html b/src/test/resources/emails/sessionClosingEmailTestingSanitizationCopyToInstructor.html index 93225e513b0..9c301cae53c 100644 --- a/src/test/resources/emails/sessionClosingEmailTestingSanitizationCopyToInstructor.html +++ b/src/test/resources/emails/sessionClosingEmailTestingSanitizationCopyToInstructor.html @@ -1,6 +1,14 @@

Hello Instructor<script> alert('hi!'); </script>,

+ Kindly note that this email simply serves as a preview of how the email will appear to the + students, and the link is not the actual link that the students will receive. + As such, please do not forward this email to students. + We recommend that you make the following announcement (edit content as you see fit) using an alternative means (e.g., your course announcements) to alert students: +


+ The TEAMMATES session Normal feedback session name in course [idOfTestingSanitizationCourse] Testing<script> alert('hi!'); </script> is now open. + If you did not receive the unique access link via email, and you can't find it in your spam box either, go to this link to recover the access link. +
The email below has been sent to students of course: [idOfTestingSanitizationCourse] Testing<script> alert('hi!'); </script>.

=== Email message as seen by the students === diff --git a/src/test/resources/emails/sessionOpeningEmailCopyToInstructor.html b/src/test/resources/emails/sessionOpeningEmailCopyToInstructor.html index feba86d7794..e4609cb333c 100644 --- a/src/test/resources/emails/sessionOpeningEmailCopyToInstructor.html +++ b/src/test/resources/emails/sessionOpeningEmailCopyToInstructor.html @@ -1,6 +1,14 @@

Hello Instructor1 Course1,

+ Kindly note that this email simply serves as a preview of how the email will appear to the + students, and the link is not the actual link that the students will receive. + As such, please do not forward this email to students. + We recommend that you make the following announcement (edit content as you see fit) using an alternative means (e.g., your course announcements) to alert students: +


+ The TEAMMATES session First feedback session in course [idOfTypicalCourse1] Typical Course 1 with 2 Evals is now open. + If you did not receive the unique access link via email, and you can't find it in your spam box either, go to this link to recover the access link. +
The email below has been sent to students of course: [idOfTypicalCourse1] Typical Course 1 with 2 Evals.

=== Email message as seen by the students === diff --git a/src/test/resources/emails/sessionOpeningEmailTestingSanitizationCopyToInstructor.html b/src/test/resources/emails/sessionOpeningEmailTestingSanitizationCopyToInstructor.html index 764f6871064..440e5d80ec5 100644 --- a/src/test/resources/emails/sessionOpeningEmailTestingSanitizationCopyToInstructor.html +++ b/src/test/resources/emails/sessionOpeningEmailTestingSanitizationCopyToInstructor.html @@ -1,6 +1,14 @@

Hello Instructor<script> alert('hi!'); </script>,

+ Kindly note that this email simply serves as a preview of how the email will appear to the + students, and the link is not the actual link that the students will receive. + As such, please do not forward this email to students. + We recommend that you make the following announcement (edit content as you see fit) using an alternative means (e.g., your course announcements) to alert students: +


+ The TEAMMATES session Normal feedback session name in course [idOfTestingSanitizationCourse] Testing<script> alert('hi!'); </script> is now open. + If you did not receive the unique access link via email, and you can't find it in your spam box either, go to this link to recover the access link. +
The email below has been sent to students of course: [idOfTestingSanitizationCourse] Testing<script> alert('hi!'); </script>.

=== Email message as seen by the students === diff --git a/src/test/resources/emails/sessionPublishedEmailCopyToInstructor.html b/src/test/resources/emails/sessionPublishedEmailCopyToInstructor.html index f0892ba556a..93a87661bd9 100644 --- a/src/test/resources/emails/sessionPublishedEmailCopyToInstructor.html +++ b/src/test/resources/emails/sessionPublishedEmailCopyToInstructor.html @@ -1,6 +1,14 @@

Hello Instructor1 Course1,

+ Kindly note that this email simply serves as a preview of how the email will appear to the + students, and the link is not the actual link that the students will receive. + As such, please do not forward this email to students. + We recommend that you make the following announcement (edit content as you see fit) using an alternative means (e.g., your course announcements) to alert students: +


+ The TEAMMATES session First feedback session in course [idOfTypicalCourse1] Typical Course 1 with 2 Evals is now open. + If you did not receive the unique access link via email, and you can't find it in your spam box either, go to this link to recover the access link. +
The email below has been sent to students of course: [idOfTypicalCourse1] Typical Course 1 with 2 Evals.

=== Email message as seen by the students === diff --git a/src/test/resources/emails/sessionReminderEmailCopyToInstructor.html b/src/test/resources/emails/sessionReminderEmailCopyToInstructor.html index 0ebbf71fa27..d4c31609da6 100644 --- a/src/test/resources/emails/sessionReminderEmailCopyToInstructor.html +++ b/src/test/resources/emails/sessionReminderEmailCopyToInstructor.html @@ -1,6 +1,14 @@

Hello Instructor1 Course1,

+ Kindly note that this email simply serves as a preview of how the email will appear to the + students, and the link is not the actual link that the students will receive. + As such, please do not forward this email to students. + We recommend that you make the following announcement (edit content as you see fit) using an alternative means (e.g., your course announcements) to alert students: +


+ The TEAMMATES session First feedback session in course [idOfTypicalCourse1] Typical Course 1 with 2 Evals is now open. + If you did not receive the unique access link via email, and you can't find it in your spam box either, go to this link to recover the access link. +
The email below has been sent to students of course: [idOfTypicalCourse1] Typical Course 1 with 2 Evals.

=== Email message as seen by the students === diff --git a/src/test/resources/emails/sessionUnpublishedEmailCopyToInstructor.html b/src/test/resources/emails/sessionUnpublishedEmailCopyToInstructor.html index 9f5c8902d57..57f882f777a 100644 --- a/src/test/resources/emails/sessionUnpublishedEmailCopyToInstructor.html +++ b/src/test/resources/emails/sessionUnpublishedEmailCopyToInstructor.html @@ -1,6 +1,14 @@

Hello Instructor1 Course1,

+ Kindly note that this email simply serves as a preview of how the email will appear to the + students, and the link is not the actual link that the students will receive. + As such, please do not forward this email to students. + We recommend that you make the following announcement (edit content as you see fit) using an alternative means (e.g., your course announcements) to alert students: +


+ The TEAMMATES session First feedback session in course [idOfTypicalCourse1] Typical Course 1 with 2 Evals is now open. + If you did not receive the unique access link via email, and you can't find it in your spam box either, go to this link to recover the access link. +
The email below has been sent to students of course: [idOfTypicalCourse1] Typical Course 1 with 2 Evals.

=== Email message as seen by the students === diff --git a/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap b/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap index bbf59a6a945..3b8d24519c6 100644 --- a/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap +++ b/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap @@ -4,6 +4,9 @@ exports[`AccountRequestTableComponent should display account requests with no re - + - +
- - + +
- +
diff --git a/src/web/app/components/account-requests-table/account-request-table.component.spec.ts b/src/web/app/components/account-requests-table/account-request-table.component.spec.ts index 40ccbc007c3..0541a0fa69b 100644 --- a/src/web/app/components/account-requests-table/account-request-table.component.spec.ts +++ b/src/web/app/components/account-requests-table/account-request-table.component.spec.ts @@ -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', @@ -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: { @@ -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); diff --git a/src/web/app/components/account-requests-table/account-request-table.component.ts b/src/web/app/components/account-requests-table/account-request-table.component.ts index 3e7fc957730..7634dfc58e1 100755 --- a/src/web/app/components/account-requests-table/account-request-table.component.ts +++ b/src/web/app/components/account-requests-table/account-request-table.component.ts @@ -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, @@ -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({ @@ -103,14 +108,17 @@ 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 ${accountRequest.name} with email ${accountRequest.email} from ${accountRequest.instituteAndCountry}? @@ -118,6 +126,10 @@ export class AccountRequestTableComponent { const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( `Reset account request for ${accountRequest.name}?`, SimpleModalType.WARNING, modalContent); + modalRef.dismissed.subscribe(() => { + this.isResettingAccount[index] = false; + }); + modalRef.result.then(() => { this.accountService.resetAccountRequest(accountRequest.id) .subscribe({ @@ -125,9 +137,11 @@ export class AccountRequestTableComponent { 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; }, }); }, () => {}); @@ -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) @@ -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; }, }); }, () => {}); diff --git a/src/web/app/components/account-requests-table/account-request-table.module.ts b/src/web/app/components/account-requests-table/account-request-table.module.ts index 2ff431b1021..eb0768663a9 100644 --- a/src/web/app/components/account-requests-table/account-request-table.module.ts +++ b/src/web/app/components/account-requests-table/account-request-table.module.ts @@ -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'; /** @@ -29,6 +30,7 @@ import { RichTextEditorModule } from '../rich-text-editor/rich-text-editor.modul NgbDropdownModule, Pipes, RichTextEditorModule, + AjaxLoadingModule, ], }) export class AccountRequestTableModule { } diff --git a/src/web/app/pages-admin/admin-search-page/__snapshots__/admin-search-page.component.spec.ts.snap b/src/web/app/pages-admin/admin-search-page/__snapshots__/admin-search-page.component.spec.ts.snap index 58f15f5a35d..ded3505ea4b 100644 --- a/src/web/app/pages-admin/admin-search-page/__snapshots__/admin-search-page.component.spec.ts.snap +++ b/src/web/app/pages-admin/admin-search-page/__snapshots__/admin-search-page.component.spec.ts.snap @@ -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]} @@ -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]} @@ -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]} @@ -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]} @@ -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]} diff --git a/src/web/app/pages-admin/admin-search-page/admin-search-page.component.html b/src/web/app/pages-admin/admin-search-page/admin-search-page.component.html index 8084c47362c..eb4e8c3ad43 100644 --- a/src/web/app/pages-admin/admin-search-page/admin-search-page.component.html +++ b/src/web/app/pages-admin/admin-search-page/admin-search-page.component.html @@ -64,7 +64,7 @@ Reset Google ID
- + @@ -159,7 +159,7 @@ Reset Google ID
- + diff --git a/src/web/app/pages-admin/admin-search-page/admin-search-page.component.spec.ts b/src/web/app/pages-admin/admin-search-page/admin-search-page.component.spec.ts index 143b3ca9d75..da5b794b03f 100644 --- a/src/web/app/pages-admin/admin-search-page/admin-search-page.component.spec.ts +++ b/src/web/app/pages-admin/admin-search-page/admin-search-page.component.spec.ts @@ -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', @@ -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: { @@ -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', @@ -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: { diff --git a/src/web/app/pages-admin/admin-search-page/admin-search-page.component.ts b/src/web/app/pages-admin/admin-search-page/admin-search-page.component.ts index 08b577d788c..ede96196a81 100755 --- a/src/web/app/pages-admin/admin-search-page/admin-search-page.component.ts +++ b/src/web/app/pages-admin/admin-search-page/admin-search-page.component.ts @@ -43,6 +43,9 @@ export class AdminSearchPageComponent { accountRequests: AccountRequestTableRowModel[] = []; characterLimit = 100; + isRegeneratingInstructorKeys: boolean[] = []; + isRegeneratingStudentKeys: boolean[] = []; + constructor( private statusMessageService: StatusMessageService, private simpleModalService: SimpleModalService, @@ -83,6 +86,9 @@ export class AdminSearchPageComponent { this.hideAllInstructorsLinks(); this.hideAllStudentsLinks(); + this.isRegeneratingInstructorKeys = new Array(this.instructors.length).fill(false); + this.isRegeneratingStudentKeys = new Array(this.students.length).fill(false); + // prompt user to use more specific terms if search results limit reached const limit: number = ApiConst.SEARCH_QUERY_SIZE_LIMIT; const limitsReached: string[] = []; @@ -222,22 +228,29 @@ export class AdminSearchPageComponent { /** * Regenerates the student's registration key. */ - regenerateStudentKey(student: StudentAccountSearchResult): void { + regenerateStudentKey(student: StudentAccountSearchResult, index: number): void { + this.isRegeneratingStudentKeys[index] = true; const modalContent: string = `Are you sure you want to regenerate the registration key for ${student.name} for the course ${student.courseId}? An email will be sent to the student with all the new course registration and feedback session links.`; const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( `Regenerate ${student.name}'s course links?`, SimpleModalType.WARNING, modalContent); + modalRef.dismissed.subscribe(() => { + this.isRegeneratingStudentKeys[index] = false; + }); + modalRef.result.then(() => { this.studentService.regenerateStudentKey(student.courseId, student.email) .subscribe({ next: (resp: RegenerateKey) => { this.statusMessageService.showSuccessToast(resp.message); this.updateDisplayedStudentCourseLinks(student, resp.newRegistrationKey); + this.isRegeneratingStudentKeys[index] = false; }, error: (response: ErrorMessageOutput) => { this.statusMessageService.showErrorToast(response.error.message); + this.isRegeneratingStudentKeys[index] = false; }, }); }, () => {}); @@ -246,22 +259,29 @@ export class AdminSearchPageComponent { /** * Regenerates the instructor's registration key. */ - regenerateInstructorKey(instructor: InstructorAccountSearchResult): void { + regenerateInstructorKey(instructor: InstructorAccountSearchResult, index: number): void { + this.isRegeneratingInstructorKeys[index] = true; const modalContent: string = `Are you sure you want to regenerate the registration key for ${instructor.name} for the course ${instructor.courseId}? An email will be sent to the instructor with all the new course registration and feedback session links.`; const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( `Regenerate ${instructor.name}'s course links?`, SimpleModalType.WARNING, modalContent); + modalRef.dismissed.subscribe(() => { + this.isRegeneratingInstructorKeys[index] = false; + }); + modalRef.result.then(() => { this.instructorService.regenerateInstructorKey(instructor.courseId, instructor.email) .subscribe({ next: (resp: RegenerateKey) => { this.statusMessageService.showSuccessToast(resp.message); this.updateDisplayedInstructorCourseLinks(instructor, resp.newRegistrationKey); + this.isRegeneratingInstructorKeys[index] = false; }, error: (response: ErrorMessageOutput) => { this.statusMessageService.showErrorToast(response.error.message); + this.isRegeneratingInstructorKeys[index] = false; }, }); }, () => {}); diff --git a/src/web/app/pages-admin/admin-search-page/admin-search-page.module.ts b/src/web/app/pages-admin/admin-search-page/admin-search-page.module.ts index 6b70a93077a..71925363326 100644 --- a/src/web/app/pages-admin/admin-search-page/admin-search-page.module.ts +++ b/src/web/app/pages-admin/admin-search-page/admin-search-page.module.ts @@ -7,6 +7,7 @@ import { AdminSearchPageComponent } from './admin-search-page.component'; import { AccountRequestTableModule, } from '../../components/account-requests-table/account-request-table.module'; +import { AjaxLoadingModule } from '../../components/ajax-loading/ajax-loading.module'; import { Pipes } from '../../pipes/pipes.module'; const routes: Routes = [ @@ -33,6 +34,7 @@ const routes: Routes = [ AccountRequestTableModule, RouterModule.forChild(routes), Pipes, + AjaxLoadingModule, ], }) export class AdminSearchPageModule { }