aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src/app/todo
diff options
context:
space:
mode:
Diffstat (limited to 'Timeline/ClientApp/src/app/todo')
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item.ts6
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css25
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html9
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts46
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts13
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-color-block.css7
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.css39
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.html21
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.spec.ts78
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-page/todo-list-page.component.ts37
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.spec.ts54
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/todo-list.service.ts43
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
+ })
+ );
+ }
+}