diff options
author | crupest <crupest@outlook.com> | 2019-03-04 19:58:48 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2019-03-04 19:58:48 +0800 |
commit | 1715b3a618ddffc28177e58ae21c91b30d586ccf (patch) | |
tree | 3bef8b64aea0792c3b415fa9236837afd1a2577e /Timeline/ClientApp/src | |
parent | 80343fab6624f74f0777968dd213c3a26e65890d (diff) | |
parent | 8033d6523885486c24af2bdd57a24b0fd62d0b00 (diff) | |
download | timeline-1715b3a618ddffc28177e58ae21c91b30d586ccf.tar.gz timeline-1715b3a618ddffc28177e58ae21c91b30d586ccf.tar.bz2 timeline-1715b3a618ddffc28177e58ae21c91b30d586ccf.zip |
Merge branch 'master' into user
Diffstat (limited to 'Timeline/ClientApp/src')
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 }) ); } |