diff --git a/src/app/display/display-routing.module.ts b/src/app/display/display-routing.module.ts
index 22edf1e..062ab4a 100644
--- a/src/app/display/display-routing.module.ts
+++ b/src/app/display/display-routing.module.ts
@@ -7,6 +7,8 @@ import { SettingsComponent } from './settings/settings.component';
import { TeamService } from '../shared/service/team.service';
import { SlideShowComponent } from './slide-show/slide-show.component';
import { SlideResolverService } from '../admin/slide-resolver.service';
+import { KanbanBoardComponent } from './kanban-board/kanban-board.component';
+import { KanbanService } from './shared';
const routes: Routes = [
{
@@ -49,6 +51,16 @@ const routes: Routes = [
data: {
autoSwitchable: true
}
+ }, {
+ path: 'kanban',
+ component: KanbanBoardComponent,
+ // canActivate: [AuthGuardService, RoleGuardService],
+ resolve: {
+ kanbanBoard: KanbanService,
+ },
+ data: {
+ autoSwitchable: false
+ }
}, {
path: 'settings',
component: SettingsComponent,
diff --git a/src/app/display/display.module.ts b/src/app/display/display.module.ts
index bbd8895..fab0336 100644
--- a/src/app/display/display.module.ts
+++ b/src/app/display/display.module.ts
@@ -9,6 +9,13 @@ import { SuiModule } from 'ng2-semantic-ui';
import { SlideComponent } from './slide/slide.component';
import { SlideShowComponent } from './slide-show/slide-show.component';
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({
imports: [
@@ -18,7 +25,22 @@ import { SlideShowService } from './slide-show.service';
DisplayRoutingModule
],
exports: [SlideComponent],
- declarations: [CommitTrackerComponent, SettingsComponent, SlideComponent, SlideShowComponent],
- providers: [SlideShowService]
+ declarations: [
+ CommitTrackerComponent,
+ SettingsComponent,
+ SlideComponent,
+ SlideShowComponent,
+ KanbanBoardComponent,
+ KanbanEntryItemComponent,
+
+ BlockedDaysPipe,
+ PrefixJiraIdPipe,
+ PriorityColorPipe,
+ ShortenTextPipe,
+ ],
+ providers: [
+ SlideShowService,
+ KanbanService,
+ ]
})
export class DisplayModule { }
diff --git a/src/app/display/kanban-board/kanban-board.component.css b/src/app/display/kanban-board/kanban-board.component.css
new file mode 100644
index 0000000..66f5da1
--- /dev/null
+++ b/src/app/display/kanban-board/kanban-board.component.css
@@ -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;
+}
diff --git a/src/app/display/kanban-board/kanban-board.component.html b/src/app/display/kanban-board/kanban-board.component.html
new file mode 100644
index 0000000..435b752
--- /dev/null
+++ b/src/app/display/kanban-board/kanban-board.component.html
@@ -0,0 +1,18 @@
+
diff --git a/src/app/display/kanban-board/kanban-board.component.spec.ts b/src/app/display/kanban-board/kanban-board.component.spec.ts
new file mode 100644
index 0000000..46025c4
--- /dev/null
+++ b/src/app/display/kanban-board/kanban-board.component.spec.ts
@@ -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;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ KanbanBoardComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(KanbanBoardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/display/kanban-board/kanban-board.component.ts b/src/app/display/kanban-board/kanban-board.component.ts
new file mode 100644
index 0000000..a39d19e
--- /dev/null
+++ b/src/app/display/kanban-board/kanban-board.component.ts
@@ -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;
+ // }
+}
diff --git a/src/app/display/kanban-entry-item/kanban-entry-item.component.css b/src/app/display/kanban-entry-item/kanban-entry-item.component.css
new file mode 100644
index 0000000..9b0fb9f
--- /dev/null
+++ b/src/app/display/kanban-entry-item/kanban-entry-item.component.css
@@ -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;
+}
diff --git a/src/app/display/kanban-entry-item/kanban-entry-item.component.html b/src/app/display/kanban-entry-item/kanban-entry-item.component.html
new file mode 100644
index 0000000..2f58d4f
--- /dev/null
+++ b/src/app/display/kanban-entry-item/kanban-entry-item.component.html
@@ -0,0 +1,32 @@
+
+ {{rowHeading}}
+
+ - {{wipCount}}/{{wipLimit}}
+
+
+
+
+
+
+
+ {{label|uppercase|blockedDays:kanbanEntry.daysBlocked}}
+
+
{{kanbanEntry.daysBlocked}}D
+
+
}})
+
+
+
}})
+
+
+
+
+
+
diff --git a/src/app/display/kanban-entry-item/kanban-entry-item.component.spec.ts b/src/app/display/kanban-entry-item/kanban-entry-item.component.spec.ts
new file mode 100644
index 0000000..cdbe204
--- /dev/null
+++ b/src/app/display/kanban-entry-item/kanban-entry-item.component.spec.ts
@@ -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;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ KanbanEntryItemComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(KanbanEntryItemComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/display/kanban-entry-item/kanban-entry-item.component.ts b/src/app/display/kanban-entry-item/kanban-entry-item.component.ts
new file mode 100644
index 0000000..175274a
--- /dev/null
+++ b/src/app/display/kanban-entry-item/kanban-entry-item.component.ts
@@ -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;
+ @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 {
+ return [].concat([kanbanEntry.assignee], kanbanEntry.additionalAssignees);
+ }
+}
diff --git a/src/app/display/shared/blocked-days.pipe.spec.ts b/src/app/display/shared/blocked-days.pipe.spec.ts
new file mode 100644
index 0000000..83e8e93
--- /dev/null
+++ b/src/app/display/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/display/shared/blocked-days.pipe.ts b/src/app/display/shared/blocked-days.pipe.ts
new file mode 100644
index 0000000..413a342
--- /dev/null
+++ b/src/app/display/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/display/shared/index.ts b/src/app/display/shared/index.ts
new file mode 100644
index 0000000..29eb779
--- /dev/null
+++ b/src/app/display/shared/index.ts
@@ -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';
diff --git a/src/app/display/shared/jira-assignee.model.ts b/src/app/display/shared/jira-assignee.model.ts
new file mode 100644
index 0000000..7c35394
--- /dev/null
+++ b/src/app/display/shared/jira-assignee.model.ts
@@ -0,0 +1,7 @@
+export class JiraAssignee {
+ public signum: string;
+ public name: string;
+ public email: string;
+ public avatar: string;
+ public active: boolean;
+}
diff --git a/src/app/display/shared/jira-issue-type.model.ts b/src/app/display/shared/jira-issue-type.model.ts
new file mode 100644
index 0000000..42c0d45
--- /dev/null
+++ b/src/app/display/shared/jira-issue-type.model.ts
@@ -0,0 +1,5 @@
+export class JiraIssueType {
+ public name: string;
+ public description: string;
+ public icon: string;
+}
diff --git a/src/app/display/shared/jira-status.model.ts b/src/app/display/shared/jira-status.model.ts
new file mode 100644
index 0000000..1c420be
--- /dev/null
+++ b/src/app/display/shared/jira-status.model.ts
@@ -0,0 +1,4 @@
+export class JiraStatus {
+ public name: string;
+ public color: string;
+}
diff --git a/src/app/display/shared/kanban-board.model.ts b/src/app/display/shared/kanban-board.model.ts
new file mode 100644
index 0000000..915ebe8
--- /dev/null
+++ b/src/app/display/shared/kanban-board.model.ts
@@ -0,0 +1,8 @@
+import {KanbanEntry} from "./kanban-entry.model";
+
+export class KanbanBoard {
+ public inbox: Array;
+ public inProgress: Array;
+ public verification: Array;
+ public done: Array;
+}
diff --git a/src/app/display/shared/kanban-entry.model.ts b/src/app/display/shared/kanban-entry.model.ts
new file mode 100644
index 0000000..b97a2db
--- /dev/null
+++ b/src/app/display/shared/kanban-entry.model.ts
@@ -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 = [];
+ public issuePriority: string;
+ public issuePriorityIcon: string;
+ public labels: Array;
+ public prio: number;
+ public functionalAreas: Array;
+ 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;
+}
diff --git a/src/app/display/shared/kanban.service.spec.ts b/src/app/display/shared/kanban.service.spec.ts
new file mode 100644
index 0000000..73432b8
--- /dev/null
+++ b/src/app/display/shared/kanban.service.spec.ts
@@ -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();
+ }));
+});
diff --git a/src/app/display/shared/kanban.service.ts b/src/app/display/shared/kanban.service.ts
new file mode 100644
index 0000000..192048e
--- /dev/null
+++ b/src/app/display/shared/kanban.service.ts
@@ -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}
+ */
+ public getList(): Observable {
+ return this.httpService.get(this.url);
+ }
+
+ /**
+ * 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);
+ }
+
+ get kanbanBoard(): KanbanBoard {
+ return this.cachedKanbanBoard;
+ }
+
+ set kanbanBoard(kanbanBoard: KanbanBoard) {
+ this.cachedKanbanBoard = kanbanBoard;
+ }
+}
diff --git a/src/app/display/shared/prefix-jira-id.pipe.spec.ts b/src/app/display/shared/prefix-jira-id.pipe.spec.ts
new file mode 100644
index 0000000..465de75
--- /dev/null
+++ b/src/app/display/shared/prefix-jira-id.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { PrefixJiraIdPipe } from './prefix-jira-id.pipe';
+
+describe('PrefixJiraIdPipe', () => {
+ it('create an instance', () => {
+ const pipe = new PrefixJiraIdPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/src/app/display/shared/prefix-jira-id.pipe.ts b/src/app/display/shared/prefix-jira-id.pipe.ts
new file mode 100644
index 0000000..247038c
--- /dev/null
+++ b/src/app/display/shared/prefix-jira-id.pipe.ts
@@ -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}`;
+ }
+
+}
diff --git a/src/app/display/shared/priority-color.pipe.spec.ts b/src/app/display/shared/priority-color.pipe.spec.ts
new file mode 100644
index 0000000..35713b4
--- /dev/null
+++ b/src/app/display/shared/priority-color.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { PriorityColorPipe } from './priority-color.pipe';
+
+describe('PriorityColorPipe', () => {
+ it('create an instance', () => {
+ const pipe = new PriorityColorPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/src/app/display/shared/priority-color.pipe.ts b/src/app/display/shared/priority-color.pipe.ts
new file mode 100644
index 0000000..e7627e3
--- /dev/null
+++ b/src/app/display/shared/priority-color.pipe.ts
@@ -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 `[${worklog}/${hoursMatch} mhr] `;
+ });
+ let sMatch = /(\[s\])/ig;
+ value = value.replace(sMatch, (fullMatch: string, mhrMatched: string) => {
+ return `${mhrMatched} `;
+ });
+ let mMatch = /(\[m\])/ig;
+ value = value.replace(mMatch, (fullMatch: string, mhrMatched: string) => {
+ return `${mhrMatched} `;
+ });
+ let lMatch = /(\[l\])/ig;
+ value = value.replace(lMatch, (fullMatch: string, mhrMatched: string) => {
+ return `${mhrMatched} `;
+ });
+ let xlMatch = /(\[xl\])/ig;
+ value = value.replace(xlMatch, (fullMatch: string, mhrMatched: string) => {
+ return `${mhrMatched} `;
+ });
+ return (prioIcon ? `
` : "") + value;
+ }
+
+}
diff --git a/src/app/display/shared/shorten-text.pipe.spec.ts b/src/app/display/shared/shorten-text.pipe.spec.ts
new file mode 100644
index 0000000..b3affd8
--- /dev/null
+++ b/src/app/display/shared/shorten-text.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { ShortenTextPipe } from './shorten-text.pipe';
+
+describe('ShortenTextPipe', () => {
+ it('create an instance', () => {
+ const pipe = new ShortenTextPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/src/app/display/shared/shorten-text.pipe.ts b/src/app/display/shared/shorten-text.pipe.ts
new file mode 100644
index 0000000..6e86d7d
--- /dev/null
+++ b/src/app/display/shared/shorten-text.pipe.ts
@@ -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;
+ }
+
+}