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",
|
||||
"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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)}`
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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]: {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
];
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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)) {
|
||||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue