TSK-203 Add classification tree view

This commit is contained in:
Martin Rojas Miguel Angel 2018-04-04 16:41:56 +02:00 committed by Holger Hagen
parent 88eca3d326
commit 2efa1c6b30
49 changed files with 747 additions and 215 deletions

View File

@ -19,9 +19,7 @@
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss",
"./assets/_site.scss",
"./assets/_forms.scss"
"./assets/_styles.scss"
],
"scripts": [
"../node_modules/jquery/dist/jquery.min.js",

23
web/package-lock.json generated
View File

@ -571,13 +571,20 @@
"integrity": "sha512-wIRpoQ3PwytxA4MRe9cgmdytXrHgTGUuTdmIFtAQvCcftUSWWkzkVaXF1QSlFip6ipHf/YacdJHFYXpnW2lWPQ=="
},
"angular-tree-component": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/angular-tree-component/-/angular-tree-component-7.0.1.tgz",
"integrity": "sha1-/I0OctjDS4cTGjuivTKtIJRWiaw=",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/angular-tree-component/-/angular-tree-component-7.1.0.tgz",
"integrity": "sha512-i0Kk4gnuU+i6p5ZsIcDcGrtHPnDLOyHk8Vqez6IpSuOLvVPZ3Y7/Y1MEOoj7Nx6qRU5NuuVaPLy2idOEB7ClRw==",
"requires": {
"lodash": "4.17.4",
"mobx": "3.4.1",
"lodash": "4.17.5",
"mobx": "3.6.2",
"mobx-angular": "2.1.1"
},
"dependencies": {
"lodash": {
"version": "4.17.5",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
"integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="
}
}
},
"ansi-html": {
@ -7259,9 +7266,9 @@
}
},
"mobx": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-3.4.1.tgz",
"integrity": "sha1-N6vl7ogtQBgo2fJsbBovR2FLu+8="
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-3.6.2.tgz",
"integrity": "sha512-Dq3boJFLpZEvuh5a/MbHLUIyN9XobKWIb0dBfkNOJffNkE3vtuY0C9kSDVpfH8BB0BPkVw8g22qCv7d05LEhKg=="
},
"mobx-angular": {
"version": "2.1.1",

View File

@ -25,7 +25,7 @@
"@angular/router": "5.2.1",
"file-saver": "1.3.3",
"angular-svg-icon": "5.0.0",
"angular-tree-component": "7.0.1",
"angular-tree-component": "7.1.0",
"bootstrap": "3.3.7",
"bootstrap-sass": "3.3.7",
"core-js": "2.5.3",

View File

@ -1,40 +1,20 @@
<div class="classification-list-full-height">
<ul id="cl-list-container" class="list-group footer-space">
<li id="cl-action-toolbar" class="list-group-item tab-align">
<div class="row">
<div class="col-xs-9">
<taskana-import-export-component [currentSelection]="selectionToImport"></taskana-import-export-component>
</div>
</div>
</li>
<taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner>
<li id="wb-action-toolbar" class="list-group-item tab-align">
<div class="row">
<div class="col-xs-12">
<button type="button" (click)="addClassification()" data-toggle="tooltip" title="Add" class="btn btn-default">
<span class="glyphicon glyphicon-plus green" aria-hidden="true"></span>
</button>
<button type="button" (click)="removeClassification()" data-toggle="tooltip" title="Remove" class="btn btn-default remove">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>
<taskana-import-export-component [currentSelection]="selectionToImport"></taskana-import-export-component>
<taskana-classification-types-selector class="pull-right" [classificationTypes]="classificationsTypes" [(classificationTypeSelected)]="classificationTypeSelected"
(classificationTypeChanged)=selectClassificationType($event)></taskana-classification-types-selector>
</div>
</div>
</li>
<taskana-spinner [isRunning]="requestInProgress" class="centered-horizontally"></taskana-spinner>
<taskana-tree [treeNodes]="classifications"></taskana-tree>
</ul>
<ul id="wb-pagination" class="pagination vertical-center">
<li>
<a href="#" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<li>
<a href="">1</a>
</li>
<li>
<a href="">2</a>
</li>
<li>
<a href="">3</a>
</li>
<li>
<a href="">4</a>
</li>
<li>
<a href="">5</a>
</li>
<li>
<a href="#" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</div>
</div>

View File

@ -2,21 +2,15 @@
height: calc(100vh - 55px);
}
.row.list-group {
margin-left: 2px;
.list-group-item {
padding: 5px 0px;
border: none;
}
.list-group > li {
border-left: none;
border-right: none;
}
.tab-align{
margin-bottom: 0px;
a > label {
height: 2em;
width: 100%;
}
.tab-align {
border-bottom: 1px solid #ddd;
padding-bottom: 12px;
&>div{
margin: 6px 0px;
}
}

View File

@ -1,25 +1,46 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import {ClassificationListComponent} from './classification-list.component';
import {ImportExportComponent} from 'app/shared/import-export/import-export.component';
import {SpinnerComponent} from 'app/shared/spinner/spinner.component';
import {WorkbasketService} from 'app/services/workbasket/workbasket.service';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {WorkbasketDefinitionService} from 'app/services/workbasket/workbasketDefinition.service';
import {AlertService} from 'app/services/alert/alert.service';
import {ClassificationService} from 'app/services/classification/classification.service';
import {DomainService} from 'app/services/domains/domain.service';
import { TreeNode } from 'app/models/tree-node';
import { ClassificationListComponent } from './classification-list.component';
import { ImportExportComponent } from 'app/shared/import-export/import-export.component';
import { SpinnerComponent } from 'app/shared/spinner/spinner.component';
import { ClassificationTypesSelectorComponent } from 'app/shared/classification-types-selector/classification-types-selector.component';
import { MapValuesPipe } from 'app/pipes/mapValues/map-values.pipe';
import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { WorkbasketDefinitionService } from 'app/services/workbasket-definition/workbasket-definition.service';
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';
@Component({
selector: 'taskana-tree',
template: ''
})
class TreeComponent {
@Input() treeNodes;
}
describe('ClassificationListComponent', () => {
let component: ClassificationListComponent;
let fixture: ComponentFixture<ClassificationListComponent>;
const treeNodes: Array<TreeNode> = new Array(new TreeNode());
const classificationTypes: Map<string, string> = new Map<string, string>([['type1', 'type1'], ['type2', 'type2']])
let classificationsSpy, classificationsTypesSpy;
let classificationsService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ClassificationListComponent, ImportExportComponent, SpinnerComponent],
declarations: [ClassificationListComponent, ImportExportComponent, SpinnerComponent, ClassificationTypesSelectorComponent,
TreeComponent, MapValuesPipe],
imports: [HttpClientModule],
providers: [
WorkbasketService, HttpClient, WorkbasketDefinitionService, AlertService, ClassificationService, DomainService
HttpClient, WorkbasketDefinitionService, AlertService, ClassificationsService, DomainService, ClassificationDefinitionService
]
})
.compileComponents();
@ -28,6 +49,9 @@ describe('ClassificationListComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ClassificationListComponent);
component = fixture.componentInstance;
classificationsService = TestBed.get(ClassificationsService);
classificationsSpy = spyOn(classificationsService, 'getClassifications').and.returnValue(Observable.of(treeNodes));
classificationsTypesSpy = spyOn(classificationsService, 'getClassificationTypes').and.returnValue(Observable.of(classificationTypes));
fixture.detectChanges();
});

View File

@ -1,19 +1,55 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { ImportType } from 'app/models/import-type';
import { Classification } from 'app/models/classification';
import { TreeNode } from 'app/models/tree-node';
import { ClassificationsService } from 'app/services/classifications/classifications.service';
@Component({
selector: 'taskana-classification-list',
templateUrl: './classification-list.component.html',
styleUrls: ['./classification-list.component.scss']
selector: 'taskana-classification-list',
templateUrl: './classification-list.component.html',
styleUrls: ['./classification-list.component.scss']
})
export class ClassificationListComponent implements OnInit {
export class ClassificationListComponent implements OnInit, OnDestroy {
selectionToImport = ImportType.CLASSIFICATIONS;
requestInProgress = false;
constructor() {
}
selectionToImport = ImportType.CLASSIFICATIONS;
requestInProgress = false;
ngOnInit() {
}
classifications: Array<Classification> = [];
classificationsTypes: Map<string, string> = new Map();
classificationTypeSelected: string;
classificationServiceSubscription: Subscription;
classificationTypeServiceSubscription: Subscription;
constructor(private classificationService: ClassificationsService) {
}
ngOnInit() {
this.classificationServiceSubscription = this.classificationService.getClassifications()
.subscribe((classifications: Array<TreeNode>) => {
this.classifications = classifications;
this.classificationTypeServiceSubscription = this.classificationService.getClassificationTypes()
.subscribe((classificationsTypes: Map<string, string>) => {
this.classificationsTypes = classificationsTypes;
this.classificationTypeSelected = this.classifications[0].type;
});
});
}
selectClassificationType(classificationTypeSelected: string) {
this.classificationService.getClassifications(true, classificationTypeSelected)
.subscribe((classifications: Array<TreeNode>) => {
this.classifications = classifications;
});
}
addClassification() { }
removeClassification() { }
ngOnDestroy(): void {
if (this.classificationServiceSubscription) { this.classificationServiceSubscription.unsubscribe(); }
if (this.classificationTypeServiceSubscription) { this.classificationTypeServiceSubscription.unsubscribe(); }
}
}

View File

@ -7,9 +7,6 @@
<button *ngIf="workbasketIdSelected" type="button" (click)="copyWorkbasket()" data-toggle="tooltip" title="copy" class="btn btn-default">
<span class="glyphicon glyphicon-copy" aria-hidden="true"></span>
</button>
<button *ngIf="workbasketIdSelected" type="button" data-toggle="tooltip" title="Remove distibution target" class="btn btn-default">
<span class="glyphicon glyphicon-erase" aria-hidden="true"></span>
</button>
<button *ngIf="workbasketIdSelected" type="button" (click)="removeWorkbasket()" data-toggle="tooltip" title="Remove" class="btn btn-default remove">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>

View File

@ -1,11 +1,11 @@
.list-group-item {
padding: 0px 15px;
padding: 5px 0px;
border: none;
}
.tab-align{
margin-bottom: 0px;
padding: 0px;
&>div{
margin: 6px 0px;
}

View File

@ -26,8 +26,8 @@ import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { WorkbasketService } from 'app/services/workbasket/workbasket.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
import { AlertService } from 'app/services/alert/alert.service';
import { ClassificationService } from 'app/services/classification/classification.service';
import { WorkbasketDefinitionService } from 'app/services/workbasket/workbasketDefinition.service';
import { ClassificationDefinitionService } from 'app/services/classification-definition/classification-definition.service';
import { WorkbasketDefinitionService } from 'app/services/workbasket-definition/workbasket-definition.service';
import { DomainService } from 'app/services/domains/domain.service';
@Component({
@ -54,7 +54,7 @@ describe('WorkbasketListToolbarComponent', () => {
declarations: [WorkbasketListToolbarComponent, SortComponent,
FilterComponent, IconTypeComponent, DummyDetailComponent, MapValuesPipe, ImportExportComponent],
providers: [ErrorModalService, WorkbasketService, RequestInProgressService, AlertService,
ClassificationService, WorkbasketDefinitionService, DomainService]
ClassificationDefinitionService, WorkbasketDefinitionService, DomainService]
})
.compileComponents();
}));

View File

@ -7,7 +7,7 @@
<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-border">
<li class="list-group-item no-space">
<div class="row"></div>
</li>
<li class="list-group-item" *ngFor="let workbasket of workbaskets" [class.active]="workbasket.workbasketId == selectedId"

View File

@ -35,6 +35,7 @@ li > div.row > dl:first-child {
margin-left: 10px;
}
.no-border {
.no-space {
border-top: none;
padding: 0px
}

View File

@ -28,8 +28,8 @@ import { ImportExportComponent } from 'app/shared/import-export/import-export.co
import { RemoveNoneTypePipe } from 'app/pipes/removeNoneType/remove-none-type.pipe';
import { MapValuesPipe } from 'app/pipes/mapValues/map-values.pipe';
import { WorkbasketDefinitionService } from 'app/services/workbasket/workbasketDefinition.service';
import { ClassificationService } from 'app/services/classification/classification.service';
import { WorkbasketDefinitionService } from 'app/services/workbasket-definition/workbasket-definition.service';
import { ClassificationDefinitionService } from 'app/services/classification-definition/classification-definition.service';
import { DomainService } from 'app/services/domains/domain.service';
@Component({
@ -91,7 +91,7 @@ describe('WorkbasketListComponent', () => {
RouterTestingModule.withRoutes(routes)
],
providers: [WorkbasketService, ErrorModalService, RequestInProgressService, AlertService,
WorkbasketDefinitionService, OrientationService, DomainService, ClassificationService]
WorkbasketDefinitionService, OrientationService, DomainService, ClassificationDefinitionService]
})
.compileComponents();

View File

@ -3,13 +3,13 @@ import { RouterModule, Routes } from '@angular/router';
import { AppComponent } from './app.component';
import { WorkbasketListComponent } from './administration/workbasket/master/list/workbasket-list.component';
import { WorkbasketDetailsComponent } from './administration/workbasket/details/workbasket-details.component';
import { MasterAndDetailComponent } from './shared/masterAndDetail/master-and-detail.component';
import { MasterAndDetailComponent } from './shared/master-and-detail/master-and-detail.component';
import { NoAccessComponent } from './administration/workbasket/details/noAccess/no-access.component';
import {ClassificationListComponent} from './administration/classification/master/list/classification-list.component';
const appRoutes: Routes = [
{
path: 'workbaskets',
path: 'administration/workbaskets',
component: MasterAndDetailComponent,
children: [
{
@ -30,7 +30,7 @@ const appRoutes: Routes = [
]
},
{
path: 'classifications',
path: 'administration/classifications',
component: MasterAndDetailComponent,
children: [
{
@ -42,7 +42,7 @@ const appRoutes: Routes = [
},
{
path: '',
redirectTo: 'workbaskets',
redirectTo: 'administration/workbaskets',
pathMatch: 'full'
}
];

View File

@ -12,10 +12,10 @@
<div class="col-xs-8 col-md-7 logo-container">
<ul class="nav nav-tabs no-border-bottom" id="myTabs" role="tablist">
<li role="presentation" class="{{workbasketsRoute? 'active' : 'inactive'}}" role="tab" data-toggle="tab">
<a routerLink="/workbaskets" aria-controls="Work baskets" routerLinkActive="active">Workbaskets</a>
<a routerLink="administration/workbaskets" aria-controls="Work baskets" routerLinkActive="active">Workbaskets</a>
</li>
<li role="presentation" class="{{workbasketsRoute? 'inactive' : 'active'}}" role="tab" data-toggle="tab">
<a routerLink="/classifications" aria-controls="Classifications" routerLinkActive="active">Classifications</a>
<a routerLink="administration/classifications" aria-controls="Classifications" routerLinkActive="active">Classifications</a>
</li>
</ul>
</div>

View File

@ -9,6 +9,7 @@ import { ErrorModalService } from './services/errorModal/error-modal.service';
import { RequestInProgressService } from './services/requestInProgress/request-in-progress.service';
import { AlertService } from './services/alert/alert.service';
import { OrientationService } from './services/orientation/orientation.service';
import { SelectedRouteService } from './services/selected-route/selected-route';
import { GeneralMessageModalComponent } from './shared/general-message-modal/general-message-modal.component'
import { SpinnerComponent } from './shared/spinner/spinner.component'
@ -20,7 +21,7 @@ describe('AppComponent', () => {
let app, fixture, debugElement;
const routes: Routes = [
{ path: 'categories', component: AppComponent }
{ path: 'classifications', component: AppComponent }
];
beforeEach(async(() => {
@ -33,7 +34,7 @@ describe('AppComponent', () => {
RouterTestingModule.withRoutes(routes),
HttpClientModule
],
providers: [ErrorModalService, RequestInProgressService, AlertService, OrientationService]
providers: [ErrorModalService, RequestInProgressService, AlertService, OrientationService, SelectedRouteService]
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
@ -59,11 +60,11 @@ describe('AppComponent', () => {
expect(debugElement.querySelector('ul p a').textContent).toContain('Taskana administration');
}));
it('should call Router.navigateByUrl("categories") and workbasketRoute should be false', (inject([Router], (router: Router) => {
it('should call Router.navigateByUrl("classifications") and workbasketRoute should be false', (inject([Router], (router: Router) => {
expect(app.workbasketsRoute).toBe(true);
fixture.detectChanges();
router.navigateByUrl(`/categories`);
router.navigateByUrl(`/classifications`);
expect(app.workbasketsRoute).toBe(false);
})));

View File

@ -1,19 +1,21 @@
import { Component, OnInit, HostListener } from '@angular/core';
import { Component, OnInit, HostListener, OnDestroy } from '@angular/core';
import { environment } from '../environments/environment';
import { Router, NavigationStart } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { ErrorModel } from './models/modal-error';
import { ErrorModalService } from './services/errorModal/error-modal.service';
import { RequestInProgressService } from './services/requestInProgress/request-in-progress.service';
import { OrientationService } from './services/orientation/orientation.service';
import { SelectedRouteService } from './services/selected-route/selected-route';
@Component({
selector: 'taskana-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
export class AppComponent implements OnInit, OnDestroy {
title = 'Taskana administration';
adminUrl: string = environment.taskanaAdminUrl;
@ -23,9 +25,15 @@ export class AppComponent implements OnInit {
modalErrorMessage = '';
modalTitle = '';
selectedRoute = '';
requestInProgress = false;
errorModalSubscription: Subscription;
requestInProgressSubscription: Subscription;
selectedRouteSubscription: Subscription;
routerSubscription: Subscription;
@HostListener('window:resize', ['$event'])
onResize(event) {
this.orientationService.onResize();
@ -35,25 +43,38 @@ export class AppComponent implements OnInit {
private router: Router,
private errorModalService: ErrorModalService,
private requestInProgressService: RequestInProgressService,
private orientationService: OrientationService) {
private orientationService: OrientationService,
private selectedRouteService: SelectedRouteService) {
}
ngOnInit() {
this.router.events.subscribe(event => {
this.routerSubscription = this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
if (event.url.indexOf('categories') !== -1) {
this.workbasketsRoute = false;
}
this.selectedRouteService.selectRoute(event);
}
});
this.errorModalService.getError().subscribe((error: ErrorModel) => {
this.errorModalSubscription = this.errorModalService.getError().subscribe((error: ErrorModel) => {
this.modalErrorMessage = error.message;
this.modalTitle = error.title;
})
this.requestInProgressService.getRequestInProgress().subscribe((value: boolean) => {
this.requestInProgressSubscription = this.requestInProgressService.getRequestInProgress().subscribe((value: boolean) => {
this.requestInProgress = value;
})
this.selectedRouteSubscription = this.selectedRouteService.getSelectedRoute().subscribe((value: string) => {
if (value.indexOf('classifications') !== -1) {
this.workbasketsRoute = false;
}
this.selectedRoute = value;
})
}
ngOnDestroy() {
if (this.routerSubscription) { this.routerSubscription.unsubscribe(); }
if (this.errorModalSubscription) { this.errorModalSubscription.unsubscribe(); }
if (this.requestInProgressSubscription) { this.requestInProgressSubscription.unsubscribe(); }
if (this.selectedRouteSubscription) { this.selectedRouteSubscription.unsubscribe(); }
}
}

View File

@ -2,61 +2,66 @@
/**
* Modules
*/
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {AppRoutingModule} from './app-routing.module';
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';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
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
*/
import {AppComponent} from './app.component';
import {WorkbasketListComponent} from './administration/workbasket/master/list/workbasket-list.component';
import {WorkbasketListToolbarComponent} from './administration/workbasket/master/list/workbasket-list-toolbar/workbasket-list-toolbar.component'
import {WorkbasketDetailsComponent} from './administration/workbasket/details/workbasket-details.component';
import {WorkbasketInformationComponent} from './administration/workbasket/details/information/workbasket-information.component';
import {DistributionTargetsComponent} from './administration/workbasket/details/distribution-targets/distribution-targets.component';
import {DualListComponent} from './administration/workbasket/details/distribution-targets/dual-list/dual-list.component';
import {AccessItemsComponent} from './administration/workbasket/details/access-items/access-items.component';
import {NoAccessComponent} from './administration/workbasket/details/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 {SortComponent} from './shared/sort/sort.component';
import {GeneralMessageModalComponent} from './shared/general-message-modal/general-message-modal.component';
import {PaginationComponent} from './administration/workbasket/master/list/pagination/pagination.component';
import {ClassificationListComponent} from './administration/classification/master/list/classification-list.component';
import {ImportExportComponent} from './shared/import-export/import-export.component';
// Shared
import {MasterAndDetailComponent} from './shared/masterAndDetail/master-and-detail.component';
import { AppComponent } from './app.component';
import { WorkbasketListComponent } from './administration/workbasket/master/list/workbasket-list.component';
import { WorkbasketListToolbarComponent } from './administration/workbasket/master/list/workbasket-list-toolbar/workbasket-list-toolbar.component'
import { WorkbasketDetailsComponent } from './administration/workbasket/details/workbasket-details.component';
import { WorkbasketInformationComponent } from './administration/workbasket/details/information/workbasket-information.component';
import { DistributionTargetsComponent } from './administration/workbasket/details/distribution-targets/distribution-targets.component';
import { DualListComponent } from './administration/workbasket/details/distribution-targets/dual-list/dual-list.component';
import { AccessItemsComponent } from './administration/workbasket/details/access-items/access-items.component';
import { NoAccessComponent } from './administration/workbasket/details/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 { SortComponent } from './shared/sort/sort.component';
import { GeneralMessageModalComponent } from './shared/general-message-modal/general-message-modal.component';
import { PaginationComponent } from './administration/workbasket/master/list/pagination/pagination.component';
import { ClassificationListComponent } from './administration/classification/master/list/classification-list.component';
import { ImportExportComponent } from './shared/import-export/import-export.component';
import { MasterAndDetailComponent } from './shared/master-and-detail/master-and-detail.component';
import { ClassificationTypesSelectorComponent } from './shared/classification-types-selector/classification-types-selector.component';
import { TreeComponent } from './shared/tree/tree.component';
/**
* Services
*/
import {WorkbasketService} from './services/workbasket/workbasket.service';
import {MasterAndDetailService} from './services/masterAndDetail/master-and-detail.service';
import {HttpClientInterceptor} from './services/httpClientInterceptor/http-client-interceptor.service';
import {PermissionService} from './services/permission/permission.service';
import {AlertService} from './services/alert/alert.service';
import {ErrorModalService} from './services/errorModal/error-modal.service';
import {RequestInProgressService} from './services/requestInProgress/request-in-progress.service';
import {SavingWorkbasketService} from './services/saving-workbaskets/saving-workbaskets.service';
import {OrientationService} from './services/orientation/orientation.service';
import {ClassificationService} from './services/classification/classification.service';
import {WorkbasketDefinitionService} from './services/workbasket/workbasketDefinition.service';
import { WorkbasketService } from './services/workbasket/workbasket.service';
import { MasterAndDetailService } from './services/masterAndDetail/master-and-detail.service';
import { HttpClientInterceptor } from './services/httpClientInterceptor/http-client-interceptor.service';
import { PermissionService } from './services/permission/permission.service';
import { AlertService } from './services/alert/alert.service';
import { ErrorModalService } from './services/errorModal/error-modal.service';
import { RequestInProgressService } from './services/requestInProgress/request-in-progress.service';
import { SavingWorkbasketService } from './services/saving-workbaskets/saving-workbaskets.service';
import { OrientationService } from './services/orientation/orientation.service';
import { ClassificationDefinitionService } from './services/classification-definition/classification-definition.service';
import { WorkbasketDefinitionService } from './services/workbasket-definition/workbasket-definition.service';
import { SelectedRouteService } from './services/selected-route/selected-route';
import { ClassificationsService } from './services/classifications/classifications.service';
/**
* Pipes
*/
import {MapValuesPipe} from './pipes/mapValues/map-values.pipe';
import {RemoveNoneTypePipe} from './pipes/removeNoneType/remove-none-type.pipe';
import {SelectWorkBasketPipe} from './pipes/selectedWorkbasket/seleted-workbasket.pipe';
import {SpreadNumberPipe} from './pipes/spreadNumber/spread-number';
import {DomainService} from './services/domains/domain.service';
import { MapValuesPipe } from './pipes/mapValues/map-values.pipe';
import { RemoveNoneTypePipe } from './pipes/removeNoneType/remove-none-type.pipe';
import { SelectWorkBasketPipe } from './pipes/selectedWorkbasket/seleted-workbasket.pipe';
import { SpreadNumberPipe } from './pipes/spreadNumber/spread-number';
import { DomainService } from './services/domains/domain.service';
const MODULES = [
BrowserModule,
@ -91,6 +96,8 @@ const DECLARATIONS = [
PaginationComponent,
ClassificationListComponent,
ImportExportComponent,
TreeComponent,
ClassificationTypesSelectorComponent,
MapValuesPipe,
RemoveNoneTypePipe,
SelectWorkBasketPipe,
@ -104,7 +111,7 @@ const DECLARATIONS = [
WorkbasketService,
MasterAndDetailService,
PermissionService,
ClassificationService,
ClassificationDefinitionService,
WorkbasketDefinitionService,
DomainService,
{
@ -116,7 +123,9 @@ const DECLARATIONS = [
ErrorModalService,
RequestInProgressService,
SavingWorkbasketService,
OrientationService
OrientationService,
SelectedRouteService,
ClassificationsService
],
bootstrap: [AppComponent]
})

View File

@ -0,0 +1,24 @@
export class ClassificationDefinition {
constructor(public classificationId: string,
public key: string,
public parentId: string,
public category: string,
public domain: string,
public isValidInDomain: boolean,
public created: string,
public modifies: string,
public name: string,
public description: string,
public priority: number,
public serviceLevel: string,
public applicationEntryPoint: string,
public custom1: string,
public custom2: string,
public custom3: string,
public custom4: string,
public custom5: string,
public custom6: string,
public custom7: string,
public custom8: string) {
}
}

View File

@ -1,24 +1,12 @@
export class Classification {
constructor(public classificationId: string,
public key: string,
public parentId: string,
public category: string,
public domain: string,
public isValidInDomain: boolean,
public created: string,
public modifies: string,
public name: string,
public description: string,
public priority: number,
public serviceLevel: string,
public applicationEntryPoint: string,
public custom1: string,
public custom2: string,
public custom3: string,
public custom4: string,
public custom5: string,
public custom6: string,
public custom7: string,
public custom8: string) {
constructor(public id: string,
public key: string,
public category: string,
public type: string,
public domain: string,
public name: string,
public parentId: string,
public priority: number,
public serviceLevel: string) {
}
}

View File

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

View File

@ -2,13 +2,13 @@ import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {environment} from '../../../environments/environment';
import {AlertService} from '../alert/alert.service';
import {Classification} from '../../models/classification';
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';
@Injectable()
export class ClassificationService {
export class ClassificationDefinitionService {
url = environment.taskanaRestUrl + '/v1/classificationdefinitions';
@ -25,7 +25,7 @@ export class ClassificationService {
// GET
exportClassifications(domain: string) {
domain = (domain === '' ? '' : '?domain=' + domain);
this.httpClient.get<Classification[]>(this.url + domain, this.httpOptions)
this.httpClient.get<ClassificationDefinition[]>(this.url + domain, this.httpOptions)
.subscribe(
response => saveAs(new Blob([JSON.stringify(response)], {type: 'text/plain;charset=utf-8'}),
'Classifications_' + TaskanaDate.getDate() + '.json')

View File

@ -0,0 +1,84 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { Classification } from 'app/models/classification';
import { TreeNode } from 'app/models/tree-node';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
@Injectable()
export class ClassificationsService {
url = environment.taskanaRestUrl + '/v1/classifications';
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'Basic VEVBTUxFQURfMTpURUFNTEVBRF8x'
})
};
private classificationRef: Observable<Array<Classification>>;
private classificationTypes: Array<string>;
constructor(private httpClient: HttpClient) {
}
// GET
getClassifications(forceRequest = false, type = 'TASK', domain = ''): Observable<Array<TreeNode>> {
if (!forceRequest && this.classificationRef) {
return this.classificationRef.map((response: Array<Classification>) => {
return this.buildHierarchy(response, type, domain);
});
}
this.classificationRef = this.httpClient.get<Array<Classification>>(`${environment.taskanaRestUrl}/v1/classifications`,
this.httpOptions);
return this.classificationRef.map((response: Array<Classification>) => {
return this.buildHierarchy(response, type, domain);
});
}
getClassificationTypes(): Observable<Map<string, string>> {
const typesSubject = new Subject<Map<string, string>>();
this.classificationRef.subscribe((classifications: Array<Classification>) => {
const types = new Map<string, string>();
classifications.forEach(element => {
types.set(element.type, element.type);
});
typesSubject.next(types);
});
return typesSubject.asObservable();
}
private buildHierarchy(classifications: Array<Classification>, type: string, domain: string) {
const roots = []
const children = new Array<any>();
for (let index = 0, len = classifications.length; index < len; ++index) {
const item = classifications[index];
if (item.type === type) {
const parent = item.parentId,
target = !parent ? roots : (children[parent] || (children[parent] = []));
target.push(item);
}
}
for (let index = 0, len = roots.length; index < len; ++index) {
this.findChildren(roots[index], children);
}
return roots;
}
private findChildren(parent: any, children: Array<any>) {
if (children[parent.id]) {
parent.children = children[parent.id];
for (let index = 0, len = parent.children.length; index < len; ++index) {
this.findChildren(parent.children[index], children);
}
}
}
}

View File

@ -0,0 +1,39 @@
import { Injectable, OnInit } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { Router, ActivatedRoute, NavigationStart } from '@angular/router';
@Injectable()
export class SelectedRouteService {
public selectedRouteTriggered = new Subject<string>();
private detailRoutes: Array<string> = ['workbaskets', 'classifications'];
constructor(private route: ActivatedRoute, private router: Router) { }
selectRoute(value) {
this.selectedRouteTriggered.next(this.getRoute(value));
}
getSelectedRoute(): Observable<string> {
return this.selectedRouteTriggered.asObservable();
}
private getRoute(event): string {
if (event === undefined) {
return this.checkUrl(this.router.url);
}
return this.checkUrl(event.url)
}
private checkUrl(url: string): string {
for (const routeDetail of this.detailRoutes) {
if (url.indexOf(routeDetail) !== -1) {
return routeDetail;
}
}
return '';
}
}

View File

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

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClassificationTypesSelectorComponent } from './classification-types-selector.component';
import { MapValuesPipe } from 'app/pipes/mapValues/map-values.pipe';
describe('ClassificationTypesSelectorComponent', () => {
let component: ClassificationTypesSelectorComponent;
let fixture: ComponentFixture<ClassificationTypesSelectorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ClassificationTypesSelectorComponent, MapValuesPipe ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClassificationTypesSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'taskana-classification-types-selector',
templateUrl: './classification-types-selector.component.html',
styleUrls: ['./classification-types-selector.component.scss']
})
export class ClassificationTypesSelectorComponent implements OnInit {
@Input() classificationTypes: Map<string, string> = new Map<string, string>();
@Input()
classificationTypeSelected: string = undefined;
@Output()
classificationTypeSelectedChange = new EventEmitter<string>();
@Output()
classificationTypeChanged = new EventEmitter<string>();
constructor() { }
ngOnInit() {
}
select(value: string) {
this.classificationTypeSelected = value;
this.classificationTypeChanged.emit(value);
}
}

View File

@ -2,8 +2,8 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ImportExportComponent} from './import-export.component';
import {WorkbasketService} from '../../services/workbasket/workbasket.service';
import {ClassificationService} from '../../services/classification/classification.service';
import {WorkbasketDefinitionService} from '../../services/workbasket/workbasketDefinition.service';
import {ClassificationDefinitionService} from '../../services/classification-definition/classification-definition.service';
import {WorkbasketDefinitionService} from '../../services/workbasket-definition/workbasket-definition.service';
import {AlertService} from '../../services/alert/alert.service';
import {HttpClientModule} from '@angular/common/http';
import {DomainService} from '../../services/domains/domain.service';
@ -17,7 +17,7 @@ describe('ImportExportComponent', () => {
TestBed.configureTestingModule({
declarations: [ImportExportComponent],
imports: [HttpClientModule],
providers: [WorkbasketService, ClassificationService, WorkbasketDefinitionService, AlertService, DomainService]
providers: [WorkbasketService, ClassificationDefinitionService, WorkbasketDefinitionService, AlertService, DomainService]
})
.compileComponents();
}));

View File

@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { ClassificationService } from 'app/services/classification/classification.service';
import { WorkbasketDefinitionService } from 'app/services/workbasket/workbasketDefinition.service';
import { ClassificationDefinitionService } from 'app/services/classification-definition/classification-definition.service';
import { WorkbasketDefinitionService } from 'app/services/workbasket-definition/workbasket-definition.service';
import { DomainService } from 'app/services/domains/domain.service';
import { ImportType } from 'app/models/import-type';
@ -15,7 +15,7 @@ export class ImportExportComponent implements OnInit {
domains: string[] = [];
constructor(private domainService: DomainService, private workbasketDefinitionService: WorkbasketDefinitionService,
private classificationService: ClassificationService) {
private classificationDefinitionService: ClassificationDefinitionService) {
}
ngOnInit() {
@ -33,7 +33,7 @@ export class ImportExportComponent implements OnInit {
if (this.currentSelection === ImportType.WORKBASKETS) {
reader.onload = <Event>(e) => this.workbasketDefinitionService.importWorkbasketDefinitions(e.target.result);
} else {
reader.onload = <Event>(e) => this.classificationService.importClassifications(e.target.result);
reader.onload = <Event>(e) => this.classificationDefinitionService.importClassifications(e.target.result);
}
reader.readAsText(file);
}
@ -42,7 +42,7 @@ export class ImportExportComponent implements OnInit {
if (this.currentSelection === ImportType.WORKBASKETS) {
this.workbasketDefinitionService.exportWorkbaskets(domain);
} else {
this.classificationService.exportClassifications(domain);
this.classificationDefinitionService.exportClassifications(domain);
}
}
}

View File

@ -9,7 +9,7 @@ import { MasterAndDetailService } from 'app/services/masterAndDetail/master-and-
})
export class MasterAndDetailComponent implements OnInit {
private detailRoutes: Array<string> = ['/workbaskets/(detail', 'classifications'];
private detailRoutes: Array<string> = ['/workbaskets/(detail', 'classifications/(detail'];
private sub: any;
showDetail: Boolean = false;

View File

@ -0,0 +1,14 @@
<tree-root [nodes]="treeNodes" [state]="state" [options]="options">
<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':
node.data.category === 'AUTOMATIC'? 'automatic':
node.data.category === 'MANUAL'? 'manual':
'closed'}}.svg"></svg-icon>
</span>
<span>
<strong>{{ node.data.key }}</strong>
</span>
<span> - {{ node.data.name }}</span>
</ng-template>
</tree-root>

View File

@ -0,0 +1,3 @@
.text-top{
vertical-align: text-top;
}

View File

@ -0,0 +1,43 @@
import { Input, Component } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TreeComponent } from './tree.component';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientModule } from '@angular/common/http';
import { HttpModule } from '@angular/http';
// tslint:disable:component-selector
@Component({
selector: 'tree-root',
template: ''
})
class TreeVendorComponent {
@Input() options;
@Input() state;
@Input() nodes;
}
// tslint:enable:component-selector
fdescribe('TreeComponent', () => {
let component: TreeComponent;
let fixture: ComponentFixture<TreeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [AngularSvgIconModule, HttpClientModule, HttpModule],
declarations: [TreeComponent, TreeVendorComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TreeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,42 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { TreeNode } from 'app/models/tree-node';
import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions, ITreeState } from 'angular-tree-component';
@Component({
selector: 'taskana-tree',
templateUrl: './tree.component.html',
styleUrls: ['./tree.component.scss']
})
export class TreeComponent implements OnInit {
@Input() treeNodes: TreeNode;
@Output() treeNodesChange = new EventEmitter<Array<TreeNode>>();
options: ITreeOptions = {
displayField: 'name',
idField: 'id',
actionMapping: {
keys: {
[KEYS.ENTER]: (tree, node, $event) => {
node.toggleExpanded();
}
}
},
animateExpand: true,
animateSpeed: 20,
levelPadding: 20
}
state: ITreeState = {
activeNodeIds: { ['']: true },
}
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,6 @@
$blue-green: #479ea9;
$blue: #337ab7;
$green: green;
$grey: grey;
$brown: #f0ad4e;
$invalid: #a94442;

View File

@ -1,11 +1,11 @@
.ng-invalid:not(form) {
border-color: #a94442;
border-color: $invalid;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.required-text {
padding-left: 15px;
color: #a94442;
color: $invalid;
}

View File

@ -1,4 +1,3 @@
.placeholder {
margin-bottom: 20px;
}
@ -70,38 +69,58 @@
}
.blue{
color: #337ab7;
color: $blue;
& svg {
fill: #337ab7;
fill: $blue;
}
}
.green {
color: green;
color: $green;
& svg {
fill: green;
fill: $green;
}
}
.grey {
color:grey;
color:$grey;
& svg {
fill: grey;
fill: $grey;
}
}
.brown {
color: #f0ad4e;
color: $brown;
& svg {
fill: #f0ad4e;
fill: $brown;
}
}
.panel-default > .panel-heading .badge.warning {
background-color: #f0ad4e;
.red {
color: crimson;
& svg {
fill: crimson;
}
}
.green-blue {
color: $blue-green;
& svg {
fill: $blue-green;
}
}
svg-icon.fa-fw > svg {
text-align: center;
width: 1.25em;
}
.panel-default > .panel-heading .badge.warning {
background-color: $brown;
}
.badge.priority {
background-color: #e1e1e1;
}
/*
@ -247,3 +266,8 @@ taskana-workbasket-information, taskana-workbasket-access-items, taskana-workbas
}
}
}
tree-viewport {
border-top: 1px solid #ddd;
height: calc(100vh - 110px);
}

View File

@ -0,0 +1,6 @@
@import 'variables';
@import '../../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap';
@import '../../node_modules/angular-tree-component/dist/angular-tree-component.css';
@import 'site';
@import 'forms';
@import 'tree';

97
web/src/assets/_tree.scss Normal file
View File

@ -0,0 +1,97 @@
tree-node-expander {
& .toggle-children {
top: 2px;
@extend .glyphicon;
@extend .glyphicon-plus;
@extend .blue;
background: white;
background-image: none;
color: $blue-green;
padding-left:3px;
}
}
.toggle-children-wrapper {
padding: 0px;
font-size: 16px;
}
tree-node-expander .toggle-children-wrapper-expanded {
& .toggle-children {
@extend .glyphicon-minus ;
transform: none;
}
}
tree-node-collection > div > tree-node > .tree-node {
padding-left: 10px;
}
.node-content-wrapper {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
margin: 0px 10px;
}
.node-wrapper {
padding: 4px 0px;
}
.node-drop-slot {
height: 0px;
}
.node-content-wrapper, .tree-children {
position: relative;
}
.node-content-wrapper::before, .tree-children::after {
content: "";
position: absolute;
}
.node-content-wrapper-active, .node-content-wrapper.node-content-wrapper-active:hover, .node-content-wrapper-active.node-content-wrapper-focused {
background-color: $blue;
& >tree-node-content{
color:white;
& >span >svg-icon{
@extend .white;
}
}
}
/* START Children branch lines*/
.node-content-wrapper::before {
border-bottom: 1px dotted $blue-green;
border-left: 1px dotted $blue-green;
height: 28px;
top: -17px;
width: 20px;
left: -28px;
}
.tree-node-level-1 > tree-node-wrapper > .node-wrapper > .node-content-wrapper::before {
display: none;
}
.tree-node-leaf > .node-wrapper > .node-content-wrapper::before {
width: 25px;
}
.tree-children::after {
border-left: 1px dotted $blue-green;
height: 100%;
top: -15px;
left: -15px;
}
tree-node:last-child > .tree-node > .tree-children::after {
border-left: none;
}
.toggle-children {
z-index: 1;
}
/* END children branch lines */

View File

@ -0,0 +1,2 @@
@import '_colors';
$icon-font-path: '../../node_modules/bootstrap-sass/assets/fonts/bootstrap/';

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M6.108 15.89c-.088-.086-.15-.406-.193-.984l-.062-.856-.664-.327a9.303 9.303 0 0 1-1.013-.576l-.348-.248-.769.325c-.923.39-1.128.404-1.349.091-.442-.627-1.679-2.822-1.679-2.98 0-.128.207-.323.72-.678l.72-.498.048-1.144.049-1.143-.223-.193a8.167 8.167 0 0 0-.72-.51c-.273-.175-.52-.372-.548-.437-.047-.112 1.473-2.84 1.77-3.177.187-.212.379-.187 1.044.138.901.44.91.441 1.395.102.235-.164.684-.416.997-.56l.57-.259.062-.823c.034-.453.1-.892.145-.976.077-.139.245-.152 1.946-.152 1.39 0 1.885.027 1.953.107.05.059.12.482.154.94l.063.834.663.327c.365.18.82.439 1.012.575l.348.249.769-.325c.923-.39 1.128-.405 1.349-.092.443.628 1.679 2.823 1.679 2.981 0 .128-.207.323-.72.678l-.72.499-.024 1.16-.024 1.16.658.483c.38.28.68.567.709.681.038.148-.153.548-.757 1.584-.899 1.543-1 1.683-1.225 1.683-.086 0-.504-.147-.928-.326l-.771-.326-.39.295a6.852 6.852 0 0 1-1.01.586l-.621.29-.062.847c-.041.571-.104.889-.191.974-.193.188-3.62.188-3.812 0zM9.19 11.79c3.02-.797 3.91-4.723 1.544-6.797-.764-.67-1.612-.985-2.664-.99C5.713 3.995 3.945 5.7 3.95 7.979c.004 2.735 2.486 4.541 5.241 3.813zm-2.18-1.65c-.895-.395-1.395-1.172-1.394-2.163.003-1.388.976-2.338 2.397-2.338 1.42 0 2.393.95 2.396 2.338.002 1.384-.973 2.335-2.396 2.335-.428 0-.729-.052-1.004-.173z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M.517 15.83c-.144-.104-.292-.32-.377-.549-.133-.357-.14-.55-.12-3.638.019-3.142.025-3.27.167-3.495.311-.494.488-.582 1.233-.611l.691-.027 2.592-3.068 2.592-3.068-.043-.31c-.05-.356.09-.75.34-.957.208-.173.694-.17.905.006.224.187.38.655.33.99-.04.262.04.37 2.55 3.339L13.97 7.51l.692.027c.745.03.922.117 1.233.61.142.226.148.355.168 3.471.024 3.663.01 3.784-.473 4.174l-.26.21H8.042c-7.19 0-7.291-.003-7.525-.172zm14.885-.63l.176-.208v-6.38l-.209-.249-.209-.248H.921l-.208.248-.209.248v6.38l.175.21.176.207h14.372zM1.72 13.564a1.386 1.386 0 0 1-.41-.437c-.147-.26-.163-.388-.163-1.315 0-1.237.09-1.51.593-1.818.265-.162.383-.188.634-.138.412.083.866.6.832.947-.036.367-.242.348-.553-.051-.293-.377-.407-.412-.715-.223-.27.166-.29.255-.29 1.286 0 .829.01.901.174 1.095.111.132.257.209.397.209.212 0 .571-.312.571-.496 0-.132.226-.193.33-.09.233.23-.002.801-.429 1.042a.933.933 0 0 1-.971-.011zm1.975.092c-.09-.108-.055-3.633.038-3.743.113-.134.168-.128.302.032.094.11.113.384.113 1.65v1.517l.617.025c.647.025.817.129.718.436-.038.118-.174.14-.893.14-.466 0-.869-.026-.895-.057zm2.468-.085c-.459-.268-.548-.537-.574-1.724-.022-.994-.013-1.079.143-1.35.263-.458.512-.633.9-.633.38 0 .689.179.909.527.122.194.146.384.166 1.328.021 1.039.013 1.12-.145 1.4-.308.546-.907.74-1.399.452zm.881-.66c.165-.196.175-.263.173-1.124-.002-.959-.04-1.098-.344-1.29-.208-.132-.462-.062-.652.18-.127.161-.145.297-.145 1.105 0 .864.011.933.176 1.128.11.132.257.209.396.209.14 0 .286-.077.396-.209zm1.526.62c-.263-.18-.493-.546-.493-.783 0-.073.063-.173.14-.222.113-.071.172-.054.287.082.08.094.144.203.144.243 0 .107.337.268.562.268.233 0 .51-.278.51-.512 0-.255-.256-.506-.518-.508-.353-.002-.818-.29-.98-.608-.594-1.155.837-2.214 1.718-1.273.252.27.31.665.117.811-.108.082-.164.045-.366-.24-.207-.291-.28-.338-.528-.338-.496 0-.767.497-.453.834.073.078.312.177.531.22.646.124.979.502.979 1.109 0 .854-.964 1.39-1.65.917zm2.057.127c-.092-.109-.058-3.633.036-3.745.056-.066.35-.102.848-.102.803 0 .964.07.91.4-.02.13-.115.155-.684.176l-.66.024V11.5l.518.025c.485.023.52.038.542.227.033.276-.09.346-.613.348h-.447v1.018l.59.001c.666.002.79.06.756.348-.024.199-.039.203-.885.226-.474.013-.884-.003-.911-.035zm2.208-.08c-.024-.074-.043-.909-.043-1.855 0-2.003-.012-1.967.613-1.88.935.131 1.532.888 1.528 1.937-.004 1.094-.727 1.932-1.667 1.932-.288 0-.399-.034-.431-.134zm1.019-.635c.382-.258.534-.55.566-1.093.023-.387-.002-.521-.14-.762a1.391 1.391 0 0 0-.792-.644l-.196-.057v1.366c0 1.334.003 1.366.15 1.366.084 0 .269-.08.412-.176zM10.845 4.66l-2.41-2.862h-.788L5.237 4.66l-2.41 2.862h10.428zM8.303 1.037c.099-.257-.02-.469-.262-.469a.345.345 0 0 0-.252.13c-.14.2.02.549.252.549.124 0 .207-.067.262-.21z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M.056 8.794V1.611h10.202L8.68 3.294 7.105 4.978H2.933v8.082h8.19V9.113l1.402-1.675c.771-.92 1.419-1.674 1.44-1.674.02 0 .036 2.298.036 5.107v5.107H.056zm9.96-2.862l-.936-.955 1.63-1.656 1.63-1.657-.8-.812-.802-.813h5.255v5.326l-.83-.837-.831-.837-1.578 1.598c-.868.878-1.628 1.597-1.69 1.597-.061 0-.533-.43-1.048-.954z"/></svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M5.622 15.86c-.279-.151-.432-.49-.482-1.069l-.044-.504-1.554-2.058C1.9 10.052 1.777 9.835 1.88 9.27c.102-.56.59-1.024 1.161-1.103.403-.055.773.072 1.117.383l.274.249.002-2.41c.001-2.717.012-2.783.533-3.214.442-.365 1.218-.435 1.576-.142.066.055.089.029.089-.103 0-.224.245-.587.575-.854.582-.47 1.5-.297 1.916.36.113.178.205.386.205.46 0 .11.033.128.16.089.622-.19.884-.19 1.266.003.417.21.772.807.773 1.3 0 .132.027.157.125.117.365-.15.599-.18.857-.11.42.113.74.363.916.713.156.309.16.374.16 2.598 0 2.41-.085 3.537-.35 4.634-.154.637-.452 1.473-.613 1.72-.058.088-.14.425-.185.748-.095.7-.202.938-.505 1.123-.216.132-.374.14-3.172.137-2.466-.002-2.976-.02-3.139-.109zm5.932-.624c.068-.052.136-.307.185-.688.05-.388.142-.725.256-.942.273-.515.517-1.29.678-2.154.124-.667.15-1.167.182-3.389.027-1.936.014-2.646-.048-2.764-.047-.087-.194-.204-.327-.26-.21-.088-.275-.088-.485 0-.134.056-.278.167-.32.246-.046.087-.076.64-.076 1.414 0 1.034-.02 1.299-.11 1.428-.125.177-.464.215-.588.065-.05-.06-.084-.819-.098-2.169l-.02-2.077-.186-.186c-.333-.333-.966-.206-1.13.227-.037.099-.068 1.073-.068 2.166v1.986l-.177.117c-.165.108-.19.108-.355 0l-.178-.117V5.73c0-1.324-.02-2.508-.044-2.63-.108-.542-.727-.763-1.14-.407l-.2.171-.02 2.618c-.012 1.603-.047 2.65-.09 2.703-.142.171-.36.168-.536-.007l-.17-.17V6.006c0-1.297-.026-2.051-.076-2.145-.1-.188-.476-.341-.722-.294-.108.02-.276.132-.373.248l-.176.21v2.811c0 1.801-.027 2.86-.074 2.948-.143.269-.428.174-.886-.294-.667-.681-.982-.784-1.376-.451-.18.151-.219.237-.219.48 0 .24.058.37.305.688.167.215.886 1.161 1.596 2.103l1.292 1.712.046.554c.029.334.088.594.15.657.088.088.522.103 2.787.095 1.916-.007 2.714-.033 2.791-.092zM.118 4.356c-.12-.12-.106-.774.027-1.292C.59 1.34 2.276-.042 3.936-.042c.427 0 .567.08.567.325 0 .233-.157.321-.695.39C2.164.878.896 2.137.763 3.693c-.02.235-.082.5-.137.587-.108.174-.37.213-.508.075zm1.475-.023c-.134-.162-.045-.881.167-1.345.365-.8 1.25-1.44 2.1-1.516.479-.044.643.035.643.308 0 .216-.226.355-.668.411-.854.109-1.602.947-1.602 1.795 0 .128-.05.282-.112.344-.142.142-.412.144-.528.003zm12.354.043c-.055-.035-.124-.244-.154-.464-.126-.93-.762-1.614-1.597-1.72-.442-.057-.668-.196-.668-.412 0-.273.164-.352.643-.308.85.077 1.696.685 2.076 1.491.225.477.327 1.206.191 1.37-.1.12-.337.14-.491.043zm1.458-.095c-.055-.088-.119-.356-.142-.597-.15-1.57-1.397-2.805-3.04-3.012-.538-.068-.695-.156-.695-.389 0-.245.14-.325.567-.325.793 0 1.694.353 2.429.954.481.393 1.044 1.214 1.256 1.831.194.567.275 1.436.148 1.59-.133.16-.408.134-.523-.052z"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB