TSK-190 Add workbasket information saving feature, spinner, modal and alert components.

This commit is contained in:
Martin Rojas Miguel Angel 2018-02-26 12:34:49 +01:00 committed by Holger Hagen
parent bc5e0e9688
commit e10cc3a0c9
40 changed files with 984 additions and 505 deletions

View File

@ -32,6 +32,7 @@ module.exports = function (config) {
},
angularCli: {
environment: 'dev'
//codeCoverage: true
},
reporters: config.angularCli && config.angularCli.codeCoverage
? ['progress', 'coverage-istanbul']

View File

@ -36,20 +36,20 @@ describe('AppComponent', () => {
document.body.removeChild(debugElement);
}));
it('should create the app', async(() => {
it('should create the app', (() => {
expect(app).toBeTruthy();
}));
it(`should have as title 'Taskana administration'`, async(() => {
it(`should have as title 'Taskana administration'`, (() => {
expect(app.title).toEqual('Taskana administration');
}));
it('should render title in a <a> tag', async(() => {
it('should render title in a <a> tag', (() => {
fixture.detectChanges();
expect(debugElement.querySelector('ul p a').textContent).toContain('Taskana administration');
}));
it('should call Router.navigateByUrl("categories") and workbasketRoute should be false', async (inject([Router], (router: Router) => {
it('should call Router.navigateByUrl("categories") and workbasketRoute should be false', (inject([Router], (router: Router) => {
expect(app.workbasketsRoute).toBe(true);
fixture.detectChanges();

View File

@ -10,6 +10,7 @@ import { AlertModule } from 'ngx-bootstrap';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { TreeModule } from 'angular-tree-component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
/**
* Components
@ -28,6 +29,8 @@ import { NoAccessComponent } from './workbasket/noAccess/no-access.component';
import { SpinnerComponent } from './shared/spinner/spinner.component';
import { FilterComponent } from './shared/filter/filter.component';
import { IconTypeComponent } from './shared/type-icon/icon-type.component';
import { AlertComponent } from './shared/alert/alert.component';
import { GeneralMessageModalComponent } from './shared/general-message-modal/general-message-modal.component';
//Shared
import { MasterAndDetailComponent} from './shared/masterAndDetail/master-and-detail.component';
@ -35,11 +38,12 @@ import { MasterAndDetailComponent} from './shared/masterAndDetail/master-and-det
/**
* Services
*/
import { WorkbasketService } from './services/workbasketservice.service';
import { WorkbasketService } from './services/workbasket.service';
import { MasterAndDetailService } from './services/master-and-detail.service';
import { HttpClientInterceptor } from './services/http-client-interceptor.service';
import { PermissionService } from './services/permission.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AlertService } from './services/alert.service';
/**
* Pipes
@ -55,7 +59,8 @@ const MODULES = [
AppRoutingModule,
AlertModule.forRoot(),
AngularSvgIconModule,
HttpClientModule
HttpClientModule,
BrowserAnimationsModule
];
const DECLARATIONS = [
@ -74,6 +79,8 @@ const DECLARATIONS = [
SpinnerComponent,
FilterComponent,
IconTypeComponent,
AlertComponent,
GeneralMessageModalComponent,
MapValuesPipe,
RemoveNoneTypePipe
];
@ -90,7 +97,7 @@ const DECLARATIONS = [
useClass: HttpClientInterceptor,
multi: true
},
AlertService
],
bootstrap: [AppComponent]
})

View File

@ -0,0 +1,6 @@
export class Links {
constructor(
public rel: string,
public href: string,
){}
}

View File

@ -1,7 +1,8 @@
import {WorkbasketSummary} from './workbasketSummary';
import { WorkbasketSummary } from './workbasketSummary';
import { Links } from './links';
export class Workbasket {
constructor(
public id: string,
public workbasketId: string,
public created: string,
public key: string,
public domain: string,
@ -18,5 +19,27 @@ export class Workbasket {
public orgLevel2: string,
public orgLevel3: string,
public orgLevel4: string,
public workbasketSummary: WorkbasketSummary){}
public links: Array<Links>){}
public static equals(org: Workbasket, comp: Workbasket): boolean {
if (org.workbasketId !== comp.workbasketId) { return false; }
if (org.created !== comp.created) { return false; }
if (org.key !== comp.key) { return false; }
if (org.domain !== comp.domain) { return false; }
if (org.type !== comp.type) { return false; }
if (org.modified !== comp.modified) { return false; }
if (org.name !== comp.name) { return false; }
if (org.description !== comp.description) { return false; }
if (org.owner !== comp.owner) { return false; }
if (org.custom1 !== comp.custom1) { return false; }
if (org.custom2 !== comp.custom2) { return false; }
if (org.custom3 !== comp.custom3) { return false; }
if (org.custom4 !== comp.custom4) { return false; }
if (org.orgLevel1 !== comp.orgLevel1) { return false; }
if (org.orgLevel2 !== comp.orgLevel2) { return false; }
if (org.orgLevel3 !== comp.orgLevel3) { return false; }
if (org.orgLevel4 !== comp.orgLevel4) { return false; }
return true;
}
}

View File

@ -1,3 +1,5 @@
import {Links} from './links';
export class WorkbasketSummary {
constructor(
public workbasketId: string,
@ -11,5 +13,6 @@ export class WorkbasketSummary {
public orgLevel1: string,
public orgLevel2: string,
public orgLevel3: string,
public orgLevel4: string){}
public orgLevel4: string,
public links: Array<Links> = undefined){}
}

View File

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

View File

@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
export enum AlertType {
SUCCESS = 'success',
INFO = 'info',
WARNING = 'warning',
DANGER = 'danger',
}
export class AlertModel {
constructor(public type: string = AlertType.SUCCESS,
public text: string = 'Success',
public autoClosing: boolean = true,
public closingDelay: number = 2500){
}
}
@Injectable()
export class AlertService {
public alertTriggered = new Subject<AlertModel>();
constructor() { }
triggerAlert(alert: AlertModel) {
this.alertTriggered.next(alert);
}
getAlert(): Observable<AlertModel> {
return this.alertTriggered.asObservable();
}
}

View File

@ -1,19 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { HttpModule } from '@angular/http';
import { HttpExtensionService } from './http-extension.service';
import { PermissionService } from './permission.service';
describe('HttpExtensionService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports:[HttpClientModule, HttpModule],
providers: [HttpExtensionService, PermissionService]
});
});
it('should be created', inject([HttpExtensionService], (service: HttpExtensionService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -1,28 +0,0 @@
import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { PermissionService } from './permission.service';
@Injectable()
export class HttpExtensionService extends Http {
permissionService: PermissionService;
constructor(backend: XHRBackend, defaultOptions: RequestOptions, permissionService: PermissionService) {
super(backend, defaultOptions);
this.permissionService = permissionService;
}
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
this.permissionService.setPermission(true);
return super.request(url, options).catch((error: Response) => {
if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
this.permissionService.setPermission(false);
}
return Observable.throw(error);
});
}
}

View File

@ -1,5 +1,5 @@
import { TestBed, inject, async } from '@angular/core/testing';
import { WorkbasketService, Direction } from './workbasketservice.service';
import { WorkbasketService, Direction } from './workbasket.service';
import { HttpModule, Http } from '@angular/http';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
@ -30,17 +30,17 @@ describe('WorkbasketService ', () => {
});
it('should have a valid query parameter expression with sortBy=name and order=desc', () => {
workbasketService.getWorkBasketsSummary('name', Direction.DESC);
workbasketService.getWorkBasketsSummary(undefined, 'name', Direction.DESC);
expect(httpClient.get).toHaveBeenCalledWith('http://localhost:8080/v1/workbaskets/?sortBy=name&order=desc', jasmine.any(Object));
});
it('should have a valid query parameter expression with sortBy=name and order=desc and descLike=some description ',() => {
workbasketService.getWorkBasketsSummary('name', Direction.DESC, undefined, undefined, 'some description');
workbasketService.getWorkBasketsSummary(undefined,'name', Direction.DESC, undefined, undefined, 'some description');
expect(httpClient.get).toHaveBeenCalledWith('http://localhost:8080/v1/workbaskets/?sortBy=name&order=desc&descLike=some description', jasmine.any(Object));
});
it('should have a valid query parameter expression with sortBy=key, order=asc, descLike=some description and type=group ',() => {
workbasketService.getWorkBasketsSummary('name', Direction.DESC, undefined, undefined, 'some description', undefined, undefined, 'group');
workbasketService.getWorkBasketsSummary(undefined,'name', Direction.DESC, undefined, undefined, 'some description', undefined, undefined, 'group');
expect(httpClient.get).toHaveBeenCalledWith('http://localhost:8080/v1/workbaskets/?sortBy=name&order=desc&descLike=some description&type=group', jasmine.any(Object));
});

View File

@ -0,0 +1,180 @@
import { Injectable } from '@angular/core';
import { HttpClientModule, HttpClient, HttpHeaders, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { WorkbasketSummary } from '../model/workbasketSummary';
import { Workbasket } from '../model/workbasket';
import { WorkbasketAuthorization } from '../model/workbasket-authorization';
import { environment } from '../../environments/environment';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { Timestamp } from 'rxjs';
//sort direction
export enum Direction {
ASC = 'asc',
DESC = 'desc'
};
@Injectable()
export class WorkbasketService {
public workBasketSelected = new Subject<string>();
public workBasketSaved = new Subject<number>();
constructor(private httpClient: HttpClient) { }
//Sorting
readonly SORTBY = 'sortBy';
readonly ORDER = 'order';
//Filtering
readonly NAME = 'name';
readonly NAMELIKE = 'nameLike';
readonly DESCLIKE = 'descLike';
readonly OWNER = 'owner';
readonly OWNERLIKE = 'ownerLike';
readonly TYPE = 'type';
readonly KEY = 'key';
readonly KEYLIKE = 'keyLike';
//Access
readonly REQUIREDPERMISSION = 'requiredPermission';
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Basic VEVBTUxFQURfMTpURUFNTEVBRF8x'
})
};
private workbasketSummaryRef: Observable<WorkbasketSummary[]>;
//#region "REST calls"
// GET
getWorkBasketsSummary( forceRequest: boolean = false,
sortBy: string = this.KEY,
order: string = Direction.ASC,
name: string = undefined,
nameLike: string = undefined,
descLike: string = undefined,
owner: string = undefined,
ownerLike: string = undefined,
type: string = undefined,
key: string = undefined,
keyLike: string = undefined,
requiredPermission: string = undefined): Observable<WorkbasketSummary[]> {
if(this.workbasketSummaryRef && !forceRequest){
return this.workbasketSummaryRef;
}
return this.httpClient.get<WorkbasketSummary[]>(`${environment.taskanaRestUrl}/v1/workbaskets/${this.getWorkbasketSummaryQueryParameters(sortBy, order, name,
nameLike, descLike, owner, ownerLike, type, key, keyLike, requiredPermission)}`, this.httpOptions);
}
// GET
getWorkBasket(url: string): Observable<Workbasket> {
return this.httpClient.get<Workbasket>(url, this.httpOptions);
}
getWorkBasket1(id: string): Observable<Workbasket> {
return this.httpClient.get<Workbasket>(environment.taskanaRestUrl + '/v1/workbaskets/' + id, this.httpOptions);
}
// POST
createWorkbasket(url: string, workbasket: Workbasket): Observable<Workbasket> {
return this.httpClient
.post<Workbasket>(url, this.httpOptions);
}
// PUT
updateWorkbasket(url: string, workbasket: Workbasket): Observable<Workbasket> {
return this.httpClient
.put<Workbasket>(url, workbasket, this.httpOptions)
.catch(this.handleError);
}
// DELETE
deleteWorkbasket(id: string) {
return this.httpClient.delete(environment.taskanaRestUrl + '/v1/workbaskets/' + id, this.httpOptions);
}
// GET
getAllWorkBasketAuthorizations(id: String): Observable<WorkbasketAuthorization[]> {
return this.httpClient.get<WorkbasketAuthorization[]>(environment.taskanaRestUrl + '/v1/workbaskets/' + id + '/authorizations', this.httpOptions);
}
// POST
createWorkBasketAuthorization(workbasketAuthorization: WorkbasketAuthorization): Observable<WorkbasketAuthorization> {
return this.httpClient.post<WorkbasketAuthorization>(environment.taskanaRestUrl + '/v1/workbaskets/authorizations', workbasketAuthorization, this.httpOptions);
}
// PUT
updateWorkBasketAuthorization(workbasketAuthorization: WorkbasketAuthorization): Observable<WorkbasketAuthorization> {
return this.httpClient.put<WorkbasketAuthorization>(environment.taskanaRestUrl + '/v1/workbaskets/authorizations/' + workbasketAuthorization.id, workbasketAuthorization, this.httpOptions)
}
// DELETE
deleteWorkBasketAuthorization(workbasketAuthorization: WorkbasketAuthorization) {
return this.httpClient.delete(environment.taskanaRestUrl + '/v1/workbaskets/authorizations/' + workbasketAuthorization.id, this.httpOptions);
}
//#endregion
//#region "Service extras"
selectWorkBasket(id: string) {
this.workBasketSelected.next(id);
}
getSelectedWorkBasket(): Observable<string> {
return this.workBasketSelected.asObservable();
}
triggerWorkBasketSaved() {
this.workBasketSaved.next(Date.now());
}
workbasketSavedTriggered(): Observable<number> {
return this.workBasketSaved.asObservable();
}
//#endregion
//#region private
private getWorkbasketSummaryQueryParameters(sortBy: string,
order: string,
name: string,
nameLike: string,
descLike: string,
owner: string,
ownerLike: string,
type: string,
key: string,
keyLike: string,
requiredPermission: string): string {
let query: string = '?';
query += sortBy ? `${this.SORTBY}=${sortBy}&` : '';
query += order ? `${this.ORDER}=${order}&` : '';
query += name ? `${this.NAME}=${name}&` : '';
query += nameLike ? `${this.NAMELIKE}=${nameLike}&` : '';
query += descLike ? `${this.DESCLIKE}=${descLike}&` : '';
query += owner ? `${this.OWNER}=${owner}&` : '';
query += ownerLike ? `${this.OWNERLIKE}=${ownerLike}&` : '';
query += type ? `${this.TYPE}=${type}&` : '';
query += key ? `${this.KEY}=${key}&` : '';
query += keyLike ? `${this.KEYLIKE}=${keyLike}&` : '';
query += requiredPermission ? `${this.REQUIREDPERMISSION}=${requiredPermission}&` : '';
if (query.lastIndexOf('&') === query.length - 1) {
query = query.slice(0, query.lastIndexOf('&'))
}
return query;
}
private handleError (error: Response | any) {
// In a real world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
//#endregion
}

View File

@ -1,136 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClientModule, HttpClient, HttpHeaders, } from '@angular/common/http';
import { WorkbasketSummary } from '../model/workbasketSummary';
import { Workbasket } from '../model/workbasket';
import { WorkbasketAuthorization } from '../model/workbasket-authorization';
import { environment } from '../../environments/environment';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
//sort direction
export enum Direction{
ASC = 'asc',
DESC = 'desc'
};
@Injectable()
export class WorkbasketService {
public workBasketSelected = new Subject<string>();
constructor(private httpClient: HttpClient) { }
//Sorting
readonly SORTBY='sortBy';
readonly ORDER='order';
//Filtering
readonly NAME='name';
readonly NAMELIKE='nameLike';
readonly DESCLIKE='descLike';
readonly OWNER='owner';
readonly OWNERLIKE='ownerLike';
readonly TYPE='type';
readonly KEY='key';
readonly KEYLIKE='keyLike';
//Access
readonly REQUIREDPERMISSION='requiredPermission';
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Basic VEVBTUxFQURfMTpURUFNTEVBRF8x'
})
};
//REST calls
getWorkBasketsSummary(sortBy: string = this.KEY,
order: string = Direction.ASC,
name: string = undefined,
nameLike: string = undefined,
descLike: string = undefined,
owner:string = undefined,
ownerLike:string = undefined,
type:string = undefined,
key:string = undefined,
keyLike:string = undefined,
requiredPermission: string = undefined): Observable<WorkbasketSummary[]> {
return this.httpClient.get<WorkbasketSummary[]>(`${environment.taskanaRestUrl}/v1/workbaskets/${this.getWorkbasketSummaryQueryParameters(sortBy, order, name,
nameLike, descLike, owner, ownerLike, type, key, keyLike, requiredPermission)}`,this.httpOptions)
}
getWorkBasket(id: string): Observable<Workbasket> {
return this.httpClient.get<Workbasket>(`${environment.taskanaRestUrl}/v1/workbaskets/${id}`, this.httpOptions);
}
createWorkbasket(workbasket: WorkbasketSummary): Observable<WorkbasketSummary> {
return this.httpClient.post<WorkbasketSummary>(environment.taskanaRestUrl + '/v1/workbaskets', workbasket, this.httpOptions);
}
deleteWorkbasket(id: string) {
return this.httpClient.delete(environment.taskanaRestUrl + '/v1/workbaskets/' + id, this.httpOptions);
}
updateWorkbasket(workbasket: WorkbasketSummary): Observable<WorkbasketSummary> {
return this.httpClient.put<WorkbasketSummary>(environment.taskanaRestUrl + '/v1/workbaskets/' + workbasket.workbasketId, workbasket, this.httpOptions);
}
getAllWorkBasketAuthorizations(id: String): Observable<WorkbasketAuthorization[]> {
return this.httpClient.get<WorkbasketAuthorization[]>(environment.taskanaRestUrl + '/v1/workbaskets/' + id + '/authorizations', this.httpOptions);
}
createWorkBasketAuthorization(workbasketAuthorization: WorkbasketAuthorization): Observable<WorkbasketAuthorization> {
return this.httpClient.post<WorkbasketAuthorization>(environment.taskanaRestUrl + '/v1/workbaskets/authorizations', workbasketAuthorization, this.httpOptions);
}
updateWorkBasketAuthorization(workbasketAuthorization: WorkbasketAuthorization): Observable<WorkbasketAuthorization> {
return this.httpClient.put<WorkbasketAuthorization>(environment.taskanaRestUrl + '/v1/workbaskets/authorizations/' + workbasketAuthorization.id, workbasketAuthorization, this.httpOptions)
}
deleteWorkBasketAuthorization(workbasketAuthorization: WorkbasketAuthorization) {
return this.httpClient.delete(environment.taskanaRestUrl + '/v1/workbaskets/authorizations/' + workbasketAuthorization.id, this.httpOptions);
}
//Service extras
selectWorkBasket(id: string){
this.workBasketSelected.next(id);
}
getSelectedWorkBasket(): Observable<string>{
return this.workBasketSelected.asObservable();
}
private getWorkbasketSummaryQueryParameters( sortBy: string,
order: string,
name: string,
nameLike: string,
descLike: string,
owner: string,
ownerLike: string,
type: string,
key: string,
keyLike: string,
requiredPermission: string): string{
let query: string = '?';
query += sortBy? `${this.SORTBY}=${sortBy}&`:'';
query += order? `${this.ORDER}=${order}&`:'';
query += name? `${this.NAME}=${name}&`:'';
query += nameLike? `${this.NAMELIKE}=${nameLike}&`:'';
query += descLike? `${this.DESCLIKE}=${descLike}&`:'';
query += owner? `${this.OWNER}=${owner}&`:'';
query += ownerLike? `${this.OWNERLIKE}=${ownerLike}&`:'';
query += type? `${this.TYPE}=${type}&`:'';
query += key? `${this.KEY}=${key}&`:'';
query += keyLike? `${this.KEYLIKE}=${keyLike}&`:'';
query += requiredPermission? `${this.REQUIREDPERMISSION}=${requiredPermission}&`:'';
if(query.lastIndexOf('&') === query.length-1){
query = query.slice(0, query.lastIndexOf('&'))
}
return query;
}
}

View File

@ -0,0 +1,8 @@
<div *ngIf="alert" [@alertState]="alert" class="alert alert-{{alert.type}} {{alert.autoClosing? '':'alert-dismissible'}} footer"
role="alert">
<span class="glyphicon {{alert.type === 'success'? 'glyphicon-thumbs-up': 'glyphicon-exclamation-sign' }}" aria-hidden="true"></span>
{{alert.text}}
<button *ngIf="!alert.autoClosing" type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>

View File

@ -0,0 +1,7 @@
.footer{
position: fixed;
bottom: 0;
width: 100%;
margin-bottom: 0px;
}

View File

@ -0,0 +1,72 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AlertService, AlertModel, AlertType } from '../../services/alert.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AlertComponent } from './alert.component';
describe('AlertComponent', () => {
let component: AlertComponent;
let fixture: ComponentFixture<AlertComponent>;
let debugElement,
alertService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports:[BrowserAnimationsModule],
declarations: [AlertComponent],
providers: [AlertService]
})
.compileComponents();
}));
beforeEach(() => {
alertService = TestBed.get(AlertService);
fixture = TestBed.createComponent(AlertComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.nativeElement;
fixture.detectChanges();
});
afterEach(() => {
document.body.removeChild(debugElement);
})
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show alert message', () => {
alertService.triggerAlert(new AlertModel(AlertType.SUCCESS,'some custom text',));
fixture.detectChanges();
expect(debugElement.querySelector('.alert.alert-success')).toBeDefined();
expect(debugElement.querySelector('.alert.alert-success').innerText).toBe('some custom text');
});
it('should have differents alert types', () => {
alertService.triggerAlert(new AlertModel(AlertType.DANGER,'some custom text',));
fixture.detectChanges();
expect(debugElement.querySelector('.alert.alert-danger')).toBeDefined();
alertService.triggerAlert(new AlertModel(AlertType.WARNING,'some custom text',));
fixture.detectChanges();
expect(debugElement.querySelector('.alert.alert-warning')).toBeDefined();
});
it('should define a closing timeout if alert has autoclosing property', (done) => {
alertService.triggerAlert(new AlertModel(AlertType.SUCCESS,'some custom text',true, 5));
fixture.detectChanges();
expect(component.alert).toBeDefined();
setTimeout(()=>{
fixture.detectChanges();
expect(component.alert).toBeUndefined();
done();
},6)
});
it('should have defined a closing button if alert has no autoclosing property', () => {
alertService.triggerAlert(new AlertModel(AlertType.DANGER,'some custom text',false));
fixture.detectChanges();
expect(debugElement.querySelector('.alert.alert-danger > button')).toBeDefined();
});
});

View File

@ -0,0 +1,41 @@
import { Component, OnInit } from '@angular/core';
import { AlertService, AlertModel } from '../../services/alert.service';
import { trigger, state, style, animate, transition } from '@angular/animations';
@Component({
selector: 'taskana-alert',
templateUrl: './alert.component.html',
styleUrls: ['./alert.component.scss'],
animations: [
trigger('alertState', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(100%)' }),
animate(100)
]),
transition('* => void', [
animate(100, style({ transform: 'translateY(100%)' }))
])
])
]
})
export class AlertComponent implements OnInit {
alert: AlertModel;
constructor(private alertService: AlertService) { }
ngOnInit() {
this.alertService.getAlert().subscribe((alert: AlertModel) => {
this.alert = alert;
if (alert.autoClosing) {
this.setTimeOutForClosing(alert.closingDelay);
}
});
}
setTimeOutForClosing(time: number) {
setTimeout(() => {
this.alert = undefined;
}, time);
}
}

View File

@ -1,4 +1,4 @@
<div type="text" id="{{target}}" class="list-group-seach collapse">
<div type="text" id="{{target}}" data-toogle="collapse" class="list-group-seach collapse">
<div class="row">
<div class="dropdown col-xs-2">
<button class="btn btn-default" type="button" id="dropdownMenufilter" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">

View File

@ -0,0 +1,19 @@
<div class="modal fade" #generalModal tabindex="-1" data-backdrop="static" data-keyboard="false" role="dialog" aria-labelledby="generalModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="generalModalLabel">{{title}}</h4>
</div>
<div *ngIf="error" class="modal-body">
<div class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
{{message}}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-danger" data-dismiss="modal" (click)="removeMessage()">Close</button>
</div>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,35 @@
import { Component, OnInit, Input, ViewChild, OnChanges, SimpleChanges } from '@angular/core';
declare var $: any;
@Component({
selector: 'taskana-general-message-modal',
templateUrl: './general-message-modal.component.html',
styleUrls: ['./general-message-modal.component.scss']
})
export class GeneralMessageModalComponent implements OnChanges {
@Input()
message: string = '';
@Input()
title: string = '';
@Input()
error: boolean = false;
@ViewChild('generalModal')
private modal;
constructor() { }
ngOnChanges(changes: SimpleChanges) {
if (this.message) {
$(this.modal.nativeElement).modal('toggle');
}
}
removeMessage() {
this.message = undefined;
}
}

View File

@ -1,14 +1,36 @@
<div [hidden]="!isDelayedRunning" class="sk-circle">
<div class="sk-circle1 sk-child"></div>
<div class="sk-circle2 sk-child"></div>
<div class="sk-circle3 sk-child"></div>
<div class="sk-circle4 sk-child"></div>
<div class="sk-circle5 sk-child"></div>
<div class="sk-circle6 sk-child"></div>
<div class="sk-circle7 sk-child"></div>
<div class="sk-circle8 sk-child"></div>
<div class="sk-circle9 sk-child"></div>
<div class="sk-circle10 sk-child"></div>
<div class="sk-circle11 sk-child"></div>
<div class="sk-circle12 sk-child"></div>
</div>
<div [hidden]="!isDelayedRunning">
<div class="sk-circle spinner-centered" [hidden]="this.isModal">
<div class="sk-circle1 sk-child"></div>
<div class="sk-circle2 sk-child"></div>
<div class="sk-circle3 sk-child"></div>
<div class="sk-circle4 sk-child"></div>
<div class="sk-circle5 sk-child"></div>
<div class="sk-circle6 sk-child"></div>
<div class="sk-circle7 sk-child"></div>
<div class="sk-circle8 sk-child"></div>
<div class="sk-circle9 sk-child"></div>
<div class="sk-circle10 sk-child"></div>
<div class="sk-circle11 sk-child"></div>
<div class="sk-circle12 sk-child"></div>
</div>
<div #spinnerModal class="modal fade" id="spinner-modal" data-backdrop="static" data-keyboard="false" role="dialog">
<div class="modal-dialog">
<div class="modal-dialog spinner-centered">
<div class="sk-circle">
<div class="sk-circle1 sk-child"></div>
<div class="sk-circle2 sk-child"></div>
<div class="sk-circle3 sk-child"></div>
<div class="sk-circle4 sk-child"></div>
<div class="sk-circle5 sk-child"></div>
<div class="sk-circle6 sk-child"></div>
<div class="sk-circle7 sk-child"></div>
<div class="sk-circle8 sk-child"></div>
<div class="sk-circle9 sk-child"></div>
<div class="sk-circle10 sk-child"></div>
<div class="sk-circle11 sk-child"></div>
<div class="sk-circle12 sk-child"></div>
</div>
</div>
</div>
</div>
</div>

View File

@ -18,7 +18,7 @@
margin: 0 auto;
width: 15%;
height: 15%;
background-color: #337ab7;
background-color: #33b784;
border-radius: 100%;
-webkit-animation: sk-circleBounceDelay 1.2s infinite ease-in-out both;
animation: sk-circleBounceDelay 1.2s infinite ease-in-out both;
@ -119,4 +119,14 @@
-webkit-transform: scale(1);
transform: scale(1);
}
}
.floating {
position: absolute;
left: 45%;
top: auto;
z-index: 10;
}
.spinner-centered {
margin-top: calc(50vh - 100px);
}

View File

@ -1,42 +1,62 @@
import { Component, Input } from '@angular/core';
import { Component, Input, ElementRef } from '@angular/core';
import { ViewChild } from '@angular/core';
declare var $: any;
@Component({
selector: 'taskana-spinner',
templateUrl: './spinner.component.html',
styleUrls: ['./spinner.component.scss']
selector: 'taskana-spinner',
templateUrl: './spinner.component.html',
styleUrls: ['./spinner.component.scss']
})
export class SpinnerComponent {
private currentTimeout: any;
isDelayedRunning: boolean = false;
export class SpinnerComponent {
private currentTimeout: any;
@Input()
delay: number = 300;
isDelayedRunning: boolean = false;
@Input()
set isRunning(value: boolean) {
if (!value) {
this.cancelTimeout();
this.isDelayedRunning = false;
return;
}
@Input()
delay: number = 300;
if (this.currentTimeout) {
return;
}
@Input()
set isRunning(value: boolean) {
if (!value) {
this.cancelTimeout();
if (this.isModal) { this.closeModal(); }
this.isDelayedRunning = false;
return;
}
this.currentTimeout = setTimeout(() => {
this.isDelayedRunning = value;
this.cancelTimeout();
}, this.delay);
}
if (this.currentTimeout) {
return;
}
this.runSpinner(value);
private cancelTimeout(): void {
clearTimeout(this.currentTimeout);
this.currentTimeout = undefined;
}
}
ngOnDestroy(): any {
this.cancelTimeout();
}
@Input()
isModal: boolean = false;
@ViewChild('spinnerModal')
private modal;
private runSpinner(value) {
this.currentTimeout = setTimeout(() => {
if (this.isModal) { $(this.modal.nativeElement).modal('toggle'); }
this.isDelayedRunning = value;
this.cancelTimeout();
}, this.delay);
}
private closeModal() {
if (this.isDelayedRunning) {
$(this.modal.nativeElement).modal('toggle');
}
}
private cancelTimeout(): void {
clearTimeout(this.currentTimeout);
this.currentTimeout = undefined;
}
ngOnDestroy(): any {
this.cancelTimeout();
}
}

View File

@ -0,0 +1,7 @@
import { Links} from '../../model/links';
export class Utils {
static getSelfRef(links: Array<Links>) {
return links.find(l => l.rel === 'self');
}
}

View File

@ -1,6 +1,6 @@
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { WorkbasketService } from '../services/workbasketservice.service';
import { WorkbasketService } from '../services/workbasket.service';
import { WorkbasketAuthorization } from '../model/workbasket-authorization';
import { WorkbasketSummary } from '../model/workbasketSummary';

View File

@ -1,6 +1,6 @@
import { Component, OnInit, Input } from '@angular/core';
import { WorkbasketSummary } from '../model/workbasketSummary';
import { WorkbasketService } from '../services/workbasketservice.service'
import { WorkbasketService } from '../services/workbasket.service'
import { Observable } from 'rxjs/Observable';
@Component({

View File

@ -1,8 +1,10 @@
<taskana-spinner [isRunning]="requestInProgress" [isModal]= "modalSpinner" class = "centered-horizontally floating"></taskana-spinner>
<taskana-general-message-modal *ngIf="modalErrorMessage" [message]="modalErrorMessage" [title]="modalTitle" error = "true" ></taskana-general-message-modal>
<div id = "wb-information" class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">
<button type="button" class="btn btn-default btn-primary">Save</button>
<button type="button" class="btn btn-default">Cancel</button>
<button type="button" (click)="onSave()" class="btn btn-default btn-primary">Save</button>
<button type="button" (click)="onClear()" class="btn btn-default">Undo changes</button>
</div>
<h4 class="panel-header">{{workbasket.name}}</h4>
</div>

View File

@ -1,5 +1,5 @@
import { async, ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
import { WorkbasketService } from '../../../services/workbasketservice.service';
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { WorkbasketService } from '../../../services/workbasket.service';
import { WorkbasketInformationComponent } from './workbasket-information.component';
import { FormsModule } from '@angular/forms';
import { AngularSvgIconModule } from 'angular-svg-icon';
@ -7,56 +7,93 @@ import { HttpClientModule } from '@angular/common/http';
import { HttpModule, JsonpModule } from '@angular/http';
import { Workbasket } from 'app/model/workbasket';
import { ICONTYPES, IconTypeComponent } from '../../../shared/type-icon/icon-type.component';
import { SpinnerComponent } from '../../../shared/spinner/spinner.component';
import { GeneralMessageModalComponent } from '../../../shared/general-message-modal/general-message-modal.component';
import { MapValuesPipe } from '../../../pipes/map-values.pipe';
import { RemoveNoneTypePipe } from '../../../pipes/remove-none-type';
import { AlertService } from '../../../services/alert.service';
import { RouterTestingModule } from '@angular/router/testing';
import { Links } from '../../../model/links';
import { Observable } from 'rxjs/Observable';
describe('InformationComponent', () => {
let component: WorkbasketInformationComponent;
let fixture: ComponentFixture<WorkbasketInformationComponent>;
let debugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ WorkbasketInformationComponent, IconTypeComponent, MapValuesPipe, RemoveNoneTypePipe],
imports:[FormsModule, AngularSvgIconModule, HttpClientModule, HttpModule],
providers:[WorkbasketService]
let component: WorkbasketInformationComponent;
let fixture: ComponentFixture<WorkbasketInformationComponent>;
let debugElement, workbasketService;
})
.compileComponents();
fixture = TestBed.createComponent(WorkbasketInformationComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.nativeElement;
}));
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [WorkbasketInformationComponent, IconTypeComponent, MapValuesPipe, RemoveNoneTypePipe, SpinnerComponent, GeneralMessageModalComponent],
imports: [FormsModule, AngularSvgIconModule, HttpClientModule, HttpModule, RouterTestingModule],
providers: [WorkbasketService, AlertService]
afterEach(() =>{
document.body.removeChild(debugElement);
});
})
.compileComponents();
fixture = TestBed.createComponent(WorkbasketInformationComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.nativeElement;
workbasketService = TestBed.get(WorkbasketService);
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create a panel with heading and form with all fields', async(() => {
component.workbasket = new Workbasket('id','created','keyModified','domain','type','modified','name','description','owner','custom1','custom2','custom3','custom4','orgLevel1','orgLevel2','orgLevel3','orgLevel4',null);
fixture.detectChanges();
expect(debugElement.querySelector('#wb-information')).toBeDefined();
expect(debugElement.querySelector('#wb-information > .panel-heading > h4').textContent).toBe('name');
expect(debugElement.querySelectorAll('#wb-information > .panel-body > form').length).toBe(2);
fixture.whenStable().then(() => {
expect(debugElement.querySelector('#wb-information > .panel-body > form:first-child > div:first-child > input').value).toBe('keyModified');
});
}));
afterEach(() => {
document.body.removeChild(debugElement);
});
it('selectType should set workbasket.type to personal with 0 and group in other case', () => {
component.workbasket = new Workbasket(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null);
expect(component.workbasket.type).toEqual(null);
component.selectType(ICONTYPES.PERSONAL);
expect(component.workbasket.type).toEqual('PERSONAL');
component.selectType(ICONTYPES.GROUP);
expect(component.workbasket.type).toEqual('GROUP');
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create a panel with heading and form with all fields', async(() => {
component.workbasket = new Workbasket('id', 'created', 'keyModified', 'domain', 'type', 'modified', 'name', 'description', 'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2', 'orgLevel3', 'orgLevel4', null);
fixture.detectChanges();
expect(debugElement.querySelector('#wb-information')).toBeDefined();
expect(debugElement.querySelector('#wb-information > .panel-heading > h4').textContent).toBe('name');
expect(debugElement.querySelectorAll('#wb-information > .panel-body > form').length).toBe(2);
fixture.whenStable().then(() => {
expect(debugElement.querySelector('#wb-information > .panel-body > form:first-child > div:first-child > input').value).toBe('keyModified');
});
}));
it('selectType should set workbasket.type to personal with 0 and group in other case', () => {
component.workbasket = new Workbasket(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
expect(component.workbasket.type).toEqual(null);
component.selectType(ICONTYPES.PERSONAL);
expect(component.workbasket.type).toEqual('PERSONAL');
component.selectType(ICONTYPES.GROUP);
expect(component.workbasket.type).toEqual('GROUP');
});
it('should create a copy of workbasket when workbasket is selected', () => {
expect(component.workbasketClone).toBeUndefined();
component.workbasket = new Workbasket('id', 'created', 'keyModified', 'domain', 'type', 'modified', 'name', 'description', 'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2', 'orgLevel3', 'orgLevel4', null);
component.ngOnInit();
fixture.detectChanges();
expect(component.workbasket.workbasketId).toEqual(component.workbasketClone.workbasketId);
});
it('should reset requestInProgress after saving request is done', fakeAsync(() => {
component.workbasket = new Workbasket('id', 'created', 'keyModified', 'domain', 'type', 'modified', 'name', 'description',
'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', new Array<Links>(new Links('self', 'someUrl')));
spyOn(workbasketService, 'updateWorkbasket').and.returnValue(Observable.of(component.workbasket));
spyOn(workbasketService, 'triggerWorkBasketSaved').and.returnValue(Observable.of(component.workbasket));
component.onSave();
expect(component.modalSpinner).toBeTruthy();
expect(component.modalErrorMessage).toBeUndefined
expect(component.requestInProgress).toBeFalsy();
}));
it('should trigger triggerWorkBasketSaved method after saving request is done', () => {
component.workbasket = new Workbasket('id', 'created', 'keyModified', 'domain', 'type', 'modified', 'name', 'description',
'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', new Array<Links>(new Links('self', 'someUrl')));
spyOn(workbasketService, 'updateWorkbasket').and.returnValue(Observable.of(component.workbasket));
spyOn(workbasketService, 'triggerWorkBasketSaved').and.returnValue(Observable.of(component.workbasket));
component.onSave();
expect(workbasketService.triggerWorkBasketSaved).toHaveBeenCalled();
});
});

View File

@ -1,26 +1,98 @@
import { Component, OnInit, Input, Output } from '@angular/core';
import { Workbasket } from '../../../model/workbasket';
import { WorkbasketService } from '../../../services/workbasketservice.service';
import { WorkbasketService } from '../../../services/workbasket.service';
import { IconTypeComponent, ICONTYPES } from '../../../shared/type-icon/icon-type.component';
import { Subscription } from 'rxjs';
import { Utils } from '../../../shared/utils/utils';
import { AlertService, AlertModel, AlertType } from '../../../services/alert.service';
import { ActivatedRoute, Params, Router, NavigationStart } from '@angular/router';
@Component({
selector: 'workbasket-information',
templateUrl: './workbasket-information.component.html',
styleUrls: ['./workbasket-information.component.scss']
selector: 'workbasket-information',
templateUrl: './workbasket-information.component.html',
styleUrls: ['./workbasket-information.component.scss']
})
export class WorkbasketInformationComponent implements OnInit {
@Input()
workbasket: Workbasket;
allTypes: Map<string, string>;
constructor(private service: WorkbasketService) {
this.allTypes = IconTypeComponent.allTypes;
}
@Input()
workbasket: Workbasket;
workbasketClone: Workbasket;
ngOnInit() {
}
allTypes: Map<string, string>;
requestInProgress: boolean = false;
modalSpinner: boolean = false;
modalErrorMessage: string;
modalTitle: string = 'There was error while saving your workbasket';
private workbasketSubscription: Subscription;
private routeSubscription: Subscription;
constructor(private workbasketService: WorkbasketService,
private alertService: AlertService,
private route: ActivatedRoute,
private router: Router, ) {
this.allTypes = IconTypeComponent.allTypes;
}
ngOnInit() {
this.workbasketClone = { ...this.workbasket };
this.routeSubscription = this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
this.checkForChanges();
}
});
}
selectType(type: ICONTYPES) {
this.workbasket.type = type;
}
onSave() {
this.beforeRequest();
this.workbasketSubscription = (this.workbasketService.updateWorkbasket((Utils.getSelfRef(this.workbasket.links).href), this.workbasket).subscribe(
workbasketUpdated => {
this.afterRequest();
this.workbasket = workbasketUpdated;
this.workbasketClone = {...this.workbasket};
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, `Workbasket ${workbasketUpdated.key} was saved successfully`))
},
error => {
this.afterRequest();
this.modalErrorMessage = error;
}
));
}
onClear() {
this.alertService.triggerAlert(new AlertModel(AlertType.INFO, 'Reset edited fields'))
this.workbasket = { ...this.workbasketClone };
}
private beforeRequest(){
this.requestInProgress = true;
this.modalSpinner = true;
this.modalErrorMessage = undefined;
}
private afterRequest(){
this.requestInProgress = false;
this.workbasketService.triggerWorkBasketSaved();
}
private checkForChanges() {
if (!Workbasket.equals(this.workbasket, this.workbasketClone)) {
this.openDiscardChangesModal();
}
}
private openDiscardChangesModal() {
}
private ngOnDestroy() {
if (this.workbasketSubscription) { this.workbasketSubscription.unsubscribe(); }
if (this.routeSubscription) { this.routeSubscription.unsubscribe(); }
}
selectType(type: ICONTYPES){
this.workbasket.type = type;
}
}

View File

@ -1,13 +1,13 @@
<div class="container-scrollable" >
<taskana-spinner [isRunning]="requestInProgress" class = "centered-horizontally"></taskana-spinner>
<app-no-access *ngIf="!hasPermission || !workbasket && selectedId" ></app-no-access>
<div id ="workbasket-details" class="workbasket-details" *ngIf="workbasket">
<app-no-access *ngIf="!requestInProgress && (!hasPermission || !workbasket && selectedId)" ></app-no-access>
<div id ="workbasket-details" class="workbasket-details" *ngIf="workbasket && !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>
<li role="presentation" class="active">
<a href="#work-baskets" aria-controls="work baskets" role="tab" data-toggle="tab" aria-expanded="true">Work baskets information</a>
<a href="#work-baskets" aria-controls="work baskets" role="tab" data-toggle="tab" aria-expanded="true">Information</a>
</li>
<li role="presentation" class="inactive">
<a href="#authorizations" aria-controls="Authorizations" role="tab" data-toggle="tab" aria-expanded="true">Authorizations</a>
@ -28,4 +28,5 @@
</div>
</div>
</div>
<taskana-alert></taskana-alert>
</div>

View File

@ -8,16 +8,21 @@ import { SpinnerComponent } from '../../shared/spinner/spinner.component';
import { ICONTYPES, IconTypeComponent } from '../../shared/type-icon/icon-type.component';
import { MapValuesPipe } from '../../pipes/map-values.pipe';
import { RemoveNoneTypePipe } from '../../pipes/remove-none-type';
import { AlertComponent } from '../../shared/alert/alert.component';
import { GeneralMessageModalComponent } from '../../shared/general-message-modal/general-message-modal.component';
import { Links } from 'app/model/links';
import { WorkbasketService } from '../../services/workbasketservice.service';
import { WorkbasketService } from '../../services/workbasket.service';
import { MasterAndDetailService } from '../../services/master-and-detail.service';
import { PermissionService } from '../../services/permission.service';
import { AlertService } from '../../services/alert.service';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule } from '@angular/forms';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientModule } from '@angular/common/http';
import { HttpModule } from '@angular/http';
import { WorkbasketSummary } from '../../model/workbasketSummary';
describe('WorkbasketDetailsComponent', () => {
let component: WorkbasketDetailsComponent;
@ -29,8 +34,8 @@ describe('WorkbasketDetailsComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports:[RouterTestingModule, FormsModule, AngularSvgIconModule, HttpClientModule, HttpModule],
declarations: [ WorkbasketDetailsComponent, NoAccessComponent, WorkbasketInformationComponent, SpinnerComponent, IconTypeComponent, MapValuesPipe, RemoveNoneTypePipe ],
providers:[WorkbasketService, MasterAndDetailService, PermissionService]
declarations: [ WorkbasketDetailsComponent, NoAccessComponent, WorkbasketInformationComponent, SpinnerComponent, IconTypeComponent, MapValuesPipe, RemoveNoneTypePipe, AlertComponent, GeneralMessageModalComponent ],
providers:[WorkbasketService, MasterAndDetailService, PermissionService, AlertService]
})
.compileComponents();
}));
@ -44,6 +49,7 @@ describe('WorkbasketDetailsComponent', () => {
workbasketService = TestBed.get(WorkbasketService);
spyOn(masterAndDetailService, 'getShowDetail').and.returnValue(Observable.of(true));
spyOn(workbasketService,'getSelectedWorkBasket').and.returnValue(Observable.of('id1'));
spyOn(workbasketService,'getWorkBasketsSummary').and.returnValue(Observable.of(new Array<WorkbasketSummary>(new WorkbasketSummary('id1','','','','','','','','','','','',new Array<Links>( new Links('self', 'someurl'))))));
spyOn(workbasketService,'getWorkBasket').and.returnValue(Observable.of(new Workbasket('id1',null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null)));
});
@ -74,18 +80,11 @@ describe('WorkbasketDetailsComponent', () => {
it('should show back button with classes "visible-xs visible-sm hidden" when showDetail property is true', () => {
component.workbasket = new Workbasket(null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null);
component.workbasket = new Workbasket('id1',null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null);
component.ngOnInit();
fixture.detectChanges();
expect(debugElement.querySelector('.visible-xs.visible-sm.hidden > a').textContent).toBe('Back');
});
it('should create a copy of workbasket when workbasket is selected', () => {
expect(component.workbasketClone).toBeUndefined();
component.ngOnInit();
fixture.detectChanges();
expect(component.workbasket.id).toEqual(component.workbasketClone.id);
});
});

View File

@ -1,84 +1,95 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Workbasket } from '../../model/workbasket';
import { WorkbasketService } from '../../services/workbasketservice.service'
import { WorkbasketService } from '../../services/workbasket.service'
import { MasterAndDetailService } from '../../services/master-and-detail.service'
import { ActivatedRoute, Params, Router, NavigationStart } from '@angular/router';
import { PermissionService } from '../../services/permission.service';
import { Subscription } from 'rxjs';
import { WorkbasketSummary } from '../../model/workbasketSummary';
import { Utils } from '../../shared/utils/utils';
@Component({
selector: 'workbasket-details',
templateUrl: './workbasket-details.component.html',
styleUrls: ['./workbasket-details.component.scss']
selector: 'workbasket-details',
templateUrl: './workbasket-details.component.html',
styleUrls: ['./workbasket-details.component.scss']
})
export class WorkbasketDetailsComponent implements OnInit {
selectedId: number = -1;
workbasket: Workbasket;
workbasketClone: Workbasket;
showDetail: boolean = false;
hasPermission: boolean = true;
requestInProgress: boolean = false;
private workbasketSelectedSubscription: Subscription;
private workbasketSubscription: Subscription;
private routeSubscription: Subscription;
private masterAndDetailSubscription: Subscription;
private permissionSubscription: Subscription;
selectedId: number = -1;
workbasket: Workbasket;
showDetail: boolean = false;
hasPermission: boolean = true;
requestInProgress: boolean = false;
constructor(private service: WorkbasketService,
private route: ActivatedRoute,
private router: Router,
private masterAndDetailService: MasterAndDetailService,
private permissionService: PermissionService) { }
private workbasketSelectedSubscription: Subscription;
private workbasketSubscription: Subscription;
private routeSubscription: Subscription;
private masterAndDetailSubscription: Subscription;
private permissionSubscription: Subscription;
constructor(private service: WorkbasketService,
private route: ActivatedRoute,
private router: Router,
private masterAndDetailService: MasterAndDetailService,
private permissionService: PermissionService) { }
ngOnInit() {
this.workbasketSelectedSubscription = this.service.getSelectedWorkBasket().subscribe( workbasketIdSelected => {
this.workbasket = undefined;
this.requestInProgress = true;
this.workbasketSubscription = this.service.getWorkBasket(workbasketIdSelected).subscribe( workbasket => {
this.workbasket = workbasket;
this.workbasketClone = { ...this.workbasket };
this.requestInProgress = false;
});
});
this.routeSubscription = this.route.params.subscribe(params => {
let id = params['id'];
if( id && id !== '') {
this.selectedId = id;
this.service.selectWorkBasket(id);
}
});
this.masterAndDetailSubscription = this.masterAndDetailService.getShowDetail().subscribe(showDetail => {
this.showDetail = showDetail;
});
this.permissionSubscription = this.permissionService.hasPermission().subscribe( permission => {
this.hasPermission = permission;
if(!this.hasPermission){
this.requestInProgress = false;
}
})
}
ngOnInit() {
this.workbasketSelectedSubscription = this.service.getSelectedWorkBasket().subscribe(workbasketIdSelected => {
this.workbasket = undefined;
this.requestInProgress = true;
this.getWorkbasketInformation(workbasketIdSelected);
});
onSave() {
}
backClicked(): void {
this.service.selectWorkBasket(undefined);
this.router.navigate(['./'], { relativeTo: this.route.parent });
}
this.routeSubscription = this.route.params.subscribe(params => {
let id = params['id'];
if (id && id !== '') {
this.selectedId = id;
this.service.selectWorkBasket(id);
}
});
ngOnDestroy(){
if(this.workbasketSelectedSubscription){this.workbasketSelectedSubscription.unsubscribe();}
if(this.workbasketSubscription){this.workbasketSubscription.unsubscribe();}
if(this.routeSubscription){this.routeSubscription.unsubscribe();}
if(this.masterAndDetailSubscription){this.masterAndDetailSubscription.unsubscribe();}
if(this.permissionSubscription){this.permissionSubscription.unsubscribe();}
}
this.masterAndDetailSubscription = this.masterAndDetailService.getShowDetail().subscribe(showDetail => {
this.showDetail = showDetail;
});
this.permissionSubscription = this.permissionService.hasPermission().subscribe(permission => {
this.hasPermission = permission;
if (!this.hasPermission) {
this.requestInProgress = false;
}
})
}
backClicked(): void {
this.service.selectWorkBasket(undefined);
this.router.navigate(['./'], { relativeTo: this.route.parent });
}
private getWorkbasketInformation(workbasketIdSelected: string) {
this.service.getWorkBasketsSummary().subscribe((workbasketSummary: Array<WorkbasketSummary>) => {
let workbasketSummarySelected = this.getWorkbasketSummaryById(workbasketSummary, workbasketIdSelected);
if (workbasketSummarySelected && workbasketSummarySelected.links) {
this.workbasketSubscription = this.service.getWorkBasket(Utils.getSelfRef(workbasketSummarySelected.links).href).subscribe(workbasket => {
this.workbasket = workbasket;
this.requestInProgress = false;
});
}
});
}
private getWorkbasketSummaryById(workbasketSummary: Array<WorkbasketSummary>, selectedId: string) {
return workbasketSummary.find((summary => summary.workbasketId === selectedId));
}
ngOnDestroy(): void {
if (this.workbasketSelectedSubscription) { this.workbasketSelectedSubscription.unsubscribe(); }
if (this.workbasketSubscription) { this.workbasketSubscription.unsubscribe(); }
if (this.routeSubscription) { this.routeSubscription.unsubscribe(); }
if (this.masterAndDetailSubscription) { this.masterAndDetailSubscription.unsubscribe(); }
if (this.permissionSubscription) { this.permissionSubscription.unsubscribe(); }
}
}

View File

@ -25,8 +25,8 @@
<span class="glyphicon {{sortDirection === 'asc'? 'glyphicon-sort-by-attributes-alt' : 'glyphicon-sort-by-attributes' }} blue"
data-toggle= "tooltip" title="{{sortDirection === 'asc'? 'A-Z' : 'Z-A' }}"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right sortby-dropdown popup" aria-labelledby="sortingDropdown">
<li>
<div class="dropdown-menu dropdown-menu-right sortby-dropdown popup" aria-labelledby="sortingDropdown">
<div>
<div class="col-xs-6">
<h5>Sort By</h5>
</div>
@ -36,8 +36,8 @@
<button id= "sort-by-direction-desc" type="button" (click)="changeOrder('desc')" data-toggle="tooltip" title="Z-A" class="btn btn-default {{sortDirection === 'desc'? 'selected' : '' }}" >
<span class="glyphicon glyphicon-sort-by-attributes blue" aria-hidden="true"></span>
</button>
</li>
<li role="separator" class="divider"></li>
</div>
<div role="separator" class="divider"></div>
<li id="sort-by-{{sortingField.key}}"(click)="changeSortBy(sortingField.key)" *ngFor="let sortingField of sortingFields | mapValues">
<a>
<label>
@ -46,7 +46,7 @@
</label>
</a>
</li>
</ul>
</div>
</div>
<div class="clearfix btn-group">
<button class="btn btn-default collapsed" type="button" id="collapsedMenufilterWb" data-toggle="collapse" data-target="#wb-filter-bar" aria-expanded="false">
@ -60,7 +60,7 @@
</div>
</li>
<taskana-spinner [isRunning]="requestInProgress" class = "centered-horizontally"></taskana-spinner>
<li class="list-group-item" *ngFor= "let workbasket of workbaskets" [class.active]="workbasket.workbasketId == selectedId" type="text" (click) ="selectWorkbasket(workbasket.workbasketId)" [routerLink]="[ {outlets: { detail: [workbasket.workbasketId] } }]">
<li class="list-group-item" *ngFor= "let workbasket of workbaskets" [class.active]="workbasket.workbasketId == selectedId" type="text" (click) ="selectWorkbasket(workbasket.workbasketId)">
<div class="row">
<dl class="col-xs-1">
<dt>

View File

@ -4,7 +4,7 @@ import { WorkbasketListComponent } from './workbasket-list.component';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientModule } from '@angular/common/http';
import { WorkbasketSummary } from '../../model/workbasketSummary';
import { WorkbasketService, Direction } from '../../services/workbasketservice.service';
import { WorkbasketService, Direction } from '../../services/workbasket.service';
import { HttpModule } from '@angular/http';
import { Router, Routes } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
@ -17,113 +17,113 @@ import { MapValuesPipe } from '../../pipes/map-values.pipe';
@Component({
selector: 'dummy-detail',
template: 'dummydetail'
selector: 'dummy-detail',
template: 'dummydetail'
})
export class DummyDetailComponent {
}
@Component({
selector: 'taskana-filter',
template: ''
selector: 'taskana-filter',
template: ''
})
export class FilterComponent {
}
const workbasketSummary: WorkbasketSummary[] = [ new WorkbasketSummary("1", "key1", "NAME1", "description 1", "owner 1", "", "", "PERSONAL", "", "", "", ""),
new WorkbasketSummary("2", "key2", "NAME2", "description 2", "owner 2", "", "", "GROUP", "", "", "", "")
];
const workbasketSummary: WorkbasketSummary[] = [new WorkbasketSummary("1", "key1", "NAME1", "description 1", "owner 1", "", "", "PERSONAL", "", "", "", ""),
new WorkbasketSummary("2", "key2", "NAME2", "description 2", "owner 2", "", "", "GROUP", "", "", "", "")
];
describe('WorkbasketListComponent', () => {
let component: WorkbasketListComponent;
let fixture: ComponentFixture<WorkbasketListComponent>;
let debugElement: any = undefined;
let workbasketService: WorkbasketService;
let component: WorkbasketListComponent;
let fixture: ComponentFixture<WorkbasketListComponent>;
let debugElement: any = undefined;
let workbasketService: WorkbasketService;
const routes: Routes = [
{ path: ':id', component: DummyDetailComponent, outlet: 'detail' }
];
const routes: Routes = [
{ path: ':id', component: DummyDetailComponent, outlet: 'detail' }
];
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ WorkbasketListComponent, DummyDetailComponent, MapValuesPipe, SpinnerComponent, FilterComponent, RemoveNoneTypePipe, IconTypeComponent],
imports:[
AngularSvgIconModule,
HttpModule,
HttpClientModule,
RouterTestingModule.withRoutes(routes)
],
providers:[WorkbasketService]
})
.compileComponents();
fixture = TestBed.createComponent(WorkbasketListComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.nativeElement;
workbasketService = TestBed.get(WorkbasketService);
spyOn(workbasketService, 'getWorkBasketsSummary').and.returnValue(Observable.of(workbasketSummary));
spyOn(workbasketService, 'getSelectedWorkBasket') .and.returnValue(Observable.of('2'));
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [WorkbasketListComponent, DummyDetailComponent, MapValuesPipe, SpinnerComponent, FilterComponent, RemoveNoneTypePipe, IconTypeComponent],
imports: [
AngularSvgIconModule,
HttpModule,
HttpClientModule,
RouterTestingModule.withRoutes(routes)
],
providers: [WorkbasketService]
})
.compileComponents();
fixture.detectChanges();
}));
afterEach(() =>{
document.body.removeChild(debugElement);
})
fixture = TestBed.createComponent(WorkbasketListComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.nativeElement;
workbasketService = TestBed.get(WorkbasketService);
spyOn(workbasketService, 'getWorkBasketsSummary').and.returnValue(Observable.of(workbasketSummary));
spyOn(workbasketService, 'getSelectedWorkBasket').and.returnValue(Observable.of('2'));
it('should be created', () => {
expect(component).toBeTruthy();
});
fixture.detectChanges();
}));
it('should call workbasketService.getWorkbasketsSummary method on init', () => {
component.ngOnInit();
expect(workbasketService.getWorkBasketsSummary).toHaveBeenCalled();
workbasketService.getWorkBasketsSummary().subscribe(value => {
expect(value).toBe(workbasketSummary);
})
});
afterEach(() => {
document.body.removeChild(debugElement);
})
it('should have wb-action-toolbar, wb-search-bar, wb-list-container, wb-pagination, collapsedMenufilterWb and taskana-filter created in the html',() => {
expect(debugElement.querySelector('#wb-action-toolbar')).toBeDefined();
expect(debugElement.querySelector('#wb-search-bar')).toBeDefined();
expect(debugElement.querySelector('#wb-pagination')).toBeDefined();
expect(debugElement.querySelector('#wb-list-container')).toBeDefined();
expect(debugElement.querySelector('#collapsedMenufilterWb')).toBeDefined();
expect(debugElement.querySelector('taskana-filter')).toBeDefined();
expect(debugElement.querySelectorAll('#wb-list-container > li').length).toBe(3);
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should have two workbasketsummary rows created with the second one selected.',() => {
expect(debugElement.querySelectorAll('#wb-list-container > li').length).toBe(3);
expect(debugElement.querySelectorAll('#wb-list-container > li')[1].getAttribute('class')).toBe('list-group-item');
expect(debugElement.querySelectorAll('#wb-list-container > li')[2].getAttribute('class')).toBe('list-group-item active');
});
it('should call workbasketService.getWorkbasketsSummary method on init', () => {
component.ngOnInit();
expect(workbasketService.getWorkBasketsSummary).toHaveBeenCalled();
workbasketService.getWorkBasketsSummary().subscribe(value => {
expect(value).toBe(workbasketSummary);
})
});
it('should have two workbasketsummary rows created with two different icons: user and users',() => {
expect(debugElement.querySelectorAll('#wb-list-container > li')[1].querySelector('svg-icon').getAttribute('ng-reflect-src')).toBe('./assets/icons/user.svg');
expect(debugElement.querySelectorAll('#wb-list-container > li')[2].querySelector('svg-icon').getAttribute('ng-reflect-src')).toBe('./assets/icons/users.svg');
});
it('should have wb-action-toolbar, wb-search-bar, wb-list-container, wb-pagination, collapsedMenufilterWb and taskana-filter created in the html', () => {
expect(debugElement.querySelector('#wb-action-toolbar')).toBeDefined();
expect(debugElement.querySelector('#wb-search-bar')).toBeDefined();
expect(debugElement.querySelector('#wb-pagination')).toBeDefined();
expect(debugElement.querySelector('#wb-list-container')).toBeDefined();
expect(debugElement.querySelector('#collapsedMenufilterWb')).toBeDefined();
expect(debugElement.querySelector('taskana-filter')).toBeDefined();
expect(debugElement.querySelectorAll('#wb-list-container > li').length).toBe(3);
});
it('should have rendered sort by: name, id, description, owner and type', () => {
expect(debugElement.querySelector('#sort-by-name')).toBeDefined();
expect(debugElement.querySelector('#sort-by-key')).toBeDefined();
expect(debugElement.querySelector('#sort-by-description')).toBeDefined();
expect(debugElement.querySelector('#sort-by-owner')).toBeDefined();
expect(debugElement.querySelector('#sort-by-type')).toBeDefined();
});
it('should have two workbasketsummary rows created with the second one selected.', () => {
expect(debugElement.querySelectorAll('#wb-list-container > li').length).toBe(3);
expect(debugElement.querySelectorAll('#wb-list-container > li')[1].getAttribute('class')).toBe('list-group-item');
expect(debugElement.querySelectorAll('#wb-list-container > li')[2].getAttribute('class')).toBe('list-group-item active');
});
it('should have performRequest after performFilter is triggered', fakeAsync( () => {
let type='PERSONAL', name = 'someName', description = 'someDescription', owner = 'someOwner', key = 'someKey';
let filter = new FilterModel(type, name, description, owner, key );
component.performFilter(filter);
expect(workbasketService.getWorkBasketsSummary).toHaveBeenCalledWith('key', 'asc', undefined, name, description, undefined, owner, type, undefined, key );
it('should have two workbasketsummary rows created with two different icons: user and users', () => {
expect(debugElement.querySelectorAll('#wb-list-container > li')[1].querySelector('svg-icon').getAttribute('ng-reflect-src')).toBe('./assets/icons/user.svg');
expect(debugElement.querySelectorAll('#wb-list-container > li')[2].querySelector('svg-icon').getAttribute('ng-reflect-src')).toBe('./assets/icons/users.svg');
});
}));
it('should have rendered sort by: name, id, description, owner and type', () => {
expect(debugElement.querySelector('#sort-by-name')).toBeDefined();
expect(debugElement.querySelector('#sort-by-key')).toBeDefined();
expect(debugElement.querySelector('#sort-by-description')).toBeDefined();
expect(debugElement.querySelector('#sort-by-owner')).toBeDefined();
expect(debugElement.querySelector('#sort-by-type')).toBeDefined();
});
it('should have performRequest with forced = true after performFilter is triggered', (() => {
let type = 'PERSONAL', name = 'someName', description = 'someDescription', owner = 'someOwner', key = 'someKey';
let filter = new FilterModel(type, name, description, owner, key);
component.performFilter(filter);
expect(workbasketService.getWorkBasketsSummary).toHaveBeenCalledWith(true, 'key', 'asc', undefined, name, description, undefined, owner, type, undefined, key);
}));
});

View File

@ -1,9 +1,10 @@
import { Component, OnInit, EventEmitter } from '@angular/core';
import { WorkbasketSummary } from '../../model/workbasketSummary';
import { WorkbasketService, Direction } from '../../services/workbasketservice.service'
import { WorkbasketService, Direction } from '../../services/workbasket.service'
import { Subscription } from 'rxjs/Subscription';
import { FilterModel } from '../../shared/filter/filter.component'
import { filter } from 'rxjs/operator/filter';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'workbasket-list',
@ -26,8 +27,9 @@ export class WorkbasketListComponent implements OnInit {
private workBasketSummarySubscription: Subscription;
private workbasketServiceSubscription: Subscription;
private workbasketServiceSavedSubscription: Subscription;
constructor(private workbasketService: WorkbasketService) { }
constructor(private workbasketService: WorkbasketService, private router: Router, private route: ActivatedRoute) { }
ngOnInit() {
this.requestInProgress = true;
@ -39,10 +41,20 @@ export class WorkbasketListComponent implements OnInit {
this.workbasketServiceSubscription = this.workbasketService.getSelectedWorkBasket().subscribe(workbasketIdSelected => {
this.selectedId = workbasketIdSelected;
});
this.workbasketServiceSavedSubscription = this.workbasketService.workbasketSavedTriggered().subscribe(value => {
this.performRequest();
});
}
selectWorkbasket(id: string) {
this.selectedId = id;
if(!this.selectedId) {
this.router.navigate(['/workbaskets']);
return
}
this.router.navigate([{outlets: { detail: [this.selectedId] } }], { relativeTo: this.route });
}
changeOrder(sortDirection: string) {
@ -61,19 +73,11 @@ export class WorkbasketListComponent implements OnInit {
}
onDelete(workbasket: WorkbasketSummary) {
this.workbasketService.deleteWorkbasket(workbasket.workbasketId).subscribe(result => {
var index = this.workbaskets.indexOf(workbasket);
if (index > -1) {
this.workbaskets.splice(index, 1);
}
});
}
onAdd() {
this.workbasketService.createWorkbasket(this.newWorkbasket).subscribe(result => {
this.workbaskets.push(result);
this.onClear();
});
}
onClear() {
@ -81,6 +85,7 @@ export class WorkbasketListComponent implements OnInit {
this.newWorkbasket.name = "";
this.newWorkbasket.description = "";
this.newWorkbasket.owner = "";
this.newWorkbasket.key = "";
}
getEmptyObject() {
@ -90,17 +95,27 @@ export class WorkbasketListComponent implements OnInit {
private performRequest(): void {
this.requestInProgress = true;
this.workbaskets = undefined;
this.workbasketServiceSubscription.add(this.workbasketService.getWorkBasketsSummary(this.sortBy, this.sortDirection, undefined,
this.workbasketServiceSubscription.add(this.workbasketService.getWorkBasketsSummary(true, this.sortBy, this.sortDirection, undefined,
this.filterBy.name, this.filterBy.description, undefined, this.filterBy.owner,
this.filterBy.type, undefined, this.filterBy.key).subscribe(resultList => {
this.workbaskets = resultList;
this.requestInProgress = false;
this.unSelectWorkbasket();
}));
}
private unSelectWorkbasket() : void{
if (!this.workbaskets.find( wb => wb.workbasketId === this.selectedId)){
this.selectWorkbasket(undefined);
}
}
private ngOnDestroy() {
this.workBasketSummarySubscription.unsubscribe();
this.workbasketServiceSubscription.unsubscribe();
this.workbasketServiceSavedSubscription.unsubscribe();
}
}

View File

@ -109,8 +109,8 @@
}
.footer-space {
max-height: calc(100vh - 160px);
height: calc(100vh - 160px);
max-height: calc(100vh - 140px);
height: calc(100vh - 140px);
overflow-y: auto;
overflow-x: hidden;
}
@ -243,11 +243,6 @@ li > div.row > dl {
-o-transition: opacity 300ms ease, visibility 300ms ease;
transition: opacity 300ms ease, visibility 300ms ease;
}
taskana-spinner.centered-horizontally > div {
margin-top: calc(50vh - 250px);
}
.vertical-align{
vertical-align: middle;
}
}

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M.157 15.852l-.128-.137v-4.892l.16-.3c.87-1.629 2.53-4.594 2.593-4.632.043-.026.189-.057.323-.068l.244-.021.018-2.632c.015-2.01.03-2.653.066-2.72.121-.227-.168-.214 4.614-.214 3.736 0 4.416.009 4.504.057.216.12.211.054.222 2.907l.01 2.622h.223c.25 0 .388.053.464.177.072.119 2.365 4.482 2.46 4.681.078.163.079.197.069 2.62l-.01 2.454-.123.117-.122.118H.284zm10.6-4.354c0-.601.036-.772.19-.912l.115-.103h1.854c1.166 0 1.854-.014 1.854-.038 0-.063-1.91-3.677-1.952-3.692-.023-.009-.036.378-.037 1.065 0 .593-.01 1.147-.024 1.231-.028.185-.188.369-.35.404-.139.03-.325-.046-.432-.174-.073-.088-.075-.161-.093-4.094l-.02-4.005-3.796-.01-3.797-.01-.01 3.996c-.01 3.985-.01 3.996-.089 4.109-.166.24-.497.253-.694.028l-.09-.102-.01-1.225c-.009-.87-.024-1.221-.053-1.21-.035.012-1.99 3.527-2.045 3.677-.015.04.368.05 1.839.05h1.857l.115.103c.155.14.19.31.191.912v.512h5.476v-.512zm-5.92-3.474c-.024-.027-.044-.104-.044-.173 0-.068.02-.146.045-.173.065-.07 6.296-.07 6.36 0 .059.063.059.284 0 .346-.064.07-6.295.07-6.36 0zm.04-1.461c-.11-.082-.117-.318-.012-.378.1-.058 6.206-.058 6.306 0 .106.06.1.296-.01.378-.127.095-6.158.095-6.285 0zm-.04-1.633c-.024-.027-.044-.105-.044-.173 0-.069.02-.146.045-.173.065-.07 6.296-.07 6.36 0 .059.062.059.283 0 .346-.064.07-6.295.07-6.36 0zm1.63-1.553c-.088-.104-.078-.27.022-.346.073-.055.36-.062 2.357-.062 2.155 0 2.277.004 2.336.073.082.097.079.26-.007.342-.062.06-.28.067-2.357.067-2.17 0-2.292-.004-2.35-.074zm-1.614-.742c-.1-.079-.068-.313.052-.378.125-.067 1.357-.072 1.475-.006.155.088.187.256.07.37-.06.057-.173.066-.8.066-.538 0-.748-.013-.797-.052z"/><path fill="none" d="M-.063.036h13.688v5.951H-.063zM14.125 4.633h2.25v.902h-2.25z"/><path fill="#fff" fill-rule="evenodd" d="M2.038.073h13.369v5.743H2.038z"/><path fill="#fff" fill-rule="evenodd" d="M11.071 4.173h.044v.219h-.044zM4.359 6.621H11.5v2.905H4.359z"/><path d="M5.16 7.084h5.733v.613H5.16zM5.164 8.042h5.733v.613H5.164zM5.159 8.954h5.733v.613H5.159zM3.605 2.452l1.277-1.125c.284-.25.284-.625 0-.875a.752.752 0 0 0-.994 0L2.611 1.577 1.334.452a.752.752 0 0 0-.993 0c-.284.25-.284.625 0 .875l1.277 1.125L.341 3.576c-.284.25-.284.625 0 .875a.75.75 0 0 0 .993 0l1.277-1.125 1.277 1.125a.75.75 0 0 0 .994 0c.284-.25.284-.625 0-.875zM4.05 5.812h7.914v.763H4.05z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><clipPath id="b"/><clipPath id="c"/><clipPath id="d"><use xlink:href="#a" width="100%" height="100%"/></clipPath><clipPath id="e"/></defs><path d="M16.02 14.074v-1.51l-.072.068-.073.068-5.501-.008-5.502-.008 1.887-1.837 1.886-1.836 1.084-.001 1.083-.002.022-.488c.027-.624.035-.662.164-.81l.107-.125 1.844-.01c1.015-.005 1.856-.02 1.87-.034.014-.014-.03-.126-.096-.249-.293-.537-.807-1.451-1.152-2.05l-.374-.65.33-.326c.181-.18.335-.32.342-.313.006.008.39.684.852 1.503l1.07 1.893.228.403.009-3.738c.005-2.056.012-.294.016 3.916s0 7.654-.008 7.654c-.009 0-.016-.68-.016-1.51zM.976 15.414c-.334-.117-.523-.55-.4-.918.03-.087 1.574-1.608 7.023-6.917 3.842-3.744 7.03-6.831 7.085-6.86.186-.103.5-.051.673.11.181.166.26.479.187.742-.038.138-.378.476-6.21 6.153-3.393 3.303-6.55 6.38-7.015 6.837-.698.685-.868.836-.971.865a.494.494 0 0 1-.372-.011zm-.812-2.832l-.116-.118V7.875l.63-1.07c.743-1.26 1.803-3.011 1.974-3.262.066-.097.152-.199.19-.225.063-.043.52-.048 4.314-.047h4.245l-.36.34-.36.338-3.195.01-3.194.009-.019 1.163c-.02 1.345-.022 1.353-.259 1.442-.18.068-.332.043-.467-.076-.097-.086-.102-.101-.121-.394a22.726 22.726 0 0 1-.027-.941c-.004-.35-.016-.725-.027-.834-.019-.186-.023-.194-.07-.14-.093.106-1.747 2.864-1.92 3.2l-.078.154.191.02c.105.01.94.02 1.856.02h1.664l.099.1a.71.71 0 0 1 .133.193c.019.05.043.325.053.61l.02.52.078.01c.068.007-.177.257-1.805 1.847L1.71 12.699H.28zm5.027-6.14L5.2 6.17l1.612-.008 1.613-.008-.28.28-.282.279H5.182zm7.041.134c-.171-.061-.213-.148-.235-.5l-.02-.31.443-.434.443-.434-.012.684c-.013.762-.029.825-.243.968-.116.077-.214.084-.376.026zm-7.041-.972l.009-.272 2.03-.008c1.117-.004 2.026.003 2.02.016a4.483 4.483 0 0 1-.278.28l-.269.255H5.182zm0-.9l.009-.271 2.505-.008 2.505-.008-.28.28-.282.279H5.182z"/></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -50,20 +50,15 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>0.16.0.RELEASE</version>
<version>0.24.0.RELEASE</version>
</dependency>
</dependencies>