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;
+ }
}