From 40613dff7486e2224c1375a218f55f442279a4e9 Mon Sep 17 00:00:00 2001
From: domoberzin <74132255+domoberzin@users.noreply.github.com>
Date: Wed, 27 Mar 2024 19:23:23 +0800
Subject: [PATCH] [#11878] Update Admin Home Page UI for ARF (#12933)
* create component for account request table
* cherry pick admin home page changes
* remove testing code
* fix lint and css issues
* fix admin home page snaps
* update admin home snaps
* remove edit approve and reject components
* modify css
* delete edit and reject modal components
* revert spec file changes
* integrate new types
* fix lint
* use enum for status
* fix lint
* fix css lint
* fix lint
* fix lint
* use enum and remove infinite scroll
* remove approve account request code
* remove extra div
* fix url
* modify comments
* revert extra formatting
* remove plural form and use date pipe
* fix naming
* fix spec file and update institute formatting
* fix lint
* combine institute and country columns
---
.../account-request-table-model.ts | 14 +++
.../account-request-table.component.html | 83 ++++++++++++++
.../account-request-table.component.scss | 63 +++++++++++
.../account-request-table.component.ts | 105 ++++++++++++++++++
.../account-request-table.module.ts | 26 +++++
.../admin-home-page.component.spec.ts.snap | 18 +++
.../admin-home-page.component.html | 1 +
.../admin-home-page.component.spec.ts | 4 +
.../admin-home-page.component.ts | 48 +++++++-
.../admin-home-page/admin-home-page.module.ts | 6 +
.../admin-search-page.component.spec.ts | 15 ++-
src/web/services/account.service.ts | 21 +++-
src/web/services/search.service.spec.ts | 1 +
src/web/services/search.service.ts | 12 +-
14 files changed, 408 insertions(+), 9 deletions(-)
create mode 100644 src/web/app/components/account-requests-table/account-request-table-model.ts
create mode 100644 src/web/app/components/account-requests-table/account-request-table.component.html
create mode 100644 src/web/app/components/account-requests-table/account-request-table.component.scss
create mode 100755 src/web/app/components/account-requests-table/account-request-table.component.ts
create mode 100644 src/web/app/components/account-requests-table/account-request-table.module.ts
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 @@
+
+
+
+
+
+
+ Name |
+ Email |
+ Status |
+ Institute, Country |
+ Created At |
+ Registered At |
+ Comments |
+ Options |
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{ accountRequest.email }} |
+ {{ accountRequest.status }} |
+ {{ accountRequest.instituteAndCountry }} |
+ {{ accountRequest.createdAtText }} |
+ {{ accountRequest.registeredAtText || 'Not Registered Yet' }} |
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
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;
}
/**