Library Recomendations (#1236)

* Updated cover regex for finding cover images in archives to ignore back_cover or back-cover

* Fixed an issue where Tags wouldn't save due to not pulling them from the DB.

* Refactored All series to it's own lazy loaded module

* Modularized Dashboard and library detail. Had to change main dashboard page to be libraries. Subject to change.

* Refactored login component into registration module

* Series Detail module created

* Refactored nav stuff into it's own module, not lazy loaded, but self contained.

* Refactored theme component into a dev only module so we don't incur load for temp testing modules

* Finished off modularization code. Only missing thing is to re-introduce some dashboard functionality for library view.

* Implemented a basic recommendation page for library detail
This commit is contained in:
Joseph Milazzo 2022-04-29 17:27:01 -05:00 committed by GitHub
parent 743a3ba935
commit f237aa7ab7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 1077 additions and 501 deletions

View file

@ -1,8 +1,8 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { ThemeService } from 'src/app/theme.service';
import { ThemeService } from 'src/app/_services/theme.service';
import { AccountService } from 'src/app/_services/account.service';
import { NavService } from 'src/app/_services/nav.service';

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { ThemeService } from 'src/app/theme.service';
import { ThemeService } from 'src/app/_services/theme.service';
import { AccountService } from 'src/app/_services/account.service';
@Component({

View file

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ConfirmEmailComponent } from './confirm-email/confirm-email.component';
import { RegistrationRoutingModule } from './registration.router.module';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { ReactiveFormsModule } from '@angular/forms';
import { SplashContainerComponent } from './splash-container/splash-container.component';
import { RegisterComponent } from './register/register.component';
@ -10,6 +10,7 @@ import { AddEmailToAccountMigrationModalComponent } from './add-email-to-account
import { ConfirmMigrationEmailComponent } from './confirm-migration-email/confirm-migration-email.component';
import { ResetPasswordComponent } from './reset-password/reset-password.component';
import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-reset-password.component';
import { UserLoginComponent } from './user-login/user-login.component';
@ -21,7 +22,8 @@ import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-
AddEmailToAccountMigrationModalComponent,
ConfirmMigrationEmailComponent,
ResetPasswordComponent,
ConfirmResetPasswordComponent
ConfirmResetPasswordComponent,
UserLoginComponent
],
imports: [
CommonModule,

View file

@ -5,8 +5,17 @@ import { ConfirmMigrationEmailComponent } from './confirm-migration-email/confir
import { ConfirmResetPasswordComponent } from './confirm-reset-password/confirm-reset-password.component';
import { RegisterComponent } from './register/register.component';
import { ResetPasswordComponent } from './reset-password/reset-password.component';
import { UserLoginComponent } from './user-login/user-login.component';
const routes: Routes = [
{
path: '',
component: UserLoginComponent
},
{
path: 'login',
component: UserLoginComponent
},
{
path: 'confirm-email',
component: ConfirmEmailComponent,

View file

@ -0,0 +1,28 @@
<app-splash-container>
<ng-container title><h2>Login</h2></ng-container>
<ng-container body>
<ng-container *ngIf="isLoaded">
<form [formGroup]="loginForm" (ngSubmit)="login()" novalidate class="needs-validation" *ngIf="!firstTimeFlow">
<div class="card-text">
<div class="mb-3">
<label for="username" class="form-label visually-hidden">Username</label>
<input class="form-control custom-input" formControlName="username" id="username" type="text" autofocus placeholder="Username">
</div>
<div class="mb-3">
<label for="password" class="form-label visually-hidden">Password</label>
<input class="form-control custom-input" formControlName="password" id="password" type="password" placeholder="Password">
</div>
<div class="mb-3">
<a routerLink="/registration/reset-password" style="color: white">Forgot Password?</a>
</div>
<div>
<button class="btn btn-secondary alt" type="submit">Submit</button>
</div>
</div>
</form>
</ng-container>
</ng-container>
</app-splash-container>

View file

@ -0,0 +1,21 @@
.btn {
width: 100%
}
div {
text-align: center;
}
a {
font-size: 0.8rem;
}
.custom-input {
background-color: #fff !important;
color: black;
}
.invalid-feedback {
display: inline-block;
color: #343c59;
}

View file

@ -0,0 +1,94 @@
import { of } from 'rxjs';
import { MemberService } from '../../_services/member.service';
import { UserLoginComponent } from './user-login.component';
xdescribe('UserLoginComponent', () => {
let accountServiceMock: any;
let routerMock: any;
let memberServiceMock: any;
let fixture: UserLoginComponent;
const http = jest.fn();
beforeEach(async () => {
accountServiceMock = {
login: jest.fn()
};
memberServiceMock = {
adminExists: jest.fn().mockReturnValue(of({
success: true,
message: false,
token: ''
}))
};
routerMock = {
navigateByUrl: jest.fn()
};
//fixture = new UserLoginComponent(accountServiceMock, routerMock, memberServiceMock);
//fixture.ngOnInit();
});
describe('Test: ngOnInit', () => {
xit('should redirect to /home if no admin user', done => {
const response = {
success: true,
message: false,
token: ''
}
const httpMock = {
get: jest.fn().mockReturnValue(of(response))
};
const serviceMock = new MemberService(httpMock as any);
serviceMock.adminExists().subscribe((data: any) => {
expect(httpMock.get).toBeDefined();
expect(httpMock.get).toHaveBeenCalled();
expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/home');
done();
});
});
xit('should initialize login form', () => {
const loginForm = {
username: '',
password: ''
};
expect(fixture.loginForm.value).toEqual(loginForm);
});
});
xdescribe('Test: Login Form', () => {
it('should invalidate the form', () => {
fixture.loginForm.controls.username.setValue('');
fixture.loginForm.controls.password.setValue('');
expect(fixture.loginForm.valid).toBeFalsy();
});
it('should validate the form', () => {
fixture.loginForm.controls.username.setValue('demo');
fixture.loginForm.controls.password.setValue('Pa$$word!');
expect(fixture.loginForm.valid).toBeTruthy();
});
});
xdescribe('Test: Form Invalid', () => {
it('should not call login', () => {
fixture.loginForm.controls.username.setValue('');
fixture.loginForm.controls.password.setValue('');
fixture.login();
expect(accountServiceMock.login).not.toHaveBeenCalled();
});
});
// describe('Test: Form valid', () => {
// it('should call login', () => {
// fixture.loginForm.controls.username.setValue('demo');
// fixture.loginForm.controls.password.setValue('Pa$$word!');
// const spyLoginUser = jest.spyOn(accountServiceMock, 'login').mockReturnValue();
// fixture.login();
// expect(accountServiceMock.login).not.toHaveBeenCalled();
// const spyRouterNavigate = jest.spyOn(routerMock, 'navigateByUrl').mockReturnValue();
// });
// });
});

View file

@ -0,0 +1,113 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators';
import { SettingsService } from '../../admin/settings.service';
import { AddEmailToAccountMigrationModalComponent } from '../add-email-to-account-migration-modal/add-email-to-account-migration-modal.component';
import { User } from '../../_models/user';
import { AccountService } from '../../_services/account.service';
import { MemberService } from '../../_services/member.service';
import { NavService } from '../../_services/nav.service';
@Component({
selector: 'app-user-login',
templateUrl: './user-login.component.html',
styleUrls: ['./user-login.component.scss']
})
export class UserLoginComponent implements OnInit {
model: any = {username: '', password: ''};
loginForm: FormGroup = new FormGroup({
username: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required])
});
memberNames: Array<string> = [];
isCollapsed: {[key: string]: boolean} = {};
/**
* If there are no admins on the server, this will enable the registration to kick in.
*/
firstTimeFlow: boolean = true;
/**
* Used for first time the page loads to ensure no flashing
*/
isLoaded: boolean = false;
constructor(private accountService: AccountService, private router: Router, private memberService: MemberService,
private toastr: ToastrService, private navService: NavService, private settingsService: SettingsService, private modalService: NgbModal) {
this.navService.showNavBar();
this.navService.hideSideNav();
}
ngOnInit(): void {
this.navService.showNavBar();
this.navService.hideSideNav();
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.navService.showSideNav();
this.router.navigateByUrl('/libraries');
}
});
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
this.firstTimeFlow = !adminExists;
if (this.firstTimeFlow) {
this.router.navigateByUrl('registration/register');
return;
}
this.setupAuthenticatedLoginFlow();
this.isLoaded = true;
});
}
setupAuthenticatedLoginFlow() {
if (this.memberNames.indexOf(' Login ') >= 0) { return; }
this.memberNames.push(' Login ');
this.memberNames.forEach(name => this.isCollapsed[name] = false);
const lastLogin = localStorage.getItem(this.accountService.lastLoginKey);
if (lastLogin != undefined && lastLogin != null && lastLogin != '') {
this.loginForm.get('username')?.setValue(lastLogin);
}
}
onAdminCreated(user: User | null) {
if (user != null) {
this.firstTimeFlow = false;
} else {
this.toastr.error('There was an issue creating the new user. Please refresh and try again.');
}
}
login() {
this.model = this.loginForm.getRawValue();
this.accountService.login(this.model).subscribe(() => {
this.loginForm.reset();
this.navService.showNavBar();
this.navService.showSideNav();
// Check if user came here from another url, else send to library route
const pageResume = localStorage.getItem('kavita--auth-intersection-url');
if (pageResume && pageResume !== '/login') {
localStorage.setItem('kavita--auth-intersection-url', '');
this.router.navigateByUrl(pageResume);
} else {
this.router.navigateByUrl('/libraries');
}
}, err => {
if (err.error === 'You are missing an email on your account. Please wait while we migrate your account.') {
const modalRef = this.modalService.open(AddEmailToAccountMigrationModalComponent, { scrollable: true, size: 'md' });
modalRef.componentInstance.username = this.model.username;
modalRef.closed.pipe(take(1)).subscribe(() => {
});
} else {
this.toastr.error(err.error);
}
});
}
}