diff options
author | 杨宇千 <crupest@outlook.com> | 2019-02-23 15:56:23 +0000 |
---|---|---|
committer | 杨宇千 <crupest@outlook.com> | 2019-02-23 15:56:23 +0000 |
commit | 77afb5d67a8bf7d34cae74f29549d85d327ea4a7 (patch) | |
tree | de7a451c3da77ab453043468fcc169ad25025a79 /Timeline/ClientApp/src/app/todo-list-page | |
parent | 5189c65192874c0ee7a69449941056294ecd8517 (diff) | |
parent | aaafdb323fddfa4654117b84b371ba49de3dd433 (diff) | |
download | timeline-77afb5d67a8bf7d34cae74f29549d85d327ea4a7.tar.gz timeline-77afb5d67a8bf7d34cae74f29549d85d327ea4a7.tar.bz2 timeline-77afb5d67a8bf7d34cae74f29549d85d327ea4a7.zip |
Merged PR 6: Add icon of todo list and redesign it.
Related work items: #1, #3
Diffstat (limited to 'Timeline/ClientApp/src/app/todo-list-page')
7 files changed, 80 insertions, 66 deletions
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 new file mode 100644 index 00000000..5e0d4ba9 --- /dev/null +++ b/Timeline/ClientApp/src/app/todo-list-page/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-list-page/todo-list-page.component.css b/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css index 89a0f0ce..754b786e 100644 --- a/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css +++ b/Timeline/ClientApp/src/app/todo-list-page/todo-list-page.component.css @@ -5,34 +5,20 @@ .item-box { display: flex; width: 100%; - padding: 5px; box-sizing: border-box; } .first-item-box { justify-content: space-between; + padding: 0 0 5px 5px; } -.item-id { - display: inline-block; - text-align: center; - border-radius: 0.2rem; - width: 1.2rem; - height: 1.2rem; -} - -.item-id-open { - background: red; -} - -.item-id-closed { - background: green; +.non-first-item-box { + padding: 5px; } -.item-title { - margin-left: 5px; - color: black; - text-decoration: none; +.space { + flex: 1 4 20px; } .sample-box { @@ -40,12 +26,12 @@ align-self: flex-start; } -.sample-container { +.sample-item { display: flex; align-items: center; } -.item-id-sample { +.sample-color-block { border-radius: 0.2em; width: 1em; height: 1em; 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 4e1aa2f1..3b4809ae 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 @@ -2,18 +2,17 @@ <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"> - <mat-card class="mat-elevation-z2" [class.align-self-bottom]="i === 0"> - <span class="item-id" [class.item-id-closed]="item.closed" - [class.item-id-open]="!item.closed">{{item.id}}</span> - <a class="mat-h3 item-title" [href]="item.detailUrl">{{item.title}}</a> - </mat-card> + <div class="item-box" [class.first-item-box]="i === 0" [class.non-first-item-box]="i !== 0"> + <app-todo-item [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-container"> - <span class="item-id-sample item-id-open"></span> means working now. + <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-container"> - <span class="item-id-sample item-id-closed"></span> means completed. + <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> 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 2da74002..9543f7ad 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 @@ -30,9 +30,9 @@ describe('TodoListPageComponent', () => { const todoListService: jasmine.SpyObj<TodoListService> = jasmine.createSpyObj('TodoListService', ['getWorkItemList']); mockWorkItems = [{ - id: 0, title: 'Test title 1', closed: true, detailUrl: 'https://test.org/workitems/0' + id: 0, title: 'Test title 1', closed: true, detailUrl: 'https://test.org/workitems/0', iconUrl: 'https://test.org/icon/0' }, { - id: 1, title: 'Test title 2', closed: false, detailUrl: 'https://test.org/workitems/1' + id: 1, title: 'Test title 2', closed: false, detailUrl: 'https://test.org/workitems/1', iconUrl: 'https://test.org/icon/1' }]; todoListService.getWorkItemList.and.returnValue(asyncData(mockWorkItems)); 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 06f49923..e58cca7d 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 @@ -4,7 +4,7 @@ import { TodoListService, WorkItem } from './todo-list.service'; @Component({ selector: 'app-todo-list-page', templateUrl: './todo-list-page.component.html', - styleUrls: ['./todo-list-page.component.css'] + styleUrls: ['./todo-list-page.component.css', './todo-list-color-block.css'] }) export class TodoListPageComponent implements OnInit { 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 3ed4e004..13bae5be 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 @@ -1,7 +1,10 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TodoListService, WorkItem, AzureDevOpsAccessInfo, WiqlResult, WiqlWorkItemResult, WorkItemResult } from './todo-list.service'; +import { + TodoListService, WorkItem, AzureDevOpsAccessInfo, + WiqlResult, WiqlWorkItemResult, WorkItemResult, WorkItemTypeResult +} from './todo-list.service'; describe('TodoListServiceService', () => { @@ -26,20 +29,17 @@ describe('TodoListServiceService', () => { project: 'testproject' }; - const generateDetailUrl = (id: number) => - `https://dev.azure.com/${mockAccessInfo.organization}/${mockAccessInfo.project}/_workitems/edit/${id}/`; - - const mockWorkItems: WorkItem[] = [{ - id: 0, - title: 'Test work item 1', - closed: true, - detailUrl: generateDetailUrl(0) - }, { - id: 1, - title: 'Test work item 2', - closed: false, - detailUrl: generateDetailUrl(1) - }]; + 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, + closed: 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])); service.getWorkItemList().subscribe(data => { expect(data).toEqual(mockWorkItems); @@ -47,31 +47,27 @@ describe('TodoListServiceService', () => { const httpController: HttpTestingController = TestBed.get(HttpTestingController); - - httpController.expectOne('/api/TodoPage/AzureDevOpsAccessInfo').flush(mockAccessInfo); - const mockWiqlWorkItems: WiqlWorkItemResult[] = [{ - id: 0, - url: `https://dev.azure.com/${mockAccessInfo.organization}/${mockAccessInfo.project}/_apis/wit/workItems/0` - }, { - id: 1, - url: `https://dev.azure.com/${mockAccessInfo.organization}/${mockAccessInfo.project}/_apis/wit/workItems/1` - }]; + 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 === `https://dev.azure.com/${mockAccessInfo.organization}/${mockAccessInfo.project}/_apis/wit/wiql?api-version=5.0` && + req.url === `${baseUrl}_apis/wit/wiql?api-version=5.0` && req.headers.get('Authorization') === authorizationHeader ).flush(<WiqlResult>{ workItems: mockWiqlWorkItems }); - function mapWorkItemToResult(workItem: WorkItem): WorkItemResult { + function mapWorkItemToResult(mockWorkItem: WorkItem): WorkItemResult { return { - id: workItem.id, + id: mockWorkItem.id, fields: { - [TodoListService.titleFieldName]: workItem.title, - [TodoListService.stateFieldName]: (workItem.closed ? 'Closed' : 'Active') + [TodoListService.titleFieldName]: mockWorkItem.title, + [TodoListService.stateFieldName]: (mockWorkItem.closed ? 'Closed' : 'Active'), + [TodoListService.typeFieldName]: workItemTypeMap.get(mockWorkItem) } }; } @@ -81,6 +77,15 @@ describe('TodoListServiceService', () => { 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 + } + }); } }); }); 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 ed28bc59..17ded67b 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 @@ -24,12 +24,18 @@ export interface WorkItemResult { fields: { [name: string]: any }; } +export interface WorkItemTypeResult { + icon: { + url: string + }; +} export interface WorkItem { id: number; title: string; closed: boolean; detailUrl: string; + iconUrl: string; } @Injectable({ @@ -39,6 +45,7 @@ export class TodoListService { public static titleFieldName = 'System.Title'; public static stateFieldName = 'System.State'; + public static typeFieldName = 'System.WorkItemType'; constructor(private client: HttpClient) { } @@ -46,6 +53,14 @@ export class TodoListService { 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( @@ -61,12 +76,14 @@ export class TodoListService { }, { headers: headers }).pipe( switchMap(result => result.workItems), concatMap(result => this.client.get<WorkItemResult>(result.url, { headers: headers })), - map(result => <WorkItem>{ + concatMap(result => this.getItemIconUrl(baseUrl, headers, result.fields[TodoListService.typeFieldName]).pipe( + map(iconResult => <WorkItem>{ id: result.id, title: <string>result.fields[TodoListService.titleFieldName], closed: ((<string>result.fields[TodoListService.stateFieldName]).toLowerCase() === 'closed'), - detailUrl: `${baseUrl}_workitems/edit/${result.id}/` - }), + detailUrl: `${baseUrl}_workitems/edit/${result.id}/`, + iconUrl: iconResult + }))), toArray() ); } |