TSK-1398: New material design for classification component (#1287)
* TSK-1394: Remove bootstrap 3 * TSK-1394: Temporarily remove cypress tests * TSK-1398: fix classification component display * TSK-1399: fix workbasket display * TSK-1398: Fixed category selection in classification component * TSK-1398: Fixed category selection in classification component TSK-1398: * TSK-1398: Added selectCategory method * TSK-1398: Refactored Classification-Types-Selector * TSK-1398: Replaced category dropdown in classification details by Select * TSK-1398: Removed color of category icons * TSK-1398: added new administration overview navbar * TSK-1398: Removed bootstrap from action toolbar in classification-list * TSK-1398: Removed bootstrap from import-export component * TSK-1398: update visual in action bar in classification list and classification tree * TSK-1398: Removed bootstrap classes from classification-list * TSK-1398: update design * TSK-1398: update classification list design * TSK-1398: Refactored action-toolbar in classification-details * TSK-1398: rework administration routing configuration, now nested in mat-tabs * TSK-1398: administration tabs displays correctly when refreshing/first time open * TSK-1398: fixed overall CSS errors * TSK-1398: design update, added tooltips to buttons * TSK-1398: minor visual changes * TSK-1398: Refactored classification-details * TSK-1398: Fixed category selector in classification-details * TSK-1398: Modified category-icon css * TSK-1398: update classification details actionbar design * TSK-1398: minor design update, correctly highlight tab when details is showing * TSK-1398: Fixed tests in classification-details * TSK-1398: fixed "valid in domain" text not showing * TSK-1398: add domain selector in administration overview tabs * TSK-1398: Fixed tests in classification-list * TSK-1398: added unit tests for admin overview * TSK-1398: Update design of action bar in classification list * TSK-1398: Fixed tests * TSK-1398: minor CSS, tooltips update * TSK-1398: fix broken test in classification list Co-authored-by: Sofie Hofmann <29145005+sofie29@users.noreply.github.com>
This commit is contained in:
parent
34f9793907
commit
6a8311a32b
|
@ -5,69 +5,77 @@ import { DomainGuard } from 'app/shared/guards/domain.guard';
|
||||||
import { AccessItemsManagementComponent } from './components/access-items-management/access-items-management.component';
|
import { AccessItemsManagementComponent } from './components/access-items-management/access-items-management.component';
|
||||||
import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component';
|
import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component';
|
||||||
import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component';
|
import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component';
|
||||||
|
import { AdministrationOverviewComponent } from './components/administration-overview/administration-overview.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'workbaskets',
|
path: '',
|
||||||
component: WorkbasketOverviewComponent,
|
component: AdministrationOverviewComponent,
|
||||||
canActivate: [DomainGuard],
|
canActivate: [DomainGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'workbaskets',
|
||||||
component: WorkbasketOverviewComponent,
|
component: WorkbasketOverviewComponent,
|
||||||
outlet: 'master'
|
canActivate: [DomainGuard],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: WorkbasketOverviewComponent,
|
||||||
|
outlet: 'master'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: WorkbasketOverviewComponent,
|
||||||
|
outlet: 'detail'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: 'classifications',
|
||||||
component: WorkbasketOverviewComponent,
|
|
||||||
outlet: 'detail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '**',
|
|
||||||
redirectTo: ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'classifications',
|
|
||||||
component: ClassificationOverviewComponent,
|
|
||||||
canActivate: [DomainGuard],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: ClassificationOverviewComponent,
|
component: ClassificationOverviewComponent,
|
||||||
outlet: 'master'
|
canActivate: [DomainGuard],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ClassificationOverviewComponent,
|
||||||
|
outlet: 'master'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: ClassificationOverviewComponent,
|
||||||
|
outlet: 'detail'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: 'access-items-management',
|
||||||
component: ClassificationOverviewComponent,
|
component: AccessItemsManagementComponent,
|
||||||
outlet: 'detail'
|
canActivate: [DomainGuard],
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
redirectTo: ''
|
redirectTo: ''
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'access-items-management',
|
|
||||||
component: AccessItemsManagementComponent,
|
|
||||||
canActivate: [DomainGuard],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '**',
|
|
||||||
redirectTo: ''
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirectTo: 'workbaskets',
|
redirectTo: '',
|
||||||
pathMatch: 'full'
|
pathMatch: 'full'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
redirectTo: 'workbaskets'
|
redirectTo: ''
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,16 @@ import { WorkbasketDefinitionService } from './services/workbasket-definition.se
|
||||||
import { ImportExportService } from './services/import-export.service';
|
import { ImportExportService } from './services/import-export.service';
|
||||||
import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component';
|
import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component';
|
||||||
import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component';
|
import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
|
import { AdministrationOverviewComponent } from './components/administration-overview/administration-overview.component';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -61,12 +71,25 @@ const DECLARATIONS = [
|
||||||
ClassificationTypesSelectorComponent,
|
ClassificationTypesSelectorComponent,
|
||||||
ClassificationDetailsComponent,
|
ClassificationDetailsComponent,
|
||||||
ImportExportComponent,
|
ImportExportComponent,
|
||||||
AccessItemsManagementComponent
|
AccessItemsManagementComponent,
|
||||||
|
AdministrationOverviewComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: DECLARATIONS,
|
declarations: DECLARATIONS,
|
||||||
imports: [MODULES, MatRadioModule],
|
imports: [
|
||||||
|
MODULES,
|
||||||
|
MatRadioModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatTabsModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatDividerModule
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ClassificationDefinitionService,
|
ClassificationDefinitionService,
|
||||||
WorkbasketDefinitionService,
|
WorkbasketDefinitionService,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="panel panel-default">
|
<div class="access-items-management panel panel-default">
|
||||||
|
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-header">Access items management</h4>
|
<h4 class="panel-header">Access items management</h4>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
@import '../../../../theme/colors';
|
@import '../../../../theme/colors';
|
||||||
|
|
||||||
|
.access-items-management {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.margin {
|
.margin {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<div class="administration-overview">
|
||||||
|
<nav mat-tab-nav-bar class="administration-overview__navbar" backgroundColor="#f5f5f5" >
|
||||||
|
<a mat-tab-link class="administration-overview__navbar-links" routerLink="/taskana/administration/workbaskets" [active]="selectedTab == 'workbaskets'"
|
||||||
|
(click)="selectedTab = 'workbaskets'">Workbaskets</a>
|
||||||
|
<a mat-tab-link class="administration-overview__navbar-links" routerLink="/taskana/administration/classifications" [active]="selectedTab == 'classifications'"
|
||||||
|
(click)="selectedTab = 'classifications'">Classifications</a>
|
||||||
|
<a mat-tab-link class="administration-overview__navbar-links" routerLink="/taskana/administration/access-items-management"
|
||||||
|
[active]="selectedTab == 'access-items-management'" (click)="selectedTab = 'access-items-management'">Access Items
|
||||||
|
Management</a>
|
||||||
|
</nav>
|
||||||
|
<div class="administration-overview__domain">
|
||||||
|
<mat-form-field appearance="legacy">
|
||||||
|
<mat-select [value]="selectedDomain" matTooltip="Select domain">
|
||||||
|
<mat-option *ngFor="let domain of domains" [value]="domain" (click)="switchDomain(domain)">
|
||||||
|
{{domain? domain: 'MASTER DOMAIN'}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
|
@ -0,0 +1,27 @@
|
||||||
|
@import 'src/theme/_colors.scss';
|
||||||
|
|
||||||
|
.administration-overview__navbar {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.administration-overview__navbar {
|
||||||
|
background-color: $light-grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.administration-overview__domain {
|
||||||
|
max-height: 48px;
|
||||||
|
position: absolute;
|
||||||
|
top: 46px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-tab-link {
|
||||||
|
color: #1a202c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-tab-label-active {
|
||||||
|
font-weight: bolder !important;
|
||||||
|
color: unset;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { AdministrationOverviewComponent } from './administration-overview.component';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { DomainService } from '../../../shared/services/domain/domain.service';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
const domainServiceSpy = jest.fn().mockImplementation(
|
||||||
|
(): Partial<DomainService> => ({
|
||||||
|
getDomains: jest.fn().mockReturnValue(of(['domain a', 'domain b'])),
|
||||||
|
getSelectedDomain: jest.fn().mockReturnValue(of('domain a')),
|
||||||
|
switchDomain: jest.fn()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('AdministrationOverviewComponent', () => {
|
||||||
|
let component: AdministrationOverviewComponent;
|
||||||
|
let fixture: ComponentFixture<AdministrationOverviewComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
MatSelectModule,
|
||||||
|
MatTabsModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
HttpClientTestingModule,
|
||||||
|
BrowserAnimationsModule
|
||||||
|
],
|
||||||
|
declarations: [AdministrationOverviewComponent],
|
||||||
|
providers: [{ provide: DomainService, useClass: domainServiceSpy }]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdministrationOverviewComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create component', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render 3 tabs in navbar', () => {
|
||||||
|
const navbar = fixture.debugElement.nativeElement.getElementsByClassName('administration-overview__navbar-links');
|
||||||
|
expect(navbar).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display current domain', () => {
|
||||||
|
const domainElem = fixture.debugElement.nativeElement.querySelector('.administration-overview__domain');
|
||||||
|
expect(domainElem).toBeTruthy();
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(domainElem.textContent).toMatch('domain a');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { DomainService } from '../../../shared/services/domain/domain.service';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'taskana-administration-overview',
|
||||||
|
templateUrl: './administration-overview.component.html',
|
||||||
|
styleUrls: ['./administration-overview.component.scss']
|
||||||
|
})
|
||||||
|
export class AdministrationOverviewComponent implements OnInit {
|
||||||
|
@Input() selectedTab = '';
|
||||||
|
domains: Array<string> = [];
|
||||||
|
selectedDomain: string;
|
||||||
|
|
||||||
|
destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(private router: Router, private domainService: DomainService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.domainService
|
||||||
|
.getDomains()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((domains) => {
|
||||||
|
this.domains = domains;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.domainService
|
||||||
|
.getSelectedDomain()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((domain) => {
|
||||||
|
this.selectedDomain = domain;
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlPaths = this.router.url.split('/');
|
||||||
|
if (this.router.url.includes('detail')) {
|
||||||
|
this.selectedTab = urlPaths[urlPaths.length - 2];
|
||||||
|
} else {
|
||||||
|
this.selectedTab = urlPaths[urlPaths.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchDomain(domain) {
|
||||||
|
this.domainService.switchDomain(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,189 +1,209 @@
|
||||||
<div class="container-scrollable">
|
<div class="classification-details">
|
||||||
<taskana-shared-spinner [isRunning]="requestInProgress" class="floating"
|
<taskana-shared-spinner [isRunning]="requestInProgress" class="floating"
|
||||||
(spinnerIsRunning)="spinnerRunning($event)"></taskana-shared-spinner>
|
(spinnerIsRunning)="spinnerRunning($event)"></taskana-shared-spinner>
|
||||||
<div id="classification-details" *ngIf="classification && !spinnerIsRunning">
|
<div class="classification-details__wrapper" id="classification-details" *ngIf="classification && !spinnerIsRunning">
|
||||||
<div id="classification" class="panel panel-default classification">
|
|
||||||
|
|
||||||
<!-- TITLE + ACTION BUTTONS -->
|
<!-- TITLE + ACTION BUTTONS -->
|
||||||
<div class="panel-heading">
|
<section class="classification-details__action-toolbar">
|
||||||
<div class="pull-right btn-group classification__menu-bar">
|
<h4 class="classification-details__headline">{{classification.name}} [{{classification.type}}]
|
||||||
<button type="button" (click)="onSubmit()" class="btn btn-default btn-primary"
|
<span *ngIf="isCreatingNewClassification" class="badge warning"> {{badgeMessage$ | async}}</span>
|
||||||
data-toggle="tooltip" title="Save">
|
</h4>
|
||||||
<span class="material-icons md-20">save</span>
|
|
||||||
</button>
|
<div>
|
||||||
<button type="button" (click)="onRestore()" class="btn btn-default" data-toggle="tooltip"
|
<button mat-button class="action-toolbar__button action-toolbar__save-button" matTooltip="Save changes in current classification" (click)="onSubmit()">
|
||||||
title="Restore Previous Version">
|
Save
|
||||||
<span class="material-icons md-20 blue">restore</span>
|
<mat-icon class="md-20">save</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" (click)="onCopy()" data-toggle="tooltip" title="Copy"
|
|
||||||
class="btn btn-default" id="copyButton">
|
<button mat-stroked-button class="action-toolbar__button" matTooltip="Revert changes to previous saved state" (click)="onRestore()">
|
||||||
<span class="material-icons md-20 green-blue">content_copy</span>
|
Undo Changes
|
||||||
</button>
|
<mat-icon class="button__green-blue md-20">restore</mat-icon>
|
||||||
<button type="button" (click)="onRemoveClassification()" data-toggle="tooltip"
|
</button>
|
||||||
title="Delete" class="btn btn-default">
|
|
||||||
<span class="material-icons md-20 red">delete</span>
|
<button mat-stroked-button [matMenuTriggerFor]="buttonMenu" matTooltip="More actions" class="action-toolbar__button" id="action-toolbar__more-buttons">
|
||||||
</button>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</div>
|
</button>
|
||||||
<h4 class="panel-header classification__headline">{{classification.name}} [{{classification.type}}]
|
|
||||||
<span *ngIf="isCreatingNewClassification" class="badge warning"> {{badgeMessage$ | async}}</span>
|
<mat-menu #buttonMenu="matMenu">
|
||||||
</h4>
|
<button mat-menu-item class="action-toolbar__dropdown" matTooltip="Copy current values to create new classification" (click)="onCopy()">
|
||||||
|
<mat-icon class="button__green-blue">content_copy</mat-icon>
|
||||||
|
<span>Copy</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item class="action-toolbar__dropdown" matTooltip="Delete this classification" (click)="onRemoveClassification()">
|
||||||
|
<mat-icon class="button__red">delete</mat-icon>
|
||||||
|
<span>Delete</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item class="action-toolbar__dropdown" style="border-bottom-style: none;" matTooltip="Close this classification and discard all changes" (click)="onCloseClassification()">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
<span>Close</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DETAILED FIELDS -->
|
</section>
|
||||||
<div class="panel-body classification__detailed-fields" style="padding: 0">
|
|
||||||
<ng-form #ClassificationForm="ngForm">
|
|
||||||
<div class="row" style="padding: 15px">
|
|
||||||
<div class="col-md-6">
|
|
||||||
|
|
||||||
<!--TODO pattern?-->
|
<!-- DETAILED FIELDS -->
|
||||||
<!-- KEY -->
|
<div class="panel-body" style="padding: 0">
|
||||||
<div class="form-group required">
|
<ng-form #ClassificationForm="ngForm">
|
||||||
<label for="classification-key" class="control-label">Key</label>
|
<div class="classification__detailed-fields">
|
||||||
<input type="text" required maxlength="32" #key="ngModel" [disabled]="!isCreatingNewClassification"
|
|
||||||
class="form-control"
|
|
||||||
id="classification-key" placeholder="Key" [(ngModel)]="classification.key"
|
|
||||||
name="classification.key" (input)="validateInputOverflow(key, 32)">
|
|
||||||
<div *ngIf="inputOverflowMap.get(key.name)" class="error">{{lengthError}}</div>
|
|
||||||
<taskana-shared-field-error-display [displayError]="key.invalid && key.dirty"
|
|
||||||
errorMessage="* Key is required">
|
|
||||||
</taskana-shared-field-error-display>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- NAME -->
|
<h6 class="classification-details__subheading" style="margin-top: 65px;"> General </h6>
|
||||||
<div class="form-group required">
|
<mat-divider class="classification-details__horizontal-line"> </mat-divider>
|
||||||
<label for="classification-name" class="control-label">Name</label>
|
|
||||||
<input type="text" required maxlength="255" #name="ngModel"
|
|
||||||
class="form-control"
|
|
||||||
id="classification-name" placeholder="Name"
|
|
||||||
[(ngModel)]="classification.name" name="classification.name"
|
|
||||||
(input)="validateInputOverflow(name, 255)">
|
|
||||||
<div *ngIf="inputOverflowMap.get(name.name)" class="error">{{lengthError}}</div>
|
|
||||||
<taskana-shared-field-error-display [displayError]="name.invalid && name.dirty"
|
|
||||||
errorMessage="* Name is required">
|
|
||||||
</taskana-shared-field-error-display>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DOMAIN -->
|
<mat-form-field appearance="outline">
|
||||||
<div class="form-group detailed-fields__domain">
|
<mat-label>Key</mat-label>
|
||||||
<label for="classification-domain" class="control-label">Domain</label>
|
<label for="classification-key"></label>
|
||||||
<input type="text" disabled #domain class="form-control" id="classification-domain"
|
<input matInput required type="text" #key="ngModel" maxlength="32" [disabled]="!isCreatingNewClassification"
|
||||||
placeholder="Domain" [(ngModel)]="classification.domain"
|
id="classification-key" placeholder="Key" [(ngModel)]="classification.key"
|
||||||
name="classification.domain">
|
name="classification.key" (input)="validateInputOverflow(key, 32)">
|
||||||
<a *ngIf="!masterDomainSelected()" (click)="validChanged()">
|
</mat-form-field>
|
||||||
<label>
|
<div *ngIf="inputOverflowMap.get(key.name)" class="error">{{lengthError}}</div>
|
||||||
<b>Valid in Domain:</b>
|
<!-- <taskana-shared-field-error-display [displayError]="key.invalid && key.dirty"
|
||||||
<span class="material-icons md-20 blue ">{{classification.isValidInDomain
|
errorMessage="* Key is required">
|
||||||
? 'check_box' :
|
</taskana-shared-field-error-display> -->
|
||||||
'check_box_outline_blank'}}</span>
|
|
||||||
</label>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PRIORITY AND CATEGORY -->
|
<!-- NAME -->
|
||||||
<div class="row">
|
<mat-form-field appearance="outline">
|
||||||
<div class="form-group required col-xs-6">
|
<mat-label>Name</mat-label>
|
||||||
<label for="classification-priority" class="control-label">Priority</label>
|
<label for="classification-name"></label>
|
||||||
<taskana-shared-number-picker [(ngModel)]="classification.priority"
|
<input matInput type="text" required maxlength="255" #name="ngModel"
|
||||||
name="classification.priority"
|
id="classification-name" placeholder="Name"
|
||||||
id="classification-priority"
|
[(ngModel)]="classification.name" name="classification.name"
|
||||||
[required]="true"></taskana-shared-number-picker>
|
(input)="validateInputOverflow(name, 255)">
|
||||||
<taskana-shared-field-error-display
|
</mat-form-field>
|
||||||
[displayError]="!isFieldValid('classification.priority')"
|
<div *ngIf="inputOverflowMap.get(name.name)" class="error">{{lengthError}}</div>
|
||||||
[validationTrigger]="this.toggleValidationMap.get('classification.priority.name')"
|
<!--
|
||||||
errorMessage="* Priority is required">
|
<taskana-shared-field-error-display [displayError]="name.invalid && name.dirty"
|
||||||
</taskana-shared-field-error-display>
|
errorMessage="* Name is required">
|
||||||
</div>
|
</taskana-shared-field-error-display> -->
|
||||||
<div class="form-group required btn-group col-xs-6">
|
|
||||||
<label for="classification-category" class="control-label">Category</label>
|
|
||||||
<div class="dropdown clearfix">
|
|
||||||
<button class="btn btn-default" type="button" data-toggle="dropdown"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="true" id="classification-category">
|
|
||||||
<span class="text-top">
|
|
||||||
<svg-icon class="blue fa-fw"
|
|
||||||
src="{{(getCategoryIcon(classification.category) | async)?.name}}"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
[title]="(getCategoryIcon(classification.category) | async)?.text"></svg-icon>
|
|
||||||
</span>
|
|
||||||
{{classification.category}}
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu dropdown-menu" aria-labelledby="dropdownMenu">
|
|
||||||
<li>
|
|
||||||
<a class="detailed-fields__categories" *ngFor="let category of getAvailableCategories(classification.type) | async"
|
|
||||||
(click)="selectCategory(category)">
|
|
||||||
<span class="text-top">
|
|
||||||
<svg-icon class="blue fa-fw"
|
|
||||||
src="{{(getCategoryIcon(category) | async)?.name}}"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
[title]="(getCategoryIcon(category) | async)?.text"></svg-icon>
|
|
||||||
{{category}}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
|
|
||||||
<!-- SERVICE LEVEL -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="classification-service-level" class="control-label">Service
|
|
||||||
Level</label>
|
|
||||||
<input type="text" maxlength="255" class="form-control"
|
|
||||||
id="classification-service-level" placeholder="Service Level"
|
|
||||||
[(ngModel)]="classification.serviceLevel" name="classification.serviceLevel"
|
|
||||||
#serviceLevel="ngModel" (input)="validateInputOverflow(serviceLevel, 255)">
|
|
||||||
<div *ngIf="inputOverflowMap.get(serviceLevel.name)" class="error">{{lengthError}}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- APPLICATION ENTRY POINT -->
|
<!-- SERVICE LEVEL AND PRIORITY-->
|
||||||
<div class="form-group">
|
<div class="classification-details__service-and-priority">
|
||||||
<label for="classification-application-entry-point" class="control-label">
|
<mat-form-field appearance="outline" class="classification-details__mat-form-field">
|
||||||
Application entry point</label>
|
<mat-label> Service Level </mat-label>
|
||||||
<input type="text" maxlength="255" class="form-control"
|
<label for="classification-service-level"></label>
|
||||||
id="classification-application-entry-point"
|
<input matInput type="text" required maxlength="255"
|
||||||
placeholder="Application entry point"
|
id="classification-service-level" placeholder="Service Level"
|
||||||
[(ngModel)]="classification.applicationEntryPoint"
|
[(ngModel)]="classification.serviceLevel" name="classification.serviceLevel"
|
||||||
name="classification.applicationEntryPoint" #appEntryPoint="ngModel"
|
#serviceLevel="ngModel" (input)="validateInputOverflow(serviceLevel, 255)">
|
||||||
(input)="validateInputOverflow(appEntryPoint, 255)">
|
</mat-form-field>
|
||||||
<div *ngIf="inputOverflowMap.get(appEntryPoint.name)" class="error">{{lengthError}}</div>
|
<div *ngIf="inputOverflowMap.get(serviceLevel.name)" class="error">{{lengthError}}</div>
|
||||||
</div>
|
|
||||||
|
<div>
|
||||||
|
<!-- I replaced this component by the mat-form-field. Is this the same?
|
||||||
|
I don't understand all methods in the number-picker component.
|
||||||
|
<taskana-shared-number-picker [(ngModel)]="classification.priority"
|
||||||
|
name="classification.priority"
|
||||||
|
id="classification-priority"
|
||||||
|
[required]="true"
|
||||||
|
[name]="'Priority'">
|
||||||
|
</taskana-shared-number-picker> -->
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Priority</mat-label>
|
||||||
|
<label for="classification-priority"></label>
|
||||||
|
<input matInput type="number" [(ngModel)]="classification.priority"
|
||||||
|
name="classification.priority" id="classification-priority" required min="0">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<taskana-shared-field-error-display
|
||||||
|
[displayError]="!isFieldValid('classification.priority')"
|
||||||
|
[validationTrigger]="this.toggleValidationMap.get('classification.priority.name')"
|
||||||
|
errorMessage="* Priority is required">
|
||||||
|
</taskana-shared-field-error-display>
|
||||||
|
|
||||||
<!-- DESCRIPTION -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="classification-description" class="control-label">Description</label>
|
|
||||||
<textarea class="form-control" maxlength="255" rows="5"
|
|
||||||
id="classification-description" placeholder="Description"
|
|
||||||
[(ngModel)]="classification.description"
|
|
||||||
name="classification.description" #description="ngModel"
|
|
||||||
(input)="validateInputOverflow(description, 255)"></textarea>
|
|
||||||
<div *ngIf="inputOverflowMap.get(description.name)" class="error">{{lengthError}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- DOMAIN AND CATEGORY -->
|
||||||
|
<div class="classification-details__domain-and-category">
|
||||||
|
|
||||||
|
<mat-form-field class="classification-details__mat-form-field" appearance="outline">
|
||||||
|
<mat-label>Domain</mat-label>
|
||||||
|
<label for="classification-domain"></label>
|
||||||
|
<input matInput type="text" disabled id="classification-domain"
|
||||||
|
placeholder="Domain" [(ngModel)]="classification.domain"
|
||||||
|
name="classification.domain">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="domain-and-category__domain-checkbox">
|
||||||
|
Valid in Domain
|
||||||
|
<a *ngIf="!masterDomainSelected()" (click)="validChanged()" title="Valid in Domain">
|
||||||
|
<mat-icon class="domain-and-category__domain-checkbox-icon">{{classification.isValidInDomain ? 'check_box' : 'check_box_outline_blank'}}</mat-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Category</mat-label>
|
||||||
|
<mat-select required [(value)]="this.classification.category">
|
||||||
|
<mat-select-trigger>
|
||||||
|
<svg-icon
|
||||||
|
class="domain-and-category__category-icon" [src]="(getCategoryIcon(this.classification.category) | async)?.name">
|
||||||
|
</svg-icon>
|
||||||
|
{{this.classification.category}}
|
||||||
|
</mat-select-trigger>
|
||||||
|
<mat-option *ngFor="let category of categories$ | async" value="{{category}}">
|
||||||
|
<svg-icon class="domain-and-category__category-icon" [src]="(getCategoryIcon(category) | async)?.name"></svg-icon>
|
||||||
|
{{category}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- APPLICATION ENTRY POINT -->
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Application entry point</mat-label>
|
||||||
|
<label for="classification-application-entry-point"></label>
|
||||||
|
<input matInput type="text" maxlength="255"
|
||||||
|
id="classification-application-entry-point"
|
||||||
|
placeholder="Application entry point"
|
||||||
|
[(ngModel)]="classification.applicationEntryPoint"
|
||||||
|
name="classification.applicationEntryPoint" #appEntryPoint="ngModel"
|
||||||
|
(input)="validateInputOverflow(appEntryPoint, 255)">
|
||||||
|
</mat-form-field>
|
||||||
|
<div *ngIf="inputOverflowMap.get(appEntryPoint.name)" class="error">{{lengthError}}</div>
|
||||||
|
|
||||||
|
<!-- DESCRIPTION -->
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Description</mat-label>
|
||||||
|
<label for="classification-description"></label>
|
||||||
|
<textarea matInput
|
||||||
|
cdkTextareaAutosize
|
||||||
|
cdkAutosizeMinRows="1"
|
||||||
|
cdkAutosizeMaxRows="5"
|
||||||
|
maxlength="255"
|
||||||
|
id="classification-description" placeholder="Description"
|
||||||
|
[(ngModel)]="classification.description"
|
||||||
|
name="classification.description" #description="ngModel"
|
||||||
|
(input)="validateInputOverflow(description, 255)"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
<div *ngIf="inputOverflowMap.get(description.name)" class="error">{{lengthError}}</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- CUSTOM FIELDS -->
|
<!-- CUSTOM FIELDS -->
|
||||||
<div class="row custom-field-row">
|
<h6 class="classification-details__subheading" style="padding-top: 50px"> Custom Fields </h6>
|
||||||
<div class="custom-classification-form"
|
<mat-divider class="classification-details__horizontal-line"> </mat-divider>
|
||||||
*ngFor="let customField of (customFields$ | async), let i = index"
|
|
||||||
style="width: 50%;">
|
|
||||||
<div *ngIf="customField.visible" class="form-group custom-field-wrapper">
|
<div *ngFor="let customField of (customFields$ | async), let i = index" >
|
||||||
<label for="classification-custom-{{i + 1}}"
|
<div *ngIf="customField.visible">
|
||||||
class="control-label">{{customField.field}}</label>
|
<mat-form-field appearance="outline" style="width: 100%">
|
||||||
<input type="text" maxlength="255" class="form-control"
|
<mat-label>{{customField.field}}</mat-label>
|
||||||
id="classification-custom-{{i + 1}}" placeholder="{{customField.field}}"
|
<label for="classification-custom-{{i + 1}}"></label>
|
||||||
[(ngModel)]="classification[getClassificationCustom(i + 1)]"
|
<input matInput type="text" maxlength="255"
|
||||||
name="classification.custom{{i + 1}}" #custom="ngModel"
|
id="classification-custom-{{i + 1}}" placeholder="{{customField.field}}"
|
||||||
(input)="validateInputOverflow(custom, 255)">
|
[(ngModel)]="classification[getClassificationCustom(i + 1)]"
|
||||||
<div *ngIf="inputOverflowMap.get(custom.name)" class="error">{{lengthError}}</div>
|
name="classification.custom{{i + 1}}" #custom="ngModel"
|
||||||
</div>
|
(input)="validateInputOverflow(custom, 255)">
|
||||||
|
</mat-form-field>
|
||||||
|
<div *ngIf="inputOverflowMap.get(custom.name)" class="error">{{lengthError}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-form>
|
|
||||||
</div>
|
</div>
|
||||||
|
</ng-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,22 +1,114 @@
|
||||||
@import 'src/theme/_colors.scss';
|
@import 'src/theme/_colors.scss';
|
||||||
|
|
||||||
.custom-field-row {
|
.classification-details {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 40vh;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
height: calc(100vh - 100px);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-field-wrapper {
|
.classification-details__wrapper {
|
||||||
height: 70px;
|
position: relative;
|
||||||
padding: 0 15px;
|
|
||||||
}
|
}
|
||||||
.dropdown-menu > li {
|
|
||||||
|
|
||||||
|
/* ACTION TOOLBAR */
|
||||||
|
.classification-details__headline {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.classification-details__action-toolbar {
|
||||||
|
width: calc(100% - 450px);
|
||||||
|
position: fixed;
|
||||||
|
padding: 12px 32px 12px 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-toolbar__button {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
margin-right: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-toolbar__dropdown {
|
||||||
|
border-color: $transparent-grey;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-toolbar__save-button {
|
||||||
|
background-color: $aquamarine;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button__green-blue {
|
||||||
|
color: $aquamarine;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button__red {
|
||||||
|
color: $invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* DETAILED FIELDS */
|
||||||
|
|
||||||
|
.classification__detailed-fields {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-details__subheading {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 15px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-details__horizontal-line {
|
||||||
|
margin: 5px 5px 25px 5px;
|
||||||
|
border-top-color: #555;
|
||||||
|
border-top-width: 1.35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-details__domain-and-category {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-and-category__domain-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
top: 64px;
|
||||||
|
left: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.domain-and-category__domain-checkbox-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.domain-and-category__category-icon {
|
||||||
|
fill: #555;
|
||||||
|
margin-right: 5px;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-details__service-and-priority {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification-details__mat-form-field {
|
||||||
|
width: 70%;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:invalid.dirty {
|
input:invalid.dirty {
|
||||||
border-color: $invalid;
|
border-color: $invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Observable, of } from 'rxjs';
|
||||||
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
|
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
|
||||||
import { DomainService } from '../../../shared/services/domain/domain.service';
|
import { DomainService } from '../../../shared/services/domain/domain.service';
|
||||||
import { ImportExportService } from '../../services/import-export.service';
|
import { ImportExportService } from '../../services/import-export.service';
|
||||||
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
|
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
|
||||||
import { ClassificationState } from '../../../shared/store/classification-store/classification.state';
|
import { ClassificationState } from '../../../shared/store/classification-store/classification.state';
|
||||||
import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state';
|
import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state';
|
||||||
|
@ -13,9 +13,6 @@ import { ClassificationDetailsComponent } from './classification-details.compone
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
|
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
|
||||||
import { FormsValidatorService } from '../../../shared/services/forms-validator/forms-validator.service';
|
import { FormsValidatorService } from '../../../shared/services/forms-validator/forms-validator.service';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
|
||||||
import { NumberPickerComponent } from '../../../shared/components/number-picker/number-picker.component';
|
|
||||||
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
||||||
import {
|
import {
|
||||||
CopyClassification,
|
CopyClassification,
|
||||||
|
@ -24,6 +21,15 @@ import {
|
||||||
SaveCreatedClassification,
|
SaveCreatedClassification,
|
||||||
SaveModifiedClassification
|
SaveModifiedClassification
|
||||||
} from '../../../shared/store/classification-store/classification.actions';
|
} from '../../../shared/store/classification-store/classification.actions';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatOptionModule } from '@angular/material/core';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({ selector: 'taskana-shared-spinner', template: '' })
|
@Component({ selector: 'taskana-shared-spinner', template: '' })
|
||||||
class SpinnerStub {
|
class SpinnerStub {
|
||||||
|
@ -116,15 +122,20 @@ describe('ClassificationDetailsComponent', () => {
|
||||||
imports: [
|
imports: [
|
||||||
NgxsModule.forRoot([ClassificationState, EngineConfigurationState]),
|
NgxsModule.forRoot([ClassificationState, EngineConfigurationState]),
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatSnackBarModule,
|
MatIconModule,
|
||||||
MatDialogModule
|
MatDividerModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatOptionModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatMenuModule,
|
||||||
|
BrowserAnimationsModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ClassificationDetailsComponent,
|
ClassificationDetailsComponent,
|
||||||
SpinnerStub,
|
SpinnerStub,
|
||||||
InputStub,
|
InputStub,
|
||||||
FieldErrorDisplayStub,
|
FieldErrorDisplayStub,
|
||||||
NumberPickerComponent,
|
|
||||||
SvgIconStub,
|
SvgIconStub,
|
||||||
TextareaStub
|
TextareaStub
|
||||||
],
|
],
|
||||||
|
@ -247,7 +258,7 @@ describe('ClassificationDetailsComponent', () => {
|
||||||
component.spinnerIsRunning = true;
|
component.spinnerIsRunning = true;
|
||||||
component.classification = {};
|
component.classification = {};
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(debugElement.nativeElement.querySelector('.classification__menu-bar')).toBeFalsy();
|
expect(debugElement.nativeElement.querySelector('.classification-details__action-toolbar')).toBeFalsy();
|
||||||
expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeFalsy();
|
expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -255,12 +266,12 @@ describe('ClassificationDetailsComponent', () => {
|
||||||
component.spinnerIsRunning = false;
|
component.spinnerIsRunning = false;
|
||||||
component.classification = null;
|
component.classification = null;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(debugElement.nativeElement.querySelector('.classification__menu-bar')).toBeFalsy();
|
expect(debugElement.nativeElement.querySelector('.classification-details__action-toolbar')).toBeFalsy();
|
||||||
expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeFalsy();
|
expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show details when classification exists and spinner is not running', () => {
|
it('should show details when classification exists and spinner is not running', () => {
|
||||||
expect(debugElement.nativeElement.querySelector('.classification__menu-bar')).toBeTruthy();
|
expect(debugElement.nativeElement.querySelector('.classification-details__action-toolbar')).toBeTruthy();
|
||||||
expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeTruthy();
|
expect(debugElement.nativeElement.querySelector('.classification__detailed-fields')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -269,7 +280,7 @@ describe('ClassificationDetailsComponent', () => {
|
||||||
component.classification = { name: 'Recommendation', type: 'DOCUMENT' };
|
component.classification = { name: 'Recommendation', type: 'DOCUMENT' };
|
||||||
component.isCreatingNewClassification = true;
|
component.isCreatingNewClassification = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const headline = debugElement.nativeElement.querySelector('.classification__headline');
|
const headline = debugElement.nativeElement.querySelector('.classification-details__headline');
|
||||||
expect(headline).toBeTruthy();
|
expect(headline).toBeTruthy();
|
||||||
expect(headline.textContent).toContain('Recommendation');
|
expect(headline.textContent).toContain('Recommendation');
|
||||||
expect(headline.textContent).toContain('DOCUMENT');
|
expect(headline.textContent).toContain('DOCUMENT');
|
||||||
|
@ -278,19 +289,22 @@ describe('ClassificationDetailsComponent', () => {
|
||||||
expect(badgeMessage.textContent.trim()).toBe('Creating new classification');
|
expect(badgeMessage.textContent.trim()).toBe('Creating new classification');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call onSubmit() when button is clicked', () => {
|
it('should call onSubmit() when button is clicked', async () => {
|
||||||
const button = debugElement.nativeElement.querySelector('.classification__menu-bar').children[0];
|
const button = debugElement.nativeElement.querySelector('.action-toolbar__save-button');
|
||||||
expect(button).toBeTruthy();
|
expect(button).toBeTruthy();
|
||||||
expect(button.title).toBe('Save');
|
expect(button.textContent).toContain('Save');
|
||||||
|
expect(button.textContent).toContain('save');
|
||||||
component.onSubmit = jest.fn().mockImplementation();
|
component.onSubmit = jest.fn().mockImplementation();
|
||||||
button.click();
|
button.click();
|
||||||
expect(component.onSubmit).toHaveBeenCalled();
|
expect(component.onSubmit).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should restore selected classification when button is clicked', async () => {
|
it('should restore selected classification when button is clicked', async () => {
|
||||||
const button = debugElement.nativeElement.querySelector('.classification__menu-bar').children[1];
|
const button = debugElement.nativeElement.querySelector('.classification-details__action-toolbar').children[1]
|
||||||
|
.children[1];
|
||||||
expect(button).toBeTruthy();
|
expect(button).toBeTruthy();
|
||||||
expect(button.title).toBe('Restore Previous Version');
|
expect(button.textContent).toContain('Undo Changes');
|
||||||
|
expect(button.textContent).toContain('restore');
|
||||||
|
|
||||||
let isActionDispatched = false;
|
let isActionDispatched = false;
|
||||||
actions$.pipe(ofActionDispatched(RestoreSelectedClassification)).subscribe(() => (isActionDispatched = true));
|
actions$.pipe(ofActionDispatched(RestoreSelectedClassification)).subscribe(() => (isActionDispatched = true));
|
||||||
|
@ -298,22 +312,39 @@ describe('ClassificationDetailsComponent', () => {
|
||||||
expect(isActionDispatched).toBe(true);
|
expect(isActionDispatched).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call onCopy() when button is clicked', () => {
|
it('should display button to show more actions', () => {
|
||||||
const button = debugElement.nativeElement.querySelector('.classification__menu-bar').children[2];
|
const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons');
|
||||||
expect(button).toBeTruthy();
|
expect(button).toBeTruthy();
|
||||||
expect(button.title).toBe('Copy');
|
|
||||||
component.onCopy = jest.fn().mockImplementation();
|
|
||||||
button.click();
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const buttonsInDropdown = debugElement.queryAll(By.css('.action-toolbar__dropdown'));
|
||||||
|
expect(buttonsInDropdown.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onCopy() when button is clicked', () => {
|
||||||
|
const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons');
|
||||||
|
expect(button).toBeTruthy();
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const copyButton = debugElement.queryAll(By.css('.action-toolbar__dropdown'))[0];
|
||||||
|
expect(copyButton.nativeElement.textContent).toContain('content_copy');
|
||||||
|
expect(copyButton.nativeElement.textContent).toContain('Copy');
|
||||||
|
component.onCopy = jest.fn().mockImplementation();
|
||||||
|
copyButton.nativeElement.click();
|
||||||
expect(component.onCopy).toHaveBeenCalled();
|
expect(component.onCopy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call onRemoveClassification() when button is clicked', () => {
|
it('should call onRemoveClassification() when button is clicked', () => {
|
||||||
const button = debugElement.nativeElement.querySelector('.classification__menu-bar').children[3];
|
const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons');
|
||||||
expect(button).toBeTruthy();
|
expect(button).toBeTruthy();
|
||||||
expect(button.title).toBe('Delete');
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const deleteButton = debugElement.queryAll(By.css('.action-toolbar__dropdown'))[1];
|
||||||
|
expect(deleteButton.nativeElement.textContent).toContain('delete');
|
||||||
|
expect(deleteButton.nativeElement.textContent).toContain('Delete');
|
||||||
|
|
||||||
const onRemoveClassificationSpy = jest.spyOn(component, 'onRemoveClassification');
|
const onRemoveClassificationSpy = jest.spyOn(component, 'onRemoveClassification');
|
||||||
button.click();
|
deleteButton.nativeElement.click();
|
||||||
expect(onRemoveClassificationSpy).toHaveBeenCalled();
|
expect(onRemoveClassificationSpy).toHaveBeenCalled();
|
||||||
onRemoveClassificationSpy.mockReset();
|
onRemoveClassificationSpy.mockReset();
|
||||||
|
|
||||||
|
@ -323,27 +354,54 @@ describe('ClassificationDetailsComponent', () => {
|
||||||
expect(showDialogSpy).toHaveBeenCalled();
|
expect(showDialogSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call onClose() when button is clicked', () => {
|
||||||
|
const button = debugElement.nativeElement.querySelector('#action-toolbar__more-buttons');
|
||||||
|
expect(button).toBeTruthy();
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const closeButton = debugElement.queryAll(By.css('.action-toolbar__dropdown'))[2];
|
||||||
|
expect(closeButton.nativeElement.textContent).toContain('close');
|
||||||
|
expect(closeButton.nativeElement.textContent).toContain('close');
|
||||||
|
component.onCloseClassification = jest.fn().mockImplementation();
|
||||||
|
closeButton.nativeElement.click();
|
||||||
|
expect(component.onCloseClassification).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
/* DETAILED FIELDS */
|
/* DETAILED FIELDS */
|
||||||
it('should display field-error-display component', () => {
|
it('should display field-error-display component', () => {
|
||||||
expect(debugElement.nativeElement.querySelector('taskana-shared-field-error-display')).toBeTruthy();
|
expect(debugElement.nativeElement.querySelector('taskana-shared-field-error-display')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display number-picker component', () => {
|
it('should display form field for key', () => {
|
||||||
expect(debugElement.nativeElement.querySelector('taskana-shared-number-picker')).toBeTruthy();
|
expect(debugElement.nativeElement.querySelector('#classification-key')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select category when button is clicked', () => {
|
it('should display form field for name', () => {
|
||||||
component.classification.category = 'A';
|
expect(debugElement.nativeElement.querySelector('#classification-name')).toBeTruthy();
|
||||||
component.getAvailableCategories = jest.fn().mockImplementation((type) => of(['B', 'C']));
|
});
|
||||||
fixture.detectChanges();
|
|
||||||
const button = debugElement.nativeElement.querySelector('.detailed-fields__categories');
|
it('should display form field for service level', () => {
|
||||||
expect(button).toBeTruthy();
|
expect(debugElement.nativeElement.querySelector('#classification-service-level')).toBeTruthy();
|
||||||
button.click();
|
});
|
||||||
expect(component.classification.category).toBe('B');
|
|
||||||
|
it('should display form field for priority', () => {
|
||||||
|
expect(debugElement.nativeElement.querySelector('#classification-priority')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display form field for domain', () => {
|
||||||
|
expect(debugElement.nativeElement.querySelector('#classification-domain')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display form field for application entry point', () => {
|
||||||
|
expect(debugElement.nativeElement.querySelector('#classification-application-entry-point')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display form field for description', () => {
|
||||||
|
expect(debugElement.nativeElement.querySelector('#classification-description')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change isValidInDomain when button is clicked', () => {
|
it('should change isValidInDomain when button is clicked', () => {
|
||||||
const button = debugElement.nativeElement.querySelector('.detailed-fields__domain').children[2];
|
const button = debugElement.nativeElement.querySelector('.domain-and-category__domain-checkbox-icon').parentNode;
|
||||||
expect(button).toBeTruthy();
|
expect(button).toBeTruthy();
|
||||||
component.classification.isValidInDomain = false;
|
component.classification.isValidInDomain = false;
|
||||||
button.click();
|
button.click();
|
||||||
|
|
|
@ -28,7 +28,8 @@ import {
|
||||||
RestoreSelectedClassification,
|
RestoreSelectedClassification,
|
||||||
SaveModifiedClassification,
|
SaveModifiedClassification,
|
||||||
SelectClassification,
|
SelectClassification,
|
||||||
CopyClassification
|
CopyClassification,
|
||||||
|
DeselectClassification
|
||||||
} from '../../../shared/store/classification-store/classification.actions';
|
} from '../../../shared/store/classification-store/classification.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -43,7 +44,6 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
|
||||||
@Select(ClassificationSelectors.selectCategories) categories$: Observable<string[]>;
|
@Select(ClassificationSelectors.selectCategories) categories$: Observable<string[]>;
|
||||||
@Select(EngineConfigurationSelectors.selectCategoryIcons) categoryIcons$: Observable<ClassificationCategoryImages>;
|
@Select(EngineConfigurationSelectors.selectCategoryIcons) categoryIcons$: Observable<ClassificationCategoryImages>;
|
||||||
@Select(ClassificationSelectors.selectedClassificationType) selectedClassificationType$: Observable<string>;
|
@Select(ClassificationSelectors.selectedClassificationType) selectedClassificationType$: Observable<string>;
|
||||||
@Select(ClassificationSelectors.selectClassificationTypesObject) classificationTypes$: Observable<CategoriesResponse>;
|
|
||||||
@Select(ClassificationSelectors.selectedClassification) selectedClassification$: Observable<Classification>;
|
@Select(ClassificationSelectors.selectedClassification) selectedClassification$: Observable<Classification>;
|
||||||
@Select(ClassificationSelectors.getBadgeMessage) badgeMessage$: Observable<string>;
|
@Select(ClassificationSelectors.getBadgeMessage) badgeMessage$: Observable<string>;
|
||||||
|
|
||||||
|
@ -127,8 +127,8 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectCategory(category: string) {
|
onCloseClassification() {
|
||||||
this.classification.category = category;
|
this.store.dispatch(new DeselectClassification());
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategoryIcon(category: string): Observable<Pair> {
|
getCategoryIcon(category: string): Observable<Pair> {
|
||||||
|
@ -157,13 +157,6 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
|
||||||
return `custom${customNumber}`;
|
return `custom${customNumber}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailableCategories(type: string): Observable<string[]> {
|
|
||||||
return this.classificationTypes$.pipe(
|
|
||||||
take(1),
|
|
||||||
map((classTypes) => classTypes[type])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onSave() {
|
async onSave() {
|
||||||
this.requestInProgressService.setRequestInProgress(true);
|
this.requestInProgressService.setRequestInProgress(true);
|
||||||
if (typeof this.classification.classificationId === 'undefined') {
|
if (typeof this.classification.classificationId === 'undefined') {
|
||||||
|
|
|
@ -1,63 +1,73 @@
|
||||||
<div class="classification-list-full-height">
|
<div class="classification-list">
|
||||||
|
|
||||||
<!-- ACTION TOOLBAR -->
|
<!-- ACTION TOOLBAR -->
|
||||||
<li class="list-group-item tab-align" id="wb-action-toolbar">
|
<section class="classification-list__action-toolbar">
|
||||||
<div class="row">
|
<div class="classification-list__action-buttons">
|
||||||
<div class="col-xs-6 btn-group">
|
<button mat-flat-button class="action-toolbar__add-button mr-1" matTooltip="Create new classification"
|
||||||
|
(click)="addClassification()">
|
||||||
|
Add
|
||||||
|
<mat-icon class="md-20">add</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-default add-classification-button" type="button" (click)="addClassification()" data-toggle="tooltip" title="Add">
|
<taskana-administration-import-export
|
||||||
<span class="material-icons md-20 green-blue">add_circle_outline</span>
|
class="classification-list__import-export" [currentSelection]="taskanaType.CLASSIFICATIONS">
|
||||||
</button>
|
</taskana-administration-import-export>
|
||||||
|
|
||||||
<taskana-administration-import-export
|
<button mat-stroked-button matTooltip="Display filter options" (click)="displayFilter()">
|
||||||
class ="btn-group" [currentSelection]="taskanaType.CLASSIFICATIONS">
|
<mat-icon *ngIf="!showFilter">search</mat-icon>
|
||||||
</taskana-administration-import-export>
|
<mat-icon *ngIf="showFilter" color="warn">clear</mat-icon>
|
||||||
</div>
|
</button>
|
||||||
<div class="col-xs-6">
|
</div>
|
||||||
<taskana-administration-classification-types-selector
|
|
||||||
class="pull-right">
|
|
||||||
</taskana-administration-classification-types-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- FILTER -->
|
<!-- FILTER -->
|
||||||
<div class="col-xs-2 category-filter">
|
<div class="classification-list__filter" *ngIf="showFilter">
|
||||||
<button class="btn btn-default" data-toggle="dropdown" type="button" id="dropdown-classification-filter" data-toggle="dropdown"
|
<div class="classification-list__category-filter">
|
||||||
aria-haspopup="true" aria-expanded="true">
|
<button mat-icon-button class="mr-2 category-filter__filter-button" [matMenuTriggerFor]="menu"
|
||||||
<svg-icon class="blue selected-category" *ngIf="selectedCategory else category_unselected" [src]="(getCategoryIcon(selectedCategory) | async)?.name"
|
matTooltip="Filter Category">
|
||||||
data-toggle="tooltip"></svg-icon>
|
<mat-icon *ngIf="selectedCategory == ''">filter_list</mat-icon>
|
||||||
<ng-template #category_unselected>
|
<svg-icon class="category-filter__icons" [src]="(getCategoryIcon(selectedCategory) | async)?.name"
|
||||||
<svg-icon data-toggle="tooltip" title="All" class="blue no-selected-category" src="./assets/icons/asterisk.svg"></svg-icon>
|
[title]="(getCategoryIcon(selectedCategory) | async)?.text"
|
||||||
</ng-template>
|
*ngIf="selectedCategory != ''">
|
||||||
</button>
|
</svg-icon>
|
||||||
<ul class="dropdown-menu dropdown-menu-classification" role="menu">
|
</button>
|
||||||
<li>
|
|
||||||
<a class="category-all" type="button" (click)="selectCategory('');" data-toggle="tooltip">
|
|
||||||
<svg-icon class="blue" src="./assets/icons/asterisk.svg"></svg-icon>
|
|
||||||
All
|
|
||||||
</a>
|
|
||||||
<a class="category-list" *ngFor="let category of categories$ | async" type="button" (click)="selectCategory(category);" data-toggle="tooltip">
|
|
||||||
<svg-icon class="blue categories" [src]="(getCategoryIcon(category) | async)?.name" data-toggle="tooltip" [title]="(getCategoryIcon(category) | async)?.text"></svg-icon>
|
|
||||||
{{category}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-8">
|
|
||||||
<input class="filter-input" [ngModel]="inputValue" (ngModelChange)="inputValue = $event" placeholder="Filter classifications">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xs-12 horizontal-bottom-divider"></div>
|
<mat-menu #menu="matMenu">
|
||||||
|
<button mat-menu-item (click)="selectCategory('')" class="category-filter__all-button">
|
||||||
|
<svg-icon class="category-filter__categories pr-2" src="./assets/icons/asterisk.svg"
|
||||||
|
[title]="(getCategoryIcon('all') | async)?.text"></svg-icon>
|
||||||
|
<span>All</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item *ngFor="let category of categories$ | async" (click)="selectCategory(category)">
|
||||||
|
<svg-icon class="category-filter__categories pr-2" [src]="(getCategoryIcon(category) | async)?.name"
|
||||||
|
[title]="(getCategoryIcon(category) | async)?.text"></svg-icon>
|
||||||
|
<span> {{category}} </span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- CLASSIFICATION TREE -->
|
<div class="filter__input">
|
||||||
<taskana-shared-spinner class="col-xs-12" [isRunning]="requestInProgress" positionClass="centered-spinner-whole-screen"></taskana-shared-spinner>
|
<mat-form-field appearance="legacy" floatLabel="auto" class="filter__input-field">
|
||||||
<taskana-administration-tree class="col-xs-12" *ngIf="(classifications && classifications.length) else empty_classifications"
|
<mat-label>Filter classification</mat-label>
|
||||||
[filterText]="inputValue" [filterIcon]="selectedCategory" (switchTaskanaSpinnerEmit)="requestInProgress=$event"></taskana-administration-tree>
|
<input matInput [ngModel]="inputValue" (ngModelChange)="inputValue = $event" matTooltip="Type to filter classifications">
|
||||||
<ng-template #empty_classifications>
|
</mat-form-field>
|
||||||
<div *ngIf="!requestInProgress" class="col-xs-12 container-no-items center-block no-classifications">
|
</div>
|
||||||
<h3 class="grey">There are no classifications</h3>
|
|
||||||
<svg-icon class="img-responsive empty-icon" src="./assets/icons/classification-empty.svg"></svg-icon>
|
<taskana-administration-classification-types-selector
|
||||||
</div>
|
class="pull-right">
|
||||||
</ng-template>
|
</taskana-administration-classification-types-selector>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- CLASSIFICATION TREE -->
|
||||||
|
<taskana-shared-spinner [isRunning]="requestInProgress"
|
||||||
|
positionClass="centered-spinner-whole-screen"></taskana-shared-spinner>
|
||||||
|
<taskana-administration-tree *ngIf="(classifications && classifications.length) else empty_classifications"
|
||||||
|
[filterText]="inputValue" [filterIcon]="selectedCategory"
|
||||||
|
(switchTaskanaSpinnerEmit)="requestInProgress=$event"></taskana-administration-tree>
|
||||||
|
<ng-template #empty_classifications>
|
||||||
|
<div *ngIf="!requestInProgress" class="classification-list__no-items">
|
||||||
|
<h3 class="grey">There are no classifications</h3>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,38 +1,53 @@
|
||||||
.classification-list-full-height {
|
@import 'src/theme/_colors.scss';
|
||||||
|
|
||||||
|
.classification-list {
|
||||||
height: calc(100vh - 55px);
|
height: calc(100vh - 55px);
|
||||||
|
width: 450px;
|
||||||
}
|
}
|
||||||
|
.classification-list__action-toolbar {
|
||||||
.list-group-item {
|
padding: 0 16px;
|
||||||
padding: 5px 0px 2px 1px;
|
min-height: 68px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.classification-list__action-buttons {
|
||||||
|
display: flex;
|
||||||
border: none;
|
border: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 16px 4px 8px 4px;
|
||||||
|
}
|
||||||
|
.action-toolbar__add-button {
|
||||||
|
background-color: $aquamarine;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-align {
|
.classification-list__import-export {
|
||||||
margin-bottom: 0px;
|
display: flex;
|
||||||
border-bottom: 1px dotted #ddd;
|
}
|
||||||
padding: 8px 12px 8px 4px;
|
.classification-list__filter {
|
||||||
& > div {
|
display: flex;
|
||||||
margin: 6px 0px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input.filter-input {
|
.classification-list__category-filter {
|
||||||
border: solid 1px grey;
|
padding-top: 7px;
|
||||||
margin: 10px 2px;
|
}
|
||||||
height: 32px;
|
.category-filter__icons {
|
||||||
|
height: 33px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
.category-filter__categories {
|
||||||
|
fill: #555;
|
||||||
|
margin: 0;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 10px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
.filter__input-field {
|
||||||
.category-filter {
|
width: 100% !important;
|
||||||
margin: 7px 2px;
|
|
||||||
}
|
}
|
||||||
|
.classification-list__no-items {
|
||||||
.dropdown-menu-classification {
|
text-align: center;
|
||||||
margin-left: 15px;
|
padding-top: 150px;
|
||||||
min-width: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.category-filter svg-icon {
|
|
||||||
position: initial;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,12 @@ import { ImportExportService } from '../../services/import-export.service';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { CreateClassification } from '../../../shared/store/classification-store/classification.actions';
|
import { CreateClassification } from '../../../shared/store/classification-store/classification.actions';
|
||||||
import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state';
|
import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({ selector: 'taskana-administration-import-export', template: '' })
|
@Component({ selector: 'taskana-administration-import-export', template: '' })
|
||||||
class ImportExportStub {
|
class ImportExportStub {
|
||||||
|
@ -76,7 +82,14 @@ describe('ClassificationListComponent', () => {
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NgxsModule.forRoot([ClassificationState, EngineConfigurationState])],
|
imports: [
|
||||||
|
NgxsModule.forRoot([ClassificationState, EngineConfigurationState]),
|
||||||
|
MatIconModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
BrowserAnimationsModule
|
||||||
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ClassificationListComponent,
|
ClassificationListComponent,
|
||||||
ClassificationTypesSelectorStub,
|
ClassificationTypesSelectorStub,
|
||||||
|
@ -113,7 +126,7 @@ describe('ClassificationListComponent', () => {
|
||||||
|
|
||||||
/* HTML: ACTION TOOLBAR */
|
/* HTML: ACTION TOOLBAR */
|
||||||
it('should call CreateClassification when add-classification button is clicked', async () => {
|
it('should call CreateClassification when add-classification button is clicked', async () => {
|
||||||
const button = debugElement.nativeElement.querySelector('.add-classification-button');
|
const button = debugElement.nativeElement.querySelector('.action-toolbar__add-button');
|
||||||
expect(button).toBeTruthy();
|
expect(button).toBeTruthy();
|
||||||
let actionDispatched = false;
|
let actionDispatched = false;
|
||||||
actions$.pipe(ofActionDispatched(CreateClassification)).subscribe(() => (actionDispatched = true));
|
actions$.pipe(ofActionDispatched(CreateClassification)).subscribe(() => (actionDispatched = true));
|
||||||
|
@ -125,7 +138,9 @@ describe('ClassificationListComponent', () => {
|
||||||
expect(debugElement.nativeElement.querySelector('taskana-administration-import-export')).toBeTruthy();
|
expect(debugElement.nativeElement.querySelector('taskana-administration-import-export')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display classification-types-selector component', () => {
|
it('should display classification-types-selector component when showFilter is true', () => {
|
||||||
|
component.showFilter = true;
|
||||||
|
fixture.detectChanges();
|
||||||
const typesSelectorComponent = debugElement.nativeElement.querySelector(
|
const typesSelectorComponent = debugElement.nativeElement.querySelector(
|
||||||
'taskana-administration-classification-types-selector'
|
'taskana-administration-classification-types-selector'
|
||||||
);
|
);
|
||||||
|
@ -133,31 +148,43 @@ describe('ClassificationListComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/* HTML: FILTER */
|
/* HTML: FILTER */
|
||||||
it('should display specific icon when selectedCategory is true', () => {
|
it('should display filter input field when showFilter is true', () => {
|
||||||
component.selectedCategory = 'EXTERNAL';
|
component.showFilter = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(debugElement.nativeElement.querySelector('.selected-category')).toBeTruthy();
|
const button = debugElement.nativeElement.querySelector('.filter__input-field');
|
||||||
|
expect(button).toBeTruthy();
|
||||||
|
expect(button.textContent).toBe('Filter classification');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display universal icon for categories when selectedCategory is false', () => {
|
it('should display filter button when showFilter is true', () => {
|
||||||
expect(debugElement.nativeElement.querySelector('svg-icon.no-selected-category')).toBeTruthy();
|
component.showFilter = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const button = debugElement.nativeElement.querySelector('.category-filter__filter-button');
|
||||||
|
expect(button).toBeTruthy();
|
||||||
|
expect(button.textContent).toBe('filter_list');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change selectedCategory property when button is clicked', () => {
|
it('should change selectedCategory property when button is clicked', () => {
|
||||||
|
component.showFilter = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const filterButton = debugElement.nativeElement.querySelector('.category-filter__filter-button');
|
||||||
|
filterButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
component.selectedCategory = 'EXTERNAL';
|
component.selectedCategory = 'EXTERNAL';
|
||||||
const button = debugElement.nativeElement.querySelector('.category-all');
|
const allButton = debugElement.query(By.css('.category-filter__all-button'));
|
||||||
button.click();
|
expect(allButton).toBeTruthy();
|
||||||
|
allButton.nativeElement.click();
|
||||||
expect(component.selectedCategory).toBe('');
|
expect(component.selectedCategory).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display list of categories which can be selected', () => {
|
it('should display list of categories which can be selected', () => {
|
||||||
expect(debugElement.nativeElement.querySelector('.category-all').textContent.trim()).toBe('All');
|
component.showFilter = true;
|
||||||
|
fixture.detectChanges();
|
||||||
const categories = fixture.debugElement.nativeElement.getElementsByClassName('category-list');
|
const filterButton = debugElement.nativeElement.querySelector('.category-filter__filter-button');
|
||||||
expect(categories.length).toBe(3);
|
filterButton.click();
|
||||||
expect(categories[0].textContent.trim()).toBe('EXTERNAL');
|
fixture.detectChanges();
|
||||||
expect(categories[1].textContent.trim()).toBe('MANUAL');
|
const matMenu = debugElement.queryAll(By.css('.category-filter__categories'));
|
||||||
expect(categories[2].textContent.trim()).toBe('AUTOMATIC');
|
expect(matMenu.length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* HTML: CLASSIFICATION TREE */
|
/* HTML: CLASSIFICATION TREE */
|
||||||
|
@ -172,10 +199,9 @@ describe('ClassificationListComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display icon and text when no classifications exist', () => {
|
it('should display icon and text when no classifications exist', () => {
|
||||||
const noClassifications = debugElement.nativeElement.querySelector('.no-classifications');
|
const noClassifications = debugElement.nativeElement.querySelector('.classification-list__no-items');
|
||||||
expect(noClassifications.childNodes.length).toBe(2);
|
expect(noClassifications.childNodes.length).toBe(1);
|
||||||
expect(noClassifications.childNodes[0].textContent).toBe('There are no classifications');
|
expect(noClassifications.childNodes[0].textContent).toBe('There are no classifications');
|
||||||
expect(noClassifications.childNodes[1].tagName).toBe('SVG-ICON');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* TS: getCategoryIcon() */
|
/* TS: getCategoryIcon() */
|
||||||
|
|
|
@ -29,6 +29,7 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
|
||||||
requestInProgress = true;
|
requestInProgress = true;
|
||||||
inputValue: string;
|
inputValue: string;
|
||||||
selectedCategory = '';
|
selectedCategory = '';
|
||||||
|
showFilter = false;
|
||||||
|
|
||||||
@Select(ClassificationSelectors.classificationTypes) classificationTypes$: Observable<string[]>;
|
@Select(ClassificationSelectors.classificationTypes) classificationTypes$: Observable<string[]>;
|
||||||
@Select(ClassificationSelectors.selectedClassificationType) classificationTypeSelected$: Observable<string>;
|
@Select(ClassificationSelectors.selectedClassificationType) classificationTypeSelected$: Observable<string>;
|
||||||
|
@ -85,18 +86,25 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
|
||||||
this.location.go(this.location.path().replace(/(classifications).*/g, 'classifications/new-classification'));
|
this.location.go(this.location.path().replace(/(classifications).*/g, 'classifications/new-classification'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategoryIcon(category: string): Observable<Pair> {
|
||||||
|
return this.categoryIcons$.pipe(
|
||||||
|
map((iconMap) => {
|
||||||
|
if (category === '') {
|
||||||
|
return new Pair(iconMap['all'], 'All');
|
||||||
|
}
|
||||||
|
return iconMap[category]
|
||||||
|
? new Pair(iconMap[category], category)
|
||||||
|
: new Pair(iconMap.missing, 'Category does not match with the configuration');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
selectCategory(category: string) {
|
selectCategory(category: string) {
|
||||||
this.selectedCategory = category;
|
this.selectedCategory = category;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategoryIcon(category: string): Observable<Pair> {
|
displayFilter() {
|
||||||
return this.categoryIcons$.pipe(
|
this.showFilter = !this.showFilter;
|
||||||
map((iconMap) =>
|
|
||||||
iconMap[category]
|
|
||||||
? new Pair(iconMap[category], category)
|
|
||||||
: new Pair(iconMap.missing, 'Category does not match with the configuration')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
<div class="no-gutter">
|
<div class="classification-overview">
|
||||||
<div class="col-xs-12 col-md-4 vertical-right-divider">
|
|
||||||
<taskana-administration-classification-list></taskana-administration-classification-list>
|
<taskana-administration-classification-list></taskana-administration-classification-list>
|
||||||
</div>
|
<div class="vertical-right-divider"></div>
|
||||||
<div class="col-xs-12 col-md-8" *ngIf="showDetail; else showEmptyPage">
|
<taskana-administration-classification-details *ngIf="showDetail; else showEmptyPage"></taskana-administration-classification-details>
|
||||||
<taskana-administration-classification-details></taskana-administration-classification-details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #showEmptyPage>
|
<ng-template #showEmptyPage>
|
||||||
<div class="hidden-xs hidden-sm col-md-8 container-no-items">
|
<div class="hidden-xs hidden-sm col-md-8 container-no-items">
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
.classification-overview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
taskana-administration-classification-details {
|
||||||
|
width: 100%
|
||||||
|
}
|
|
@ -1,17 +1,8 @@
|
||||||
<div class="dropdown clearfix btn-group">
|
<mat-form-field class="types-selector">
|
||||||
<button type="button" class="btn btn-default selected-type"> {{classificationTypeSelected$ | async}}</button>
|
<mat-label>Type</mat-label>
|
||||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
|
<mat-select [value]="classificationTypeSelected$ | async" class="types-selector__selected-type" matTooltip="Change type">
|
||||||
aria-expanded="false">
|
<mat-option class="types-selector__options" *ngFor="let classificationType of classificationTypes$ | async" [value]="classificationType" (click)="select(classificationType)">
|
||||||
<span class="caret"></span>
|
{{ classificationType }}
|
||||||
<span class="sr-only">Toggle Dropdown</span>
|
</mat-option>
|
||||||
</button>
|
</mat-select>
|
||||||
<div class="dropdown-menu dropdown-menu-right popup" aria-labelledby="sortingDropdown">
|
</mat-form-field>
|
||||||
<mat-radio-group name="classificationTypeSelector" color="accent" class="radio-group">
|
|
||||||
<mat-radio-button class="classification-types" *ngFor="let classificationType of classificationTypes$ | async"
|
|
||||||
name="classificationTypeSelector" id="select-{{classificationType}}" [checked]="classificationType === (classificationTypeSelected$ | async)"
|
|
||||||
(change)="select(classificationType)" [value]="classificationType">
|
|
||||||
{{classificationType}}
|
|
||||||
</mat-radio-button>
|
|
||||||
</mat-radio-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
mat-form-field {
|
||||||
|
max-width: 120px !important;
|
||||||
|
}
|
||||||
.radio-group {
|
.radio-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -3,12 +3,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
import { NgxsModule, Store } from '@ngxs/store';
|
import { NgxsModule, Store } from '@ngxs/store';
|
||||||
import { ClassificationState } from '../../../shared/store/classification-store/classification.state';
|
import { ClassificationState } from '../../../shared/store/classification-store/classification.state';
|
||||||
import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
|
|
||||||
import { ClassificationsService } from '../../../shared/services/classifications/classifications.service';
|
import { ClassificationsService } from '../../../shared/services/classifications/classifications.service';
|
||||||
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
|
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
|
||||||
import { DomainService } from '../../../shared/services/domain/domain.service';
|
import { DomainService } from '../../../shared/services/domain/domain.service';
|
||||||
import { MatRippleModule } from '@angular/material/core';
|
|
||||||
import { classificationStateMock } from '../../../shared/store/mock-data/mock-store';
|
import { classificationStateMock } from '../../../shared/store/mock-data/mock-store';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
const classificationServiceSpy = jest.fn();
|
const classificationServiceSpy = jest.fn();
|
||||||
const classificationCategoriesServiceSpy = jest.fn();
|
const classificationCategoriesServiceSpy = jest.fn();
|
||||||
|
@ -17,13 +19,18 @@ const domainServiceSpy = jest.fn();
|
||||||
describe('ClassificationTypesSelectorComponent', () => {
|
describe('ClassificationTypesSelectorComponent', () => {
|
||||||
let fixture: ComponentFixture<ClassificationTypesSelectorComponent>;
|
let fixture: ComponentFixture<ClassificationTypesSelectorComponent>;
|
||||||
let debugElement: DebugElement;
|
let debugElement: DebugElement;
|
||||||
let app: ClassificationTypesSelectorComponent;
|
let component: ClassificationTypesSelectorComponent;
|
||||||
let store: Store;
|
let store: Store;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NgxsModule.forRoot([ClassificationState]), MatRippleModule],
|
imports: [
|
||||||
declarations: [ClassificationTypesSelectorComponent, MatRadioButton, MatRadioGroup],
|
NgxsModule.forRoot([ClassificationState]),
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
BrowserAnimationsModule
|
||||||
|
],
|
||||||
|
declarations: [ClassificationTypesSelectorComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ClassificationsService, useClass: classificationServiceSpy },
|
{ provide: ClassificationsService, useClass: classificationServiceSpy },
|
||||||
{ provide: ClassificationCategoriesService, useClass: classificationCategoriesServiceSpy },
|
{ provide: ClassificationCategoriesService, useClass: classificationCategoriesServiceSpy },
|
||||||
|
@ -33,7 +40,7 @@ describe('ClassificationTypesSelectorComponent', () => {
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ClassificationTypesSelectorComponent);
|
fixture = TestBed.createComponent(ClassificationTypesSelectorComponent);
|
||||||
debugElement = fixture.debugElement;
|
debugElement = fixture.debugElement;
|
||||||
app = fixture.debugElement.componentInstance;
|
component = fixture.debugElement.componentInstance;
|
||||||
store = TestBed.inject(Store);
|
store = TestBed.inject(Store);
|
||||||
store.reset({
|
store.reset({
|
||||||
...store.snapshot(),
|
...store.snapshot(),
|
||||||
|
@ -43,18 +50,28 @@ describe('ClassificationTypesSelectorComponent', () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should create the app', () => {
|
it('should create the app', () => {
|
||||||
expect(app).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display form-field for types-selector', () => {
|
||||||
|
const button = debugElement.nativeElement.getElementsByClassName('types-selector');
|
||||||
|
expect(button).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display selected classification type', () => {
|
it('should display selected classification type', () => {
|
||||||
const button = fixture.debugElement.nativeElement.getElementsByClassName('selected-type');
|
fixture.detectChanges();
|
||||||
expect(button[0].textContent.trim()).toBe('DOCUMENT');
|
const button = debugElement.nativeElement.querySelector('.types-selector__selected-type');
|
||||||
|
expect(button.textContent).toBe('DOCUMENT');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display list of classification types', () => {
|
it('should display dropdown with 2 objects', () => {
|
||||||
const radioButtons = fixture.debugElement.nativeElement.getElementsByClassName('classification-types');
|
const dropdownButton = debugElement.nativeElement.querySelector('.types-selector__selected-type');
|
||||||
expect(radioButtons.length).toBe(2);
|
expect(dropdownButton).toBeTruthy();
|
||||||
expect(radioButtons[0].textContent.trim()).toBe('TASK');
|
dropdownButton.click();
|
||||||
expect(radioButtons[1].textContent.trim()).toBe('DOCUMENT');
|
fixture.detectChanges();
|
||||||
|
const options = debugElement.queryAll(By.css('.types-selector__options'));
|
||||||
|
expect(options.length).toBe(2);
|
||||||
|
expect(options[0].nativeElement.textContent.trim()).toBe('TASK');
|
||||||
|
expect(options[1].nativeElement.textContent.trim()).toBe('DOCUMENT');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
|
|
||||||
<button type="button" [ngClass]="{disabled: uploadservice?.isInUse}" (click)="selectedFile.click()" data-toggle="tooltip" title="Import" class="btn btn-default">
|
<button mat-stroked-button class="mr-1" matTooltip="Import classification" [ngClass]="{disabled: uploadService?.isInUse}" (click)="selectedFile.click()" title="Import">
|
||||||
<span class="material-icons md-20 green-blue">cloud_upload</span>
|
Import
|
||||||
|
<mat-icon>cloud_upload</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<form class="hidden" id="upload_form" enctype="multipart/form-data" method="post">
|
|
||||||
|
<form class="hidden" enctype="multipart/form-data" method="post">
|
||||||
<input #selectedFile type="file" accept=".json" (change)="uploadFile()" class="hide" />
|
<input #selectedFile type="file" accept=".json" (change)="uploadFile()" class="hide" />
|
||||||
</form>
|
</form>
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li>
|
<button mat-stroked-button class="mr-1" matTooltip="Export classification" [matMenuTriggerFor]="menu" [ngClass]="{disabled: uploadService?.isInUse}" title="Export">
|
||||||
<a href="javascript:void(0)" class="dropdown-item" (click)="export()">
|
Export
|
||||||
<label>All Domains</label>
|
<mat-icon>cloud_download</mat-icon>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<div role="separator" class="divider"></div>
|
|
||||||
<li *ngFor="let domain of domains">
|
|
||||||
<a href="javascript:void(0)" class="dropdown-item" (click)="export(domain)">
|
|
||||||
<label>{{domain === '' ? 'Master' : domain}}</label>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<button [ngClass]="{disabled: uploadservice?.isInUse}" type="button" data-toggle="dropdown" title="Export" class="btn btn-default">
|
|
||||||
<span class="material-icons md-20 red">cloud_download</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
<mat-menu #menu="matMenu">
|
||||||
|
<button mat-menu-item href="javascript:void(0)" (click)="export()">
|
||||||
|
All Domains
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item *ngFor="let domain of domains" href="javascript:void(0)" (click)="export(domain)">
|
||||||
|
{{domain === '' ? 'Master' : domain}}
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
.hide {
|
.hide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
mat-icon {
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
color: #555
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class ImportExportComponent implements OnInit {
|
||||||
private workbasketDefinitionService: WorkbasketDefinitionService,
|
private workbasketDefinitionService: WorkbasketDefinitionService,
|
||||||
private classificationDefinitionService: ClassificationDefinitionService,
|
private classificationDefinitionService: ClassificationDefinitionService,
|
||||||
private notificationsService: NotificationService,
|
private notificationsService: NotificationService,
|
||||||
public uploadservice: UploadService,
|
public uploadService: UploadService,
|
||||||
private errorsService: NotificationService,
|
private errorsService: NotificationService,
|
||||||
private importExportService: ImportExportService
|
private importExportService: ImportExportService
|
||||||
) {}
|
) {}
|
||||||
|
@ -67,14 +67,14 @@ export class ImportExportComponent implements OnInit {
|
||||||
ajax.setRequestHeader('Authorization', 'Basic YWRtaW46YWRtaW4=');
|
ajax.setRequestHeader('Authorization', 'Basic YWRtaW46YWRtaW4=');
|
||||||
}
|
}
|
||||||
ajax.send(formdata);
|
ajax.send(formdata);
|
||||||
this.uploadservice.isInUse = true;
|
this.uploadService.isInUse = true;
|
||||||
this.uploadservice.setCurrentProgressValue(1);
|
this.uploadService.setCurrentProgressValue(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progressHandler(event) {
|
progressHandler(event) {
|
||||||
const percent = (event.loaded / event.total) * 100;
|
const percent = (event.loaded / event.total) * 100;
|
||||||
this.uploadservice.setCurrentProgressValue(Math.round(percent));
|
this.uploadService.setCurrentProgressValue(Math.round(percent));
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkFormatFile(file): boolean {
|
private checkFormatFile(file): boolean {
|
||||||
|
@ -90,8 +90,8 @@ export class ImportExportComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetProgress() {
|
private resetProgress() {
|
||||||
this.uploadservice.setCurrentProgressValue(0);
|
this.uploadService.setCurrentProgressValue(0);
|
||||||
this.uploadservice.isInUse = false;
|
this.uploadService.isInUse = false;
|
||||||
this.selectedFileInput.nativeElement.value = '';
|
this.selectedFileInput.nativeElement.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
(moveNode)="onMoveNode($event)" (treeDrop)="onDrop($event)">
|
(moveNode)="onMoveNode($event)" (treeDrop)="onDrop($event)">
|
||||||
<ng-template #treeNodeTemplate let-node let-index="index">
|
<ng-template #treeNodeTemplate let-node let-index="index">
|
||||||
<span class="text-top">
|
<span class="text-top">
|
||||||
<svg-icon *ngIf="node.data.category" class="blue fa-fw" [src]="(getCategoryIcon(node.data.category) | async)?.name" data-toggle="tooltip"
|
<svg-icon *ngIf="node.data.category" class="fa-fw pr-1" [src]="(getCategoryIcon(node.data.category) | async)?.name" data-toggle="tooltip"
|
||||||
[title]="(getCategoryIcon(node.data.category) | async)?.text"></svg-icon>
|
[title]="(getCategoryIcon(node.data.category) | async)?.text"></svg-icon>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg-icon.blue.fa-fw {
|
svg-icon {
|
||||||
position: initial;
|
fill: #555;
|
||||||
|
top: -5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="container-scrollable">
|
<div class="container container-scrollable" style="min-width: 70vw;">
|
||||||
<taskana-shared-spinner [isRunning]="requestInProgress"></taskana-shared-spinner>
|
<taskana-shared-spinner [isRunning]="requestInProgress"></taskana-shared-spinner>
|
||||||
<div id="workbasket-details" *ngIf="workbasket && !requestInProgress">
|
<div id="workbasket-details" *ngIf="workbasket && !requestInProgress">
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="footer-space-pagination-list">
|
<div class="workbasket-list footer-space-pagination-list">
|
||||||
<div #wbToolbar>
|
<div #wbToolbar>
|
||||||
<taskana-administration-workbasket-list-toolbar [workbaskets]="workbasketsSummary$ | async" (performFilter)="performFilter($event)"
|
<taskana-administration-workbasket-list-toolbar [workbaskets]="workbasketsSummary$ | async" (performFilter)="performFilter($event)"
|
||||||
(performSorting)="performSorting($event)" [workbasketDefaultSortBy]="workbasketDefaultSortBy">
|
(performSorting)="performSorting($event)" [workbasketDefaultSortBy]="workbasketDefaultSortBy">
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
.workbasket-list {
|
||||||
|
min-width: 30vw;
|
||||||
|
}
|
||||||
.row.list-group {
|
.row.list-group {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<div class="no-gutter">
|
<div class="workbasket-overview">
|
||||||
<div class="col-xs-12 col-md-4 vertical-right-divider">
|
<div class="vertical-right-divider">
|
||||||
<taskana-administration-workbasket-list></taskana-administration-workbasket-list>
|
<taskana-administration-workbasket-list></taskana-administration-workbasket-list>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-8" *ngIf="showDetail; else showEmptyPage">
|
<div *ngIf="showDetail; else showEmptyPage">
|
||||||
<taskana-administration-workbasket-details></taskana-administration-workbasket-details>
|
<taskana-administration-workbasket-details></taskana-administration-workbasket-details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
.workbasket-overview {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from '../../../shared/store/workbasket-store/workbasket.actions';
|
} from '../../../shared/store/workbasket-store/workbasket.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-workbasket-overview',
|
selector: 'taskana-administration-workbasket-overview',
|
||||||
templateUrl: './workbasket-overview.component.html',
|
templateUrl: './workbasket-overview.component.html',
|
||||||
styleUrls: ['./workbasket-overview.component.scss']
|
styleUrls: ['./workbasket-overview.component.scss']
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<taskana-shared-nav-bar></taskana-shared-nav-bar>
|
<taskana-shared-nav-bar></taskana-shared-nav-bar>
|
||||||
<div (window:resize)="onResize()" class="container-fluid container-main">
|
|
||||||
<div class="row ">
|
<div (window:resize)="onResize()" class="">
|
||||||
|
<div class="taskana-main">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
<taskana-shared-spinner [isRunning]="requestInProgress" isModal=true></taskana-shared-spinner>
|
<taskana-shared-spinner [isRunning]="requestInProgress" isModal=true></taskana-shared-spinner>
|
||||||
<taskana-shared-progress-bar [hidden]="currentProgressValue === 0" currentValue={{currentProgressValue}}></taskana-shared-progress-bar>
|
<taskana-shared-progress-bar [hidden]="currentProgressValue === 0" currentValue={{currentProgressValue}}></taskana-shared-progress-bar>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="no-gutter">
|
<div class="master-detail row row-no-gutters">
|
||||||
<div class="{{showDetail? 'col-md-4 hidden-xs hidden-sm':'col-xs-12 col-md-4'}} vertical-right-divider">
|
<div class="{{showDetail? 'col-md-4 hidden-xs hidden-sm':'col-xs-12 col-md-4'}} vertical-right-divider">
|
||||||
<router-outlet name="master"></router-outlet>
|
<router-outlet name="master"></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
|
||||||
|
.master-detail {
|
||||||
|
min-width: 100vw;
|
||||||
|
}
|
||||||
.center-block.no-detail {
|
.center-block.no-detail {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-inverse {
|
.navbar-inverse {
|
||||||
|
top: 0;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: $dark-green;
|
background-color: $dark-green;
|
||||||
box-shadow: 0px 1px 5px -1px black;
|
box-shadow: 0px 1px 5px -1px black;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { TestBed, async } from '@angular/core/testing';
|
import { TestBed, async } from '@angular/core/testing';
|
||||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
import { Customisation, CustomisationContent } from 'app/shared/models/customisation';
|
import { Customisation, CustomisationContent } from 'app/shared/models/customisation';
|
||||||
import { ClassificationCategoriesService, missingIcon } from './classification-categories.service';
|
import { asteriskIcon, ClassificationCategoriesService, missingIcon } from './classification-categories.service';
|
||||||
|
|
||||||
describe('ClassificationCategoriesService', () => {
|
describe('ClassificationCategoriesService', () => {
|
||||||
let categoryService: ClassificationCategoriesService;
|
let categoryService: ClassificationCategoriesService;
|
||||||
|
@ -19,7 +19,7 @@ describe('ClassificationCategoriesService', () => {
|
||||||
|
|
||||||
it('should insert missing icon into customisation', async(() => {
|
it('should insert missing icon into customisation', async(() => {
|
||||||
const expectedCustomisationContent: CustomisationContent = {
|
const expectedCustomisationContent: CustomisationContent = {
|
||||||
classifications: { categories: { missing: missingIcon } }
|
classifications: { categories: { all: asteriskIcon, missing: missingIcon } }
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedCustomisation: Customisation = { EN: expectedCustomisationContent, DE: expectedCustomisationContent };
|
const expectedCustomisation: Customisation = { EN: expectedCustomisationContent, DE: expectedCustomisationContent };
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Customisation } from '../../models/customisation';
|
||||||
const customisationUrl = 'environments/data-sources/taskana-customization.json';
|
const customisationUrl = 'environments/data-sources/taskana-customization.json';
|
||||||
|
|
||||||
export const missingIcon = 'assets/icons/categories/missing-icon.svg';
|
export const missingIcon = 'assets/icons/categories/missing-icon.svg';
|
||||||
|
export const asteriskIcon = './assets/icons/asterisk.svg';
|
||||||
|
|
||||||
export interface CategoriesResponse {
|
export interface CategoriesResponse {
|
||||||
[key: string]: string[];
|
[key: string]: string[];
|
||||||
|
@ -29,11 +30,12 @@ export class ClassificationCategoriesService {
|
||||||
Object.keys(customisation).forEach((lang) => {
|
Object.keys(customisation).forEach((lang) => {
|
||||||
if (customisation[lang]?.classifications?.categories) {
|
if (customisation[lang]?.classifications?.categories) {
|
||||||
customisation[lang].classifications.categories.missing = missingIcon;
|
customisation[lang].classifications.categories.missing = missingIcon;
|
||||||
|
customisation[lang].classifications.categories.all = asteriskIcon;
|
||||||
} else {
|
} else {
|
||||||
if (customisation[lang]?.classifications) {
|
if (customisation[lang]?.classifications) {
|
||||||
customisation[lang].classifications.categories = { missing: missingIcon };
|
customisation[lang].classifications.categories = { missing: missingIcon, all: asteriskIcon };
|
||||||
} else {
|
} else {
|
||||||
customisation[lang].classifications = { categories: { missing: missingIcon } };
|
customisation[lang].classifications = { categories: { missing: missingIcon, all: asteriskIcon } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,6 +52,8 @@ import { HttpClientInterceptor } from './services/http-client-interceptor/http-c
|
||||||
import { AccessIdsService } from './services/access-ids/access-ids.service';
|
import { AccessIdsService } from './services/access-ids/access-ids.service';
|
||||||
import { ToastComponent } from './components/toast/toast.component';
|
import { ToastComponent } from './components/toast/toast.component';
|
||||||
import { DialogPopUpComponent } from './components/popup/dialog-pop-up.component';
|
import { DialogPopUpComponent } from './components/popup/dialog-pop-up.component';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -97,7 +99,7 @@ const DECLARATIONS = [
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: DECLARATIONS,
|
declarations: DECLARATIONS,
|
||||||
imports: [MODULES, MatRadioModule],
|
imports: [MODULES, MatRadioModule, MatFormFieldModule, MatInputModule],
|
||||||
exports: DECLARATIONS,
|
exports: DECLARATIONS,
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,11 +19,6 @@ export class ClassificationSelectors {
|
||||||
return state.classificationTypes[state.selectedClassificationType];
|
return state.classificationTypes[state.selectedClassificationType];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Selector([ClassificationState])
|
|
||||||
static selectClassificationTypesObject(state: ClassificationStateModel): CategoriesResponse {
|
|
||||||
return state.classificationTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Selector([ClassificationState])
|
@Selector([ClassificationState])
|
||||||
static classifications(state: ClassificationStateModel): Classification[] {
|
static classifications(state: ClassificationStateModel): Classification[] {
|
||||||
return state.classifications;
|
return state.classifications;
|
||||||
|
|
|
@ -145,7 +145,7 @@ svg-icon.fa-fw > svg {
|
||||||
|
|
||||||
svg-icon {
|
svg-icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 4px;
|
top: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-default > .panel-heading .badge.warning {
|
.panel-default > .panel-heading .badge.warning {
|
||||||
|
@ -483,8 +483,18 @@ li.list-group-item:hover {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-content-wrapper {
|
||||||
|
margin: 0 18px !important;
|
||||||
|
}
|
||||||
/* our header has a z-index of 1031 and the default z-index of the
|
/* our header has a z-index of 1031 and the default z-index of the
|
||||||
mat-dialog is just 1000 which leads to undesirable overlap*/
|
mat-dialog is just 1000 which leads to undesirable overlap*/
|
||||||
.cdk-overlay-container {
|
.cdk-overlay-container {
|
||||||
z-index: 1337;
|
z-index: 1337;
|
||||||
}
|
}
|
||||||
|
.mat-menu-content {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
.mat-select-value-text {
|
||||||
|
color: #4a5568 !important;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue