diff --git a/src/web/app/components/account-requests-table/account-request-table-model.ts b/src/web/app/components/account-requests-table/account-request-table-model.ts new file mode 100644 index 00000000000..ef8b8732f0b --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table-model.ts @@ -0,0 +1,14 @@ +/** + * Model for the row entries in the account requests table. + */ +export interface AccountRequestTableRowModel { + name: string; + email: string; + status: string; + instituteAndCountry: string; + createdAtText: string; + registeredAtText: string; + comments: string; + registrationLink: string; + showLinks: boolean; +} diff --git a/src/web/app/components/account-requests-table/account-request-table.component.html b/src/web/app/components/account-requests-table/account-request-table.component.html new file mode 100644 index 00000000000..7dcb95031c7 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.html @@ -0,0 +1,83 @@ +
+
+
+ Account Requests Found +
+ + Pending Account Requests + +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameEmailStatusInstitute, CountryCreated AtRegistered AtCommentsOptions
+
+
+ + +
+
{{ accountRequest.email }}{{ accountRequest.status }}{{ accountRequest.instituteAndCountry }}{{ accountRequest.createdAtText }}{{ accountRequest.registeredAtText || 'Not Registered Yet' }} +
+ {{ accountRequest.comments }} +
+
+
+
+ + + +
+
+ + + +
+
+ +
+
+
+
    +
  • + Account Registration Link + +
  • +
+
+
diff --git a/src/web/app/components/account-requests-table/account-request-table.component.scss b/src/web/app/components/account-requests-table/account-request-table.component.scss new file mode 100644 index 00000000000..ce0f5d400b3 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.scss @@ -0,0 +1,63 @@ +::ng-deep .highlighted-text { + background-color: yellow; +} + +.table-responsive { + overflow: -moz-scrollbars-horizontal; +} + +.table-responsive > table > thead > tr > th { + white-space: nowrap; +} + +/* stylelint-disable property-no-vendor-prefix */ +::-webkit-scrollbar { + -webkit-appearance: none; + width: 1px; +} + +::-webkit-scrollbar-thumb { + border-radius: 0; + background-color: rgb(0 0 0 / 50%); + box-shadow: 0 0 1px rgb(255 255 255 / 50%); +} + + +#search-table-account-request { + border-collapse: collapse; +} + + +#search-table-account-request th:last-child, +#search-table-account-request td:last-child { + min-width: 10vw; + position: sticky; + right: 0; + z-index: 1; + background-color: #F8F9FA; +} + +#search-table-account-request th:last-child::after, +#search-table-account-request td:last-child::after { + content: ""; + position: absolute; + left: -1px; + top: 0; + bottom: 0; + width: 1px; + background: #c8c7c7; + z-index: 1; +} + +#comment-box { + min-height: 5vh; + width: max(800px, 35vw); + max-width: max-content; + word-break: break-word; + word-wrap: break-all; + +} + +.dropdown-item { + border: none; +} 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 new file mode 100755 index 00000000000..dc3ef132795 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.ts @@ -0,0 +1,105 @@ +import { Component, Input } from '@angular/core'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { AccountRequestTableRowModel } from './account-request-table-model'; +import { AccountService } from '../../../services/account.service'; +import { SimpleModalService } from '../../../services/simple-modal.service'; +import { StatusMessageService } from '../../../services/status-message.service'; +import { MessageOutput } from '../../../types/api-output'; +import { ErrorMessageOutput } from '../../error-message-output'; +import { SimpleModalType } from '../simple-modal/simple-modal-type'; +import { collapseAnim } from '../teammates-common/collapse-anim'; + +/** + * Account requests table component. + */ +@Component({ + selector: 'tm-account-request-table', + templateUrl: './account-request-table.component.html', + styleUrls: ['./account-request-table.component.scss'], + animations: [collapseAnim], +}) + +export class AccountRequestTableComponent { + + @Input() + accountRequests: AccountRequestTableRowModel[] = []; + + @Input() + searchString = ''; + + constructor( + private statusMessageService: StatusMessageService, + private simpleModalService: SimpleModalService, + private accountService: AccountService, + ) {} + + /** + * Shows all account requests' links in the page. + */ + showAllAccountRequestsLinks(): void { + for (const accountRequest of this.accountRequests) { + accountRequest.showLinks = true; + } + } + + /** + * Hides all account requests' links in the page. + */ + hideAllAccountRequestsLinks(): void { + for (const accountRequest of this.accountRequests) { + accountRequest.showLinks = false; + } + } + + resetAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent = `Are you sure you want to reset the account request for + ${accountRequest.name} with email ${accountRequest.email} from + ${accountRequest.instituteAndCountry}? + An email with the account registration link will also be sent to the instructor.`; + const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( + `Reset account request for ${accountRequest.name}?`, SimpleModalType.WARNING, modalContent); + + modalRef.result.then(() => { + this.accountService.resetAccountRequest(accountRequest.email, accountRequest.instituteAndCountry) + .subscribe({ + next: () => { + this.statusMessageService + .showSuccessToast(`Reset successful. An email has been sent to ${accountRequest.email}.`); + accountRequest.registeredAtText = ''; + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + deleteAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent: string = `Are you sure you want to delete the account request for + ${accountRequest.name} with email ${accountRequest.email} from + ${accountRequest.instituteAndCountry}?`; + const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( + `Delete account request for ${accountRequest.name}?`, SimpleModalType.DANGER, modalContent); + + modalRef.result.then(() => { + this.accountService.deleteAccountRequest(accountRequest.email, accountRequest.instituteAndCountry) + .subscribe({ + next: (resp: MessageOutput) => { + this.statusMessageService.showSuccessToast(resp.message); + this.accountRequests = this.accountRequests.filter((x: AccountRequestTableRowModel) => x !== accountRequest); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + viewAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent: string = `Comment: ${accountRequest.comments || ''}`; + const modalRef: NgbModalRef = this.simpleModalService.openInformationModal( + `Comments for ${accountRequest.name} Request`, SimpleModalType.INFO, modalContent); + + modalRef.result.then(() => {}, () => {}); + } +} 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 new file mode 100644 index 00000000000..1a05086cc4b --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.module.ts @@ -0,0 +1,26 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbTooltipModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { AccountRequestTableComponent } from './account-request-table.component'; +import { Pipes } from '../../pipes/pipes.module'; + +/** + * Module for account requests table. + */ +@NgModule({ + declarations: [ + AccountRequestTableComponent, + ], + exports: [ + AccountRequestTableComponent, + ], + imports: [ + CommonModule, + FormsModule, + NgbTooltipModule, + NgbDropdownModule, + Pipes, + ], +}) +export class AccountRequestTableModule { } diff --git a/src/web/app/pages-admin/admin-home-page/__snapshots__/admin-home-page.component.spec.ts.snap b/src/web/app/pages-admin/admin-home-page/__snapshots__/admin-home-page.component.spec.ts.snap index db32b59235a..bc11da02df6 100644 --- a/src/web/app/pages-admin/admin-home-page/__snapshots__/admin-home-page.component.spec.ts.snap +++ b/src/web/app/pages-admin/admin-home-page/__snapshots__/admin-home-page.component.spec.ts.snap @@ -2,9 +2,12 @@ exports[`AdminHomePageComponent should snap with default view 1`] = `
Student for the following {{ account.studentCours
+ diff --git a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.spec.ts b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.spec.ts index 3b25ac9d1a4..00dbb94d783 100644 --- a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.spec.ts +++ b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.spec.ts @@ -13,8 +13,10 @@ import { SimpleModalService } from '../../../services/simple-modal.service'; import { StatusMessageService } from '../../../services/status-message.service'; import { createMockNgbModalRef } from '../../../test-helpers/mock-ngb-modal-ref'; import { AccountRequestStatus } from '../../../types/api-output'; +import { AccountRequestTableModule } from '../../components/account-requests-table/account-request-table.module'; import { AjaxLoadingModule } from '../../components/ajax-loading/ajax-loading.module'; import { LoadingSpinnerModule } from '../../components/loading-spinner/loading-spinner.module'; +import { FormatDateDetailPipe } from '../../components/teammates-common/format-date-detail.pipe'; describe('AdminHomePageComponent', () => { let component: AdminHomePageComponent; @@ -34,12 +36,14 @@ describe('AdminHomePageComponent', () => { FormsModule, HttpClientTestingModule, LoadingSpinnerModule, + AccountRequestTableModule, AjaxLoadingModule, RouterTestingModule, ], providers: [ AccountService, CourseService, + FormatDateDetailPipe, SimpleModalService, StatusMessageService, LinkService, diff --git a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.ts b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.ts index e6544a0e7b7..efe69122c39 100644 --- a/src/web/app/pages-admin/admin-home-page/admin-home-page.component.ts +++ b/src/web/app/pages-admin/admin-home-page/admin-home-page.component.ts @@ -1,4 +1,4 @@ -import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { Component, TemplateRef, ViewChild, OnInit } from '@angular/core'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { forkJoin, Observable, of, throwError } from 'rxjs'; import { catchError, finalize, map, mergeMap } from 'rxjs/operators'; @@ -8,8 +8,11 @@ import { CourseService } from '../../../services/course.service'; import { LinkService } from '../../../services/link.service'; import { SimpleModalService } from '../../../services/simple-modal.service'; import { StatusMessageService } from '../../../services/status-message.service'; -import { Account, AccountRequest, Accounts, Courses, JoinLink } from '../../../types/api-output'; +import { TimezoneService } from '../../../services/timezone.service'; +import { Account, AccountRequest, Accounts, AccountRequests, Courses, JoinLink } from '../../../types/api-output'; +import { AccountRequestTableRowModel } from '../../components/account-requests-table/account-request-table-model'; import { SimpleModalType } from '../../components/simple-modal/simple-modal-type'; +import { FormatDateDetailPipe } from '../../components/teammates-common/format-date-detail.pipe'; import { ErrorMessageOutput } from '../../error-message-output'; /** @@ -20,7 +23,7 @@ import { ErrorMessageOutput } from '../../error-message-output'; templateUrl: './admin-home-page.component.html', styleUrls: ['./admin-home-page.component.scss'], }) -export class AdminHomePageComponent { +export class AdminHomePageComponent implements OnInit { instructorDetails: string = ''; instructorName: string = ''; @@ -28,7 +31,11 @@ export class AdminHomePageComponent { instructorInstitution: string = ''; instructorsConsolidated: InstructorData[] = []; + accountReqs: AccountRequestTableRowModel[] = []; activeRequests: number = 0; + currentPage: number = 1; + pageSize: number = 20; + items$: Observable = of([]); isAddingInstructors: boolean = false; @@ -43,10 +50,16 @@ export class AdminHomePageComponent { private courseService: CourseService, private simpleModalService: SimpleModalService, private statusMessageService: StatusMessageService, + private timezoneService: TimezoneService, private linkService: LinkService, private ngbModal: NgbModal, + private formatDateDetailPipe: FormatDateDetailPipe, ) {} + ngOnInit(): void { + this.fetchAccountRequests(); + } + /** * Validates and adds the instructor details filled with first form. */ @@ -236,6 +249,35 @@ export class AdminHomePageComponent { ); } + private formatAccountRequests(requests: AccountRequests): AccountRequestTableRowModel[] { + const timezone: string = this.timezoneService.guessTimezone() || 'UTC'; + return requests.accountRequests.map((request) => { + return { + name: request.name, + email: request.email, + status: request.status, + instituteAndCountry: request.institute, + createdAtText: this.formatDateDetailPipe.transform(request.createdAt, timezone), + registeredAtText: request.registeredAt + ? this.formatDateDetailPipe.transform(request.registeredAt, timezone) : '', + comments: request.comments || '', + registrationLink: '', + showLinks: false, + }; + }); + } + + fetchAccountRequests(): void { + this.accountService.getPendingAccountRequests().subscribe({ + next: (resp: AccountRequests) => { + this.accountReqs = this.formatAccountRequests(resp); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + } + resetAccountRequest(i: number): void { const modalContent = `Are you sure you want to reset the account request for ${this.instructorsConsolidated[i].name} with email diff --git a/src/web/app/pages-admin/admin-home-page/admin-home-page.module.ts b/src/web/app/pages-admin/admin-home-page/admin-home-page.module.ts index 163b70e981e..d336c46e5ba 100644 --- a/src/web/app/pages-admin/admin-home-page/admin-home-page.module.ts +++ b/src/web/app/pages-admin/admin-home-page/admin-home-page.module.ts @@ -4,8 +4,10 @@ import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { AdminHomePageComponent } from './admin-home-page.component'; import { NewInstructorDataRowComponent } from './new-instructor-data-row/new-instructor-data-row.component'; +import { AccountRequestTableModule } from '../../components/account-requests-table/account-request-table.module'; import { AjaxLoadingModule } from '../../components/ajax-loading/ajax-loading.module'; import { LoadingSpinnerModule } from '../../components/loading-spinner/loading-spinner.module'; +import { FormatDateDetailPipe } from '../../components/teammates-common/format-date-detail.pipe'; const routes: Routes = [ { @@ -29,8 +31,12 @@ const routes: Routes = [ CommonModule, FormsModule, RouterModule.forChild(routes), + AccountRequestTableModule, AjaxLoadingModule, LoadingSpinnerModule, ], + providers: [ + FormatDateDetailPipe, + ], }) export class AdminHomePageModule { } 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 6b076b170bd..ba3a2abbad5 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 @@ -19,6 +19,7 @@ import { import { StatusMessageService } from '../../../services/status-message.service'; import { StudentService } from '../../../services/student.service'; import { createMockNgbModalRef } from '../../../test-helpers/mock-ngb-modal-ref'; +import { AccountRequestStatus } from '../../../types/api-output'; const DEFAULT_FEEDBACK_SESSION_GROUP: FeedbackSessionsGroup = { sessionName: { @@ -72,10 +73,12 @@ const DEFAULT_ACCOUNT_REQUEST_SEARCH_RESULT: AccountRequestSearchResult = { name: 'name', email: 'email', institute: 'institute', + status: AccountRequestStatus.PENDING, registrationLink: 'registrationLink', createdAtText: 'Tue, 08 Feb 2022, 08:23 AM +00:00', registeredAtText: null, showLinks: false, + comments: '', }; describe('AdminSearchPageComponent', () => { @@ -239,10 +242,12 @@ describe('AdminSearchPageComponent', () => { name: 'name', email: 'email', institute: 'institute', + status: AccountRequestStatus.PENDING, registrationLink: 'registrationLink', createdAtText: 'Tue, 08 Feb 2022, 08:23 AM +00:00', registeredAtText: null, showLinks: true, + comments: '', }, ]; @@ -409,18 +414,22 @@ describe('AdminSearchPageComponent', () => { name: 'name1', email: 'email1', institute: 'institute1', + status: AccountRequestStatus.PENDING, registrationLink: 'registrationLink1', createdAtText: 'Tue, 08 Feb 2022, 08:23 AM +00:00', - registeredAtText: null, - showLinks: true, + registeredAtText: '', + showLinks: false, + comments: '', }, { name: 'name2', email: 'email2', institute: 'institute2', + status: AccountRequestStatus.PENDING, registrationLink: 'registrationLink2', createdAtText: 'Tue, 08 Feb 2022, 08:23 AM +00:00', registeredAtText: 'Wed, 09 Feb 2022, 10:23 AM +00:00', - showLinks: true, + showLinks: false, + comments: '', }]; jest.spyOn(searchService, 'searchAdmin').mockReturnValue(of({ diff --git a/src/web/services/account.service.ts b/src/web/services/account.service.ts index 914a825a21a..8877da6ec29 100644 --- a/src/web/services/account.service.ts +++ b/src/web/services/account.service.ts @@ -2,7 +2,15 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpRequestService } from './http-request.service'; import { ResourceEndpoints } from '../types/api-const'; -import { Account, AccountRequest, Accounts, JoinLink, MessageOutput } from '../types/api-output'; +import { + Account, + AccountRequest, + Accounts, + AccountRequests, + JoinLink, + MessageOutput, + AccountRequestStatus, +} from '../types/api-output'; import { AccountCreateRequest } from '../types/api-request'; /** @@ -107,4 +115,15 @@ export class AccountService { return this.httpRequestService.get(ResourceEndpoints.ACCOUNTS, paramMap); } + /** + * Gets account requests by calling API. + */ + getPendingAccountRequests(): Observable { + const paramMap = { + status: AccountRequestStatus.PENDING, + }; + + return this.httpRequestService.get(ResourceEndpoints.ACCOUNT_REQUESTS, paramMap); + } + } diff --git a/src/web/services/search.service.spec.ts b/src/web/services/search.service.spec.ts index e548b693cff..a58e525e2a2 100644 --- a/src/web/services/search.service.spec.ts +++ b/src/web/services/search.service.spec.ts @@ -191,6 +191,7 @@ describe('SearchService', () => { name: 'Test Instructor', institute: 'Test Institute', email: 'test@example.com', + comments: 'This is a test account request', status: AccountRequestStatus.APPROVED, }; diff --git a/src/web/services/search.service.ts b/src/web/services/search.service.ts index 7542da645cc..5d4dfdc0d53 100644 --- a/src/web/services/search.service.ts +++ b/src/web/services/search.service.ts @@ -306,16 +306,22 @@ export class SearchService { registeredAtText: '', registrationLink: '', showLinks: false, + status: '', + comments: '', }; - const { registrationKey, createdAt, registeredAt, name, institute, email }: AccountRequest = accountRequest; + const { + registrationKey, createdAt, registeredAt, + name, institute, email, status, comments, + }: AccountRequest = accountRequest; const timezone: string = this.timezoneService.guessTimezone() || 'UTC'; accountRequestResult.createdAtText = this.formatTimestampAsString(createdAt, timezone); accountRequestResult.registeredAtText = registeredAt ? this.formatTimestampAsString(registeredAt, timezone) : null; + accountRequestResult.comments = comments || ''; const registrationLink: string = this.linkService.generateAccountRegistrationLink(registrationKey); - accountRequestResult = { ...accountRequestResult, name, email, institute, registrationLink }; + accountRequestResult = { ...accountRequestResult, name, email, institute, registrationLink, status }; return accountRequestResult; } @@ -466,11 +472,13 @@ export interface AdminSearchResult { export interface AccountRequestSearchResult { name: string; email: string; + status: string; institute: string; createdAtText: string; registeredAtText: string | null; registrationLink: string; showLinks: boolean; + comments: string; } /**