Compare commits

..

7 Commits

Author SHA1 Message Date
Dávid Danyi
c61d1c8f13 * expedite tag color added 2017-09-05 13:31:08 +02:00
Dávid Danyi
990dbfa565 * revision.json cache disable
* wip limit in the headers
* DÖNER is has no prio borders anymore, but is reverse ordered by modification time
2017-09-01 11:51:34 +02:00
Dávid Danyi
4704a6a0d1 * scrollbar visible on desktop when mouse is hovered over the site 2017-08-28 13:06:45 +02:00
Dávid Danyi
6fc65e54d5 * wip limit excludes blocked issues
* blocked for days is now visible
* kanban issues are now clickable
* docblock documentation added
2017-08-25 11:32:29 +02:00
Dávid Danyi
e434523925 * minor usage documentation
* colors added for some new tags
* tag text case doesn't matter in matching
2017-08-24 18:43:24 +02:00
Danyi Dávid
c07ef0efcb Merge branch 'emakazi_dev' of TSP/taurus-tv into master 2017-08-24 17:33:01 +02:00
Danyi Dávid
0ed37b025e Merge branch 'emakazi_dev' of TSP/taurus-tv into master 2017-08-24 16:45:51 +02:00
18 changed files with 299 additions and 62 deletions

View File

@ -1,28 +1,36 @@
# TaurusTv # TaurusTv
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.2.6. ## Tools used for development
## Development server - [deployer](https://deployer.org/download)
- [nodeJS](https://nodejs.org/en/download/)
- [php7.1](http://php.net) - [installation on ELX](https://www.colinodell.com/blog/2016-12/installing-php-7-1)
- [phpStorm](https://www.jetbrains.com/phpstorm/download/)
Install the latest versions, add them to your $PATH.
## Prepare for development
### Install required node packages
```bash
sudo npm -g i typescript gulp @angular/cli
# Inside your project root
npm install
```
## Deployment
Run `dep deploy` in the project root, to deploy the application to `vasgyuro.tsp`. The application will automatically reload on the TV when a new version is deployed.
## Angular cli
### Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding ### Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`. Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`.
## Build ### Further help
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
Before running the tests make sure you are serving the app via `ng serve`.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -21,4 +21,12 @@ RewriteRule ^(.*)$ %{ENV:BASE}index.html [NC,L]
</Limit> </Limit>
<LimitExcept GET POST PUT DELETE HEAD OPTIONS> <LimitExcept GET POST PUT DELETE HEAD OPTIONS>
Require all denied Require all denied
</LimitExcept> </LimitExcept>
<Files revision.json>
FileETag None
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT"
</Files>

View File

@ -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(() => {

View File

@ -0,0 +1,5 @@
:host {
display: inline-block;
height: 100vh;
overflow: scroll;
}

View File

@ -1,12 +1,18 @@
<div class="ui main fullwide-container"> <div class="ui main fullwide-container">
<div class="ui grid"> <div class="ui grid">
<div app-kanban-entry-item class="four wide column" <div app-kanban-entry-item class="four wide column"
rowHeading="INBOX" [kanbanEntries]="kanbanBoard.inbox"></div> rowHeading="INBOX"
[kanbanEntries]="kanbanBoard.inbox"></div>
<div app-kanban-entry-item class="four wide column" [ngClass]="inprogressWipClass" <div app-kanban-entry-item class="four wide column" [ngClass]="inprogressWipClass"
rowHeading="INPROGRESS" [kanbanEntries]="kanbanBoard.inProgress"></div> rowHeading="INPROGRESS"
[wipLimit]="inprogressWipLimit" [wipCount]="inprogressWipCount"
[kanbanEntries]="kanbanBoard.inProgress"></div>
<div app-kanban-entry-item class="four wide column" [ngClass]="verificationWipClass" <div app-kanban-entry-item class="four wide column" [ngClass]="verificationWipClass"
rowHeading="VERIFICATION" [kanbanEntries]="kanbanBoard.verification"></div> rowHeading="VERIFICATION"
[wipLimit]="verificationWipLimit" [wipCount]="verificationWipCount"
[kanbanEntries]="kanbanBoard.verification"></div>
<div app-kanban-entry-item class="four wide column" <div app-kanban-entry-item class="four wide column"
rowHeading="DÖNER" [kanbanEntries]="kanbanBoard.done"></div> rowHeading="DÖNER"
[kanbanEntries]="kanbanBoard.done"></div>
</div> </div>
</div> </div>

View File

@ -1,15 +1,19 @@
import {Component, OnInit} from '@angular/core'; import {Component, HostBinding, HostDecorator, HostListener, OnInit} from '@angular/core';
import {Title} from '@angular/platform-browser'; 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;
const STYLE_HIDDEN = 'hidden';
const STYLE_VISIBLE = 'scroll';
@Component({ @Component({
selector: 'app-kanban-board', selector: 'app-kanban-board',
templateUrl: './kanban-board.component.html', templateUrl: './kanban-board.component.html',
@ -17,11 +21,16 @@ const WIP_LIMIT_VERIFICATION = 8;
}) })
export class KanbanBoardComponent implements OnInit { export class KanbanBoardComponent implements OnInit {
@HostBinding('style.overflow') hostOverflow = STYLE_HIDDEN;
constructor(private titleService: Title, constructor(private titleService: Title,
private route: ActivatedRoute, private route: ActivatedRoute,
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 +44,61 @@ export class KanbanBoardComponent implements OnInit {
this.kanbanService.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() { get inprogressWipClass() {
return { return {
'over-wip': this.kanbanBoard.inProgress.length > WIP_LIMIT_INPROGRESS, '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() { get verificationWipClass() {
return { return {
'over-wip': this.kanbanBoard.verification.length > WIP_LIMIT_VERIFICATION, 'over-wip': this.verificationWipCount > WIP_LIMIT_VERIFICATION,
}; };
} }
@HostListener('mouseover')
private onMouseOver() {
this.hostOverflow = STYLE_VISIBLE;
}
@HostListener('mouseout')
private onMouseOut() {
this.hostOverflow = STYLE_HIDDEN;
}
} }

View File

@ -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;
@ -30,19 +38,19 @@
} }
.ui.divided.items > .item.blocker.bottom-separator { .ui.divided.items > .item.blocker.bottom-separator {
border-bottom: 1px solid rgba(219, 40, 40, 0.5); border-bottom: 3px double rgba(219, 40, 40, 0.5);
} }
.ui.divided.items > .item.critical.bottom-separator { .ui.divided.items > .item.critical.bottom-separator {
border-bottom: 1px solid rgba(242, 113, 28, 0.5); border-bottom: 3px double rgba(242, 113, 28, 0.5);
} }
.ui.divided.items > .item.major.bottom-separator { .ui.divided.items > .item.major.bottom-separator {
border-bottom: 1px solid rgba(181, 204, 24, 0.5); border-bottom: 3px double rgba(181, 204, 24, 0.5);
} }
.ui.divided.items > .item.minor.bottom-separator { .ui.divided.items > .item.minor.bottom-separator {
border-bottom: 1px solid rgba(0, 181, 173, 0.5); border-bottom: 3px double rgba(0, 181, 173, 0.5);
} }
/*Nothing below trivial, no separator needed*/ /*Nothing below trivial, no separator needed*/

View File

@ -1,15 +1,21 @@
<h1>{{rowHeading}}</h1> <h1>
{{rowHeading}}
<ng-template [ngIf]="wipCount">
- {{wipCount}}/{{wipLimit}}
</ng-template>
</h1>
<div class="ui divided items"> <div class="ui divided items">
<div class="item {{kanbanEntry.issuePriority|lowercase}}" [ngClass]="entryClass(kanbanEntry)" *ngFor="let kanbanEntry of kanbanEntries"> <div class="item {{kanbanEntry.issuePriority|lowercase}}" [ngClass]="entryClass(kanbanEntry)" *ngFor="let kanbanEntry of kanbanEntries">
<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>

View File

@ -1,16 +1,19 @@
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',
MTAS: 'orange', MTAS: 'orange',
Internal: 'yellow', INTERNAL: 'yellow',
Team: 'yellow', TEAM: 'yellow',
BLOCKED: 'red', BLOCKED: 'red',
SPIKE: 'purple',
EXPEDITE: 'pink',
}; };
@Component({ @Component({
@ -18,33 +21,80 @@ 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 = "";
@Input() wipLimit: number = 0;
@Input() wipCount: number = 0;
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]; return labelColors[label.toUpperCase()];
} catch(e) { } catch(e) {
return 'white'; 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) { 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');
}
} }

View File

@ -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,

View File

@ -0,0 +1,8 @@
import { BlockedDaysPipe } from './blocked-days.pipe';
describe('BlockedDaysPipe', () => {
it('create an instance', () => {
const pipe = new BlockedDaysPipe();
expect(pipe).toBeTruthy();
});
});

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

View File

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

View File

@ -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,20 +51,21 @@ 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'].map(progress => {
kanbanBoard[progress].map(entry => entry.isLastOfPriority = false); kanbanBoard[progress].map(entry => entry.isLastOfPriority = false);
['Trivial', ['Trivial',
'Minor', 'Minor',
'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) {}

View File

@ -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> `;

View File

@ -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(

View File

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

View File

@ -1,13 +1,22 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
html,
body {
height: 100% !important;
overflow: hidden;
}
body { body {
background-color: #303030 !important; background-color: #303030 !important;
color: #eeeeee !important; color: #eeeeee !important;
margin-top: 1em !important; margin-top: 1em !important;
margin-bottom: 1em !important; margin-bottom: 1em !important;
overflow: hidden;
font-weight: bold !important; font-weight: bold !important;
} }
app-kanban-board {
overflow: hidden;
}
.ui.fullwide-container { .ui.fullwide-container {
margin-left: 1em; margin-left: 1em;
margin-right: 1em; margin-right: 1em;