From 2f9e5f37c1a7237b45caf57c2606a07fd52955c9 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 26 Feb 2019 18:18:29 +0800 Subject: Create user dialog. --- Timeline/ClientApp/src/app/app.module.ts | 4 +++- .../src/app/user-dialog/user-dialog.component.css | 0 .../src/app/user-dialog/user-dialog.component.html | 3 +++ .../app/user-dialog/user-dialog.component.spec.ts | 25 ++++++++++++++++++++++ .../src/app/user-dialog/user-dialog.component.ts | 15 +++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css create mode 100644 Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html create mode 100644 Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts index 86511be8..1f5e71a6 100644 --- a/Timeline/ClientApp/src/app/app.module.ts +++ b/Timeline/ClientApp/src/app/app.module.ts @@ -13,13 +13,15 @@ import { AppComponent } from './app.component'; import { HomeComponent } from './home/home.component'; import { TodoListPageComponent } from './todo-list-page/todo-list-page.component'; import { TodoItemComponent } from './todo-item/todo-item.component'; +import { UserDialogComponent } from './user-dialog/user-dialog.component'; @NgModule({ declarations: [ AppComponent, HomeComponent, TodoListPageComponent, - TodoItemComponent + TodoItemComponent, + UserDialogComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css new file mode 100644 index 00000000..e69de29b diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html new file mode 100644 index 00000000..36fc9792 --- /dev/null +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html @@ -0,0 +1,3 @@ +

+ user-dialog works! +

diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts new file mode 100644 index 00000000..786fc0d4 --- /dev/null +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserDialogComponent } from './user-dialog.component'; + +describe('UserDialogComponent', () => { + let component: UserDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UserDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts new file mode 100644 index 00000000..0db40952 --- /dev/null +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-user-dialog', + templateUrl: './user-dialog.component.html', + styleUrls: ['./user-dialog.component.css'] +}) +export class UserDialogComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} -- cgit v1.2.3 From 0073d146e8d9dab4ad87c0c782dbbfb26378a3b2 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 4 Mar 2019 16:30:44 +0800 Subject: Develop some basic parts of auth. --- Timeline/ClientApp/.vscode/launch.json | 33 +++--- Timeline/ClientApp/package.json | 1 + Timeline/ClientApp/src/app/app.component.html | 2 +- Timeline/ClientApp/src/app/app.component.ts | 11 +- Timeline/ClientApp/src/app/app.module.ts | 21 +++- .../src/app/debounce-click.directive.spec.ts | 124 +++++++++++++++++++++ .../ClientApp/src/app/debounce-click.directive.ts | 39 +++++++ .../src/app/user-dialog/user-dialog.component.html | 6 +- .../app/user-dialog/user-dialog.component.spec.ts | 2 +- .../src/app/user-dialog/user-dialog.component.ts | 5 + .../src/app/user-dialog/user.service.spec.ts | 12 ++ .../ClientApp/src/app/user-dialog/user.service.ts | 119 ++++++++++++++++++++ .../src/app/user-login/user-login.component.css | 12 ++ .../src/app/user-login/user-login.component.html | 13 +++ .../app/user-login/user-login.component.spec.ts | 25 +++++ .../src/app/user-login/user-login.component.ts | 27 +++++ 16 files changed, 420 insertions(+), 32 deletions(-) create mode 100644 Timeline/ClientApp/src/app/debounce-click.directive.spec.ts create mode 100644 Timeline/ClientApp/src/app/debounce-click.directive.ts create mode 100644 Timeline/ClientApp/src/app/user-dialog/user.service.spec.ts create mode 100644 Timeline/ClientApp/src/app/user-dialog/user.service.ts create mode 100644 Timeline/ClientApp/src/app/user-login/user-login.component.css create mode 100644 Timeline/ClientApp/src/app/user-login/user-login.component.html create mode 100644 Timeline/ClientApp/src/app/user-login/user-login.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/user-login/user-login.component.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/.vscode/launch.json b/Timeline/ClientApp/.vscode/launch.json index 96a3c552..73e17a72 100644 --- a/Timeline/ClientApp/.vscode/launch.json +++ b/Timeline/ClientApp/.vscode/launch.json @@ -1,22 +1,15 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch app", - "url": "https://localhost:5001", - "webRoot": "${workspaceFolder}" - }, - { - "type": "chrome", - "request": "launch", - "name": "Launch test", - "url": "http://localhost:9876", - "webRoot": "${workspaceFolder}" - } - ] + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch app", + "url": "https://localhost:5001", + "webRoot": "${workspaceFolder}" + } + ] } diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json index 9d948004..f725a0f5 100644 --- a/Timeline/ClientApp/package.json +++ b/Timeline/ClientApp/package.json @@ -4,6 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve", + "start-dotnet": "dotnet run --project ..", "build": "ng build", "build:ssr": "ng run Timeline:server:dev", "test": "ng test", diff --git a/Timeline/ClientApp/src/app/app.component.html b/Timeline/ClientApp/src/app/app.component.html index 9d20bc91..a5df80ac 100644 --- a/Timeline/ClientApp/src/app/app.component.html +++ b/Timeline/ClientApp/src/app/app.component.html @@ -4,7 +4,7 @@ Timeline TodoList - diff --git a/Timeline/ClientApp/src/app/app.component.ts b/Timeline/ClientApp/src/app/app.component.ts index bba1f59d..58fe7cac 100644 --- a/Timeline/ClientApp/src/app/app.component.ts +++ b/Timeline/ClientApp/src/app/app.component.ts @@ -1,4 +1,6 @@ import { Component } from '@angular/core'; +import { MatDialog } from '@angular/material'; +import { UserDialogComponent } from './user-dialog/user-dialog.component'; @Component({ selector: 'app-root', @@ -6,7 +8,12 @@ import { Component } from '@angular/core'; styleUrls: ['./app.component.css'] }) export class AppComponent { - title = 'app'; - public isCollapse = false; + constructor(private dialog: MatDialog) { } + + openUserDialog() { + this.dialog.open(UserDialogComponent, { + width: '250px' + }); + } } diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts index 1f5e71a6..5add9395 100644 --- a/Timeline/ClientApp/src/app/app.module.ts +++ b/Timeline/ClientApp/src/app/app.module.ts @@ -1,12 +1,12 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatMenuModule, MatIconModule, MatButtonModule, MatToolbarModule, MatListModule, - MatProgressBarModule, MatCardModule + MatProgressBarModule, MatCardModule, MatDialogModule, MatInputModule, MatFormFieldModule } from '@angular/material'; import { AppComponent } from './app.component'; @@ -14,6 +14,14 @@ import { HomeComponent } from './home/home.component'; import { TodoListPageComponent } from './todo-list-page/todo-list-page.component'; import { TodoItemComponent } from './todo-item/todo-item.component'; import { UserDialogComponent } from './user-dialog/user-dialog.component'; +import { DebounceClickDirective } from './debounce-click.directive'; +import { UserLoginComponent } from './user-login/user-login.component'; + +const importedMatModules = [ + MatMenuModule, MatIconModule, MatButtonModule, MatToolbarModule, + MatListModule, MatProgressBarModule, MatCardModule, MatDialogModule, + MatInputModule, MatFormFieldModule +]; @NgModule({ declarations: [ @@ -21,19 +29,22 @@ import { UserDialogComponent } from './user-dialog/user-dialog.component'; HomeComponent, TodoListPageComponent, TodoItemComponent, - UserDialogComponent + UserDialogComponent, + DebounceClickDirective, + UserLoginComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), HttpClientModule, - FormsModule, + ReactiveFormsModule, BrowserAnimationsModule, - MatMenuModule, MatIconModule, MatButtonModule, MatToolbarModule, MatListModule, MatProgressBarModule, MatCardModule, + ...importedMatModules, RouterModule.forRoot([ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'todo', component: TodoListPageComponent } ]) ], + entryComponents: [UserDialogComponent], providers: [], bootstrap: [AppComponent] }) diff --git a/Timeline/ClientApp/src/app/debounce-click.directive.spec.ts b/Timeline/ClientApp/src/app/debounce-click.directive.spec.ts new file mode 100644 index 00000000..75710d0c --- /dev/null +++ b/Timeline/ClientApp/src/app/debounce-click.directive.spec.ts @@ -0,0 +1,124 @@ +import { Component, ViewChild } from '@angular/core'; +import { async, TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DebounceClickDirective } from './debounce-click.directive'; + +interface TestComponent { + clickHandler: () => void; +} + +@Component({ + selector: 'app-default-test', + template: '' +}) +class DefaultDebounceTimeTestComponent { + @ViewChild(DebounceClickDirective) + directive: DebounceClickDirective; + + clickHandler: () => void = () => { }; +} + +@Component({ + selector: 'app-default-test', + template: '' +}) +class CustomDebounceTimeTestComponent { + debounceTime: number; + + @ViewChild(DebounceClickDirective) + directive: DebounceClickDirective; + + clickHandler: () => void = () => { }; +} + + +describe('DebounceClickDirective', () => { + let counter: number; + + function initComponent(component: TestComponent) { + component.clickHandler = () => counter++; + } + + beforeEach(() => { + counter = 0; + }); + + describe('default debounce time', () => { + let component: DefaultDebounceTimeTestComponent; + let componentFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DebounceClickDirective, DefaultDebounceTimeTestComponent] + }).compileComponents(); + })); + + beforeEach(() => { + componentFixture = TestBed.createComponent(DefaultDebounceTimeTestComponent); + component = componentFixture.componentInstance; + initComponent(component); + }); + + it('should create an instance', () => { + componentFixture.detectChanges(); + expect(component.directive).toBeTruthy(); + }); + + it('should work well', fakeAsync(() => { + function click() { + (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); + } + componentFixture.detectChanges(); + expect(counter).toBe(0); + click(); + tick(300); + expect(counter).toBe(0); + click(); + tick(); + expect(counter).toBe(0); + tick(500); + expect(counter).toBe(1); + })); + }); + + + describe('custom debounce time', () => { + let component: CustomDebounceTimeTestComponent; + let componentFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DebounceClickDirective, CustomDebounceTimeTestComponent] + }).compileComponents(); + })); + + beforeEach(() => { + componentFixture = TestBed.createComponent(CustomDebounceTimeTestComponent); + component = componentFixture.componentInstance; + initComponent(component); + component.debounceTime = 600; + }); + + it('should create an instance', () => { + componentFixture.detectChanges(); + expect(component.directive).toBeTruthy(); + }); + + it('should work well', fakeAsync(() => { + function click() { + (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); + } + componentFixture.detectChanges(); + expect(counter).toBe(0); + click(); + tick(300); + expect(counter).toBe(0); + click(); + tick(); + expect(counter).toBe(0); + tick(600); + expect(counter).toBe(1); + })); + }); +}); diff --git a/Timeline/ClientApp/src/app/debounce-click.directive.ts b/Timeline/ClientApp/src/app/debounce-click.directive.ts new file mode 100644 index 00000000..feb0404e --- /dev/null +++ b/Timeline/ClientApp/src/app/debounce-click.directive.ts @@ -0,0 +1,39 @@ +import { Directive, Output, Input, EventEmitter, ElementRef, OnInit, OnDestroy } from '@angular/core'; +import { fromEvent, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Directive({ + selector: '[appDebounceClick]' +}) +export class DebounceClickDirective implements OnInit, OnDestroy { + + private subscription: Subscription; + + @Output('appDebounceClick') clickEvent = new EventEmitter(); + + // tslint:disable-next-line:no-input-rename + @Input('appDebounceClickTime') + set debounceTime(value: number) { + if (this.subscription) { + this.subscription.unsubscribe(); + } + this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( + debounceTime(value) + ).subscribe(o => this.clickEvent.emit(o)); + } + + constructor(private element: ElementRef) { + } + + ngOnInit() { + if (!this.subscription) { + this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( + debounceTime(500) + ).subscribe(o => this.clickEvent.emit(o)); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html index 36fc9792..2c5d1879 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html @@ -1,3 +1,3 @@ -

- user-dialog works! -

+
+ +
diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts index 786fc0d4..884a3710 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts @@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { UserDialogComponent } from './user-dialog.component'; -describe('UserDialogComponent', () => { +xdescribe('UserDialogComponent', () => { let component: UserDialogComponent; let fixture: ComponentFixture; diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts index 0db40952..1d9536c8 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts @@ -9,7 +9,12 @@ export class UserDialogComponent implements OnInit { constructor() { } + state: 'login' | 'success' = 'login'; + ngOnInit() { } + login() { + + } } diff --git a/Timeline/ClientApp/src/app/user-dialog/user.service.spec.ts b/Timeline/ClientApp/src/app/user-dialog/user.service.spec.ts new file mode 100644 index 00000000..b9221b90 --- /dev/null +++ b/Timeline/ClientApp/src/app/user-dialog/user.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +xdescribe('UserService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: UserService = TestBed.get(UserService); + expect(service).toBeTruthy(); + }); +}); diff --git a/Timeline/ClientApp/src/app/user-dialog/user.service.ts b/Timeline/ClientApp/src/app/user-dialog/user.service.ts new file mode 100644 index 00000000..1afebc91 --- /dev/null +++ b/Timeline/ClientApp/src/app/user-dialog/user.service.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Observable, of, throwError } from 'rxjs'; +import { map, catchError, retry } from 'rxjs/operators'; + +export interface UserCredentials { + username: string; + password: string; +} + +export interface UserInfo { + username: string; + roles: string[]; +} + +export interface CreateTokenResult { + token: string; + userInfo: UserInfo; +} + +export interface TokenValidationRequest { + token: string; +} + +export interface TokenValidationResult { + isValid: boolean; + userInfo?: UserInfo; +} + +export interface UserLoginState { + state: 'nologin' | 'invalid' | 'success'; + userInfo?: UserInfo; +} + +export class BadNetworkException extends Error { + constructor() { + super('Network is bad.'); + } +} + +export class AlreadyLoginException extends Error { + constructor() { + super('There is already a token saved. Please call validateUserLoginState first.'); + } +} + +export class BadCredentialsException extends Error { + constructor(username: string = null , password: string = null) { + super(`Username[${username}] or password[${password}] is wrong.`); + } +} + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + private token: string; + private userInfo: UserInfo; + + constructor(private httpClient: HttpClient) { } + + validateUserLoginState(): Observable { + if (this.token === undefined || this.token === null) { + return of({ state: 'nologin' }); + } + + return this.httpClient.post('/api/User/ValidateToken', { token: this.token }).pipe( + retry(3), + catchError(error => { + console.error('Failed to validate token.'); + return throwError(error); + }), + map(result => { + if (result.isValid) { + this.userInfo = result.userInfo; + return { + state: 'success', + userInfo: result.userInfo + }; + } else { + this.token = null; + this.userInfo = null; + return { + state: 'invalid' + }; + } + }) + ); + } + + tryLogin(username: string, password: string): Observable { + if (this.token) { + return throwError(new AlreadyLoginException()); + } + + return this.httpClient.post('/api/User/CreateToken', { + username, password + }).pipe( + catchError((error: HttpErrorResponse) => { + if (error.error instanceof ErrorEvent) { + console.error('An error occurred when login: ' + error.error.message); + return throwError(new BadNetworkException()); + } else if (error.status === 400) { + console.error('An error occurred when login: wrong credentials.'); + return throwError(new BadCredentialsException(username, password)); + } else { + console.error('An unknown error occurred when login: ' + error); + return throwError(error); + } + }), + map(result => { + this.token = result.token; + this.userInfo = result.userInfo; + return result.userInfo; + }) + ); + } +} diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.css b/Timeline/ClientApp/src/app/user-login/user-login.component.css new file mode 100644 index 00000000..4cdd865f --- /dev/null +++ b/Timeline/ClientApp/src/app/user-login/user-login.component.css @@ -0,0 +1,12 @@ +form { + display: flex; + flex-wrap: wrap; +} + +div.w-100 { + width: 100%; +} + +.login-button { + margin-left: auto; +} diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.html b/Timeline/ClientApp/src/app/user-login/user-login.component.html new file mode 100644 index 00000000..6fed6bb5 --- /dev/null +++ b/Timeline/ClientApp/src/app/user-login/user-login.component.html @@ -0,0 +1,13 @@ +
+ + Username + + +
+ + Password + + +
+ +
diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.spec.ts b/Timeline/ClientApp/src/app/user-login/user-login.component.spec.ts new file mode 100644 index 00000000..b606b7b4 --- /dev/null +++ b/Timeline/ClientApp/src/app/user-login/user-login.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserLoginComponent } from './user-login.component'; + +describe('UserLoginComponent', () => { + let component: UserLoginComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UserLoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserLoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user-login/user-login.component.ts new file mode 100644 index 00000000..072f04af --- /dev/null +++ b/Timeline/ClientApp/src/app/user-login/user-login.component.ts @@ -0,0 +1,27 @@ +import { Component, Output, OnInit, EventEmitter } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; + +export class LoginEvent { + username: string; + password: string; +} + +@Component({ + selector: 'app-user-login', + templateUrl: './user-login.component.html', + styleUrls: ['./user-login.component.css'] +}) +export class UserLoginComponent { + + @Output() + login = new EventEmitter(); + + form = new FormGroup({ + username: new FormControl(''), + password: new FormControl('') + }); + + onLoginButtonClick() { + this.login.emit(this.form.value); + } +} -- cgit v1.2.3 From b7ce3f7139798a734905b6df7530feb8c5d0c373 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 4 Mar 2019 20:41:17 +0800 Subject: ... --- Timeline/ClientApp/src/app/app.component.ts | 2 +- Timeline/ClientApp/src/app/app.module.ts | 4 +-- .../src/app/user-dialog/user-dialog.component.css | 5 ++++ .../src/app/user-dialog/user-dialog.component.html | 11 +++++++- .../src/app/user-dialog/user-dialog.component.ts | 32 +++++++++++++++++++--- .../ClientApp/src/app/user-dialog/user.service.ts | 2 +- 6 files changed, 47 insertions(+), 9 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/app.component.ts b/Timeline/ClientApp/src/app/app.component.ts index 58fe7cac..1798d8f4 100644 --- a/Timeline/ClientApp/src/app/app.component.ts +++ b/Timeline/ClientApp/src/app/app.component.ts @@ -13,7 +13,7 @@ export class AppComponent { openUserDialog() { this.dialog.open(UserDialogComponent, { - width: '250px' + width: '300px' }); } } diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts index 5add9395..133aa3e6 100644 --- a/Timeline/ClientApp/src/app/app.module.ts +++ b/Timeline/ClientApp/src/app/app.module.ts @@ -6,7 +6,7 @@ import { RouterModule } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatMenuModule, MatIconModule, MatButtonModule, MatToolbarModule, MatListModule, - MatProgressBarModule, MatCardModule, MatDialogModule, MatInputModule, MatFormFieldModule + MatProgressBarModule, MatCardModule, MatDialogModule, MatInputModule, MatFormFieldModule, MatProgressSpinnerModule } from '@angular/material'; import { AppComponent } from './app.component'; @@ -20,7 +20,7 @@ import { UserLoginComponent } from './user-login/user-login.component'; const importedMatModules = [ MatMenuModule, MatIconModule, MatButtonModule, MatToolbarModule, MatListModule, MatProgressBarModule, MatCardModule, MatDialogModule, - MatInputModule, MatFormFieldModule + MatInputModule, MatFormFieldModule, MatProgressSpinnerModule ]; @NgModule({ diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css index e69de29b..c21d71b1 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css @@ -0,0 +1,5 @@ +.progress-container { + display: flex; + justify-content: center; + align-content: center; +} diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html index 2c5d1879..5d3829c7 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html @@ -1,3 +1,12 @@
- +
+ +
+
+

{{ loginMessage }}

+ +
+
+

You have been login as {{ userInfo.username }}.

+
diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts index 1d9536c8..6fe5b6d9 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts @@ -1,4 +1,6 @@ import { Component, OnInit } from '@angular/core'; +import { UserService, UserInfo } from './user.service'; +import { LoginEvent } from '../user-login/user-login.component'; @Component({ selector: 'app-user-dialog', @@ -7,14 +9,36 @@ import { Component, OnInit } from '@angular/core'; }) export class UserDialogComponent implements OnInit { - constructor() { } + constructor(private userService: UserService) { } - state: 'login' | 'success' = 'login'; + state: 'loading' | 'login' | 'success' = 'loading'; + + loginMessage: string; + + userInfo: UserInfo; ngOnInit() { + this.userService.validateUserLoginState().subscribe(result => { + if (result.state === 'success') { + this.userInfo = result.userInfo; + this.state = 'success'; + } else { + if (result.state === 'invalid') { + this.loginMessage = 'Your login is no longer valid'; + } else { + this.loginMessage = 'You haven\'t logged in.'; + } + this.state = 'login'; + } + }); } - login() { - + login(event: LoginEvent) { + this.userService.tryLogin(event.username, event.password).subscribe(result => { + this.userInfo = result; + this.state = 'success'; + }, (error: Error) => { + this.loginMessage = error.message; + }); } } diff --git a/Timeline/ClientApp/src/app/user-dialog/user.service.ts b/Timeline/ClientApp/src/app/user-dialog/user.service.ts index 1afebc91..b0a6eb15 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user.service.ts +++ b/Timeline/ClientApp/src/app/user-dialog/user.service.ts @@ -46,7 +46,7 @@ export class AlreadyLoginException extends Error { export class BadCredentialsException extends Error { constructor(username: string = null , password: string = null) { - super(`Username[${username}] or password[${password}] is wrong.`); + super(`Username or password is wrong.`); } } -- cgit v1.2.3 From 49b12399715de59cd051352101bd997ff2052177 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 6 Mar 2019 17:36:57 +0800 Subject: Move login message into user login component. --- .../ClientApp/src/app/user-dialog/user-dialog.component.css | 1 + .../ClientApp/src/app/user-dialog/user-dialog.component.html | 6 ++---- .../ClientApp/src/app/user-dialog/user-dialog.component.ts | 10 +++++----- .../ClientApp/src/app/user-login/user-login.component.css | 12 ++++++++++++ .../ClientApp/src/app/user-login/user-login.component.html | 5 +++++ .../ClientApp/src/app/user-login/user-login.component.ts | 7 ++++++- 6 files changed, 31 insertions(+), 10 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css index c21d71b1..868749cf 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css @@ -3,3 +3,4 @@ justify-content: center; align-content: center; } + diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html index 5d3829c7..e790aec1 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html @@ -2,11 +2,9 @@
-
-

{{ loginMessage }}

- -
+

You have been login as {{ userInfo.username }}.

+

Your roles are {{ userInfo.roles.join(', ') }}.

diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts index 6fe5b6d9..368a1775 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { UserService, UserInfo } from './user.service'; -import { LoginEvent } from '../user-login/user-login.component'; +import { LoginEvent, LoginMessage } from '../user-login/user-login.component'; @Component({ selector: 'app-user-dialog', @@ -13,7 +13,7 @@ export class UserDialogComponent implements OnInit { state: 'loading' | 'login' | 'success' = 'loading'; - loginMessage: string; + loginMessage: LoginMessage; userInfo: UserInfo; @@ -24,9 +24,9 @@ export class UserDialogComponent implements OnInit { this.state = 'success'; } else { if (result.state === 'invalid') { - this.loginMessage = 'Your login is no longer valid'; - } else { - this.loginMessage = 'You haven\'t logged in.'; + this.loginMessage = 'invalidlogin'; + } else if (result.state === 'nologin') { + this.loginMessage = 'nologin'; } this.state = 'login'; } diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.css b/Timeline/ClientApp/src/app/user-login/user-login.component.css index 4cdd865f..8bf6b408 100644 --- a/Timeline/ClientApp/src/app/user-login/user-login.component.css +++ b/Timeline/ClientApp/src/app/user-login/user-login.component.css @@ -10,3 +10,15 @@ div.w-100 { .login-button { margin-left: auto; } + +.no-login-message { + color: blue; +} + +.invalid-login-message { + color: red; +} + +.error-message { + color: red; +} diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.html b/Timeline/ClientApp/src/app/user-login/user-login.component.html index 6fed6bb5..b1dd289d 100644 --- a/Timeline/ClientApp/src/app/user-login/user-login.component.html +++ b/Timeline/ClientApp/src/app/user-login/user-login.component.html @@ -1,4 +1,9 @@
+ + + +

{{ message }}

+
Username diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user-login/user-login.component.ts index 072f04af..da642cb8 100644 --- a/Timeline/ClientApp/src/app/user-login/user-login.component.ts +++ b/Timeline/ClientApp/src/app/user-login/user-login.component.ts @@ -1,6 +1,8 @@ -import { Component, Output, OnInit, EventEmitter } from '@angular/core'; +import { Component, Output, OnInit, EventEmitter, Input } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; +export type LoginMessage = 'nologin' | 'invalidlogin' | string; + export class LoginEvent { username: string; password: string; @@ -13,6 +15,9 @@ export class LoginEvent { }) export class UserLoginComponent { + @Input() + message: LoginMessage; + @Output() login = new EventEmitter(); -- cgit v1.2.3 From ece86a1ddd11e81d86e6dd0cf4fe8d677b56ab57 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 6 Mar 2019 17:47:25 +0800 Subject: ... --- Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css | 3 +-- Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html | 6 ++---- Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts | 6 +----- Timeline/ClientApp/src/app/user-dialog/user.service.ts | 8 ++++---- 4 files changed, 8 insertions(+), 15 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css index 868749cf..a443e3c0 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css @@ -1,6 +1,5 @@ -.progress-container { +.container { display: flex; justify-content: center; align-content: center; } - diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html index e790aec1..1e1f1b79 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html @@ -1,7 +1,5 @@ -
-
- -
+
+

You have been login as {{ userInfo.username }}.

diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts index 368a1775..88e48799 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts @@ -23,11 +23,7 @@ export class UserDialogComponent implements OnInit { this.userInfo = result.userInfo; this.state = 'success'; } else { - if (result.state === 'invalid') { - this.loginMessage = 'invalidlogin'; - } else if (result.state === 'nologin') { - this.loginMessage = 'nologin'; - } + this.loginMessage = result.state; this.state = 'login'; } }); diff --git a/Timeline/ClientApp/src/app/user-dialog/user.service.ts b/Timeline/ClientApp/src/app/user-dialog/user.service.ts index b0a6eb15..47e98d4d 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user.service.ts +++ b/Timeline/ClientApp/src/app/user-dialog/user.service.ts @@ -28,7 +28,7 @@ export interface TokenValidationResult { } export interface UserLoginState { - state: 'nologin' | 'invalid' | 'success'; + state: 'nologin' | 'invalidlogin' | 'success'; userInfo?: UserInfo; } @@ -45,7 +45,7 @@ export class AlreadyLoginException extends Error { } export class BadCredentialsException extends Error { - constructor(username: string = null , password: string = null) { + constructor() { super(`Username or password is wrong.`); } } @@ -82,7 +82,7 @@ export class UserService { this.token = null; this.userInfo = null; return { - state: 'invalid' + state: 'invalidlogin' }; } }) @@ -103,7 +103,7 @@ export class UserService { return throwError(new BadNetworkException()); } else if (error.status === 400) { console.error('An error occurred when login: wrong credentials.'); - return throwError(new BadCredentialsException(username, password)); + return throwError(new BadCredentialsException()); } else { console.error('An unknown error occurred when login: ' + error); return throwError(error); -- cgit v1.2.3 From 8ec742c4a00c1732d6aa58a1cb8b628428d23c4a Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 6 Mar 2019 21:12:59 +0800 Subject: Extract out user-login-success component. --- Timeline/ClientApp/src/app/app.module.ts | 4 +++- .../src/app/user-dialog/user-dialog.component.html | 5 +---- .../src/app/user-dialog/user-dialog.component.ts | 2 ++ .../user-login-success.component.css | 7 ++++++ .../user-login-success.component.html | 5 +++++ .../user-login-success.component.spec.ts | 25 ++++++++++++++++++++++ .../user-login-success.component.ts | 22 +++++++++++++++++++ 7 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 Timeline/ClientApp/src/app/user-login-success/user-login-success.component.css create mode 100644 Timeline/ClientApp/src/app/user-login-success/user-login-success.component.html create mode 100644 Timeline/ClientApp/src/app/user-login-success/user-login-success.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/user-login-success/user-login-success.component.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts index 133aa3e6..c2852a11 100644 --- a/Timeline/ClientApp/src/app/app.module.ts +++ b/Timeline/ClientApp/src/app/app.module.ts @@ -16,6 +16,7 @@ import { TodoItemComponent } from './todo-item/todo-item.component'; import { UserDialogComponent } from './user-dialog/user-dialog.component'; import { DebounceClickDirective } from './debounce-click.directive'; import { UserLoginComponent } from './user-login/user-login.component'; +import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component'; const importedMatModules = [ MatMenuModule, MatIconModule, MatButtonModule, MatToolbarModule, @@ -31,7 +32,8 @@ const importedMatModules = [ TodoItemComponent, UserDialogComponent, DebounceClickDirective, - UserLoginComponent + UserLoginComponent, + UserLoginSuccessComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html index 1e1f1b79..50d6ba56 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html @@ -1,8 +1,5 @@
-
-

You have been login as {{ userInfo.username }}.

-

Your roles are {{ userInfo.roles.join(', ') }}.

-
+
diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts index 88e48799..cf62b831 100644 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts +++ b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts @@ -15,6 +15,7 @@ export class UserDialogComponent implements OnInit { loginMessage: LoginMessage; + displayLoginSuccessMessage = false; userInfo: UserInfo; ngOnInit() { @@ -32,6 +33,7 @@ export class UserDialogComponent implements OnInit { login(event: LoginEvent) { this.userService.tryLogin(event.username, event.password).subscribe(result => { this.userInfo = result; + this.displayLoginSuccessMessage = true; this.state = 'success'; }, (error: Error) => { this.loginMessage = error.message; diff --git a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.css b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.css new file mode 100644 index 00000000..6486142b --- /dev/null +++ b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.css @@ -0,0 +1,7 @@ +.login-success-message { + color: green; +} + +.username { + color: blue; +} diff --git a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.html b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.html new file mode 100644 index 00000000..943c137f --- /dev/null +++ b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.html @@ -0,0 +1,5 @@ + +

You have been login as {{ userInfo.username }}.

+

Your roles are {{ userInfo.roles.join(', ') }}.

diff --git a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.spec.ts b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.spec.ts new file mode 100644 index 00000000..bdcd354b --- /dev/null +++ b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserLoginSuccessComponent } from './user-login-success.component'; + +describe('UserLoginSuccessComponent', () => { + let component: UserLoginSuccessComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UserLoginSuccessComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserLoginSuccessComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.ts b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.ts new file mode 100644 index 00000000..e0fe6cc1 --- /dev/null +++ b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { UserInfo } from '../user-dialog/user.service'; + +@Component({ + selector: 'app-user-login-success', + templateUrl: './user-login-success.component.html', + styleUrls: ['./user-login-success.component.css'] +}) +export class UserLoginSuccessComponent implements OnInit { + + @Input() + displayLoginSuccessMessage = false; + + @Input() + userInfo: UserInfo; + + constructor() { } + + ngOnInit() { + } + +} -- cgit v1.2.3 From 63735a8267d44892a64da5b599b7c2e20f373464 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 6 Mar 2019 21:29:36 +0800 Subject: Reorganize file structure. --- Timeline/ClientApp/src/app/app.component.ts | 2 +- Timeline/ClientApp/src/app/app.module.ts | 13 ++- .../src/app/todo-item/todo-item.component.css | 25 ----- .../src/app/todo-item/todo-item.component.html | 9 -- .../src/app/todo-item/todo-item.component.spec.ts | 46 -------- .../src/app/todo-item/todo-item.component.ts | 13 --- .../app/todo-list-page/todo-list-color-block.css | 7 -- .../todo-list-page/todo-list-page.component.css | 39 ------- .../todo-list-page/todo-list-page.component.html | 21 ---- .../todo-list-page.component.spec.ts | 76 ------------- .../app/todo-list-page/todo-list-page.component.ts | 36 ------- .../app/todo-list-page/todo-list.service.spec.ts | 55 ---------- .../src/app/todo-list-page/todo-list.service.ts | 48 --------- Timeline/ClientApp/src/app/todo/todo-item.ts | 6 ++ .../src/app/todo/todo-item/todo-item.component.css | 25 +++++ .../app/todo/todo-item/todo-item.component.html | 9 ++ .../app/todo/todo-item/todo-item.component.spec.ts | 46 ++++++++ .../src/app/todo/todo-item/todo-item.component.ts | 13 +++ .../src/app/todo/todo-list-color-block.css | 7 ++ .../todo-list-page/todo-list-page.component.css | 39 +++++++ .../todo-list-page/todo-list-page.component.html | 21 ++++ .../todo-list-page.component.spec.ts | 78 ++++++++++++++ .../todo-list-page/todo-list-page.component.ts | 37 +++++++ .../todo/todo-service/todo-list.service.spec.ts | 54 ++++++++++ .../src/app/todo/todo-service/todo-list.service.ts | 43 ++++++++ .../src/app/user-dialog/user-dialog.component.css | 5 - .../src/app/user-dialog/user-dialog.component.html | 5 - .../app/user-dialog/user-dialog.component.spec.ts | 25 ----- .../src/app/user-dialog/user-dialog.component.ts | 42 -------- .../src/app/user-dialog/user.service.spec.ts | 12 --- .../ClientApp/src/app/user-dialog/user.service.ts | 119 --------------------- .../user-login-success.component.css | 7 -- .../user-login-success.component.html | 5 - .../user-login-success.component.spec.ts | 25 ----- .../user-login-success.component.ts | 22 ---- .../src/app/user-login/user-login.component.css | 24 ----- .../src/app/user-login/user-login.component.html | 18 ---- .../app/user-login/user-login.component.spec.ts | 25 ----- .../src/app/user-login/user-login.component.ts | 32 ------ .../app/user/user-dialog/user-dialog.component.css | 5 + .../user/user-dialog/user-dialog.component.html | 5 + .../user/user-dialog/user-dialog.component.spec.ts | 25 +++++ .../app/user/user-dialog/user-dialog.component.ts | 43 ++++++++ Timeline/ClientApp/src/app/user/user-info.ts | 4 + .../user-login-success.component.css | 7 ++ .../user-login-success.component.html | 5 + .../user-login-success.component.spec.ts | 25 +++++ .../user-login-success.component.ts | 22 ++++ .../app/user/user-login/user-login.component.css | 24 +++++ .../app/user/user-login/user-login.component.html | 18 ++++ .../user/user-login/user-login.component.spec.ts | 25 +++++ .../app/user/user-login/user-login.component.ts | 32 ++++++ .../src/app/user/user-service/user.service.spec.ts | 12 +++ .../src/app/user/user-service/user.service.ts | 116 ++++++++++++++++++++ 54 files changed, 755 insertions(+), 747 deletions(-) delete mode 100644 Timeline/ClientApp/src/app/todo-item/todo-item.component.css delete mode 100644 Timeline/ClientApp/src/app/todo-item/todo-item.component.html delete mode 100644 Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts delete mode 100644 Timeline/ClientApp/src/app/todo-item/todo-item.component.ts delete mode 100644 Timeline/ClientApp/src/app/todo-list-page/todo-list-color-block.css delete mode 100644 Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css delete mode 100644 Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.html delete mode 100644 Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts delete mode 100644 Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.ts delete mode 100644 Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts delete mode 100644 Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-item.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css create mode 100644 Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html create mode 100644 Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-list-color-block.css create mode 100644 Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css create mode 100644 Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html create mode 100644 Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts delete mode 100644 Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css delete mode 100644 Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html delete mode 100644 Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts delete mode 100644 Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts delete mode 100644 Timeline/ClientApp/src/app/user-dialog/user.service.spec.ts delete mode 100644 Timeline/ClientApp/src/app/user-dialog/user.service.ts delete mode 100644 Timeline/ClientApp/src/app/user-login-success/user-login-success.component.css delete mode 100644 Timeline/ClientApp/src/app/user-login-success/user-login-success.component.html delete mode 100644 Timeline/ClientApp/src/app/user-login-success/user-login-success.component.spec.ts delete mode 100644 Timeline/ClientApp/src/app/user-login-success/user-login-success.component.ts delete mode 100644 Timeline/ClientApp/src/app/user-login/user-login.component.css delete mode 100644 Timeline/ClientApp/src/app/user-login/user-login.component.html delete mode 100644 Timeline/ClientApp/src/app/user-login/user-login.component.spec.ts delete mode 100644 Timeline/ClientApp/src/app/user-login/user-login.component.ts create mode 100644 Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css create mode 100644 Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html create mode 100644 Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts create mode 100644 Timeline/ClientApp/src/app/user/user-info.ts create mode 100644 Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css create mode 100644 Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html create mode 100644 Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts create mode 100644 Timeline/ClientApp/src/app/user/user-login/user-login.component.css create mode 100644 Timeline/ClientApp/src/app/user/user-login/user-login.component.html create mode 100644 Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/user/user-login/user-login.component.ts create mode 100644 Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts create mode 100644 Timeline/ClientApp/src/app/user/user-service/user.service.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/app.component.ts b/Timeline/ClientApp/src/app/app.component.ts index 1798d8f4..0e2a9799 100644 --- a/Timeline/ClientApp/src/app/app.component.ts +++ b/Timeline/ClientApp/src/app/app.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { MatDialog } from '@angular/material'; -import { UserDialogComponent } from './user-dialog/user-dialog.component'; +import { UserDialogComponent } from './user/user-dialog/user-dialog.component'; @Component({ selector: 'app-root', diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts index c2852a11..d0b6a5c6 100644 --- a/Timeline/ClientApp/src/app/app.module.ts +++ b/Timeline/ClientApp/src/app/app.module.ts @@ -11,12 +11,15 @@ import { import { AppComponent } from './app.component'; import { HomeComponent } from './home/home.component'; -import { TodoListPageComponent } from './todo-list-page/todo-list-page.component'; -import { TodoItemComponent } from './todo-item/todo-item.component'; -import { UserDialogComponent } from './user-dialog/user-dialog.component'; + import { DebounceClickDirective } from './debounce-click.directive'; -import { UserLoginComponent } from './user-login/user-login.component'; -import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component'; + +import { TodoListPageComponent } from './todo/todo-list-page/todo-list-page.component'; +import { TodoItemComponent } from './todo/todo-item/todo-item.component'; + +import { UserDialogComponent } from './user/user-dialog/user-dialog.component'; +import { UserLoginComponent } from './user/user-login/user-login.component'; +import { UserLoginSuccessComponent } from './user/user-login-success/user-login-success.component'; const importedMatModules = [ MatMenuModule, MatIconModule, MatButtonModule, MatToolbarModule, diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.css b/Timeline/ClientApp/src/app/todo-item/todo-item.component.css deleted file mode 100644 index dcf25fd8..00000000 --- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.css +++ /dev/null @@ -1,25 +0,0 @@ -.item-card { - padding: 0; - display: flex; - overflow: hidden; -} - -.item-body-box { - margin: 5px!important -} - -.item-color-block { - width: 15px; - align-self: stretch; - flex: 0 0 auto; -} - -.item-title { - vertical-align: middle; -} - -.item-detail-button { - width: unset; - height: unset; - line-height: unset; -} diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.html b/Timeline/ClientApp/src/app/todo-item/todo-item.component.html deleted file mode 100644 index 6f76e73b..00000000 --- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- {{ item.number }}. {{ item.title }} - - arrow_forward - -
-
diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts b/Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts deleted file mode 100644 index 520b6136..00000000 --- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { TodoItemComponent } from './todo-item.component'; -import { TodoItem } from '../todo-list-page/todo-list.service'; -import { By } from '@angular/platform-browser'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; - -describe('TodoItemComponent', () => { - let component: TodoItemComponent; - let fixture: ComponentFixture; - - const mockTodoItem: TodoItem = { - number: 1, - title: 'Title', - isClosed: true, - detailUrl: '/detail', - }; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [TodoItemComponent], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(TodoItemComponent); - component = fixture.componentInstance; - component.item = mockTodoItem; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should set title', () => { - expect((fixture.debugElement.query(By.css('span.item-title')).nativeElement as HTMLSpanElement).textContent).toBe( - mockTodoItem.number + '. ' + mockTodoItem.title - ); - }); - - it('should set detail link', () => { - expect(fixture.debugElement.query(By.css('a.item-detail-button')).properties['href']).toBe(mockTodoItem.detailUrl); - }); -}); diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.ts b/Timeline/ClientApp/src/app/todo-item/todo-item.component.ts deleted file mode 100644 index 325812f1..00000000 --- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { TodoItem } from '../todo-list-page/todo-list.service'; - -@Component({ - selector: 'app-todo-item', - templateUrl: './todo-item.component.html', - styleUrls: ['./todo-item.component.css', '../todo-list-page/todo-list-color-block.css'] -}) -export class TodoItemComponent { - - @Input() item: TodoItem; - -} diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-color-block.css b/Timeline/ClientApp/src/app/todo-list-page/todo-list-color-block.css deleted file mode 100644 index 5e0d4ba9..00000000 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-color-block.css +++ /dev/null @@ -1,7 +0,0 @@ -.color-block-open { - background: red; -} - -.color-block-closed { - background: green; -} diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css b/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css deleted file mode 100644 index 754b786e..00000000 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css +++ /dev/null @@ -1,39 +0,0 @@ -.align-self-bottom { - align-self: flex-end; -} - -.item-box { - display: flex; - width: 100%; - box-sizing: border-box; -} - -.first-item-box { - justify-content: space-between; - padding: 0 0 5px 5px; -} - -.non-first-item-box { - padding: 5px; -} - -.space { - flex: 1 4 20px; -} - -.sample-box { - box-sizing: border-box; - align-self: flex-start; -} - -.sample-item { - display: flex; - align-items: center; -} - -.sample-color-block { - border-radius: 0.2em; - width: 1em; - height: 1em; - margin-right: 2px; -} diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.html b/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.html deleted file mode 100644 index 50180fe8..00000000 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - -
- -
-
-
- - means working now. -
-
- - means completed. -
-
click on item to see details.
-
-
-
-
diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts b/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts deleted file mode 100644 index 5706bf51..00000000 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; - -import { Observable, from } from 'rxjs'; - -import { TodoListPageComponent } from './todo-list-page.component'; -import { TodoListService, TodoItem } from './todo-list.service'; -import { By } from '@angular/platform-browser'; -import { delay } from 'rxjs/operators'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -@Component({ - /* tslint:disable-next-line:component-selector*/ - selector: 'mat-progress-bar', - template: '' -}) -class MatProgressBarStubComponent {} - -function asyncArray(data: T[]): Observable { - return from(data).pipe(delay(0)); -} - -describe('TodoListPageComponent', () => { - let component: TodoListPageComponent; - let fixture: ComponentFixture; - - const mockTodoItems: TodoItem[] = [ - { - number: 0, - title: 'Test title 1', - isClosed: true, - detailUrl: 'test_url1' - }, - { - number: 1, - title: 'Test title 2', - isClosed: false, - detailUrl: 'test_url2' - } - ]; - - beforeEach(async(() => { - const todoListService: jasmine.SpyObj = jasmine.createSpyObj('TodoListService', ['getWorkItemList']); - - todoListService.getWorkItemList.and.returnValue(asyncArray(mockTodoItems)); - - TestBed.configureTestingModule({ - declarations: [TodoListPageComponent, MatProgressBarStubComponent], - imports: [NoopAnimationsModule], - providers: [{ provide: TodoListService, useValue: todoListService }], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(TodoListPageComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should show progress bar during loading', () => { - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('mat-progress-bar'))).toBeTruthy(); - }); - - it('should hide progress bar after loading', fakeAsync(() => { - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('mat-progress-bar'))).toBeFalsy(); - })); -}); diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.ts b/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.ts deleted file mode 100644 index c62dd808..00000000 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { TodoListService, TodoItem } from './todo-list.service'; -import { trigger, transition, style, animate } from '@angular/animations'; - -@Component({ - selector: 'app-todo-list-page', - templateUrl: './todo-list-page.component.html', - styleUrls: ['./todo-list-page.component.css', './todo-list-color-block.css'], - animations: [ - trigger('itemEnter', [ - transition(':enter', [ - style({ - transform: 'translateX(-100%) translateX(-20px)' - }), - animate('400ms ease-out', style({ - transform: 'none' - })) - ]) - ]) - ] -}) -export class TodoListPageComponent implements OnInit { - - items: TodoItem[] = []; - isLoadCompleted = false; - - constructor(private todoService: TodoListService) { - } - - ngOnInit() { - this.todoService.getWorkItemList().subscribe({ - next: result => this.items.push(result), - complete: () => { this.isLoadCompleted = true; } - }); - } -} diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts b/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts deleted file mode 100644 index a2ad0cbd..00000000 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; - -import { - TodoListService, IssueResponse, IssueResponseItem, TodoItem -} from './todo-list.service'; -import { toArray } from 'rxjs/operators'; - - -describe('TodoListServiceService', () => { - beforeEach(() => TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] - })); - - it('should be created', () => { - const service: TodoListService = TestBed.get(TodoListService); - expect(service).toBeTruthy(); - }); - - it('should work well', () => { - const service: TodoListService = TestBed.get(TodoListService); - - const baseUrl = service.baseUrl; - - const mockIssueList: IssueResponse = [{ - number: 1, - title: 'Issue title 1', - state: 'open', - html_url: 'test_url1' - }, { - number: 2, - title: 'Issue title 2', - state: 'closed', - html_url: 'test_url2', - pull_request: {} - }]; - - const mockTodoItemList: TodoItem[] = [{ - number: 1, - title: 'Issue title 1', - isClosed: false, - detailUrl: 'test_url1' - }]; - - service.getWorkItemList().pipe(toArray()).subscribe(data => { - expect(data).toEqual(mockTodoItemList); - }); - - const httpController: HttpTestingController = TestBed.get(HttpTestingController); - - httpController.expectOne(request => request.url === baseUrl + '/issues' && request.params.get('state') === 'all').flush(mockIssueList); - - httpController.verify(); - }); -}); diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts b/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts deleted file mode 100644 index ffcbbc6f..00000000 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, from } from 'rxjs'; -import { switchMap, map, filter } from 'rxjs/operators'; - -export interface IssueResponseItem { - number: number; - title: string; - state: string; - html_url: string; - pull_request?: any; -} - -export type IssueResponse = IssueResponseItem[]; - -export interface TodoItem { - number: number; - title: string; - isClosed: boolean; - detailUrl: string; -} - -@Injectable({ - providedIn: 'root' -}) -export class TodoListService { - - readonly baseUrl = 'https://api.github.com/repos/crupest/Timeline'; - - constructor(private client: HttpClient) { } - - getWorkItemList(): Observable { - return this.client.get(`${this.baseUrl}/issues`, { - params: { - state: 'all' - } - }).pipe( - switchMap(result => from(result)), - filter(result => result.pull_request === undefined), // filter out pull requests. - map(result => { - number: result.number, - title: result.title, - isClosed: result.state === 'closed', - detailUrl: result.html_url - }) - ); - } -} diff --git a/Timeline/ClientApp/src/app/todo/todo-item.ts b/Timeline/ClientApp/src/app/todo/todo-item.ts new file mode 100644 index 00000000..b19d8335 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-item.ts @@ -0,0 +1,6 @@ +export interface TodoItem { + number: number; + title: string; + isClosed: boolean; + detailUrl: string; +} diff --git a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css new file mode 100644 index 00000000..dcf25fd8 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css @@ -0,0 +1,25 @@ +.item-card { + padding: 0; + display: flex; + overflow: hidden; +} + +.item-body-box { + margin: 5px!important +} + +.item-color-block { + width: 15px; + align-self: stretch; + flex: 0 0 auto; +} + +.item-title { + vertical-align: middle; +} + +.item-detail-button { + width: unset; + height: unset; + line-height: unset; +} diff --git a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html new file mode 100644 index 00000000..6f76e73b --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html @@ -0,0 +1,9 @@ + + +
+ {{ item.number }}. {{ item.title }} + + arrow_forward + +
+
diff --git a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts new file mode 100644 index 00000000..239ffc42 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts @@ -0,0 +1,46 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { TodoItem } from '../todo-item'; +import { TodoItemComponent } from '../todo-item/todo-item.component'; + +describe('TodoItemComponent', () => { + let component: TodoItemComponent; + let fixture: ComponentFixture; + + const mockTodoItem: TodoItem = { + number: 1, + title: 'Title', + isClosed: true, + detailUrl: '/detail', + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TodoItemComponent], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TodoItemComponent); + component = fixture.componentInstance; + component.item = mockTodoItem; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set title', () => { + expect((fixture.debugElement.query(By.css('span.item-title')).nativeElement as HTMLSpanElement).textContent).toBe( + mockTodoItem.number + '. ' + mockTodoItem.title + ); + }); + + it('should set detail link', () => { + expect(fixture.debugElement.query(By.css('a.item-detail-button')).properties['href']).toBe(mockTodoItem.detailUrl); + }); +}); diff --git a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts new file mode 100644 index 00000000..2ea6997a --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from '@angular/core'; +import { TodoItem } from '../todo-item'; + +@Component({ + selector: 'app-todo-item', + templateUrl: './todo-item.component.html', + styleUrls: ['./todo-item.component.css', '../todo-list-color-block.css'] +}) +export class TodoItemComponent { + + @Input() item: TodoItem; + +} diff --git a/Timeline/ClientApp/src/app/todo/todo-list-color-block.css b/Timeline/ClientApp/src/app/todo/todo-list-color-block.css new file mode 100644 index 00000000..5e0d4ba9 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-list-color-block.css @@ -0,0 +1,7 @@ +.color-block-open { + background: red; +} + +.color-block-closed { + background: green; +} diff --git a/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css new file mode 100644 index 00000000..754b786e --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css @@ -0,0 +1,39 @@ +.align-self-bottom { + align-self: flex-end; +} + +.item-box { + display: flex; + width: 100%; + box-sizing: border-box; +} + +.first-item-box { + justify-content: space-between; + padding: 0 0 5px 5px; +} + +.non-first-item-box { + padding: 5px; +} + +.space { + flex: 1 4 20px; +} + +.sample-box { + box-sizing: border-box; + align-self: flex-start; +} + +.sample-item { + display: flex; + align-items: center; +} + +.sample-color-block { + border-radius: 0.2em; + width: 1em; + height: 1em; + margin-right: 2px; +} diff --git a/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html new file mode 100644 index 00000000..50180fe8 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html @@ -0,0 +1,21 @@ + + + + +
+ +
+
+
+ + means working now. +
+
+ + means completed. +
+
click on item to see details.
+
+
+
+
diff --git a/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts new file mode 100644 index 00000000..0af113dc --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts @@ -0,0 +1,78 @@ +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { Observable, from } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { TodoItem } from '../todo-item'; +import { TodoListPageComponent } from './todo-list-page.component'; +import { TodoListService } from '../todo-service/todo-list.service'; + + +@Component({ + /* tslint:disable-next-line:component-selector*/ + selector: 'mat-progress-bar', + template: '' +}) +class MatProgressBarStubComponent { } + +function asyncArray(data: T[]): Observable { + return from(data).pipe(delay(0)); +} + +describe('TodoListPageComponent', () => { + let component: TodoListPageComponent; + let fixture: ComponentFixture; + + const mockTodoItems: TodoItem[] = [ + { + number: 0, + title: 'Test title 1', + isClosed: true, + detailUrl: 'test_url1' + }, + { + number: 1, + title: 'Test title 2', + isClosed: false, + detailUrl: 'test_url2' + } + ]; + + beforeEach(async(() => { + const todoListService: jasmine.SpyObj = jasmine.createSpyObj('TodoListService', ['getWorkItemList']); + + todoListService.getWorkItemList.and.returnValue(asyncArray(mockTodoItems)); + + TestBed.configureTestingModule({ + declarations: [TodoListPageComponent, MatProgressBarStubComponent], + imports: [NoopAnimationsModule], + providers: [{ provide: TodoListService, useValue: todoListService }], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TodoListPageComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should show progress bar during loading', () => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('mat-progress-bar'))).toBeTruthy(); + }); + + it('should hide progress bar after loading', fakeAsync(() => { + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('mat-progress-bar'))).toBeFalsy(); + })); +}); diff --git a/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts new file mode 100644 index 00000000..a69c6856 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit } from '@angular/core'; +import { TodoItem } from '../todo-item'; +import { TodoListService } from '../todo-service/todo-list.service'; +import { trigger, transition, style, animate } from '@angular/animations'; + +@Component({ + selector: 'app-todo-list-page', + templateUrl: './todo-list-page.component.html', + styleUrls: ['./todo-list-page.component.css', '../todo-list-color-block.css'], + animations: [ + trigger('itemEnter', [ + transition(':enter', [ + style({ + transform: 'translateX(-100%) translateX(-20px)' + }), + animate('400ms ease-out', style({ + transform: 'none' + })) + ]) + ]) + ] +}) +export class TodoListPageComponent implements OnInit { + + items: TodoItem[] = []; + isLoadCompleted = false; + + constructor(private todoService: TodoListService) { + } + + ngOnInit() { + this.todoService.getWorkItemList().subscribe({ + next: result => this.items.push(result), + complete: () => { this.isLoadCompleted = true; } + }); + } +} diff --git a/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts new file mode 100644 index 00000000..d8283b54 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts @@ -0,0 +1,54 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { toArray } from 'rxjs/operators'; + +import { TodoItem } from '../todo-item'; +import { TodoListService, IssueResponse } from './todo-list.service'; + + +describe('TodoListServiceService', () => { + beforeEach(() => TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + })); + + it('should be created', () => { + const service: TodoListService = TestBed.get(TodoListService); + expect(service).toBeTruthy(); + }); + + it('should work well', () => { + const service: TodoListService = TestBed.get(TodoListService); + + const baseUrl = service.baseUrl; + + const mockIssueList: IssueResponse = [{ + number: 1, + title: 'Issue title 1', + state: 'open', + html_url: 'test_url1' + }, { + number: 2, + title: 'Issue title 2', + state: 'closed', + html_url: 'test_url2', + pull_request: {} + }]; + + const mockTodoItemList: TodoItem[] = [{ + number: 1, + title: 'Issue title 1', + isClosed: false, + detailUrl: 'test_url1' + }]; + + service.getWorkItemList().pipe(toArray()).subscribe(data => { + expect(data).toEqual(mockTodoItemList); + }); + + const httpController: HttpTestingController = TestBed.get(HttpTestingController); + + httpController.expectOne(request => request.url === baseUrl + '/issues' && request.params.get('state') === 'all').flush(mockIssueList); + + httpController.verify(); + }); +}); diff --git a/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts new file mode 100644 index 00000000..83bf47ec --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, from } from 'rxjs'; +import { switchMap, map, filter } from 'rxjs/operators'; + +import {TodoItem} from '../todo-item'; + +export interface IssueResponseItem { + number: number; + title: string; + state: string; + html_url: string; + pull_request?: any; +} + +export type IssueResponse = IssueResponseItem[]; + +@Injectable({ + providedIn: 'root' +}) +export class TodoListService { + + readonly baseUrl = 'https://api.github.com/repos/crupest/Timeline'; + + constructor(private client: HttpClient) { } + + getWorkItemList(): Observable { + return this.client.get(`${this.baseUrl}/issues`, { + params: { + state: 'all' + } + }).pipe( + switchMap(result => from(result)), + filter(result => result.pull_request === undefined), // filter out pull requests. + map(result => { + number: result.number, + title: result.title, + isClosed: result.state === 'closed', + detailUrl: result.html_url + }) + ); + } +} diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css deleted file mode 100644 index a443e3c0..00000000 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.css +++ /dev/null @@ -1,5 +0,0 @@ -.container { - display: flex; - justify-content: center; - align-content: center; -} diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html deleted file mode 100644 index 50d6ba56..00000000 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.html +++ /dev/null @@ -1,5 +0,0 @@ -
- - - -
diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts deleted file mode 100644 index 884a3710..00000000 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { UserDialogComponent } from './user-dialog.component'; - -xdescribe('UserDialogComponent', () => { - let component: UserDialogComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ UserDialogComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(UserDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts deleted file mode 100644 index cf62b831..00000000 --- a/Timeline/ClientApp/src/app/user-dialog/user-dialog.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { UserService, UserInfo } from './user.service'; -import { LoginEvent, LoginMessage } from '../user-login/user-login.component'; - -@Component({ - selector: 'app-user-dialog', - templateUrl: './user-dialog.component.html', - styleUrls: ['./user-dialog.component.css'] -}) -export class UserDialogComponent implements OnInit { - - constructor(private userService: UserService) { } - - state: 'loading' | 'login' | 'success' = 'loading'; - - loginMessage: LoginMessage; - - displayLoginSuccessMessage = false; - userInfo: UserInfo; - - ngOnInit() { - this.userService.validateUserLoginState().subscribe(result => { - if (result.state === 'success') { - this.userInfo = result.userInfo; - this.state = 'success'; - } else { - this.loginMessage = result.state; - this.state = 'login'; - } - }); - } - - login(event: LoginEvent) { - this.userService.tryLogin(event.username, event.password).subscribe(result => { - this.userInfo = result; - this.displayLoginSuccessMessage = true; - this.state = 'success'; - }, (error: Error) => { - this.loginMessage = error.message; - }); - } -} diff --git a/Timeline/ClientApp/src/app/user-dialog/user.service.spec.ts b/Timeline/ClientApp/src/app/user-dialog/user.service.spec.ts deleted file mode 100644 index b9221b90..00000000 --- a/Timeline/ClientApp/src/app/user-dialog/user.service.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { UserService } from './user.service'; - -xdescribe('UserService', () => { - beforeEach(() => TestBed.configureTestingModule({})); - - it('should be created', () => { - const service: UserService = TestBed.get(UserService); - expect(service).toBeTruthy(); - }); -}); diff --git a/Timeline/ClientApp/src/app/user-dialog/user.service.ts b/Timeline/ClientApp/src/app/user-dialog/user.service.ts deleted file mode 100644 index 47e98d4d..00000000 --- a/Timeline/ClientApp/src/app/user-dialog/user.service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Observable, of, throwError } from 'rxjs'; -import { map, catchError, retry } from 'rxjs/operators'; - -export interface UserCredentials { - username: string; - password: string; -} - -export interface UserInfo { - username: string; - roles: string[]; -} - -export interface CreateTokenResult { - token: string; - userInfo: UserInfo; -} - -export interface TokenValidationRequest { - token: string; -} - -export interface TokenValidationResult { - isValid: boolean; - userInfo?: UserInfo; -} - -export interface UserLoginState { - state: 'nologin' | 'invalidlogin' | 'success'; - userInfo?: UserInfo; -} - -export class BadNetworkException extends Error { - constructor() { - super('Network is bad.'); - } -} - -export class AlreadyLoginException extends Error { - constructor() { - super('There is already a token saved. Please call validateUserLoginState first.'); - } -} - -export class BadCredentialsException extends Error { - constructor() { - super(`Username or password is wrong.`); - } -} - -@Injectable({ - providedIn: 'root' -}) -export class UserService { - - private token: string; - private userInfo: UserInfo; - - constructor(private httpClient: HttpClient) { } - - validateUserLoginState(): Observable { - if (this.token === undefined || this.token === null) { - return of({ state: 'nologin' }); - } - - return this.httpClient.post('/api/User/ValidateToken', { token: this.token }).pipe( - retry(3), - catchError(error => { - console.error('Failed to validate token.'); - return throwError(error); - }), - map(result => { - if (result.isValid) { - this.userInfo = result.userInfo; - return { - state: 'success', - userInfo: result.userInfo - }; - } else { - this.token = null; - this.userInfo = null; - return { - state: 'invalidlogin' - }; - } - }) - ); - } - - tryLogin(username: string, password: string): Observable { - if (this.token) { - return throwError(new AlreadyLoginException()); - } - - return this.httpClient.post('/api/User/CreateToken', { - username, password - }).pipe( - catchError((error: HttpErrorResponse) => { - if (error.error instanceof ErrorEvent) { - console.error('An error occurred when login: ' + error.error.message); - return throwError(new BadNetworkException()); - } else if (error.status === 400) { - console.error('An error occurred when login: wrong credentials.'); - return throwError(new BadCredentialsException()); - } else { - console.error('An unknown error occurred when login: ' + error); - return throwError(error); - } - }), - map(result => { - this.token = result.token; - this.userInfo = result.userInfo; - return result.userInfo; - }) - ); - } -} diff --git a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.css b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.css deleted file mode 100644 index 6486142b..00000000 --- a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.css +++ /dev/null @@ -1,7 +0,0 @@ -.login-success-message { - color: green; -} - -.username { - color: blue; -} diff --git a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.html b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.html deleted file mode 100644 index 943c137f..00000000 --- a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.html +++ /dev/null @@ -1,5 +0,0 @@ - -

You have been login as {{ userInfo.username }}.

-

Your roles are {{ userInfo.roles.join(', ') }}.

diff --git a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.spec.ts b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.spec.ts deleted file mode 100644 index bdcd354b..00000000 --- a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { UserLoginSuccessComponent } from './user-login-success.component'; - -describe('UserLoginSuccessComponent', () => { - let component: UserLoginSuccessComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ UserLoginSuccessComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(UserLoginSuccessComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.ts b/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.ts deleted file mode 100644 index e0fe6cc1..00000000 --- a/Timeline/ClientApp/src/app/user-login-success/user-login-success.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { UserInfo } from '../user-dialog/user.service'; - -@Component({ - selector: 'app-user-login-success', - templateUrl: './user-login-success.component.html', - styleUrls: ['./user-login-success.component.css'] -}) -export class UserLoginSuccessComponent implements OnInit { - - @Input() - displayLoginSuccessMessage = false; - - @Input() - userInfo: UserInfo; - - constructor() { } - - ngOnInit() { - } - -} diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.css b/Timeline/ClientApp/src/app/user-login/user-login.component.css deleted file mode 100644 index 8bf6b408..00000000 --- a/Timeline/ClientApp/src/app/user-login/user-login.component.css +++ /dev/null @@ -1,24 +0,0 @@ -form { - display: flex; - flex-wrap: wrap; -} - -div.w-100 { - width: 100%; -} - -.login-button { - margin-left: auto; -} - -.no-login-message { - color: blue; -} - -.invalid-login-message { - color: red; -} - -.error-message { - color: red; -} diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.html b/Timeline/ClientApp/src/app/user-login/user-login.component.html deleted file mode 100644 index b1dd289d..00000000 --- a/Timeline/ClientApp/src/app/user-login/user-login.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - -

{{ message }}

-
- - Username - - -
- - Password - - -
- - diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.spec.ts b/Timeline/ClientApp/src/app/user-login/user-login.component.spec.ts deleted file mode 100644 index b606b7b4..00000000 --- a/Timeline/ClientApp/src/app/user-login/user-login.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { UserLoginComponent } from './user-login.component'; - -describe('UserLoginComponent', () => { - let component: UserLoginComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ UserLoginComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(UserLoginComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/Timeline/ClientApp/src/app/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user-login/user-login.component.ts deleted file mode 100644 index da642cb8..00000000 --- a/Timeline/ClientApp/src/app/user-login/user-login.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Component, Output, OnInit, EventEmitter, Input } from '@angular/core'; -import { FormGroup, FormControl } from '@angular/forms'; - -export type LoginMessage = 'nologin' | 'invalidlogin' | string; - -export class LoginEvent { - username: string; - password: string; -} - -@Component({ - selector: 'app-user-login', - templateUrl: './user-login.component.html', - styleUrls: ['./user-login.component.css'] -}) -export class UserLoginComponent { - - @Input() - message: LoginMessage; - - @Output() - login = new EventEmitter(); - - form = new FormGroup({ - username: new FormControl(''), - password: new FormControl('') - }); - - onLoginButtonClick() { - this.login.emit(this.form.value); - } -} diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css new file mode 100644 index 00000000..a443e3c0 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css @@ -0,0 +1,5 @@ +.container { + display: flex; + justify-content: center; + align-content: center; +} diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html new file mode 100644 index 00000000..50d6ba56 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts new file mode 100644 index 00000000..884a3710 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserDialogComponent } from './user-dialog.component'; + +xdescribe('UserDialogComponent', () => { + let component: UserDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UserDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts new file mode 100644 index 00000000..7511de16 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit } from '@angular/core'; +import { UserInfo } from '../user-info'; +import { UserService } from '../user-service/user.service'; +import { LoginEvent, LoginMessage } from '../user-login/user-login.component'; + +@Component({ + selector: 'app-user-dialog', + templateUrl: './user-dialog.component.html', + styleUrls: ['./user-dialog.component.css'] +}) +export class UserDialogComponent implements OnInit { + + constructor(private userService: UserService) { } + + state: 'loading' | 'login' | 'success' = 'loading'; + + loginMessage: LoginMessage; + + displayLoginSuccessMessage = false; + userInfo: UserInfo; + + ngOnInit() { + this.userService.validateUserLoginState().subscribe(result => { + if (result.state === 'success') { + this.userInfo = result.userInfo; + this.state = 'success'; + } else { + this.loginMessage = result.state; + this.state = 'login'; + } + }); + } + + login(event: LoginEvent) { + this.userService.tryLogin(event.username, event.password).subscribe(result => { + this.userInfo = result; + this.displayLoginSuccessMessage = true; + this.state = 'success'; + }, (error: Error) => { + this.loginMessage = error.message; + }); + } +} diff --git a/Timeline/ClientApp/src/app/user/user-info.ts b/Timeline/ClientApp/src/app/user/user-info.ts new file mode 100644 index 00000000..490b00ba --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-info.ts @@ -0,0 +1,4 @@ +export interface UserInfo { + username: string; + roles: string[]; +} diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css new file mode 100644 index 00000000..6486142b --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css @@ -0,0 +1,7 @@ +.login-success-message { + color: green; +} + +.username { + color: blue; +} diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html new file mode 100644 index 00000000..943c137f --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html @@ -0,0 +1,5 @@ + +

You have been login as {{ userInfo.username }}.

+

Your roles are {{ userInfo.roles.join(', ') }}.

diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts new file mode 100644 index 00000000..bdcd354b --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserLoginSuccessComponent } from './user-login-success.component'; + +describe('UserLoginSuccessComponent', () => { + let component: UserLoginSuccessComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UserLoginSuccessComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserLoginSuccessComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts new file mode 100644 index 00000000..99de5970 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { UserInfo } from '../user-info'; + +@Component({ + selector: 'app-user-login-success', + templateUrl: './user-login-success.component.html', + styleUrls: ['./user-login-success.component.css'] +}) +export class UserLoginSuccessComponent implements OnInit { + + @Input() + displayLoginSuccessMessage = false; + + @Input() + userInfo: UserInfo; + + constructor() { } + + ngOnInit() { + } + +} diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.css b/Timeline/ClientApp/src/app/user/user-login/user-login.component.css new file mode 100644 index 00000000..8bf6b408 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.css @@ -0,0 +1,24 @@ +form { + display: flex; + flex-wrap: wrap; +} + +div.w-100 { + width: 100%; +} + +.login-button { + margin-left: auto; +} + +.no-login-message { + color: blue; +} + +.invalid-login-message { + color: red; +} + +.error-message { + color: red; +} diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.html b/Timeline/ClientApp/src/app/user/user-login/user-login.component.html new file mode 100644 index 00000000..b1dd289d --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.html @@ -0,0 +1,18 @@ +
+ + + +

{{ message }}

+
+ + Username + + +
+ + Password + + +
+ +
diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts new file mode 100644 index 00000000..b606b7b4 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserLoginComponent } from './user-login.component'; + +describe('UserLoginComponent', () => { + let component: UserLoginComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UserLoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserLoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts new file mode 100644 index 00000000..da642cb8 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts @@ -0,0 +1,32 @@ +import { Component, Output, OnInit, EventEmitter, Input } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; + +export type LoginMessage = 'nologin' | 'invalidlogin' | string; + +export class LoginEvent { + username: string; + password: string; +} + +@Component({ + selector: 'app-user-login', + templateUrl: './user-login.component.html', + styleUrls: ['./user-login.component.css'] +}) +export class UserLoginComponent { + + @Input() + message: LoginMessage; + + @Output() + login = new EventEmitter(); + + form = new FormGroup({ + username: new FormControl(''), + password: new FormControl('') + }); + + onLoginButtonClick() { + this.login.emit(this.form.value); + } +} diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts new file mode 100644 index 00000000..b9221b90 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +xdescribe('UserService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: UserService = TestBed.get(UserService); + expect(service).toBeTruthy(); + }); +}); diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.ts new file mode 100644 index 00000000..009e5292 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user-service/user.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Observable, of, throwError } from 'rxjs'; +import { map, catchError, retry } from 'rxjs/operators'; + +import { UserInfo } from '../user-info'; + +export interface UserCredentials { + username: string; + password: string; +} + +export interface CreateTokenResult { + token: string; + userInfo: UserInfo; +} + +export interface TokenValidationRequest { + token: string; +} + +export interface TokenValidationResult { + isValid: boolean; + userInfo?: UserInfo; +} + +export interface UserLoginState { + state: 'nologin' | 'invalidlogin' | 'success'; + userInfo?: UserInfo; +} + +export class BadNetworkException extends Error { + constructor() { + super('Network is bad.'); + } +} + +export class AlreadyLoginException extends Error { + constructor() { + super('There is already a token saved. Please call validateUserLoginState first.'); + } +} + +export class BadCredentialsException extends Error { + constructor() { + super(`Username or password is wrong.`); + } +} + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + private token: string; + private userInfo: UserInfo; + + constructor(private httpClient: HttpClient) { } + + validateUserLoginState(): Observable { + if (this.token === undefined || this.token === null) { + return of({ state: 'nologin' }); + } + + return this.httpClient.post('/api/User/ValidateToken', { token: this.token }).pipe( + retry(3), + catchError(error => { + console.error('Failed to validate token.'); + return throwError(error); + }), + map(result => { + if (result.isValid) { + this.userInfo = result.userInfo; + return { + state: 'success', + userInfo: result.userInfo + }; + } else { + this.token = null; + this.userInfo = null; + return { + state: 'invalidlogin' + }; + } + }) + ); + } + + tryLogin(username: string, password: string): Observable { + if (this.token) { + return throwError(new AlreadyLoginException()); + } + + return this.httpClient.post('/api/User/CreateToken', { + username, password + }).pipe( + catchError((error: HttpErrorResponse) => { + if (error.error instanceof ErrorEvent) { + console.error('An error occurred when login: ' + error.error.message); + return throwError(new BadNetworkException()); + } else if (error.status === 400) { + console.error('An error occurred when login: wrong credentials.'); + return throwError(new BadCredentialsException()); + } else { + console.error('An unknown error occurred when login: ' + error); + return throwError(error); + } + }), + map(result => { + this.token = result.token; + this.userInfo = result.userInfo; + return result.userInfo; + }) + ); + } +} -- cgit v1.2.3 From 2ee5c455152a0553453e400b387109b0b518ec99 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 6 Mar 2019 23:14:45 +0800 Subject: Write all unit tests. --- .../user/user-dialog/user-dialog.component.spec.ts | 101 +++++++++++++++++-- .../user-login-success.component.html | 2 +- .../user-login-success.component.spec.ts | 18 +++- .../user/user-login/user-login.component.spec.ts | 43 +++++++- .../src/app/user/user-service/user.service.spec.ts | 108 ++++++++++++++++++++- 5 files changed, 256 insertions(+), 16 deletions(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts index 884a3710..d24c0cd2 100644 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts @@ -1,25 +1,114 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, Output, EventEmitter } from '@angular/core'; +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { of } from 'rxjs'; +import { delay } from 'rxjs/operators'; +import { UserInfo } from '../user-info'; import { UserDialogComponent } from './user-dialog.component'; +import { UserService, UserLoginState } from '../user-service/user.service'; +import { LoginEvent } from '../user-login/user-login.component'; -xdescribe('UserDialogComponent', () => { +@Component({ + /* tslint:disable-next-line:component-selector*/ + selector: 'mat-progress-spinner', + template: '' +}) +class MatProgressSpinnerStubComponent { } + +@Component({ + selector: 'app-user-login', + /* tslint:disable-next-line:use-input-property-decorator*/ + inputs: ['message'], + template: '' +}) +class UserLoginStubComponent { + @Output() + login = new EventEmitter(); +} + +@Component({ + selector: 'app-user-login-success', + /* tslint:disable-next-line:use-input-property-decorator*/ + inputs: ['userInfo', 'displayLoginSuccessMessage'], + template: '' +}) +class UserLoginSuccessStubComponent { } + +describe('UserDialogComponent', () => { let component: UserDialogComponent; let fixture: ComponentFixture; + let mockUserService: jasmine.SpyObj; beforeEach(async(() => { + mockUserService = jasmine.createSpyObj('UserService', ['validateUserLoginState', 'tryLogin']); + TestBed.configureTestingModule({ - declarations: [ UserDialogComponent ] + declarations: [UserDialogComponent, MatProgressSpinnerStubComponent, + UserLoginStubComponent, UserLoginSuccessStubComponent], + providers: [{ provide: UserService, useValue: mockUserService }] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(UserDialogComponent); component = fixture.componentInstance; + }); + + it('progress spinner should work well', fakeAsync(() => { + mockUserService.validateUserLoginState.and.returnValue(of({ state: 'nologin' }).pipe(delay(10))); + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('mat-progress-spinner'))).toBeTruthy(); + tick(10); fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('mat-progress-spinner'))).toBeFalsy(); + })); + + it('nologin should work well', () => { + mockUserService.validateUserLoginState.and.returnValue(of({ state: 'nologin' })); + + fixture.detectChanges(); + + expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(fixture.debugElement.query(By.css('app-user-login'))).toBeTruthy(); + expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeFalsy(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('success should work well', () => { + mockUserService.validateUserLoginState.and.returnValue(of({ state: 'success', userInfo: {} })); + + fixture.detectChanges(); + + expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(fixture.debugElement.query(By.css('app-user-login'))).toBeFalsy(); + expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeTruthy(); + }); + + it('login should work well', () => { + mockUserService.validateUserLoginState.and.returnValue(of({ state: 'nologin' })); + + fixture.detectChanges(); + expect(mockUserService.validateUserLoginState).toHaveBeenCalled(); + expect(fixture.debugElement.query(By.css('app-user-login'))).toBeTruthy(); + expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeFalsy(); + + mockUserService.tryLogin.withArgs('user', 'user').and.returnValue(of({ + username: 'user', + roles: ['user'] + })); + + (fixture.debugElement.query(By.css('app-user-login')).componentInstance as + UserLoginStubComponent).login.emit({ + username: 'user', + password: 'user' + }); + + fixture.detectChanges(); + + expect(mockUserService.tryLogin).toHaveBeenCalledWith('user', 'user'); + + expect(fixture.debugElement.query(By.css('app-user-login'))).toBeFalsy(); + expect(fixture.debugElement.query(By.css('app-user-login-success'))).toBeTruthy(); }); }); diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html index 943c137f..e156f0f8 100644 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html @@ -2,4 +2,4 @@ Login succeeds!

You have been login as {{ userInfo.username }}.

-

Your roles are {{ userInfo.roles.join(', ') }}.

+

Your roles are {{ userInfo.roles.join(', ') }}.

diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts index bdcd354b..ba015ae6 100644 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts @@ -1,25 +1,39 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { UserLoginSuccessComponent } from './user-login-success.component'; +import { By } from '@angular/platform-browser'; describe('UserLoginSuccessComponent', () => { let component: UserLoginSuccessComponent; let fixture: ComponentFixture; + const mockUserInfo = { + username: 'crupest', + roles: ['superman', 'coder'] + }; + beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ UserLoginSuccessComponent ] + declarations: [UserLoginSuccessComponent] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(UserLoginSuccessComponent); component = fixture.componentInstance; + component.userInfo = mockUserInfo; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should work well', () => { + expect((fixture.debugElement.query(By.css('span.username')).nativeElement as HTMLSpanElement).textContent) + .toBe(mockUserInfo.username); + expect((fixture.debugElement.query(By.css('span.roles')).nativeElement as HTMLSpanElement).textContent) + .toBe(mockUserInfo.roles.join(', ')); + }); }); diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts index b606b7b4..acd13721 100644 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts @@ -1,6 +1,9 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; -import { UserLoginComponent } from './user-login.component'; +import { UserLoginComponent, LoginEvent } from './user-login.component'; describe('UserLoginComponent', () => { let component: UserLoginComponent; @@ -8,9 +11,11 @@ describe('UserLoginComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ UserLoginComponent ] + declarations: [UserLoginComponent], + imports: [ReactiveFormsModule], + schemas: [NO_ERRORS_SCHEMA] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { @@ -22,4 +27,34 @@ describe('UserLoginComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('reactive form should work well', () => { + const usernameInput = fixture.debugElement.query(By.css('input[type=text]')).nativeElement as HTMLInputElement; + const passwordInput = fixture.debugElement.query(By.css('input[type=password]')).nativeElement as HTMLInputElement; + + usernameInput.value = 'user'; + usernameInput.dispatchEvent(new Event('input')); + passwordInput.value = 'user'; + passwordInput.dispatchEvent(new Event('input')); + + fixture.detectChanges(); + + expect(component.form.value).toEqual({ + username: 'user', + password: 'user' + }); + }); + + it('login event should work well', fakeAsync(() => { + let userCredential: LoginEvent; + component.login.subscribe((e: LoginEvent) => { userCredential = e; }); + fixture.detectChanges(); + const mockValue = { + username: 'user', + password: 'user' + }; + component.form.setValue(mockValue); + component.onLoginButtonClick(); + expect(userCredential).toEqual(mockValue); + })); }); diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts index b9221b90..28cfefd7 100644 --- a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts @@ -1,12 +1,114 @@ import { TestBed } from '@angular/core/testing'; +import { HttpRequest } from '@angular/common/http'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { UserService } from './user.service'; +import { UserInfo } from '../user-info'; +import { + UserService, UserCredentials, CreateTokenResult, + UserLoginState, TokenValidationRequest, TokenValidationResult +} from './user.service'; -xdescribe('UserService', () => { - beforeEach(() => TestBed.configureTestingModule({})); +describe('UserService', () => { + const tokenCreateUrl = '/api/User/CreateToken'; + + beforeEach(() => TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + })); it('should be created', () => { const service: UserService = TestBed.get(UserService); expect(service).toBeTruthy(); }); + + it('should be nologin at first', () => { + const service: UserService = TestBed.get(UserService); + service.validateUserLoginState().subscribe(result => { + expect(result.state).toBe('nologin'); + }); + }); + + it('login should work well', () => { + const service: UserService = TestBed.get(UserService); + + const mockUserInfo: UserInfo = { + username: 'user', + roles: ['user', 'other'] + }; + + service.tryLogin('user', 'user').subscribe(result => { + expect(result).toEqual(mockUserInfo); + }); + + const httpController = TestBed.get(HttpTestingController) as HttpTestingController; + + httpController.expectOne((request: HttpRequest) => + request.url === tokenCreateUrl && + request.body.username === 'user' && + request.body.password === 'user').flush({ + token: 'test-token', + userInfo: mockUserInfo + }); + + httpController.verify(); + }); + + describe('validateUserLoginState', () => { + let service: UserService; + let httpController: HttpTestingController; + + const mockUserInfo: UserInfo = { + username: 'user', + roles: ['user', 'other'] + }; + + const mockToken = 'mock-token'; + + const tokenValidateRequestMatcher = (req: HttpRequest) => { + return req.url === '/api/User/ValidateToken' && req.body.token === mockToken; + } + + beforeEach(() => { + service = TestBed.get(UserService); + httpController = TestBed.get(HttpTestingController); + + service.tryLogin('user', 'user').subscribe(); // subscribe to activate login + + httpController.expectOne(tokenCreateUrl).flush({ + token: mockToken, + userInfo: mockUserInfo + }); + }); + + it('success should work well', () => { + service.validateUserLoginState().subscribe((result: UserLoginState) => { + expect(result).toEqual({ + state: 'success', + userInfo: mockUserInfo + }); + }); + + httpController.expectOne(tokenValidateRequestMatcher).flush({ + isValid: true, + userInfo: mockUserInfo + }); + + httpController.verify(); + }); + + it('invalid should work well', () => { + service.validateUserLoginState().subscribe((result: UserLoginState) => { + expect(result).toEqual({ + state: 'invalidlogin' + }); + }); + + httpController.expectOne(tokenValidateRequestMatcher).flush({ + isValid: false + }); + + httpController.verify(); + }); + }); + + // TODO: test on error situations. }); -- cgit v1.2.3 From b5e01c4571061cbaf5915aa4c0f1b7126ef1ed18 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 6 Mar 2019 23:20:49 +0800 Subject: Fix a lint error. --- Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts index 28cfefd7..0095f031 100644 --- a/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts +++ b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts @@ -65,7 +65,7 @@ describe('UserService', () => { const tokenValidateRequestMatcher = (req: HttpRequest) => { return req.url === '/api/User/ValidateToken' && req.body.token === mockToken; - } + }; beforeEach(() => { service = TestBed.get(UserService); -- cgit v1.2.3