TSK-1725, TSK-1740: Settings Component, Workbasket-Priority-Report Filter
This commit is contained in:
parent
79742782cb
commit
ecdf333c8e
|
@ -1,100 +1,64 @@
|
||||||
{
|
{
|
||||||
"nameHighPrio": "High Prio",
|
"nameHighPriority": "High Priority",
|
||||||
"nameMediumPrio": "Medium P",
|
"nameMediumPriority": "Medium Priority",
|
||||||
"nameLowPrio": "Low Prio",
|
"nameLowPriority": "Low Priority",
|
||||||
"intervalHighPrio": [
|
"intervalHighPriority": [3, 300],
|
||||||
100,
|
"intervalMediumPriority": [2, 2],
|
||||||
200
|
"intervalLowPriority": [0, 1],
|
||||||
],
|
"colorHighPriority": "#FF0000",
|
||||||
"intervalMediumPrio": [
|
"colorLowPriority": "#5FAD00",
|
||||||
21,
|
"colorMediumPriority": "#FFD700",
|
||||||
100
|
"filter": "{ \"Tasks with state READY\": { \"state\": [\"READY\"]}, \"Tasks with state CLAIMED\": {\"state\": [\"CLAIMED\"] }}",
|
||||||
],
|
|
||||||
"intervalLowPrio": [
|
|
||||||
1,
|
|
||||||
20
|
|
||||||
],
|
|
||||||
"colorHighPrio": "#ff0000",
|
|
||||||
"colorLowPrio": "#5FAD00",
|
|
||||||
"colorMediumPrio": "#FFEE00",
|
|
||||||
"filter": {
|
|
||||||
"Filter": {
|
|
||||||
"custom-1": [
|
|
||||||
"true"
|
|
||||||
],
|
|
||||||
"custom-5": [
|
|
||||||
"3"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Filter neu": {
|
|
||||||
"custom-5": [
|
|
||||||
"7",
|
|
||||||
"5"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Filter Doppelt": {
|
|
||||||
"custom-5": [
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
"90"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schema": {
|
"schema": {
|
||||||
"Priority-Report": {
|
"Monitor Workbasket-Priority-Report": {
|
||||||
"displayName": "Priority Report",
|
"displayName": "Monitor Workbasket-Priority-Report",
|
||||||
"members": {
|
"members": {
|
||||||
"nameHighPrio": {
|
"nameHighPriority": {
|
||||||
"displayName": "High Prio Name",
|
"displayName": "High Priority Name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"min": 1
|
"max": 32
|
||||||
},
|
},
|
||||||
"nameMediumPrio": {
|
"nameMediumPriority": {
|
||||||
"displayName": "Medium Prio Name",
|
"displayName": "Medium Priority Name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"min": 1,
|
"max": 32
|
||||||
"max": 8
|
|
||||||
},
|
},
|
||||||
"nameLowPrio": {
|
"nameLowPriority": {
|
||||||
"displayName": "Low Prio Name",
|
"displayName": "Low Priority Name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"min": 2,
|
"max": 32
|
||||||
"max": 64
|
|
||||||
},
|
},
|
||||||
"intervalHighPrio": {
|
"intervalHighPriority": {
|
||||||
"displayName": "High Prio Interval",
|
"displayName": "High Priority Interval",
|
||||||
"type": "interval",
|
|
||||||
"min": 0,
|
|
||||||
"max": 300
|
|
||||||
},
|
|
||||||
"intervalMediumPrio": {
|
|
||||||
"displayName": "Medium Prio Interval",
|
|
||||||
"type": "interval",
|
|
||||||
"min": -5,
|
|
||||||
"max": 300
|
|
||||||
},
|
|
||||||
"intervalLowPrio": {
|
|
||||||
"displayName": "Low Prio Interval",
|
|
||||||
"type": "interval",
|
"type": "interval",
|
||||||
"min": 0
|
"min": 0
|
||||||
},
|
},
|
||||||
"colorHighPrio": {
|
"intervalMediumPriority": {
|
||||||
"displayName": "High Prio Color",
|
"displayName": "Medium Priority Interval",
|
||||||
|
"type": "interval",
|
||||||
|
"min": 0
|
||||||
|
},
|
||||||
|
"intervalLowPriority": {
|
||||||
|
"displayName": "Low Priority Interval",
|
||||||
|
"type": "interval",
|
||||||
|
"min": 0
|
||||||
|
},
|
||||||
|
"colorHighPriority": {
|
||||||
|
"displayName": "High Priority Color",
|
||||||
"type": "color"
|
"type": "color"
|
||||||
},
|
},
|
||||||
"colorMediumPrio": {
|
"colorMediumPriority": {
|
||||||
"displayName": "Medium Prio Color",
|
"displayName": "Medium Priority Color",
|
||||||
"type": "color"
|
"type": "color"
|
||||||
},
|
},
|
||||||
"colorLowPrio": {
|
"colorLowPriority": {
|
||||||
"displayName": "Low Prio Color",
|
"displayName": "Low Priority Color",
|
||||||
"type": "color"
|
"type": "color"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Filter": {
|
"Monitor Workbasket-Priority-Report-Filter": {
|
||||||
"displayName": "Filter for Task-Priority-Report",
|
"displayName": "Monitor Workbasket-Priority-Report-Filter",
|
||||||
"members": {
|
|
||||||
"filter": {
|
"filter": {
|
||||||
"displayName": "Filter values",
|
"displayName": "Filter values",
|
||||||
"type": "json",
|
"type": "json",
|
||||||
|
|
|
@ -39,6 +39,11 @@ const appRoutes: Routes = [
|
||||||
path: 'administration',
|
path: 'administration',
|
||||||
redirectTo: 'administration/workbaskets'
|
redirectTo: 'administration/workbaskets'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
canActivate: [BusinessAdminGuard],
|
||||||
|
path: 'settings',
|
||||||
|
loadChildren: () => import('./settings/settings.module').then((m) => m.SettingsModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
redirectTo: 'workplace'
|
redirectTo: 'workplace'
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
import { NgxsModule } from '@ngxs/store';
|
import { NgxsModule, Store } from '@ngxs/store';
|
||||||
import { CanvasComponent } from './canvas.component';
|
import { CanvasComponent } from './canvas.component';
|
||||||
import { workbasketReportMock } from '../task-priority-report/monitor-mock-data';
|
import { workbasketReportMock } from '../task-priority-report/monitor-mock-data';
|
||||||
|
import { SettingsState } from '../../../shared/store/settings-store/settings.state';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
import { settingsStateMock } from '../../../shared/store/mock-data/mock-store';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
|
||||||
describe('CanvasComponent', () => {
|
describe('CanvasComponent', () => {
|
||||||
let fixture: ComponentFixture<CanvasComponent>;
|
let fixture: ComponentFixture<CanvasComponent>;
|
||||||
|
@ -12,7 +16,7 @@ describe('CanvasComponent', () => {
|
||||||
beforeEach(
|
beforeEach(
|
||||||
waitForAsync(() => {
|
waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NgxsModule.forRoot([])],
|
imports: [NgxsModule.forRoot([SettingsState]), HttpClientTestingModule, MatDialogModule],
|
||||||
declarations: [CanvasComponent]
|
declarations: [CanvasComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
@ -20,6 +24,11 @@ describe('CanvasComponent', () => {
|
||||||
debugElement = fixture.debugElement;
|
debugElement = fixture.debugElement;
|
||||||
component = fixture.debugElement.componentInstance;
|
component = fixture.debugElement.componentInstance;
|
||||||
component.id = '1';
|
component.id = '1';
|
||||||
|
const store: Store = TestBed.inject(Store);
|
||||||
|
store.reset({
|
||||||
|
...store.snapshot(),
|
||||||
|
settings: settingsStateMock
|
||||||
|
});
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,18 +1,47 @@
|
||||||
import { AfterViewInit, Component, Input } from '@angular/core';
|
import { AfterViewInit, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
import { priorityTypes } from '../../models/priority';
|
|
||||||
|
|
||||||
import { ReportRow } from '../../models/report-row';
|
import { ReportRow } from '../../models/report-row';
|
||||||
|
import { Select } from '@ngxs/store';
|
||||||
|
import { SettingsSelectors } from '../../../shared/store/settings-store/settings.selectors';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { Settings } from '../../../settings/models/settings';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
import { SettingMembers } from '../../../settings/components/Settings/expected-members';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'taskana-monitor-canvas',
|
selector: 'taskana-monitor-canvas',
|
||||||
templateUrl: './canvas.component.html',
|
templateUrl: './canvas.component.html',
|
||||||
styleUrls: ['./canvas.component.scss']
|
styleUrls: ['./canvas.component.scss']
|
||||||
})
|
})
|
||||||
export class CanvasComponent implements AfterViewInit {
|
export class CanvasComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@Input() row: ReportRow;
|
@Input() row: ReportRow;
|
||||||
@Input() id: string;
|
@Input() id: string;
|
||||||
@Input() isReversed: boolean;
|
|
||||||
|
labels: string[];
|
||||||
|
colors: string[];
|
||||||
|
destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@Select(SettingsSelectors.getSettings)
|
||||||
|
settings$: Observable<Settings>;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.settings$.pipe(takeUntil(this.destroy$)).subscribe((settings) => {
|
||||||
|
this.setValuesFromSettings(settings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setValuesFromSettings(settings: Settings) {
|
||||||
|
this.labels = [
|
||||||
|
settings[SettingMembers.nameHighPriority],
|
||||||
|
settings[SettingMembers.nameMediumPriority],
|
||||||
|
settings[SettingMembers.nameLowPriority]
|
||||||
|
];
|
||||||
|
this.colors = [
|
||||||
|
settings[SettingMembers.colorHighPriority],
|
||||||
|
settings[SettingMembers.colorMediumPriority],
|
||||||
|
settings[SettingMembers.colorLowPriority]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
const canvas = document.getElementById(this.id) as HTMLCanvasElement;
|
const canvas = document.getElementById(this.id) as HTMLCanvasElement;
|
||||||
|
@ -26,13 +55,12 @@ export class CanvasComponent implements AfterViewInit {
|
||||||
new Chart(canvas, {
|
new Chart(canvas, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
labels: [priorityTypes.HIGH, priorityTypes.MEDIUM, priorityTypes.LOW],
|
labels: this.labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Tasks by Priority',
|
label: 'Tasks by Priority',
|
||||||
// depends on whether backend sends data sorted in ascending or descending order
|
data: row.cells,
|
||||||
data: this.isReversed ? row.cells.reverse() : row.cells,
|
backgroundColor: this.colors,
|
||||||
backgroundColor: ['red', 'gold', 'limegreen'],
|
|
||||||
borderWidth: 0
|
borderWidth: 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -49,4 +77,11 @@ export class CanvasComponent implements AfterViewInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
document.getElementById(this.id).outerHTML = ''; // destroy HTML element
|
||||||
|
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<mat-accordion>
|
||||||
|
<mat-expansion-panel (opened)="isPanelOpen = true"
|
||||||
|
(closed)="isPanelOpen = false">
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Task Filter
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<div *ngFor="let key of keys">
|
||||||
|
<mat-checkbox (change)="emitFilter($event.checked, key)">{{key}}</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!filtersAreSpecified"> There are not filters specified. </div>
|
||||||
|
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</mat-accordion>
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { NgxsModule, Store } from '@ngxs/store';
|
||||||
|
import { settingsStateMock } from '../../../shared/store/mock-data/mock-store';
|
||||||
|
import { SettingsState } from '../../../shared/store/settings-store/settings.state';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
import { TaskPriorityReportFilterComponent } from './task-priority-report-filter.component';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
describe('TaskPriorityReportFilterComponent', () => {
|
||||||
|
let fixture: ComponentFixture<TaskPriorityReportFilterComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
let component: TaskPriorityReportFilterComponent;
|
||||||
|
|
||||||
|
beforeEach(
|
||||||
|
waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgxsModule.forRoot([SettingsState]),
|
||||||
|
HttpClientTestingModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatExpansionModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
MatDialogModule
|
||||||
|
],
|
||||||
|
declarations: [TaskPriorityReportFilterComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TaskPriorityReportFilterComponent);
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
component = fixture.debugElement.componentInstance;
|
||||||
|
const store: Store = TestBed.inject(Store);
|
||||||
|
store.reset({
|
||||||
|
...store.snapshot(),
|
||||||
|
settings: settingsStateMock
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should create the component', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append filter name to activeFilters list when it is selected', () => {
|
||||||
|
component.activeFilters = ['Tasks with state READY'];
|
||||||
|
component.emitFilter(true, 'Tasks with state CLAIMED');
|
||||||
|
expect(component.activeFilters).toStrictEqual(['Tasks with state READY', 'Tasks with state CLAIMED']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove filter name from list when it is not selected anymore', () => {
|
||||||
|
component.activeFilters = ['Tasks with state READY', 'Tasks with state CLAIMED'];
|
||||||
|
component.emitFilter(false, 'Tasks with state CLAIMED');
|
||||||
|
expect(component.activeFilters).toStrictEqual(['Tasks with state READY']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit query according to values in activeFilters', () => {
|
||||||
|
const emitSpy = jest.spyOn(component.applyFilter, 'emit');
|
||||||
|
component.activeFilters = ['Tasks with state READY'];
|
||||||
|
component.emitFilter(true, 'Tasks with state CLAIMED');
|
||||||
|
expect(emitSpy).toBeCalledWith({ state: ['READY', 'CLAIMED'] });
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
import { Select } from '@ngxs/store';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { SettingsSelectors } from '../../../shared/store/settings-store/settings.selectors';
|
||||||
|
import { Settings } from '../../../settings/models/settings';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'taskana-monitor-task-priority-report-filter',
|
||||||
|
templateUrl: './task-priority-report-filter.component.html',
|
||||||
|
styleUrls: ['./task-priority-report-filter.component.scss']
|
||||||
|
})
|
||||||
|
export class TaskPriorityReportFilterComponent implements OnInit, OnDestroy {
|
||||||
|
isPanelOpen = false;
|
||||||
|
filters: {}[];
|
||||||
|
keys: string[];
|
||||||
|
activeFilters = [];
|
||||||
|
filtersAreSpecified: boolean = true;
|
||||||
|
destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@Output() applyFilter = new EventEmitter<Object>();
|
||||||
|
|
||||||
|
@Select(SettingsSelectors.getSettings)
|
||||||
|
settings$: Observable<Settings>;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.settings$.pipe(takeUntil(this.destroy$)).subscribe((settings) => {
|
||||||
|
this.filtersAreSpecified = settings['filter'] && settings['filter'] !== '';
|
||||||
|
if (this.filtersAreSpecified) {
|
||||||
|
this.filters = JSON.parse(settings['filter']);
|
||||||
|
this.keys = Object.keys(this.filters);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emitFilter(isEnabled: boolean, key: string) {
|
||||||
|
this.activeFilters = isEnabled
|
||||||
|
? [...this.activeFilters, key]
|
||||||
|
: this.activeFilters.filter((element) => element !== key);
|
||||||
|
|
||||||
|
this.applyFilter.emit(this.buildQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
buildQuery(): {} {
|
||||||
|
let filterQuery = {};
|
||||||
|
this.activeFilters.forEach((activeFilter) => {
|
||||||
|
const filter = this.filters[activeFilter];
|
||||||
|
const keys = Object.keys(filter);
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const newValue = filter[key];
|
||||||
|
filterQuery[key] = filterQuery[key] ? [...filterQuery[key], ...newValue] : newValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return filterQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,15 +34,3 @@ export const workbasketReportMock: ReportData = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const workbasketReportUnexpectedHeaderMock: ReportData = {
|
|
||||||
meta: {
|
|
||||||
name: 'WorkbasketPriorityReport',
|
|
||||||
date: '2021-08-24T11:44:34.023901Z',
|
|
||||||
header: ['<10', '11 - 100', '>101'],
|
|
||||||
rowDesc: ['WORKBASKET'],
|
|
||||||
sumRowDesc: 'Total'
|
|
||||||
},
|
|
||||||
rows: [],
|
|
||||||
sumRow: []
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
<div *ngIf="reportData" class="task-priority-report">
|
<div *ngIf="reportData" class="task-priority-report">
|
||||||
<h4> {{reportData?.meta.name}} ({{reportData?.meta.date | germanTimeFormat }}) </h4>
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<div class="task-priority-report__header">
|
||||||
|
<h4 class="task-priority-report__headline"> {{reportData?.meta.name}} ({{reportData?.meta.date | germanTimeFormat }}) </h4>
|
||||||
|
<taskana-monitor-task-priority-report-filter (applyFilter)="applyFilter($event)"> </taskana-monitor-task-priority-report-filter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-divider class="task-priority-report__divider"> </mat-divider>
|
||||||
|
|
||||||
<div *ngIf="reportData?.rows.length == 0"> There are no Workbaskets with type TOPIC. </div>
|
<div *ngIf="reportData?.rows.length == 0"> There are no Workbaskets with type TOPIC. </div>
|
||||||
|
|
||||||
|
@ -7,12 +14,12 @@
|
||||||
<div *ngFor="let row of reportData?.rows; let i = index" class="task-priority-report__workbasket">
|
<div *ngFor="let row of reportData?.rows; let i = index" class="task-priority-report__workbasket">
|
||||||
|
|
||||||
<!-- WORKBASKET NAME -->
|
<!-- WORKBASKET NAME -->
|
||||||
<div class="task-priority-report__headline">
|
<div class="task-priority-report__workbasket-headline">
|
||||||
<h6> {{row.desc[0]}} </h6>
|
<h6> {{row.desc[0]}} </h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CHART -->
|
<!-- CHART -->
|
||||||
<taskana-monitor-canvas [row]="row" [id]="toString(i)" [isReversed]="isReversed"> </taskana-monitor-canvas>
|
<taskana-monitor-canvas [row]="row" [id]="indexToString(i)"> </taskana-monitor-canvas>
|
||||||
|
|
||||||
<!-- TABLE -->
|
<!-- TABLE -->
|
||||||
<div>
|
<div>
|
||||||
|
@ -22,9 +29,9 @@
|
||||||
<ng-container matColumnDef="priority">
|
<ng-container matColumnDef="priority">
|
||||||
<th mat-header-cell *matHeaderCellDef> Priority </th>
|
<th mat-header-cell *matHeaderCellDef> Priority </th>
|
||||||
<td mat-cell *matCellDef="let element" [ngClass]="{
|
<td mat-cell *matCellDef="let element" [ngClass]="{
|
||||||
'task-priority-report__row--high': element.priority == 'High',
|
'task-priority-report__row--high': element.priority == nameHighPriority,
|
||||||
'task-priority-report__row--medium': element.priority == 'Medium',
|
'task-priority-report__row--medium': element.priority == nameMediumPriority,
|
||||||
'task-priority-report__row--low': element.priority == 'Low'}">
|
'task-priority-report__row--low': element.priority == nameLowPriority}">
|
||||||
{{element.priority}} </td>
|
{{element.priority}} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -32,9 +39,9 @@
|
||||||
<ng-container matColumnDef="number">
|
<ng-container matColumnDef="number">
|
||||||
<th mat-header-cell *matHeaderCellDef> Number of Tasks </th>
|
<th mat-header-cell *matHeaderCellDef> Number of Tasks </th>
|
||||||
<td mat-cell *matCellDef="let element" [ngClass]="{
|
<td mat-cell *matCellDef="let element" [ngClass]="{
|
||||||
'task-priority-report__row--high': element.priority == 'High',
|
'task-priority-report__row--high': element.priority == nameHighPriority,
|
||||||
'task-priority-report__row--medium': element.priority == 'Medium',
|
'task-priority-report__row--medium': element.priority == nameMediumPriority,
|
||||||
'task-priority-report__row--low': element.priority == 'Low'}">
|
'task-priority-report__row--low': element.priority == nameLowPriority}">
|
||||||
{{element.number}} </td>
|
{{element.number}} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,22 @@ table {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: calc(100vh - 104px);
|
height: calc(100vh - 104px);
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 4px;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__headline {
|
||||||
|
margin: 0 0 0 8px;
|
||||||
|
position: relative;
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
&__workbaskets {
|
&__workbaskets {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -20,21 +36,8 @@ table {
|
||||||
margin: 24px 24px 36px 24px;
|
margin: 24px 24px 36px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__headline {
|
&__workbasket-headline {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__row--low {
|
|
||||||
color: limegreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__row--medium {
|
|
||||||
color: gold;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__row--high {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { Component, DebugElement, Input, Pipe, PipeTransform } from '@angular/core';
|
import { Component, DebugElement, Input, Pipe, PipeTransform } from '@angular/core';
|
||||||
import { NgxsModule } from '@ngxs/store';
|
import { NgxsModule, Store } from '@ngxs/store';
|
||||||
import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service';
|
|
||||||
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
||||||
import { TaskPriorityReportComponent } from './task-priority-report.component';
|
import { TaskPriorityReportComponent } from './task-priority-report.component';
|
||||||
import { MonitorService } from '../../services/monitor.service';
|
import { MonitorService } from '../../services/monitor.service';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { priorityTypes } from '../../models/priority';
|
import { workbasketReportMock } from './monitor-mock-data';
|
||||||
import { workbasketReportMock, workbasketReportUnexpectedHeaderMock } from './monitor-mock-data';
|
import { settingsStateMock } from '../../../shared/store/mock-data/mock-store';
|
||||||
|
import { SettingsState } from '../../../shared/store/settings-store/settings.state';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
|
||||||
@Pipe({ name: 'germanTimeFormat' })
|
@Pipe({ name: 'germanTimeFormat' })
|
||||||
class GermanTimeFormatPipe implements PipeTransform {
|
class GermanTimeFormatPipe implements PipeTransform {
|
||||||
|
@ -24,14 +26,13 @@ class CanvasStub {
|
||||||
@Input() isReversed;
|
@Input() isReversed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({ selector: 'taskana-monitor-task-priority-report-filter', template: '' })
|
||||||
|
class TaskPriorityReportFilterStub {}
|
||||||
|
|
||||||
const monitorServiceSpy: Partial<MonitorService> = {
|
const monitorServiceSpy: Partial<MonitorService> = {
|
||||||
getTasksByPriorityReport: jest.fn().mockReturnValue(of(workbasketReportMock))
|
getTasksByPriorityReport: jest.fn().mockReturnValue(of(workbasketReportMock))
|
||||||
};
|
};
|
||||||
|
|
||||||
const monitorServiceWithDifferentDataSpy: Partial<MonitorService> = {
|
|
||||||
getTasksByPriorityReport: jest.fn().mockReturnValue(of(workbasketReportUnexpectedHeaderMock))
|
|
||||||
};
|
|
||||||
|
|
||||||
const notificationServiceSpy: Partial<NotificationService> = {
|
const notificationServiceSpy: Partial<NotificationService> = {
|
||||||
showWarning: jest.fn()
|
showWarning: jest.fn()
|
||||||
};
|
};
|
||||||
|
@ -44,8 +45,8 @@ describe('TaskPriorityReportComponent', () => {
|
||||||
beforeEach(
|
beforeEach(
|
||||||
waitForAsync(() => {
|
waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NgxsModule.forRoot([]), MatTableModule],
|
imports: [NgxsModule.forRoot([SettingsState]), MatTableModule, HttpClientTestingModule, MatDividerModule],
|
||||||
declarations: [TaskPriorityReportComponent, GermanTimeFormatPipe, CanvasStub],
|
declarations: [TaskPriorityReportComponent, GermanTimeFormatPipe, CanvasStub, TaskPriorityReportFilterStub],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: MonitorService, useValue: monitorServiceSpy },
|
{ provide: MonitorService, useValue: monitorServiceSpy },
|
||||||
{ provide: NotificationService, useValue: notificationServiceSpy }
|
{ provide: NotificationService, useValue: notificationServiceSpy }
|
||||||
|
@ -55,6 +56,11 @@ describe('TaskPriorityReportComponent', () => {
|
||||||
fixture = TestBed.createComponent(TaskPriorityReportComponent);
|
fixture = TestBed.createComponent(TaskPriorityReportComponent);
|
||||||
debugElement = fixture.debugElement;
|
debugElement = fixture.debugElement;
|
||||||
component = fixture.debugElement.componentInstance;
|
component = fixture.debugElement.componentInstance;
|
||||||
|
const store: Store = TestBed.inject(Store);
|
||||||
|
store.reset({
|
||||||
|
...store.snapshot(),
|
||||||
|
settings: settingsStateMock
|
||||||
|
});
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -78,56 +84,4 @@ describe('TaskPriorityReportComponent', () => {
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
expect(showWarningSpy).toHaveBeenCalledTimes(0);
|
expect(showWarningSpy).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set isReserved to true when high priority has a higher index than low priority', () => {
|
|
||||||
expect(component.isReversed).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set tableDataArray', () => {
|
|
||||||
const expectedTableData = [
|
|
||||||
[
|
|
||||||
{ priority: priorityTypes.HIGH, number: 0 },
|
|
||||||
{ priority: priorityTypes.MEDIUM, number: 0 },
|
|
||||||
{ priority: priorityTypes.LOW, number: 5 },
|
|
||||||
{ priority: 'Total', number: 5 }
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ priority: priorityTypes.HIGH, number: 2 },
|
|
||||||
{ priority: priorityTypes.MEDIUM, number: 5 },
|
|
||||||
{ priority: priorityTypes.LOW, number: 3 },
|
|
||||||
{ priority: 'Total', number: 10 }
|
|
||||||
]
|
|
||||||
];
|
|
||||||
expect(component.tableDataArray).toStrictEqual(expectedTableData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('TaskPriorityReportComponent with report data containing an unexpected header', () => {
|
|
||||||
let fixture: ComponentFixture<TaskPriorityReportComponent>;
|
|
||||||
let debugElement: DebugElement;
|
|
||||||
let component: TaskPriorityReportComponent;
|
|
||||||
|
|
||||||
beforeEach(
|
|
||||||
waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [NgxsModule.forRoot([]), MatTableModule],
|
|
||||||
declarations: [TaskPriorityReportComponent, GermanTimeFormatPipe, CanvasStub],
|
|
||||||
providers: [
|
|
||||||
WorkbasketService,
|
|
||||||
{ provide: MonitorService, useValue: monitorServiceWithDifferentDataSpy },
|
|
||||||
{ provide: NotificationService, useValue: notificationServiceSpy }
|
|
||||||
]
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(TaskPriorityReportComponent);
|
|
||||||
debugElement = fixture.debugElement;
|
|
||||||
component = fixture.debugElement.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
it('should show warning when actual header does not match the expected header', () => {
|
|
||||||
const showWarningSpy = jest.spyOn(notificationServiceSpy, 'showWarning');
|
|
||||||
expect(showWarningSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,52 +1,131 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { AfterViewChecked, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { priorityTypes } from '../../models/priority';
|
|
||||||
import { ReportData } from '../../models/report-data';
|
import { ReportData } from '../../models/report-data';
|
||||||
import { MonitorService } from '../../services/monitor.service';
|
import { MonitorService } from '../../services/monitor.service';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
||||||
import { WorkbasketType } from '../../../shared/models/workbasket-type';
|
import { WorkbasketType } from '../../../shared/models/workbasket-type';
|
||||||
|
import { Select } from '@ngxs/store';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { SettingsSelectors } from '../../../shared/store/settings-store/settings.selectors';
|
||||||
|
import { Settings } from '../../../settings/models/settings';
|
||||||
|
import { mergeMap, take, takeUntil } from 'rxjs/operators';
|
||||||
|
import { SettingMembers } from '../../../settings/components/Settings/expected-members';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'taskana-monitor-task-priority-report',
|
selector: 'taskana-monitor-task-priority-report',
|
||||||
templateUrl: './task-priority-report.component.html',
|
templateUrl: './task-priority-report.component.html',
|
||||||
styleUrls: ['./task-priority-report.component.scss']
|
styleUrls: ['./task-priority-report.component.scss']
|
||||||
})
|
})
|
||||||
export class TaskPriorityReportComponent implements OnInit {
|
export class TaskPriorityReportComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||||
columns: string[] = ['priority', 'number'];
|
columns: string[] = ['priority', 'number'];
|
||||||
reportData: ReportData;
|
reportData: ReportData;
|
||||||
tableDataArray: { priority: string; number: number }[][] = [];
|
tableDataArray: { priority: string; number: number }[][] = [];
|
||||||
isReversed = true;
|
colorShouldChange = true;
|
||||||
|
priority = [];
|
||||||
|
|
||||||
|
nameHighPriority: string;
|
||||||
|
nameMediumPriority: string;
|
||||||
|
nameLowPriority: string;
|
||||||
|
colorHighPriority: string;
|
||||||
|
colorMediumPriority: string;
|
||||||
|
colorLowPriority: string;
|
||||||
|
|
||||||
|
destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@Select(SettingsSelectors.getSettings)
|
||||||
|
settings$: Observable<Settings>;
|
||||||
|
|
||||||
constructor(private monitorService: MonitorService, private notificationService: NotificationService) {}
|
constructor(private monitorService: MonitorService, private notificationService: NotificationService) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.monitorService
|
this.settings$
|
||||||
.getTasksByPriorityReport([WorkbasketType.TOPIC])
|
.pipe(
|
||||||
.pipe(take(1))
|
takeUntil(this.destroy$),
|
||||||
|
mergeMap((settings) => {
|
||||||
|
this.setValuesFromSettings(settings);
|
||||||
|
// the order must be high, medium, low because the canvas component defines its labels in this order
|
||||||
|
this.priority = [
|
||||||
|
settings[SettingMembers.intervalHighPriority],
|
||||||
|
settings[SettingMembers.intervalMediumPriority],
|
||||||
|
settings[SettingMembers.intervalLowPriority]
|
||||||
|
].map((arr) => ({ lowerBound: arr[0], upperBound: arr[1] }));
|
||||||
|
return this.monitorService.getTasksByPriorityReport([WorkbasketType.TOPIC], this.priority);
|
||||||
|
})
|
||||||
|
)
|
||||||
.subscribe((reportData) => {
|
.subscribe((reportData) => {
|
||||||
this.reportData = reportData;
|
this.setValuesFromReportData(reportData);
|
||||||
let indexHigh = reportData.meta.header.indexOf('>501');
|
|
||||||
let indexMedium = reportData.meta.header.indexOf('250 - 500');
|
|
||||||
let indexLow = reportData.meta.header.indexOf('<249');
|
|
||||||
if (indexHigh == -1 || indexMedium == -1 || indexLow == -1) {
|
|
||||||
this.notificationService.showWarning('REPORT_DATA_WRONG_HEADER');
|
|
||||||
indexHigh = 2;
|
|
||||||
indexMedium = 1;
|
|
||||||
indexLow = 0;
|
|
||||||
}
|
|
||||||
this.isReversed = indexHigh > indexLow;
|
|
||||||
reportData.rows.forEach((row) => {
|
|
||||||
this.tableDataArray.push([
|
|
||||||
{ priority: priorityTypes.HIGH, number: row.cells[indexHigh] },
|
|
||||||
{ priority: priorityTypes.MEDIUM, number: row.cells[indexMedium] },
|
|
||||||
{ priority: priorityTypes.LOW, number: row.cells[indexLow] },
|
|
||||||
{ priority: 'Total', number: row.total }
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(i: number): string {
|
ngAfterViewChecked() {
|
||||||
|
if (this.colorShouldChange) {
|
||||||
|
const highPriorityElements = document.getElementsByClassName('task-priority-report__row--high');
|
||||||
|
if (highPriorityElements.length > 0) {
|
||||||
|
this.colorShouldChange = false;
|
||||||
|
this.changeColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValuesFromSettings(settings: Settings) {
|
||||||
|
this.nameHighPriority = settings[SettingMembers.nameHighPriority];
|
||||||
|
this.nameMediumPriority = settings[SettingMembers.nameMediumPriority];
|
||||||
|
this.nameLowPriority = settings[SettingMembers.nameLowPriority];
|
||||||
|
this.colorHighPriority = settings[SettingMembers.colorHighPriority];
|
||||||
|
this.colorMediumPriority = settings[SettingMembers.colorMediumPriority];
|
||||||
|
this.colorLowPriority = settings[SettingMembers.colorLowPriority];
|
||||||
|
}
|
||||||
|
|
||||||
|
setValuesFromReportData(reportData) {
|
||||||
|
this.reportData = reportData;
|
||||||
|
|
||||||
|
// the order must be high, medium, low because the canvas component defines its labels in this order
|
||||||
|
let indexHigh = 0;
|
||||||
|
let indexMedium = 1;
|
||||||
|
let indexLow = 2;
|
||||||
|
|
||||||
|
this.tableDataArray = [];
|
||||||
|
reportData.rows.forEach((row) => {
|
||||||
|
this.tableDataArray.push([
|
||||||
|
{ priority: this.nameHighPriority, number: row.cells[indexHigh] },
|
||||||
|
{ priority: this.nameMediumPriority, number: row.cells[indexMedium] },
|
||||||
|
{ priority: this.nameLowPriority, number: row.cells[indexLow] },
|
||||||
|
{ priority: 'Total', number: row.total }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeColor() {
|
||||||
|
const highPriorityElements = document.getElementsByClassName('task-priority-report__row--high');
|
||||||
|
const mediumPriorityElements = document.getElementsByClassName('task-priority-report__row--medium');
|
||||||
|
const lowPriorityElements = document.getElementsByClassName('task-priority-report__row--low');
|
||||||
|
this.applyColorOnClasses(highPriorityElements, this.colorHighPriority);
|
||||||
|
this.applyColorOnClasses(mediumPriorityElements, this.colorMediumPriority);
|
||||||
|
this.applyColorOnClasses(lowPriorityElements, this.colorLowPriority);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyColorOnClasses(elements: HTMLCollectionOf<Element>, color: string) {
|
||||||
|
for (let i = 0; i < elements.length; i++) {
|
||||||
|
(<HTMLElement>elements[i]).style.color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indexToString(i: number): string {
|
||||||
return String(i);
|
return String(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyFilter(filter: {}) {
|
||||||
|
this.monitorService
|
||||||
|
.getTasksByPriorityReport([WorkbasketType.TOPIC], this.priority, filter)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((reportData) => {
|
||||||
|
this.colorShouldChange = true;
|
||||||
|
this.setValuesFromReportData(reportData);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { WorkbasketReportPlannedDateComponent } from './components/workbasket-re
|
||||||
import { WorkbasketReportDueDateComponent } from './components/workbasket-report-due-date/workbasket-report-due-date.component';
|
import { WorkbasketReportDueDateComponent } from './components/workbasket-report-due-date/workbasket-report-due-date.component';
|
||||||
import { TaskPriorityReportComponent } from './components/task-priority-report/task-priority-report.component';
|
import { TaskPriorityReportComponent } from './components/task-priority-report/task-priority-report.component';
|
||||||
import { CanvasComponent } from './components/canvas/canvas.component';
|
import { CanvasComponent } from './components/canvas/canvas.component';
|
||||||
|
import { TaskPriorityReportFilterComponent } from './components/task-priority-report-filter/task-priority-report-filter.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Services
|
* Services
|
||||||
|
@ -35,6 +36,9 @@ import { MonitorService } from './services/monitor.service';
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -48,12 +52,16 @@ const MODULES = [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatTableModule
|
MatTableModule,
|
||||||
|
MatExpansionModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatDividerModule
|
||||||
];
|
];
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
ReportTableComponent,
|
ReportTableComponent,
|
||||||
MonitorComponent,
|
MonitorComponent,
|
||||||
TaskPriorityReportComponent,
|
TaskPriorityReportComponent,
|
||||||
|
TaskPriorityReportFilterComponent,
|
||||||
CanvasComponent,
|
CanvasComponent,
|
||||||
TimestampReportComponent,
|
TimestampReportComponent,
|
||||||
WorkbasketReportComponent,
|
WorkbasketReportComponent,
|
||||||
|
|
|
@ -59,10 +59,13 @@ export class MonitorService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTasksByPriorityReport(type: WorkbasketType[] = []): Observable<ReportData> {
|
getTasksByPriorityReport(type: string[], priority: any[], customFilters: {} = {}): Observable<ReportData> {
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
'workbasket-type': type
|
'workbasket-type': type,
|
||||||
|
columnHeader: priority,
|
||||||
|
...customFilters
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.httpClient.get<ReportData>(
|
return this.httpClient.get<ReportData>(
|
||||||
`${environment.taskanaRestUrl + monitorUrl}workbasket-priority-report${asUrlQueryString(queryParams)}`
|
`${environment.taskanaRestUrl + monitorUrl}workbasket-priority-report${asUrlQueryString(queryParams)}`
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export enum SettingMembers {
|
||||||
|
nameHighPriority = 'nameHighPriority',
|
||||||
|
nameMediumPriority = 'nameMediumPriority',
|
||||||
|
nameLowPriority = 'nameLowPriority',
|
||||||
|
colorHighPriority = 'colorHighPriority',
|
||||||
|
colorMediumPriority = 'colorMediumPriority',
|
||||||
|
colorLowPriority = 'colorLowPriority',
|
||||||
|
intervalHighPriority = 'intervalHighPriority',
|
||||||
|
intervalMediumPriority = 'intervalMediumPriority',
|
||||||
|
intervalLowPriority = 'intervalLowPriority'
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
<div class="settings">
|
||||||
|
|
||||||
|
<!-- BUTTONS -->
|
||||||
|
<div class="settings__buttons">
|
||||||
|
<button mat-button class="settings__button--primary" matTooltip="Save settings" (click)="onSave()">
|
||||||
|
Save
|
||||||
|
<mat-icon class="md-20">save</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button mat-stroked-button class="settings__button--secondary" matTooltip="Revert changes" (click)="onReset()">
|
||||||
|
Undo changes
|
||||||
|
<mat-icon class="settings__icon md-20">restore</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings__content">
|
||||||
|
|
||||||
|
<div *ngFor="let group of groups; let outerIndex = index">
|
||||||
|
<h4 class="settings__domain-name"> {{settings.schema[group].displayName}} </h4>
|
||||||
|
<div *ngFor="let member of members[outerIndex]; let innerIndex = index">
|
||||||
|
|
||||||
|
<!-- STRING -->
|
||||||
|
<div *ngIf="getMember(group,member).type == settingTypes.TEXT" class="settings__grid">
|
||||||
|
<span> {{getMember(group,member).displayName}} </span>
|
||||||
|
<mat-form-field appearance="outline" class="settings__grid--two-columns">
|
||||||
|
<mat-label class="{{member}}">{{getMember(group,member).displayName}}</mat-label>
|
||||||
|
<input matInput type="text" placeholder="{{getMember(group,member).displayName}}" [(ngModel)]="settings[member]"
|
||||||
|
minlength="{{settings.schema[group].members[member].min}}" maxlength="{{settings.schema[group].members[member].max}}">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INTERVAL -->
|
||||||
|
<div *ngIf="getMember(group,member).type == settingTypes.INTERVAL" class="settings__grid">
|
||||||
|
<span>{{getMember(group,member).displayName}}</span>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" >
|
||||||
|
<mat-label class="{{member}}">Lower boundary</mat-label>
|
||||||
|
<input matInput type="number" placeholder="Lower boundary" [(ngModel)]="settings[member][0]"
|
||||||
|
min="{{getMember(group,member).min}}" max="{{getMember(group,member).max}}">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label class="{{member}}">Upper boundary</mat-label>
|
||||||
|
<input matInput type="number" placeholder="Upper boundary" [(ngModel)]="settings[member][1]"
|
||||||
|
min="{{getMember(group,member).min}}" max="{{getMember(group,member).max}}">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- COLOR -->
|
||||||
|
<div *ngIf="getMember(group,member).type == settingTypes.COLOR" class="settings__grid settings__color">
|
||||||
|
<span>{{getMember(group,member).displayName}}</span>
|
||||||
|
<input matInput value="{{settings[member]}}" type="color" id="{{member}}"
|
||||||
|
(change)="onColorChange(member)" class="settings__colors--input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JSON -->
|
||||||
|
<div *ngIf="getMember(group,member).type == settingTypes.JSON" class="settings__grid">
|
||||||
|
<span>{{getMember(group,member).displayName}}</span>
|
||||||
|
<mat-form-field appearance="outline" class="settings__grid--two-columns">
|
||||||
|
<mat-label class="{{member}}">
|
||||||
|
{{getMember(group,member).displayName}}
|
||||||
|
</mat-label>
|
||||||
|
<textarea matInput cdkTextareaAutosize cdkAutosizeMinRows="1" cdkAutosizeMaxRows="10"
|
||||||
|
placeholder="{{getMember(group,member).displayName}}" [(ngModel)]="settings[member]"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="settings__spacer"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,66 @@
|
||||||
|
@import 'src/theme/_colors.scss';
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
height: calc(100vh - 116px);
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__domain-name {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button--primary {
|
||||||
|
background-color: $aquamarine;
|
||||||
|
color: white;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button--secondary {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spacer {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 4px;
|
||||||
|
grid-template-columns: 300px 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid--center {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid--two-columns {
|
||||||
|
grid-column: 2 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__color {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__colors--input {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
color: $aquamarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .ng-invalid.ng-touched:not(form) {
|
||||||
|
box-shadow: unset;
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { SettingsState } from '../../../shared/store/settings-store/settings.state';
|
||||||
|
import { SettingsComponent } from './settings.component';
|
||||||
|
import { settingsStateMock } from '../../../shared/store/mock-data/mock-store';
|
||||||
|
import { SetSettings } from '../../../shared/store/settings-store/settings.actions';
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
const notificationServiceSpy: Partial<NotificationService> = {
|
||||||
|
showError: jest.fn(),
|
||||||
|
showSuccess: jest.fn(),
|
||||||
|
showDialog: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('SettingsComponent', () => {
|
||||||
|
let fixture: ComponentFixture<SettingsComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
let component: SettingsComponent;
|
||||||
|
let store: Store;
|
||||||
|
let actions$: Observable<any>;
|
||||||
|
|
||||||
|
beforeEach(
|
||||||
|
waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgxsModule.forRoot([SettingsState]),
|
||||||
|
HttpClientTestingModule,
|
||||||
|
FormsModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
BrowserAnimationsModule
|
||||||
|
],
|
||||||
|
declarations: [SettingsComponent],
|
||||||
|
providers: [{ provide: NotificationService, useValue: notificationServiceSpy }]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SettingsComponent);
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
component = fixture.debugElement.componentInstance;
|
||||||
|
store = TestBed.inject(Store);
|
||||||
|
actions$ = TestBed.inject(Actions);
|
||||||
|
store.reset({
|
||||||
|
...store.snapshot(),
|
||||||
|
settings: settingsStateMock
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should create component', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show success when form is saved successfully', () => {
|
||||||
|
const showSuccessSpy = jest.spyOn(notificationServiceSpy, 'showSuccess');
|
||||||
|
component.onSave();
|
||||||
|
expect(showSuccessSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error when an invalid form is tried to be saved', () => {
|
||||||
|
component.settings['intervalHighPriority'] = [-100, 100];
|
||||||
|
const showErrorSpy = jest.spyOn(notificationServiceSpy, 'showError');
|
||||||
|
component.onSave();
|
||||||
|
expect(showErrorSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch action onValidate() returns true', async () => {
|
||||||
|
let isActionDispatched = false;
|
||||||
|
actions$.pipe(ofActionDispatched(SetSettings)).subscribe(() => (isActionDispatched = true));
|
||||||
|
component.onSave();
|
||||||
|
expect(isActionDispatched).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { Settings, SettingsMember, SettingTypes } from '../../models/settings';
|
||||||
|
import { Select, Store } from '@ngxs/store';
|
||||||
|
import { NotificationService } from '../../../shared/services/notifications/notification.service';
|
||||||
|
import { SetSettings } from '../../../shared/store/settings-store/settings.actions';
|
||||||
|
import { SettingsSelectors } from '../../../shared/store/settings-store/settings.selectors';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
import { validateForm } from './settings.validators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'taskana-administration-settings',
|
||||||
|
templateUrl: './settings.component.html',
|
||||||
|
styleUrls: ['./settings.component.scss']
|
||||||
|
})
|
||||||
|
export class SettingsComponent implements OnInit, OnDestroy {
|
||||||
|
settingTypes = SettingTypes;
|
||||||
|
settings: Settings;
|
||||||
|
oldSettings: Settings;
|
||||||
|
groups: string[];
|
||||||
|
members: string[][] = [];
|
||||||
|
invalidMembers: string[] = [];
|
||||||
|
destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@Select(SettingsSelectors.getSettings) settings$: Observable<Settings>;
|
||||||
|
|
||||||
|
constructor(private store: Store, private notificationService: NotificationService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.settings$.pipe(takeUntil(this.destroy$)).subscribe((settings) => {
|
||||||
|
this.settings = this.deepCopy(settings);
|
||||||
|
this.oldSettings = this.deepCopy(settings);
|
||||||
|
this.getKeysOfSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deepCopy(settings: Settings): Settings {
|
||||||
|
return JSON.parse(JSON.stringify(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeysOfSettings() {
|
||||||
|
this.groups = Object.keys(this.settings.schema);
|
||||||
|
this.groups.forEach((group) => {
|
||||||
|
let groupMembers = Object.keys(this.settings.schema[group].members);
|
||||||
|
this.members.push(groupMembers);
|
||||||
|
groupMembers.forEach((member) => {
|
||||||
|
if (!(member in this.settings)) {
|
||||||
|
this.notificationService.showWarning('SETTINGS_INVALID_DATA', { setting: member });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave() {
|
||||||
|
this.changeLabelColor('grey');
|
||||||
|
this.invalidMembers = validateForm(this.members, this.settings, this.groups);
|
||||||
|
if (this.invalidMembers.length === 0) {
|
||||||
|
this.store.dispatch(new SetSettings(this.settings)).subscribe(() => {
|
||||||
|
this.notificationService.showSuccess('SETTINGS_SAVE');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.changeLabelColor('red');
|
||||||
|
this.notificationService.showError('SETTINGS_SAVE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLabelColor(color: string) {
|
||||||
|
this.invalidMembers.forEach((member) => {
|
||||||
|
const elements = Array.from(document.getElementsByClassName(member));
|
||||||
|
elements.forEach((element) => {
|
||||||
|
(element as HTMLElement).style.color = color;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset() {
|
||||||
|
this.changeLabelColor('grey');
|
||||||
|
this.settings = this.deepCopy(this.oldSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMember(group: string, member: string): SettingsMember {
|
||||||
|
return this.settings.schema[group].members[member];
|
||||||
|
}
|
||||||
|
|
||||||
|
onColorChange(member: string) {
|
||||||
|
this.settings[member] = (document.getElementById(member) as HTMLInputElement).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Settings, SettingTypes } from '../../models/settings';
|
||||||
|
|
||||||
|
export const validateForm = (members: string[][], settings: Settings, groups: string[]): string[] => {
|
||||||
|
const invalidMembers = [];
|
||||||
|
|
||||||
|
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
||||||
|
for (let memberIndex = 0; memberIndex < members[groupIndex].length; memberIndex++) {
|
||||||
|
const memberKey = members[groupIndex][memberIndex];
|
||||||
|
const member = settings.schema[groups[groupIndex]].members[memberKey];
|
||||||
|
const value = settings[memberKey];
|
||||||
|
|
||||||
|
if (member.type == SettingTypes.TEXT || member.type == SettingTypes.INTERVAL) {
|
||||||
|
let compareWithMin;
|
||||||
|
let compareWithMax;
|
||||||
|
switch (member.type) {
|
||||||
|
case SettingTypes.TEXT:
|
||||||
|
compareWithMin = value.length;
|
||||||
|
compareWithMax = value.length;
|
||||||
|
break;
|
||||||
|
case SettingTypes.INTERVAL:
|
||||||
|
compareWithMin = value[0];
|
||||||
|
compareWithMax = value[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
if ((member.min || member.min == 0) && member.max) {
|
||||||
|
isValid = compareWithMin >= member.min && compareWithMax <= member.max;
|
||||||
|
} else if (member.min || member.min == 0) {
|
||||||
|
isValid = compareWithMin >= member.min;
|
||||||
|
} else if (member.max) {
|
||||||
|
isValid = compareWithMax <= member.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
invalidMembers.push(memberKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.type == SettingTypes.INTERVAL && compareWithMin > compareWithMax) {
|
||||||
|
invalidMembers.push(memberKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.type == SettingTypes.JSON) {
|
||||||
|
try {
|
||||||
|
JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
invalidMembers.push(memberKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return invalidMembers;
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
export interface SettingsMember {
|
||||||
|
displayName: string;
|
||||||
|
type: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
[setting: string]: any;
|
||||||
|
schema: {
|
||||||
|
[parameterGroup: string]: {
|
||||||
|
displayName: string;
|
||||||
|
members: {
|
||||||
|
[memberName: string]: SettingsMember;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SettingTypes {
|
||||||
|
TEXT = 'text',
|
||||||
|
INTERVAL = 'interval',
|
||||||
|
COLOR = 'color',
|
||||||
|
JSON = 'json'
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Settings } from '../models/settings';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SettingsService {
|
||||||
|
constructor(private httpClient: HttpClient) {}
|
||||||
|
|
||||||
|
// GET
|
||||||
|
getSettings(): Observable<Settings> {
|
||||||
|
return this.httpClient
|
||||||
|
.get<SettingsRepresentation>(`${environment.taskanaRestUrl}/v1/config/custom-attributes`)
|
||||||
|
.pipe(map((b) => b.customAttributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT
|
||||||
|
updateSettings(settings: Settings) {
|
||||||
|
return this.httpClient.put<Settings>(`${environment.taskanaRestUrl}/v1/config/custom-attributes`, {
|
||||||
|
customAttributes: settings
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsRepresentation {
|
||||||
|
customAttributes: Settings;
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { SettingsComponent } from './components/Settings/settings.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: SettingsComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: '',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class SettingsRoutingModule {}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SettingsComponent } from './components/Settings/settings.component';
|
||||||
|
import { SettingsRoutingModule } from './settings-routing.module';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SettingsService } from './services/settings-service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [SettingsComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SettingsRoutingModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatInputModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
providers: [SettingsService]
|
||||||
|
})
|
||||||
|
export class SettingsModule {}
|
|
@ -18,6 +18,7 @@ export class NavBarComponent implements OnInit {
|
||||||
titleMonitor = 'Monitor';
|
titleMonitor = 'Monitor';
|
||||||
titleWorkplace = 'Workplace';
|
titleWorkplace = 'Workplace';
|
||||||
titleHistory = 'History';
|
titleHistory = 'History';
|
||||||
|
titleSettings = 'Settings';
|
||||||
toggle: boolean = false;
|
toggle: boolean = false;
|
||||||
title = this.titleWorkplace;
|
title = this.titleWorkplace;
|
||||||
|
|
||||||
|
@ -27,6 +28,8 @@ export class NavBarComponent implements OnInit {
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.selectedRouteSubscription = this.selectedRouteService.getSelectedRoute().subscribe((value: string) => {
|
this.selectedRouteSubscription = this.selectedRouteService.getSelectedRoute().subscribe((value: string) => {
|
||||||
|
// does not work
|
||||||
|
// console.log('router', value);
|
||||||
this.selectedRoute = value;
|
this.selectedRoute = value;
|
||||||
this.setTitle(value);
|
this.setTitle(value);
|
||||||
});
|
});
|
||||||
|
@ -50,6 +53,8 @@ export class NavBarComponent implements OnInit {
|
||||||
this.title = this.titleAccessItems;
|
this.title = this.titleAccessItems;
|
||||||
} else if (value.indexOf('history') === 0) {
|
} else if (value.indexOf('history') === 0) {
|
||||||
this.title = this.titleHistory;
|
this.title = this.titleHistory;
|
||||||
|
} else if (value.indexOf('settings') === 0) {
|
||||||
|
this.title = this.titleSettings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,4 +24,6 @@
|
||||||
</div>
|
</div>
|
||||||
<a mat-list-item class="navlist__item navlist__history" [routerLink]=[historyUrl] [routerLinkActive]="['active']"
|
<a mat-list-item class="navlist__item navlist__history" [routerLink]=[historyUrl] [routerLinkActive]="['active']"
|
||||||
*ngIf="historyAccess" (click)="toggleSidenav()">History</a>
|
*ngIf="historyAccess" (click)="toggleSidenav()">History</a>
|
||||||
|
<a mat-list-item class="navlist__item navlist__setting" [routerLink]=[settingsURL] [routerLinkActive]="['active']"
|
||||||
|
*ngIf="settingsAccess" (click)="toggleSidenav()">UI Settings</a>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
|
|
|
@ -72,7 +72,7 @@ describe('SidenavListComponent', () => {
|
||||||
component.historyAccess = true;
|
component.historyAccess = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const menuList = debugElement.queryAll(By.css('.navlist__item'));
|
const menuList = debugElement.queryAll(By.css('.navlist__item'));
|
||||||
expect(menuList.length).toBe(9);
|
expect(menuList.length).toBe(10);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -81,6 +81,7 @@ describe('SidenavListComponent', () => {
|
||||||
component.monitorAccess = true;
|
component.monitorAccess = true;
|
||||||
component.workplaceAccess = false;
|
component.workplaceAccess = false;
|
||||||
component.historyAccess = false;
|
component.historyAccess = false;
|
||||||
|
component.settingsAccess = false;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const menuList = debugElement.queryAll(By.css('.navlist__item'));
|
const menuList = debugElement.queryAll(By.css('.navlist__item'));
|
||||||
expect(menuList.length).toBe(1);
|
expect(menuList.length).toBe(1);
|
||||||
|
|
|
@ -20,11 +20,13 @@ export class SidenavListComponent implements OnInit {
|
||||||
classificationUrl = 'taskana/administration/classifications';
|
classificationUrl = 'taskana/administration/classifications';
|
||||||
workbasketsUrl = 'taskana/administration/workbaskets';
|
workbasketsUrl = 'taskana/administration/workbaskets';
|
||||||
administrationsUrl = 'taskana/administration/workbaskets';
|
administrationsUrl = 'taskana/administration/workbaskets';
|
||||||
|
settingsURL = 'taskana/settings';
|
||||||
|
|
||||||
administrationAccess = false;
|
administrationAccess = false;
|
||||||
monitorAccess = false;
|
monitorAccess = false;
|
||||||
workplaceAccess = false;
|
workplaceAccess = false;
|
||||||
historyAccess = false;
|
historyAccess = false;
|
||||||
|
settingsAccess = false;
|
||||||
|
|
||||||
constructor(private taskanaEngineService: TaskanaEngineService, private sidenavService: SidenavService) {}
|
constructor(private taskanaEngineService: TaskanaEngineService, private sidenavService: SidenavService) {}
|
||||||
|
|
||||||
|
@ -35,6 +37,7 @@ export class SidenavListComponent implements OnInit {
|
||||||
this.taskanaEngineService.isHistoryProviderEnabled().subscribe((value) => {
|
this.taskanaEngineService.isHistoryProviderEnabled().subscribe((value) => {
|
||||||
this.historyAccess = value;
|
this.historyAccess = value;
|
||||||
});
|
});
|
||||||
|
this.settingsAccess = this.administrationAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSidenav() {
|
toggleSidenav() {
|
||||||
|
|
|
@ -58,7 +58,11 @@ export const messageByErrorCode = {
|
||||||
IMPORT_EXPORT_UPLOAD_FAILED_NOT_FOUND: 'Upload failed because operation was not found',
|
IMPORT_EXPORT_UPLOAD_FAILED_NOT_FOUND: 'Upload failed because operation was not found',
|
||||||
IMPORT_EXPORT_UPLOAD_FAILED_CONFLICTS: 'Upload failed because operation has conflicts',
|
IMPORT_EXPORT_UPLOAD_FAILED_CONFLICTS: 'Upload failed because operation has conflicts',
|
||||||
IMPORT_EXPORT_UPLOAD_FAILED_SIZE: 'Upload failed because maximum file size exceeded',
|
IMPORT_EXPORT_UPLOAD_FAILED_SIZE: 'Upload failed because maximum file size exceeded',
|
||||||
IMPORT_EXPORT_UPLOAD_FILE_FORMAT: 'File format is not allowed. Please use a .json file.'
|
IMPORT_EXPORT_UPLOAD_FILE_FORMAT: 'File format is not allowed. Please use a .json file.',
|
||||||
|
|
||||||
|
SETTINGS_SAVE: 'Settings cannot be saved since the form contains invalid values.',
|
||||||
|
SETTINGS_NO_SCHEMA:
|
||||||
|
'Wrong data format of UI settings. The object must contain field "schema". Please contact your administrator.'
|
||||||
},
|
},
|
||||||
|
|
||||||
[messageTypes.SUCCESS]: {
|
[messageTypes.SUCCESS]: {
|
||||||
|
@ -89,7 +93,9 @@ export const messageByErrorCode = {
|
||||||
TASK_CREATE: 'Task with name {taskName} was created',
|
TASK_CREATE: 'Task with name {taskName} was created',
|
||||||
TASK_UPDATE: 'Task with name {taskName} was updated',
|
TASK_UPDATE: 'Task with name {taskName} was updated',
|
||||||
TASK_DELETE: 'Task with name {taskName} was deleted',
|
TASK_DELETE: 'Task with name {taskName} was deleted',
|
||||||
TASK_RESTORE: 'Task restored'
|
TASK_RESTORE: 'Task restored',
|
||||||
|
|
||||||
|
SETTINGS_SAVE: 'Settings were updated'
|
||||||
},
|
},
|
||||||
|
|
||||||
[messageTypes.INFORMATION]: {
|
[messageTypes.INFORMATION]: {
|
||||||
|
@ -99,7 +105,10 @@ export const messageByErrorCode = {
|
||||||
[messageTypes.WARNING]: {
|
[messageTypes.WARNING]: {
|
||||||
REPORT_DATA_WRONG_HEADER:
|
REPORT_DATA_WRONG_HEADER:
|
||||||
'The received header of the Report data does not match the expected header. ' +
|
'The received header of the Report data does not match the expected header. ' +
|
||||||
'The data might be displayed incorrectly. Please contact your administrator.'
|
'The data might be displayed incorrectly. Please contact your administrator.',
|
||||||
|
SETTINGS_INVALID_DATA:
|
||||||
|
'The data structure is invalid. The setting {setting} is configured under UI settings but there is no matching ' +
|
||||||
|
'attribute to save the value. Please contact your administrator.'
|
||||||
},
|
},
|
||||||
|
|
||||||
[messageTypes.DIALOG]: {
|
[messageTypes.DIALOG]: {
|
||||||
|
|
|
@ -24,14 +24,11 @@ export class TaskanaEngineService {
|
||||||
.toPromise();
|
.toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
hasRole(roles2Find: Array<string>): boolean {
|
hasRole(roles2Find: string[]): boolean {
|
||||||
if (!this.currentUserInfo || this.currentUserInfo.roles.length < 1) {
|
if (!this.currentUserInfo || this.currentUserInfo.roles.length < 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.findRole(roles2Find)) {
|
return !!this.findRole(roles2Find);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion(): Observable<Version> {
|
getVersion(): Observable<Version> {
|
||||||
|
@ -46,7 +43,7 @@ export class TaskanaEngineService {
|
||||||
return this.httpClient.get<boolean>(`${environment.taskanaRestUrl}/v1/history-provider-enabled`);
|
return this.httpClient.get<boolean>(`${environment.taskanaRestUrl}/v1/history-provider-enabled`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private findRole(roles2Find: Array<string>) {
|
private findRole(roles2Find: string[]) {
|
||||||
return this.currentUserInfo.roles.find((role) => roles2Find.some((roleLookingFor) => role === roleLookingFor));
|
return this.currentUserInfo.roles.find((role) => roles2Find.some((roleLookingFor) => role === roleLookingFor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Selector } from '@ngxs/store';
|
import { Selector } from '@ngxs/store';
|
||||||
import { ClassificationStateModel, ClassificationState } from './classification.state';
|
import { ClassificationStateModel, ClassificationState } from './classification.state';
|
||||||
import { Classification } from '../../models/classification';
|
import { Classification } from '../../models/classification';
|
||||||
import { CategoriesResponse } from '../../services/classification-categories/classification-categories.service';
|
|
||||||
|
|
||||||
export class ClassificationSelectors {
|
export class ClassificationSelectors {
|
||||||
@Selector([ClassificationState])
|
@Selector([ClassificationState])
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { WorkbasketState } from './workbasket-store/workbasket.state';
|
||||||
import { AccessItemsManagementState } from './access-items-management-store/access-items-management.state';
|
import { AccessItemsManagementState } from './access-items-management-store/access-items-management.state';
|
||||||
import { FilterState } from './filter-store/filter.state';
|
import { FilterState } from './filter-store/filter.state';
|
||||||
import { WorkplaceState } from './workplace-store/workplace.state';
|
import { WorkplaceState } from './workplace-store/workplace.state';
|
||||||
|
import { SettingsState } from './settings-store/settings.state';
|
||||||
|
|
||||||
export const STATES = [
|
export const STATES = [
|
||||||
EngineConfigurationState,
|
EngineConfigurationState,
|
||||||
|
@ -11,5 +12,6 @@ export const STATES = [
|
||||||
WorkbasketState,
|
WorkbasketState,
|
||||||
AccessItemsManagementState,
|
AccessItemsManagementState,
|
||||||
FilterState,
|
FilterState,
|
||||||
WorkplaceState
|
WorkplaceState,
|
||||||
|
SettingsState
|
||||||
];
|
];
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Workbasket } from '../../models/workbasket';
|
||||||
import { WorkbasketType } from '../../models/workbasket-type';
|
import { WorkbasketType } from '../../models/workbasket-type';
|
||||||
import { ACTION } from '../../models/action';
|
import { ACTION } from '../../models/action';
|
||||||
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
|
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
|
||||||
|
import { Settings } from '../../../settings/models/settings';
|
||||||
|
|
||||||
export const classificationStateMock = {
|
export const classificationStateMock = {
|
||||||
classifications: [],
|
classifications: [],
|
||||||
|
@ -515,3 +516,79 @@ export const workbasketReadStateMock = {
|
||||||
],
|
],
|
||||||
workbasketAccessItems: workbasketAccessItemsMock
|
workbasketAccessItems: workbasketAccessItemsMock
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const settingsStateMock = {
|
||||||
|
settings: {
|
||||||
|
nameHighPriority: 'High Priority',
|
||||||
|
nameMediumPriority: 'Medium Priority',
|
||||||
|
nameLowPriority: 'Low Priority',
|
||||||
|
intervalHighPriority: [3, 300],
|
||||||
|
intervalMediumPriority: [2, 2],
|
||||||
|
intervalLowPriority: [0, 1],
|
||||||
|
colorHighPriority: '#FF0000',
|
||||||
|
colorLowPriority: '#5FAD00',
|
||||||
|
colorMediumPriority: '#FFD700',
|
||||||
|
filter: '{ "Tasks with state READY": { "state": ["READY"]}, "Tasks with state CLAIMED": {"state": ["CLAIMED"] }}',
|
||||||
|
schema: {
|
||||||
|
'Monitor Workbasket-Priority-Report': {
|
||||||
|
displayName: 'Priority Report',
|
||||||
|
members: {
|
||||||
|
nameHighPriority: {
|
||||||
|
displayName: 'High Priority Name',
|
||||||
|
type: 'text',
|
||||||
|
max: 32
|
||||||
|
},
|
||||||
|
nameMediumPriority: {
|
||||||
|
displayName: 'Medium Priority Name',
|
||||||
|
type: 'text',
|
||||||
|
min: 0,
|
||||||
|
max: 32
|
||||||
|
},
|
||||||
|
nameLowPriority: {
|
||||||
|
displayName: 'Low Priority Name',
|
||||||
|
type: 'text',
|
||||||
|
min: 0,
|
||||||
|
max: 32
|
||||||
|
},
|
||||||
|
intervalHighPriority: {
|
||||||
|
displayName: 'High Priority Interval',
|
||||||
|
type: 'interval',
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
intervalMediumPriority: {
|
||||||
|
displayName: 'Medium Priority Interval',
|
||||||
|
type: 'interval',
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
intervalLowPriority: {
|
||||||
|
displayName: 'Low Priority Interval',
|
||||||
|
type: 'interval',
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
colorHighPriority: {
|
||||||
|
displayName: 'High Priority Color',
|
||||||
|
type: 'color'
|
||||||
|
},
|
||||||
|
colorMediumPriority: {
|
||||||
|
displayName: 'Medium Priority Color',
|
||||||
|
type: 'color'
|
||||||
|
},
|
||||||
|
colorLowPriority: {
|
||||||
|
displayName: 'Low Priority Color',
|
||||||
|
type: 'color'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Monitor Workbasket-Priority-Report-Filter': {
|
||||||
|
displayName: 'Filter for Task-Priority-Report',
|
||||||
|
members: {
|
||||||
|
filter: {
|
||||||
|
displayName: 'Filter values',
|
||||||
|
type: 'json',
|
||||||
|
min: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Settings } from '../../../settings/models/settings';
|
||||||
|
|
||||||
|
export class RetrieveSettings {
|
||||||
|
static readonly type = '[Settings] Get settings from backend';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SetSettings {
|
||||||
|
static readonly type = '[Settings] Modify settings according to user input';
|
||||||
|
constructor(public settings: Settings) {}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Selector } from '@ngxs/store';
|
||||||
|
import { SettingsState, SettingsStateModel } from './settings.state';
|
||||||
|
import { Settings } from '../../../settings/models/settings';
|
||||||
|
|
||||||
|
export class SettingsSelectors {
|
||||||
|
@Selector([SettingsState])
|
||||||
|
static getSettings(state: SettingsStateModel): Settings {
|
||||||
|
return state.settings;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Action, NgxsAfterBootstrap, State, StateContext } from '@ngxs/store';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RetrieveSettings, SetSettings } from './settings.actions';
|
||||||
|
import { Settings } from '../../../settings/models/settings';
|
||||||
|
import { SettingsService } from '../../../settings/services/settings-service';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { NotificationService } from '../../services/notifications/notification.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@State<SettingsStateModel>({ name: 'settings' })
|
||||||
|
export class SettingsState implements NgxsAfterBootstrap {
|
||||||
|
constructor(private settingsService: SettingsService, private notificationService: NotificationService) {}
|
||||||
|
|
||||||
|
@Action(RetrieveSettings)
|
||||||
|
initializeStore(ctx: StateContext<SettingsStateModel>) {
|
||||||
|
return this.settingsService
|
||||||
|
.getSettings()
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((settings) => {
|
||||||
|
if (!settings.schema) {
|
||||||
|
this.notificationService.showError('SETTINGS_NO_SCHEMA');
|
||||||
|
} else {
|
||||||
|
ctx.patchState({
|
||||||
|
settings: settings
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngxsAfterBootstrap(ctx?: StateContext<any>): void {
|
||||||
|
ctx.dispatch(new RetrieveSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Action(SetSettings)
|
||||||
|
setSettings(ctx: StateContext<SettingsStateModel>, action: SetSettings) {
|
||||||
|
return this.settingsService
|
||||||
|
.updateSettings(action.settings)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe(() => {
|
||||||
|
ctx.patchState({
|
||||||
|
settings: action.settings
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingsStateModel {
|
||||||
|
settings: Settings;
|
||||||
|
}
|
|
@ -4,8 +4,17 @@ export function asUrlQueryString(params: Object): string {
|
||||||
for (const [key, value] of Object.entries(params)) {
|
for (const [key, value] of Object.entries(params)) {
|
||||||
if (value) {
|
if (value) {
|
||||||
let values: any[] = value instanceof Array ? value : [value];
|
let values: any[] = value instanceof Array ? value : [value];
|
||||||
values.filter((v) => v !== undefined).forEach((v) => (query += (query ? '&' : '?') + `${key}=${v}`));
|
values
|
||||||
|
.filter((v) => v !== undefined)
|
||||||
|
.forEach((v) => (query += (query ? '&' : '?') + `${key}=${convertValue(v)}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertValue(value: any) {
|
||||||
|
if (value instanceof Object) {
|
||||||
|
return encodeURIComponent(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue