TSK-204-205-206-208 Add classification actions feature: add, delete, reset and unselect classification from tree

This commit is contained in:
Martin Rojas Miguel Angel 2018-04-12 13:43:27 +02:00 committed by Holger Hagen
parent 1dd6dcddf1
commit 3408371d6e
44 changed files with 353 additions and 129 deletions

View File

@ -1,4 +1,5 @@
<div class="container-scrollable">
<taskana-spinner [isRunning]="requestInProgress" class="floating"></taskana-spinner>
<div id="classification-details" *ngIf="classification && !requestInProgress">
<ul class="nav nav-tabs" role="tablist">
<li *ngIf="showDetail" class="visible-xs visible-sm hidden">
@ -7,7 +8,6 @@
</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">
@ -28,7 +28,7 @@
<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">
<div *ngIf="!key.valid" class="required-text">
* Key is required
</div>
</div>
@ -36,7 +36,7 @@
<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">
<div *ngIf="!name.valid" class="required-text">
* Name is required
</div>
</div>
@ -44,28 +44,29 @@
<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">
<div *ngIf="!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 class="form-group required">
<label for="classification-category" class="control-label">Category</label>
<input type="text" required #category="ngModel" class="form-control" id="classification-category" placeholder="category"
[(ngModel)]="classification.category" name="classification.category">
<div *ngIf="!category.valid" class="required-text">
* Category is required
</div>
</div>
<div class="form-group">
<fieldset disabled>
<label for="classification-type" class="control-label">Type</label>
<div class="dropdown clearfix btn-group disabled">
<button class="btn btn-default" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{classification.type}}
<span class="caret"></span>
</button>
</div>
</fieldset>
</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"

View File

@ -13,6 +13,8 @@ import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-
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';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { AlertService } from '../../../services/alert/alert.service';
@Component({
@ -39,7 +41,7 @@ describe('ClassificationDetailsComponent', () => {
TestBed.configureTestingModule({
imports: [FormsModule, HttpClientModule, RouterTestingModule.withRoutes(routes)],
declarations: [ClassificationDetailsComponent, SpinnerComponent, DummyDetailComponent],
providers: [MasterAndDetailService, RequestInProgressService, ClassificationsService, HttpClient]
providers: [MasterAndDetailService, RequestInProgressService, ClassificationsService, HttpClient, ErrorModalService, AlertService]
})
.compileComponents();
}));

View File

@ -4,9 +4,17 @@ import { Subscription } from 'rxjs/Subscription';
import { ClassificationDefinition } from 'app/models/classification-definition';
import { ACTION } from 'app/models/action';
import { Classification } from 'app/models/classification';
import { ErrorModel } from 'app/models/modal-error';
import { AlertModel, AlertType } from 'app/models/alert';
import { TaskanaDate } from 'app/shared/util/taskana.date';
import { ClassificationsService } from 'app/services/classifications/classifications.service';
import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-detail.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
import { AlertService } from 'app/services/alert/alert.service';
@Component({
selector: 'taskana-classification-details',
@ -16,22 +24,29 @@ import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-
export class ClassificationDetailsComponent implements OnInit, OnDestroy {
classification: ClassificationDefinition;
classificationClone: ClassificationDefinition;
selectedId: string = undefined;
showDetail = false;
requestInProgress = false;
classificationTypes: Array<string> = [];
badgeMessage = '';
requestInProgress = false;
private action: any;
private classificationServiceSubscription: Subscription;
private classificationSelectedSubscription: Subscription;
private routeSubscription: Subscription;
private masterAndDetailSubscription: Subscription;
private classificationSavingSubscription: Subscription;
private classificationRemoveSubscription: Subscription;
private selectedClassificationSubscription: Subscription;
constructor(private classificationsService: ClassificationsService,
private route: ActivatedRoute,
private router: Router,
private masterAndDetailService: MasterAndDetailService) { }
private masterAndDetailService: MasterAndDetailService,
private errorModalService: ErrorModalService,
private requestInProgressService: RequestInProgressService,
private alertService: AlertService) { }
ngOnInit() {
@ -52,8 +67,11 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
this.action = undefined;
if (id && id.indexOf('new-classification') !== -1) {
this.action = ACTION.CREATE;
id = undefined;
this.badgeMessage = 'Creating new workbasket';
this.badgeMessage = 'Creating new classification';
id = id.replace('new-classification/', '');
if (id === 'undefined') {
id = undefined;
}
this.getClassificationInformation(id);
}
@ -76,35 +94,100 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
selectType(type: string) {
this.classification.type = type;
}
removeClassification() { }
onSave() { }
removeClassification() {
this.requestInProgressService.setRequestInProgress(true);
this.classificationRemoveSubscription = this.classificationsService
.deleteClassification(this.classification._links.self.href)
.subscribe(() => {
const key = this.classification.key;
this.classification = undefined;
this.afterRequest();
this.classificationsService.selectClassification(undefined);
this.router.navigate(['administration/classifications']);
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, `Classification ${key} was removed successfully`))
}, error => {
this.errorModalService.triggerError(new ErrorModel('There was error while removing your classification', error))
this.afterRequest();
})
}
onClear() { }
onSave() {
this.requestInProgressService.setRequestInProgress(true);
if (this.action === ACTION.CREATE) {
this.classificationSavingSubscription = this.classificationsService.postClassification(this.classification)
.subscribe((classification: ClassificationDefinition) => {
this.classification = classification;
this.afterRequest();
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, `Classification ${classification.key} was saved successfully`));
},
error => {
this.errorModalService.triggerError(new ErrorModel('There was an error creating a classification', error))
this.afterRequest();
});
} else {
this.classificationSavingSubscription = this.classificationsService
.putClassification(this.classification._links.self.href, this.classification)
.subscribe((classification: ClassificationDefinition) => {
this.classification = classification;
this.afterRequest();
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, `Classification ${classification.key} was saved successfully`));
}, error => {
this.errorModalService.triggerError(new ErrorModel('There was error while saving your classification', error))
this.afterRequest();
})
}
}
onClear() {
this.alertService.triggerAlert(new AlertModel(AlertType.INFO, 'Reset edited fields'))
this.classification = { ...this.classificationClone };
}
private afterRequest() {
this.requestInProgressService.setRequestInProgress(false);
this.classificationsService.triggerClassificationSaved();
}
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();
this.selectedClassificationSubscription = this.classificationsService.getSelectedClassificationType().subscribe(value => {
if (this.classification) { this.classification.type = value; }
});
this.addDateToClassification();
this.classification.parentId = classificationIdSelected;
this.classificationClone = { ...this.classification };
} else {
this.requestInProgress = true;
this.classificationServiceSubscription = this.classificationsService.getClassification(classificationIdSelected)
.subscribe((classification: ClassificationDefinition) => {
this.classification = classification;
this.classificationClone = { ...this.classification };
this.requestInProgress = false;
});
}
}
private addDateToClassification() {
const date = TaskanaDate.getDate();
this.classification.created = date;
this.classification.modified = date;
}
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(); }
if (this.classificationSavingSubscription) { this.classificationSavingSubscription.unsubscribe(); }
if (this.classificationRemoveSubscription) { this.classificationRemoveSubscription.unsubscribe(); }
if (this.selectedClassificationSubscription) { this.selectedClassificationSubscription.unsubscribe(); }
}
}

View File

@ -11,7 +11,6 @@
</div>
</div>
</li>
<taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner>
<taskana-tree [treeNodes]="classifications" [selectNodeId] ="selectedId" (selectNodeIdChanged) ="selectClassification($event)"></taskana-tree>
<taskana-spinner [isRunning]="requestInProgress" ></taskana-spinner>
<taskana-tree *ngIf="classifications" [treeNodes]="classifications" [selectNodeId] ="selectedId" (selectNodeIdChanged) ="selectClassification($event)"></taskana-tree>
</div>

View File

@ -9,7 +9,7 @@
.tab-align{
margin-bottom: 0px;
border-bottom: 1px solid #ddd;
&>div{
margin: 6px 0px;
}

View File

@ -18,7 +18,7 @@ import { AlertService } from 'app/services/alert/alert.service';
import { ClassificationsService } from 'app/services/classifications/classifications.service';
import { ClassificationDefinitionService } from 'app/services/classification-definition/classification-definition.service';
import { DomainService } from 'app/services/domains/domain.service';
import {ErrorModalService} from '../../../../services/errorModal/error-modal.service';
import {ErrorModalService} from 'app/services/errorModal/error-modal.service';
@Component({
selector: 'taskana-tree',

View File

@ -19,6 +19,7 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
selectedId: string;
selectionToImport = ImportType.CLASSIFICATIONS;
requestInProgress = false;
initialized = false;
classifications: Array<Classification> = [];
classificationsTypes: Array<string> = [];
@ -26,6 +27,8 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
classificationServiceSubscription: Subscription;
classificationTypeServiceSubscription: Subscription;
classificationSelectedSubscription: Subscription;
classificationSavedSubscription: Subscription;
selectedClassificationSubscription: Subscription;
constructor(
private classificationService: ClassificationsService,
@ -34,43 +37,74 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.classificationServiceSubscription = this.classificationService.getClassifications()
.subscribe((classifications: Array<TreeNodeModel>) => {
this.classifications = classifications;
this.classificationTypeServiceSubscription = this.classificationService.getClassificationTypes()
.subscribe((classificationsTypes: Array<string>) => {
this.classificationsTypes = classificationsTypes;
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);
});
this.classificationSavedSubscription = this.classificationService
.classificationSavedTriggered()
.subscribe(value => {
this.performRequest(true);
})
this.selectedClassificationSubscription = this.classificationService.getSelectedClassificationType().subscribe(value => {
this.classificationTypeSelected = value;
this.performRequest();
})
}
selectClassificationType(classificationTypeSelected: string) {
this.classifications = [];
this.requestInProgress = true;
this.classificationService.selectClassificationType(classificationTypeSelected);
this.classificationService.getClassifications(true, classificationTypeSelected)
.subscribe((classifications: Array<TreeNodeModel>) => {
this.classifications = classifications;
this.requestInProgress = false;
});
}
selectClassification(id: string) {
this.selectedId = id;
if (!id) {
this.router.navigate(['administration/classifications']);
return;
}
this.router.navigate([{ outlets: { detail: [this.selectedId] } }], { relativeTo: this.route });
}
addClassification() {
this.selectedId = undefined;
this.router.navigate([{ outlets: { detail: ['new-classification'] } }], { relativeTo: this.route });
this.router.navigate([{ outlets: { detail: [`new-classification/${this.selectedId}`] } }], { relativeTo: this.route });
}
private performRequest(forceRequest = false) {
if (this.initialized && !forceRequest) {
return;
}
this.requestInProgress = true;
this.classifications = [];
this.classificationServiceSubscription = this.classificationService.getClassifications(true)
.subscribe((classifications: Array<TreeNodeModel>) => {
this.requestInProgress = false;
if (!classifications.length) {
return null;
}
this.classifications = classifications;
this.classificationTypeServiceSubscription = this.classificationService.getClassificationTypes()
.subscribe((classificationsTypes: Array<string>) => {
this.classificationsTypes = classificationsTypes;
});
});
this.classificationSelectedSubscription = this.classificationService.getSelectedClassification()
.subscribe((classificationSelected: string) => {
setTimeout(() => { this.selectedId = classificationSelected; }, 0);
});
this.initialized = true;
}
ngOnDestroy(): void {
if (this.classificationServiceSubscription) { this.classificationServiceSubscription.unsubscribe(); }
if (this.classificationTypeServiceSubscription) { this.classificationTypeServiceSubscription.unsubscribe(); }
if (this.classificationSelectedSubscription) { this.classificationSelectedSubscription.unsubscribe(); }
if (this.classificationSavedSubscription) { this.classificationSavedSubscription.unsubscribe(); }
}
}

View File

@ -1,4 +1,3 @@
<taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
@ -112,5 +111,6 @@
</span>
Add new access
</button>
</div>
<taskana-spinner [isRunning]="requestInProgress" [positionClass]=""></taskana-spinner>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
@ -20,9 +21,7 @@ import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-workbaskets/saving-workbaskets.service';
import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { AlertService } from 'app/services/alert/alert.service';
import { SimpleChange } from '@angular/core';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
describe('AccessItemsComponent', () => {
let component: AccessItemsComponent;
@ -33,7 +32,7 @@ describe('AccessItemsComponent', () => {
TestBed.configureTestingModule({
declarations: [SpinnerComponent, AccessItemsComponent, GeneralMessageModalComponent],
imports: [FormsModule, AngularSvgIconModule, HttpClientModule, HttpModule, ReactiveFormsModule],
providers: [WorkbasketService, AlertService, ErrorModalService, SavingWorkbasketService]
providers: [WorkbasketService, AlertService, ErrorModalService, SavingWorkbasketService, RequestInProgressService]
})
.compileComponents();

View File

@ -12,6 +12,7 @@ import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { AlertService } from 'app/services/alert/alert.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
declare var $: any;
@ -36,7 +37,6 @@ export class AccessItemsComponent implements OnChanges, OnDestroy {
accessItemsClone: Array<WorkbasketAccessItems>;
accessItemsResetClone: Array<WorkbasketAccessItems>;
requestInProgress = false;
modalSpinner = true;
modalTitle: string;
modalErrorMessage: string;
accessItemsubscription: Subscription;
@ -48,7 +48,8 @@ export class AccessItemsComponent implements OnChanges, OnDestroy {
private workbasketService: WorkbasketService,
private alertService: AlertService,
private errorModalService: ErrorModalService,
private savingWorkbaskets: SavingWorkbasketService) { }
private savingWorkbaskets: SavingWorkbasketService,
private requestInProgressService: RequestInProgressService) { }
ngOnChanges(changes: SimpleChanges): void {
if (!this.initialized && changes.active && changes.active.currentValue === 'accessItems') {
@ -63,12 +64,14 @@ export class AccessItemsComponent implements OnChanges, OnDestroy {
if (!this.workbasket._links.accessItems) {
return;
}
this.requestInProgress = true;
this.accessItemsubscription = this.workbasketService.getWorkBasketAccessItems(this.workbasket._links.accessItems.href)
.subscribe((accessItemsResource: WorkbasketAccessItemsResource) => {
this.accessItemsResource = accessItemsResource;
this.accessItems = accessItemsResource._embedded ? accessItemsResource._embedded.accessItems : [];
this.accessItemsClone = this.cloneAccessItems(this.accessItems);
this.accessItemsResetClone = this.cloneAccessItems(this.accessItems);
this.requestInProgress = false;
})
this.savingAccessItemsSubscription = this.savingWorkbaskets.triggeredAccessItemsSaving()
.subscribe((savingInformation: SavingInformation) => {
@ -98,18 +101,18 @@ export class AccessItemsComponent implements OnChanges, OnDestroy {
}
onSave(): boolean {
this.requestInProgress = true;
this.requestInProgressService.setRequestInProgress(true);
this.workbasketService.updateWorkBasketAccessItem(this.accessItemsResource._links.self.href, this.accessItems)
.subscribe(response => {
this.accessItemsClone = this.cloneAccessItems(this.accessItems);
this.accessItemsResetClone = this.cloneAccessItems(this.accessItems);
this.alertService.triggerAlert(new AlertModel(
AlertType.SUCCESS, `Workbasket ${this.workbasket.name} Access items were saved successfully`));
this.requestInProgress = false;
this.requestInProgressService.setRequestInProgress(false);
return true;
}, error => {
this.errorModalService.triggerError(new ErrorModel(`There was error while saving your workbasket's access items`, error))
this.requestInProgress = false;
this.requestInProgressService.setRequestInProgress(false);
return false;
})
return false;

View File

@ -1,5 +1,5 @@
<taskana-spinner [isRunning]="requestInProgress" isModal="true" (requestTimeoutExceeded)="requestTimeoutExceeded($event)"
class="centered-horizontally floating"></taskana-spinner>
<taskana-spinner [isRunning]="requestInProgress" isModal="true"
class="floating"></taskana-spinner>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">

View File

@ -17,6 +17,7 @@ import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { AlertService } from 'app/services/alert/alert.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-workbaskets/saving-workbaskets.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
import { DualListComponent } from './dual-list/dual-list.component';
import { DistributionTargetsComponent, Side } from './distribution-targets.component';
@ -44,7 +45,6 @@ export class FilterComponent {
target: string;
}
describe('DistributionTargetsComponent', () => {
let component: DistributionTargetsComponent;
let fixture: ComponentFixture<DistributionTargetsComponent>;
@ -57,7 +57,7 @@ describe('DistributionTargetsComponent', () => {
imports: [AngularSvgIconModule, HttpClientModule, HttpModule, JsonpModule],
declarations: [DistributionTargetsComponent, SpinnerComponent, GeneralMessageModalComponent,
FilterComponent, SelectWorkBasketPipe, IconTypeComponent, DualListComponent],
providers: [WorkbasketService, AlertService, SavingWorkbasketService, ErrorModalService]
providers: [WorkbasketService, AlertService, SavingWorkbasketService, ErrorModalService, RequestInProgressService]
})
.compileComponents();
}));

View File

@ -14,6 +14,7 @@ import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { AlertService } from 'app/services/alert/alert.service';
import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-workbaskets/saving-workbaskets.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
export enum Side {
LEFT,
@ -57,7 +58,8 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
private workbasketService: WorkbasketService,
private alertService: AlertService,
private savingWorkbaskets: SavingWorkbasketService,
private errorModalService: ErrorModalService) { }
private errorModalService: ErrorModalService,
private requestInProgressService: RequestInProgressService) { }
ngOnChanges(changes: SimpleChanges): void {
if (!this.initialized && changes.active && changes.active.currentValue === 'distributionTargets') {
@ -116,6 +118,7 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
}
onSave() {
this.requestInProgressService.setRequestInProgress(true);
this.requestInProgress = true;
this.workbasketService.updateWorkBasketsDistributionTargets(
this.distributionTargetsSelectedResource._links.self.href, this.getSeletedIds()).subscribe(response => {
@ -144,10 +147,6 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
this.distributionTargetsSelected = Object.assign([], this.distributionTargetsSelectedClone);
}
requestTimeoutExceeded(message: string) {
this.modalErrorMessage = message;
}
performFilter(dualListFilter: any) {
dualListFilter.side === Side.RIGHT ? this.distributionTargetsRight = undefined : this.distributionTargetsLeft = undefined;

View File

@ -17,7 +17,7 @@
<div [@toggle]="toolbarState" *ngIf="toolbarState" class="row">
<taskana-filter class="col-xs-12" (performFilter)="performAvailableFilter($event)"></taskana-filter>
</div>
<taskana-spinner [isRunning]="requestInProgress" positionClass="centered-spinner" class="centered-horizontally floating"></taskana-spinner>
<taskana-spinner [isRunning]="requestInProgress" positionClass="centered-spinner" class="floating"></taskana-spinner>
<div>
<ul class="list-group">
<li class="list-group-item" *ngFor="let distributionTarget of distributionTargets | selectWorkbaskets: distributionTargetsSelected: side"

View File

@ -1,4 +1,4 @@
<taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner>
<taskana-spinner [isRunning]="requestInProgress" class="floating"></taskana-spinner>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
@ -22,7 +22,7 @@
<label for="wb-key" class="control-label">Key</label>
<input type="text" required #key="ngModel" class="form-control" id="wb-key" placeholder="Key" [(ngModel)]="workbasket.key"
name="workbasket.key">
<div [hidden]="key.valid" class="required-text">
<div *ngIf="!key.valid" class="required-text">
* Key is required
</div>
</div>
@ -30,7 +30,7 @@
<label for="wb-name" class="control-label">Name</label>
<input type="text" required #name="ngModel" class="form-control" id="wb-name" placeholder="Name" [(ngModel)]="workbasket.name"
name="workbasket.name">
<div [hidden]="name.valid" class="required-text">
<div *ngIf="!name.valid" class="required-text">
* Name is required
</div>
</div>
@ -38,7 +38,7 @@
<label for="wb-owner" class="control-label">Owner</label>
<input type="text" required #owner="ngModel" class="form-control" id="wb-owner" placeholder="Owner" [(ngModel)]="workbasket.owner"
name="workbasket.owner">
<div [hidden]="owner.valid" class="required-text">
<div *ngIf="!owner.valid" class="required-text">
* Owner is required
</div>
</div>
@ -46,7 +46,7 @@
<label for="wb-domain" class="control-label">Domain</label>
<input type="text" required #domain="ngModel" class="form-control" id="wb-domain" placeholder="Domain" [(ngModel)]="workbasket.domain"
name="workbasket.domain">
<div [hidden]="domain.valid" class="required-text">
<div *ngIf="!domain.valid" class="required-text">
* Domain is required
</div>
</div>

View File

@ -109,7 +109,6 @@ describe('InformationComponent', () => {
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.requestInProgress).toBeFalsy();
}));

View File

@ -1,6 +1,5 @@
import { Component, OnInit, Input, Output, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { ActivatedRoute, Params, Router, NavigationStart } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
@ -10,6 +9,7 @@ import { ErrorModel } from 'app/models/modal-error';
import { ACTION } from 'app/models/action';
import { Workbasket } from 'app/models/workbasket';
import { AlertModel, AlertType } from 'app/models/alert';
import { TaskanaDate } from 'app/shared/util/taskana.date';
import { AlertService } from 'app/services/alert/alert.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
@ -17,9 +17,6 @@ import { SavingWorkbasketService, SavingInformation } from 'app/services/saving-
import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
const dateFormat = 'yyyy-MM-ddTHH:mm:ss';
const dateLocale = 'en-US';
@Component({
selector: 'taskana-workbasket-information',
templateUrl: './workbasket-information.component.html',
@ -35,8 +32,6 @@ export class WorkbasketInformationComponent implements OnChanges, OnDestroy {
allTypes: Map<string, string>;
requestInProgress = false;
modalSpinner = false;
hasChanges = false;
badgeMessage = '';
@ -91,7 +86,6 @@ export class WorkbasketInformationComponent implements OnChanges, OnDestroy {
onClear() {
this.alertService.triggerAlert(new AlertModel(AlertType.INFO, 'Reset edited fields'))
this.workbasket = { ...this.workbasketClone };
this.hasChanges = false;
}
removeWorkbasket() {
@ -105,7 +99,7 @@ export class WorkbasketInformationComponent implements OnChanges, OnDestroy {
}, error => {
this.requestInProgressService.setRequestInProgress(false);
this.errorModalService.triggerError(new ErrorModel(
`There was an error deleting workbasket ${this.workbasket.workbasketId}`, error.error.message))
`There was an error deleting workbasket ${this.workbasket.workbasketId}`, error))
});
}
@ -116,7 +110,6 @@ export class WorkbasketInformationComponent implements OnChanges, OnDestroy {
private beforeRequest() {
this.requestInProgressService.setRequestInProgress(true);
this.modalSpinner = true;
}
private afterRequest() {
@ -142,15 +135,13 @@ export class WorkbasketInformationComponent implements OnChanges, OnDestroy {
new SavingInformation(this.workbasket._links.accessItems.href, this.workbasket.workbasketId));
}
}, error => {
this.errorModalService.triggerError(new ErrorModel('There was an error creating a workbasket',
error.error ? error.error.message : error))
this.requestInProgress = false;
this.errorModalService.triggerError(new ErrorModel('There was an error creating a workbasket', error))
this.requestInProgressService.setRequestInProgress(false);
});
}
private addDateToWorkbasket() {
const datePipe = new DatePipe(dateLocale);
const date = datePipe.transform(Date.now(), dateFormat) + 'Z';
const date = TaskanaDate.getDate();
this.workbasket.created = date;
this.workbasket.modified = date;
}

View File

@ -1,5 +1,5 @@
<div class="container-scrollable">
<taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner>
<taskana-spinner [isRunning]="requestInProgress" ></taskana-spinner>
<taskana-no-access *ngIf="!requestInProgress && (!hasPermission && !workbasket || !workbasket && selectedId)"></taskana-no-access>
<div id="workbasket-details" *ngIf="workbasket && !requestInProgress">
<ul class="nav nav-tabs" role="tablist">

View File

@ -40,7 +40,6 @@ import { SelectWorkBasketPipe } from 'app/pipes/selectedWorkbasket/seleted-workb
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
@Component({
selector: 'taskana-filter',
template: ''

View File

@ -4,7 +4,7 @@
<button type="button" (click)="addWorkbasket()" data-toggle="tooltip" title="Add" class="btn btn-default">
<span class="glyphicon glyphicon-plus green" 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>
</div>
<div class="pull-right margin-right">
<taskana-sort (performSorting)="sorting($event)"></taskana-sort>

View File

@ -3,7 +3,6 @@
<div #wbToolbar>
<taskana-workbasket-list-toolbar [workbaskets]="workbaskets" (performFilter)="performFilter($event)" (performSorting)="performSorting ($event)"></taskana-workbasket-list-toolbar>
</div>
<taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner>
<div>
<ul #wbList id="wb-list-container" class="list-group">
<li class="list-group-item no-space">
@ -26,6 +25,8 @@
</li>
</ul>
</div>
<taskana-spinner [isRunning]="requestInProgress"></taskana-spinner>
</div>
<taskana-pagination [(workbasketsResource)]="workbasketsResource" (changePage)="changePage($event)"></taskana-pagination>
</div>

View File

@ -24,6 +24,11 @@ const appRoutes: Routes = [
component: NoAccessComponent,
outlet: 'detail'
},
{
path: 'new-classification/:id',
component: WorkbasketDetailsComponent,
outlet: 'detail'
},
{
path: ':id',
component: WorkbasketDetailsComponent,

View File

@ -55,7 +55,11 @@ export class AppComponent implements OnInit, OnDestroy {
}
});
this.errorModalSubscription = this.errorModalService.getError().subscribe((error: ErrorModel) => {
this.modalErrorMessage = error.message;
if (typeof error.message === 'string') {
this.modalErrorMessage = error.message
} else {
this.modalErrorMessage = error.message.error ? (error.message.error.error + ' ' + error.message.error.message) : error.message.message;
}
this.modalTitle = error.title;
})

View File

@ -9,7 +9,7 @@ export class ClassificationDefinition {
public type: string = undefined,
public isValidInDomain: boolean = undefined,
public created: string = undefined,
public modifies: string = undefined,
public modified: string = undefined,
public name: string = undefined,
public description: string = undefined,
public priority: number = undefined,

View File

@ -1,6 +1,6 @@
export class ErrorModel {
constructor(
public title: string = undefined,
public message: string = undefined
public message: any = undefined
) { }
}

View File

@ -6,8 +6,6 @@ import {ClassificationDefinition} from '../../models/classification-definition';
import {AlertModel, AlertType} from '../../models/alert';
import {saveAs} from 'file-saver/FileSaver';
import {TaskanaDate} from '../../shared/util/taskana.date';
import {ErrorModel} from '../../models/modal-error';
import {ErrorModalService} from '../errorModal/error-modal.service';
@Injectable()
export class ClassificationDefinitionService {
@ -21,8 +19,7 @@ export class ClassificationDefinitionService {
})
};
constructor(private httpClient: HttpClient, private alertService: AlertService,
private errorModalService: ErrorModalService) {
constructor(private httpClient: HttpClient, private alertService: AlertService) {
}
// GET
@ -41,8 +38,7 @@ export class ClassificationDefinitionService {
this.httpClient.post(this.url + '/import',
JSON.parse(classifications), this.httpOptions).subscribe(
classificationsUpdated => this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, 'Import was successful')),
error => this.errorModalService.triggerError(new ErrorModel(
`There was an error importing classifications`, error.message))
error => this.alertService.triggerAlert(new AlertModel(AlertType.DANGER, 'Import was not successful'))
);
}
}

View File

@ -1,20 +1,24 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { Classification } from 'app/models/classification';
import { TreeNodeModel } from 'app/models/tree-node';
import { ClassificationDefinition } from 'app/models/classification-definition';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { ClassificationResource } from '../../models/classification-resource';
@Injectable()
export class ClassificationsService {
url = environment.taskanaRestUrl + '/v1/classifications';
classificationSelected = new Subject<string>();
private url = environment.taskanaRestUrl + '/v1/classifications';
private classificationSelected = new Subject<string>();
private classificationSaved = new Subject<number>();
private classificationTypeSelectedValue = 'TASK';
private classificationTypeSelected = new BehaviorSubject<string>(this.classificationTypeSelectedValue);
httpOptions = {
headers: new HttpHeaders({
@ -30,13 +34,14 @@ export class ClassificationsService {
}
// GET
getClassifications(forceRequest = false, type = 'TASK', domain = ''): Observable<Array<TreeNodeModel>> {
getClassifications(forceRequest = false, domain = ''): Observable<Array<TreeNodeModel>> {
if (!forceRequest && this.classificationRef) {
return this.classificationRef.map((response: ClassificationResource) => {
if (!response._embedded) {
return [];
}
return this.buildHierarchy(response._embedded.classificationSummaryResourceList, type, domain);
return this.buildHierarchy(response._embedded.classificationSummaryResourceList, this.classificationTypeSelectedValue, domain);
});
}
this.classificationRef = this.httpClient.get<ClassificationResource>(`${environment.taskanaRestUrl}/v1/classifications`,
@ -46,13 +51,18 @@ export class ClassificationsService {
if (!response._embedded) {
return [];
}
return this.buildHierarchy(response._embedded.classificationSummaryResourceList, type, domain);
return this.buildHierarchy(response._embedded.classificationSummaryResourceList, this.classificationTypeSelectedValue, domain);
});
}
// GET
getClassification(id: string): Observable<ClassificationDefinition> {
return this.httpClient.get<ClassificationDefinition>(`${environment.taskanaRestUrl}/v1/classifications/${id}`, this.httpOptions);
return this.httpClient.get<ClassificationDefinition>(`${environment.taskanaRestUrl}/v1/classifications/${id}`, this.httpOptions)
.do((classification: ClassificationDefinition) => {
if (classification) {
this.selectClassificationType(classification.type);
}
});
}
getClassificationTypes(): Observable<Array<string>> {
@ -71,6 +81,22 @@ export class ClassificationsService {
return typesSubject.asObservable();
}
// POST
postClassification(classification: Classification): Observable<Classification> {
return this.httpClient.post<Classification>(`${environment.taskanaRestUrl}/v1/classifications`, classification,
this.httpOptions);
}
// PUT
putClassification(url: string, classification: Classification): Observable<Classification> {
return this.httpClient.put<Classification>(url, classification, this.httpOptions);
}
// DELETE
deleteClassification(url: string): Observable<string> {
return this.httpClient.delete<string>(url, this.httpOptions);
}
// #region "Service extras"
selectClassification(id: string) {
this.classificationSelected.next(id);
@ -80,6 +106,23 @@ export class ClassificationsService {
return this.classificationSelected.asObservable();
}
triggerClassificationSaved() {
this.classificationSaved.next(Date.now());
}
classificationSavedTriggered(): Observable<number> {
return this.classificationSaved.asObservable();
}
selectClassificationType(id: string) {
this.classificationTypeSelectedValue = id;
this.classificationTypeSelected.next(id);
}
getSelectedClassificationType(): Observable<string> {
return this.classificationTypeSelected.asObservable();
}
// #endregion
private buildHierarchy(classifications: Array<Classification>, type: string, domain: string) {

View File

@ -4,12 +4,14 @@ import { HttpModule } from '@angular/http';
import { HttpClientInterceptor } from './http-client-interceptor.service';
import { PermissionService } from 'app/services/permission/permission.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
describe('HttpExtensionService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule, HttpModule],
providers: [HttpClientInterceptor, PermissionService]
providers: [HttpClientInterceptor, PermissionService, ErrorModalService, RequestInProgressService]
});
});

View File

@ -4,14 +4,21 @@ import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ErrorModel } from 'app/models/modal-error';
import { PermissionService } from 'app/services/permission/permission.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
@Injectable()
export class HttpClientInterceptor implements HttpInterceptor {
permissionService: PermissionService;
constructor(permissionService: PermissionService) {
this.permissionService = permissionService;
constructor(
private permissionService: PermissionService,
private errorModalService: ErrorModalService,
private requestInProgressService: RequestInProgressService) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
@ -19,8 +26,12 @@ export class HttpClientInterceptor implements HttpInterceptor {
this.permissionService.setPermission(true);
}, err => {
this.requestInProgressService.setRequestInProgress(false);
if (err instanceof HttpErrorResponse && (err.status === 401 || err.status === 403)) {
this.permissionService.setPermission(false)
} else {
this.errorModalService.triggerError(
new ErrorModel('There was error, please contact with your administrator ', err))
}
});
}

View File

@ -24,7 +24,7 @@ export class WorkbasketDefinitionService {
constructor(private httpClient: HttpClient, private alertService: AlertService,
private errorModalService: ErrorModalService) {
}
}
// GET
exportWorkbaskets(domain: string) {

View File

@ -5,4 +5,5 @@
width: 100%;
margin-bottom: 0px;
text-align: center;
z-index: 1050;
}

View File

@ -1 +1,4 @@
.word-break{word-break: break-word; }
.word-break{word-break: break-word; }
.modal{
z-index: 1051;
}

View File

@ -7,9 +7,13 @@
</router-outlet>
</div>
<div class="{{showDetail? 'hidden': 'hidden-xs hidden-sm col-md-8 container-no-detail'}}">
<div class="center-block no-detail">
<div *ngIf="currentRoute === 'workbaskets'" class="center-block no-detail">
<h3 class="grey">Select a worbasket</h3>
<svg-icon class="img-responsive no-detail-icon" src="./assets/icons/wb-empty.svg"></svg-icon>
</div>
<div *ngIf="currentRoute === 'classifications'" class="center-block no-detail">
<h3 class="grey">Select a classification</h3>
<svg-icon class="img-responsive no-detail-icon" src="./assets/icons/classification-empty.svg"></svg-icon>
</div>
</div>
</div>

View File

@ -9,10 +9,13 @@ import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-
})
export class MasterAndDetailComponent implements OnInit {
private classifications = 'classifications';
private workbaskets = 'workbaskets';
private detailRoutes: Array<string> = ['/workbaskets/(detail', 'classifications/(detail'];
private sub: any;
showDetail: Boolean = false;
currentRoute = '';
constructor(private route: ActivatedRoute, private router: Router, private masterAndDetailService: MasterAndDetailService) {
}
@ -39,6 +42,7 @@ export class MasterAndDetailComponent implements OnInit {
}
private checkUrl(url: string): Boolean {
this.checkRoute(url);
for (const routeDetail of this.detailRoutes) {
if (url.indexOf(routeDetail) !== -1) {
return true;
@ -46,4 +50,12 @@ export class MasterAndDetailComponent implements OnInit {
}
return false;
}
private checkRoute(url: string) {
if (url.indexOf(this.workbaskets) !== -1) {
this.currentRoute = this.workbaskets;
} else if (url.indexOf(this.classifications) !== -1) {
this.currentRoute = this.classifications;
}
}
}

View File

@ -1,5 +1,5 @@
<div [hidden]="!isDelayedRunning">
<div class="sk-circle {{positionClass? positionClass: 'spinner-centered'}}" [hidden]="this.isModal">
<div [ngClass]="{'no-display':!isDelayedRunning}">
<div *ngIf ="!isModal" class="sk-circle {{positionClass? positionClass: 'spinner-centered'}}">
<div class="sk-circle1 sk-child"></div>
<div class="sk-circle2 sk-child"></div>
<div class="sk-circle3 sk-child"></div>

View File

@ -128,5 +128,9 @@
z-index: 10;
}
.spinner-centered {
margin-top: calc(50vh - 100px);
margin-top: calc(50vh - 150px);
}
.no-display{
display: none;
}

View File

@ -1,6 +1,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SpinnerComponent } from './spinner.component';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
describe('SpinnerComponent', () => {
let component: SpinnerComponent;
@ -8,7 +9,8 @@ describe('SpinnerComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SpinnerComponent]
declarations: [SpinnerComponent],
providers: [ErrorModalService]
})
.compileComponents();
}));

View File

@ -1,5 +1,9 @@
import { Component, Input, ElementRef, Output, EventEmitter, OnDestroy } from '@angular/core';
import { ViewChild } from '@angular/core';
import { ErrorModel } from 'app/models/modal-error';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
declare var $: any;
@Component({
@ -39,19 +43,23 @@ export class SpinnerComponent implements OnDestroy {
@Input()
positionClass: string = undefined;
@Output()
requestTimeoutExceeded = new EventEmitter<string>()
@ViewChild('spinnerModal')
private modal;
constructor(private errorModalService: ErrorModalService) {
}
private runSpinner(value) {
this.currentTimeout = setTimeout(() => {
if (this.isModal) { $(this.modal.nativeElement).modal('toggle'); }
this.isDelayedRunning = value;
this.cancelTimeout();
this.requestTimeout = setTimeout(() => {
this.requestTimeoutExceeded.emit('There was an error with your request, please make sure you have internet connection');
this.errorModalService.triggerError(
new ErrorModel('There was an error with your request, please make sure you have internet connection',
'Request time execeed'));
this.cancelTimeout();
this.isRunning = false;
}, this.maxRequestTimeout);

View File

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

View File

@ -52,11 +52,16 @@ export class TaskanaTreeComponent implements OnInit, AfterViewChecked {
this.selectNodeIdChanged.emit(treeNode.node.data.classificationId + '');
}
onDeactivate(treeNode: any) {
this.selectNodeIdChanged.emit(undefined);
}
private selectNode(nodeId: string) {
if (nodeId) {
const selectedNode = this.getSelectedNode(nodeId)
if (selectedNode) {
selectedNode.setIsActive(true)
this.expandParent(selectedNode);
}
}
}
@ -71,6 +76,14 @@ export class TaskanaTreeComponent implements OnInit, AfterViewChecked {
return this.tree.treeModel.getNodeById(nodeId);
}
private expandParent(node: TreeNode) {
if (!node.parent) {
return
}
node.parent.expand();
this.expandParent(node.parent);
}
}

View File

@ -2,7 +2,7 @@ import {DatePipe} from '@angular/common';
export class TaskanaDate {
public static getDate(): string {
const dateFormat = 'yyyy-MM-ddTHH:mm:ss';
const dateFormat = 'yyyy-MM-ddTHH:mm:ss.sss';
const dateLocale = 'en-US';
const datePipe = new DatePipe(dateLocale);
return datePipe.transform(Date.now(), dateFormat) + 'Z';

View File

@ -268,6 +268,5 @@ taskana-workbasket-information, taskana-workbasket-access-items, taskana-workbas
}
tree-viewport {
border-top: 1px solid #ddd;
height: calc(100vh - 110px);
}

View File

@ -60,6 +60,11 @@ tree-node-collection > div > tree-node > .tree-node {
}
}
.node-content-wrapper-focused {
background: none;
box-shadow: none;
}
/* START Children branch lines*/
.node-content-wrapper::before {
@ -94,4 +99,5 @@ tree-node-collection > div > tree-node > .tree-node {
.toggle-children {
z-index: 1;
}
/* END children branch lines */

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M9.637 15.934c-.22-.08-.298-.217-.298-.53v-.277l-3.016-.01-3.016-.01-.292-.091c-.357-.112-.657-.307-.825-.534l-.125-.17-.014-5.348-.013-5.348h-.73c-.811 0-.996-.033-1.148-.204-.08-.09-.088-.247-.088-1.582C.072.207.063.253.382.143c.227-.078 5.73-.077 5.956 0 .308.107.309.114.309 1.722 0 1.434-.002 1.468-.108 1.557-.2.17-.343.194-1.132.194h-.728v.9l2.317-.01 2.317-.008.026-.318c.024-.293.038-.326.178-.423l.152-.106h5.92l.132.082a.55.55 0 0 1 .189.216c.08.194.07 2.756-.012 2.915a.543.543 0 0 1-.237.198l-.176.08h-2.843c-3.118 0-3.062.003-3.225-.207-.05-.066-.078-.198-.078-.38v-.278H4.628v2.661h4.711v-.31c0-.297.007-.314.16-.431l.161-.123h5.929l.136.091c.075.05.16.147.188.216.078.186.064 2.753-.015 2.906a.543.543 0 0 1-.237.199l-.176.079h-2.843c-3.118 0-3.062.003-3.224-.206-.052-.066-.079-.199-.079-.38v-.278h-4.66v2.66h4.66v-.308c0-.28.012-.317.134-.414l.134-.107h6.044l.106.076a.547.547 0 0 1 .157.199c.077.184.062 2.752-.016 2.904a.543.543 0 0 1-.237.198l-.176.08-2.853-.002c-2.422-.001-2.874-.01-2.995-.053zm5.035-1.691v-.917l-2.007.01-2.006.008-.014.864c-.007.475-.002.883.012.907.02.034.456.044 2.02.044h1.995zm-5.281.02c0-.373-.013-.448-.082-.465-.045-.012-1.22-.021-2.612-.021h-2.53l-.08-.077c-.07-.067-.08-.284-.08-1.669 0-1.384.01-1.601.08-1.668l.08-.077H9.343l-.014-.458-.015-.458-2.563-.008c-2.54-.008-2.563-.01-2.653-.082-.083-.067-.09-.2-.09-1.668 0-1.388.01-1.606.08-1.673l.08-.076h5.224V4.93H4.168l-.081-.077c-.065-.062-.08-.188-.08-.657v-.58H2.711v5.435c.001 2.989.016 5.47.033 5.513.021.053.084.087.18.1.083.01.15.03.15.041 0 .012 1.42.018 3.158.013l3.158-.01zm5.332-4.443v-.916h-4.09v1.831h4.09zm-.103-4.424v-.881h-3.987v1.762h3.987zM5.392 1.862l-.014-.89L3.372.965 1.366.954v.88c0 .67.015.882.065.895.035.01.944.02 2.02.02l1.955.003z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB