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