Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/web/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ const routes: Routes = [
canActivate: [RoleGuard],
canActivateChild: [RoleGuard],
},
{
path: 'role-selection',
component: PageComponent,
children: [
{
path: '',
loadComponent: () =>
import('./page-role-selection/role-selection-page.component').then((m) => m.RoleSelectionPageComponent),
},
],
canActivate: [RoleGuard],
canActivateChild: [RoleGuard],
},
{
path: 'login',
component: PageComponent,
Expand Down
2 changes: 1 addition & 1 deletion src/web/app/page-login/login-page.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class LoginPageComponent implements OnInit {
private readonly configService = inject(ConfigService);
private readonly statusMessageService = inject(StatusMessageService);

readonly nextUrl = input('/');
readonly nextUrl = input('/web/role-selection');

readonly isLoadingLoginMethods = signal(true);
readonly loginMethods = signal<ReadonlySet<LoginMethod>>(new Set());
Expand Down
31 changes: 31 additions & 0 deletions src/web/app/page-role-selection/role-selection-page.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div *tmIsLoading="isLoadingRoles()">
<div class="card bg-light mx-auto" style="max-width: 600px">
<div class="card-header bg-primary text-white">
<h1 class="mb-0 h3">Welcome to TEAMMATES!</h1>
</div>
<div class="card-body">
@if (hasRolePages()) {
<p>Choose a page to continue.</p>
<div style="margin-top: 20px">
Comment thread
TobyCyan marked this conversation as resolved.
Outdated
@for (rolePage of rolePages(); track rolePage.url) {
<div class="d-sm-flex align-items-center justify-content-between border-top py-3">
<div class="mb-2 mb-sm-0">
<strong>{{ rolePage.role }}</strong>
</div>
<a class="btn btn-success" [routerLink]="rolePage.url">Continue</a>
</div>
}
</div>
} @else {
<p>You do not currently have access to any instructor or student pages in TEAMMATES.</p>
<p>
If you believe you should have access, contact your instructor or
<a href="mailto:{{ supportEmail() }}">contact TEAMMATES support</a>.
</p>
<div class="text-center" style="margin-top: 20px">
<a class="btn btn-primary" routerLink="/web/front/">Return to main page</a>
</div>
}
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { of } from 'rxjs';
import { AuthService } from '../../services/auth.service';
import { AuthInfo } from '../../types/api-output';
import { RoleSelectionPageComponent } from './role-selection-page.component';

const authInfoFor = (roles: {
isStudent?: boolean;
isInstructor?: boolean;
isAdmin?: boolean;
isMaintainer?: boolean;
}): AuthInfo => ({
masquerade: false,
user: {
accountId: 'account-id',
accountEmail: 'user@example.com',
isStudent: !!roles.isStudent,
isInstructor: !!roles.isInstructor,
isAdmin: !!roles.isAdmin,
isMaintainer: !!roles.isMaintainer,
},
});

describe('RoleSelectionPageComponent', () => {
let fixture: ComponentFixture<RoleSelectionPageComponent>;
let authService: AuthService;

beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [provideRouter([])],
}).compileComponents();
fixture = TestBed.createComponent(RoleSelectionPageComponent);
authService = TestBed.inject(AuthService);
});

it('should show available role pages', () => {
vi.spyOn(authService, 'getAuthUser').mockReturnValue(of(authInfoFor({ isStudent: true, isInstructor: true })));

fixture.detectChanges();

const pageText = fixture.nativeElement.textContent;
expect(pageText).toContain('Student');
expect(pageText).toContain('Instructor');
expect(pageText).not.toContain('Return to main page');
});

it('should prompt users with no roles to return to the main page', () => {
vi.spyOn(authService, 'getAuthUser').mockReturnValue(of(authInfoFor({})));

fixture.detectChanges();

const pageText = fixture.nativeElement.textContent;
expect(pageText).toContain('You do not currently have access to any instructor or student pages in TEAMMATES.');
expect(pageText).toContain('Return to main page');
});
});
81 changes: 81 additions & 0 deletions src/web/app/page-role-selection/role-selection-page.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { map, finalize } from 'rxjs/operators';
import { AuthService } from '../../services/auth.service';
import { AuthInfo } from '../../types/api-output';
import { LoadingSpinnerDirective } from '../components/loading-spinner/loading-spinner.directive';
import { ConfigService } from '../../services/config.service';
import { toSignal } from '@angular/core/rxjs-interop';

interface RolePage {
role: string;
url: string;
}

@Component({
selector: 'tm-role-selection-page',
templateUrl: './role-selection-page.component.html',
imports: [RouterLink, LoadingSpinnerDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RoleSelectionPageComponent implements OnInit {
private readonly authService = inject(AuthService);
private readonly configService = inject(ConfigService);

readonly isLoadingRoles = signal(true);
readonly rolePages = signal<RolePage[]>([]);
readonly hasRolePages = computed(() => this.rolePages().length > 0);
readonly supportEmail = toSignal(this.configService.getConfig().pipe(map((config) => config.supportEmail)), {
initialValue: '',
});

ngOnInit(): void {
this.isLoadingRoles.set(true);
this.authService
.getAuthUser()
.pipe(
finalize(() => {
this.isLoadingRoles.set(false);
}),
)
.subscribe({
next: (authInfo: AuthInfo) => {
const user = authInfo.user;
const rolePages: RolePage[] = [];

if (user?.isStudent) {
rolePages.push({
role: 'Student',
url: '/web/student',
});
}

if (user?.isInstructor) {
rolePages.push({
role: 'Instructor',
url: '/web/instructor',
});
}

if (user?.isAdmin) {
rolePages.push({
role: 'Admin',
url: '/web/admin',
});
}

if (user?.isMaintainer) {
rolePages.push({
role: 'Maintainer',
url: '/web/maintainer',
});
}

this.rolePages.set(rolePages);
},
error: () => {
this.rolePages.set([]);
},
Comment thread
TobyCyan marked this conversation as resolved.
});
}
}
2 changes: 1 addition & 1 deletion src/web/app/page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
class="nav-link"
id="login-btn"
routerLink="/web/login"
[queryParams]="{ nextUrl: '/' }"
[queryParams]="{ nextUrl: '/web/role-selection' }"
(click)="toggleCollapse(); closeModal()"
>
Sign in
Expand Down
Loading