aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2019-03-04 19:58:48 +0800
committercrupest <crupest@outlook.com>2019-03-04 19:58:48 +0800
commit1715b3a618ddffc28177e58ae21c91b30d586ccf (patch)
tree3bef8b64aea0792c3b415fa9236837afd1a2577e /Timeline/ClientApp/src
parent80343fab6624f74f0777968dd213c3a26e65890d (diff)
parent8033d6523885486c24af2bdd57a24b0fd62d0b00 (diff)
downloadtimeline-1715b3a618ddffc28177e58ae21c91b30d586ccf.tar.gz
timeline-1715b3a618ddffc28177e58ae21c91b30d586ccf.tar.bz2
timeline-1715b3a618ddffc28177e58ae21c91b30d586ccf.zip
Merge branch 'master' into user
Diffstat (limited to 'Timeline/ClientApp/src')
-rw-r--r--Timeline/ClientApp/src/app/todo-item/todo-item.component.css10
-rw-r--r--Timeline/ClientApp/src/app/todo-item/todo-item.component.html8
-rw-r--r--Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts27
-rw-r--r--Timeline/ClientApp/src/app/todo-item/todo-item.component.ts7
-rw-r--r--Timeline/ClientApp/src/app/todo-list-page/todo-list-color-block.css4
-rw-r--r--Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.html4
-rw-r--r--Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.spec.ts36
-rw-r--r--Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.ts4
-rw-r--r--Timeline/ClientApp/src/app/todo-list-page/todo-list.service.spec.ts87
-rw-r--r--Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts112
10 files changed, 95 insertions, 204 deletions
diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.css b/Timeline/ClientApp/src/app/todo-item/todo-item.component.css
index ef952a04..dcf25fd8 100644
--- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.css
+++ b/Timeline/ClientApp/src/app/todo-item/todo-item.component.css
@@ -4,18 +4,16 @@
overflow: hidden;
}
+.item-body-box {
+ margin: 5px!important
+}
+
.item-color-block {
width: 15px;
align-self: stretch;
flex: 0 0 auto;
}
-.item-icon {
- width: 1.2em;
- height: 1.2em;
- vertical-align: -0.25em;
-}
-
.item-title {
vertical-align: middle;
}
diff --git a/Timeline/ClientApp/src/app/todo-item/todo-item.component.html b/Timeline/ClientApp/src/app/todo-item/todo-item.component.html
index bf080e83..6f76e73b 100644
--- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.html
+++ b/Timeline/ClientApp/src/app/todo-item/todo-item.component.html
@@ -1,9 +1,7 @@
<mat-card class="mat-elevation-z2 item-card">
- <span class="item-color-block" [class.color-block-completed]="item.isCompleted" [class.color-block-resolving]="!item.isCompleted"></span>
- <!-- Do not move the margin style to class because there is some preset classes on mat-card children making it invalid. -->
- <div class="mat-h3 item-body-box" style="margin: 5px;">
- <img class="item-icon" [src]="item.iconUrl" />
- <span class="item-title">{{ item.id }}. {{ item.title }}</span>
+ <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>
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
index 277eca23..520b6136 100644
--- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts
+++ b/Timeline/ClientApp/src/app/todo-item/todo-item.component.spec.ts
@@ -1,7 +1,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TodoItemComponent } from './todo-item.component';
-import { WorkItem } from '../todo-list-page/todo-list.service';
+import { TodoItem } from '../todo-list-page/todo-list.service';
import { By } from '@angular/platform-browser';
import { NO_ERRORS_SCHEMA } from '@angular/core';
@@ -9,7 +9,12 @@ describe('TodoItemComponent', () => {
let component: TodoItemComponent;
let fixture: ComponentFixture<TodoItemComponent>;
- let mockWorkItem: WorkItem;
+ const mockTodoItem: TodoItem = {
+ number: 1,
+ title: 'Title',
+ isClosed: true,
+ detailUrl: '/detail',
+ };
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -19,17 +24,9 @@ describe('TodoItemComponent', () => {
}));
beforeEach(() => {
- mockWorkItem = {
- id: 0,
- title: 'Title',
- isCompleted: true,
- detailUrl: '/detail',
- iconUrl: '/icon'
- };
-
fixture = TestBed.createComponent(TodoItemComponent);
component = fixture.componentInstance;
- component.item = mockWorkItem;
+ component.item = mockTodoItem;
fixture.detectChanges();
});
@@ -37,17 +34,13 @@ describe('TodoItemComponent', () => {
expect(component).toBeTruthy();
});
- it('should set icon', () => {
- expect(fixture.debugElement.query(By.css('img.item-icon')).properties['src']).toBe(mockWorkItem.iconUrl);
- });
-
it('should set title', () => {
expect((fixture.debugElement.query(By.css('span.item-title')).nativeElement as HTMLSpanElement).textContent).toBe(
- mockWorkItem.id + '. ' + mockWorkItem.title
+ mockTodoItem.number + '. ' + mockTodoItem.title
);
});
it('should set detail link', () => {
- expect(fixture.debugElement.query(By.css('a.item-detail-button')).properties['href']).toBe(mockWorkItem.detailUrl);
+ 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
index 27d57e28..325812f1 100644
--- a/Timeline/ClientApp/src/app/todo-item/todo-item.component.ts
+++ b/Timeline/ClientApp/src/app/todo-item/todo-item.component.ts
@@ -1,5 +1,5 @@
-import { Component, OnInit, Input } from '@angular/core';
-import { WorkItem } from '../todo-list-page/todo-list.service';
+import { Component, Input } from '@angular/core';
+import { TodoItem } from '../todo-list-page/todo-list.service';
@Component({
selector: 'app-todo-item',
@@ -8,7 +8,6 @@ import { WorkItem } from '../todo-list-page/todo-list.service';
})
export class TodoItemComponent {
- @Input() item: WorkItem;
-
+ @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
index 456e477f..5e0d4ba9 100644
--- 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
@@ -1,7 +1,7 @@
-.color-block-resolving {
+.color-block-open {
background: red;
}
-.color-block-completed {
+.color-block-closed {
background: green;
}
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
index e8f5f30b..50180fe8 100644
--- 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
@@ -7,11 +7,11 @@
<div class="space"></div>
<div class="sample-box" *ngIf="i === 0">
<div class="mat-caption sample-item">
- <span class="sample-color-block color-block-resolving"></span>
+ <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-completed"></span>
+ <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>
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
index a757b2a5..5706bf51 100644
--- 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
@@ -4,7 +4,7 @@ import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core
import { Observable, from } from 'rxjs';
import { TodoListPageComponent } from './todo-list-page.component';
-import { TodoListService, WorkItem } from './todo-list.service';
+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';
@@ -24,29 +24,25 @@ describe('TodoListPageComponent', () => {
let component: TodoListPageComponent;
let fixture: ComponentFixture<TodoListPageComponent>;
- let mockWorkItems: WorkItem[];
+ 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']);
- mockWorkItems = [
- {
- id: 0,
- title: 'Test title 1',
- isCompleted: true,
- detailUrl: 'https://test.org/workitems/0',
- iconUrl: 'https://test.org/icon/0'
- },
- {
- id: 1,
- title: 'Test title 2',
- isCompleted: false,
- detailUrl: 'https://test.org/workitems/1',
- iconUrl: 'https://test.org/icon/1'
- }
- ];
-
- todoListService.getWorkItemList.and.returnValue(asyncArray(mockWorkItems));
+ todoListService.getWorkItemList.and.returnValue(asyncArray(mockTodoItems));
TestBed.configureTestingModule({
declarations: [TodoListPageComponent, MatProgressBarStubComponent],
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
index b04d1300..c62dd808 100644
--- 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
@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
-import { TodoListService, WorkItem } from './todo-list.service';
+import { TodoListService, TodoItem } from './todo-list.service';
import { trigger, transition, style, animate } from '@angular/animations';
@Component({
@@ -21,7 +21,7 @@ import { trigger, transition, style, animate } from '@angular/animations';
})
export class TodoListPageComponent implements OnInit {
- items: WorkItem[] = [];
+ items: TodoItem[] = [];
isLoadCompleted = false;
constructor(private todoService: TodoListService) {
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
index 49b7bbc4..a2ad0cbd 100644
--- 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
@@ -2,8 +2,7 @@ import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import {
- TodoListService, WorkItem, AzureDevOpsAccessInfo,
- WiqlResult, WiqlWorkItemResult, WorkItemResult, WorkItemTypeResult
+ TodoListService, IssueResponse, IssueResponseItem, TodoItem
} from './todo-list.service';
import { toArray } from 'rxjs/operators';
@@ -14,80 +13,42 @@ describe('TodoListServiceService', () => {
}));
it('should be created', () => {
-
const service: TodoListService = TestBed.get(TodoListService);
expect(service).toBeTruthy();
});
it('should work well', () => {
const service: TodoListService = TestBed.get(TodoListService);
- expect(service).toBeTruthy();
- const mockAccessInfo: AzureDevOpsAccessInfo = {
- username: 'testusername',
- personalAccessToken: 'testtoken',
- organization: 'testorganization',
- project: 'testproject'
- };
-
- const baseUrl = `https://dev.azure.com/${mockAccessInfo.organization}/${mockAccessInfo.project}/`;
-
- const mockWorkItems: WorkItem[] = Array.from({ length: 2 }, (_, i) => <WorkItem>{
- id: i,
- title: 'Test work item ' + i,
- isCompleted: i === 0,
- detailUrl: `${baseUrl}_workitems/edit/${i}/`,
- iconUrl: `${baseUrl}_api/wit/icon/${i}`,
- });
-
- const workItemTypeMap = new Map<WorkItem, string>(Array.from(mockWorkItems, v => <[WorkItem, string]>[v, 'type' + v.id]));
+ 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(mockWorkItems);
+ expect(data).toEqual(mockTodoItemList);
});
const httpController: HttpTestingController = TestBed.get(HttpTestingController);
- httpController.expectOne('/api/TodoPage/AzureDevOpsAccessInfo').flush(mockAccessInfo);
-
- const mockWiqlWorkItems: WiqlWorkItemResult[] = Array.from(mockWorkItems, v => <WiqlWorkItemResult>{
- id: v.id,
- url: `${baseUrl}_apis/wit/workItems/${v.id}`
- });
-
- const authorizationHeader = 'Basic ' + btoa(mockAccessInfo.username + ':' + mockAccessInfo.personalAccessToken);
-
- httpController.expectOne(req =>
- req.url === `${baseUrl}_apis/wit/wiql?api-version=5.0` &&
- req.headers.get('Authorization') === authorizationHeader
- ).flush(<WiqlResult>{ workItems: mockWiqlWorkItems });
-
- function mapWorkItemToResult(mockWorkItem: WorkItem): WorkItemResult {
- return {
- id: mockWorkItem.id,
- fields: {
- [TodoListService.titleFieldName]: mockWorkItem.title,
- [TodoListService.stateFieldName]: (mockWorkItem.isCompleted ? 'Closed' : 'Active'),
- [TodoListService.typeFieldName]: workItemTypeMap.get(mockWorkItem)
- }
- };
- }
-
- for (let i = 0; i < mockWorkItems.length; i++) {
- httpController.expectOne(req =>
- req.url === mockWiqlWorkItems[i].url &&
- req.headers.get('Authorization') === authorizationHeader
- ).flush(mapWorkItemToResult(mockWorkItems[i]));
-
- httpController.expectOne(req =>
- req.url === `${baseUrl}_apis/wit/workitemtypes/${encodeURIComponent(workItemTypeMap.get(mockWorkItems[i]))}?api-version=5.0` &&
- req.headers.get('Authorization') === authorizationHeader
- ).flush(<WorkItemTypeResult>{
- icon: {
- url: mockWorkItems[i].iconUrl
- }
- });
- }
+ 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
index bfeb3285..ffcbbc6f 100644
--- a/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts
+++ b/Timeline/ClientApp/src/app/todo-list-page/todo-list.service.ts
@@ -1,101 +1,47 @@
import { Injectable } from '@angular/core';
-import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { HttpClient } from '@angular/common/http';
import { Observable, from } from 'rxjs';
-import { switchMap, concatMap, map } from 'rxjs/operators';
+import { switchMap, map, filter } from 'rxjs/operators';
-export interface AzureDevOpsAccessInfo {
- username: string;
- personalAccessToken: string;
- organization: string;
- project: string;
-}
-
-export interface WiqlWorkItemResult {
- id: number;
- url: string;
-}
-
-export interface WiqlResult {
- workItems: WiqlWorkItemResult[];
-}
-
-export interface WorkItemResult {
- id: number;
- fields: { [name: string]: any };
+export interface IssueResponseItem {
+ number: number;
+ title: string;
+ state: string;
+ html_url: string;
+ pull_request?: any;
}
-export interface WorkItemTypeResult {
- icon: {
- url: string;
- };
-}
+export type IssueResponse = IssueResponseItem[];
-export interface WorkItem {
- id: number;
+export interface TodoItem {
+ number: number;
title: string;
- isCompleted: boolean;
+ isClosed: boolean;
detailUrl: string;
- iconUrl: string;
}
@Injectable({
providedIn: 'root'
})
export class TodoListService {
- public static titleFieldName = 'System.Title';
- public static stateFieldName = 'System.State';
- public static typeFieldName = 'System.WorkItemType';
-
- constructor(private client: HttpClient) {}
-
- private getAzureDevOpsAccessInfo(): Observable<AzureDevOpsAccessInfo> {
- return this.client.get<AzureDevOpsAccessInfo>('/api/TodoPage/AzureDevOpsAccessInfo');
- }
-
- private getItemIconUrl(baseUrl: string, headers: HttpHeaders, type: string): Observable<string> {
- return this.client
- .get<WorkItemTypeResult>(`${baseUrl}_apis/wit/workitemtypes/${encodeURIComponent(type)}?api-version=5.0`, {
- headers: headers
- })
- .pipe(map(result => result.icon.url));
- }
- getWorkItemList(): Observable<WorkItem> {
- return this.getAzureDevOpsAccessInfo().pipe(
- switchMap(accessInfo => {
- const baseUrl = `https://dev.azure.com/${accessInfo.organization}/${accessInfo.project}/`;
- const headers = new HttpHeaders({
- Accept: 'application/json',
- Authorization: `Basic ${btoa(accessInfo.username + ':' + accessInfo.personalAccessToken)}`
- });
- return this.client
- .post<WiqlResult>(
- `${baseUrl}_apis/wit/wiql?api-version=5.0`,
- {
- query: 'SELECT [System.Id] FROM workitems WHERE [System.TeamProject] = @project'
- },
- { headers: headers }
- )
- .pipe(
- concatMap(result => from(result.workItems)),
- concatMap(result => this.client.get<WorkItemResult>(result.url, { headers: headers })),
- concatMap(result =>
- this.getItemIconUrl(baseUrl, headers, result.fields[TodoListService.typeFieldName]).pipe(
- map(
- iconResult =>
- <WorkItem>{
- id: result.id,
- title: <string>result.fields[TodoListService.titleFieldName],
- isCompleted: (function(stateErasedCase: string): Boolean {
- return stateErasedCase === 'closed' || stateErasedCase === 'resolved';
- })((result.fields[TodoListService.stateFieldName] as string).toLowerCase()),
- detailUrl: `${baseUrl}_workitems/edit/${result.id}/`,
- iconUrl: iconResult
- }
- )
- )
- )
- );
+ 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
})
);
}