Skip to content

Commit

Permalink
Merge pull request #110 from Devoxx4Kids-NPO/LITTIL-284-Admin-login
Browse files Browse the repository at this point in the history
LITTIL-284 Admin menu for users with admin role
  • Loading branch information
MarcelWildenburg authored Oct 9, 2024
2 parents 226e610 + 410aa7f commit af43d08
Show file tree
Hide file tree
Showing 39 changed files with 396 additions and 60 deletions.
11 changes: 10 additions & 1 deletion src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import { AuthenticatorResolver } from './services/authentication.resolver';
import { CompleteProfileGuardService } from './services/complete-profile-guard.service';

const routes: Routes = [
{
path: 'user',
loadChildren: () =>
import('./pages/user/user.module').then((m) => m.UserModule),
canActivate: [CompleteProfileGuardService],
resolve: {
auth: AuthenticatorResolver,
},
},
{
path: 'admin',
loadChildren: () =>
Expand Down Expand Up @@ -42,7 +51,7 @@ const routes: Routes = [
@NgModule({
imports: [RouterModule.forRoot(routes, {
scrollPositionRestoration: 'top',
enableTracing: false,
enableTracing: false, // debugging purpose
})],
exports: [RouterModule],
})
Expand Down
2 changes: 1 addition & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
>{{ route.menuText }}</littil-main-menu-button>
<littil-main-menu-dropdown-button
class="flex-1"
*ngIf="route.subRoutes"
*ngIf="!route.disabled && route.subRoutes"
[title]="route.menuText"
[path]="route.path"
[subRoutes]="route.subRoutes"
Expand Down
55 changes: 50 additions & 5 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { AuthService } from '@auth0/auth0-angular';
import { Spectator } from '@ngneat/spectator';
import { createRoutingFactory } from '@ngneat/spectator/jest';
import { MockComponent, MockProvider } from 'ng-mocks';
import { of } from 'rxjs';
import { Subject } from 'rxjs';
import { LittilConfig, LITTILCONFIG } from '../littilConfig';
import { AppComponent } from './app.component';
import { ContentContainerComponent } from './components/content-container/content-container.component';
import { MainMenuButtonComponent } from './components/main-menu-button/main-menu-button.component';
import {
MainMenuDropdownButtonComponent
} from './components/main-menu-dropdown-button/main-menu-dropdown-button.component';
import { MainMenuDropdownButtonComponent } from './components/main-menu-dropdown-button/main-menu-dropdown-button.component';
import { UserMenuComponent } from './components/user-menu/user-menu.component';
import { FeedbackFinToken } from './feedback/feedbackfin.token';
import { PermissionController } from './services/permission.controller';
import { NgcCookieConsentService } from 'ngx-cookieconsent';
import {MenuType} from "./pages/menu.routes";
import {DOCUMENT} from "@angular/common";

describe('AppComponent', () => {
let spectator: Spectator<AppComponent>;
Expand All @@ -22,6 +22,8 @@ describe('AppComponent', () => {
let authServiceLoginSpy: jest.SpyInstance;
let authServiceLogoutSpy: jest.SpyInstance;

const onPermissionChangeSubject = new Subject<void>();

const createComponent = createRoutingFactory({
component: AppComponent,
declarations: [
Expand All @@ -33,8 +35,10 @@ describe('AppComponent', () => {
providers: [
MockProvider(AuthService, {}),
MockProvider(PermissionController, {
onPermissionChange: of(),
onPermissionChange: onPermissionChangeSubject.asObservable(),
activeAccount: undefined,
loggedIn: false,
hasAdminRole: jest.fn().mockReturnValue(false),
}),
MockProvider(Document),
{
Expand Down Expand Up @@ -67,4 +71,45 @@ describe('AppComponent', () => {
it('should create the app', () => {
expect(spectator.component).toBeTruthy();
});

it('should initialize feedbackFin config and assign to window object on init', () => {
const feedbackFin = spectator.inject(FeedbackFinToken);
const littilConfig = spectator.inject(LITTILCONFIG);
const documentMock = spectator.inject(DOCUMENT);

spectator.component.ngOnInit();

expect(feedbackFin.config.url).toBe(`${littilConfig.apiHost}/api/v1/feedback`);
expect(feedbackFin.config.mode).toBe('form');
expect((documentMock.defaultView as any).feedbackfin).toBe(feedbackFin);
});

it('should subscribe to permission changes and update menu routes', () => {
spectator.component.ngOnInit();
onPermissionChangeSubject.next();
spectator.component.menuRoutes.forEach(route => {
console.log(route.menuText);
if (route.type === MenuType.User) {
expect(route.disabled).toBe(true);
}
if (route.type === MenuType.Admin) {
expect(route.disabled).toBe(true);
}
});
});

it('should toggle mobileMenuOpen when toggleMobileMenu is called', () => {
spectator.component.mobileMenuOpen = false;
spectator.component.toggleMobileMenu();
expect(spectator.component.mobileMenuOpen).toBe(true);

spectator.component.toggleMobileMenu();
expect(spectator.component.mobileMenuOpen).toBe(false);
});

it('should set mobileMenuOpen to false when hideMobileMenu is called', () => {
spectator.component.mobileMenuOpen = true;
spectator.component.hideMobileMenu();
expect(spectator.component.mobileMenuOpen).toBe(false);
});
});
21 changes: 14 additions & 7 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { DOCUMENT } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { LITTILCONFIG, LittilConfig } from '../littilConfig';
Expand Down Expand Up @@ -33,12 +32,8 @@ export class AppComponent implements OnInit {
(this.document.defaultView as any).feedbackfin = this.feedbackFin;

this.permissionController.onPermissionChange.subscribe(() => {
const adminPages: IMenuItem[] = menuRoutes.filter(
(route) => route.type === MenuType.Admin
);
adminPages.forEach((item: IMenuItem) => {
item.disabled = !this.permissionController.loggedIn;
});
updateMenuRoutes(menuRoutes, MenuType.User, !this.permissionController.loggedIn);
updateMenuRoutes(menuRoutes, MenuType.Admin, !this.permissionController.hasAdminRole());
});
}

Expand All @@ -50,3 +45,15 @@ export class AppComponent implements OnInit {
this.mobileMenuOpen = false;
}
}

function updateMenuRoutes(menuRoutes: IMenuItem[], menuType: MenuType, disabled: boolean) {
const pages : IMenuItem[] = menuRoutes.filter((route)=> route.type === menuType)
pages.forEach((item:IMenuItem)=> {
item.disabled = disabled;
if (item.subRoutes) {
updateMenuRoutes(item.subRoutes, menuType, disabled)
} else {
item.subRoutes = undefined
}
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ <h1>{{ title }}</h1>
</div>
<section class="flex flex-row gap-10 place-items-start">
<nav class="space-y-1 w-1/4" aria-label="Sidebar">
<a [routerLink]="'/admin/profile'" routerLinkActive="bg-gray-100 text-gray-900" class="text-gray-600 hover:bg-gray-50 hover:text-gray-900 group flex items-center rounded-md px-3 py-2 text-sm font-medium"
<a [routerLink]="'/user/profile'" routerLinkActive="bg-gray-100 text-gray-900" class="text-gray-600 hover:bg-gray-50 hover:text-gray-900 group flex items-center rounded-md px-3 py-2 text-sm font-medium"
aria-current="page">
<svg class="text-gray-400 group-hover:text-gray-500 -ml-1 mr-3 h-6 w-6 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
<span class="truncate">Profiel</span>
</a>
<a [routerLink]="'/admin/modules'"
<a [routerLink]="'/user/modules'"
*ngIf="isGuestTeacher"
routerLinkActive="bg-gray-100 text-gray-900"
class="text-gray-600 hover:bg-gray-50 hover:text-gray-900 group flex items-center rounded-md px-3 py-2 text-sm font-medium"
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/user-menu/user-menu.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</div>
<div class="py-1" role="none">
<!-- Active: "bg-gray-100 text-gray-900", Not Active: "text-gray-700" -->
<a class="text-gray-700 block px-4 py-2 text-sm no-underline" role="menuitem" tabindex="-1" id="menu-item-0" [routerLink]="'/admin/profile'">Profielinstellingen</a>
<a class="text-gray-700 block px-4 py-2 text-sm no-underline" role="menuitem" tabindex="-1" id="menu-item-0" [routerLink]="'/user/profile'">Profielinstellingen</a>
</div>
<div class="py-1" role="none">
<button data-test="logout-btn" class="text-gray-700 block w-full px-4 py-2 text-left text-sm" role="menuitem" tabindex="-1" id="menu-item-3" (click)="logOut()">Uitloggen</button>
Expand Down
46 changes: 34 additions & 12 deletions src/app/pages/admin/admin.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AdminComponent } from './admin.component';
import { routes } from './admin.routing';
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ContentContainerModule} from "../../components/content-container/content-container.module";
import {RouterModule, Routes} from "@angular/router";
import { FooterModule } from '../../components/footer/footer.module';
import {AdminComponent} from "./admin/admin.component";
import { TitleModule } from "../../components/title/title.module";
import {ContactBannerModule} from "../../components/contact-banner/contact-banner.module";
import {UsersComponent} from "./users/users.component";

const routes: Routes = [
{
path: '',
component: AdminComponent,
},
{
path: 'users',
component: UsersComponent,
},
];

@NgModule({
declarations: [AdminComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
providers: [],
exports: [AdminComponent],
entryComponents: [],
declarations: [
AdminComponent,
UsersComponent,
],
imports: [
CommonModule,
ContentContainerModule,
RouterModule.forChild(routes),
TitleModule,
ContactBannerModule,
FooterModule
],
exports: [ContactBannerModule]
})
export class AdminModule {}

export class AdminModule { }
11 changes: 11 additions & 0 deletions src/app/pages/admin/admin/admin.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<littil-title>Administrator</littil-title>

<littil-content-container>
<div class="grid grid-cols-12 gap-x-4 my-12">
<article class="col-span-12 md:col-span-8">
<h1>TODO !!!!!!</h1>
</article>

</div>
</littil-content-container>
<littil-footer></littil-footer>
29 changes: 29 additions & 0 deletions src/app/pages/admin/admin/admin.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContentContainerComponent } from '../../../components/content-container/content-container.component';
import { TitleComponent } from '../../../components/title/title.component';
import { AdminComponent } from './admin.component';
import {ContactBannerComponent} from "../../../components/contact-banner/contact-banner.component";

describe('AdminComponent', () => {
let component: AdminComponent;
let fixture: ComponentFixture<AdminComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AdminComponent,
TitleComponent,
ContentContainerComponent,
ContactBannerComponent
],
}).compileComponents();

fixture = TestBed.createComponent(AdminComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Component } from '@angular/core';
import {Component} from '@angular/core';

@Component({
selector: 'littil-admin',
templateUrl: './admin.component.html',
})
export class AdminComponent {}
export class AdminComponent {
}
24 changes: 24 additions & 0 deletions src/app/pages/admin/users/users.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<littil-title>Gebruikers</littil-title>
<littil-content-container>
<div>
<table>
<thead>
<tr>
<th>email</th>
<th>roles</th>
<th>Provider</th>
<th>providerId</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users$ | async">
<td>{{ user.emailAddress }}</td>
<td>{{ user.roles }}</td>
<td>{{ user.provider }}</td>
<td>{{ user.providerId }}</td>
</tr>
</tbody>
</table>
</div>
</littil-content-container>
<littil-footer></littil-footer>
62 changes: 62 additions & 0 deletions src/app/pages/admin/users/users.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ButtonComponent } from '../../../components/button/button.component';
import { ContentContainerComponent } from '../../../components/content-container/content-container.component';
import { TitleComponent } from '../../../components/title/title.component';
import { UsersComponent } from './users.component';
import {ContactBannerComponent} from "../../../components/contact-banner/contact-banner.component";
import {NO_ERRORS_SCHEMA} from "@angular/core";
import {MockProvider} from "ng-mocks";
import {of} from "rxjs";
import {LittilUserService} from "../../../services/littil-user/littil-user.service";
import {User} from "../../../api/generated";
import {By} from "@angular/platform-browser";


const littilUsers: User[] = [
{"id": "12345", "emailAddress": "[email protected]",
"provider": "AUTH0", "providerId" : "auth0|12345"}
]

function getUserServiceDef() {
return {
declarations: [
UsersComponent,
TitleComponent,
ButtonComponent,
ContentContainerComponent,
ContactBannerComponent
],
schemas: [NO_ERRORS_SCHEMA],
providers: [
MockProvider(LittilUserService, {
getAll: () => of(littilUsers),
}),
]
};
}

describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule(getUserServiceDef())
.compileComponents();

fixture = TestBed.createComponent(UsersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should display users when data is available', () => {
// userServiceMock.getAll.and.returnValue(of(users));
fixture.detectChanges();
const rows = fixture.debugElement.queryAll(By.css('tbody tr'));
expect(rows.length).toBe(1);
});

});
Loading

0 comments on commit af43d08

Please sign in to comment.