TSK-207 Add classification details JSON+HAL support for classifications.

This commit is contained in:
miguelmartinrojas 2018-04-09 09:45:33 +02:00 committed by holgerhagen
parent 70be178dd1
commit 62859af49e
38 changed files with 652 additions and 182 deletions

View File

@ -0,0 +1,133 @@
<div class="container-scrollable">
<div id="classification-details" *ngIf="classification && !requestInProgress">
<ul class="nav nav-tabs" role="tablist">
<li *ngIf="showDetail" class="visible-xs visible-sm hidden">
<a (click)="backClicked()">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>Back</a>
</li>
</ul>
<taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner>
<div *ngIf="classification" id="classification" class="panel panel-default classification">
<div class="panel-heading">
<div class="pull-right">
<button type="button" [disabled]="!ClassificationForm.form.valid" (click)="onSave()" class="btn btn-default btn-primary">Save</button>
<button type="button" (click)="onClear()" class="btn btn-default">Undo</button>
<button type="button" (click)="removeClassification()" data-toggle="tooltip" title="Remove" class="btn btn-default remove">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>
</div>
<h4 class="panel-header">{{classification.name}}&nbsp;
<span *ngIf="!classification.classificationId" class="badge warning"> {{badgeMessage}}</span>
</h4>
</div>
<div class="panel-body">
<form #ClassificationForm="ngForm">
<div class="col-md-6">
<div class="form-group required">
<label for="classification-key" class="control-label">Key</label>
<input type="text" required #key="ngModel" class="form-control" id="classification-key" placeholder="Key" [(ngModel)]="classification.key"
name="classification.key">
<div [hidden]="key.valid" class="required-text">
* Key is required
</div>
</div>
<div class="form-group required">
<label for="classification-name" class="control-label">Name</label>
<input type="text" required #name="ngModel" class="form-control" id="classification-name" placeholder="Name" [(ngModel)]="classification.name"
name="classification.name">
<div [hidden]="name.valid" class="required-text">
* Name is required
</div>
</div>
<div class="form-group required">
<label for="classification-domain" class="control-label">Domain</label>
<input type="text" required #domain="ngModel" class="form-control" id="classification-domain" placeholder="Domain" [(ngModel)]="classification.domain"
name="classification.domain">
<div [hidden]="domain.valid" class="required-text">
* Domain is required
</div>
</div>
<div class="form-group">
<label for="classification-type" class="control-label">Type</label>
<div class="dropdown clearfix btn-group">
<button class="btn btn-default" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{classification.type}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu" aria-labelledby="dropdownMenu">
<li *ngFor="let type of classificationTypes" (click)="selectType(type)">
<a>
<label>
{{type}}
</label>
</a>
</li>
</ul>
</div>
</div>
<div class="form-group">
<label for="classification-description" class="control-label">Description</label>
<textarea class="form-control" rows="5" id="classification-description" placeholder="Description" [(ngModel)]="classification.description"
name="classification.description"></textarea>
</div>
<div class="form-group">
<label for="classification-service-level" class="control-label">Service Level</label>
<input type="text" class="form-control" id="classification-service-level" placeholder="Service Level" [(ngModel)]="classification.serviceLevel"
name="classification.serviceLevel">
</div>
<div class="form-group">
<label for="classification-application-entry-point" class="control-label">Application entry point</label>
<input type="text" class="form-control" id="classification-application-entry-point" placeholder="Application entry point"
[(ngModel)]="classification.applicationEntryPoint" name="classification.applicationEntryPoint">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="classification-custom-1" class="control-label">Custom 1</label>
<input type="text" class="form-control" id="classification-custom-1" placeholder="Custom 1" [(ngModel)]="classification.custom1"
name="classification.custom1">
</div>
<div class="form-group">
<label for="classification-custom-2" class="control-label">Custom 2</label>
<input type="text" class="form-control" id="classification-custom-2" placeholder="Custom 2" [(ngModel)]="classification.custom2"
name="classification.custom2">
</div>
<div class="form-group">
<label for="classification-custom-3" class="control-label">Custom 3</label>
<input type="text" class="form-control" id="classification-custom-3" placeholder="Custom 3" [(ngModel)]="classification.custom3"
name="classification.custom3">
</div>
<div class="form-group">
<label for="classification-custom-4" class="control-label">Custom 4</label>
<input type="text" class="form-control" id="classification-custom-4" placeholder="Custom 4" [(ngModel)]="classification.custom4"
name="classification.custom4">
</div>
<div class="form-group">
<label for="classification-custom-5" class="control-label">Custom 5</label>
<input type="text" class="form-control" id="classification-custom-5" placeholder="Custom 5" [(ngModel)]="classification.custom5"
name="classification.custom5">
</div>
<div class="form-group">
<label for="classification-custom-6" class="control-label">Custom 6</label>
<input type="text" class="form-control" id="classification-custom-6" placeholder="Custom 6" [(ngModel)]="classification.custom6"
name="classification.custom6">
</div>
<div class="form-group">
<label for="classification-custom-7" class="control-label">Custom 7</label>
<input type="text" class="form-control" id="classification-custom-7" placeholder="Custom 7" [(ngModel)]="classification.custom7"
name="classification.custom7">
</div>
<div class="form-group">
<label for="classification-custom-8" class="control-label">Custom 8</label>
<input type="text" class="form-control" id="classification-custom-8" placeholder="Custom 8" [(ngModel)]="classification.custom8"
name="classification.custom8">
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,11 @@
.classification.panel{
border: none;
box-shadow: none;
margin-bottom: 0px;
&> .panel-body {
height: 80vh;
max-height: 80vh;
overflow-y: auto;
}
}

View File

@ -0,0 +1,59 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Routes } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ClassificationDetailsComponent } from './classification-details.component';
import { SpinnerComponent } from 'app/shared/spinner/spinner.component';
import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-detail.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
import { ClassificationsService } from 'app/services/classifications/classifications.service';
import { TreeNodeModel } from 'app/models/tree-node';
@Component({
selector: 'taskana-dummy-detail',
template: 'dummydetail'
})
class DummyDetailComponent {
}
const routes: Routes = [
{ path: ':id', component: DummyDetailComponent, outlet: 'detail' },
{ path: 'classifications', component: DummyDetailComponent }
];
describe('ClassificationDetailsComponent', () => {
let component: ClassificationDetailsComponent;
let fixture: ComponentFixture<ClassificationDetailsComponent>;
const treeNodes: Array<TreeNodeModel> = new Array(new TreeNodeModel());
const classificationTypes: Array<string> = new Array<string>('type1', 'type2');
let classificationsSpy, classificationsTypesSpy;
let classificationsService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [FormsModule, HttpClientModule, RouterTestingModule.withRoutes(routes)],
declarations: [ClassificationDetailsComponent, SpinnerComponent, DummyDetailComponent],
providers: [MasterAndDetailService, RequestInProgressService, ClassificationsService, HttpClient]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClassificationDetailsComponent);
component = fixture.componentInstance;
classificationsService = TestBed.get(ClassificationsService);
classificationsSpy = spyOn(classificationsService, 'getClassifications').and.returnValue(Observable.of(treeNodes));
classificationsTypesSpy = spyOn(classificationsService, 'getClassificationTypes').and.returnValue(Observable.of(classificationTypes));
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,110 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { ClassificationDefinition } from 'app/models/classification-definition';
import { ACTION } from 'app/models/action';
import { ClassificationsService } from 'app/services/classifications/classifications.service';
import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-detail.service';
@Component({
selector: 'taskana-classification-details',
templateUrl: './classification-details.component.html',
styleUrls: ['./classification-details.component.scss']
})
export class ClassificationDetailsComponent implements OnInit, OnDestroy {
classification: ClassificationDefinition;
selectedId: string = undefined;
showDetail = false;
requestInProgress = false;
classificationTypes: Array<string> = [];
badgeMessage = '';
private action: any;
private classificationServiceSubscription: Subscription;
private classificationSelectedSubscription: Subscription;
private routeSubscription: Subscription;
private masterAndDetailSubscription: Subscription;
constructor(private classificationsService: ClassificationsService,
private route: ActivatedRoute,
private router: Router,
private masterAndDetailService: MasterAndDetailService) { }
ngOnInit() {
this.classificationsService.getClassificationTypes().subscribe((classificationTypes: Array<string>) => {
this.classificationTypes = classificationTypes;
})
this.classificationSelectedSubscription = this.classificationsService.getSelectedClassification()
.subscribe(classificationIdSelected => {
this.classification = undefined;
if (classificationIdSelected) {
this.getClassificationInformation(classificationIdSelected);
}
});
this.routeSubscription = this.route.params.subscribe(params => {
let id = params['id'];
this.action = undefined;
if (id && id.indexOf('new-classification') !== -1) {
this.action = ACTION.CREATE;
id = undefined;
this.badgeMessage = 'Creating new workbasket';
this.getClassificationInformation(id);
}
if (id && id !== '') {
this.selectClassification(id);
}
});
this.masterAndDetailSubscription = this.masterAndDetailService.getShowDetail().subscribe(showDetail => {
this.showDetail = showDetail;
});
}
backClicked(): void {
this.classificationsService.selectClassification(undefined);
this.router.navigate(['./'], { relativeTo: this.route.parent });
}
selectType(type: string) {
this.classification.type = type;
}
removeClassification() { }
onSave() { }
onClear() { }
private selectClassification(id: string) {
this.selectedId = id;
this.classificationsService.selectClassification(id);
}
private getClassificationInformation(classificationIdSelected: string) {
if (this.action === ACTION.CREATE) { // CREATE
this.classification = new ClassificationDefinition();
} else {
this.requestInProgress = true;
this.classificationServiceSubscription = this.classificationsService.getClassification(classificationIdSelected)
.subscribe((classification: ClassificationDefinition) => {
this.classification = classification;
this.requestInProgress = false;
});
}
}
ngOnDestroy(): void {
if (this.masterAndDetailSubscription) { this.masterAndDetailSubscription.unsubscribe(); }
if (this.routeSubscription) { this.routeSubscription.unsubscribe(); }
if (this.classificationSelectedSubscription) { this.classificationSelectedSubscription.unsubscribe(); }
if (this.classificationServiceSubscription) { this.classificationServiceSubscription.unsubscribe(); }
}
}

View File

@ -5,9 +5,6 @@
<button type="button" (click)="addClassification()" data-toggle="tooltip" title="Add" class="btn btn-default"> <button type="button" (click)="addClassification()" data-toggle="tooltip" title="Add" class="btn btn-default">
<span class="glyphicon glyphicon-plus green" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus green" aria-hidden="true"></span>
</button> </button>
<button type="button" (click)="removeClassification()" data-toggle="tooltip" title="Remove" class="btn btn-default remove">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>
<taskana-import-export-component [currentSelection]="selectionToImport"></taskana-import-export-component> <taskana-import-export-component [currentSelection]="selectionToImport"></taskana-import-export-component>
<taskana-classification-types-selector class="pull-right" [classificationTypes]="classificationsTypes" [(classificationTypeSelected)]="classificationTypeSelected" <taskana-classification-types-selector class="pull-right" [classificationTypes]="classificationsTypes" [(classificationTypeSelected)]="classificationTypeSelected"
(classificationTypeChanged)=selectClassificationType($event)></taskana-classification-types-selector> (classificationTypeChanged)=selectClassificationType($event)></taskana-classification-types-selector>
@ -15,6 +12,6 @@
</div> </div>
</li> </li>
<taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner> <taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner>
<taskana-tree [treeNodes]="classifications"></taskana-tree> <taskana-tree [treeNodes]="classifications" [selectNodeId] ="selectedId" (selectNodeIdChanged) ="selectClassification($event)"></taskana-tree>
</div> </div>

View File

@ -3,7 +3,7 @@
} }
.list-group-item { .list-group-item {
padding: 5px 0px; padding: 5px 0px 2px 1px;
border: none; border: none;
} }

View File

@ -2,14 +2,15 @@ import { Component, Input } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Routes } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TreeNode } from 'app/models/tree-node'; import { TreeNodeModel } from 'app/models/tree-node';
import { ClassificationListComponent } from './classification-list.component'; import { ClassificationListComponent } from './classification-list.component';
import { ImportExportComponent } from 'app/shared/import-export/import-export.component'; import { ImportExportComponent } from 'app/shared/import-export/import-export.component';
import { SpinnerComponent } from 'app/shared/spinner/spinner.component'; import { SpinnerComponent } from 'app/shared/spinner/spinner.component';
import { ClassificationTypesSelectorComponent } from 'app/shared/classification-types-selector/classification-types-selector.component'; import { ClassificationTypesSelectorComponent } from 'app/shared/classification-types-selector/classification-types-selector.component';
import { MapValuesPipe } from 'app/pipes/mapValues/map-values.pipe';
import { WorkbasketService } from 'app/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { WorkbasketDefinitionService } from 'app/services/workbasket-definition/workbasket-definition.service'; import { WorkbasketDefinitionService } from 'app/services/workbasket-definition/workbasket-definition.service';
@ -22,25 +23,40 @@ import { DomainService } from 'app/services/domains/domain.service';
selector: 'taskana-tree', selector: 'taskana-tree',
template: '' template: ''
}) })
class TreeComponent { class TaskanaTreeComponent {
@Input() treeNodes; @Input() treeNodes;
@Input() selectNodeId;
} }
@Component({
selector: 'taskana-dummy-detail',
template: 'dummydetail'
})
class DummyDetailComponent {
}
const routes: Routes = [
{ path: ':id', component: DummyDetailComponent, outlet: 'detail' },
{ path: 'classifications', component: DummyDetailComponent }
];
describe('ClassificationListComponent', () => { describe('ClassificationListComponent', () => {
let component: ClassificationListComponent; let component: ClassificationListComponent;
let fixture: ComponentFixture<ClassificationListComponent>; let fixture: ComponentFixture<ClassificationListComponent>;
const treeNodes: Array<TreeNode> = new Array(new TreeNode()); const treeNodes: Array<TreeNodeModel> = new Array(new TreeNodeModel());
const classificationTypes: Map<string, string> = new Map<string, string>([['type1', 'type1'], ['type2', 'type2']]) const classificationTypes: Array<string> = new Array<string>('type1', 'type2');
let classificationsSpy, classificationsTypesSpy; let classificationsSpy, classificationsTypesSpy;
let classificationsService; let classificationsService;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ClassificationListComponent, ImportExportComponent, SpinnerComponent, ClassificationTypesSelectorComponent, declarations: [ClassificationListComponent, ImportExportComponent, SpinnerComponent, ClassificationTypesSelectorComponent,
TreeComponent, MapValuesPipe], TaskanaTreeComponent, DummyDetailComponent],
imports: [HttpClientModule], imports: [HttpClientModule, RouterTestingModule.withRoutes(routes)],
providers: [ providers: [
HttpClient, WorkbasketDefinitionService, AlertService, ClassificationsService, DomainService, ClassificationDefinitionService HttpClient, WorkbasketDefinitionService, AlertService, ClassificationsService, DomainService, ClassificationDefinitionService
] ]
}) })
.compileComponents(); .compileComponents();

View File

@ -1,9 +1,10 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { Router, ActivatedRoute } from '@angular/router';
import { ImportType } from 'app/models/import-type'; import { ImportType } from 'app/models/import-type';
import { Classification } from 'app/models/classification'; import { Classification } from 'app/models/classification';
import { TreeNode } from 'app/models/tree-node'; import { TreeNodeModel } from 'app/models/tree-node';
import { ClassificationsService } from 'app/services/classifications/classifications.service'; import { ClassificationsService } from 'app/services/classifications/classifications.service';
@ -15,41 +16,61 @@ import { ClassificationsService } from 'app/services/classifications/classificat
export class ClassificationListComponent implements OnInit, OnDestroy { export class ClassificationListComponent implements OnInit, OnDestroy {
selectedId: string;
selectionToImport = ImportType.CLASSIFICATIONS; selectionToImport = ImportType.CLASSIFICATIONS;
requestInProgress = false; requestInProgress = false;
classifications: Array<Classification> = []; classifications: Array<Classification> = [];
classificationsTypes: Map<string, string> = new Map(); classificationsTypes: Array<string> = [];
classificationTypeSelected: string; classificationTypeSelected: string;
classificationServiceSubscription: Subscription; classificationServiceSubscription: Subscription;
classificationTypeServiceSubscription: Subscription; classificationTypeServiceSubscription: Subscription;
constructor(private classificationService: ClassificationsService) { classificationSelectedSubscription: Subscription;
constructor(
private classificationService: ClassificationsService,
private router: Router,
private route: ActivatedRoute, ) {
} }
ngOnInit() { ngOnInit() {
this.classificationServiceSubscription = this.classificationService.getClassifications() this.classificationServiceSubscription = this.classificationService.getClassifications()
.subscribe((classifications: Array<TreeNode>) => { .subscribe((classifications: Array<TreeNodeModel>) => {
this.classifications = classifications; this.classifications = classifications;
this.classificationTypeServiceSubscription = this.classificationService.getClassificationTypes() this.classificationTypeServiceSubscription = this.classificationService.getClassificationTypes()
.subscribe((classificationsTypes: Map<string, string>) => { .subscribe((classificationsTypes: Array<string>) => {
this.classificationsTypes = classificationsTypes; this.classificationsTypes = classificationsTypes;
this.classificationTypeSelected = this.classifications[0].type; this.classificationTypeSelected = this.classifications[0].type;
}); });
}); });
this.classificationSelectedSubscription = this.classificationService.getSelectedClassification()
.subscribe((classificationSelected: string) => {
// TODO should be done in a different way.
setTimeout(() => { this.selectedId = classificationSelected; }, 0);
});
} }
selectClassificationType(classificationTypeSelected: string) { selectClassificationType(classificationTypeSelected: string) {
this.classificationService.getClassifications(true, classificationTypeSelected) this.classificationService.getClassifications(true, classificationTypeSelected)
.subscribe((classifications: Array<TreeNode>) => { .subscribe((classifications: Array<TreeNodeModel>) => {
this.classifications = classifications; this.classifications = classifications;
}); });
} }
selectClassification(id: string) {
this.selectedId = id;
this.router.navigate([{ outlets: { detail: [this.selectedId] } }], { relativeTo: this.route });
}
addClassification() { } addClassification() {
removeClassification() { } this.selectedId = undefined;
this.router.navigate([{ outlets: { detail: ['new-classification'] } }], { relativeTo: this.route });
}
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.classificationServiceSubscription) { this.classificationServiceSubscription.unsubscribe(); } if (this.classificationServiceSubscription) { this.classificationServiceSubscription.unsubscribe(); }
if (this.classificationTypeServiceSubscription) { this.classificationTypeServiceSubscription.unsubscribe(); } if (this.classificationTypeServiceSubscription) { this.classificationTypeServiceSubscription.unsubscribe(); }
if (this.classificationSelectedSubscription) { this.classificationSelectedSubscription.unsubscribe(); }
} }
} }

View File

@ -1,9 +1,9 @@
<taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner> <taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default"> <div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="btn-group pull-right"> <div class="pull-right">
<button type="button" (click)="onSave()" [disabled]="!AccessItemsForm.form.valid || action === 'COPY'" class="btn btn-default btn-primary">Save</button> <button type="button" (click)="onSave()" [disabled]="!AccessItemsForm.form.valid || action === 'COPY'" class="btn btn-default btn-primary">Save</button>
<button type="button" (click)="clear()" class="btn btn-default">Undo changes</button> <button type="button" (click)="clear()" class="btn btn-default">Undo</button>
</div> </div>
<h4 class="panel-header">{{workbasket.name}} <h4 class="panel-header">{{workbasket.name}}
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span> <span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>

View File

@ -54,6 +54,9 @@ export class AccessItemsComponent implements OnChanges, OnDestroy {
if (!this.initialized && changes.active && changes.active.currentValue === 'accessItems') { if (!this.initialized && changes.active && changes.active.currentValue === 'accessItems') {
this.init(); this.init();
} }
if (changes.action) {
this.setBadge();
}
} }
private init() { private init() {
this.initialized = true; this.initialized = true;
@ -75,9 +78,7 @@ export class AccessItemsComponent implements OnChanges, OnDestroy {
this.onSave(); this.onSave();
} }
}) })
if (this.action === ACTION.COPY) {
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
}
} }
addAccessItem() { addAccessItem() {
@ -113,6 +114,11 @@ export class AccessItemsComponent implements OnChanges, OnDestroy {
}) })
return false; return false;
} }
private setBadge() {
if (this.action === ACTION.COPY) {
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
}
}
private cloneAccessItems(inputaccessItem): Array<WorkbasketAccessItems> { private cloneAccessItems(inputaccessItem): Array<WorkbasketAccessItems> {
const accessItemClone = new Array<WorkbasketAccessItems>(); const accessItemClone = new Array<WorkbasketAccessItems>();

View File

@ -2,9 +2,9 @@
class="centered-horizontally floating"></taskana-spinner> class="centered-horizontally floating"></taskana-spinner>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default"> <div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="btn-group pull-right"> <div class="pull-right">
<button type="button" (click)="onSave()" [disabled]="action === 'COPY'" class="btn btn-default btn-primary">Save</button> <button type="button" (click)="onSave()" [disabled]="action === 'COPY'" class="btn btn-default btn-primary">Save</button>
<button type="button" (click)="onClear()" class="btn btn-default">Undo changes</button> <button type="button" (click)="onClear()" class="btn btn-default">Undo</button>
</div> </div>
<h4 class="panel-header">{{workbasket.name}} <h4 class="panel-header">{{workbasket.name}}
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span> <span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>

View File

@ -60,9 +60,12 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
private errorModalService: ErrorModalService) { } private errorModalService: ErrorModalService) { }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (!this.initialized && changes.active && changes.active.currentValue === 'distributionTargets' ) { if (!this.initialized && changes.active && changes.active.currentValue === 'distributionTargets') {
this.init(); this.init();
} }
if (changes.action) {
this.setBadge();
}
} }
private init() { private init() {
@ -97,9 +100,6 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
} }
}); });
if (this.action === ACTION.COPY) {
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
}
} }
moveDistributionTargets(side: number) { moveDistributionTargets(side: number) {
@ -162,6 +162,12 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
}); });
} }
private setBadge() {
if (this.action === ACTION.COPY) {
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
}
}
private getSelectedItems(originList: any, destinationList: any): Array<any> { private getSelectedItems(originList: any, destinationList: any): Array<any> {
return originList.filter((item: any) => { return (item.selected === true) }); return originList.filter((item: any) => { return (item.selected === true) });
} }

View File

@ -1,9 +1,15 @@
<taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner> <taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default"> <div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="btn-group pull-right"> <div class="pull-right">
<button type="button" [disabled]="!WorkbasketForm.form.valid" (click)="onSave()" class="btn btn-default btn-primary">Save</button> <button type="button" [disabled]="!WorkbasketForm.form.valid" (click)="onSave()" class="btn btn-default btn-primary">Save</button>
<button type="button" (click)="onClear()" class="btn btn-default">Undo changes</button> <button type="button" (click)="onClear()" class="btn btn-default">Undo</button>
<button type="button" (click)="copyWorkbasket()" data-toggle="tooltip" title="copy" class="btn btn-default">
<span class="glyphicon glyphicon-copy green" aria-hidden="true"></span>
</button>
<button type="button" (click)="removeWorkbasket()" data-toggle="tooltip" title="Remove" class="btn btn-default remove">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>
</div> </div>
<h4 class="panel-header">{{workbasket.name}}&nbsp; <h4 class="panel-header">{{workbasket.name}}&nbsp;
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span> <span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>

View File

@ -24,6 +24,7 @@ import { RemoveNoneTypePipe } from 'app/pipes/removeNoneType/remove-none-type.pi
import { ErrorModalService } from 'app/services/errorModal/error-modal.service'; import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-workbaskets/saving-workbaskets.service'; import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-workbaskets/saving-workbaskets.service';
import { AlertService } from 'app/services/alert/alert.service'; import { AlertService } from 'app/services/alert/alert.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
@Component({ @Component({
selector: 'taskana-dummy-detail', selector: 'taskana-dummy-detail',
@ -47,7 +48,7 @@ describe('InformationComponent', () => {
declarations: [WorkbasketInformationComponent, IconTypeComponent, MapValuesPipe, declarations: [WorkbasketInformationComponent, IconTypeComponent, MapValuesPipe,
RemoveNoneTypePipe, SpinnerComponent, GeneralMessageModalComponent, DummyDetailComponent], RemoveNoneTypePipe, SpinnerComponent, GeneralMessageModalComponent, DummyDetailComponent],
imports: [FormsModule, AngularSvgIconModule, HttpClientModule, HttpModule, RouterTestingModule.withRoutes(routes)], imports: [FormsModule, AngularSvgIconModule, HttpClientModule, HttpModule, RouterTestingModule.withRoutes(routes)],
providers: [WorkbasketService, AlertService, SavingWorkbasketService, ErrorModalService] providers: [WorkbasketService, AlertService, SavingWorkbasketService, ErrorModalService, RequestInProgressService]
}) })
.compileComponents(); .compileComponents();
@ -162,4 +163,15 @@ describe('InformationComponent', () => {
expect(savingWorkbasketService.triggerAccessItemsSaving).toHaveBeenCalled(); expect(savingWorkbasketService.triggerAccessItemsSaving).toHaveBeenCalled();
}); });
// it('should call to workbasket service to remove workbasket after click on remove workbasket', () => {
// const spy = spyOn(router, 'navigate');
// component.removeWorkbasket();
// expect(requestInProgressService.setRequestInProgress).toHaveBeenCalledWith(true);
// expect(workbasketService.deleteWorkbasket).toHaveBeenCalledWith('selfLink');
// expect(requestInProgressService.setRequestInProgress).toHaveBeenCalledWith(false);
// expect(workbasketService.triggerWorkBasketSaved).toHaveBeenCalled();
// expect(spy.calls.first().args[0][0]).toBe('/workbaskets');
// });
}); });

View File

@ -1,4 +1,5 @@
import { Component, OnInit, Input, Output, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, Output, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { ActivatedRoute, Params, Router, NavigationStart } from '@angular/router'; import { ActivatedRoute, Params, Router, NavigationStart } from '@angular/router';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
@ -8,13 +9,14 @@ import { ICONTYPES } from 'app/models/type';
import { ErrorModel } from 'app/models/modal-error'; import { ErrorModel } from 'app/models/modal-error';
import { ACTION } from 'app/models/action'; import { ACTION } from 'app/models/action';
import { Workbasket } from 'app/models/workbasket'; import { Workbasket } from 'app/models/workbasket';
import { AlertModel, AlertType } from 'app/models/alert';
import { AlertService } from 'app/services/alert/alert.service'; import { AlertService } from 'app/services/alert/alert.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service'; import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { FormGroup } from '@angular/forms';
import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-workbaskets/saving-workbaskets.service'; import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-workbaskets/saving-workbaskets.service';
import { WorkbasketService } from 'app/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { AlertModel, AlertType } from 'app/models/alert'; import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
const dateFormat = 'yyyy-MM-ddTHH:mm:ss'; const dateFormat = 'yyyy-MM-ddTHH:mm:ss';
const dateLocale = 'en-US'; const dateLocale = 'en-US';
@ -46,7 +48,8 @@ export class WorkbasketInformationComponent implements OnChanges, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private errorModalService: ErrorModalService, private errorModalService: ErrorModalService,
private savingWorkbasket: SavingWorkbasketService) { private savingWorkbasket: SavingWorkbasketService,
private requestInProgressService: RequestInProgressService) {
this.allTypes = IconTypeComponent.allTypes; this.allTypes = IconTypeComponent.allTypes;
} }
@ -91,13 +94,33 @@ export class WorkbasketInformationComponent implements OnChanges, OnDestroy {
this.hasChanges = false; this.hasChanges = false;
} }
removeWorkbasket() {
this.requestInProgressService.setRequestInProgress(true);
this.workbasketService.deleteWorkbasket(this.workbasket._links.self.href).subscribe(response => {
this.requestInProgressService.setRequestInProgress(false);
this.workbasketService.triggerWorkBasketSaved();
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS,
`Workbasket ${this.workbasket.workbasketId} was removed successfully`))
this.router.navigate(['administration/workbaskets']);
}, error => {
this.requestInProgressService.setRequestInProgress(false);
this.errorModalService.triggerError(new ErrorModel(
`There was an error deleting workbasket ${this.workbasket.workbasketId}`, error.error.message))
});
}
copyWorkbasket() {
this.router.navigate([{ outlets: { detail: ['copy-workbasket'] } }], { relativeTo: this.route.parent });
}
private beforeRequest() { private beforeRequest() {
this.requestInProgress = true; this.requestInProgressService.setRequestInProgress(true);
this.modalSpinner = true; this.modalSpinner = true;
} }
private afterRequest() { private afterRequest() {
this.requestInProgress = false; this.requestInProgressService.setRequestInProgress(false);
this.workbasketService.triggerWorkBasketSaved(); this.workbasketService.triggerWorkBasketSaved();
} }

View File

@ -16,12 +16,12 @@ import { WorkbasketAccessItemsResource } from 'app/models/workbasket-access-item
import { ICONTYPES } from 'app/models/type'; import { ICONTYPES } from 'app/models/type';
import { Links } from 'app/models/links'; import { Links } from 'app/models/links';
import { WorkbasketAccessItems } from 'app/models/workbasket-access-items'; import { WorkbasketAccessItems } from 'app/models/workbasket-access-items';
import { LinksWorkbasketSummary } from 'app/models/links-workbasket-summary';
import { WorkbasketService } from 'app/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-detail.service'; import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-detail.service';
import { PermissionService } from 'app/services/permission/permission.service'; import { PermissionService } from 'app/services/permission/permission.service';
import { AlertService } from 'app/services/alert/alert.service'; import { AlertService } from 'app/services/alert/alert.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { SavingWorkbasketService } from 'app/services/saving-workbaskets/saving-workbaskets.service'; import { SavingWorkbasketService } from 'app/services/saving-workbaskets/saving-workbaskets.service';
import { WorkbasketDetailsComponent } from './workbasket-details.component'; import { WorkbasketDetailsComponent } from './workbasket-details.component';
@ -37,7 +37,9 @@ import { GeneralMessageModalComponent } from 'app/shared/general-message-modal/g
import { MapValuesPipe } from 'app/pipes/mapValues/map-values.pipe'; import { MapValuesPipe } from 'app/pipes/mapValues/map-values.pipe';
import { RemoveNoneTypePipe } from 'app/pipes/removeNoneType/remove-none-type.pipe'; import { RemoveNoneTypePipe } from 'app/pipes/removeNoneType/remove-none-type.pipe';
import { SelectWorkBasketPipe } from 'app/pipes/selectedWorkbasket/seleted-workbasket.pipe'; import { SelectWorkBasketPipe } from 'app/pipes/selectedWorkbasket/seleted-workbasket.pipe';
import { LinksWorkbasketSummary } from '../../../models/links-workbasket-summary'; import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
@Component({ @Component({
selector: 'taskana-filter', selector: 'taskana-filter',
@ -76,8 +78,8 @@ describe('WorkbasketDetailsComponent', () => {
declarations: [WorkbasketDetailsComponent, NoAccessComponent, WorkbasketInformationComponent, SpinnerComponent, declarations: [WorkbasketDetailsComponent, NoAccessComponent, WorkbasketInformationComponent, SpinnerComponent,
IconTypeComponent, MapValuesPipe, RemoveNoneTypePipe, AlertComponent, GeneralMessageModalComponent, AccessItemsComponent, IconTypeComponent, MapValuesPipe, RemoveNoneTypePipe, AlertComponent, GeneralMessageModalComponent, AccessItemsComponent,
DistributionTargetsComponent, FilterComponent, DualListComponent, DummyDetailComponent, SelectWorkBasketPipe], DistributionTargetsComponent, FilterComponent, DualListComponent, DummyDetailComponent, SelectWorkBasketPipe],
providers: [WorkbasketService, MasterAndDetailService, PermissionService, providers: [WorkbasketService, MasterAndDetailService, PermissionService, ErrorModalService, RequestInProgressService,
AlertService, ErrorModalService, SavingWorkbasketService] AlertService, SavingWorkbasketService]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@ -41,8 +41,7 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private masterAndDetailService: MasterAndDetailService, private masterAndDetailService: MasterAndDetailService,
private permissionService: PermissionService, private permissionService: PermissionService) { }
private errorModalService: ErrorModalService) { }

View File

@ -4,12 +4,6 @@
<button type="button" (click)="addWorkbasket()" data-toggle="tooltip" title="Add" class="btn btn-default"> <button type="button" (click)="addWorkbasket()" data-toggle="tooltip" title="Add" class="btn btn-default">
<span class="glyphicon glyphicon-plus green" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus green" aria-hidden="true"></span>
</button> </button>
<button *ngIf="workbasketIdSelected" type="button" (click)="copyWorkbasket()" data-toggle="tooltip" title="copy" class="btn btn-default">
<span class="glyphicon glyphicon-copy" aria-hidden="true"></span>
</button>
<button *ngIf="workbasketIdSelected" type="button" (click)="removeWorkbasket()" data-toggle="tooltip" title="Remove" class="btn btn-default remove">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>
<taskana-import-export-component [currentSelection]="'workbaskets'"></taskana-import-export-component> <taskana-import-export-component [currentSelection]="'workbaskets'"></taskana-import-export-component>
</div> </div>
<div class="pull-right margin-right"> <div class="pull-right margin-right">

View File

@ -1,5 +1,5 @@
.list-group-item { .list-group-item {
padding: 5px 0px; padding: 5px 0px 2px 1px;
border: none; border: none;
} }

View File

@ -73,7 +73,6 @@ describe('WorkbasketListToolbarComponent', () => {
component.workbaskets = new Array<WorkbasketSummary>( component.workbaskets = new Array<WorkbasketSummary>(
new WorkbasketSummary('1', 'key1', 'NAME1', 'description 1', 'owner 1', new WorkbasketSummary('1', 'key1', 'NAME1', 'description 1', 'owner 1',
undefined, undefined, undefined, undefined, undefined, undefined, undefined, new Links({ 'href': 'selfLink' }))); undefined, undefined, undefined, undefined, undefined, undefined, undefined, new Links({ 'href': 'selfLink' })));
component.workbasketIdSelected = '1';
fixture.detectChanges(); fixture.detectChanges();
}); });
@ -93,23 +92,6 @@ describe('WorkbasketListToolbarComponent', () => {
}); });
it('should navigate to copy-workbasket when click on add copy workbasket', () => {
const spy = spyOn(router, 'navigate');
component.copyWorkbasket();
expect(spy.calls.first().args[0][0].outlets.detail[0]).toBe('copy-workbasket');
});
it('should call to workbasket service to remove workbasket after click on remove workbasket', () => {
const spy = spyOn(router, 'navigate');
component.removeWorkbasket();
expect(requestInProgressService.setRequestInProgress).toHaveBeenCalledWith(true);
expect(workbasketService.deleteWorkbasket).toHaveBeenCalledWith('selfLink');
expect(requestInProgressService.setRequestInProgress).toHaveBeenCalledWith(false);
expect(workbasketService.triggerWorkBasketSaved).toHaveBeenCalled();
expect(spy.calls.first().args[0][0]).toBe('/workbaskets');
});
it('should emit performSorting when sorting is triggered', () => { it('should emit performSorting when sorting is triggered', () => {
let sort: SortingModel; let sort: SortingModel;
const compareSort = new SortingModel(); const compareSort = new SortingModel();

View File

@ -38,8 +38,6 @@ export class WorkbasketListToolbarComponent implements OnInit {
@Input() workbaskets: Array<WorkbasketSummary>; @Input() workbaskets: Array<WorkbasketSummary>;
@Input() workbasketIdSelected: string;
@Output() workbasketIdSelectedChanged: string;
@Output() performSorting = new EventEmitter<SortingModel>(); @Output() performSorting = new EventEmitter<SortingModel>();
@Output() performFilter = new EventEmitter<FilterModel>(); @Output() performFilter = new EventEmitter<FilterModel>();
workbasketServiceSubscription: Subscription; workbasketServiceSubscription: Subscription;
@ -66,31 +64,7 @@ export class WorkbasketListToolbarComponent implements OnInit {
} }
addWorkbasket() { addWorkbasket() {
this.workbasketIdSelected = undefined; this.workbasketService.selectWorkBasket(undefined);
this.router.navigate([{ outlets: { detail: ['new-workbasket'] } }], { relativeTo: this.route }); this.router.navigate([{ outlets: { detail: ['new-workbasket'] } }], { relativeTo: this.route });
} }
removeWorkbasket() {
this.requestInProgressService.setRequestInProgress(true);
this.workbasketService.deleteWorkbasket(this.findWorkbasketSelectedObject()._links.self.href).subscribe(response => {
this.requestInProgressService.setRequestInProgress(false);
this.workbasketService.triggerWorkBasketSaved();
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS,
`Workbasket ${this.workbasketIdSelected} was removed successfully`))
this.router.navigate(['/workbaskets']);
}, error => {
this.requestInProgressService.setRequestInProgress(false);
this.errorModalService.triggerError(new ErrorModel(
`There was an error deleting workbasket ${this.workbasketIdSelected}`, error.error.message))
});
}
copyWorkbasket() {
this.workbasketIdSelected = undefined;
this.router.navigate([{ outlets: { detail: ['copy-workbasket'] } }], { relativeTo: this.route });
}
private findWorkbasketSelectedObject() {
return this.workbaskets.find(element => element.workbasketId === this.workbasketIdSelected);
}
} }

View File

@ -1,8 +1,7 @@
<div class="workbasket-list-full-height"> <div class="workbasket-list-full-height">
<div class="footer-space"> <div class="footer-space">
<div #wbToolbar> <div #wbToolbar>
<taskana-workbasket-list-toolbar [workbaskets]="workbaskets" (performFilter)="performFilter($event)" (performSorting)="performSorting ($event)" <taskana-workbasket-list-toolbar [workbaskets]="workbaskets" (performFilter)="performFilter($event)" (performSorting)="performSorting ($event)"></taskana-workbasket-list-toolbar>
[(workbasketIdSelected)]="selectedId"></taskana-workbasket-list-toolbar>
</div> </div>
<taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner> <taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner>
<div> <div>

View File

@ -1,11 +1,13 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { WorkbasketListComponent } from './administration/workbasket/master/list/workbasket-list.component'; import { WorkbasketListComponent } from './administration/workbasket/master/list/workbasket-list.component';
import { WorkbasketDetailsComponent } from './administration/workbasket/details/workbasket-details.component'; import { WorkbasketDetailsComponent } from './administration/workbasket/details/workbasket-details.component';
import { MasterAndDetailComponent } from './shared/master-and-detail/master-and-detail.component'; import { MasterAndDetailComponent } from './shared/master-and-detail/master-and-detail.component';
import { NoAccessComponent } from './administration/workbasket/details/noAccess/no-access.component'; import { NoAccessComponent } from './administration/workbasket/details/noAccess/no-access.component';
import {ClassificationListComponent} from './administration/classification/master/list/classification-list.component'; import { ClassificationListComponent } from './administration/classification/master/list/classification-list.component';
import { ClassificationDetailsComponent } from 'app/administration/classification/details/classification-details.component';
const appRoutes: Routes = [ const appRoutes: Routes = [
{ {
@ -30,15 +32,20 @@ const appRoutes: Routes = [
] ]
}, },
{ {
path: 'administration/classifications', path: 'administration/classifications',
component: MasterAndDetailComponent, component: MasterAndDetailComponent,
children: [ children: [
{ {
path: '', path: '',
component: ClassificationListComponent, component: ClassificationListComponent,
outlet: 'master' outlet: 'master'
} },
] {
path: ':id',
component: ClassificationDetailsComponent,
outlet: 'detail'
}
]
}, },
{ {
path: '', path: '',

View File

@ -32,10 +32,11 @@ import { SortComponent } from './shared/sort/sort.component';
import { GeneralMessageModalComponent } from './shared/general-message-modal/general-message-modal.component'; import { GeneralMessageModalComponent } from './shared/general-message-modal/general-message-modal.component';
import { PaginationComponent } from './administration/workbasket/master/list/pagination/pagination.component'; import { PaginationComponent } from './administration/workbasket/master/list/pagination/pagination.component';
import { ClassificationListComponent } from './administration/classification/master/list/classification-list.component'; import { ClassificationListComponent } from './administration/classification/master/list/classification-list.component';
import { ClassificationDetailsComponent } from './administration/classification/details/classification-details.component';
import { ImportExportComponent } from './shared/import-export/import-export.component'; import { ImportExportComponent } from './shared/import-export/import-export.component';
import { MasterAndDetailComponent } from './shared/master-and-detail/master-and-detail.component'; import { MasterAndDetailComponent } from './shared/master-and-detail/master-and-detail.component';
import { ClassificationTypesSelectorComponent } from './shared/classification-types-selector/classification-types-selector.component'; import { ClassificationTypesSelectorComponent } from './shared/classification-types-selector/classification-types-selector.component';
import { TreeComponent } from './shared/tree/tree.component'; import { TaskanaTreeComponent } from './shared/tree/tree.component';
/** /**
* Services * Services
@ -96,8 +97,9 @@ const DECLARATIONS = [
PaginationComponent, PaginationComponent,
ClassificationListComponent, ClassificationListComponent,
ImportExportComponent, ImportExportComponent,
TreeComponent, TaskanaTreeComponent,
ClassificationTypesSelectorComponent, ClassificationTypesSelectorComponent,
ClassificationDetailsComponent,
MapValuesPipe, MapValuesPipe,
RemoveNoneTypePipe, RemoveNoneTypePipe,
SelectWorkBasketPipe, SelectWorkBasketPipe,

View File

@ -1,24 +1,28 @@
import { LinksClassification } from 'app/models/links-classfication';
export class ClassificationDefinition { export class ClassificationDefinition {
constructor(public classificationId: string, constructor(public classificationId: string = undefined,
public key: string, public key: string = undefined,
public parentId: string, public parentId: string = undefined,
public category: string, public category: string = undefined,
public domain: string, public domain: string = undefined,
public isValidInDomain: boolean, public type: string = undefined,
public created: string, public isValidInDomain: boolean = undefined,
public modifies: string, public created: string = undefined,
public name: string, public modifies: string = undefined,
public description: string, public name: string = undefined,
public priority: number, public description: string = undefined,
public serviceLevel: string, public priority: number = undefined,
public applicationEntryPoint: string, public serviceLevel: string = undefined,
public custom1: string, public applicationEntryPoint: string = undefined,
public custom2: string, public custom1: string = undefined,
public custom3: string, public custom2: string = undefined,
public custom4: string, public custom3: string = undefined,
public custom5: string, public custom4: string = undefined,
public custom6: string, public custom5: string = undefined,
public custom7: string, public custom6: string = undefined,
public custom8: string) { public custom7: string = undefined,
public custom8: string = undefined,
public _links: LinksClassification = new LinksClassification()) {
} }
} }

View File

@ -0,0 +1,13 @@
import { Classification } from './classification';
import { Links } from './links';
export class ClassificationResource {
constructor(
public _embedded: {
'classificationSummaryResourceList': Array<Classification>
} = { 'classificationSummaryResourceList': [] },
public _links: Links = new Links(),
) {
}
}

View File

@ -1,5 +1,7 @@
import { Links } from 'app/models/links';
export class Classification { export class Classification {
constructor(public id: string, constructor(public classificationId: string,
public key: string, public key: string,
public category: string, public category: string,
public type: string, public type: string,
@ -7,6 +9,7 @@ export class Classification {
public name: string, public name: string,
public parentId: string, public parentId: string,
public priority: number, public priority: number,
public serviceLevel: string) { public serviceLevel: string,
public _links: Links = new Links()) {
} }
} }

View File

@ -0,0 +1,12 @@
import { Links } from './links';
export class LinksClassification extends Links {
constructor(
self = undefined,
distributionTargets = undefined,
accessItems = undefined,
public getAllClassifications: { 'href': string } = undefined,
public createClassification: { 'href': string } = undefined,
public updateClassification: { 'href': string } = undefined,
) { super(self, distributionTargets, accessItems) }
}

View File

@ -1,6 +1,6 @@
import { Classification } from 'app/models/classification'; import { Classification } from 'app/models/classification';
export class TreeNode extends Classification { export class TreeNodeModel extends Classification {
constructor(public id: string = '', constructor(public id: string = '',
public key: string = '', public key: string = '',
public category: string = '', public category: string = '',
@ -10,7 +10,7 @@ export class TreeNode extends Classification {
public parentId: string = '', public parentId: string = '',
public priority: number = 0, public priority: number = 0,
public serviceLevel: string = '', public serviceLevel: string = '',
public children: Array<TreeNode> = undefined) { public children: Array<TreeNodeModel> = undefined) {
super(id, key, category, type, domain, name, parentId, priority, serviceLevel); super(id, key, category, type, domain, name, parentId, priority, serviceLevel);
} }
} }

View File

@ -3,14 +3,18 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { Classification } from 'app/models/classification'; import { Classification } from 'app/models/classification';
import { TreeNode } from 'app/models/tree-node'; import { TreeNodeModel } from 'app/models/tree-node';
import { ClassificationDefinition } from 'app/models/classification-definition';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject'; import { Subject } from 'rxjs/Subject';
import { ClassificationResource } from '../../models/classification-resource';
@Injectable() @Injectable()
export class ClassificationsService { export class ClassificationsService {
url = environment.taskanaRestUrl + '/v1/classifications'; url = environment.taskanaRestUrl + '/v1/classifications';
classificationSelected = new Subject<string>();
httpOptions = { httpOptions = {
headers: new HttpHeaders({ headers: new HttpHeaders({
@ -19,39 +23,65 @@ export class ClassificationsService {
}) })
}; };
private classificationRef: Observable<Array<Classification>>; private classificationRef: Observable<ClassificationResource>;
private classificationTypes: Array<string>; private classificationTypes: Array<string>;
constructor(private httpClient: HttpClient) { constructor(private httpClient: HttpClient) {
} }
// GET // GET
getClassifications(forceRequest = false, type = 'TASK', domain = ''): Observable<Array<TreeNode>> { getClassifications(forceRequest = false, type = 'TASK', domain = ''): Observable<Array<TreeNodeModel>> {
if (!forceRequest && this.classificationRef) { if (!forceRequest && this.classificationRef) {
return this.classificationRef.map((response: Array<Classification>) => { return this.classificationRef.map((response: ClassificationResource) => {
return this.buildHierarchy(response, type, domain); if (!response._embedded) {
return [];
}
return this.buildHierarchy(response._embedded.classificationSummaryResourceList, type, domain);
}); });
} }
this.classificationRef = this.httpClient.get<Array<Classification>>(`${environment.taskanaRestUrl}/v1/classifications`, this.classificationRef = this.httpClient.get<ClassificationResource>(`${environment.taskanaRestUrl}/v1/classifications`,
this.httpOptions); this.httpOptions);
return this.classificationRef.map((response: Array<Classification>) => { return this.classificationRef.map((response: ClassificationResource) => {
return this.buildHierarchy(response, type, domain); if (!response._embedded) {
return [];
}
return this.buildHierarchy(response._embedded.classificationSummaryResourceList, type, domain);
}); });
} }
getClassificationTypes(): Observable<Map<string, string>> { // GET
const typesSubject = new Subject<Map<string, string>>(); getClassification(id: string): Observable<ClassificationDefinition> {
this.classificationRef.subscribe((classifications: Array<Classification>) => { return this.httpClient.get<ClassificationDefinition>(`${environment.taskanaRestUrl}/v1/classifications/${id}`, this.httpOptions);
}
getClassificationTypes(): Observable<Array<string>> {
const typesSubject = new Subject<Array<string>>();
this.classificationRef.subscribe((classifications: ClassificationResource) => {
if (!classifications._embedded) {
return typesSubject;
}
const types = new Map<string, string>(); const types = new Map<string, string>();
classifications.forEach(element => { classifications._embedded.classificationSummaryResourceList.forEach(element => {
types.set(element.type, element.type); types.set(element.type, element.type);
}); });
typesSubject.next(types);
typesSubject.next(this.map2Array(types));
}); });
return typesSubject.asObservable(); return typesSubject.asObservable();
} }
// #region "Service extras"
selectClassification(id: string) {
this.classificationSelected.next(id);
}
getSelectedClassification(): Observable<string> {
return this.classificationSelected.asObservable();
}
// #endregion
private buildHierarchy(classifications: Array<Classification>, type: string, domain: string) { private buildHierarchy(classifications: Array<Classification>, type: string, domain: string) {
const roots = [] const roots = []
const children = new Array<any>(); const children = new Array<any>();
@ -73,12 +103,22 @@ export class ClassificationsService {
private findChildren(parent: any, children: Array<any>) { private findChildren(parent: any, children: Array<any>) {
if (children[parent.id]) { if (children[parent.classificationId]) {
parent.children = children[parent.id]; parent.children = children[parent.classificationId];
for (let index = 0, len = parent.children.length; index < len; ++index) { for (let index = 0, len = parent.children.length; index < len; ++index) {
this.findChildren(parent.children[index], children); this.findChildren(parent.children[index], children);
} }
} }
} }
private map2Array(map: Map<string, string>): Array<string> {
const returnArray = [];
map.forEach((entryVal, entryKey) => {
returnArray.push(entryKey);
});
return returnArray;
}
} }

View File

@ -1,17 +1,17 @@
<div class="dropdown clearfix btn-group"> <div class="dropdown clearfix btn-group">
<button type="button" class="btn btn-default"> {{classificationTypeSelected}}</button> <button type="button" class="btn btn-default"> {{classificationTypeSelected}}</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span> <span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span> <span class="sr-only">Toggle Dropdown</span>
</button> </button>
<div class="dropdown-menu dropdown-menu-right sortby-dropdown popup" aria-labelledby="sortingDropdown"> <div class="dropdown-menu dropdown-menu-right sortby-dropdown popup" aria-labelledby="sortingDropdown">
<li *ngFor="let classificationType of classificationTypes | mapValues"> <li *ngFor="let classificationType of classificationTypes">
<a (click)="select(classificationType.key)"> <a (click)="select(classificationType)">
<label> <label>
<span class="glyphicon {{classificationTypeSelected === classificationType.key? 'glyphicon-check': 'glyphicon-unchecked'}} blue" <span class="glyphicon {{classificationTypeSelected === classificationType? 'glyphicon-check': 'glyphicon-unchecked'}} blue"
aria-hidden="true"></span> aria-hidden="true"></span>
{{classificationType.key}} {{classificationType}}
</label> </label>
</a> </a>
</li> </li>

View File

@ -1,7 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClassificationTypesSelectorComponent } from './classification-types-selector.component'; import { ClassificationTypesSelectorComponent } from './classification-types-selector.component';
import { MapValuesPipe } from 'app/pipes/mapValues/map-values.pipe';
describe('ClassificationTypesSelectorComponent', () => { describe('ClassificationTypesSelectorComponent', () => {
let component: ClassificationTypesSelectorComponent; let component: ClassificationTypesSelectorComponent;
@ -9,9 +8,9 @@ describe('ClassificationTypesSelectorComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ ClassificationTypesSelectorComponent, MapValuesPipe ] declarations: [ClassificationTypesSelectorComponent]
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@ -7,7 +7,7 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
}) })
export class ClassificationTypesSelectorComponent implements OnInit { export class ClassificationTypesSelectorComponent implements OnInit {
@Input() classificationTypes: Map<string, string> = new Map<string, string>(); @Input() classificationTypes: Array<string> = [];
@Input() @Input()
classificationTypeSelected: string = undefined; classificationTypeSelected: string = undefined;
@Output() @Output()

View File

@ -1,4 +1,4 @@
<tree-root [nodes]="treeNodes" [state]="state" [options]="options"> <tree-root #tree [nodes]="treeNodes" [options]="options" (activate)="onActivate($event)">
<ng-template #treeNodeTemplate let-node let-index="index"> <ng-template #treeNodeTemplate let-node let-index="index">
<span class="text-top"> <span class="text-top">
<svg-icon class="blue small fa-fw" src="./assets/icons/{{node.data.category === 'EXTERN'? 'external': <svg-icon class="blue small fa-fw" src="./assets/icons/{{node.data.category === 'EXTERN'? 'external':

View File

@ -1,7 +1,7 @@
import { Input, Component } from '@angular/core'; import { Input, Component } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TreeComponent } from './tree.component'; import { TaskanaTreeComponent } from './tree.component';
import { AngularSvgIconModule } from 'angular-svg-icon'; import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
@ -16,23 +16,26 @@ class TreeVendorComponent {
@Input() options; @Input() options;
@Input() state; @Input() state;
@Input() nodes; @Input() nodes;
treeModel = {
getActiveNode() { }
}
} }
// tslint:enable:component-selector // tslint:enable:component-selector
fdescribe('TreeComponent', () => {
let component: TreeComponent; describe('TaskanaTreeComponent', () => {
let fixture: ComponentFixture<TreeComponent>; let component: TaskanaTreeComponent;
let fixture: ComponentFixture<TaskanaTreeComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [AngularSvgIconModule, HttpClientModule, HttpModule], imports: [AngularSvgIconModule, HttpClientModule, HttpModule],
declarations: [TreeComponent, TreeVendorComponent] declarations: [TaskanaTreeComponent, TreeVendorComponent]
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(TreeComponent); fixture = TestBed.createComponent(TaskanaTreeComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -1,20 +1,27 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, ViewChild, AfterViewChecked } from '@angular/core';
import { TreeNode } from 'app/models/tree-node'; import { TreeNodeModel } from 'app/models/tree-node';
import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions, ITreeState } from 'angular-tree-component';
import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions, ITreeState, TreeComponent, TreeNode } from 'angular-tree-component';
@Component({ @Component({
selector: 'taskana-tree', selector: 'taskana-tree',
templateUrl: './tree.component.html', templateUrl: './tree.component.html',
styleUrls: ['./tree.component.scss'] styleUrls: ['./tree.component.scss']
}) })
export class TreeComponent implements OnInit { export class TaskanaTreeComponent implements OnInit, AfterViewChecked {
@Input() treeNodes: TreeNode;
@Output() treeNodesChange = new EventEmitter<Array<TreeNode>>(); @ViewChild('tree')
private tree: TreeComponent;
@Input() treeNodes: TreeNodeModel;
@Output() treeNodesChange = new EventEmitter<Array<TreeNodeModel>>();
@Input() selectNodeId: string;
@Output() selectNodeIdChanged = new EventEmitter<string>();
options: ITreeOptions = { options: ITreeOptions = {
displayField: 'name', displayField: 'name',
idField: 'id', idField: 'classificationId',
actionMapping: { actionMapping: {
keys: { keys: {
[KEYS.ENTER]: (tree, node, $event) => { [KEYS.ENTER]: (tree, node, $event) => {
@ -27,14 +34,41 @@ export class TreeComponent implements OnInit {
levelPadding: 20 levelPadding: 20
} }
state: ITreeState = {
activeNodeIds: { ['']: true },
}
constructor() { } constructor() { }
ngOnInit() { ngOnInit() {
this.selectNode(this.selectNodeId);
}
ngAfterViewChecked(): void {
if (this.selectNodeId && !this.tree.treeModel.getActiveNode()) {
this.selectNode(this.selectNodeId);
} else if (!this.selectNodeId && this.tree.treeModel.getActiveNode()) {
this.unSelectActiveNode();
}
}
onActivate(treeNode: any) {
this.selectNodeIdChanged.emit(treeNode.node.data.classificationId + '');
}
private selectNode(nodeId: string) {
if (nodeId) {
const selectedNode = this.getSelectedNode(nodeId)
if (selectedNode) {
selectedNode.setIsActive(true)
}
}
}
private unSelectActiveNode() {
const activeNode = this.tree.treeModel.getActiveNode();
activeNode.setIsActive(false);
activeNode.blur();
}
private getSelectedNode(nodeId: string) {
return this.tree.treeModel.getNodeById(nodeId);
} }
} }

View File

@ -1,4 +1,3 @@
.ng-invalid:not(form) { .ng-invalid:not(form) {
border-color: $invalid; border-color: $invalid;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
@ -8,4 +7,8 @@
.required-text { .required-text {
padding-left: 15px; padding-left: 15px;
color: $invalid; color: $invalid;
} }
textarea {
resize: none;
}

View File

@ -230,7 +230,7 @@ svg-icon.fa-fw > svg {
} }
.panel-heading{ .panel-heading{
min-height: 60px; padding: 8px 15px 4px 15px;
} }
.panel { .panel {
border-radius: 0px; border-radius: 0px;