Compare commits

..

15 Commits

Author SHA1 Message Date
Dávid Danyi
cf7ec832b2 * no idea what 2019-06-20 13:15:46 +02:00
Dávid Danyi
1a357f4121 * prod deploy 2018-10-04 15:43:32 +02:00
Dávid Danyi
8aa0828701 * Home key added to navigation, navigates to dashboard
* route animation reworked into routeTransition to avoid memory leaks
2018-09-24 15:57:13 +02:00
Dávid Danyi
41ad9d9a28 * slideshow start changed to slideshowService call
* builtin slide switching added to the team forms
* parts of team form made conditional
* fixed keyboard slide switching when not in slideshow
* slideshow slide management totally reworked into linked list
* Date.getDay() -> Date.getDate() as it was originally meant to be
2018-09-17 16:03:23 +02:00
Dávid Danyi
6af8ccbf7a * parametered animation for slideshow direction
* keyboard control
* angular2 update
2018-09-14 17:06:12 +02:00
Dávid Danyi
270a55f6b9 * slideshow goes to non-fixed watchers now 2018-09-13 15:28:25 +02:00
Dávid Danyi
ad8f5491b5 * added watcher slide type to the slideshow
* auto reloading fixed on revision change
* added shortening pipe to the comment field in watcher view
2018-09-13 11:34:01 +02:00
Dávid Danyi
79227258e8 * dashboard start slide navigation button removed when team is not selected
* watcher table icons changed
2018-09-12 18:01:28 +02:00
Dávid Danyi
80dc5b54e8 * watcher slide type implementation, not yet in the loop
* min slide duration adjusted to 5seconds
2018-09-12 17:18:53 +02:00
Dávid Danyi
57f85768eb * daily kanban slide lock added 2018-09-11 16:10:47 +02:00
Dávid Danyi
aa995e845a * basic validation and error display added to slide editor and team editor 2018-09-06 17:08:33 +02:00
Dávid Danyi
a15cb298a0 * deploy config fixed 2018-09-06 16:39:54 +02:00
Dávid Danyi
ea05a47086 * team customized labels added
* team editor interface minor redesign
2018-09-06 15:38:50 +02:00
Dávid Danyi
0f535881a4 * angular2 version uplift
* configurable kanban board added
* iframe slide type added
* many to many implementation of team-slide connection
2018-09-05 17:03:21 +02:00
Dávid Danyi
96918b50ca * disabled kanban 2018-07-26 14:45:23 +02:00
70 changed files with 6224 additions and 6947 deletions

View File

@ -1,63 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "mtas-tv-frontend"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"../node_modules/semantic-ui-css/semantic.css",
"styles.css"
],
"scripts": [
"../node_modules/marked/lib/marked.js"
],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"component": {}
}
}

156
angular.json Executable file
View File

@ -0,0 +1,156 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"mtas-tv-frontend": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"node_modules/semantic-ui-css/semantic.css",
"src/styles.css"
],
"scripts": [
"node_modules/marked/lib/marked.js"
]
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"staging": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.stg.ts"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "mtas-tv-frontend:build"
},
"configurations": {
"production": {
"browserTarget": "mtas-tv-frontend:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "mtas-tv-frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"scripts": [
"node_modules/marked/lib/marked.js"
],
"styles": [
"node_modules/semantic-ui-css/semantic.css",
"src/styles.css"
],
"assets": [
"src/assets",
"src/favicon.ico"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"mtas-tv-frontend-e2e": {
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "mtas-tv-frontend:serve"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "mtas-tv-frontend",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "css"
},
"@schematics/angular:directive": {
"prefix": "app"
}
}
}

34
deploy.php Normal file → Executable file
View File

@ -15,21 +15,29 @@ set('writable_dirs', []);
set('keep_releases', 3);
set('default_stage', 'production');
// Servers
// Servers - mtas : esekivws5222a.rnd.ki.sw.ericsson.se
host('mtas')
->stage('production')
->user('edvidan')
->forwardAgent()
->set('ng_basehref', '/mtastv/')
->set('ng_configuration', 'production')
->set('env_vars', 'NODE_ENV=production')
->set('deploy_path', '/proj/webdocs/mtoolbox/root/mtastv-inst/frontend');
host('vasgyuro.tsp')
->stage('production')
->user('edvidan')
->forwardAgent()
->set('ng_basehref', '/mtas-tv/')
->set('ng_target', 'production')
->set('ng_environment', 'prod')
->set('env_vars', 'NODE_ENV=production')
->set('deploy_path', '/home/edvidan/applications/mtas-tv');
->stage('staging')
->user('edvidan')
->forwardAgent()
->set('ng_basehref', '/mtas-tv/')
->set('ng_configuration', 'staging')
->set('env_vars', 'NODE_ENV=production')
->set('deploy_path', '/home/edvidan/applications/mtas-tv');
// Tasks
desc('Prepare release');
task('deploy:ng-prepare', function() {
runLocally("ng build --base-href={{ng_basehref}} --target={{ng_target}} --environment={{ng_environment}}");
desc('Build release');
task('deploy:ng-build', function() {
runLocally("ng build --base-href={{ng_basehref}} --configuration={{ng_configuration}}");
runLocally("tar -cJf dist.tar.xz dist");
});
@ -52,7 +60,7 @@ task('deploy', [
'deploy:prepare',
'deploy:lock',
'deploy:release',
'deploy:ng-prepare',
'deploy:ng-build',
'deploy:ng-upload',
'deploy:shared',
'deploy:clear_paths',

View File

@ -16,12 +16,12 @@ RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
RewriteRule ^(.*)$ %{ENV:BASE}index.html [NC,L]
<Limit GET POST PUT DELETE HEAD OPTIONS>
Require all granted
</Limit>
<LimitExcept GET POST PUT DELETE HEAD OPTIONS>
Require all denied
</LimitExcept>
#<Limit GET POST PUT DELETE HEAD OPTIONS>
# Require all granted
#</Limit>
#<LimitExcept GET POST PUT DELETE HEAD OPTIONS>
# Require all denied
#</LimitExcept>
<Files revision.json>
FileETag None

View File

@ -4,24 +4,22 @@
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
require('@angular-devkit/build-angular/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,

11211
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +1,54 @@
{
"name": "mtas-tv-frontend",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --prod",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^5.2.0",
"@angular/common": "^5.2.0",
"@angular/compiler": "^5.2.0",
"@angular/core": "^5.2.0",
"@angular/forms": "^5.2.0",
"@angular/http": "^5.2.0",
"@angular/platform-browser": "^5.2.0",
"@angular/platform-browser-dynamic": "^5.2.0",
"@angular/router": "^5.2.0",
"@types/marked": "^0.3.0",
"core-js": "^2.4.1",
"marked": "^0.3.19",
"@angular/animations": "^6.1.7",
"@angular/common": "^6.1.7",
"@angular/compiler": "^6.1.7",
"@angular/core": "^6.1.7",
"@angular/forms": "^6.1.7",
"@angular/http": "^6.1.7",
"@angular/platform-browser": "^6.1.7",
"@angular/platform-browser-dynamic": "^6.1.7",
"@angular/router": "^6.1.7",
"@types/date-fns": "^2.6.0",
"@types/marked": "^0.4.1",
"core-js": "^2.5.4",
"date-fns": "^1.29.0",
"marked": "^0.5.0",
"ng2-semantic-ui": "^0.9.7",
"rxjs": "^5.5.6",
"semantic-ui-css": "^2.3.1",
"zone.js": "^0.8.19"
"rxjs": "^6.3.2",
"semantic-ui-css": "^2.3.3",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular/cli": "~1.7.3",
"@angular/compiler-cli": "^5.2.0",
"@angular/language-service": "^5.2.0",
"@types/jasmine": "~2.8.3",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.8.0",
"@angular-devkit/build-angular": "~0.7.0",
"@angular/cli": "~6.1.5",
"@angular/compiler-cli": "^6.1.7",
"@angular/language-service": "^6.1.7",
"@types/jasmine": "^2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.2.1",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~2.0.0",
"karma": "~1.7.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-coverage-istanbul-reporter": "^2.0.4",
"karma-jasmine": "^1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~4.1.0",
"protractor": "^5.4.1",
"ts-node": "~5.0.1",
"tslint": "~5.9.1",
"typescript": "~2.5.3"
"typescript": "~2.7.2"
}
}

23
src/app/admin/admin-routing.module.ts Normal file → Executable file
View File

@ -11,63 +11,56 @@ import { SlideResolverService } from './slide-resolver.service';
import { SlideService } from '../shared/service/slide.service';
import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [
{
path: 'admin',
redirectTo: '/dashboard',
pathMatch: 'full'
// canActivate: [AuthGuardService, RoleGuardService],
pathMatch: 'full',
}, {
path: 'dashboard',
component: DashboardComponent,
// canActivate: [AuthGuardService, RoleGuardService],
}, {
path: 'admin/teams',
component: TeamListComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
teams: TeamService,
}
}, {
path: 'admin/team/new',
component: TeamEditorComponent,
// canActivate: [AuthGuardService, RoleGuardService],
}, {
path: 'admin/team/edit/:id',
component: TeamEditorComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
team: TeamResolverService,
}
},
}, {
path: 'admin/slides',
component: SlideListComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
slides: SlideService,
teams: TeamService,
}
},
}, {
path: 'admin/slide/new',
component: SlideEditorComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
teams: TeamService,
}
},
}, {
path: 'admin/slide/edit/:id',
component: SlideEditorComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
slide: SlideResolverService,
teams: TeamService,
}
},
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule {
}
export class AdminRoutingModule {}

2
src/app/admin/dashboard/dashboard.component.html Normal file → Executable file
View File

@ -2,7 +2,7 @@
<h1 class="ui dividing header">Dashboard</h1>
<div class="ui four cards">
<a class="ui raised yellow card" [routerLink]="['/commit-tracker']">
<a class="ui raised yellow card" (click)="startSlideShow()" *ngIf="hasTeamSelected">
<div class="content">
<div class="header">Start slideshow</div>
<div class="meta">

13
src/app/admin/dashboard/dashboard.component.ts Normal file → Executable file
View File

@ -1,5 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import {SettingsService} from '../../shared/service/settings.service';
import {SlideShowService} from '../../display/slide-show.service';
@Component({
selector: 'app-dashboard',
@ -8,10 +10,19 @@ import { Title } from '@angular/platform-browser';
})
export class DashboardComponent implements OnInit {
constructor(private titleService: Title) { }
constructor(private titleService: Title,
private settingService: SettingsService,
private slideShowService: SlideShowService) { }
ngOnInit() {
this.titleService.setTitle('Dashboard : MTAStv');
}
get hasTeamSelected(): boolean {
return this.settingService.team.id !== null;
}
public startSlideShow() {
this.slideShowService.startWithFirstSlide();
}
}

56
src/app/admin/slide-editor/slide-editor.component.html Normal file → Executable file
View File

@ -2,31 +2,55 @@
<h1 class="ui dividing header">Slide editor</h1>
<form class="ui form" #slideEditorForm (ngSubmit)="saveSlide()">
<div class="two fields">
<div class="eight wide field">
<div class="six wide field">
<label for="slide_name">Slide title</label>
<input id="slide_name" type="text" name="slide_name" [(ngModel)]="slide.title">
</div>
<div class="two wide field">
<label>Visibility</label>
<div class="field">
<sui-radio-button name="slide_visibility" value="public" [(ngModel)]="slide.visibility">Public</sui-radio-button>
</div>
<div class="field">
<sui-radio-button name="slide_visibility" value="team" [(ngModel)]="slide.visibility">Team</sui-radio-button>
</div>
</div>
<div class="eight wide field">
<label for="team">Visible to this team</label>
<sui-select class="selection"
id="team"
name="team"
[(ngModel)]="slide.team"
<label for="teams">Visible to this team</label>
<sui-multi-select class="selection"
id="teams"
name="teams"
[(ngModel)]="slide.teams"
[isDisabled]="slide.visibility=='public'"
labelField="name"
[isSearchable]="true"
#select>
<sui-select-option [value]="emptyTeam"></sui-select-option>
<sui-select-option *ngFor="let team of teams" [value]="team"></sui-select-option>
</sui-select>
</sui-multi-select>
</div>
</div>
<div class="field">
<label for="slide_data">Slide data</label>
<textarea id="slide_data" rows="30" name="slide_data" [(ngModel)]="slide.slideData"></textarea>
<div class="inline fields">
<label>Slide type</label>
<div class="field">
<sui-radio-button name="slide_type" value="markdown" [(ngModel)]="slide.type">Markdown</sui-radio-button>
</div>
<div class="field">
<sui-radio-button name="slide_type" value="iframe" [(ngModel)]="slide.type">Iframe</sui-radio-button>
</div>
</div>
<div class="field" *ngIf="isMarkdown">
<label for="slide_data">Slide data</label>
<textarea id="slide_data" rows="30" name="slide_data" [(ngModel)]="slide.slideData"></textarea>
</div>
<div class="field" *ngIf="isIframe" [class.error]="checkError(slideUrl)">
<label for="slide_url">Slide url</label>
<input id="slide_url" type="url" name="slide_url"
[(ngModel)]="slide.slideUrl" #slideUrl="ngModel">
</div>
<div class="five wide field">
<div class="ui checkbox">
@ -43,7 +67,8 @@
[class.primary]="canPreview"
[class.disabled]="!canPreview"
(click)="preview()"><i class="search icon"></i>Preview</button>
<a class="ui button orange"
<a *ngIf="isMarkdown"
class="ui button orange"
href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet"
target="_blank"><i class="question circle outline icon"></i>MD howto</a>
<a class="ui button"
@ -54,5 +79,10 @@
[data]="renderedPreview"
[preview]="true"
[(visible)]="previewVisible"
*ngIf="previewVisible"></app-slide>
*ngIf="previewVisible && isMarkdown"></app-slide>
<app-slide-iframe class="preview"
[data]="slide.slideUrl"
[preview]="true"
[(visible)]="previewVisible"
*ngIf="previewVisible && isIframe"></app-slide-iframe>
</div>

44
src/app/admin/slide-editor/slide-editor.component.ts Normal file → Executable file
View File

@ -1,11 +1,12 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Title} from '@angular/platform-browser';
import * as marked from 'marked';
import { Slide } from '../../shared/slide';
import { SlideService } from '../../shared/service/slide.service';
import { Team } from '../../shared/team';
import {Slide, SlideType, SlideVisibility} from '../../shared/slide';
import {SlideService} from '../../shared/service/slide.service';
import {Team} from '../../shared/team';
import {NgModel} from '@angular/forms';
@Component({
selector: 'app-slide-editor',
@ -14,7 +15,6 @@ import { Team } from '../../shared/team';
})
export class SlideEditorComponent implements OnInit {
private md;
public emptyTeam: Team = new Team();
public slide: Slide;
public teams: Array<Team> = [];
public renderedPreview: String = '';
@ -25,7 +25,6 @@ export class SlideEditorComponent implements OnInit {
private route: ActivatedRoute,
private router: Router) {
this.md = marked.setOptions({});
this.emptyTeam.name = 'All teams';
}
ngOnInit() {
@ -36,9 +35,11 @@ export class SlideEditorComponent implements OnInit {
}) => {
this.teams = data.teams;
this.slide = data.slide ? data.slide : new Slide;
this.slide.team = this.slide.team === null
? this.emptyTeam
: this.teams.find(team => team.id === this.slide.team.id);
this.slide.teams = this.teams.filter(
team => this.slide.teams.some(
st => team.id === st.id
)
);
});
}
@ -53,16 +54,31 @@ export class SlideEditorComponent implements OnInit {
get canSave(): boolean {
return [
this.slide.title,
this.slide.slideData
].every(field => field.trim().length > 0);
this.isMarkdown ? this.slide.slideData : this.slide.slideUrl
].every(field => field.trim().length > 0) && (
this.slide.visibility === SlideVisibility.Public ||
this.slide.visibility === SlideVisibility.Team && this.slide.teams.length > 0
);
}
get canPreview(): boolean {
return this.slide.slideData.trim().length > 0;
return this.isMarkdown && this.slide.slideData.trim().length > 0 || this.isIframe && this.slide.slideUrl.trim().length > 0;
}
public preview() {
this.previewVisible = true;
this.renderedPreview = this.md.parse(this.slide.slideData);
}
get isMarkdown(): boolean {
return this.slide.type === SlideType.MarkDown;
}
get isIframe(): boolean {
return this.slide.type === SlideType.IFrame;
}
public checkError(fieldModel: NgModel): boolean {
return fieldModel.invalid && (fieldModel.dirty || fieldModel.touched);
}
}

6
src/app/admin/slide-list/slide-list.component.html Normal file → Executable file
View File

@ -19,8 +19,8 @@
<tr>
<th class="collapsing"></th>
<th><i class="large address book outline icon"></i>Slide title</th>
<th class="collapsing"><i class="large users icon"></i>Owner team</th>
<th class="collapsing"><i class="large check square outline icon"></i>Visible</th>
<th class="collapsing"><i class="large users icon"></i>Visible to</th>
<th class="collapsing"><i class="large check square outline icon"></i>Active</th>
<th class="collapsing"><i class="large arrows alternate vertical icon"></i>Order</th>
</tr>
</thead>
@ -33,7 +33,7 @@
class="large link red fitted trash alternate outline icon"></i></a>
</td>
<td>{{slide.title}}</td>
<td class="collapsing">{{slideTeam(slide.team)}}</td>
<td class="collapsing">{{slideTeam(slide)}}</td>
<td class="center aligned"><i class="large icon" [ngClass]="visibleClass(slide)"></i></td>
<td class="center aligned">
<a title="Up" (click)="moveUp(slide)"><i

27
src/app/admin/slide-list/slide-list.component.ts Normal file → Executable file
View File

@ -1,10 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Title} from '@angular/platform-browser';
import { SlideService } from '../../shared/service/slide.service';
import { Slide } from '../../shared/slide';
import { Team } from '../../shared/team';
import {SlideService} from '../../shared/service/slide.service';
import {Slide, SlideVisibility} from '../../shared/slide';
import {Team} from '../../shared/team';
@Component({
selector: 'app-slide-list',
@ -38,7 +38,7 @@ export class SlideListComponent implements OnInit {
return this.selectedTeam === this.emptyTeam
? this.slideService.slides
: this.slideService.slides.filter(
slide => slide.team == null || slide.team.id === this.selectedTeam.id
slide => slide.teams == null || slide.teams.some(s => s.id === this.selectedTeam.id)
);
}
@ -46,8 +46,10 @@ export class SlideListComponent implements OnInit {
this.slideService.slides = slides;
}
public slideTeam(team: Team): String {
return team === null ? 'All teams' : team.name;
public slideTeam(slide: Slide): string {
return slide.visibility === SlideVisibility.Team && slide.teams.length > 0
? slide.teams.map(team => team.name).join(', ')
: 'All teams';
}
public moveUp(slide: Slide) {
@ -106,4 +108,11 @@ export class SlideListComponent implements OnInit {
'red times': !slide.isVisible
};
}
public visibilityIcon(slide: Slide) {
return {
'green globe': slide.visibility === SlideVisibility.Public,
'green users': slide.visibility === SlideVisibility.Team
};
}
}

2
src/app/admin/slide-resolver.service.ts Normal file → Executable file
View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Slide } from '../shared/slide';

189
src/app/admin/team-editor/team-editor.component.html Normal file → Executable file
View File

@ -1,20 +1,186 @@
<div class="ui main container">
<h1 class="ui dividing header">Team editor</h1>
<form class="ui form" #teamEditorForm (ngSubmit)="saveTeam()">
<div class="six wide field">
<label for="team_name">Team name</label>
<input id="team_name" type="text" name="team_name" [(ngModel)]="team.name">
<form class="ui form" #teamEditorForm (ngSubmit)="saveTeam(f)" #f="ngForm">
<div class="two inline fields">
<div class="six wide field" [class.error]="checkError(teamName)">
<label for="team_name">Team name</label>
<input id="team_name" type="text" name="team_name"
required [(ngModel)]="team.name" #teamName="ngModel">
</div>
<div class="six wide field">
<label> </label>
<div class="ui checkbox">
<input type="checkbox" id="team_is_active" name="team_is_active"
[(ngModel)]="team.isActive">
<label for="team_is_active">Active</label>
</div>
</div>
</div>
<div class="six wide field">
<h4 class="ui dividing header">Built-in slides</h4>
<div class="three inline fields">
<div class="three wide field">
<label for="team_name"> </label>
<div class="ui checkbox">
<input type="checkbox" id="team_is_active" name="team_is_active"
[(ngModel)]="team.isActive">
<label for="team_is_active">Active</label>
<input type="checkbox" id="kanban_enabled" name="kanban_enabled"
[(ngModel)]="team.kanbanEnabled">
<label for="kanban_enabled">Kanban board</label>
</div>
</div>
<div class="three wide field">
<label for="team_name"> </label>
<div class="ui checkbox">
<input type="checkbox" id="commit_tracker_enabled" name="commit_tracker_enabled"
[(ngModel)]="team.commitTrackerEnabled">
<label for="commit_tracker_enabled">Commit-tracker</label>
</div>
</div>
<div class="three wide field">
<label for="team_name"> </label>
<div class="ui checkbox">
<input type="checkbox" id="watched_enabled" name="watched_enabled"
[(ngModel)]="team.watchedEnabled">
<label for="watched_enabled">Watched</label>
</div>
</div>
</div>
<ng-container *ngIf="team.kanbanEnabled">
<h3 class="ui dividing header">Kanban configuration</h3>
<div class="six wide field" [class.error]="checkError(filterId)">
<label for="filter_id">Jira filter id</label>
<input id="filter_id" type="number" name="filter_id"
required minlength="4" min="1" [required]="team.kanbanEnabled"
[(ngModel)]="team.filterId" #filterId="ngModel">
</div>
<h5 class="ui dividing header">Daily standup timer</h5>
<div class="three inline fields">
<div class="two wide field">
<div class="ui checkbox">
<input type="checkbox" id="daily_lock_timer" name="daily_lock_timer"
[(ngModel)]="team.dailyLockEnabled">
<label for="daily_lock_timer">Enabled</label>
</div>
</div>
<div class="four wide field" [class.error]="checkError(startTime)">
<label for="daily_start">Starts</label>
<div class="ui left icon input">
<input type="time" id="daily_start" name="daily_start"
min="9:00" max="15:00" [required]="team.dailyLockEnabled && team.kanbanEnabled"
[(ngModel)]="team.dailyStartTime" #startTime="ngModel">
<i class="time icon"></i>
</div>
</div>
<div class="four wide field" [class.error]="checkError(endTime)">
<label for="daily_end">Ends</label>
<div class="ui left icon input">
<input type="time" id="daily_end" name="daily_end"
min="9:00" max="15:00" [required]="team.dailyLockEnabled && team.kanbanEnabled"
[(ngModel)]="team.dailyEndTime" #endTime="ngModel">
<i class="time icon"></i>
</div>
</div>
</div>
<h4 class="ui dividing header">Columns</h4>
<div class="four fields">
<div class="six wide field">
<label>Jira status</label>
<input type="text" name="column1_js"
placeholder="Jira column name" [(ngModel)]="team.backlogColumn.jiraStatusName">
</div>
<div class="four wide field">
<label>Display name</label>
<input type="text" name="column1_l"
placeholder="Kanban board header" [(ngModel)]="team.backlogColumn.label">
</div>
<div class="two wide field">
<label>WIP limit</label>
<input type="text" name="column1_wip"
placeholder="WIP limit" [(ngModel)]="team.backlogColumn.wipLimit">
</div>
</div>
<div class="four fields">
<div class="six wide field">
<input type="text" name="column2_js"
placeholder="Jira column name" [(ngModel)]="team.inprogressColumn.jiraStatusName">
</div>
<div class="four wide field">
<input type="text" name="column2_l"
placeholder="Kanban board header" [(ngModel)]="team.inprogressColumn.label">
</div>
<div class="two wide field">
<input type="text" name="column2_wip"
placeholder="WIP limit" [(ngModel)]="team.inprogressColumn.wipLimit">
</div>
</div>
<div class="four fields">
<div class="six wide field">
<input type="text" name="column3_js"
placeholder="Jira column name" [(ngModel)]="team.verificationColumn.jiraStatusName">
</div>
<div class="four wide field">
<input type="text" name="column3_l"
placeholder="Kanban board header" [(ngModel)]="team.verificationColumn.label">
</div>
<div class="two wide field">
<input type="text" name="column3_wip"
placeholder="WIP limit" [(ngModel)]="team.verificationColumn.wipLimit">
</div>
</div>
<div class="four fields">
<div class="six wide field">
<input type="text" name="column4_js"
placeholder="Jira column name" [(ngModel)]="team.doneColumn.jiraStatusName">
</div>
<div class="four wide field">
<input type="text" name="column4_l"
placeholder="Kanban board header" [(ngModel)]="team.doneColumn.label">
</div>
<div class="two wide field">
<input type="text" name="column4_wip"
placeholder="WIP limit" [(ngModel)]="team.doneColumn.wipLimit">
</div>
</div>
<h4 class="ui dividing header">Team members</h4>
<h4 class="ui dividing header">Labels</h4>
<div class="three inline fields">
<div class="two wide field">
<button type="button" class="ui fluid button"
[class.positive]="canAddLabel"
[class.disabled]="!canAddLabel"
(keydown.enter)="handleEnter($event)"
(click)="addLabel()">Add
</button>
</div>
<div class="five wide field">
<input type="text" #labelInput
name="label_name"
placeholder="Label text"
(keydown.enter)="handleLabelEnter($event)"
[(ngModel)]="label.name">
</div>
<ng-template let-option #optionTemplate>
<span class="ui tiny {{option}} label">{{option}}</span>
</ng-template>
<sui-select class="ui right floated selection"
id="label_color"
name="label_color"
[(ngModel)]="label.color"
[optionTemplate]="optionTemplate"
[isSearchable]="false"
#labelSelect>
<sui-select-option *ngFor="let labelColor of labelColors" [value]="labelColor"></sui-select-option>
</sui-select>
</div>
<div>
<span *ngFor="let label of team.labels"
class="ui medium {{label.color}} label">{{label.name}}<i class="large delete icon"
(click)="removeLabel(label)"></i></span>
</div>
</ng-container>
<h3 class="ui dividing header">Team members</h3>
<div class="three inline fields">
<div class="two wide field">
<button type="button" class="ui fluid button"
@ -39,6 +205,7 @@
[(ngModel)]="member.name">
</div>
</div>
<h4 class="ui dividing header"></h4>
<table class="ui celled definition table" *ngIf="team.members.length">
<thead>
@ -59,8 +226,8 @@
</table>
<button type="submit" class="ui button"
[class.positive]="canSave"
[class.disabled]="!canSave"><i class="save outline icon"></i>Save changes
[class.positive]="canSave(f)"
[class.disabled]="!canSave(f)"><i class="save outline icon"></i>Save changes
</button>
<a class="ui button"
[routerLink]="['/admin/teams']"><i class="left angle icon"></i>Back to teams list</a>

72
src/app/admin/team-editor/team-editor.component.ts Normal file → Executable file
View File

@ -1,11 +1,12 @@
import { Component, ElementRef, HostBinding, OnInit, ViewChild } from '@angular/core';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { TeamService } from '../../shared/service/team.service';
import { Team } from '../../shared/team';
import { Member } from '../../shared/member';
import { slideInOutAnimation } from '../../shared/slide-in-out-animation';
import { Label } from '../../shared/label';
import {NgForm, NgModel} from '@angular/forms';
@Component({
selector: 'app-team-editor',
@ -13,8 +14,10 @@ import { slideInOutAnimation } from '../../shared/slide-in-out-animation';
styleUrls: ['./team-editor.component.css']
})
export class TeamEditorComponent implements OnInit {
@ViewChild('labelInput') labelInputElement: ElementRef;
@ViewChild('signumInput') signumInputElement: ElementRef;
public team: Team;
public label: Label = new Label();
public member: Member = new Member();
constructor(private teamService: TeamService,
@ -28,6 +31,57 @@ export class TeamEditorComponent implements OnInit {
this.route.data.subscribe((data: { team: Team }) => this.team = data.team ? data.team : new Team());
}
get labelColors(): Array<string> {
return [
'red',
'orange',
'yellow',
'olive',
'green',
'teal',
'blue',
'violet',
'purple',
'pink',
'brown',
'grey',
'black',
'white'
];
}
get canAddLabel(): boolean {
try {
return [this.label.name, this.label.color].every(field => field.length !== 0)
&& this.team.labels.every(label => label.name !== this.label.name);
} catch (e) {
return false;
}
}
public addLabel() {
this.team.labels = this.team.labels
.concat(Object.assign({}, this.label))
.sort((a: Label, b: Label) => a.name < b.name ? -1 : 1);
this.label = new Label();
}
public removeLabel(label: Label) {
this.team.labels = this.team.labels.filter(teamLabel => teamLabel !== label);
}
public handleLabelEnter(ev: KeyboardEvent) {
ev.preventDefault();
if (this.canAddLabel) {
this.addLabel();
this.focusLabelField();
}
}
public focusLabelField() {
this.labelInputElement.nativeElement.focus();
}
get canAddMember(): boolean {
try {
return [this.member.name, this.member.signum].every(field => field.length !== 0)
@ -62,18 +116,22 @@ export class TeamEditorComponent implements OnInit {
}
}
get canSave(): boolean {
return [
public canSave(form: NgForm): boolean {
return form.valid && [
this.team.name.trim(),
this.team.members
].every(field => field.length > 0);
].every(field => field.length > 0) && this.team.filterId > 0;
}
public saveTeam() {
if (this.canSave) {
public saveTeam(form: NgForm) {
if (this.canSave(form)) {
this.teamService.persist(this.team).subscribe(
() => this.router.navigate(['/admin/teams'])
);
}
}
public checkError(fieldModel: NgModel): boolean {
return fieldModel.invalid && (fieldModel.dirty || fieldModel.touched);
}
}

2
src/app/admin/team-resolver.service.ts Normal file → Executable file
View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { Team } from '../shared/team';
import { environment } from '../../environments/environment';

9
src/app/app.component.css Normal file → Executable file
View File

@ -0,0 +1,9 @@
.pause-indicator {
position: fixed;
width: 100%;
z-index: 999;
background-color: lightgrey;
color: red;
text-align: center;
font-weight: bold;
}

5
src/app/app.component.html Normal file → Executable file
View File

@ -1 +1,4 @@
<router-outlet></router-outlet>
<div class="pause-indicator" *ngIf="paused">Slideshow is paused</div>
<main [@routerTransition]="getAnimationData(o)">
<router-outlet #o="outlet"></router-outlet>
</main>

51
src/app/app.component.ts Normal file → Executable file
View File

@ -1,12 +1,57 @@
import { Component, OnInit } from '@angular/core';
import { Component, HostListener, OnInit } from '@angular/core';
import { TimerService } from './shared/service/timer.service';
import { SlideShowService } from './display/slide-show.service';
import { Router, RouterOutlet } from '@angular/router';
import { slideInOutAnimation } from './shared/slide-in-out-animation';
import { AnimationDirection, SettingsService } from './shared/service/settings.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
styleUrls: ['./app.component.css'],
animations: [slideInOutAnimation],
})
export class AppComponent implements OnInit {
constructor(private timerService: TimerService) {}
constructor(private timerService: TimerService,
private slideShowService: SlideShowService,
private settings: SettingsService,
private router: Router) {}
public ngOnInit() {}
@HostListener('document:keyup', ['$event.key'])
private keyPressed(key: string) {
if (this.timerService.autoSwitch) {
switch (key) {
case 'Home':
this.router.navigate(['/dashboard']);
break;
case ' ':
this.timerService.togglePause();
break;
case 'ArrowLeft':
this.timerService.pause();
this.slideShowService.prevSlide();
break;
case 'ArrowRight':
this.timerService.pause();
this.slideShowService.nextSlide();
break;
}
}
}
public get paused(): boolean {
return this.timerService.paused;
}
public getAnimationData(outlet: RouterOutlet) {
return {
value: outlet.activatedRouteData.state ? outlet.activatedRouteData.state : false,
params: {
offsetEnter: this.settings.animationDirection === AnimationDirection.LEFT ? -100 : 100,
offsetLeave: this.settings.animationDirection === AnimationDirection.LEFT ? 100 : -100,
}
};
}
}

View File

@ -1,5 +1,11 @@
:host {
background-color: #444;
position: fixed;
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.ui.label.inprogress {
@ -43,6 +49,8 @@
.ui.jira-avatar.image > img {
border-radius: 4px;
max-width: 45px;
height: auto;
}
@ -56,4 +64,4 @@
opacity: 0;
width: 100%;
}
}
}

View File

@ -1,5 +1,5 @@
<div class="ui main wide-container dark">
<table *ngIf="commits?.length" class="ui large padded inverted celled2 table">
<table *ngIf="commits?.length" class="ui large padded inverted table">
<thead>
<tr>
<th class="collapsing"><i class="user icon"></i>Owner</th>
@ -43,4 +43,4 @@
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -1,17 +1,15 @@
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { TimerObservable } from 'rxjs/observable/TimerObservable';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Title} from '@angular/platform-browser';
import {ActivatedRoute} from '@angular/router';
import {Subscription, timer} from 'rxjs';
import { slideInOutAnimation } from '../../shared/slide-in-out-animation';
import { CommitTrackerService } from '../../shared/service/commit-tracker.service';
import { SettingsService } from '../../shared/service/settings.service';
import { Commit } from '../../shared/commit';
import { CommitStatus } from '../../shared/commit-status.enum';
import { Result } from '../../shared/result.enum';
import { Build } from '../../shared/build';
import { environment } from '../../../environments/environment';
import {CommitTrackerService} from '../../shared/service/commit-tracker.service';
import {SettingsService} from '../../shared/service/settings.service';
import {Commit} from '../../shared/commit';
import {CommitStatus} from '../../shared/commit-status.enum';
import {Result} from '../../shared/result.enum';
import {Build} from '../../shared/build';
import {environment} from '../../../environments/environment';
const TIMER_COMMITTRACKER_REFRESH = 10000;
const DEFAULT_AVATAR = '/assets/riddler.png';
@ -20,16 +18,12 @@ const DEFAULT_AVATAR = '/assets/riddler.png';
selector: 'app-commit-tracker',
templateUrl: './commit-tracker.component.html',
styleUrls: ['./commit-tracker.component.css'],
animations: [slideInOutAnimation]
})
export class CommitTrackerComponent implements OnInit, OnDestroy {
public CommitStatus = CommitStatus;
private refreshCommitTrackerTimer: Subscription;
@HostBinding('@slideInOutAnimation')
slideIn = false;
constructor(private commitTrackerService: CommitTrackerService,
private settings: SettingsService,
private titleService: Title,
@ -40,7 +34,7 @@ export class CommitTrackerComponent implements OnInit, OnDestroy {
this.titleService.setTitle('Commit-tracker : MTAStv');
this.route.data.subscribe((data: { commits: Array<Commit> }) => this.commits = data.commits);
const timerCT = TimerObservable.create(TIMER_COMMITTRACKER_REFRESH, TIMER_COMMITTRACKER_REFRESH);
const timerCT = timer(TIMER_COMMITTRACKER_REFRESH, TIMER_COMMITTRACKER_REFRESH);
this.refreshCommitTrackerTimer = timerCT.subscribe(() => {
this.commitTrackerService.getTeamCommits(this.settings.team.members.map(member => member.signum))
.subscribe(commits => this.commits = commits);

60
src/app/display/display-routing.module.ts Normal file → Executable file
View File

@ -9,87 +9,117 @@ 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';
import { WatchersComponent } from './watchers/watchers.component';
import { WatcherService } from './shared/watcher.service';
const routes: Routes = [
{
path: 'slideshow/:id',
component: SlideShowComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
slide: SlideResolverService,
},
data: {
autoSwitchable: false
autoSwitchable: false,
state: 'slideshow',
}
}, {
path: 'slideshow-odd/:id',
component: SlideShowComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
slide: SlideResolverService,
},
data: {
autoSwitchable: true
autoSwitchable: true,
state: 'slideshow-odd',
}
}, {
path: 'slideshow-even/:id',
component: SlideShowComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
slide: SlideResolverService,
},
data: {
autoSwitchable: true
autoSwitchable: true,
state: 'slideshow-even',
}
}, {
path: 'commit-tracker',
component: CommitTrackerComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
commits: CommitTrackerService,
},
data: {
autoSwitchable: true
autoSwitchable: true,
state: 'commit-tracker',
}
}, {
path: 'commit-tracker-fixed',
component: CommitTrackerComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
commits: CommitTrackerService,
},
data: {
autoSwitchable: false,
state: 'commit-tracker-fixed',
}
}, {
path: 'kanban',
component: KanbanBoardComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
kanbanBoard: KanbanService,
},
data: {
autoSwitchable: true
autoSwitchable: true,
state: 'kanban',
}
}, {
path: 'kanban-fixed',
component: KanbanBoardComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
kanbanBoard: KanbanService,
},
data: {
autoSwitchable: false
autoSwitchable: false,
state: 'kanban-fixed',
}
}, {
path: 'watchers',
component: WatchersComponent,
resolve: {
watchers: WatcherService,
},
data: {
autoSwitchable: true,
state: 'watchers',
}
}, {
path: 'watchers-fixed',
component: WatchersComponent,
resolve: {
watchers: WatcherService,
},
data: {
autoSwitchable: false,
state: 'watchers-fixed',
}
}, {
path: 'settings',
component: SettingsComponent,
// canActivate: [AuthGuardService, RoleGuardService],
resolve: {
teams: TeamService,
},
data: {
autoSwitchable: true,
// state: 'settings',
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DisplayRoutingModule { }
export class DisplayRoutingModule {}

6
src/app/display/display.module.ts Normal file → Executable file
View File

@ -16,6 +16,8 @@ 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';
import { SlideIframeComponent } from './slide-iframe/slide-iframe.component';
import { WatchersComponent } from './watchers/watchers.component';
@NgModule({
imports: [
@ -24,7 +26,7 @@ import { KanbanService } from './shared';
SuiModule,
DisplayRoutingModule
],
exports: [SlideComponent],
exports: [SlideComponent, SlideIframeComponent],
declarations: [
CommitTrackerComponent,
SettingsComponent,
@ -37,6 +39,8 @@ import { KanbanService } from './shared';
PrefixJiraIdPipe,
PriorityColorPipe,
ShortenTextPipe,
SlideIframeComponent,
WatchersComponent,
],
providers: [
SlideShowService,

10
src/app/display/kanban-board/kanban-board.component.css Normal file → Executable file
View File

@ -1,7 +1,13 @@
:host {
display: inline-block;
position: fixed;
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100vh;
overflow: hidden;
overflow-x: hidden;
overflow-y: scroll;
background-color: #444;
padding: 10px;
}

View File

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

46
src/app/display/kanban-board/kanban-board.component.ts Normal file → Executable file
View File

@ -1,34 +1,28 @@
import { Component, HostBinding, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import {Component, OnInit} from '@angular/core';
import {Title} from '@angular/platform-browser';
import {ActivatedRoute} from '@angular/router';
import { KanbanBoard, KanbanEntry, KanbanService, } from '../shared';
import { slideInOutAnimation } from '../../shared/slide-in-out-animation';
const WIP_LIMIT_INPROGRESS = 12;
const WIP_LIMIT_VERIFICATION = 8;
import {KanbanBoard, KanbanEntry, KanbanService,} from '../shared';
import {SettingsService} from '../../shared/service/settings.service';
@Component({
selector: 'app-kanban-board',
templateUrl: './kanban-board.component.html',
styleUrls: ['./kanban-board.component.css'],
animations: [slideInOutAnimation]
})
export class KanbanBoardComponent implements OnInit {
@HostBinding('@slideInOutAnimation')
slideIn = true;
constructor(private titleService: Title,
private route: ActivatedRoute,
private kanbanService: KanbanService) {
private kanbanService: KanbanService,
private settings: SettingsService) {
}
/**
* Set page title, and handle preloaded kanbanBoard data
*/
ngOnInit() {
this.titleService.setTitle('TaurusXFT : Kanban board');
this.titleService.setTitle(`${this.settings.team.name} : Kanban board`);
this.route.data.subscribe((data: {
kanbanBoard: KanbanBoard,
}) => {
@ -45,7 +39,7 @@ export class KanbanBoardComponent implements OnInit {
}
get inprogressWipLimit(): number {
return WIP_LIMIT_INPROGRESS;
return this.settings.team.inprogressColumn.wipLimit;
}
get inprogressWipCount(): number {
@ -64,12 +58,12 @@ export class KanbanBoardComponent implements OnInit {
*/
get inprogressWipClass() {
return {
'over-wip': this.inprogressWipCount > WIP_LIMIT_INPROGRESS,
'over-wip': this.inprogressWipCount > this.settings.team.inprogressColumn.wipLimit,
};
}
get verificationWipLimit(): number {
return WIP_LIMIT_VERIFICATION;
return this.settings.team.verificationColumn.wipLimit;
}
get verificationWipCount(): number {
@ -88,7 +82,23 @@ export class KanbanBoardComponent implements OnInit {
*/
get verificationWipClass() {
return {
'over-wip': this.verificationWipCount > WIP_LIMIT_VERIFICATION,
'over-wip': this.verificationWipCount > this.settings.team.verificationColumn.wipLimit,
};
}
get backlogLabel(): string {
return this.settings.team.backlogColumn.label;
}
get inProgressLabel(): string {
return this.settings.team.inprogressColumn.label;
}
get verificationLabel(): string {
return this.settings.team.verificationColumn.label;
}
get doneLabel(): string {
return this.settings.team.doneColumn.label;
}
}

View File

@ -10,10 +10,10 @@
[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="kanbanEntry.epicName"
class="ui mini olive right floated label">{{kanbanEntry.epicName}}</span>
<span *ngFor="let label of filteredLabels(kanbanEntry)"
class="ui mini {{labelClass(label)}} right floated label">{{label|blockedDays:kanbanEntry.daysBlocked}}</span>
<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">

View File

@ -2,19 +2,10 @@ import { Component, Input } from '@angular/core';
import { environment } from '../../../environments/environment';
import { JiraAssignee, KanbanEntry } from '../shared';
import { SettingsService } from '../../shared/service/settings.service';
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',
};
const JIRA_BOARD_BASE_HREF = 'https://cc-jira.rnd.ki.sw.ericsson.se/browse/';
@Component({
selector: 'app-kanban-entry-item,[app-kanban-entry-item]',
@ -27,8 +18,7 @@ export class KanbanEntryItemComponent {
@Input() wipLimit = 0;
@Input() wipCount = 0;
constructor() {
}
constructor(private settingService: SettingsService) {}
/**
* Returns the full url of the assignee avatar,
@ -38,17 +28,9 @@ export class KanbanEntryItemComponent {
* @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;
return environment.apiUrl + (avatarPath
? avatarPath
: DEFAULT_AVATAR);
}
/**
@ -58,11 +40,16 @@ export class KanbanEntryItemComponent {
* @returns {string}
*/
public labelClass(label: string): string {
try {
return labelColors[label.toUpperCase()];
} catch (e) {
return 'white';
if (this.settingService.team.labels) {
const color = this.settingService.team.labels.find(
teamLabel => teamLabel.name.toLocaleLowerCase() === label.toLocaleLowerCase()
).color;
if (color !== null) {
return color;
}
}
return 'white';
}
/**
@ -77,6 +64,15 @@ export class KanbanEntryItemComponent {
};
}
public filteredLabels(kanbanEntry: KanbanEntry): Array<string> {
if (this.settingService.team.labels) {
return kanbanEntry.labels.filter(entryLabel => this.settingService.team.labels.some(
teamLabel => teamLabel.name.toLocaleLowerCase() === entryLabel.toLocaleLowerCase()
));
}
return [];
}
/**
* Generate jira issue href
*
@ -119,4 +115,5 @@ export class KanbanEntryItemComponent {
public getAssignees(kanbanEntry: KanbanEntry): Array<JiraAssignee> {
return [].concat([kanbanEntry.assignee], kanbanEntry.additionalAssignees);
}
}

2
src/app/display/settings/settings.component.html Normal file → Executable file
View File

@ -18,7 +18,7 @@
<div class="four wide field">
<label for="slide_interval">Slide duration: {{slideInterval}}ms</label>
<input id="slide_interval" name="slide_interval"
type="range" min="1000" max="120000" step="100"
type="range" min="5000" max="120000" step="250"
[(ngModel)]="slideInterval">
</div>
</div>

10
src/app/display/settings/settings.component.ts Normal file → Executable file
View File

@ -15,20 +15,20 @@ export class SettingsComponent implements OnInit {
public slideInterval: number;
constructor(private route: ActivatedRoute,
private settingsService: SettingsService,
private settings: SettingsService,
private router: Router) {}
ngOnInit() {
this.route.data.subscribe((data: {teams: Array<Team>}) => {
this.teams = data.teams;
this.selectedTeam = this.teams.find(team => team.id === this.settingsService.team.id);
this.slideInterval = this.settingsService.slideInterval;
this.selectedTeam = this.teams.find(team => team.id === this.settings.team.id);
this.slideInterval = this.settings.slideInterval;
});
}
public saveSettings() {
this.settingsService.team = this.selectedTeam;
this.settingsService.slideInterval = this.slideInterval;
this.settings.team = this.selectedTeam;
this.settings.slideInterval = this.slideInterval;
this.router.navigate(['/admin']);
}
}

2
src/app/display/shared/kanban-board.model.ts Normal file → Executable file
View File

@ -1,4 +1,4 @@
import {KanbanEntry} from "./kanban-entry.model";
import {KanbanEntry} from './kanban-entry.model';
export class KanbanBoard {
public inbox: Array<KanbanEntry>;

7
src/app/display/shared/kanban-entry.model.ts Normal file → Executable file
View File

@ -1,12 +1,13 @@
import {JiraIssueType} from "./jira-issue-type.model";
import {JiraStatus} from "./jira-status.model";
import {JiraAssignee} from "./jira-assignee.model";
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 epicName: string;
public status: JiraStatus;
public assignee: JiraAssignee;
public additionalAssignees: Array<JiraAssignee> = [];

14
src/app/display/shared/kanban.service.ts Normal file → Executable file
View File

@ -1,10 +1,13 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { KanbanBoard } from './kanban-board.model';
import { SettingsService } from '../../shared/service/settings.service';
import { TeamService } from '../../shared/service/team.service';
import { flatMap } from 'rxjs/operators';
@Injectable()
export class KanbanService {
@ -12,16 +15,21 @@ export class KanbanService {
private cachedKanbanBoard: KanbanBoard = new KanbanBoard();
constructor(private httpService: HttpClient) {
constructor(private httpService: HttpClient,
private teamService: TeamService,
private settingService: SettingsService) {
}
/**
* Returns an observable instance to the kanban board api
* Reloads team data before, to refresh team config
*
* @returns {Observable<KanbanBoard>}
*/
public getList(): Observable<KanbanBoard> {
return this.httpService.get<KanbanBoard>(this.url);
return this.teamService.get(this.settingService.team.id).pipe(
flatMap(() => this.httpService.get<KanbanBoard>(`${this.url}/${this.settingService.team.id}`))
);
}
/**

View File

@ -0,0 +1,13 @@
export class WatchedIssue {
public issue = '';
public summary = '';
public assignee = '';
public comment: WatchedIssueComment;
}
export class WatchedIssueComment {
public signum = '';
public name = '';
public content = '';
public date = '';
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { WatcherService } from './watcher.service';
describe('WatcherService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [WatcherService]
});
});
it('should be created', inject([WatcherService], (service: WatcherService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { KanbanBoard } from './kanban-board.model';
import { SettingsService } from '../../shared/service/settings.service';
import { TeamService } from '../../shared/service/team.service';
import { flatMap } from 'rxjs/operators';
import {WatchedIssue} from './watched-issue.model';
@Injectable({
providedIn: 'root'
})
export class WatcherService {
private url = environment.apiUrl + '/api/watched';
private cachedWatchers: Array<WatchedIssue> = [];
constructor(private httpService: HttpClient,
private teamService: TeamService,
private settingService: SettingsService) {
}
/**
* Returns an observable instance to the kanban board api
* Reloads team data before, to refresh team config
*
* @returns {Observable<KanbanBoard>}
*/
public getList(): Observable<Array<WatchedIssue>> {
return this.teamService.get(this.settingService.team.id).pipe(
flatMap(() => this.httpService.get<Array<WatchedIssue>>(`${this.url}/${this.settingService.team.id}`))
);
}
/**
* Route preload resolver
*
* @param {ActivatedRouteSnapshot} route
* @returns {Promise<KanbanBoard>}
*/
public resolve(route: ActivatedRouteSnapshot): Promise<Array<WatchedIssue> | boolean> {
return this.getList().toPromise().then(result => result ? result : false);
}
/**
* Reload the board
*/
public reload() {
this.getList().subscribe(result => this.cachedWatchers = result);
}
get watchers(): Array<WatchedIssue> {
return this.cachedWatchers;
}
set watchers(kanbanBoard: Array<WatchedIssue>) {
this.cachedWatchers = kanbanBoard;
}
}

View File

@ -0,0 +1,21 @@
:host {
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
padding: 3px;
background: #222;
}
:host.preview {
position: absolute;
z-index: 1;
padding: 30px;
}
iframe {
width: 100%;
height: 100%;
border: 0;
}

View File

@ -0,0 +1 @@
<iframe [src]="sanitizedUrl"></iframe>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SlideIframeComponent } from './slide-iframe.component';
describe('SlideIframeComponent', () => {
let component: SlideIframeComponent;
let fixture: ComponentFixture<SlideIframeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SlideIframeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SlideIframeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,29 @@
import {Component, EventEmitter, HostListener, Input, OnInit, Output} from '@angular/core';
import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
@Component({
selector: 'app-slide-iframe',
templateUrl: './slide-iframe.component.html',
styleUrls: ['./slide-iframe.component.css']
})
export class SlideIframeComponent implements OnInit {
@Input() data = '';
@Input() preview = false;
@Input() visible = true;
@Output() visibleChange = new EventEmitter();
constructor(private sanitizer: DomSanitizer) {}
ngOnInit() {}
@HostListener('click')
public hide() {
if (this.preview) {
this.visibleChange.emit(false);
}
}
get sanitizedUrl(): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(this.data);
}
}

88
src/app/display/slide-show.service.ts Normal file → Executable file
View File

@ -1,46 +1,86 @@
import { Injectable } from '@angular/core';
import { Slide } from '../shared/slide';
import { Slide, SlideVisibility } from '../shared/slide';
import { SlideService } from '../shared/service/slide.service';
import { Router } from '@angular/router';
import { SettingsService } from '../shared/service/settings.service';
import { AnimationDirection, SettingsService } from '../shared/service/settings.service';
import { TwoWayLinkedList } from '../shared/two-way-linked-list';
import { SlideWrapper, WrappedType } from '../shared/slide-wrapper';
@Injectable()
export class SlideShowService {
private oddEven = false;
private currentSlideIndex = -1;
private slides: Array<Slide> = [];
private cachedSlides: TwoWayLinkedList<SlideWrapper> = new TwoWayLinkedList<SlideWrapper>();
constructor(private slideService: SlideService,
private settingsService: SettingsService,
private settings: SettingsService,
private router: Router) {
this.reloadSlides();
}
public nextSlide() {
if (this.currentSlideIndex === this.slides.length - 1) {
// this.currentSlideIndex++;
// this.router.navigate(['/kanban']);
// } else if (this.currentSlideIndex === this.slides.length) {
this.currentSlideIndex = -1;
this.reloadSlides();
this.router.navigate(['/commit-tracker']);
public startWithFirstSlide() {
this.settings.animationDirection = AnimationDirection.RIGHT;
this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.first));
}
public prevSlide() {
this.settings.animationDirection = AnimationDirection.LEFT;
if (this.cachedSlides.isFirst()) {
this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.last));
} else {
this.oddEven = !this.oddEven;
this.currentSlideIndex++;
this.router.navigate([
this.oddEven ? '/slideshow-odd' : '/slideshow-even',
this.slides[this.currentSlideIndex].id
]);
const prevSlide = this.cachedSlides.prev();
this.switchToWrappedSlide(prevSlide);
}
}
private reloadSlides() {
const team = this.settingsService.team;
public nextSlide() {
this.settings.animationDirection = AnimationDirection.RIGHT;
if (this.cachedSlides.isLast()) {
this.reloadSlides(() => this.switchToWrappedSlide(this.cachedSlides.first));
} else {
const nextSlide = this.cachedSlides.next();
this.switchToWrappedSlide(nextSlide);
}
}
private switchToWrappedSlide(wrappedSlide: SlideWrapper) {
if (WrappedType.BUILTIN === wrappedSlide.type) {
this.router.navigate([wrappedSlide.slideRoute]);
} else if (WrappedType.USER === wrappedSlide.type) {
this.oddEven = !this.oddEven;
this.router.navigate([
this.oddEven ? '/slideshow-odd' : '/slideshow-even',
wrappedSlide.slideData.id
]);
} else {
throw Error(`Unknown slide type: ${wrappedSlide.type}`);
}
}
private reloadSlides(onReloadFinish: () => void = null) {
this.slideService.list().subscribe(
slides => this.slides = slides.filter(
slide => slide.team === null || slide.team.id === team.id
)
slides => {
this.cachedSlides.clear();
slides.filter(
(slide: Slide) => slide.isVisible && (slide.visibility === SlideVisibility.Public || slide.teams.some(
s => s.id === this.settings.team.id
))
).map(slide => this.cachedSlides.push(new SlideWrapper(WrappedType.USER, slide)));
if (this.settings.team.kanbanEnabled) {
this.cachedSlides.push(new SlideWrapper(WrappedType.BUILTIN, '/kanban'));
}
if (this.settings.team.commitTrackerEnabled) {
this.cachedSlides.push(new SlideWrapper(WrappedType.BUILTIN, '/commit-tracker'));
}
if (this.settings.team.watchedEnabled) {
this.cachedSlides.push(new SlideWrapper(WrappedType.BUILTIN, '/watchers'));
}
if (null !== onReloadFinish) {
onReloadFinish();
}
}
);
}
}

8
src/app/display/slide-show/slide-show.component.css Normal file → Executable file
View File

@ -0,0 +1,8 @@
:host {
position: fixed;
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

3
src/app/display/slide-show/slide-show.component.html Normal file → Executable file
View File

@ -1 +1,2 @@
<app-slide [data]="renderedSlide"></app-slide>
<app-slide *ngIf="isMarkdown" [data]="renderedSlide"></app-slide>
<app-slide-iframe *ngIf="isIframe" [data]="slide.slideUrl"></app-slide-iframe>

21
src/app/display/slide-show/slide-show.component.ts Normal file → Executable file
View File

@ -1,24 +1,19 @@
import { Component, HostBinding, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Title} from '@angular/platform-browser';
import * as marked from 'marked';
import { slideInOutAnimation } from '../../shared/slide-in-out-animation';
import { Slide } from '../../shared/slide';
import {Slide, SlideType} from '../../shared/slide';
@Component({
selector: 'app-slide-show',
templateUrl: './slide-show.component.html',
styleUrls: ['./slide-show.component.css'],
animations: [slideInOutAnimation]
})
export class SlideShowComponent implements OnInit {
private md;
public slide: Slide;
@HostBinding('@slideInOutAnimation')
slideIn = true;
constructor(private route: ActivatedRoute,
private titleService: Title) {
this.md = marked.setOptions({});
@ -34,4 +29,12 @@ export class SlideShowComponent implements OnInit {
get renderedSlide(): String {
return this.md.parse(this.slide.slideData);
}
get isMarkdown(): boolean {
return this.slide.type === SlideType.MarkDown;
}
get isIframe(): boolean {
return this.slide.type === SlideType.IFrame;
}
}

1
src/app/display/slide/slide.component.ts Normal file → Executable file
View File

@ -12,7 +12,6 @@ export class SlideComponent implements OnInit {
@Input() visible = true;
@Output() visibleChange = new EventEmitter();
constructor() {}
ngOnInit() {}

View File

@ -0,0 +1,58 @@
:host {
background-color: #444;
position: fixed;
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* avatar */
.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;
max-width: 45px;
height: auto;
}
/* items */
.ui.items .item .header {
color: white;
}
.ui.items .item .meta {
color: lightgray;
}
/* comments */
.ui.comments .comment .metadata {
color: lightgray;
}
.ui.comments .comment .text {
color: white;
}
.ui.comments .comment .author {
color: white;
}
/* header */
h1.massive {
font-size: 72px;
position: absolute;
top: calc(50% - 157px);
left: calc(50% - 541px);
}
.ui.comments {
max-width: initial;
}

View File

@ -0,0 +1,48 @@
<div class="ui main wide-container dark">
<table class="ui large padded inverted celled2 table" *ngIf="watchers.length">
<thead>
<tr>
<th class="collapsing"><i class="user icon"></i></th>
<th><i class="tasks icon"></i>JIRA</th>
<th><i class="envelope open outline icon"></i>Last comment</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let issue of watchers">
<td class="collapsing">
<div class="ui jira-avatar image">
<img src="{{avatarUrl(issue.assignee)}}" [title]="issue.assignee">
</div>
</td>
<td class="five wide">
<div class="ui items">
<div class="item">
<div class="content">
<a class="header">{{issue.issue}}</a>
<div class="meta">{{issue.summary}}</div>
</div>
</div>
</div>
</td>
<td>
<div class="ui fluid comments">
<div class="comment">
<a class="avatar"><img src="{{avatarUrl(issue.comment.signum)}}"></a>
<div class="content">
<span class="author">{{issue.comment.name}}</span>
<div class="metadata"><span class="date">{{issue.comment.date}}</span></div>
<div class="text">{{issue.comment.content|shortenText:500}}</div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div *ngIf="!watchers.length">
<h1 class="ui massive green center aligned icon header">
<i class="ui massive check circle outline icon"></i>
<div class="content">No watched item needs attention.</div>
</h1>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { WatchersComponent } from './watchers.component';
describe('WatchersComponent', () => {
let component: WatchersComponent;
let fixture: ComponentFixture<WatchersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ WatchersComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WatchersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,43 @@
import {Component, OnInit} from '@angular/core';
import {Title} from '@angular/platform-browser';
import {ActivatedRoute} from '@angular/router';
import {SettingsService} from '../../shared/service/settings.service';
import {WatcherService} from '../shared/watcher.service';
import {WatchedIssue} from '../shared/watched-issue.model';
import {environment} from '../../../environments/environment';
const DEFAULT_AVATAR = '/assets/riddler.png';
@Component({
selector: 'app-watchers',
templateUrl: './watchers.component.html',
styleUrls: ['./watchers.component.css'],
})
export class WatchersComponent implements OnInit {
constructor(private titleService: Title,
private route: ActivatedRoute,
private watcherService: WatcherService,
private settingService: SettingsService) { }
ngOnInit() {
this.titleService.setTitle(`${this.settingService.team.name} : Watched issue activity`);
this.route.data.subscribe((data: {
watchers: Array<WatchedIssue>,
}) => {
this.watchers = data.watchers;
});
}
get watchers(): Array<WatchedIssue> {
return this.watcherService.watchers;
}
set watchers(watchers: Array<WatchedIssue>) {
this.watcherService.watchers = watchers;
}
public avatarUrl(signum: string): string {
return environment.apiUrl + (signum ? `/avatars/${signum}` : DEFAULT_AVATAR);
}
}

View File

@ -0,0 +1,5 @@
export class KanbanColumn {
jiraStatusName = '';
label = '';
wipLimit = 0;
}

4
src/app/shared/label.ts Executable file
View File

@ -0,0 +1,4 @@
export class Label {
name: string;
color: string;
}

2
src/app/shared/service/commit-tracker.service.ts Normal file → Executable file
View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { Commit } from '../commit';

12
src/app/shared/service/self-updater.service.ts Normal file → Executable file
View File

@ -1,12 +1,12 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { Location } from '@angular/common';
import { Observable } from 'rxjs';
@Injectable()
export class SelfUpdaterService {
private appRevision: number = 0;
private appRevision = 0;
private initFailed = false;
constructor(private httpClient: HttpClient,
@ -16,7 +16,7 @@ export class SelfUpdaterService {
() => {
console.log(
'%c Couldn\'t load initial revision data from server. Self update disabled.',
'background: #222; color: #bada55;'
'background: #222; color: #FFC300;'
);
this.initFailed = true;
}
@ -31,7 +31,11 @@ export class SelfUpdaterService {
if (!this.initFailed) {
this.getDeployedRevision().subscribe(revision => {
if (revision > this.appRevision) {
this.locationService.go('/');
console.log(
`%c Version change detected (${this.appRevision}=>${revision}), reloading app.`,
'background: #222; color: #BADA55;'
);
window.location.reload(true);
}
});
}

10
src/app/shared/service/settings.service.ts Normal file → Executable file
View File

@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { Observable, Subject } from 'rxjs';
import { Team } from '../team';
const DEFAULT_SLIDE_INTERVAL = 30000;
@ -14,6 +13,8 @@ export class SettingsService {
private teamSubject: Subject<Team> = new Subject<Team>();
private intervalSubject: Subject<number> = new Subject<number>();
public animationDirection: AnimationDirection = AnimationDirection.RIGHT;
constructor() {}
get team(): Team {
@ -52,3 +53,8 @@ export class SettingsService {
return this.teamSubject.asObservable();
}
}
export enum AnimationDirection {
LEFT,
RIGHT,
}

18
src/app/shared/service/slide.service.ts Normal file → Executable file
View File

@ -1,10 +1,13 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { Slide } from '../slide';
import { TeamService } from './team.service';
import { SettingsService } from './settings.service';
import { flatMap } from 'rxjs/operators';
@Injectable()
export class SlideService implements Resolve<Array<Slide>>{
@ -13,14 +16,14 @@ export class SlideService implements Resolve<Array<Slide>>{
private apiEndPointPosition = environment.apiUrl + '/api/slide-position';
private cachedSlides: Array<Slide> = [];
constructor(private httpClient: HttpClient) {}
constructor(private httpClient: HttpClient,
private teamService: TeamService,
private settings: SettingsService) {}
private static prepareSlideData(slide: Slide) {
const slideToSave = <any>Object.assign({}, slide);
try {
slideToSave.team = slideToSave.team.id === null
? null
: slideToSave.team.id;
slideToSave.teams = slide.teams.map(team => team.id);
} catch (e) {}
return slideToSave;
}
@ -30,6 +33,11 @@ export class SlideService implements Resolve<Array<Slide>>{
}
public list(): Observable<Array<Slide>> {
if (this.settings.team.id) {
return this.teamService.get(this.settings.team.id).pipe(
flatMap( () => this.httpClient.get<Array<Slide>>(this.apiEndPoint))
);
}
return this.httpClient.get<Array<Slide>>(this.apiEndPoint);
}

20
src/app/shared/service/team.service.ts Normal file → Executable file
View File

@ -1,10 +1,12 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { Team } from '../team';
import {SettingsService} from './settings.service';
import {map} from 'rxjs/operators';
@Injectable()
export class TeamService implements Resolve<Array<Team>> {
@ -12,7 +14,8 @@ export class TeamService implements Resolve<Array<Team>> {
private apiEndPoint = environment.apiUrl + '/api/team';
private cachedTeams: Array<Team> = [];
constructor(private httpClient: HttpClient) { }
constructor(private httpClient: HttpClient,
private settingsService: SettingsService) { }
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Array<Team>> {
return this.list().toPromise();
@ -22,6 +25,12 @@ export class TeamService implements Resolve<Array<Team>> {
return this.httpClient.get<Array<Team>>(this.apiEndPoint);
}
public get(id: number): Observable<Team> {
return this.httpClient.get<Team>(`${this.apiEndPoint}/${id}`).pipe(
map(team => this.updateSettingsWhenSelected(team))
);
}
public persist(team: Team): Observable<Team> {
return team.id === null
? this.create(team)
@ -47,4 +56,11 @@ export class TeamService implements Resolve<Array<Team>> {
set teams(teams: Array<Team>) {
this.cachedTeams = teams;
}
private updateSettingsWhenSelected(team: Team): Team {
if (this.settingsService.team.id === team.id) {
this.settingsService.team = team;
}
return team;
}
}

59
src/app/shared/service/timer.service.ts Normal file → Executable file
View File

@ -1,19 +1,21 @@
import { Injectable, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { SettingsService } from './settings.service';
import { SelfUpdaterService } from './self-updater.service';
import { TimerObservable } from 'rxjs/observable/TimerObservable';
import { Subscription } from 'rxjs/Subscription';
import { Subject, timer, Subscription } from 'rxjs';
import { ActivationStart, Router } from '@angular/router';
import { SlideShowService } from '../../display/slide-show.service';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/switchMap';
import { filter, switchMap } from 'rxjs/operators';
import * as isWithinRange from 'date-fns/is_within_range';
import * as min from 'date-fns/min';
import * as max from 'date-fns/max';
const TIMER_UPDATE_POLL_INTERVAL = 30000;
const TIME_SEPARATOR = ':';
@Injectable()
export class TimerService implements OnDestroy {
private autoSwitch = false;
public paused = false;
public autoSwitch = false;
private slideShowTimer: Subscription;
private selfUpdateCheckerTimer: Subscription;
private slideTimerSubject: Subject<number> = new Subject<number>();
@ -24,18 +26,19 @@ export class TimerService implements OnDestroy {
private router: Router,
private slideShowService: SlideShowService) {
const timerSUC = TimerObservable.create(TIMER_UPDATE_POLL_INTERVAL, TIMER_UPDATE_POLL_INTERVAL);
const timerSUC = timer(TIMER_UPDATE_POLL_INTERVAL, TIMER_UPDATE_POLL_INTERVAL);
this.selfUpdateCheckerTimer = timerSUC.subscribe(() => {
this.selfUpdaterService.checkAndReloadIfNecessary();
});
this.slideShowTimer = this.slideTimerSubject.switchMap((period: number) => TimerObservable.create(period))
this.slideShowTimer = this.slideTimerSubject
.pipe(switchMap((period: number) => timer(period)))
.subscribe(() => this.changeSlide());
this.setSlideTimer(this.settings.slideInterval);
this.autoSwitch = false;
this.router.events
.filter(event => event instanceof ActivationStart)
.pipe(filter(event => event instanceof ActivationStart))
.subscribe((event: ActivationStart) => this.autoSwitch = !!event.snapshot.data.autoSwitchable);
this.slideIntervalSubscription = this.settings.slideIntervalChanged.subscribe(
@ -50,13 +53,45 @@ export class TimerService implements OnDestroy {
}
private changeSlide() {
if (this.autoSwitch) {
this.slideShowService.nextSlide();
if (!this.paused) {
if (this.autoSwitch && this.isDuringDailyStandup()) {
this.router.navigate(['/kanban']);
}
if (this.autoSwitch && !this.isDuringDailyStandup()) {
this.slideShowService.nextSlide();
}
this.setSlideTimer(this.settings.slideInterval);
}
this.setSlideTimer(this.settings.slideInterval);
}
public setSlideTimer(delay: number) {
this.slideTimerSubject.next(delay);
}
public togglePause() {
this.paused = !this.paused;
if (!this.paused) {
this.setSlideTimer(this.settings.slideInterval);
}
}
public pause() {
this.paused = true;
}
private isDuringDailyStandup(): boolean {
if (this.settings.team.dailyLockEnabled) {
const now = new Date();
const startsParts = this.settings.team.dailyStartTime.split(TIME_SEPARATOR).map(part => +part);
const endsParts = this.settings.team.dailyEndTime.split(TIME_SEPARATOR).map(part => +part);
const times = [
new Date(now.getFullYear(), now.getMonth(), now.getDate(), startsParts[0], startsParts[1]),
new Date(now.getFullYear(), now.getMonth(), now.getDate(), endsParts[0], endsParts[1])
];
const startsAt = min(...times);
const endsAt = max(...times);
return isWithinRange(now, startsAt, endsAt);
}
return false;
}
}

64
src/app/shared/slide-in-out-animation.ts Normal file → Executable file
View File

@ -1,42 +1,34 @@
// import the required animation functions from the angular animations module
import { animate, state, style, transition, trigger } from '@angular/animations';
import {animate, group, query, state, style, transition, trigger} from '@angular/animations';
export const slideInOutAnimation =
// trigger name for attaching this animation to an element using the [@triggerName] syntax
trigger('slideInOutAnimation', [
// end state styles for route container (host)
state('*', style({
// the view covers the whole screen with a semi tranparent background
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0
})),
// route 'enter' transition
transition(':enter', [
// styles at start of transition
style({
// start with the content positioned off the right of the screen,
// -400% is required instead of -100% because the negative position adds to the width of the element
transform: 'translateX(100%)'
trigger('routerTransition', [
transition('* <=> *', [
query(':enter, :leave', style({
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0
}), {
optional: true
}),
// animation and styles at end of transition
animate('.75s cubic-bezier(0.175, 0.885, 0.32, 1.275)', style({
// transition the right position to 0 which slides the content into view
transform: 'translateX(0)'
}))
]),
// route 'leave' transition
transition(':leave', [
// animation and styles at end of transition
animate('.75s cubic-bezier(0.175, 0.885, 0.32, 1.275)', style({
// transition the right position to -400% which slides the content out of view
transform: 'translateX(-100%)'
}))
group([
query(':enter', [
style({ transform: 'translateX({{offsetEnter}}%)'}),
animate('.75s cubic-bezier(0.175, 0.885, 0.32, 1.275)', style({
transform: 'translateX(0%)'
}))
], {
optional: true
}),
query(':leave', [
style({ transform: 'translateX(0%)' }),
animate('.75s cubic-bezier(0.175, 0.885, 0.32, 1.275)', style({
transform: 'translateX({{offsetLeave}}%)'
}))
], { optional: true })
])
])
]);

23
src/app/shared/slide-wrapper.ts Executable file
View File

@ -0,0 +1,23 @@
import { Slide } from './slide';
export class SlideWrapper {
public type: WrappedType = null;
public slideData: Slide = null;
public slideRoute: string = null;
public constructor(type: WrappedType, data: string|Slide) {
this.type = type;
if (type === WrappedType.USER) {
this.slideData = data as Slide;
} else if (type === WrappedType.BUILTIN) {
this.slideRoute = data as string;
} else {
throw new Error(`Unknown slide type: ${type}`);
}
}
}
export enum WrappedType {
BUILTIN,
USER
}

23
src/app/shared/slide.ts Normal file → Executable file
View File

@ -2,11 +2,24 @@ import { Team } from './team';
export class Slide {
id: number = null;
title: String = '';
team: Team = null;
slideData: String = '';
title = '';
type: SlideType = SlideType.MarkDown;
visibility: SlideVisibility = SlideVisibility.Public;
teams: Array<Team> = [];
slideData = '';
slideUrl = '';
isVisible = true;
createdAt: String = null;
updatedAt: String = null;
createdAt: string = null;
updatedAt: string = null;
position = 0;
}
export enum SlideType {
MarkDown = 'markdown',
IFrame = 'iframe',
}
export enum SlideVisibility {
Public = 'public',
Team = 'team',
}

16
src/app/shared/team.ts Normal file → Executable file
View File

@ -1,10 +1,24 @@
import { Member } from './member';
import { KanbanColumn } from './kanban-column';
import { Label } from './label';
export class Team {
id: number = null;
name: String = '';
members: Array<Member> = [];
isActive = false;
filterId = 0;
kanbanEnabled = true;
commitTrackerEnabled = true;
watchedEnabled = true;
dailyLockEnabled = false;
dailyStartTime: string;
dailyEndTime: string;
backlogColumn: KanbanColumn = new KanbanColumn();
inprogressColumn: KanbanColumn = new KanbanColumn();
verificationColumn: KanbanColumn = new KanbanColumn();
doneColumn: KanbanColumn = new KanbanColumn();
labels: Array<Label> = [];
isActive = true;
createdAt: String = null;
updatedAt: String = null;
}

View File

@ -0,0 +1,116 @@
export class TwoWayLinkedList<T> {
private count = 0;
private entryNode: Node<T> = null;
private lastNode: Node<T> = null;
private nodePtr: Node<T> = null;
public push(newNode: T): TwoWayLinkedList<T> {
if (null === this.lastNode) {
this.entryNode = new Node<T>(newNode);
this.lastNode = this.entryNode;
this.nodePtr = this.entryNode;
this.entryNode
.setPrev(this.entryNode)
.setNext(this.entryNode);
} else {
const prevPtr = this.lastNode;
const nodeToAdd = new Node(newNode);
nodeToAdd.setNext(this.entryNode).setPrev(prevPtr);
prevPtr.setNext(nodeToAdd);
this.lastNode = nodeToAdd;
}
this.count++;
return this;
}
public pop(): T {
if (null === this.lastNode) {
throw new Error('No items in list');
}
let returnData: T;
if (this.entryNode === this.lastNode) {
returnData = this.lastNode.getData();
this.lastNode.setPrev(null).setNext(null);
this.entryNode = null;
this.lastNode = null;
this.nodePtr = null;
} else {
const newLast = this.lastNode.getPrev();
if (this.nodePtr === this.lastNode) {
this.nodePtr = newLast;
}
newLast.setNext(this.entryNode);
returnData = this.lastNode.getData();
this.lastNode.setPrev(null).setNext(null);
this.lastNode = newLast;
}
this.count--;
return returnData;
}
public clear() {
while (this.count > 0) {
this.pop();
}
}
public prev(): T {
this.nodePtr = this.nodePtr.getPrev();
return this.nodePtr.getData();
}
public next(): T {
this.nodePtr = this.nodePtr.getNext();
return this.nodePtr.getData();
}
public isFirst(): boolean {
return this.nodePtr === this.lastNode;
}
public isLast(): boolean {
return this.nodePtr === this.lastNode;
}
public get first(): T {
return this.entryNode.getData();
}
public get last(): T {
return this.lastNode.getData();
}
}
export class Node<T> {
private prev: Node<T> = null;
private next: Node<T> = null;
private readonly data: T = null;
constructor(data: T) {
this.data = data;
}
public getPrev(): Node<T> {
return this.prev;
}
public setPrev(node: Node<T>): Node<T> {
this.prev = node;
return this;
}
public getNext(): Node<T> {
return this.next;
}
public setNext(node: Node<T>): Node<T> {
this.next = node;
return this;
}
public getData(): T {
return this.data;
}
}

2
src/environments/environment.prod.ts Normal file → Executable file
View File

@ -1,5 +1,5 @@
export const environment = {
production: true,
apiUrl: 'http://ttt-api.tsp.eth.ericsson.se/mtastv-api',
apiUrl: 'https://mtoolbox.rnd.ki.sw.ericsson.se/mtastv-api',
commitTrackerApiUrl: 'https://mtas-trex.rnd.ki.sw.ericsson.se:8080/committracker/api/'
};

View File

@ -0,0 +1,5 @@
export const environment = {
production: true,
apiUrl: 'http://ttt-api.tsp.eth.ericsson.se/mtastv-api',
commitTrackerApiUrl: 'https://mtas-trex.rnd.ki.sw.ericsson.se:8080/committracker/api/'
};

View File

@ -10,7 +10,8 @@
]
},
"files": [
"test.ts"
"test.ts",
"polyfills.ts"
],
"include": [
"**/*.spec.ts",

View File

@ -14,6 +14,8 @@
"lib": [
"es2017",
"dom"
]
],
"module": "es2015",
"baseUrl": "./"
}
}
}

View File

@ -18,7 +18,6 @@
"forin": true,
"import-blacklist": [
true,
"rxjs",
"rxjs/Rx"
],
"import-spacing": true,