* kanban view
This commit is contained in:
parent
f72521ae82
commit
ad20a35da4
@ -7,6 +7,8 @@ import { SettingsComponent } from './settings/settings.component';
|
|||||||
import { TeamService } from '../shared/service/team.service';
|
import { TeamService } from '../shared/service/team.service';
|
||||||
import { SlideShowComponent } from './slide-show/slide-show.component';
|
import { SlideShowComponent } from './slide-show/slide-show.component';
|
||||||
import { SlideResolverService } from '../admin/slide-resolver.service';
|
import { SlideResolverService } from '../admin/slide-resolver.service';
|
||||||
|
import { KanbanBoardComponent } from './kanban-board/kanban-board.component';
|
||||||
|
import { KanbanService } from './shared';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -49,6 +51,16 @@ const routes: Routes = [
|
|||||||
data: {
|
data: {
|
||||||
autoSwitchable: true
|
autoSwitchable: true
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
path: 'kanban',
|
||||||
|
component: KanbanBoardComponent,
|
||||||
|
// canActivate: [AuthGuardService, RoleGuardService],
|
||||||
|
resolve: {
|
||||||
|
kanbanBoard: KanbanService,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
autoSwitchable: false
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
|
|||||||
@ -9,6 +9,13 @@ import { SuiModule } from 'ng2-semantic-ui';
|
|||||||
import { SlideComponent } from './slide/slide.component';
|
import { SlideComponent } from './slide/slide.component';
|
||||||
import { SlideShowComponent } from './slide-show/slide-show.component';
|
import { SlideShowComponent } from './slide-show/slide-show.component';
|
||||||
import { SlideShowService } from './slide-show.service';
|
import { SlideShowService } from './slide-show.service';
|
||||||
|
import { KanbanBoardComponent } from './kanban-board/kanban-board.component';
|
||||||
|
import { KanbanEntryItemComponent } from './kanban-entry-item/kanban-entry-item.component';
|
||||||
|
import { BlockedDaysPipe } from './shared/blocked-days.pipe';
|
||||||
|
import { PrefixJiraIdPipe } from './shared/prefix-jira-id.pipe';
|
||||||
|
import { PriorityColorPipe } from './shared/priority-color.pipe';
|
||||||
|
import { ShortenTextPipe } from './shared/shorten-text.pipe';
|
||||||
|
import { KanbanService } from './shared';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -18,7 +25,22 @@ import { SlideShowService } from './slide-show.service';
|
|||||||
DisplayRoutingModule
|
DisplayRoutingModule
|
||||||
],
|
],
|
||||||
exports: [SlideComponent],
|
exports: [SlideComponent],
|
||||||
declarations: [CommitTrackerComponent, SettingsComponent, SlideComponent, SlideShowComponent],
|
declarations: [
|
||||||
providers: [SlideShowService]
|
CommitTrackerComponent,
|
||||||
|
SettingsComponent,
|
||||||
|
SlideComponent,
|
||||||
|
SlideShowComponent,
|
||||||
|
KanbanBoardComponent,
|
||||||
|
KanbanEntryItemComponent,
|
||||||
|
|
||||||
|
BlockedDaysPipe,
|
||||||
|
PrefixJiraIdPipe,
|
||||||
|
PriorityColorPipe,
|
||||||
|
ShortenTextPipe,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
SlideShowService,
|
||||||
|
KanbanService,
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class DisplayModule { }
|
export class DisplayModule { }
|
||||||
|
|||||||
15
src/app/display/kanban-board/kanban-board.component.css
Normal file
15
src/app/display/kanban-board/kanban-board.component.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #444;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.over-wip {
|
||||||
|
background-color: rgba(194, 59, 34, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
18
src/app/display/kanban-board/kanban-board.component.html
Normal file
18
src/app/display/kanban-board/kanban-board.component.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="ui main fullwide-container">
|
||||||
|
<div class="ui grid">
|
||||||
|
<div app-kanban-entry-item class="four wide column"
|
||||||
|
rowHeading="INBOX"
|
||||||
|
[kanbanEntries]="kanbanBoard.inbox"></div>
|
||||||
|
<div app-kanban-entry-item class="four wide column" [ngClass]="inprogressWipClass"
|
||||||
|
rowHeading="INPROGRESS"
|
||||||
|
[wipLimit]="inprogressWipLimit" [wipCount]="inprogressWipCount"
|
||||||
|
[kanbanEntries]="kanbanBoard.inProgress"></div>
|
||||||
|
<div app-kanban-entry-item class="four wide column" [ngClass]="verificationWipClass"
|
||||||
|
rowHeading="VERIFICATION"
|
||||||
|
[wipLimit]="verificationWipLimit" [wipCount]="verificationWipCount"
|
||||||
|
[kanbanEntries]="kanbanBoard.verification"></div>
|
||||||
|
<div app-kanban-entry-item class="four wide column"
|
||||||
|
rowHeading="DÖNER"
|
||||||
|
[kanbanEntries]="kanbanBoard.done"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
25
src/app/display/kanban-board/kanban-board.component.spec.ts
Normal file
25
src/app/display/kanban-board/kanban-board.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KanbanBoardComponent } from './kanban-board.component';
|
||||||
|
|
||||||
|
describe('KanbanBoardComponent', () => {
|
||||||
|
let component: KanbanBoardComponent;
|
||||||
|
let fixture: ComponentFixture<KanbanBoardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ KanbanBoardComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(KanbanBoardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
104
src/app/display/kanban-board/kanban-board.component.ts
Normal file
104
src/app/display/kanban-board/kanban-board.component.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { Component, HostBinding, HostListener, OnInit } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { KanbanBoard, KanbanEntry, KanbanService, } from '../shared';
|
||||||
|
|
||||||
|
const WIP_LIMIT_INPROGRESS = 12;
|
||||||
|
const WIP_LIMIT_VERIFICATION = 8;
|
||||||
|
|
||||||
|
const STYLE_HIDDEN = 'hidden';
|
||||||
|
const STYLE_VISIBLE = 'scroll';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-kanban-board',
|
||||||
|
templateUrl: './kanban-board.component.html',
|
||||||
|
styleUrls: ['./kanban-board.component.css']
|
||||||
|
})
|
||||||
|
export class KanbanBoardComponent implements OnInit {
|
||||||
|
|
||||||
|
@HostBinding('style.overflow') hostOverflow = STYLE_HIDDEN;
|
||||||
|
|
||||||
|
constructor(private titleService: Title,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get kanbanBoard(): KanbanBoard {
|
||||||
|
return this.kanbanService.kanbanBoard;
|
||||||
|
}
|
||||||
|
|
||||||
|
set kanbanBoard(kanbanBoard: KanbanBoard) {
|
||||||
|
this.kanbanService.kanbanBoard = kanbanBoard;
|
||||||
|
}
|
||||||
|
|
||||||
|
get inprogressWipLimit(): number {
|
||||||
|
return WIP_LIMIT_INPROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
get inprogressWipCount(): number {
|
||||||
|
return this.kanbanBoard.inProgress.filter(
|
||||||
|
(entry: KanbanEntry) => entry.labels.every(
|
||||||
|
label => label.toUpperCase() !== 'BLOCKED'
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.inprogressWipCount > WIP_LIMIT_INPROGRESS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get verificationWipLimit(): number {
|
||||||
|
return WIP_LIMIT_VERIFICATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
get verificationWipCount(): number {
|
||||||
|
return this.kanbanBoard.verification.filter(
|
||||||
|
(entry: KanbanEntry) => entry.labels.every(
|
||||||
|
label => label.toUpperCase() !== 'BLOCKED'
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.verificationWipCount > WIP_LIMIT_VERIFICATION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// @HostListener('mouseover')
|
||||||
|
// private onMouseOver() {
|
||||||
|
// this.hostOverflow = STYLE_VISIBLE;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @HostListener('mouseout')
|
||||||
|
// private onMouseOut() {
|
||||||
|
// this.hostOverflow = STYLE_HIDDEN;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
h1 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #eeeeee !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #4183C4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-description {
|
||||||
|
font-size: 14pt;
|
||||||
|
line-height: 1.25em;
|
||||||
|
/*text-align: justify;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.jira-avatar.image {
|
||||||
|
width: 45px;
|
||||||
|
height: auto;
|
||||||
|
/*font-size: 1em;*/
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.jira-avatar.image > img {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.divided.items > .item:last-child {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.divided.items > .item {
|
||||||
|
border-bottom: 1px solid rgba(250, 250, 250, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.label {
|
||||||
|
opacity: 0.85;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.divided.items > .item.blocker.bottom-separator {
|
||||||
|
border-bottom: 3px double rgba(219, 40, 40, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.divided.items > .item.critical.bottom-separator {
|
||||||
|
border-bottom: 3px double rgba(242, 113, 28, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.divided.items > .item.major.bottom-separator {
|
||||||
|
border-bottom: 3px double rgba(181, 204, 24, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.divided.items > .item.minor.bottom-separator {
|
||||||
|
border-bottom: 3px double rgba(0, 181, 173, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Nothing below trivial, no separator needed*/
|
||||||
|
/*.ui.divided.items > .item.trivial.bottom-separator {*/
|
||||||
|
/*border-bottom: 1px solid rgba(181, 204, 24, 0.5);*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
.ui.images {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.images .image {
|
||||||
|
margin: 0;
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2 images */
|
||||||
|
.ui.two.images .image {
|
||||||
|
width: 22px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3 images */
|
||||||
|
.ui.three.images .image {
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
.ui.three.images .image:first-child {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.ui.three.images .image:not(:first-child) {
|
||||||
|
width: 22px;
|
||||||
|
margin-top: -14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4 images */
|
||||||
|
.ui.four.images .image {
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
<h1>
|
||||||
|
{{rowHeading}}
|
||||||
|
<ng-template [ngIf]="wipLimit">
|
||||||
|
- {{wipCount}}/{{wipLimit}}
|
||||||
|
</ng-template>
|
||||||
|
</h1>
|
||||||
|
<div class="ui divided items">
|
||||||
|
<div *ngFor="let kanbanEntry of kanbanEntries"
|
||||||
|
class="item {{kanbanEntry.issuePriority|lowercase}}"
|
||||||
|
[ngClass]="entryClass(kanbanEntry)">
|
||||||
|
<div class="content">
|
||||||
|
<div class="task-description">
|
||||||
|
<ng-template [ngIf]="hasLabels(kanbanEntry)">
|
||||||
|
<span *ngFor="let label of kanbanEntry.labels"
|
||||||
|
class="ui mini {{labelClass(label)}} right floated label">{{label|uppercase|blockedDays:kanbanEntry.daysBlocked}}</span>
|
||||||
|
</ng-template>
|
||||||
|
<span *ngIf="wasBlocked(kanbanEntry)" class="ui mini {{labelClass('blocked')}} right floated label">{{kanbanEntry.daysBlocked}}D</span>
|
||||||
|
<div *ngIf="!hasMultiAssignee(kanbanEntry)" class="ui jira-avatar floated image">
|
||||||
|
<img src="{{avatarUrl(kanbanEntry.assignee?.avatar)}}" [title]="kanbanEntry.assignee?.name">
|
||||||
|
</div>
|
||||||
|
<div *ngIf="hasMultiAssignee(kanbanEntry)" class="ui jira-avatar floated {{assigneeCount(kanbanEntry)}} images">
|
||||||
|
<img *ngFor="let assignee of getAssignees(kanbanEntry)" class="image"
|
||||||
|
src="{{avatarUrl(assignee?.avatar)}}"
|
||||||
|
[title]="assignee?.name">
|
||||||
|
</div>
|
||||||
|
<a [href]="jiraHref(kanbanEntry)" target="_blank"
|
||||||
|
[innerHTML]="kanbanEntry.summary|shortenText|priorityColor:kanbanEntry.issuePriorityIcon:kanbanEntry.worklog|prefixJiraId:kanbanEntry.key"
|
||||||
|
[title]="kanbanEntry.summary"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KanbanEntryItemComponent } from './kanban-entry-item.component';
|
||||||
|
|
||||||
|
describe('KanbanEntryItemComponent', () => {
|
||||||
|
let component: KanbanEntryItemComponent;
|
||||||
|
let fixture: ComponentFixture<KanbanEntryItemComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ KanbanEntryItemComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(KanbanEntryItemComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
122
src/app/display/kanban-entry-item/kanban-entry-item.component.ts
Normal file
122
src/app/display/kanban-entry-item/kanban-entry-item.component.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { JiraAssignee, 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',
|
||||||
|
MTAS: 'orange',
|
||||||
|
INTERNAL: 'yellow',
|
||||||
|
TEAM: 'yellow',
|
||||||
|
BLOCKED: 'red',
|
||||||
|
SPIKE: 'purple',
|
||||||
|
EXPEDITE: 'pink',
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-kanban-entry-item,[app-kanban-entry-item]',
|
||||||
|
templateUrl: './kanban-entry-item.component.html',
|
||||||
|
styleUrls: ['./kanban-entry-item.component.css']
|
||||||
|
})
|
||||||
|
export class KanbanEntryItemComponent {
|
||||||
|
@Input() kanbanEntries: Array<KanbanEntry>;
|
||||||
|
@Input() rowHeading = '';
|
||||||
|
@Input() wipLimit = 0;
|
||||||
|
@Input() wipCount = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.apiUrl + (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()];
|
||||||
|
} catch (e) {
|
||||||
|
return 'white';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasMultiAssignee(kanbanEntry: KanbanEntry): boolean {
|
||||||
|
return kanbanEntry.additionalAssignees.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public assigneeCount(kanbanEntry: KanbanEntry): string {
|
||||||
|
const count = 1 + kanbanEntry.additionalAssignees.length;
|
||||||
|
switch (count) {
|
||||||
|
case 2:
|
||||||
|
return 'two';
|
||||||
|
case 3:
|
||||||
|
return 'three';
|
||||||
|
case 4:
|
||||||
|
default:
|
||||||
|
return 'four';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAssignees(kanbanEntry: KanbanEntry): Array<JiraAssignee> {
|
||||||
|
return [].concat([kanbanEntry.assignee], kanbanEntry.additionalAssignees);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/app/display/shared/blocked-days.pipe.spec.ts
Normal file
8
src/app/display/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/display/shared/blocked-days.pipe.ts
Normal file
21
src/app/display/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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
6
src/app/display/shared/index.ts
Normal file
6
src/app/display/shared/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './jira-assignee.model';
|
||||||
|
export * from './jira-issue-type.model';
|
||||||
|
export * from './jira-status.model';
|
||||||
|
export * from './kanban-board.model';
|
||||||
|
export * from './kanban-entry.model';
|
||||||
|
export * from './kanban.service';
|
||||||
7
src/app/display/shared/jira-assignee.model.ts
Normal file
7
src/app/display/shared/jira-assignee.model.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export class JiraAssignee {
|
||||||
|
public signum: string;
|
||||||
|
public name: string;
|
||||||
|
public email: string;
|
||||||
|
public avatar: string;
|
||||||
|
public active: boolean;
|
||||||
|
}
|
||||||
5
src/app/display/shared/jira-issue-type.model.ts
Normal file
5
src/app/display/shared/jira-issue-type.model.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class JiraIssueType {
|
||||||
|
public name: string;
|
||||||
|
public description: string;
|
||||||
|
public icon: string;
|
||||||
|
}
|
||||||
4
src/app/display/shared/jira-status.model.ts
Normal file
4
src/app/display/shared/jira-status.model.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export class JiraStatus {
|
||||||
|
public name: string;
|
||||||
|
public color: string;
|
||||||
|
}
|
||||||
8
src/app/display/shared/kanban-board.model.ts
Normal file
8
src/app/display/shared/kanban-board.model.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {KanbanEntry} from "./kanban-entry.model";
|
||||||
|
|
||||||
|
export class KanbanBoard {
|
||||||
|
public inbox: Array<KanbanEntry>;
|
||||||
|
public inProgress: Array<KanbanEntry>;
|
||||||
|
public verification: Array<KanbanEntry>;
|
||||||
|
public done: Array<KanbanEntry>;
|
||||||
|
}
|
||||||
29
src/app/display/shared/kanban-entry.model.ts
Normal file
29
src/app/display/shared/kanban-entry.model.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import {JiraIssueType} from "./jira-issue-type.model";
|
||||||
|
import {JiraStatus} from "./jira-status.model";
|
||||||
|
import {JiraAssignee} from "./jira-assignee.model";
|
||||||
|
|
||||||
|
export class KanbanEntry {
|
||||||
|
public id: number;
|
||||||
|
public key: string;
|
||||||
|
public summary: string;
|
||||||
|
public issueType: JiraIssueType;
|
||||||
|
public status: JiraStatus;
|
||||||
|
public assignee: JiraAssignee;
|
||||||
|
public additionalAssignees: Array<JiraAssignee> = [];
|
||||||
|
public issuePriority: string;
|
||||||
|
public issuePriorityIcon: string;
|
||||||
|
public labels: Array<string>;
|
||||||
|
public prio: number;
|
||||||
|
public functionalAreas: Array<string>;
|
||||||
|
public externalId: string;
|
||||||
|
public externalLink: string;
|
||||||
|
public project: string;
|
||||||
|
public mhwebStatus: string;
|
||||||
|
public mhwebHot: boolean;
|
||||||
|
public mhwebExternal: boolean;
|
||||||
|
public team: string;
|
||||||
|
public answerCode: string;
|
||||||
|
public isLastOfPriority: boolean;
|
||||||
|
public worklog: number;
|
||||||
|
public daysBlocked: number;
|
||||||
|
}
|
||||||
15
src/app/display/shared/kanban.service.spec.ts
Normal file
15
src/app/display/shared/kanban.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { KanbanService } from './kanban.service';
|
||||||
|
|
||||||
|
describe('KanbanService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [KanbanService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', inject([KanbanService], (service: KanbanService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
||||||
51
src/app/display/shared/kanban.service.ts
Normal file
51
src/app/display/shared/kanban.service.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { KanbanBoard } from './kanban-board.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KanbanService {
|
||||||
|
private url = environment.apiUrl + '/api/kanban';
|
||||||
|
|
||||||
|
private cachedKanbanBoard: KanbanBoard = new KanbanBoard();
|
||||||
|
|
||||||
|
constructor(private httpService: HttpClient) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable instance to the kanban board api
|
||||||
|
*
|
||||||
|
* @returns {Observable<KanbanBoard>}
|
||||||
|
*/
|
||||||
|
public getList(): Observable<KanbanBoard> {
|
||||||
|
return this.httpService.get<KanbanBoard>(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route preload resolver
|
||||||
|
*
|
||||||
|
* @param {ActivatedRouteSnapshot} route
|
||||||
|
* @returns {Promise<KanbanBoard>}
|
||||||
|
*/
|
||||||
|
public resolve(route: ActivatedRouteSnapshot): Promise<KanbanBoard | boolean> {
|
||||||
|
return this.getList().toPromise().then(result => result ? result : false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the board
|
||||||
|
*/
|
||||||
|
public reload() {
|
||||||
|
this.getList().subscribe(result => this.cachedKanbanBoard = result);
|
||||||
|
}
|
||||||
|
|
||||||
|
get kanbanBoard(): KanbanBoard {
|
||||||
|
return this.cachedKanbanBoard;
|
||||||
|
}
|
||||||
|
|
||||||
|
set kanbanBoard(kanbanBoard: KanbanBoard) {
|
||||||
|
this.cachedKanbanBoard = kanbanBoard;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/app/display/shared/prefix-jira-id.pipe.spec.ts
Normal file
8
src/app/display/shared/prefix-jira-id.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { PrefixJiraIdPipe } from './prefix-jira-id.pipe';
|
||||||
|
|
||||||
|
describe('PrefixJiraIdPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new PrefixJiraIdPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/app/display/shared/prefix-jira-id.pipe.ts
Normal file
12
src/app/display/shared/prefix-jira-id.pipe.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'prefixJiraId'
|
||||||
|
})
|
||||||
|
export class PrefixJiraIdPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform(value: string, jiraId: string): string {
|
||||||
|
return `[${jiraId}] ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
src/app/display/shared/priority-color.pipe.spec.ts
Normal file
8
src/app/display/shared/priority-color.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { PriorityColorPipe } from './priority-color.pipe';
|
||||||
|
|
||||||
|
describe('PriorityColorPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new PriorityColorPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
40
src/app/display/shared/priority-color.pipe.ts
Normal file
40
src/app/display/shared/priority-color.pipe.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'priorityColor'
|
||||||
|
})
|
||||||
|
export class PriorityColorPipe implements PipeTransform {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 `<span class="match-mhr">[${worklog}/${hoursMatch} mhr] </span> `;
|
||||||
|
});
|
||||||
|
let sMatch = /(\[s\])/ig;
|
||||||
|
value = value.replace(sMatch, (fullMatch: string, mhrMatched: string) => {
|
||||||
|
return `<span class="match-s">${mhrMatched}</span> `;
|
||||||
|
});
|
||||||
|
let mMatch = /(\[m\])/ig;
|
||||||
|
value = value.replace(mMatch, (fullMatch: string, mhrMatched: string) => {
|
||||||
|
return `<span class="match-m">${mhrMatched}</span> `;
|
||||||
|
});
|
||||||
|
let lMatch = /(\[l\])/ig;
|
||||||
|
value = value.replace(lMatch, (fullMatch: string, mhrMatched: string) => {
|
||||||
|
return `<span class="match-l">${mhrMatched}</span> `;
|
||||||
|
});
|
||||||
|
let xlMatch = /(\[xl\])/ig;
|
||||||
|
value = value.replace(xlMatch, (fullMatch: string, mhrMatched: string) => {
|
||||||
|
return `<span class="match-xl">${mhrMatched}</span> `;
|
||||||
|
});
|
||||||
|
return (prioIcon ? `<img class="prio-icon" width="16" height="16" src="${prioIcon}"> ` : "") + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
src/app/display/shared/shorten-text.pipe.spec.ts
Normal file
8
src/app/display/shared/shorten-text.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { ShortenTextPipe } from './shorten-text.pipe';
|
||||||
|
|
||||||
|
describe('ShortenTextPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new ShortenTextPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
21
src/app/display/shared/shorten-text.pipe.ts
Normal file
21
src/app/display/shared/shorten-text.pipe.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'shortenText'
|
||||||
|
})
|
||||||
|
export class ShortenTextPipe implements PipeTransform {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user