UI: add create notification and template from other wizard
This commit is contained in:
parent
6a0e8c03a4
commit
72348b049e
@ -49,7 +49,9 @@ import {
|
|||||||
import { EscalationsComponent } from '@home/pages/notification-center/rule-table/escalations.component';
|
import { EscalationsComponent } from '@home/pages/notification-center/rule-table/escalations.component';
|
||||||
import { EscalationFormComponent } from '@home/pages/notification-center/rule-table/escalation-form.component';
|
import { EscalationFormComponent } from '@home/pages/notification-center/rule-table/escalation-form.component';
|
||||||
import { AlarmTypeListComponent } from '@home/pages/notification-center/rule-table/alarm-type-list.component';
|
import { AlarmTypeListComponent } from '@home/pages/notification-center/rule-table/alarm-type-list.component';
|
||||||
import { AlarmSeveritiesListComponent } from '@home/pages/notification-center/rule-table/alarm-severities-list.component';
|
import {
|
||||||
|
AlarmSeveritiesListComponent
|
||||||
|
} from '@home/pages/notification-center/rule-table/alarm-severities-list.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|||||||
@ -49,6 +49,11 @@
|
|||||||
placeholderText="{{ 'notification.target' | translate }}"
|
placeholderText="{{ 'notification.target' | translate }}"
|
||||||
requiredText="{{ 'notification.targets-required' | translate }}"
|
requiredText="{{ 'notification.targets-required' | translate }}"
|
||||||
[entityType]="entityType.NOTIFICATION_TARGET">
|
[entityType]="entityType.NOTIFICATION_TARGET">
|
||||||
|
<button #createTargetButton
|
||||||
|
mat-button color="primary" matSuffix
|
||||||
|
(click)="createTarget($event, createTargetButton)">
|
||||||
|
{{ 'notification.create-new' | translate }}
|
||||||
|
</button>
|
||||||
</tb-entity-list>
|
</tb-entity-list>
|
||||||
<section formGroupName="additionalConfig" class="additional-config-group">
|
<section formGroupName="additionalConfig" class="additional-config-group">
|
||||||
<mat-slide-toggle formControlName="enabled" class="toggle">
|
<mat-slide-toggle formControlName="enabled" class="toggle">
|
||||||
|
|||||||
@ -14,13 +14,18 @@
|
|||||||
/// limitations under the License.
|
/// limitations under the License.
|
||||||
///
|
///
|
||||||
|
|
||||||
import { NotificationRequest, NotificationRequestPreview, NotificationType } from '@shared/models/notification.models';
|
import {
|
||||||
|
NotificationRequest,
|
||||||
|
NotificationRequestPreview,
|
||||||
|
NotificationTarget,
|
||||||
|
NotificationType
|
||||||
|
} from '@shared/models/notification.models';
|
||||||
import { Component, Inject, OnDestroy, ViewChild } from '@angular/core';
|
import { Component, Inject, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { DialogComponent } from '@shared/components/dialog.component';
|
import { DialogComponent } from '@shared/components/dialog.component';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { NotificationService } from '@core/http/notification.service';
|
import { NotificationService } from '@core/http/notification.service';
|
||||||
import { deepTrim } from '@core/utils';
|
import { deepTrim } from '@core/utils';
|
||||||
@ -32,6 +37,11 @@ import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'
|
|||||||
import { MediaBreakpoints } from '@shared/models/constants';
|
import { MediaBreakpoints } from '@shared/models/constants';
|
||||||
import { map, takeUntil } from 'rxjs/operators';
|
import { map, takeUntil } from 'rxjs/operators';
|
||||||
import { getCurrentTime } from '@shared/models/time/time.models';
|
import { getCurrentTime } from '@shared/models/time/time.models';
|
||||||
|
import {
|
||||||
|
TargetNotificationDialogComponent,
|
||||||
|
TargetsNotificationDialogData
|
||||||
|
} from '@home/pages/notification-center/targets-table/target-notification-dialog.componet';
|
||||||
|
import { MatButton } from '@angular/material/button';
|
||||||
|
|
||||||
export interface RequestNotificationDialogData {
|
export interface RequestNotificationDialogData {
|
||||||
request?: NotificationRequest;
|
request?: NotificationRequest;
|
||||||
@ -66,7 +76,8 @@ export class RequestNotificationDialogComponent extends
|
|||||||
@Inject(MAT_DIALOG_DATA) public data: RequestNotificationDialogData,
|
@Inject(MAT_DIALOG_DATA) public data: RequestNotificationDialogData,
|
||||||
private breakpointObserver: BreakpointObserver,
|
private breakpointObserver: BreakpointObserver,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
private notificationService: NotificationService) {
|
private notificationService: NotificationService,
|
||||||
|
private dialog: MatDialog) {
|
||||||
super(store, router, dialogRef);
|
super(store, router, dialogRef);
|
||||||
|
|
||||||
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
|
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
|
||||||
@ -199,4 +210,27 @@ export class RequestNotificationDialogComponent extends
|
|||||||
date.setDate(date.getDate() + 7);
|
date.setDate(date.getDate() + 7);
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createTarget($event: Event, button: MatButton) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
button._elementRef.nativeElement.blur();
|
||||||
|
this.dialog.open<TargetNotificationDialogComponent, TargetsNotificationDialogData,
|
||||||
|
NotificationTarget>(TargetNotificationDialogComponent, {
|
||||||
|
disableClose: true,
|
||||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||||
|
data: {}
|
||||||
|
}).afterClosed()
|
||||||
|
.subscribe((res) => {
|
||||||
|
if (res) {
|
||||||
|
let formValue: string[] = this.notificationRequestForm.get('targets').value;
|
||||||
|
if (!formValue) {
|
||||||
|
formValue = [];
|
||||||
|
}
|
||||||
|
formValue.push(res.id.id);
|
||||||
|
this.notificationRequestForm.get('targets').patchValue(formValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,9 @@
|
|||||||
<form [formGroup]="escalationFormGroup" fxLayout="column">
|
<form [formGroup]="escalationFormGroup" fxLayout="column">
|
||||||
<div class="escalation" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center" fxLayoutAlign.xs="start">
|
<div class="escalation" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center" fxLayoutAlign.xs="start">
|
||||||
<div fxFlex fxLayout="row" ngStyle.xs="padding-top: 10px">
|
<div fxFlex fxLayout="row" ngStyle.xs="padding-top: 10px">
|
||||||
<div fxFlex *ngIf="systemEscalation" class="escalation-padding" translate>notification.first-recipient</div>
|
<div fxFlex *ngIf="systemEscalation; else selectTime" class="escalation-padding" translate>notification.first-recipient</div>
|
||||||
<div fxFlex *ngIf="!systemEscalation" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center">
|
<ng-template #selectTime>
|
||||||
|
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center">
|
||||||
<span class="escalation-padding">After</span>
|
<span class="escalation-padding">After</span>
|
||||||
<tb-timeinterval
|
<tb-timeinterval
|
||||||
style="min-width: 100px;"
|
style="min-width: 100px;"
|
||||||
@ -31,13 +32,19 @@
|
|||||||
disabledAdvanced></tb-timeinterval>
|
disabledAdvanced></tb-timeinterval>
|
||||||
<span fxFlex class="escalation-notify" translate>notification.notify</span>
|
<span fxFlex class="escalation-notify" translate>notification.notify</span>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div fxFlex class="escalation-padding">
|
<div fxFlex="60" class="escalation-padding">
|
||||||
<tb-entity-list
|
<tb-entity-list
|
||||||
required
|
required
|
||||||
formControlName="targets"
|
formControlName="targets"
|
||||||
[entityType]="entityType.NOTIFICATION_TARGET"
|
[entityType]="entityType.NOTIFICATION_TARGET"
|
||||||
placeholderText="{{ 'notification.add-target' | translate }}">
|
placeholderText="{{ 'notification.add-target' | translate }}">
|
||||||
|
<button #createTargetButton
|
||||||
|
mat-button [fxHide]="disabled" matSuffix
|
||||||
|
(click)="createTarget($event, createTargetButton)">
|
||||||
|
{{ 'notification.create-new' | translate }}
|
||||||
|
</button>
|
||||||
</tb-entity-list>
|
</tb-entity-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,3 +32,13 @@
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep {
|
||||||
|
tb-timeinterval {
|
||||||
|
min-width: 100px;
|
||||||
|
|
||||||
|
.mat-form-field-infix {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -25,12 +25,17 @@ import {
|
|||||||
Validator,
|
Validator,
|
||||||
Validators
|
Validators
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
import { UtilsService } from '@core/services/utils.service';
|
|
||||||
import { isDefinedAndNotNull } from '@core/utils';
|
import { isDefinedAndNotNull } from '@core/utils';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models';
|
import { NonConfirmedNotificationEscalation, NotificationTarget } from '@shared/models/notification.models';
|
||||||
import { EntityType } from '@shared/models/entity-type.models';
|
import { EntityType } from '@shared/models/entity-type.models';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
TargetNotificationDialogComponent,
|
||||||
|
TargetsNotificationDialogData
|
||||||
|
} from '@home/pages/notification-center/targets-table/target-notification-dialog.componet';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatButton } from '@angular/material/button';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-escalation-form',
|
selector: 'tb-escalation-form',
|
||||||
@ -66,8 +71,8 @@ export class EscalationFormComponent implements ControlValueAccessor, OnInit, On
|
|||||||
private propagateChangePending = false;
|
private propagateChangePending = false;
|
||||||
private destroy$ = new Subject();
|
private destroy$ = new Subject();
|
||||||
|
|
||||||
constructor(private utils: UtilsService,
|
constructor(private fb: FormBuilder,
|
||||||
private fb: FormBuilder) {
|
private dialog: MatDialog) {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerOnChange(fn: any): void {
|
registerOnChange(fn: any): void {
|
||||||
@ -120,6 +125,29 @@ export class EscalationFormComponent implements ControlValueAccessor, OnInit, On
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createTarget($event: Event, button: MatButton) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
button._elementRef.nativeElement.blur();
|
||||||
|
this.dialog.open<TargetNotificationDialogComponent, TargetsNotificationDialogData,
|
||||||
|
NotificationTarget>(TargetNotificationDialogComponent, {
|
||||||
|
disableClose: true,
|
||||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||||
|
data: {}
|
||||||
|
}).afterClosed()
|
||||||
|
.subscribe((res) => {
|
||||||
|
if (res) {
|
||||||
|
let formValue: string[] = this.escalationFormGroup.get('targets').value;
|
||||||
|
if (!formValue) {
|
||||||
|
formValue = [];
|
||||||
|
}
|
||||||
|
formValue.push(res.id.id);
|
||||||
|
this.escalationFormGroup.get('targets').patchValue(formValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public validate(c: FormControl) {
|
public validate(c: FormControl) {
|
||||||
return (this.escalationFormGroup.valid) ? null : {
|
return (this.escalationFormGroup.valid) ? null : {
|
||||||
escalation: {
|
escalation: {
|
||||||
|
|||||||
@ -21,7 +21,10 @@
|
|||||||
*ngFor="let escalationControl of escalationsFormArray.controls;
|
*ngFor="let escalationControl of escalationsFormArray.controls;
|
||||||
let $index = index; last as isLast;"
|
let $index = index; last as isLast;"
|
||||||
[ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
|
[ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
|
||||||
<tb-escalation-form fxFlex [formControl]="escalationControl" [systemEscalation]="$index === 0"></tb-escalation-form>
|
<tb-escalation-form fxFlex
|
||||||
|
[formControl]="escalationControl"
|
||||||
|
[systemEscalation]="$index === 0">
|
||||||
|
</tb-escalation-form>
|
||||||
<span *ngIf="$index === 0" style="width: 40px;"></span>
|
<span *ngIf="$index === 0" style="width: 40px;"></span>
|
||||||
<button *ngIf="!($index === 0) && !disabled" mat-icon-button style="min-width: 40px;"
|
<button *ngIf="!($index === 0) && !disabled" mat-icon-button style="min-width: 40px;"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -31,7 +31,6 @@ import { Store } from '@ngrx/store';
|
|||||||
import { AppState } from '@app/core/core.state';
|
import { AppState } from '@app/core/core.state';
|
||||||
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { UtilsService } from '@core/services/utils.service';
|
|
||||||
import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models';
|
import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -79,7 +78,6 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
|
|||||||
private propagateChange = (v: any) => { };
|
private propagateChange = (v: any) => { };
|
||||||
|
|
||||||
constructor(private store: Store<AppState>,
|
constructor(private store: Store<AppState>,
|
||||||
private utils: UtilsService,
|
|
||||||
private fb: FormBuilder) {
|
private fb: FormBuilder) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,15 +118,18 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
|
|||||||
|
|
||||||
writeValue(escalations: {[key: string]: Array<string>} | null): void {
|
writeValue(escalations: {[key: string]: Array<string>} | null): void {
|
||||||
const escalationParse: Array<NonConfirmedNotificationEscalation> = [];
|
const escalationParse: Array<NonConfirmedNotificationEscalation> = [];
|
||||||
|
// tslint:disable-next-line:forin
|
||||||
for (const escalation in escalations) {
|
for (const escalation in escalations) {
|
||||||
escalationParse.push({delayInSec: Number(escalation), targets: escalations[escalation]});
|
escalationParse.push({delayInSec: Number(escalation), targets: escalations[escalation]});
|
||||||
}
|
}
|
||||||
if (escalationParse?.length === this.escalationsFormArray.length) {
|
if (escalationParse.length === 0) {
|
||||||
|
this.addEscalation();
|
||||||
|
} else if (escalationParse?.length === this.escalationsFormArray.length) {
|
||||||
this.escalationsFormArray.patchValue(escalationParse, {emitEvent: false});
|
this.escalationsFormArray.patchValue(escalationParse, {emitEvent: false});
|
||||||
} else {
|
} else {
|
||||||
const escalationsControls: Array<AbstractControl> = [];
|
const escalationsControls: Array<AbstractControl> = [];
|
||||||
if (escalationParse) {
|
if (escalationParse) {
|
||||||
escalationParse.forEach((escalation, index) => {
|
escalationParse.forEach(escalation => {
|
||||||
escalationsControls.push(this.fb.control(escalation, [Validators.required]));
|
escalationsControls.push(this.fb.control(escalation, [Validators.required]));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -56,11 +56,32 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<tb-template-autocomplete
|
<tb-template-autocomplete
|
||||||
required
|
required
|
||||||
|
allowCreate
|
||||||
formControlName="templateId"
|
formControlName="templateId"
|
||||||
[notificationTypes]="ruleNotificationForm.get('triggerType').value"
|
[notificationTypes]="ruleNotificationForm.get('triggerType').value"
|
||||||
labelText="notification.template-name"
|
labelText="notification.template-name"
|
||||||
requiredText="notification.template-required">
|
requiredText="notification.template-required">
|
||||||
</tb-template-autocomplete>
|
</tb-template-autocomplete>
|
||||||
|
<section formGroupName="recipientsConfig"
|
||||||
|
*ngIf="ruleNotificationForm.get('triggerType').value !== triggerType.ALARM; else alarmTargesConfig">
|
||||||
|
<tb-entity-list
|
||||||
|
required
|
||||||
|
formControlName="targets"
|
||||||
|
[entityType]="entityType.NOTIFICATION_TARGET"
|
||||||
|
placeholderText="{{ 'notification.target' | translate }}">
|
||||||
|
<button #createTargetButton
|
||||||
|
mat-button color="primary" matSuffix
|
||||||
|
(click)="createTarget($event, createTargetButton)">
|
||||||
|
{{ 'notification.create-new' | translate }}
|
||||||
|
</button>
|
||||||
|
</tb-entity-list>
|
||||||
|
</section>
|
||||||
|
<ng-template #alarmTargesConfig>
|
||||||
|
<fieldset class="fields-group" formGroupName="recipientsConfig">
|
||||||
|
<legend translate>notification.hierarchy-of-receiving</legend>
|
||||||
|
<tb-escalations-component formControlName="escalationTable"></tb-escalations-component>
|
||||||
|
</fieldset>
|
||||||
|
</ng-template>
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
</mat-step>
|
||||||
<mat-step [stepControl]="alarmTemplateForm"
|
<mat-step [stepControl]="alarmTemplateForm"
|
||||||
@ -94,11 +115,6 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<fieldset class="fields-group" formGroupName="recipientsConfig">
|
|
||||||
<legend translate>notification.hierarchy-of-receiving</legend>
|
|
||||||
<tb-escalations-component formControlName="escalationTable"></tb-escalations-component>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
</form>
|
||||||
<form [formGroup]="ruleNotificationForm">
|
<form [formGroup]="ruleNotificationForm">
|
||||||
<section formGroupName="additionalConfig">
|
<section formGroupName="additionalConfig">
|
||||||
@ -143,14 +159,6 @@
|
|||||||
<div class="tb-hint" translate>notification.device-profiles-list-rule-hint</div>
|
<div class="tb-hint" translate>notification.device-profiles-list-rule-hint</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</section>
|
</section>
|
||||||
<section formGroupName="recipientsConfig">
|
|
||||||
<tb-entity-list
|
|
||||||
required
|
|
||||||
formControlName="targets"
|
|
||||||
[entityType]="entityType.NOTIFICATION_TARGET"
|
|
||||||
placeholderText="{{ 'notification.target' | translate }}">
|
|
||||||
</tb-entity-list>
|
|
||||||
</section>
|
|
||||||
</form>
|
</form>
|
||||||
<form [formGroup]="ruleNotificationForm">
|
<form [formGroup]="ruleNotificationForm">
|
||||||
<section formGroupName="additionalConfig">
|
<section formGroupName="additionalConfig">
|
||||||
@ -180,14 +188,6 @@
|
|||||||
<mat-slide-toggle formControlName="deleted">{{ 'notification.deleted' | translate }}</mat-slide-toggle>
|
<mat-slide-toggle formControlName="deleted">{{ 'notification.deleted' | translate }}</mat-slide-toggle>
|
||||||
</section>
|
</section>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<section formGroupName="recipientsConfig">
|
|
||||||
<tb-entity-list
|
|
||||||
required
|
|
||||||
formControlName="targets"
|
|
||||||
[entityType]="entityType.NOTIFICATION_TARGET"
|
|
||||||
placeholderText="notification.target">
|
|
||||||
</tb-entity-list>
|
|
||||||
</section>
|
|
||||||
</form>
|
</form>
|
||||||
<form [formGroup]="ruleNotificationForm">
|
<form [formGroup]="ruleNotificationForm">
|
||||||
<section formGroupName="additionalConfig">
|
<section formGroupName="additionalConfig">
|
||||||
@ -203,14 +203,7 @@
|
|||||||
[stepControl]="alarmCommentTemplateForm">
|
[stepControl]="alarmCommentTemplateForm">
|
||||||
<ng-template matStepLabel>{{ 'notification.alarm-comment-trigger-settings' | translate }}</ng-template>
|
<ng-template matStepLabel>{{ 'notification.alarm-comment-trigger-settings' | translate }}</ng-template>
|
||||||
<form [formGroup]="alarmCommentTemplateForm">
|
<form [formGroup]="alarmCommentTemplateForm">
|
||||||
<section formGroupName="recipientsConfig">
|
|
||||||
<tb-entity-list
|
|
||||||
required
|
|
||||||
formControlName="targets"
|
|
||||||
[entityType]="entityType.NOTIFICATION_TARGET"
|
|
||||||
placeholderText="{{ 'notification.target' | translate }}">
|
|
||||||
</tb-entity-list>
|
|
||||||
</section>
|
|
||||||
</form>
|
</form>
|
||||||
<form [formGroup]="ruleNotificationForm">
|
<form [formGroup]="ruleNotificationForm">
|
||||||
<section formGroupName="additionalConfig">;
|
<section formGroupName="additionalConfig">;
|
||||||
|
|||||||
@ -14,13 +14,18 @@
|
|||||||
/// limitations under the License.
|
/// limitations under the License.
|
||||||
///
|
///
|
||||||
|
|
||||||
import { NotificationRule, TriggerType, TriggerTypeTranslationMap } from '@shared/models/notification.models';
|
import {
|
||||||
|
NotificationRule,
|
||||||
|
NotificationTarget,
|
||||||
|
TriggerType,
|
||||||
|
TriggerTypeTranslationMap
|
||||||
|
} from '@shared/models/notification.models';
|
||||||
import { Component, Inject, OnDestroy, ViewChild } from '@angular/core';
|
import { Component, Inject, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { DialogComponent } from '@shared/components/dialog.component';
|
import { DialogComponent } from '@shared/components/dialog.component';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { NotificationService } from '@core/http/notification.service';
|
import { NotificationService } from '@core/http/notification.service';
|
||||||
import { EntityType } from '@shared/models/entity-type.models';
|
import { EntityType } from '@shared/models/entity-type.models';
|
||||||
@ -33,6 +38,11 @@ import { MediaBreakpoints } from '@shared/models/constants';
|
|||||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||||
import { AlarmStatus, alarmStatusTranslations } from '@shared/models/alarm.models';
|
import { AlarmStatus, alarmStatusTranslations } from '@shared/models/alarm.models';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
TargetNotificationDialogComponent,
|
||||||
|
TargetsNotificationDialogData
|
||||||
|
} from '@home/pages/notification-center/targets-table/target-notification-dialog.componet';
|
||||||
|
import { MatButton } from '@angular/material/button';
|
||||||
|
|
||||||
export interface RuleNotificationDialogData {
|
export interface RuleNotificationDialogData {
|
||||||
rule?: NotificationRule;
|
rule?: NotificationRule;
|
||||||
@ -86,7 +96,8 @@ export class RuleNotificationDialogComponent extends
|
|||||||
private breakpointObserver: BreakpointObserver,
|
private breakpointObserver: BreakpointObserver,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
public translate: TranslateService,
|
public translate: TranslateService,
|
||||||
private notificationService: NotificationService) {
|
private notificationService: NotificationService,
|
||||||
|
private dialog: MatDialog) {
|
||||||
super(store, router, dialogRef);
|
super(store, router, dialogRef);
|
||||||
|
|
||||||
if (isDefined(data.isAdd)) {
|
if (isDefined(data.isAdd)) {
|
||||||
@ -100,13 +111,28 @@ export class RuleNotificationDialogComponent extends
|
|||||||
name: [null, Validators.required],
|
name: [null, Validators.required],
|
||||||
templateId: [null, Validators.required],
|
templateId: [null, Validators.required],
|
||||||
triggerType: [TriggerType.ALARM, Validators.required],
|
triggerType: [TriggerType.ALARM, Validators.required],
|
||||||
recipientsConfig: [null],
|
recipientsConfig: this.fb.group({
|
||||||
|
targets: [{value: null, disabled: true}, Validators.required],
|
||||||
|
escalationTable: [null, Validators.required]
|
||||||
|
}),
|
||||||
triggerConfig: [null],
|
triggerConfig: [null],
|
||||||
additionalConfig: this.fb.group({
|
additionalConfig: this.fb.group({
|
||||||
description: ['']
|
description: ['']
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.ruleNotificationForm.get('triggerType').valueChanges.pipe(
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(value => {
|
||||||
|
if (value === TriggerType.ALARM) {
|
||||||
|
this.ruleNotificationForm.get('recipientsConfig.escalationTable').enable({emitEvent: false});
|
||||||
|
this.ruleNotificationForm.get('recipientsConfig.targets').disable({emitEvent: false});
|
||||||
|
} else {
|
||||||
|
this.ruleNotificationForm.get('recipientsConfig.escalationTable').disable({emitEvent: false});
|
||||||
|
this.ruleNotificationForm.get('recipientsConfig.targets').enable({emitEvent: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.alarmTemplateForm = this.fb.group({
|
this.alarmTemplateForm = this.fb.group({
|
||||||
triggerConfig: this.fb.group({
|
triggerConfig: this.fb.group({
|
||||||
alarmTypes: [null],
|
alarmTypes: [null],
|
||||||
@ -114,9 +140,6 @@ export class RuleNotificationDialogComponent extends
|
|||||||
clearRule: this.fb.group({
|
clearRule: this.fb.group({
|
||||||
alarmStatus: [null]
|
alarmStatus: [null]
|
||||||
})
|
})
|
||||||
}),
|
|
||||||
recipientsConfig: this.fb.group({
|
|
||||||
escalationTable: []
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -125,9 +148,6 @@ export class RuleNotificationDialogComponent extends
|
|||||||
filterByDevice: [true],
|
filterByDevice: [true],
|
||||||
devices: [null],
|
devices: [null],
|
||||||
deviceProfiles: [{value: null, disabled: true}]
|
deviceProfiles: [{value: null, disabled: true}]
|
||||||
}),
|
|
||||||
recipientsConfig: this.fb.group({
|
|
||||||
targets: [[], Validators.required]
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -149,17 +169,10 @@ export class RuleNotificationDialogComponent extends
|
|||||||
created: [false],
|
created: [false],
|
||||||
updated: [false],
|
updated: [false],
|
||||||
deleted: [false]
|
deleted: [false]
|
||||||
}),
|
|
||||||
recipientsConfig: this.fb.group({
|
|
||||||
targets: [[], Validators.required]
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.alarmCommentTemplateForm = this.fb.group({
|
this.alarmCommentTemplateForm = this.fb.group({ });
|
||||||
recipientsConfig: this.fb.group({
|
|
||||||
targets: [[], Validators.required]
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
this.triggerTypeFormsMap = new Map<TriggerType, FormGroup>([
|
this.triggerTypeFormsMap = new Map<TriggerType, FormGroup>([
|
||||||
[TriggerType.ALARM, this.alarmTemplateForm],
|
[TriggerType.ALARM, this.alarmTemplateForm],
|
||||||
@ -179,6 +192,7 @@ export class RuleNotificationDialogComponent extends
|
|||||||
}
|
}
|
||||||
this.ruleNotificationForm.reset({}, {emitEvent: false});
|
this.ruleNotificationForm.reset({}, {emitEvent: false});
|
||||||
this.ruleNotificationForm.patchValue(this.ruleNotification, {emitEvent: false});
|
this.ruleNotificationForm.patchValue(this.ruleNotification, {emitEvent: false});
|
||||||
|
this.ruleNotificationForm.get('triggerType').updateValueAndValidity({onlySelf: true});
|
||||||
const currentForm = this.triggerTypeFormsMap.get(this.ruleNotification.triggerType);
|
const currentForm = this.triggerTypeFormsMap.get(this.ruleNotification.triggerType);
|
||||||
currentForm.patchValue(this.ruleNotification, {emitEvent: false});
|
currentForm.patchValue(this.ruleNotification, {emitEvent: false});
|
||||||
if (this.ruleNotification.triggerType === TriggerType.DEVICE_INACTIVITY) {
|
if (this.ruleNotification.triggerType === TriggerType.DEVICE_INACTIVITY) {
|
||||||
@ -188,6 +202,12 @@ export class RuleNotificationDialogComponent extends
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
changeStep($event: StepperSelectionEvent) {
|
changeStep($event: StepperSelectionEvent) {
|
||||||
this.selectedIndex = $event.selectedIndex;
|
this.selectedIndex = $event.selectedIndex;
|
||||||
}
|
}
|
||||||
@ -247,13 +267,30 @@ export class RuleNotificationDialogComponent extends
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
super.ngOnDestroy();
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(): void {
|
cancel(): void {
|
||||||
this.dialogRef.close(null);
|
this.dialogRef.close(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createTarget($event: Event, button: MatButton) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
button._elementRef.nativeElement.blur();
|
||||||
|
this.dialog.open<TargetNotificationDialogComponent, TargetsNotificationDialogData,
|
||||||
|
NotificationTarget>(TargetNotificationDialogComponent, {
|
||||||
|
disableClose: true,
|
||||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||||
|
data: {}
|
||||||
|
}).afterClosed()
|
||||||
|
.subscribe((res) => {
|
||||||
|
if (res) {
|
||||||
|
let formValue: string[] = this.ruleNotificationForm.get('recipientsConfig.targets').value;
|
||||||
|
if (!formValue) {
|
||||||
|
formValue = [];
|
||||||
|
}
|
||||||
|
formValue.push(res.id.id);
|
||||||
|
this.ruleNotificationForm.get('recipientsConfig.targets').patchValue(formValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,7 +145,6 @@ export class TargetNotificationDialogComponent extends
|
|||||||
if (isDefined(this.data.target)) {
|
if (isDefined(this.data.target)) {
|
||||||
formValue = Object.assign({}, this.data.target, formValue);
|
formValue = Object.assign({}, this.data.target, formValue);
|
||||||
}
|
}
|
||||||
formValue.type = formValue.configuration.type;
|
|
||||||
this.notificationService.saveNotificationTarget(formValue).subscribe(
|
this.notificationService.saveNotificationTarget(formValue).subscribe(
|
||||||
(target) => this.dialogRef.close(target)
|
(target) => this.dialogRef.close(target)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -28,6 +28,12 @@
|
|||||||
(click)="clear()">
|
(click)="clear()">
|
||||||
<mat-icon class="material-icons">close</mat-icon>
|
<mat-icon class="material-icons">close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
<button #createTemplateButton
|
||||||
|
mat-button color="primary" matSuffix
|
||||||
|
*ngIf="allowCreate && !selectTemplateFormGroup.get('templateName').value && !disabled"
|
||||||
|
(click)="createTemplate($event, createTemplateButton)">
|
||||||
|
{{ 'notification.create-new' | translate }}
|
||||||
|
</button>
|
||||||
<mat-autocomplete class="tb-autocomplete"
|
<mat-autocomplete class="tb-autocomplete"
|
||||||
#templateAutocomplete="matAutocomplete"
|
#templateAutocomplete="matAutocomplete"
|
||||||
[displayWith]="displayTemplateFn"
|
[displayWith]="displayTemplateFn"
|
||||||
|
|||||||
@ -35,6 +35,12 @@ import {
|
|||||||
} from '@shared/models/notification.models';
|
} from '@shared/models/notification.models';
|
||||||
import { NotificationService } from '@core/http/notification.service';
|
import { NotificationService } from '@core/http/notification.service';
|
||||||
import { isEqual } from '@core/utils';
|
import { isEqual } from '@core/utils';
|
||||||
|
import {
|
||||||
|
TemplateNotificationDialogComponent,
|
||||||
|
TemplateNotificationDialogData
|
||||||
|
} from '@home/pages/notification-center/template-table/template-notification-dialog.component';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatButton } from '@angular/material/button';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-template-autocomplete',
|
selector: 'tb-template-autocomplete',
|
||||||
@ -66,6 +72,16 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
|
|||||||
this.requiredValue = coerceBooleanProperty(value);
|
this.requiredValue = coerceBooleanProperty(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private allowCreateValue = false;
|
||||||
|
get allowCreate(): boolean {
|
||||||
|
return this.allowCreateValue;
|
||||||
|
}
|
||||||
|
@Input()
|
||||||
|
set allowCreate(value: boolean) {
|
||||||
|
this.allowCreateValue = coerceBooleanProperty(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
||||||
@ -97,7 +113,8 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
|
|||||||
public truncate: TruncatePipe,
|
public truncate: TruncatePipe,
|
||||||
private entityService: EntityService,
|
private entityService: EntityService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private fb: FormBuilder) {
|
private fb: FormBuilder,
|
||||||
|
private dialog: MatDialog) {
|
||||||
this.selectTemplateFormGroup = this.fb.group({
|
this.selectTemplateFormGroup = this.fb.group({
|
||||||
templateName: [null]
|
templateName: [null]
|
||||||
});
|
});
|
||||||
@ -192,6 +209,26 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createTemplate($event: Event, button: MatButton) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
button._elementRef.nativeElement.blur();
|
||||||
|
this.dialog.open<TemplateNotificationDialogComponent, TemplateNotificationDialogData,
|
||||||
|
NotificationTemplate>(TemplateNotificationDialogComponent, {
|
||||||
|
disableClose: true,
|
||||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||||
|
data: {
|
||||||
|
predefinedType: this.notificationTypes
|
||||||
|
}
|
||||||
|
}).afterClosed()
|
||||||
|
.subscribe((res) => {
|
||||||
|
if (res) {
|
||||||
|
this.selectTemplateFormGroup.get('templateName').patchValue(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private updateView(value: EntityId | null) {
|
private updateView(value: EntityId | null) {
|
||||||
if (!isEqual(this.modelValue, value)) {
|
if (!isEqual(this.modelValue, value)) {
|
||||||
this.modelValue = value;
|
this.modelValue = value;
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
{{ 'notification.target-name-required' | translate }}
|
{{ 'notification.target-name-required' | translate }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field fxFlex class="mat-block">
|
<mat-form-field fxFlex class="mat-block" *ngIf="!hideSelectType">
|
||||||
<mat-label translate>notification.type</mat-label>
|
<mat-label translate>notification.type</mat-label>
|
||||||
<mat-select formControlName="notificationType">
|
<mat-select formControlName="notificationType">
|
||||||
<mat-option *ngFor="let notificationType of notificationTypes" [value]="notificationType">
|
<mat-option *ngFor="let notificationType of notificationTypes" [value]="notificationType">
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { Router } from '@angular/router';
|
|||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms';
|
||||||
import { NotificationService } from '@core/http/notification.service';
|
import { NotificationService } from '@core/http/notification.service';
|
||||||
import { deepClone, deepTrim } from '@core/utils';
|
import { deepClone, deepTrim, isDefinedAndNotNull } from '@core/utils';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { map, takeUntil } from 'rxjs/operators';
|
import { map, takeUntil } from 'rxjs/operators';
|
||||||
import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper';
|
import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||||
@ -40,6 +40,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
|
|
||||||
export interface TemplateNotificationDialogData {
|
export interface TemplateNotificationDialogData {
|
||||||
template?: NotificationTemplate;
|
template?: NotificationTemplate;
|
||||||
|
predefinedType?: NotificationType;
|
||||||
isAdd?: boolean;
|
isAdd?: boolean;
|
||||||
isCopy?: boolean;
|
isCopy?: boolean;
|
||||||
}
|
}
|
||||||
@ -69,6 +70,7 @@ export class TemplateNotificationDialogComponent
|
|||||||
notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap;
|
notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap;
|
||||||
|
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
|
hideSelectType = false;
|
||||||
|
|
||||||
tinyMceOptions: Record<string, any> = {
|
tinyMceOptions: Record<string, any> = {
|
||||||
base_url: '/assets/tinymce',
|
base_url: '/assets/tinymce',
|
||||||
@ -100,9 +102,13 @@ export class TemplateNotificationDialogComponent
|
|||||||
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
|
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
|
||||||
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
|
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
|
||||||
|
|
||||||
|
if (isDefinedAndNotNull(this.data?.predefinedType)) {
|
||||||
|
this.hideSelectType = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.templateNotificationForm = this.fb.group({
|
this.templateNotificationForm = this.fb.group({
|
||||||
name: ['', Validators.required],
|
name: ['', Validators.required],
|
||||||
notificationType: [NotificationType.GENERAL],
|
notificationType: [this.hideSelectType ? this.data.predefinedType : NotificationType.GENERAL],
|
||||||
configuration: this.fb.group({
|
configuration: this.fb.group({
|
||||||
notificationSubject: ['', Validators.required],
|
notificationSubject: ['', Validators.required],
|
||||||
defaultTextTemplate: ['', Validators.required],
|
defaultTextTemplate: ['', Validators.required],
|
||||||
|
|||||||
@ -52,4 +52,7 @@
|
|||||||
<mat-error *ngIf="entityListFormGroup.get('entities').hasError('required')">
|
<mat-error *ngIf="entityListFormGroup.get('entities').hasError('required')">
|
||||||
{{ requiredText }}
|
{{ requiredText }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
|
<div matSuffix>
|
||||||
|
<ng-content select="[matSuffix]"></ng-content>
|
||||||
|
</div>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|||||||
@ -131,7 +131,6 @@ export interface NonConfirmedNotificationEscalation {
|
|||||||
|
|
||||||
export interface NotificationTarget extends Omit<BaseData<NotificationTargetId>, 'label'>{
|
export interface NotificationTarget extends Omit<BaseData<NotificationTargetId>, 'label'>{
|
||||||
tenantId: TenantId;
|
tenantId: TenantId;
|
||||||
type: NotificationTargetType;
|
|
||||||
configuration: NotificationTargetConfig;
|
configuration: NotificationTargetConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,14 +39,14 @@ export class DateAgoPipe implements PipeTransform {
|
|||||||
|
|
||||||
transform(value: number): string {
|
transform(value: number): string {
|
||||||
if (value) {
|
if (value) {
|
||||||
const seconds = Math.floor((+new Date() - +new Date(value)) / 1000);
|
const ms = Math.floor((+new Date() - +new Date(value)));
|
||||||
if (seconds < 29) { // less than 30 seconds ago will show as 'Just now'
|
if (ms < 29 * SECOND) { // less than 30 seconds ago will show as 'Just now'
|
||||||
return this.translate.instant('timewindow.just-now');
|
return this.translate.instant('timewindow.just-now');
|
||||||
}
|
}
|
||||||
let counter;
|
let counter;
|
||||||
// tslint:disable-next-line:forin
|
// tslint:disable-next-line:forin
|
||||||
for (const i in intervals) {
|
for (const i in intervals) {
|
||||||
counter = Math.floor(seconds / intervals[i]);
|
counter = Math.floor(ms / intervals[i]);
|
||||||
if (counter > 0) {
|
if (counter > 0) {
|
||||||
return this.translate.instant(`timewindow.${i}`, {[i]: counter});
|
return this.translate.instant(`timewindow.${i}`, {[i]: counter});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2710,6 +2710,7 @@
|
|||||||
"copy-rule": "Copy rule",
|
"copy-rule": "Copy rule",
|
||||||
"copy-template": "Copy template",
|
"copy-template": "Copy template",
|
||||||
"create-target": "Create recipient",
|
"create-target": "Create recipient",
|
||||||
|
"create-new": "Create new",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"created-time": "Created time",
|
"created-time": "Created time",
|
||||||
"delete-request-text": "Be careful, after the confirmation the notification request will become unrecoverable.",
|
"delete-request-text": "Be careful, after the confirmation the notification request will become unrecoverable.",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user