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:
parent
743a3ba935
commit
f237aa7ab7
77 changed files with 1077 additions and 501 deletions
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
|
||||
});
|
||||
113
UI/Web/src/app/registration/user-login/user-login.component.ts
Normal file
113
UI/Web/src/app/registration/user-login/user-login.component.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue