diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/todo')
12 files changed, 378 insertions, 0 deletions
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 @@ +<mat-card class="mat-elevation-z2 item-card"> + <span class="item-color-block" [class.color-block-closed]="item.isClosed" [class.color-block-open]="!item.isClosed"></span> + <div class="mat-h3 item-body-box"> + <span class="item-title">{{ item.number }}. {{ item.title }}</span> + <a mat-icon-button class="item-detail-button" [href]="item.detailUrl"> + <mat-icon>arrow_forward</mat-icon> + </a> + </div> +</mat-card> 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<TodoItemComponent>; + + 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 @@ +<mat-progress-bar *ngIf="!isLoadCompleted" mode="indeterminate"></mat-progress-bar> + +<mat-list> + <mat-list-item *ngFor="let item of items; let i = index" style="height:unset;"> + <div class="item-box" [class.first-item-box]="i === 0" [class.non-first-item-box]="i !== 0"> + <app-todo-item @itemEnter [class.align-self-bottom]="i === 0" [item]="item"></app-todo-item> + <div class="space"></div> + <div class="sample-box" *ngIf="i === 0"> + <div class="mat-caption sample-item"> + <span class="sample-color-block color-block-open"></span> + <span> means working now.</span> + </div> + <div class="mat-caption sample-item"> + <span class="sample-color-block color-block-closed"></span> + <span> means completed.</span> + </div> + <div class="mat-caption">click on item to see details.</div> + </div> + </div> + </mat-list-item> +</mat-list> 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<T>(data: T[]): Observable<T> { + return from(data).pipe(delay(0)); +} + +describe('TodoListPageComponent', () => { + let component: TodoListPageComponent; + let fixture: ComponentFixture<TodoListPageComponent>; + + 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<TodoListService> = 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<TodoItem> { + return this.client.get<IssueResponse>(`${this.baseUrl}/issues`, { + params: { + state: 'all' + } + }).pipe( + switchMap(result => from(result)), + filter(result => result.pull_request === undefined), // filter out pull requests. + map(result => <TodoItem>{ + number: result.number, + title: result.title, + isClosed: result.state === 'closed', + detailUrl: result.html_url + }) + ); + } +} |