TSK-1725, TSK-1740: Settings Component, Workbasket-Priority-Report Filter

This commit is contained in:
Sofie Hofmann 2021-09-29 14:48:59 +02:00 committed by Mustapha Zorgati
parent 79742782cb
commit ecdf333c8e
38 changed files with 1092 additions and 229 deletions

View File

@ -1,100 +1,64 @@
{
"nameHighPrio": "High Prio",
"nameMediumPrio": "Medium P",
"nameLowPrio": "Low Prio",
"intervalHighPrio": [
100,
200
],
"intervalMediumPrio": [
21,
100
],
"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"
]
}
},
"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": {
"Priority-Report": {
"displayName": "Priority Report",
"Monitor Workbasket-Priority-Report": {
"displayName": "Monitor Workbasket-Priority-Report",
"members": {
"nameHighPrio": {
"displayName": "High Prio Name",
"nameHighPriority": {
"displayName": "High Priority Name",
"type": "text",
"min": 1
"max": 32
},
"nameMediumPrio": {
"displayName": "Medium Prio Name",
"nameMediumPriority": {
"displayName": "Medium Priority Name",
"type": "text",
"min": 1,
"max": 8
"max": 32
},
"nameLowPrio": {
"displayName": "Low Prio Name",
"nameLowPriority": {
"displayName": "Low Priority Name",
"type": "text",
"min": 2,
"max": 64
"max": 32
},
"intervalHighPrio": {
"displayName": "High Prio Interval",
"type": "interval",
"min": 0,
"max": 300
},
"intervalMediumPrio": {
"displayName": "Medium Prio Interval",
"type": "interval",
"min": -5,
"max": 300
},
"intervalLowPrio": {
"displayName": "Low Prio Interval",
"intervalHighPriority": {
"displayName": "High Priority Interval",
"type": "interval",
"min": 0
},
"colorHighPrio": {
"displayName": "High Prio Color",
"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"
},
"colorMediumPrio": {
"displayName": "Medium Prio Color",
"colorMediumPriority": {
"displayName": "Medium Priority Color",
"type": "color"
},
"colorLowPrio": {
"displayName": "Low Prio Color",
"colorLowPriority": {
"displayName": "Low Priority Color",
"type": "color"
}
}
},
"Filter": {
"displayName": "Filter for Task-Priority-Report",
"members": {
"Monitor Workbasket-Priority-Report-Filter": {
"displayName": "Monitor Workbasket-Priority-Report-Filter",
"filter": {
"displayName": "Filter values",
"type": "json",

View File

@ -39,6 +39,11 @@ const appRoutes: Routes = [
path: 'administration',
redirectTo: 'administration/workbaskets'
},
{
canActivate: [BusinessAdminGuard],
path: 'settings',
loadChildren: () => import('./settings/settings.module').then((m) => m.SettingsModule)
},
{
path: '**',
redirectTo: 'workplace'

View File

@ -1,8 +1,12 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { NgxsModule } from '@ngxs/store';
import { NgxsModule, Store } from '@ngxs/store';
import { CanvasComponent } from './canvas.component';
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', () => {
let fixture: ComponentFixture<CanvasComponent>;
@ -12,7 +16,7 @@ describe('CanvasComponent', () => {
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NgxsModule.forRoot([])],
imports: [NgxsModule.forRoot([SettingsState]), HttpClientTestingModule, MatDialogModule],
declarations: [CanvasComponent]
}).compileComponents();
@ -20,6 +24,11 @@ describe('CanvasComponent', () => {
debugElement = fixture.debugElement;
component = fixture.debugElement.componentInstance;
component.id = '1';
const store: Store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
settings: settingsStateMock
});
fixture.detectChanges();
})
);

View File

@ -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 { priorityTypes } from '../../models/priority';
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({
selector: 'taskana-monitor-canvas',
templateUrl: './canvas.component.html',
styleUrls: ['./canvas.component.scss']
})
export class CanvasComponent implements AfterViewInit {
export class CanvasComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() row: ReportRow;
@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() {
const canvas = document.getElementById(this.id) as HTMLCanvasElement;
@ -26,13 +55,12 @@ export class CanvasComponent implements AfterViewInit {
new Chart(canvas, {
type: 'doughnut',
data: {
labels: [priorityTypes.HIGH, priorityTypes.MEDIUM, priorityTypes.LOW],
labels: this.labels,
datasets: [
{
label: 'Tasks by Priority',
// depends on whether backend sends data sorted in ascending or descending order
data: this.isReversed ? row.cells.reverse() : row.cells,
backgroundColor: ['red', 'gold', 'limegreen'],
data: row.cells,
backgroundColor: this.colors,
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();
}
}

View File

@ -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>

View File

@ -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'] });
});
});

View File

@ -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();
}
}

View File

@ -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: []
};

View File

@ -1,5 +1,12 @@
<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>
@ -7,12 +14,12 @@
<div *ngFor="let row of reportData?.rows; let i = index" class="task-priority-report__workbasket">
<!-- WORKBASKET NAME -->
<div class="task-priority-report__headline">
<div class="task-priority-report__workbasket-headline">
<h6> {{row.desc[0]}} </h6>
</div>
<!-- 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 -->
<div>
@ -22,9 +29,9 @@
<ng-container matColumnDef="priority">
<th mat-header-cell *matHeaderCellDef> Priority </th>
<td mat-cell *matCellDef="let element" [ngClass]="{
'task-priority-report__row--high': element.priority == 'High',
'task-priority-report__row--medium': element.priority == 'Medium',
'task-priority-report__row--low': element.priority == 'Low'}">
'task-priority-report__row--high': element.priority == nameHighPriority,
'task-priority-report__row--medium': element.priority == nameMediumPriority,
'task-priority-report__row--low': element.priority == nameLowPriority}">
{{element.priority}} </td>
</ng-container>
@ -32,9 +39,9 @@
<ng-container matColumnDef="number">
<th mat-header-cell *matHeaderCellDef> Number of Tasks </th>
<td mat-cell *matCellDef="let element" [ngClass]="{
'task-priority-report__row--high': element.priority == 'High',
'task-priority-report__row--medium': element.priority == 'Medium',
'task-priority-report__row--low': element.priority == 'Low'}">
'task-priority-report__row--high': element.priority == nameHighPriority,
'task-priority-report__row--medium': element.priority == nameMediumPriority,
'task-priority-report__row--low': element.priority == nameLowPriority}">
{{element.number}} </td>
</ng-container>

View File

@ -7,6 +7,22 @@ table {
overflow-y: scroll;
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 {
display: flex;
flex-direction: row;
@ -20,21 +36,8 @@ table {
margin: 24px 24px 36px 24px;
}
&__headline {
&__workbasket-headline {
text-align: center;
margin-bottom: 8px;
}
&__row--low {
color: limegreen;
}
&__row--medium {
color: gold;
}
&__row--high {
color: red;
}
}

View File

@ -1,14 +1,16 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Component, DebugElement, Input, Pipe, PipeTransform } from '@angular/core';
import { NgxsModule } from '@ngxs/store';
import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service';
import { NgxsModule, Store } from '@ngxs/store';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { TaskPriorityReportComponent } from './task-priority-report.component';
import { MonitorService } from '../../services/monitor.service';
import { of } from 'rxjs';
import { MatTableModule } from '@angular/material/table';
import { priorityTypes } from '../../models/priority';
import { workbasketReportMock, workbasketReportUnexpectedHeaderMock } from './monitor-mock-data';
import { workbasketReportMock } 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' })
class GermanTimeFormatPipe implements PipeTransform {
@ -24,14 +26,13 @@ class CanvasStub {
@Input() isReversed;
}
@Component({ selector: 'taskana-monitor-task-priority-report-filter', template: '' })
class TaskPriorityReportFilterStub {}
const monitorServiceSpy: Partial<MonitorService> = {
getTasksByPriorityReport: jest.fn().mockReturnValue(of(workbasketReportMock))
};
const monitorServiceWithDifferentDataSpy: Partial<MonitorService> = {
getTasksByPriorityReport: jest.fn().mockReturnValue(of(workbasketReportUnexpectedHeaderMock))
};
const notificationServiceSpy: Partial<NotificationService> = {
showWarning: jest.fn()
};
@ -44,8 +45,8 @@ describe('TaskPriorityReportComponent', () => {
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NgxsModule.forRoot([]), MatTableModule],
declarations: [TaskPriorityReportComponent, GermanTimeFormatPipe, CanvasStub],
imports: [NgxsModule.forRoot([SettingsState]), MatTableModule, HttpClientTestingModule, MatDividerModule],
declarations: [TaskPriorityReportComponent, GermanTimeFormatPipe, CanvasStub, TaskPriorityReportFilterStub],
providers: [
{ provide: MonitorService, useValue: monitorServiceSpy },
{ provide: NotificationService, useValue: notificationServiceSpy }
@ -55,6 +56,11 @@ describe('TaskPriorityReportComponent', () => {
fixture = TestBed.createComponent(TaskPriorityReportComponent);
debugElement = fixture.debugElement;
component = fixture.debugElement.componentInstance;
const store: Store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
settings: settingsStateMock
});
fixture.detectChanges();
})
);
@ -78,56 +84,4 @@ describe('TaskPriorityReportComponent', () => {
component.ngOnInit();
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);
});
});

View File

@ -1,52 +1,131 @@
import { Component, OnInit } from '@angular/core';
import { priorityTypes } from '../../models/priority';
import { AfterViewChecked, Component, OnDestroy, OnInit } from '@angular/core';
import { ReportData } from '../../models/report-data';
import { MonitorService } from '../../services/monitor.service';
import { take } from 'rxjs/operators';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
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({
selector: 'taskana-monitor-task-priority-report',
templateUrl: './task-priority-report.component.html',
styleUrls: ['./task-priority-report.component.scss']
})
export class TaskPriorityReportComponent implements OnInit {
export class TaskPriorityReportComponent implements OnInit, AfterViewChecked, OnDestroy {
columns: string[] = ['priority', 'number'];
reportData: ReportData;
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) {}
ngOnInit() {
this.monitorService
.getTasksByPriorityReport([WorkbasketType.TOPIC])
.pipe(take(1))
this.settings$
.pipe(
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) => {
this.reportData = 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 }
]);
});
this.setValuesFromReportData(reportData);
});
}
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);
}
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();
}
}

View File

@ -23,6 +23,7 @@ import { WorkbasketReportPlannedDateComponent } from './components/workbasket-re
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 { CanvasComponent } from './components/canvas/canvas.component';
import { TaskPriorityReportFilterComponent } from './components/task-priority-report-filter/task-priority-report-filter.component';
/**
* Services
@ -35,6 +36,9 @@ import { MonitorService } from './services/monitor.service';
import { MatTabsModule } from '@angular/material/tabs';
import { MatButtonModule } from '@angular/material/button';
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 = [
CommonModule,
@ -48,12 +52,16 @@ const MODULES = [
SharedModule,
MatTabsModule,
MatButtonModule,
MatTableModule
MatTableModule,
MatExpansionModule,
MatCheckboxModule,
MatDividerModule
];
const DECLARATIONS = [
ReportTableComponent,
MonitorComponent,
TaskPriorityReportComponent,
TaskPriorityReportFilterComponent,
CanvasComponent,
TimestampReportComponent,
WorkbasketReportComponent,

View File

@ -59,10 +59,13 @@ export class MonitorService {
});
}
getTasksByPriorityReport(type: WorkbasketType[] = []): Observable<ReportData> {
getTasksByPriorityReport(type: string[], priority: any[], customFilters: {} = {}): Observable<ReportData> {
const queryParams = {
'workbasket-type': type
'workbasket-type': type,
columnHeader: priority,
...customFilters
};
return this.httpClient.get<ReportData>(
`${environment.taskanaRestUrl + monitorUrl}workbasket-priority-report${asUrlQueryString(queryParams)}`
);

View File

@ -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'
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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);
});
});

View File

@ -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();
}
}

View File

@ -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;
};

View File

@ -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'
}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -18,6 +18,7 @@ export class NavBarComponent implements OnInit {
titleMonitor = 'Monitor';
titleWorkplace = 'Workplace';
titleHistory = 'History';
titleSettings = 'Settings';
toggle: boolean = false;
title = this.titleWorkplace;
@ -27,6 +28,8 @@ export class NavBarComponent implements OnInit {
ngOnInit() {
this.selectedRouteSubscription = this.selectedRouteService.getSelectedRoute().subscribe((value: string) => {
// does not work
// console.log('router', value);
this.selectedRoute = value;
this.setTitle(value);
});
@ -50,6 +53,8 @@ export class NavBarComponent implements OnInit {
this.title = this.titleAccessItems;
} else if (value.indexOf('history') === 0) {
this.title = this.titleHistory;
} else if (value.indexOf('settings') === 0) {
this.title = this.titleSettings;
}
}
}

View File

@ -24,4 +24,6 @@
</div>
<a mat-list-item class="navlist__item navlist__history" [routerLink]=[historyUrl] [routerLinkActive]="['active']"
*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>

View File

@ -72,7 +72,7 @@ describe('SidenavListComponent', () => {
component.historyAccess = true;
fixture.detectChanges();
const menuList = debugElement.queryAll(By.css('.navlist__item'));
expect(menuList.length).toBe(9);
expect(menuList.length).toBe(10);
fixture.detectChanges();
});
@ -81,6 +81,7 @@ describe('SidenavListComponent', () => {
component.monitorAccess = true;
component.workplaceAccess = false;
component.historyAccess = false;
component.settingsAccess = false;
fixture.detectChanges();
const menuList = debugElement.queryAll(By.css('.navlist__item'));
expect(menuList.length).toBe(1);

View File

@ -20,11 +20,13 @@ export class SidenavListComponent implements OnInit {
classificationUrl = 'taskana/administration/classifications';
workbasketsUrl = 'taskana/administration/workbaskets';
administrationsUrl = 'taskana/administration/workbaskets';
settingsURL = 'taskana/settings';
administrationAccess = false;
monitorAccess = false;
workplaceAccess = false;
historyAccess = false;
settingsAccess = false;
constructor(private taskanaEngineService: TaskanaEngineService, private sidenavService: SidenavService) {}
@ -35,6 +37,7 @@ export class SidenavListComponent implements OnInit {
this.taskanaEngineService.isHistoryProviderEnabled().subscribe((value) => {
this.historyAccess = value;
});
this.settingsAccess = this.administrationAccess;
}
toggleSidenav() {

View File

@ -58,7 +58,11 @@ export const messageByErrorCode = {
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_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]: {
@ -89,7 +93,9 @@ export const messageByErrorCode = {
TASK_CREATE: 'Task with name {taskName} was created',
TASK_UPDATE: 'Task with name {taskName} was updated',
TASK_DELETE: 'Task with name {taskName} was deleted',
TASK_RESTORE: 'Task restored'
TASK_RESTORE: 'Task restored',
SETTINGS_SAVE: 'Settings were updated'
},
[messageTypes.INFORMATION]: {
@ -99,7 +105,10 @@ export const messageByErrorCode = {
[messageTypes.WARNING]: {
REPORT_DATA_WRONG_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]: {

View File

@ -24,14 +24,11 @@ export class TaskanaEngineService {
.toPromise();
}
hasRole(roles2Find: Array<string>): boolean {
hasRole(roles2Find: string[]): boolean {
if (!this.currentUserInfo || this.currentUserInfo.roles.length < 1) {
return false;
}
if (this.findRole(roles2Find)) {
return true;
}
return false;
return !!this.findRole(roles2Find);
}
getVersion(): Observable<Version> {
@ -46,7 +43,7 @@ export class TaskanaEngineService {
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));
}
}

View File

@ -1,7 +1,6 @@
import { Selector } from '@ngxs/store';
import { ClassificationStateModel, ClassificationState } from './classification.state';
import { Classification } from '../../models/classification';
import { CategoriesResponse } from '../../services/classification-categories/classification-categories.service';
export class ClassificationSelectors {
@Selector([ClassificationState])

View File

@ -4,6 +4,7 @@ import { WorkbasketState } from './workbasket-store/workbasket.state';
import { AccessItemsManagementState } from './access-items-management-store/access-items-management.state';
import { FilterState } from './filter-store/filter.state';
import { WorkplaceState } from './workplace-store/workplace.state';
import { SettingsState } from './settings-store/settings.state';
export const STATES = [
EngineConfigurationState,
@ -11,5 +12,6 @@ export const STATES = [
WorkbasketState,
AccessItemsManagementState,
FilterState,
WorkplaceState
WorkplaceState,
SettingsState
];

View File

@ -2,6 +2,7 @@ import { Workbasket } from '../../models/workbasket';
import { WorkbasketType } from '../../models/workbasket-type';
import { ACTION } from '../../models/action';
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
import { Settings } from '../../../settings/models/settings';
export const classificationStateMock = {
classifications: [],
@ -515,3 +516,79 @@ export const workbasketReadStateMock = {
],
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
}
}
}
}
}
};

View File

@ -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) {}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -4,8 +4,17 @@ export function asUrlQueryString(params: Object): string {
for (const [key, value] of Object.entries(params)) {
if (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;
}
function convertValue(value: any) {
if (value instanceof Object) {
return encodeURIComponent(JSON.stringify(value));
}
return value;
}