aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-03-06 23:29:12 +0800
committerGitHub <noreply@github.com>2019-03-06 23:29:12 +0800
commitd4410036b62a65cedbe977efdcea023440a3198e (patch)
treefdc6e957aed9cd0f1433f2c42e1e4ed89d13b4ee
parent8033d6523885486c24af2bdd57a24b0fd62d0b00 (diff)
parent7107d431fe7019ccc20e90e5aecb5feb64fc53b3 (diff)
downloadtimeline-d4410036b62a65cedbe977efdcea023440a3198e.tar.gz
timeline-d4410036b62a65cedbe977efdcea023440a3198e.tar.bz2
timeline-d4410036b62a65cedbe977efdcea023440a3198e.zip
Merge pull request #3 from crupest/user
Develop user dialog.
-rw-r--r--Timeline/ClientApp/.vscode/launch.json33
-rw-r--r--Timeline/ClientApp/package.json1
-rw-r--r--Timeline/ClientApp/src/app/app.component.html2
-rw-r--r--Timeline/ClientApp/src/app/app.component.ts11
-rw-r--r--Timeline/ClientApp/src/app/app.module.ts32
-rw-r--r--Timeline/ClientApp/src/app/debounce-click.directive.spec.ts124
-rw-r--r--Timeline/ClientApp/src/app/debounce-click.directive.ts39
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item.ts6
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css (renamed from Timeline/ClientApp/src/app/todo-item/todo-item.component.css)0
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html (renamed from Timeline/ClientApp/src/app/todo-item/todo-item.component.html)0
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts (renamed from Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts)8
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts (renamed from Timeline/ClientApp/src/app/todo-item/todo-item.component.ts)4
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-color-block.css (renamed from Timeline/ClientApp/src/app/todo-list-page/todo-list-color-block.css)0
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css (renamed from Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css)0
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html (renamed from Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.html)0
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts (renamed from Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts)12
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts (renamed from Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.ts)5
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts (renamed from Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts)7
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts (renamed from Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts)9
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css5
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html5
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts114
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts43
-rw-r--r--Timeline/ClientApp/src/app/user/user-info.ts4
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css7
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html5
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts39
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts22
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.css24
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.html18
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts60
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.ts32
-rw-r--r--Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts114
-rw-r--r--Timeline/ClientApp/src/app/user/user-service/user.service.ts116
-rw-r--r--azure-pipelines.yml3
35 files changed, 850 insertions, 54 deletions
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 @@
<img width="30" height="30" src="assets/icon.svg"> Timeline</a>
<a mat-button routerLink="/todo">TodoList</a>
<span class="fill-remaining-space"></span>
- <button mat-icon-button>
+ <button mat-icon-button (click)="openUserDialog()">
<mat-icon>account_circle</mat-icon>
</button>
</mat-toolbar>
diff --git a/Timeline/ClientApp/src/app/app.component.ts b/Timeline/ClientApp/src/app/app.component.ts
index bba1f59d..0e2a9799 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/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: '300px'
+ });
+ }
}
diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts
index 86511be8..d0b6a5c6 100644
--- a/Timeline/ClientApp/src/app/app.module.ts
+++ b/Timeline/ClientApp/src/app/app.module.ts
@@ -1,37 +1,55 @@
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, MatProgressSpinnerModule
} from '@angular/material';
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 { DebounceClickDirective } from './debounce-click.directive';
+
+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,
+ MatListModule, MatProgressBarModule, MatCardModule, MatDialogModule,
+ MatInputModule, MatFormFieldModule, MatProgressSpinnerModule
+];
@NgModule({
declarations: [
AppComponent,
HomeComponent,
TodoListPageComponent,
- TodoItemComponent
+ TodoItemComponent,
+ UserDialogComponent,
+ DebounceClickDirective,
+ UserLoginComponent,
+ UserLoginSuccessComponent
],
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: '<button (appDebounceClick)="clickHandler()"></button>'
+})
+class DefaultDebounceTimeTestComponent {
+ @ViewChild(DebounceClickDirective)
+ directive: DebounceClickDirective;
+
+ clickHandler: () => void = () => { };
+}
+
+@Component({
+ selector: 'app-default-test',
+ template: '<button (appDebounceClick)="clickHandler()" [appDebounceClickTime]="debounceTime"></button>'
+})
+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<DefaultDebounceTimeTestComponent>;
+
+ 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() {
+ (<HTMLButtonElement>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<CustomDebounceTimeTestComponent>;
+
+ 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() {
+ (<HTMLButtonElement>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<any>();
+
+ // tslint:disable-next-line:no-input-rename
+ @Input('appDebounceClickTime')
+ set debounceTime(value: number) {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ this.subscription = fromEvent(<HTMLElement>this.element.nativeElement, 'click').pipe(
+ debounceTime(value)
+ ).subscribe(o => this.clickEvent.emit(o));
+ }
+
+ constructor(private element: ElementRef) {
+ }
+
+ ngOnInit() {
+ if (!this.subscription) {
+ this.subscription = fromEvent(<HTMLElement>this.element.nativeElement, 'click').pipe(
+ debounceTime(500)
+ ).subscribe(o => this.clickEvent.emit(o));
+ }
+ }
+
+ ngOnDestroy() {
+ this.subscription.unsubscribe();
+ }
+}
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-item/todo-item.component.css b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css
index dcf25fd8..dcf25fd8 100644
--- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.css
+++ b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css
diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.html b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html
index 6f76e73b..6f76e73b 100644
--- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.html
+++ b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html
diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts
index 520b6136..239ffc42 100644
--- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts
+++ b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts
@@ -1,9 +1,9 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
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';
+
+import { TodoItem } from '../todo-item';
+import { TodoItemComponent } from '../todo-item/todo-item.component';
describe('TodoItemComponent', () => {
let component: TodoItemComponent;
diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.ts b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts
index 325812f1..2ea6997a 100644
--- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.ts
+++ b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts
@@ -1,10 +1,10 @@
import { Component, Input } from '@angular/core';
-import { TodoItem } from '../todo-list-page/todo-list.service';
+import { TodoItem } from '../todo-item';
@Component({
selector: 'app-todo-item',
templateUrl: './todo-item.component.html',
- styleUrls: ['./todo-item.component.css', '../todo-list-page/todo-list-color-block.css']
+ styleUrls: ['./todo-item.component.css', '../todo-list-color-block.css']
})
export class TodoItemComponent {
diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-color-block.css b/Timeline/ClientApp/src/app/todo/todo-list-color-block.css
index 5e0d4ba9..5e0d4ba9 100644
--- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-color-block.css
+++ b/Timeline/ClientApp/src/app/todo/todo-list-color-block.css
diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css
index 754b786e..754b786e 100644
--- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css
+++ b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css
diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.html b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html
index 50180fe8..50180fe8 100644
--- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.html
+++ b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html
diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts
index 5706bf51..0af113dc 100644
--- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts
+++ b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts
@@ -1,20 +1,22 @@
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, TodoItem } from './todo-list.service';
-import { By } from '@angular/platform-browser';
-import { delay } from 'rxjs/operators';
-import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { TodoListService } from '../todo-service/todo-list.service';
+
@Component({
/* tslint:disable-next-line:component-selector*/
selector: 'mat-progress-bar',
template: ''
})
-class MatProgressBarStubComponent {}
+class MatProgressBarStubComponent { }
function asyncArray<T>(data: T[]): Observable<T> {
return from(data).pipe(delay(0));
diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.ts b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts
index c62dd808..a69c6856 100644
--- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.ts
+++ b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts
@@ -1,11 +1,12 @@
import { Component, OnInit } from '@angular/core';
-import { TodoListService, TodoItem } from './todo-list.service';
+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'],
+ styleUrls: ['./todo-list-page.component.css', '../todo-list-color-block.css'],
animations: [
trigger('itemEnter', [
transition(':enter', [
diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts
index a2ad0cbd..d8283b54 100644
--- a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts
+++ b/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts
@@ -1,11 +1,10 @@
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';
+import { TodoItem } from '../todo-item';
+import { TodoListService, IssueResponse } from './todo-list.service';
+
describe('TodoListServiceService', () => {
beforeEach(() => TestBed.configureTestingModule({
diff --git a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts
index ffcbbc6f..83bf47ec 100644
--- a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts
+++ b/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts
@@ -3,6 +3,8 @@ 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;
@@ -13,13 +15,6 @@ export interface IssueResponseItem {
export type IssueResponse = IssueResponseItem[];
-export interface TodoItem {
- number: number;
- title: string;
- isClosed: boolean;
- detailUrl: string;
-}
-
@Injectable({
providedIn: 'root'
})
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 @@
+<div [ngSwitch]="state" class="container">
+ <mat-progress-spinner *ngSwitchCase="'loading'" mode="indeterminate" diameter="50"></mat-progress-spinner>
+ <app-user-login *ngSwitchCase="'login'" (login)="login($event)" [message]="loginMessage"></app-user-login>
+ <app-user-login-success *ngSwitchCase="'success'" [userInfo]="userInfo" [displayLoginSuccessMessage]="displayLoginSuccessMessage"></app-user-login-success>
+</div>
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..d24c0cd2
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts
@@ -0,0 +1,114 @@
+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';
+
+@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<LoginEvent>();
+}
+
+@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<UserDialogComponent>;
+ let mockUserService: jasmine.SpyObj<UserService>;
+
+ beforeEach(async(() => {
+ mockUserService = jasmine.createSpyObj('UserService', ['validateUserLoginState', 'tryLogin']);
+
+ TestBed.configureTestingModule({
+ declarations: [UserDialogComponent, MatProgressSpinnerStubComponent,
+ UserLoginStubComponent, UserLoginSuccessStubComponent],
+ providers: [{ provide: UserService, useValue: mockUserService }]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserDialogComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('progress spinner should work well', fakeAsync(() => {
+ mockUserService.validateUserLoginState.and.returnValue(of(<UserLoginState>{ 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(<UserLoginState>{ 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('success should work well', () => {
+ mockUserService.validateUserLoginState.and.returnValue(of(<UserLoginState>{ 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(<UserLoginState>{ 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(<UserInfo>{
+ username: 'user',
+ roles: ['user']
+ }));
+
+ (fixture.debugElement.query(By.css('app-user-login')).componentInstance as
+ UserLoginStubComponent).login.emit(<LoginEvent>{
+ 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-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..e156f0f8
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html
@@ -0,0 +1,5 @@
+<p *ngIf="displayLoginSuccessMessage" class="mat-body login-success-message">
+ Login succeeds!
+</p>
+<p class="mat-body">You have been login as <span class="username">{{ userInfo.username }}</span>.</p>
+<p class="mat-body">Your roles are <span class="roles">{{ userInfo.roles.join(', ') }}</span>.</p>
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..ba015ae6
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts
@@ -0,0 +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<UserLoginSuccessComponent>;
+
+ const mockUserInfo = {
+ username: 'crupest',
+ roles: ['superman', 'coder']
+ };
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [UserLoginSuccessComponent]
+ })
+ .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-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 @@
+<form [formGroup]="form">
+ <ng-container *ngIf="message" [ngSwitch]="message">
+ <p *ngSwitchCase="'nologin'" class="mat-body no-login-message">You haven't login.</p>
+ <p *ngSwitchCase="'invalidlogin'" class="mat-body invalid-login-message">Your login is no longer valid.</p>
+ <p *ngSwitchDefault class="mat-body error-message">{{ message }}</p>
+ </ng-container>
+ <mat-form-field>
+ <mat-label>Username</mat-label>
+ <input formControlName="username" matInput type="text" />
+ </mat-form-field>
+ <div class="w-100"></div>
+ <mat-form-field>
+ <mat-label>Password</mat-label>
+ <input formControlName="password" matInput type="password" />
+ </mat-form-field>
+ <div class="w-100"></div>
+ <button mat-flat-button class="login-button" (appDebounceClick)="onLoginButtonClick()">Login</button>
+</form>
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..acd13721
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts
@@ -0,0 +1,60 @@
+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, LoginEvent } from './user-login.component';
+
+describe('UserLoginComponent', () => {
+ let component: UserLoginComponent;
+ let fixture: ComponentFixture<UserLoginComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [UserLoginComponent],
+ imports: [ReactiveFormsModule],
+ schemas: [NO_ERRORS_SCHEMA]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserLoginComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ 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-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<LoginEvent>();
+
+ 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..0095f031
--- /dev/null
+++ b/Timeline/ClientApp/src/app/user/user-service/user.service.spec.ts
@@ -0,0 +1,114 @@
+import { TestBed } from '@angular/core/testing';
+import { HttpRequest } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+
+import { UserInfo } from '../user-info';
+import {
+ UserService, UserCredentials, CreateTokenResult,
+ UserLoginState, TokenValidationRequest, TokenValidationResult
+} from './user.service';
+
+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<UserCredentials>) =>
+ request.url === tokenCreateUrl &&
+ request.body.username === 'user' &&
+ request.body.password === 'user').flush(<CreateTokenResult>{
+ 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<TokenValidationRequest>) => {
+ 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(<CreateTokenResult>{
+ token: mockToken,
+ userInfo: mockUserInfo
+ });
+ });
+
+ it('success should work well', () => {
+ service.validateUserLoginState().subscribe((result: UserLoginState) => {
+ expect(result).toEqual(<UserLoginState>{
+ state: 'success',
+ userInfo: mockUserInfo
+ });
+ });
+
+ httpController.expectOne(tokenValidateRequestMatcher).flush(<TokenValidationResult>{
+ isValid: true,
+ userInfo: mockUserInfo
+ });
+
+ httpController.verify();
+ });
+
+ it('invalid should work well', () => {
+ service.validateUserLoginState().subscribe((result: UserLoginState) => {
+ expect(result).toEqual(<UserLoginState>{
+ state: 'invalidlogin'
+ });
+ });
+
+ httpController.expectOne(tokenValidateRequestMatcher).flush(<TokenValidationResult>{
+ isValid: false
+ });
+
+ httpController.verify();
+ });
+ });
+
+ // TODO: test on error situations.
+});
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<UserLoginState> {
+ if (this.token === undefined || this.token === null) {
+ return of(<UserLoginState>{ state: 'nologin' });
+ }
+
+ return this.httpClient.post<TokenValidationResult>('/api/User/ValidateToken', <TokenValidationRequest>{ 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 <UserLoginState>{
+ state: 'success',
+ userInfo: result.userInfo
+ };
+ } else {
+ this.token = null;
+ this.userInfo = null;
+ return <UserLoginState>{
+ state: 'invalidlogin'
+ };
+ }
+ })
+ );
+ }
+
+ tryLogin(username: string, password: string): Observable<UserInfo> {
+ if (this.token) {
+ return throwError(new AlreadyLoginException());
+ }
+
+ return this.httpClient.post<CreateTokenResult>('/api/User/CreateToken', <UserCredentials>{
+ 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/azure-pipelines.yml b/azure-pipelines.yml
index 4bbf12af..1cdf1c3a 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -3,6 +3,9 @@
# Add steps that run tests, create a NuGet package, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
+trigger:
+- master
+
pool:
vmImage: 'Ubuntu-16.04'