From 00757c9b23d1c614960d74b54054ccc35129150c Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 7 Mar 2019 20:56:50 +0800 Subject: Reorganize with modules. --- Timeline/ClientApp/src/app/app.module.ts | 45 ++------ .../src/app/debounce-click.directive.spec.ts | 124 --------------------- .../ClientApp/src/app/debounce-click.directive.ts | 39 ------- Timeline/ClientApp/src/app/home/home.component.ts | 8 +- Timeline/ClientApp/src/app/home/home.module.ts | 17 +++ .../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 ------ .../src/app/todo/todo-page/todo-page.component.css | 39 +++++++ .../app/todo/todo-page/todo-page.component.html | 21 ++++ .../app/todo/todo-page/todo-page.component.spec.ts | 78 +++++++++++++ .../src/app/todo/todo-page/todo-page.component.ts | 39 +++++++ .../todo/todo-service/todo-list.service.spec.ts | 54 --------- .../src/app/todo/todo-service/todo-list.service.ts | 43 ------- .../src/app/todo/todo-service/todo.service.spec.ts | 54 +++++++++ .../src/app/todo/todo-service/todo.service.ts | 43 +++++++ Timeline/ClientApp/src/app/todo/todo.module.ts | 27 +++++ Timeline/ClientApp/src/app/user/user.module.ts | 24 ++++ .../app/utility/debounce-click.directive.spec.ts | 124 +++++++++++++++++++++ .../src/app/utility/debounce-click.directive.ts | 39 +++++++ .../ClientApp/src/app/utility/utility.module.ts | 11 ++ 22 files changed, 526 insertions(+), 478 deletions(-) delete mode 100644 Timeline/ClientApp/src/app/debounce-click.directive.spec.ts delete mode 100644 Timeline/ClientApp/src/app/debounce-click.directive.ts create mode 100644 Timeline/ClientApp/src/app/home/home.module.ts delete mode 100644 Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css delete mode 100644 Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html delete mode 100644 Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts delete mode 100644 Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.css create mode 100644 Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.html create mode 100644 Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.spec.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.ts delete mode 100644 Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts delete mode 100644 Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts create mode 100644 Timeline/ClientApp/src/app/todo/todo.module.ts create mode 100644 Timeline/ClientApp/src/app/user/user.module.ts create mode 100644 Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts create mode 100644 Timeline/ClientApp/src/app/utility/debounce-click.directive.ts create mode 100644 Timeline/ClientApp/src/app/utility/utility.module.ts (limited to 'Timeline/ClientApp/src') diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts index d0b6a5c6..85c4c43d 100644 --- a/Timeline/ClientApp/src/app/app.module.ts +++ b/Timeline/ClientApp/src/app/app.module.ts @@ -1,56 +1,27 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -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, MatDialogModule, MatInputModule, MatFormFieldModule, MatProgressSpinnerModule -} from '@angular/material'; +import { MatIconModule, MatButtonModule, MatToolbarModule, MatDialogModule } from '@angular/material'; import { AppComponent } from './app.component'; -import { HomeComponent } from './home/home.component'; -import { DebounceClickDirective } from './debounce-click.directive'; +import { TodoModule } from './todo/todo.module'; +import { HomeModule } from './home/home.module'; +import { UserModule } from './user/user.module'; -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, - UserDialogComponent, - DebounceClickDirective, - UserLoginComponent, - UserLoginSuccessComponent - ], + declarations: [AppComponent], imports: [ BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), - HttpClientModule, - ReactiveFormsModule, BrowserAnimationsModule, - ...importedMatModules, + MatIconModule, MatButtonModule, MatToolbarModule, MatDialogModule, + HomeModule, TodoModule, UserModule, RouterModule.forRoot([ - { path: '', component: HomeComponent, pathMatch: 'full' }, - { path: 'todo', component: TodoListPageComponent } + { path: '', redirectTo: '/home', pathMatch: 'full' }, ]) ], - entryComponents: [UserDialogComponent], - providers: [], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/Timeline/ClientApp/src/app/debounce-click.directive.spec.ts b/Timeline/ClientApp/src/app/debounce-click.directive.spec.ts deleted file mode 100644 index 75710d0c..00000000 --- a/Timeline/ClientApp/src/app/debounce-click.directive.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { async, TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; - -import { DebounceClickDirective } from './debounce-click.directive'; - -interface TestComponent { - clickHandler: () => void; -} - -@Component({ - selector: 'app-default-test', - template: '' -}) -class DefaultDebounceTimeTestComponent { - @ViewChild(DebounceClickDirective) - directive: DebounceClickDirective; - - clickHandler: () => void = () => { }; -} - -@Component({ - selector: 'app-default-test', - template: '' -}) -class CustomDebounceTimeTestComponent { - debounceTime: number; - - @ViewChild(DebounceClickDirective) - directive: DebounceClickDirective; - - clickHandler: () => void = () => { }; -} - - -describe('DebounceClickDirective', () => { - let counter: number; - - function initComponent(component: TestComponent) { - component.clickHandler = () => counter++; - } - - beforeEach(() => { - counter = 0; - }); - - describe('default debounce time', () => { - let component: DefaultDebounceTimeTestComponent; - let componentFixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [DebounceClickDirective, DefaultDebounceTimeTestComponent] - }).compileComponents(); - })); - - beforeEach(() => { - componentFixture = TestBed.createComponent(DefaultDebounceTimeTestComponent); - component = componentFixture.componentInstance; - initComponent(component); - }); - - it('should create an instance', () => { - componentFixture.detectChanges(); - expect(component.directive).toBeTruthy(); - }); - - it('should work well', fakeAsync(() => { - function click() { - (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); - } - componentFixture.detectChanges(); - expect(counter).toBe(0); - click(); - tick(300); - expect(counter).toBe(0); - click(); - tick(); - expect(counter).toBe(0); - tick(500); - expect(counter).toBe(1); - })); - }); - - - describe('custom debounce time', () => { - let component: CustomDebounceTimeTestComponent; - let componentFixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [DebounceClickDirective, CustomDebounceTimeTestComponent] - }).compileComponents(); - })); - - beforeEach(() => { - componentFixture = TestBed.createComponent(CustomDebounceTimeTestComponent); - component = componentFixture.componentInstance; - initComponent(component); - component.debounceTime = 600; - }); - - it('should create an instance', () => { - componentFixture.detectChanges(); - expect(component.directive).toBeTruthy(); - }); - - it('should work well', fakeAsync(() => { - function click() { - (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); - } - componentFixture.detectChanges(); - expect(counter).toBe(0); - click(); - tick(300); - expect(counter).toBe(0); - click(); - tick(); - expect(counter).toBe(0); - tick(600); - expect(counter).toBe(1); - })); - }); -}); diff --git a/Timeline/ClientApp/src/app/debounce-click.directive.ts b/Timeline/ClientApp/src/app/debounce-click.directive.ts deleted file mode 100644 index feb0404e..00000000 --- a/Timeline/ClientApp/src/app/debounce-click.directive.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Directive, Output, Input, EventEmitter, ElementRef, OnInit, OnDestroy } from '@angular/core'; -import { fromEvent, Subscription } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; - -@Directive({ - selector: '[appDebounceClick]' -}) -export class DebounceClickDirective implements OnInit, OnDestroy { - - private subscription: Subscription; - - @Output('appDebounceClick') clickEvent = new EventEmitter(); - - // tslint:disable-next-line:no-input-rename - @Input('appDebounceClickTime') - set debounceTime(value: number) { - if (this.subscription) { - this.subscription.unsubscribe(); - } - this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( - debounceTime(value) - ).subscribe(o => this.clickEvent.emit(o)); - } - - constructor(private element: ElementRef) { - } - - ngOnInit() { - if (!this.subscription) { - this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( - debounceTime(500) - ).subscribe(o => this.clickEvent.emit(o)); - } - } - - ngOnDestroy() { - this.subscription.unsubscribe(); - } -} diff --git a/Timeline/ClientApp/src/app/home/home.component.ts b/Timeline/ClientApp/src/app/home/home.component.ts index 2b16eef7..0cb0d0f5 100644 --- a/Timeline/ClientApp/src/app/home/home.component.ts +++ b/Timeline/ClientApp/src/app/home/home.component.ts @@ -1,14 +1,10 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.css'] }) -export class HomeComponent implements OnInit { +export class HomeComponent { - message = ''; - - ngOnInit() { - } } diff --git a/Timeline/ClientApp/src/app/home/home.module.ts b/Timeline/ClientApp/src/app/home/home.module.ts new file mode 100644 index 00000000..98456238 --- /dev/null +++ b/Timeline/ClientApp/src/app/home/home.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; + +import { HomeComponent } from './home.component'; + +@NgModule({ + declarations: [HomeComponent], + imports: [ + CommonModule, + RouterModule.forChild([ + { path: 'home', component: HomeComponent } + ]) + ], + exports: [RouterModule] +}) +export class HomeModule { } 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 deleted file mode 100644 index 754b786e..00000000 --- a/Timeline/ClientApp/src/app/todo/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/todo-list-page/todo-list-page.component.html b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html deleted file mode 100644 index 50180fe8..00000000 --- a/Timeline/ClientApp/src/app/todo/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/todo-list-page/todo-list-page.component.spec.ts b/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts deleted file mode 100644 index 0af113dc..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index a69c6856..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -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-page/todo-page.component.css b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.css new file mode 100644 index 00000000..754b786e --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-page/todo-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-page/todo-page.component.html b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.html new file mode 100644 index 00000000..50180fe8 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-page/todo-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-page/todo-page.component.spec.ts b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.spec.ts new file mode 100644 index 00000000..16c77376 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-page/todo-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 { TodoPageComponent } from './todo-page.component'; +import { TodoService } from '../todo-service/todo.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: TodoPageComponent; + 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 mockTodoService: jasmine.SpyObj = jasmine.createSpyObj('TodoService', ['getWorkItemList']); + + mockTodoService.getWorkItemList.and.returnValue(asyncArray(mockTodoItems)); + + TestBed.configureTestingModule({ + declarations: [TodoPageComponent, MatProgressBarStubComponent], + imports: [NoopAnimationsModule], + providers: [{ provide: TodoService, useValue: mockTodoService }], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TodoPageComponent); + 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-page/todo-page.component.ts b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.ts new file mode 100644 index 00000000..7b658228 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.ts @@ -0,0 +1,39 @@ +import { Component, OnInit } from '@angular/core'; +import { trigger, transition, style, animate } from '@angular/animations'; + + +import { TodoItem } from '../todo-item'; +import { TodoService } from '../todo-service/todo.service'; + +@Component({ + selector: 'app-todo-page', + templateUrl: './todo-page.component.html', + styleUrls: ['./todo-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 TodoPageComponent implements OnInit { + + items: TodoItem[] = []; + isLoadCompleted = false; + + constructor(private todoService: TodoService) { + } + + 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 deleted file mode 100644 index d8283b54..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 83bf47ec..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts +++ /dev/null @@ -1,43 +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'; - -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/todo/todo-service/todo.service.spec.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts new file mode 100644 index 00000000..b0b35f7b --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-service/todo.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 { TodoService, IssueResponse } from './todo.service'; + + +describe('TodoService', () => { + beforeEach(() => TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + })); + + it('should be created', () => { + const service: TodoService = TestBed.get(TodoService); + expect(service).toBeTruthy(); + }); + + it('should work well', () => { + const service: TodoService = TestBed.get(TodoService); + + 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.service.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts new file mode 100644 index 00000000..ed1f2cbe --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo-service/todo.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 TodoService { + + 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.module.ts b/Timeline/ClientApp/src/app/todo/todo.module.ts new file mode 100644 index 00000000..5bcfefbd --- /dev/null +++ b/Timeline/ClientApp/src/app/todo/todo.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { MatListModule, MatIconModule, MatCardModule, MatProgressBarModule, MatButtonModule } from '@angular/material'; +import { HttpClientModule } from '@angular/common/http'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { TodoItemComponent } from './todo-item/todo-item.component'; +import { TodoPageComponent } from './todo-page/todo-page.component'; + +@NgModule({ + declarations: [ + TodoItemComponent, + TodoPageComponent + ], + imports: [ + CommonModule, HttpClientModule, BrowserAnimationsModule, + MatListModule, MatCardModule, MatIconModule, MatProgressBarModule, MatButtonModule, + RouterModule.forChild([ + { path: 'todo', component: TodoPageComponent } + ]) + ], + exports: [ + RouterModule + ] +}) +export class TodoModule { } diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts new file mode 100644 index 00000000..67de90a2 --- /dev/null +++ b/Timeline/ClientApp/src/app/user/user.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { + MatFormFieldModule, MatProgressSpinnerModule, + MatDialogModule, MatInputModule, MatButtonModule +} from '@angular/material'; + +import { UserDialogComponent } from './user-dialog/user-dialog.component'; +import { UserLoginComponent } from './user-login/user-login.component'; +import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component'; +import { UtilityModule } from '../utility/utility.module'; + +@NgModule({ + declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent], + imports: [ + CommonModule, HttpClientModule, ReactiveFormsModule, + MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, + UtilityModule + ], + entryComponents: [UserDialogComponent] +}) +export class UserModule { } diff --git a/Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts b/Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts new file mode 100644 index 00000000..75710d0c --- /dev/null +++ b/Timeline/ClientApp/src/app/utility/debounce-click.directive.spec.ts @@ -0,0 +1,124 @@ +import { Component, ViewChild } from '@angular/core'; +import { async, TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DebounceClickDirective } from './debounce-click.directive'; + +interface TestComponent { + clickHandler: () => void; +} + +@Component({ + selector: 'app-default-test', + template: '' +}) +class DefaultDebounceTimeTestComponent { + @ViewChild(DebounceClickDirective) + directive: DebounceClickDirective; + + clickHandler: () => void = () => { }; +} + +@Component({ + selector: 'app-default-test', + template: '' +}) +class CustomDebounceTimeTestComponent { + debounceTime: number; + + @ViewChild(DebounceClickDirective) + directive: DebounceClickDirective; + + clickHandler: () => void = () => { }; +} + + +describe('DebounceClickDirective', () => { + let counter: number; + + function initComponent(component: TestComponent) { + component.clickHandler = () => counter++; + } + + beforeEach(() => { + counter = 0; + }); + + describe('default debounce time', () => { + let component: DefaultDebounceTimeTestComponent; + let componentFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DebounceClickDirective, DefaultDebounceTimeTestComponent] + }).compileComponents(); + })); + + beforeEach(() => { + componentFixture = TestBed.createComponent(DefaultDebounceTimeTestComponent); + component = componentFixture.componentInstance; + initComponent(component); + }); + + it('should create an instance', () => { + componentFixture.detectChanges(); + expect(component.directive).toBeTruthy(); + }); + + it('should work well', fakeAsync(() => { + function click() { + (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); + } + componentFixture.detectChanges(); + expect(counter).toBe(0); + click(); + tick(300); + expect(counter).toBe(0); + click(); + tick(); + expect(counter).toBe(0); + tick(500); + expect(counter).toBe(1); + })); + }); + + + describe('custom debounce time', () => { + let component: CustomDebounceTimeTestComponent; + let componentFixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DebounceClickDirective, CustomDebounceTimeTestComponent] + }).compileComponents(); + })); + + beforeEach(() => { + componentFixture = TestBed.createComponent(CustomDebounceTimeTestComponent); + component = componentFixture.componentInstance; + initComponent(component); + component.debounceTime = 600; + }); + + it('should create an instance', () => { + componentFixture.detectChanges(); + expect(component.directive).toBeTruthy(); + }); + + it('should work well', fakeAsync(() => { + function click() { + (componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); + } + componentFixture.detectChanges(); + expect(counter).toBe(0); + click(); + tick(300); + expect(counter).toBe(0); + click(); + tick(); + expect(counter).toBe(0); + tick(600); + expect(counter).toBe(1); + })); + }); +}); diff --git a/Timeline/ClientApp/src/app/utility/debounce-click.directive.ts b/Timeline/ClientApp/src/app/utility/debounce-click.directive.ts new file mode 100644 index 00000000..feb0404e --- /dev/null +++ b/Timeline/ClientApp/src/app/utility/debounce-click.directive.ts @@ -0,0 +1,39 @@ +import { Directive, Output, Input, EventEmitter, ElementRef, OnInit, OnDestroy } from '@angular/core'; +import { fromEvent, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Directive({ + selector: '[appDebounceClick]' +}) +export class DebounceClickDirective implements OnInit, OnDestroy { + + private subscription: Subscription; + + @Output('appDebounceClick') clickEvent = new EventEmitter(); + + // tslint:disable-next-line:no-input-rename + @Input('appDebounceClickTime') + set debounceTime(value: number) { + if (this.subscription) { + this.subscription.unsubscribe(); + } + this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( + debounceTime(value) + ).subscribe(o => this.clickEvent.emit(o)); + } + + constructor(private element: ElementRef) { + } + + ngOnInit() { + if (!this.subscription) { + this.subscription = fromEvent(this.element.nativeElement, 'click').pipe( + debounceTime(500) + ).subscribe(o => this.clickEvent.emit(o)); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/Timeline/ClientApp/src/app/utility/utility.module.ts b/Timeline/ClientApp/src/app/utility/utility.module.ts new file mode 100644 index 00000000..dd686bf7 --- /dev/null +++ b/Timeline/ClientApp/src/app/utility/utility.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { DebounceClickDirective } from './debounce-click.directive'; + +@NgModule({ + declarations: [DebounceClickDirective], + imports: [CommonModule], + exports: [DebounceClickDirective] +}) +export class UtilityModule { } -- cgit v1.2.3