* wip limit excludes blocked issues
* blocked for days is now visible * kanban issues are now clickable * docblock documentation added
This commit is contained in:
parent
e434523925
commit
6fc65e54d5
@ -28,6 +28,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private router: Router
|
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() {
|
public ngOnInit() {
|
||||||
let timer0 = TimerObservable.create(TIMER_DEPLOY_REFRESH, TIMER_JIRA_REFRESH);
|
let timer0 = TimerObservable.create(TIMER_DEPLOY_REFRESH, TIMER_JIRA_REFRESH);
|
||||||
this.selfUpdateCheckerTimer = timer0.subscribe(() => {
|
this.selfUpdateCheckerTimer = timer0.subscribe(() => {
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import {Title} from '@angular/platform-browser';
|
|||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
KanbanBoard
|
KanbanBoard,
|
||||||
|
KanbanService,
|
||||||
|
KanbanEntry,
|
||||||
} from "../shared";
|
} from "../shared";
|
||||||
import {KanbanService} from "../shared/kanban.service";
|
|
||||||
|
|
||||||
const WIP_LIMIT_INPROGRESS = 12;
|
const WIP_LIMIT_INPROGRESS = 12;
|
||||||
const WIP_LIMIT_VERIFICATION = 8;
|
const WIP_LIMIT_VERIFICATION = 8;
|
||||||
@ -22,6 +23,9 @@ export class KanbanBoardComponent implements OnInit {
|
|||||||
private kanbanService: KanbanService) {
|
private kanbanService: KanbanService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set page title, and handle preloaded kanbanBoard data
|
||||||
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.titleService.setTitle('TaurusXFT : Kanban board');
|
this.titleService.setTitle('TaurusXFT : Kanban board');
|
||||||
this.route.data.subscribe((data: { kanbanBoard: KanbanBoard }) => this.kanbanBoard = data.kanbanBoard);
|
this.route.data.subscribe((data: { kanbanBoard: KanbanBoard }) => this.kanbanBoard = data.kanbanBoard);
|
||||||
@ -35,15 +39,35 @@ export class KanbanBoardComponent implements OnInit {
|
|||||||
this.kanbanService.kanbanBoard = kanbanBoard;
|
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() {
|
get inprogressWipClass() {
|
||||||
return {
|
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() {
|
get verificationWipClass() {
|
||||||
return {
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,11 @@
|
|||||||
|
a {
|
||||||
|
color: #eeeeee !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #4183C4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.task-description {
|
.task-description {
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
line-height: 1.25em;
|
line-height: 1.25em;
|
||||||
|
|||||||
@ -4,12 +4,13 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="task-description">
|
<div class="task-description">
|
||||||
<ng-template [ngIf]="hasLabels(kanbanEntry)">
|
<ng-template [ngIf]="hasLabels(kanbanEntry)">
|
||||||
<a *ngFor="let label of kanbanEntry.labels" class="ui mini {{labelClass(label)}} right floated label">{{label}}</a>
|
<span *ngFor="let label of kanbanEntry.labels" class="ui mini {{labelClass(label)}} right floated label">{{label|uppercase|blockedDays:kanbanEntry.daysBlocked}}</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
<span *ngIf="wasBlocked(kanbanEntry)" class="ui mini {{labelClass('blocked')}} right floated label">{{kanbanEntry.daysBlocked}}D</span>
|
||||||
<div class="ui jira-avatar floated image">
|
<div class="ui jira-avatar floated image">
|
||||||
<img src="{{avatarUrl(kanbanEntry.assignee?.avatar)}}">
|
<img src="{{avatarUrl(kanbanEntry.assignee?.avatar)}}" [title]="kanbanEntry.assignee?.name">
|
||||||
</div>
|
</div>
|
||||||
<span [innerHTML]="kanbanEntry.summary|shortenText|priorityColor:kanbanEntry.issuePriorityIcon:kanbanEntry.worklog" [title]="kanbanEntry.summary"></span>
|
<a [href]="jiraHref(kanbanEntry)" target="_blank" [innerHTML]="kanbanEntry.summary|shortenText|priorityColor:kanbanEntry.issuePriorityIcon:kanbanEntry.worklog" [title]="kanbanEntry.summary"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import {Component, Input, OnInit} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
import {environment} from "../../../environments/environment";
|
import {environment} from "../../../environments/environment";
|
||||||
import {KanbanEntry} from "../shared/kanban-entry.model";
|
import {KanbanEntry} from "../shared";
|
||||||
|
|
||||||
const DEFAULT_AVATAR = '/assets/riddler.png';
|
const DEFAULT_AVATAR = '/assets/riddler.png';
|
||||||
|
const JIRA_BOARD_BASE_HREF = 'https://jirapducc.mo.ca.am.ericsson.se/browse/';
|
||||||
|
|
||||||
const labelColors = {
|
const labelColors = {
|
||||||
TSP: 'teal',
|
TSP: 'teal',
|
||||||
@ -19,22 +20,39 @@ const labelColors = {
|
|||||||
templateUrl: './kanban-entry-item.component.html',
|
templateUrl: './kanban-entry-item.component.html',
|
||||||
styleUrls: ['./kanban-entry-item.component.css']
|
styleUrls: ['./kanban-entry-item.component.css']
|
||||||
})
|
})
|
||||||
export class KanbanEntryItemComponent implements OnInit {
|
export class KanbanEntryItemComponent {
|
||||||
@Input() kanbanEntries: Array<KanbanEntry>;
|
@Input() kanbanEntries: Array<KanbanEntry>;
|
||||||
@Input() rowHeading: string = "";
|
@Input() rowHeading: string = "";
|
||||||
|
|
||||||
constructor() {}
|
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 {
|
public avatarUrl(avatarPath: string): string {
|
||||||
return environment.apiUri + ( avatarPath ? avatarPath : DEFAULT_AVATAR );
|
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 {
|
public hasLabels(entry: KanbanEntry): boolean {
|
||||||
return entry.labels.length > 0;
|
return entry.labels.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set label colors
|
||||||
|
*
|
||||||
|
* @param {string} label
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
public labelClass(label: string): string {
|
public labelClass(label: string): string {
|
||||||
try {
|
try {
|
||||||
return labelColors[label.toUpperCase()];
|
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) {
|
public entryClass(entry: KanbanEntry) {
|
||||||
return {
|
return {
|
||||||
'bottom-separator': entry.isLastOfPriority,
|
'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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { KanbanEntryItemComponent } from './kanban-entry-item/kanban-entry-item.
|
|||||||
import { PriorityColorPipe } from './shared/priority-color.pipe';
|
import { PriorityColorPipe } from './shared/priority-color.pipe';
|
||||||
import { ShortenTextPipe } from './shared/shorten-text.pipe';
|
import { ShortenTextPipe } from './shared/shorten-text.pipe';
|
||||||
import { SelfUpdaterService } from './shared/self-updater.service';
|
import { SelfUpdaterService } from './shared/self-updater.service';
|
||||||
|
import { BlockedDaysPipe } from './shared/blocked-days.pipe';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -20,6 +21,7 @@ import { SelfUpdaterService } from './shared/self-updater.service';
|
|||||||
KanbanEntryItemComponent,
|
KanbanEntryItemComponent,
|
||||||
PriorityColorPipe,
|
PriorityColorPipe,
|
||||||
ShortenTextPipe,
|
ShortenTextPipe,
|
||||||
|
BlockedDaysPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
KanbanService,
|
KanbanService,
|
||||||
|
|||||||
8
src/app/kanban/shared/blocked-days.pipe.spec.ts
Normal file
8
src/app/kanban/shared/blocked-days.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { BlockedDaysPipe } from './blocked-days.pipe';
|
||||||
|
|
||||||
|
describe('BlockedDaysPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new BlockedDaysPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
21
src/app/kanban/shared/blocked-days.pipe.ts
Normal file
21
src/app/kanban/shared/blocked-days.pipe.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -24,4 +24,5 @@ export class KanbanEntry {
|
|||||||
public answerCode: string;
|
public answerCode: string;
|
||||||
public isLastOfPriority: boolean;
|
public isLastOfPriority: boolean;
|
||||||
public worklog: number;
|
public worklog: number;
|
||||||
|
public daysBlocked: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,14 +17,28 @@ export class KanbanService {
|
|||||||
|
|
||||||
constructor(private httpService: Http) {}
|
constructor(private httpService: Http) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable instance to the kanban board api
|
||||||
|
*
|
||||||
|
* @returns {Observable<KanbanBoard>}
|
||||||
|
*/
|
||||||
public getList(): Observable<KanbanBoard> {
|
public getList(): Observable<KanbanBoard> {
|
||||||
return this.httpService.get(this.url).map(res => this.preprocessPriorities(<KanbanBoard>res.json()));
|
return this.httpService.get(this.url).map(res => this.preprocessPriorities(<KanbanBoard>res.json()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route preload resolver
|
||||||
|
*
|
||||||
|
* @param {ActivatedRouteSnapshot} route
|
||||||
|
* @returns {Promise<KanbanBoard>}
|
||||||
|
*/
|
||||||
public resolve(route: ActivatedRouteSnapshot): Promise<KanbanBoard> {
|
public resolve(route: ActivatedRouteSnapshot): Promise<KanbanBoard> {
|
||||||
return this.getList().toPromise().then(result => result ? result : false);
|
return this.getList().toPromise().then(result => result ? result : false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the board
|
||||||
|
*/
|
||||||
public reload() {
|
public reload() {
|
||||||
this.getList().subscribe(result => this.cachedKanbanBoard = result);
|
this.getList().subscribe(result => this.cachedKanbanBoard = result);
|
||||||
}
|
}
|
||||||
@ -37,6 +51,12 @@ export class KanbanService {
|
|||||||
this.cachedKanbanBoard = kanbanBoard;
|
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 {
|
private preprocessPriorities(kanbanBoard: KanbanBoard): KanbanBoard {
|
||||||
['inbox','inProgress','verification','done'].map(progress => {
|
['inbox','inProgress','verification','done'].map(progress => {
|
||||||
kanbanBoard[progress].map(entry => entry.isLastOfPriority = false);
|
kanbanBoard[progress].map(entry => entry.isLastOfPriority = false);
|
||||||
@ -45,12 +65,7 @@ export class KanbanService {
|
|||||||
'Major',
|
'Major',
|
||||||
'Critical',
|
'Critical',
|
||||||
'Blocker'].map(prio => {
|
'Blocker'].map(prio => {
|
||||||
let prioLastIndex = -1;
|
let prioLastIndex = kanbanBoard[progress].reduce((accumulator, value, idx) => value.issuePriority==prio ? idx : accumulator, -1);
|
||||||
kanbanBoard[progress].map( (entry, idx) => {
|
|
||||||
if(entry.issuePriority == prio) {
|
|
||||||
prioLastIndex = idx;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
kanbanBoard[progress][prioLastIndex].isLastOfPriority = true;
|
kanbanBoard[progress][prioLastIndex].isLastOfPriority = true;
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|||||||
@ -5,7 +5,15 @@ import {Pipe, PipeTransform} from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class PriorityColorPipe implements PipeTransform {
|
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;
|
let mhrMatch = /(\[(.*)mhr\])/ig;
|
||||||
value = value.replace(mhrMatch, (fullMatch: string, mhrMatched: string, hoursMatch: number) => {
|
value = value.replace(mhrMatch, (fullMatch: string, mhrMatched: string, hoursMatch: number) => {
|
||||||
return `<span class="match-mhr">[${worklog}/${hoursMatch} mhr] </span> `;
|
return `<span class="match-mhr">[${worklog}/${hoursMatch} mhr] </span> `;
|
||||||
|
|||||||
@ -10,6 +10,12 @@ export class SelfUpdaterService {
|
|||||||
private appRevision: number = 0;
|
private appRevision: number = 0;
|
||||||
private initFailed: boolean = false;
|
private initFailed: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load current revision data from the server on initialization
|
||||||
|
*
|
||||||
|
* @param {Http} httpService
|
||||||
|
* @param {Location} locationService
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private httpService: Http,
|
private httpService: Http,
|
||||||
private locationService: Location,
|
private locationService: Location,
|
||||||
@ -26,10 +32,17 @@ export class SelfUpdaterService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return observable instance to the installed version on the server
|
||||||
|
* @returns {Observable<number>}
|
||||||
|
*/
|
||||||
private getDeployedRevision(): Observable<number> {
|
private getDeployedRevision(): Observable<number> {
|
||||||
return this.httpService.get(this.locationService.prepareExternalUrl("/revision.json")).map(result => result.json());
|
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() {
|
public checkAndReloadIfNecessary() {
|
||||||
if (!this.initFailed) {
|
if (!this.initFailed) {
|
||||||
this.getDeployedRevision().subscribe(
|
this.getDeployedRevision().subscribe(
|
||||||
|
|||||||
@ -1,14 +1,21 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core';
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'shortenText'
|
name: 'shortenText'
|
||||||
})
|
})
|
||||||
export class ShortenTextPipe implements PipeTransform {
|
export class ShortenTextPipe implements PipeTransform {
|
||||||
|
|
||||||
transform(value: string, length: number = 120): any {
|
/**
|
||||||
return value.length > length
|
* Shorten long text, postfixing it with '...'
|
||||||
? (value.substring(0,length) + '...')
|
*
|
||||||
: value;
|
* @param {string} value
|
||||||
}
|
* @param {number} length
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
transform(value: string, length: number = 120): any {
|
||||||
|
return value.length > length
|
||||||
|
? (value.substring(0, length) + '...')
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user