From 6fc65e54d5426fac03691331eb0e7a2a8f463266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Danyi?= Date: Fri, 25 Aug 2017 11:32:29 +0200 Subject: [PATCH] * wip limit excludes blocked issues * blocked for days is now visible * kanban issues are now clickable * docblock documentation added --- src/app/app.component.ts | 7 +++ .../kanban-board/kanban-board.component.ts | 32 +++++++++-- .../kanban-entry-item.component.css | 8 +++ .../kanban-entry-item.component.html | 7 ++- .../kanban-entry-item.component.ts | 56 +++++++++++++++++-- src/app/kanban/kanban.module.ts | 2 + .../kanban/shared/blocked-days.pipe.spec.ts | 8 +++ src/app/kanban/shared/blocked-days.pipe.ts | 21 +++++++ src/app/kanban/shared/kanban-entry.model.ts | 1 + src/app/kanban/shared/kanban.service.ts | 27 +++++++-- src/app/kanban/shared/priority-color.pipe.ts | 10 +++- src/app/kanban/shared/self-updater.service.ts | 13 +++++ src/app/kanban/shared/shorten-text.pipe.ts | 21 ++++--- 13 files changed, 187 insertions(+), 26 deletions(-) create mode 100644 src/app/kanban/shared/blocked-days.pipe.spec.ts create mode 100644 src/app/kanban/shared/blocked-days.pipe.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index bd8215f..97face8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -28,6 +28,13 @@ export class AppComponent implements OnInit, OnDestroy { private router: Router ) {} + /** + * Initialize application timers: + * - selfUpdateCheckerTimer is used to see if there is a newer revision deployed on the server + * - reloadJiraIssueTimer is used to refresh the status of the jira board + * - pageSwitchTimer handles switching back and forth between page views + * @todo: pageSwitchTimer + */ public ngOnInit() { let timer0 = TimerObservable.create(TIMER_DEPLOY_REFRESH, TIMER_JIRA_REFRESH); this.selfUpdateCheckerTimer = timer0.subscribe(() => { diff --git a/src/app/kanban/kanban-board/kanban-board.component.ts b/src/app/kanban/kanban-board/kanban-board.component.ts index 03f36e9..0dc54e9 100644 --- a/src/app/kanban/kanban-board/kanban-board.component.ts +++ b/src/app/kanban/kanban-board/kanban-board.component.ts @@ -3,9 +3,10 @@ import {Title} from '@angular/platform-browser'; import {ActivatedRoute} from '@angular/router'; import { - KanbanBoard + KanbanBoard, + KanbanService, + KanbanEntry, } from "../shared"; -import {KanbanService} from "../shared/kanban.service"; const WIP_LIMIT_INPROGRESS = 12; const WIP_LIMIT_VERIFICATION = 8; @@ -22,6 +23,9 @@ export class KanbanBoardComponent implements OnInit { private kanbanService: KanbanService) { } + /** + * Set page title, and handle preloaded kanbanBoard data + */ ngOnInit() { this.titleService.setTitle('TaurusXFT : Kanban board'); this.route.data.subscribe((data: { kanbanBoard: KanbanBoard }) => this.kanbanBoard = data.kanbanBoard); @@ -35,15 +39,35 @@ export class KanbanBoardComponent implements OnInit { this.kanbanService.kanbanBoard = kanbanBoard; } + /** + * Set 'over-wip' class on inprogress row if its over the wip limit + * This excludes issues marked as BLOCKED with labels + * + * @returns {{over-wip: boolean}} + */ get inprogressWipClass() { return { - 'over-wip': this.kanbanBoard.inProgress.length > WIP_LIMIT_INPROGRESS, + 'over-wip': this.kanbanBoard.inProgress.filter( + (entry: KanbanEntry) => entry.labels.every( + label => label.toUpperCase() != 'BLOCKED' + ) + ).length > WIP_LIMIT_INPROGRESS, }; } + /** + * Set 'over-wip' class on verification row if its over the wip limit + * This excludes issues marked as BLOCKED with labels + * + * @returns {{over-wip: boolean}} + */ get verificationWipClass() { return { - 'over-wip': this.kanbanBoard.verification.length > WIP_LIMIT_VERIFICATION, + 'over-wip': this.kanbanBoard.verification.filter( + (entry: KanbanEntry) => entry.labels.every( + label => label.toUpperCase() != 'BLOCKED' + ) + ).length > WIP_LIMIT_VERIFICATION, }; } } diff --git a/src/app/kanban/kanban-entry-item/kanban-entry-item.component.css b/src/app/kanban/kanban-entry-item/kanban-entry-item.component.css index 03453d4..37b9079 100644 --- a/src/app/kanban/kanban-entry-item/kanban-entry-item.component.css +++ b/src/app/kanban/kanban-entry-item/kanban-entry-item.component.css @@ -1,3 +1,11 @@ +a { + color: #eeeeee !important; +} + +a:hover { + color: #4183C4 !important; +} + .task-description { font-size: 14pt; line-height: 1.25em; diff --git a/src/app/kanban/kanban-entry-item/kanban-entry-item.component.html b/src/app/kanban/kanban-entry-item/kanban-entry-item.component.html index 567d347..7bfbb3a 100644 --- a/src/app/kanban/kanban-entry-item/kanban-entry-item.component.html +++ b/src/app/kanban/kanban-entry-item/kanban-entry-item.component.html @@ -4,12 +4,13 @@
- {{label}} + {{label|uppercase|blockedDays:kanbanEntry.daysBlocked}} + {{kanbanEntry.daysBlocked}}D
- +
- +
diff --git a/src/app/kanban/kanban-entry-item/kanban-entry-item.component.ts b/src/app/kanban/kanban-entry-item/kanban-entry-item.component.ts index 5ff9373..ab0fdd0 100644 --- a/src/app/kanban/kanban-entry-item/kanban-entry-item.component.ts +++ b/src/app/kanban/kanban-entry-item/kanban-entry-item.component.ts @@ -1,9 +1,10 @@ -import {Component, Input, OnInit} from '@angular/core'; +import {Component, Input} from '@angular/core'; import {environment} from "../../../environments/environment"; -import {KanbanEntry} from "../shared/kanban-entry.model"; +import {KanbanEntry} from "../shared"; const DEFAULT_AVATAR = '/assets/riddler.png'; +const JIRA_BOARD_BASE_HREF = 'https://jirapducc.mo.ca.am.ericsson.se/browse/'; const labelColors = { TSP: 'teal', @@ -19,22 +20,39 @@ const labelColors = { templateUrl: './kanban-entry-item.component.html', styleUrls: ['./kanban-entry-item.component.css'] }) -export class KanbanEntryItemComponent implements OnInit { +export class KanbanEntryItemComponent { @Input() kanbanEntries: Array; @Input() rowHeading: string = ""; constructor() {} - ngOnInit() {} - + /** + * Returns the full url of the assignee avatar, + * or the default riddler avatar if there is no assignee + * + * @param {string} avatarPath + * @returns {string} + */ public avatarUrl(avatarPath: string): string { return environment.apiUri + ( avatarPath ? avatarPath : DEFAULT_AVATAR ); } + /** + * Returns true if issue has any labels attached + * + * @param {KanbanEntry} entry + * @returns {boolean} + */ public hasLabels(entry: KanbanEntry): boolean { return entry.labels.length > 0; } + /** + * Set label colors + * + * @param {string} label + * @returns {string} + */ public labelClass(label: string): string { try { return labelColors[label.toUpperCase()]; @@ -43,9 +61,37 @@ export class KanbanEntryItemComponent implements OnInit { } } + /** + * Add 'bottom-separator' class to every item that is the last of its priority type + * + * @param {KanbanEntry} entry + * @returns {{bottom-separator: boolean}} + */ public entryClass(entry: KanbanEntry) { return { 'bottom-separator': entry.isLastOfPriority, }; } + + /** + * Generate jira issue href + * + * @param {KanbanEntry} kanbanEntry + * @returns {string} + */ + public jiraHref(kanbanEntry: KanbanEntry): string { + return JIRA_BOARD_BASE_HREF + kanbanEntry.key; + } + + /** + * Returns true if issue has blocked days logged at any point of time, + * but is not in BLOCKED state any more. + * + * @param {KanbanEntry} kanbanEntry + * @returns {boolean} + */ + public wasBlocked(kanbanEntry: KanbanEntry): boolean { + return kanbanEntry.daysBlocked > 0 + && kanbanEntry.labels.every(label => label.toUpperCase() != 'BLOCKED'); + } } diff --git a/src/app/kanban/kanban.module.ts b/src/app/kanban/kanban.module.ts index 8da0a94..0d6a037 100644 --- a/src/app/kanban/kanban.module.ts +++ b/src/app/kanban/kanban.module.ts @@ -9,6 +9,7 @@ import { KanbanEntryItemComponent } from './kanban-entry-item/kanban-entry-item. import { PriorityColorPipe } from './shared/priority-color.pipe'; import { ShortenTextPipe } from './shared/shorten-text.pipe'; import { SelfUpdaterService } from './shared/self-updater.service'; +import { BlockedDaysPipe } from './shared/blocked-days.pipe'; @NgModule({ imports: [ @@ -20,6 +21,7 @@ import { SelfUpdaterService } from './shared/self-updater.service'; KanbanEntryItemComponent, PriorityColorPipe, ShortenTextPipe, + BlockedDaysPipe, ], providers: [ KanbanService, diff --git a/src/app/kanban/shared/blocked-days.pipe.spec.ts b/src/app/kanban/shared/blocked-days.pipe.spec.ts new file mode 100644 index 0000000..83e8e93 --- /dev/null +++ b/src/app/kanban/shared/blocked-days.pipe.spec.ts @@ -0,0 +1,8 @@ +import { BlockedDaysPipe } from './blocked-days.pipe'; + +describe('BlockedDaysPipe', () => { + it('create an instance', () => { + const pipe = new BlockedDaysPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/kanban/shared/blocked-days.pipe.ts b/src/app/kanban/shared/blocked-days.pipe.ts new file mode 100644 index 0000000..413a342 --- /dev/null +++ b/src/app/kanban/shared/blocked-days.pipe.ts @@ -0,0 +1,21 @@ +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({ + name: 'blockedDays' +}) +export class BlockedDaysPipe implements PipeTransform { + + /** + * Format the BLOCKED label, including days since the task is blocked + * + * @param {string} value + * @param {number} days + * @returns {any} + */ + transform(value: string, days: number): any { + return ( value.toUpperCase() == 'BLOCKED' && days > 0 ) + ? `${value} - ${days}D` + : value; + } + +} diff --git a/src/app/kanban/shared/kanban-entry.model.ts b/src/app/kanban/shared/kanban-entry.model.ts index b5d5eb4..5a61082 100644 --- a/src/app/kanban/shared/kanban-entry.model.ts +++ b/src/app/kanban/shared/kanban-entry.model.ts @@ -24,4 +24,5 @@ export class KanbanEntry { public answerCode: string; public isLastOfPriority: boolean; public worklog: number; + public daysBlocked: number; } diff --git a/src/app/kanban/shared/kanban.service.ts b/src/app/kanban/shared/kanban.service.ts index 9ededfa..3650faf 100644 --- a/src/app/kanban/shared/kanban.service.ts +++ b/src/app/kanban/shared/kanban.service.ts @@ -17,14 +17,28 @@ export class KanbanService { constructor(private httpService: Http) {} + /** + * Returns an observable instance to the kanban board api + * + * @returns {Observable} + */ public getList(): Observable { return this.httpService.get(this.url).map(res => this.preprocessPriorities(res.json())); } + /** + * Route preload resolver + * + * @param {ActivatedRouteSnapshot} route + * @returns {Promise} + */ public resolve(route: ActivatedRouteSnapshot): Promise { return this.getList().toPromise().then(result => result ? result : false); } + /** + * Reload the board + */ public reload() { this.getList().subscribe(result => this.cachedKanbanBoard = result); } @@ -37,6 +51,12 @@ export class KanbanService { this.cachedKanbanBoard = kanbanBoard; } + /** + * Used to preprocess all the entries, mark last item of every priority type for display formatting + * + * @param {KanbanBoard} kanbanBoard + * @returns {KanbanBoard} + */ private preprocessPriorities(kanbanBoard: KanbanBoard): KanbanBoard { ['inbox','inProgress','verification','done'].map(progress => { kanbanBoard[progress].map(entry => entry.isLastOfPriority = false); @@ -45,12 +65,7 @@ export class KanbanService { 'Major', 'Critical', 'Blocker'].map(prio => { - let prioLastIndex = -1; - kanbanBoard[progress].map( (entry, idx) => { - if(entry.issuePriority == prio) { - prioLastIndex = idx; - } - }); + let prioLastIndex = kanbanBoard[progress].reduce((accumulator, value, idx) => value.issuePriority==prio ? idx : accumulator, -1); try { kanbanBoard[progress][prioLastIndex].isLastOfPriority = true; } catch(e) {} diff --git a/src/app/kanban/shared/priority-color.pipe.ts b/src/app/kanban/shared/priority-color.pipe.ts index e365cb6..d4452e1 100644 --- a/src/app/kanban/shared/priority-color.pipe.ts +++ b/src/app/kanban/shared/priority-color.pipe.ts @@ -5,7 +5,15 @@ import {Pipe, PipeTransform} from '@angular/core'; }) export class PriorityColorPipe implements PipeTransform { - transform(value: any, prioIcon: string = "", worklog: number = 0): any { + /** + * Format the []-tags in the issue summary + * + * @param value + * @param {string} prioIcon + * @param {number} worklog + * @returns {string} + */ + transform(value: string, prioIcon: string = "", worklog: number = 0): string { let mhrMatch = /(\[(.*)mhr\])/ig; value = value.replace(mhrMatch, (fullMatch: string, mhrMatched: string, hoursMatch: number) => { return `[${worklog}/${hoursMatch} mhr] `; diff --git a/src/app/kanban/shared/self-updater.service.ts b/src/app/kanban/shared/self-updater.service.ts index a3bdda0..5cf5cd1 100644 --- a/src/app/kanban/shared/self-updater.service.ts +++ b/src/app/kanban/shared/self-updater.service.ts @@ -10,6 +10,12 @@ export class SelfUpdaterService { private appRevision: number = 0; private initFailed: boolean = false; + /** + * Load current revision data from the server on initialization + * + * @param {Http} httpService + * @param {Location} locationService + */ constructor( private httpService: Http, private locationService: Location, @@ -26,10 +32,17 @@ export class SelfUpdaterService { ); } + /** + * Return observable instance to the installed version on the server + * @returns {Observable} + */ private getDeployedRevision(): Observable { return this.httpService.get(this.locationService.prepareExternalUrl("/revision.json")).map(result => result.json()); } + /** + * Reload the application if the server revision is newer than the current running revision + */ public checkAndReloadIfNecessary() { if (!this.initFailed) { this.getDeployedRevision().subscribe( diff --git a/src/app/kanban/shared/shorten-text.pipe.ts b/src/app/kanban/shared/shorten-text.pipe.ts index e36b585..6e86d7d 100644 --- a/src/app/kanban/shared/shorten-text.pipe.ts +++ b/src/app/kanban/shared/shorten-text.pipe.ts @@ -1,14 +1,21 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ - name: 'shortenText' + name: 'shortenText' }) export class ShortenTextPipe implements PipeTransform { - transform(value: string, length: number = 120): any { - return value.length > length - ? (value.substring(0,length) + '...') - : value; - } + /** + * Shorten long text, postfixing it with '...' + * + * @param {string} value + * @param {number} length + * @returns {any} + */ + transform(value: string, length: number = 120): any { + return value.length > length + ? (value.substring(0, length) + '...') + : value; + } }