From 61844a348b2934321567b1457e6d05f318fc8b7e 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') 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