Browse Source

Relation edit dialog improvements. Minor improvements.

pull/2147/head
Igor Kulikov 7 years ago
parent
commit
52ab3d6dea
  1. 13
      ui-ngx/src/app/core/guards/auth.guard.ts
  2. 23
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  3. 13
      ui-ngx/src/app/core/notification/notification.actions.ts
  4. 9
      ui-ngx/src/app/core/notification/notification.effects.ts
  5. 5
      ui-ngx/src/app/core/notification/notification.models.ts
  6. 5
      ui-ngx/src/app/core/notification/notification.reducer.ts
  7. 28
      ui-ngx/src/app/core/services/dialog.service.ts
  8. 13
      ui-ngx/src/app/core/services/notification.service.ts
  9. 9
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts
  10. 7
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts
  11. 7
      ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html
  12. 31
      ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts
  13. 8
      ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts
  14. 8
      ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts
  15. 4
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html
  16. 4
      ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html
  17. 7
      ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts
  18. 8
      ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts
  19. 8
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
  20. 4
      ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html
  21. 7
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts
  22. 4
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html
  23. 4
      ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.html
  24. 7
      ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts
  25. 7
      ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts
  26. 51
      ui-ngx/src/app/shared/components/dialog.component.ts
  27. 1
      ui-ngx/src/app/shared/components/entity/entity-list.component.html
  28. 26
      ui-ngx/src/app/shared/components/entity/entity-list.component.ts
  29. 21
      ui-ngx/src/app/shared/components/fullscreen.directive.ts
  30. 6
      ui-ngx/src/app/shared/components/json-object-edit.component.html
  31. 23
      ui-ngx/src/app/shared/components/json-object-edit.component.scss
  32. 52
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  33. 4
      ui-ngx/src/app/shared/components/page.component.ts
  34. 15
      ui-ngx/src/app/shared/components/toast.directive.ts

13
ui-ngx/src/app/core/guards/auth.guard.ts

@ -82,18 +82,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
} else {
const authority = Authority[authState.authUser.authority];
if (data.auth && data.auth.indexOf(authority) === -1) {
this.dialogService.confirm(
this.translate.instant('access.access-forbidden'),
this.translate.instant('access.access-forbidden-text'),
this.translate.instant('action.cancel'),
this.translate.instant('action.sign-in'),
true
).subscribe((res) => {
if (res) {
this.authService.logout();
}
}
);
this.dialogService.forbidden();
return false;
} else {
return true;

23
ui-ngx/src/app/core/interceptors/global-http-interceptor.ts

@ -50,6 +50,8 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
'/api/auth/token'
];
private activeRequests = 0;
constructor(private store: Store<AppState>,
private dialogService: DialogService,
private translate: TranslateService,
@ -135,7 +137,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
} else if (errorResponse.status === 403) {
if (!ignoreErrors) {
this.permissionDenied();
this.dialogService.forbidden();
}
} else if (errorResponse.status === 0 || errorResponse.status === -1) {
this.showError('Unable to connect');
@ -241,7 +243,16 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
private updateLoadingState(config: InterceptorConfig, isLoading: boolean) {
if (!config.ignoreLoading) {
this.store.dispatch(isLoading ? new ActionLoadStart() : new ActionLoadFinish());
if (isLoading) {
this.activeRequests++;
} else {
this.activeRequests--;
}
if (this.activeRequests === 1) {
this.store.dispatch(new ActionLoadStart());
} else if (this.activeRequests === 0) {
this.store.dispatch(new ActionLoadFinish());
}
}
}
@ -253,14 +264,6 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
}
private permissionDenied() {
this.dialogService.alert(
this.translate.instant('access.permission-denied'),
this.translate.instant('access.permission-denied-text'),
this.translate.instant('action.close')
);
}
private showError(error: string, timeout: number = 0) {
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: error, type: 'error'}));

13
ui-ngx/src/app/core/notification/notification.actions.ts

@ -15,10 +15,11 @@
///
import { Action } from '@ngrx/store';
import { NotificationMessage } from '@app/core/notification/notification.models';
import { NotificationMessage, HideNotification } from '@app/core/notification/notification.models';
export enum NotificationActionTypes {
SHOW_NOTIFICATION = '[Notification] Show'
SHOW_NOTIFICATION = '[Notification] Show',
HIDE_NOTIFICATION = '[Notification] Hide'
}
export class ActionNotificationShow implements Action {
@ -27,5 +28,11 @@ export class ActionNotificationShow implements Action {
constructor(readonly notification: NotificationMessage ) {}
}
export class ActionNotificationHide implements Action {
readonly type = NotificationActionTypes.HIDE_NOTIFICATION;
constructor(readonly hideNotification: HideNotification ) {}
}
export type NotificationActions =
| ActionNotificationShow;
| ActionNotificationShow | ActionNotificationHide;

9
ui-ngx/src/app/core/notification/notification.effects.ts

@ -44,4 +44,13 @@ export class NotificationEffects {
})
);
@Effect({dispatch: false})
hideNotification = this.actions$.pipe(
ofType(
NotificationActionTypes.HIDE_NOTIFICATION,
),
map(({ hideNotification }) => {
this.notificationService.hideNotification(hideNotification);
})
);
}

5
ui-ngx/src/app/core/notification/notification.models.ts

@ -17,6 +17,7 @@
export interface NotificationState {
notification: NotificationMessage;
hideNotification: HideNotification;
}
export declare type NotificationType = 'info' | 'success' | 'error';
@ -31,3 +32,7 @@ export class NotificationMessage {
horizontalPosition?: NotificationHorizontalPosition;
verticalPosition?: NotificationVerticalPosition;
}
export class HideNotification {
target?: string;
}

5
ui-ngx/src/app/core/notification/notification.reducer.ts

@ -18,7 +18,8 @@ import { NotificationState } from './notification.models';
import { NotificationActions, NotificationActionTypes } from './notification.actions';
export const initialState: NotificationState = {
notification: null
notification: null,
hideNotification: null
};
export function notificationReducer(
@ -28,6 +29,8 @@ export function notificationReducer(
switch (action.type) {
case NotificationActionTypes.SHOW_NOTIFICATION:
return { ...state, notification: action.notification };
case NotificationActionTypes.HIDE_NOTIFICATION:
return { ...state, hideNotification: action.hideNotification };
default:
return state;
}

28
ui-ngx/src/app/core/services/dialog.service.ts

@ -20,7 +20,8 @@ import { MatDialog, MatDialogConfig } from '@angular/material';
import { ConfirmDialogComponent } from '@core/services/dialog/confirm-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component';
import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component";
import { TodoDialogComponent } from '@core/services/dialog/todo-dialog.component';
import { AuthService } from '@core/auth/auth.service';
@Injectable(
{
@ -31,6 +32,7 @@ export class DialogService {
constructor(
private translate: TranslateService,
private authService: AuthService,
public dialog: MatDialog
) {
}
@ -68,6 +70,30 @@ export class DialogService {
return dialogRef.afterClosed();
}
private permissionDenied() {
this.alert(
this.translate.instant('access.permission-denied'),
this.translate.instant('access.permission-denied-text'),
this.translate.instant('action.close')
);
}
forbidden(): Observable<boolean> {
const observable = this.confirm(
this.translate.instant('access.access-forbidden'),
this.translate.instant('access.access-forbidden-text'),
this.translate.instant('action.cancel'),
this.translate.instant('action.sign-in'),
true
);
observable.subscribe((res) => {
if (res) {
this.authService.logout();
}
});
return observable;
}
todo(): Observable<any> {
const dialogConfig: MatDialogConfig = {
disableClose: true,

13
ui-ngx/src/app/core/services/notification.service.ts

@ -15,7 +15,7 @@
///
import { Injectable } from '@angular/core';
import { NotificationMessage } from '@app/core/notification/notification.models';
import { HideNotification, NotificationMessage } from '@app/core/notification/notification.models';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
@ -28,6 +28,8 @@ export class NotificationService {
private notificationSubject: Subject<NotificationMessage> = new Subject();
private hideNotificationSubject: Subject<HideNotification> = new Subject();
constructor(
) {
}
@ -36,8 +38,15 @@ export class NotificationService {
this.notificationSubject.next(notification);
}
hideNotification(hideNotification: HideNotification) {
this.hideNotificationSubject.next(hideNotification);
}
getNotification(): Observable<NotificationMessage> {
return this.notificationSubject;
return this.notificationSubject.asObservable();
}
getHideNotification(): Observable<HideNotification> {
return this.hideNotificationSubject.asObservable();
}
}

9
ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts

@ -30,6 +30,8 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { ActionStatus, AuditLog } from '@shared/models/audit-log.models';
import * as ace from 'ace-builds';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface AuditLogDetailsDialogData {
auditLog: AuditLog;
@ -40,7 +42,7 @@ export interface AuditLogDetailsDialogData {
templateUrl: './audit-log-details-dialog.component.html',
styleUrls: ['./audit-log-details-dialog.component.scss']
})
export class AuditLogDetailsDialogComponent extends PageComponent implements OnInit {
export class AuditLogDetailsDialogComponent extends DialogComponent<AuditLogDetailsDialogComponent> implements OnInit {
@ViewChild('actionDataEditor', {static: true})
actionDataEditorElmRef: ElementRef;
@ -58,10 +60,11 @@ export class AuditLogDetailsDialogComponent extends PageComponent implements OnI
@ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AuditLogDetailsDialogData,
public dialogRef: MatDialogRef<AuditLogDetailsDialogComponent>,
private renderer: Renderer2) {
super(store);
super(store, router, dialogRef);
}
ngOnInit(): void {
@ -114,7 +117,7 @@ export class AuditLogDetailsDialogComponent extends PageComponent implements OnI
});
newWidth = 8 * maxLineLength + 16;
}
newHeight = Math.min(400, newHeight);
// newHeight = Math.min(400, newHeight);
this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px');
this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px');
this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px');

7
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts

@ -27,6 +27,8 @@ import {TbAnchorComponent} from '@shared/components/tb-anchor.component';
import {EntityComponent} from './entity.component';
import {EntityTableConfig} from '@home/models/entity/entities-table-config.models';
import {AddEntityDialogData} from '@home/models/entity/entity-component.models';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
@Component({
selector: 'tb-add-entity-dialog',
@ -34,7 +36,7 @@ import {AddEntityDialogData} from '@home/models/entity/entity-component.models';
providers: [{provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent}],
styleUrls: ['./add-entity-dialog.component.scss']
})
export class AddEntityDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
export class AddEntityDialogComponent extends DialogComponent<AddEntityDialogComponent, BaseData<HasId>> implements OnInit, ErrorStateMatcher {
entityComponent: EntityComponent<BaseData<HasId>>;
detailsForm: NgForm;
@ -49,11 +51,12 @@ export class AddEntityDialogComponent extends PageComponent implements OnInit, E
@ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<HasId>>,
public dialogRef: MatDialogRef<AddEntityDialogComponent, BaseData<HasId>>,
private componentFactoryResolver: ComponentFactoryResolver,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
super(store);
super(store, router, dialogRef);
}
ngOnInit(): void {

7
ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html

@ -41,11 +41,12 @@
required="true">
</tb-entity-list-select>
<tb-json-object-edit
formControlName="additionalInfo"
#additionalInfoEdit
[formControl]="additionalInfo"
label="{{ 'relation.additional-info' | translate }}">
</tb-json-object-edit>
<div class="tb-error-messages" *ngIf="submitted &&
relationFormGroup.get('additionalInfo').invalid" role="alert">
additionalInfo.invalid" role="alert">
<div translate class="tb-error-message">relation.invalid-additional-info</div>
</div>
</fieldset>
@ -54,7 +55,7 @@
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async)">
[disabled]="(isLoading$ | async) || relationFormGroup.invalid || !(relationFormGroup.dirty || additionalInfo.dirty)">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
<button mat-button color="primary"

31
ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.ts

@ -14,9 +14,8 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { Component, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
@ -28,7 +27,10 @@ import {
} from '@shared/models/relation.models';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { EntityId } from '@shared/models/id/entity-id';
import { Observable, forkJoin } from 'rxjs';
import { forkJoin, Observable } from 'rxjs';
import { JsonObjectEditComponent } from '@app/shared/components/json-object-edit.component';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
export interface RelationDialogData {
isAdd: boolean;
@ -42,7 +44,7 @@ export interface RelationDialogData {
providers: [{provide: ErrorStateMatcher, useExisting: RelationDialogComponent}],
styleUrls: ['./relation-dialog.component.scss']
})
export class RelationDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
export class RelationDialogComponent extends DialogComponent<RelationDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
relationFormGroup: FormGroup;
@ -50,15 +52,20 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
direction: EntitySearchDirection;
entitySearchDirection = EntitySearchDirection;
additionalInfo: FormControl;
@ViewChild('additionalInfoEdit', {static: true}) additionalInfoEdit: JsonObjectEditComponent;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: RelationDialogData,
private entityRelationService: EntityRelationService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<RelationDialogComponent, boolean>,
public fb: FormBuilder) {
super(store);
super(store, router, dialogRef);
this.isAdd = data.isAdd;
this.direction = data.direction;
}
@ -68,14 +75,14 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
type: [this.isAdd ? CONTAINS_TYPE : this.data.relation.type, [Validators.required]],
targetEntityIds: [this.isAdd ? null :
[this.direction === EntitySearchDirection.FROM ? this.data.relation.to : this.data.relation.from],
[Validators.required]],
additionalInfo: [this.data.relation.additionalInfo]
[Validators.required]]
});
this.additionalInfo = new FormControl(this.data.relation.additionalInfo ? {...this.data.relation.additionalInfo} : null);
if (!this.isAdd) {
this.relationFormGroup.get('type').disable();
this.relationFormGroup.get('targetEntityIds').disable();
}
this.relationFormGroup.valueChanges.subscribe(
this.additionalInfo.valueChanges.subscribe(
() => {
this.submitted = false;
}
@ -94,8 +101,8 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
save(): void {
this.submitted = true;
if (this.relationFormGroup.valid) {
const additionalInfo = this.relationFormGroup.get('additionalInfo').value;
this.additionalInfoEdit.validateOnSubmit();
if (!this.relationFormGroup.invalid && !this.additionalInfo.invalid) {
if (this.isAdd) {
const tasks: Observable<EntityRelation>[] = [];
const type: string = this.relationFormGroup.get('type').value;
@ -103,7 +110,7 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
entityIds.forEach(entityId => {
const relation = {
type,
additionalInfo,
additionalInfo: this.additionalInfo.value,
typeGroup: RelationTypeGroup.COMMON
} as EntityRelation;
if (this.direction === EntitySearchDirection.FROM) {
@ -122,7 +129,7 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
);
} else {
const relation: EntityRelation = {...this.data.relation};
relation.additionalInfo = additionalInfo;
relation.additionalInfo = this.additionalInfo.value;
this.entityRelationService.saveRelation(relation).subscribe(
() => {
this.dialogRef.close(true);

8
ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts

@ -26,6 +26,8 @@ import {forkJoin, Observable} from 'rxjs';
import {AssetService} from '@core/http/asset.service';
import {EntityViewService} from '@core/http/entity-view.service';
import {DashboardService} from '@core/http/dashboard.service';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface AddEntitiesToCustomerDialogData {
customerId: string;
@ -38,7 +40,8 @@ export interface AddEntitiesToCustomerDialogData {
providers: [{provide: ErrorStateMatcher, useExisting: AddEntitiesToCustomerDialogComponent}],
styleUrls: []
})
export class AddEntitiesToCustomerDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
export class AddEntitiesToCustomerDialogComponent extends
DialogComponent<AddEntitiesToCustomerDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
addEntitiesToCustomerFormGroup: FormGroup;
@ -50,6 +53,7 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen
assignToCustomerText: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddEntitiesToCustomerDialogData,
private deviceService: DeviceService,
private assetService: AssetService,
@ -58,7 +62,7 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddEntitiesToCustomerDialogComponent, boolean>,
public fb: FormBuilder) {
super(store);
super(store, router, dialogRef);
this.entityType = data.entityType;
}

8
ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts

@ -26,6 +26,8 @@ import {EntityType} from '@shared/models/entity-type.models';
import {forkJoin, Observable} from 'rxjs';
import {AssetService} from '@core/http/asset.service';
import {EntityViewService} from '@core/http/entity-view.service';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface AssignToCustomerDialogData {
entityIds: Array<EntityId>;
@ -38,7 +40,8 @@ export interface AssignToCustomerDialogData {
providers: [{provide: ErrorStateMatcher, useExisting: AssignToCustomerDialogComponent}],
styleUrls: []
})
export class AssignToCustomerDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
export class AssignToCustomerDialogComponent extends
DialogComponent<AssignToCustomerDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
assignToCustomerFormGroup: FormGroup;
@ -50,6 +53,7 @@ export class AssignToCustomerDialogComponent extends PageComponent implements On
assignToCustomerText: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData,
private deviceService: DeviceService,
private assetService: AssetService,
@ -57,7 +61,7 @@ export class AssignToCustomerDialogComponent extends PageComponent implements On
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AssignToCustomerDialogComponent, boolean>,
public fb: FormBuilder) {
super(store);
super(store, router, dialogRef);
}
ngOnInit(): void {

4
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html

@ -20,6 +20,10 @@
<tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>

4
ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html

@ -20,6 +20,10 @@
<tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id" detailsMode="true"></tb-audit-log-table>

7
ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.ts

@ -26,6 +26,8 @@ import {forkJoin, Observable} from 'rxjs';
import {DashboardInfo} from '@app/shared/models/dashboard.models';
import {ActionNotificationShow} from '@core/notification/notification.actions';
import {TranslateService} from '@ngx-translate/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface MakeDashboardPublicDialogData {
dashboard: DashboardInfo;
@ -36,19 +38,20 @@ export interface MakeDashboardPublicDialogData {
templateUrl: './make-dashboard-public-dialog.component.html',
styleUrls: []
})
export class MakeDashboardPublicDialogComponent extends PageComponent implements OnInit {
export class MakeDashboardPublicDialogComponent extends DialogComponent<MakeDashboardPublicDialogComponent> implements OnInit {
dashboard: DashboardInfo;
publicLink: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MakeDashboardPublicDialogData,
public translate: TranslateService,
private dashboardService: DashboardService,
public dialogRef: MatDialogRef<MakeDashboardPublicDialogComponent>,
public fb: FormBuilder) {
super(store);
super(store, router, dialogRef);
this.dashboard = data.dashboard;
this.publicLink = dashboardService.getPublicDashboardLink(this.dashboard);

8
ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.ts

@ -23,6 +23,8 @@ import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm} from '@
import {EntityType} from '@shared/models/entity-type.models';
import {DashboardService} from '@core/http/dashboard.service';
import {forkJoin, Observable} from 'rxjs';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export type ManageDashboardCustomersActionType = 'assign' | 'manage' | 'unassign';
@ -38,7 +40,8 @@ export interface ManageDashboardCustomersDialogData {
providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardCustomersDialogComponent}],
styleUrls: []
})
export class ManageDashboardCustomersDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
export class ManageDashboardCustomersDialogComponent extends
DialogComponent<ManageDashboardCustomersDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
dashboardCustomersFormGroup: FormGroup;
@ -53,12 +56,13 @@ export class ManageDashboardCustomersDialogComponent extends PageComponent imple
assignedCustomersIds: string[];
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ManageDashboardCustomersDialogData,
private dashboardService: DashboardService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<ManageDashboardCustomersDialogComponent, boolean>,
public fb: FormBuilder) {
super(store);
super(store, router, dialogRef);
this.assignedCustomersIds = data.assignedCustomersIds || [];
switch (data.actionType) {

8
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts

@ -25,6 +25,8 @@ import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import {DeviceService} from '@core/http/device.service';
import {DeviceCredentials, DeviceCredentialsType, credentialTypeNames} from '@shared/models/device.models';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface DeviceCredentialsDialogData {
isReadOnly: boolean;
@ -37,7 +39,8 @@ export interface DeviceCredentialsDialogData {
providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}],
styleUrls: []
})
export class DeviceCredentialsDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
export class DeviceCredentialsDialogComponent extends
DialogComponent<DeviceCredentialsDialogComponent, DeviceCredentials> implements OnInit, ErrorStateMatcher {
deviceCredentialsFormGroup: FormGroup;
@ -54,12 +57,13 @@ export class DeviceCredentialsDialogComponent extends PageComponent implements O
credentialTypeNamesMap = credentialTypeNames;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogData,
private deviceService: DeviceService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DeviceCredentialsDialogComponent, DeviceCredentials>,
public fb: FormBuilder) {
super(store);
super(store, router, dialogRef);
this.isReadOnly = data.isReadOnly;
}

4
ui-ngx/src/app/modules/home/pages/entity-view/entity-view-tabs.component.html

@ -20,6 +20,10 @@
<tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>

7
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts

@ -23,22 +23,25 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
@Component({
selector: 'tb-change-password-dialog',
templateUrl: './change-password-dialog.component.html',
styleUrls: ['./change-password-dialog.component.scss']
})
export class ChangePasswordDialogComponent extends PageComponent implements OnInit {
export class ChangePasswordDialogComponent extends DialogComponent<ChangePasswordDialogComponent> implements OnInit {
changePassword: FormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
private translate: TranslateService,
private authService: AuthService,
public dialogRef: MatDialogRef<ChangePasswordDialogComponent>,
public fb: FormBuilder) {
super(store);
super(store, router, dialogRef);
}
ngOnInit(): void {

4
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html

@ -23,6 +23,10 @@
[tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>

4
ui-ngx/src/app/modules/home/pages/tenant/tenant-tabs.component.html

@ -20,3 +20,7 @@
<tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="nullUid"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>

7
ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts

@ -21,6 +21,8 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface ActivationLinkDialogData {
activationLink: string;
@ -30,15 +32,16 @@ export interface ActivationLinkDialogData {
selector: 'tb-activation-link-dialog',
templateUrl: './activation-link-dialog.component.html'
})
export class ActivationLinkDialogComponent extends PageComponent implements OnInit {
export class ActivationLinkDialogComponent extends DialogComponent<ActivationLinkDialogComponent, void> implements OnInit {
activationLink: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ActivationLinkDialogData,
public dialogRef: MatDialogRef<ActivationLinkDialogComponent, void>,
private translate: TranslateService) {
super(store);
super(store, router, dialogRef);
this.activationLink = this.data.activationLink;
}

7
ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts

@ -31,6 +31,8 @@ import {
ActivationLinkDialogData
} from '@modules/home/pages/user/activation-link-dialog.component';
import {TenantId} from '@app/shared/models/id/tenant-id';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface AddUserDialogData {
tenantId: string;
@ -42,7 +44,7 @@ export interface AddUserDialogData {
selector: 'tb-add-user-dialog',
templateUrl: './add-user-dialog.component.html'
})
export class AddUserDialogComponent extends PageComponent implements OnInit {
export class AddUserDialogComponent extends DialogComponent<AddUserDialogComponent, User> implements OnInit {
detailsForm: NgForm;
user: User;
@ -56,11 +58,12 @@ export class AddUserDialogComponent extends PageComponent implements OnInit {
@ViewChild(UserComponent, {static: true}) userComponent: UserComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddUserDialogData,
public dialogRef: MatDialogRef<AddUserDialogComponent, User>,
private userService: UserService,
private dialog: MatDialog) {
super(store);
super(store, router, dialogRef);
}
ngOnInit(): void {

51
ui-ngx/src/app/shared/components/dialog.component.ts

@ -0,0 +1,51 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { OnDestroy } from '@angular/core';
import { PageComponent } from './page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { MatDialogRef } from '@angular/material/dialog';
import { NavigationStart, Router, RouterEvent } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
export abstract class DialogComponent<T, R = any> extends PageComponent implements OnDestroy {
routerSubscription: Subscription;
protected constructor(protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<T, R>) {
super(store);
this.routerSubscription = this.router.events
.pipe(
filter((event: RouterEvent) => event instanceof NavigationStart),
filter(() => !!this.dialogRef)
)
.subscribe(() => {
this.dialogRef.close();
});
}
ngOnDestroy(): void {
console.log('Dialog destroy called');
super.ngOnDestroy();
if (this.routerSubscription) {
this.routerSubscription.unsubscribe();
}
}
}

1
ui-ngx/src/app/shared/components/entity/entity-list.component.html

@ -28,6 +28,7 @@
<input matInput type="text" placeholder="{{ !disabled ? ('entity.entity-list' | translate) : '' }}"
style="max-width: 200px;"
#entityInput
(focusin)="onFocus()"
formControlName="entity"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"

26
ui-ngx/src/app/shared/components/entity/entity-list.component.ts

@ -99,6 +99,8 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
private searchText = '';
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@ -126,7 +128,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
ngOnInit() {
this.filteredEntities = this.entityListFormGroup.get('entity').valueChanges
.pipe(
startWith<string | BaseData<EntityId>>(''),
// startWith<string | BaseData<EntityId>>(''),
tap((value) => {
if (value && typeof value !== 'string') {
this.add(value);
@ -145,12 +147,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
}
setDisabledState(isDisabled: boolean): void {
const emitEvent = this.disabled !== isDisabled;
this.disabled = isDisabled;
if (isDisabled) {
this.entityListFormGroup.disable({emitEvent});
this.entityListFormGroup.disable({emitEvent: false});
} else {
this.entityListFormGroup.enable({emitEvent});
this.entityListFormGroup.enable({emitEvent: false});
}
}
@ -169,14 +170,19 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
this.entityListFormGroup.get('entities').setValue(this.entities);
this.modelValue = null;
}
this.dirty = true;
}
reset() {
this.entities = [];
this.entityListFormGroup.get('entities').setValue(this.entities);
this.modelValue = null;
this.entityListFormGroup.get('entity').patchValue('', {emitEvent: true});
if (this.entityInput) {
this.entityInput.nativeElement.value = '';
}
this.entityListFormGroup.get('entity').patchValue('', {emitEvent: false});
this.propagateChange(this.modelValue);
this.dirty = true;
}
add(entity: BaseData<EntityId>): void {
@ -212,12 +218,18 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> {
this.searchText = searchText;
return this.disabled ? of([]) :
this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText,
return this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText,
50, '', false, true).pipe(
map((data) => data ? data : []));
}
onFocus() {
if (this.dirty) {
this.entityListFormGroup.get('entity').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
clear(value: string = '') {
this.entityInput.nativeElement.value = value;
this.entityListFormGroup.get('entity').patchValue(value, {emitEvent: true});

21
ui-ngx/src/app/shared/components/fullscreen.directive.ts

@ -54,13 +54,13 @@ export class FullscreenDirective {
constructor(public elementRef: ElementRef,
private viewContainerRef: ViewContainerRef,
private overlay: Overlay) {
}
enterFullscreen() {
this.parentElement = this.elementRef.nativeElement.parentElement;
this.parentElement.removeChild(this.elementRef.nativeElement);
this.elementRef.nativeElement.classList.add('tb-fullscreen');
const targetElement = this.elementRef;
this.parentElement = targetElement.nativeElement.parentElement;
this.parentElement.removeChild(targetElement.nativeElement);
targetElement.nativeElement.classList.add('tb-fullscreen');
const position = this.overlay.position();
const config = new OverlayConfig({
hasBackdrop: false,
@ -73,21 +73,24 @@ export class FullscreenDirective {
this.overlayRef = this.overlay.create(config);
this.overlayRef.attach(new EmptyPortal());
this.overlayRef.overlayElement.append( this.elementRef.nativeElement );
this.overlayRef.overlayElement.append( targetElement.nativeElement );
this.fullscreenChanged.emit(true);
}
exitFullscreen() {
const targetElement = this.elementRef;
if (this.parentElement) {
this.overlayRef.overlayElement.removeChild( this.elementRef.nativeElement );
this.parentElement.append( this.elementRef.nativeElement );
this.overlayRef.overlayElement.removeChild( targetElement.nativeElement );
this.parentElement.append(targetElement.nativeElement);
this.parentElement = null;
}
this.elementRef.nativeElement.classList.remove('tb-fullscreen');
targetElement.nativeElement.classList.remove('tb-fullscreen');
if (this.elementRef !== targetElement) {
this.elementRef.nativeElement.classList.remove('tb-fullscreen');
}
this.overlayRef.dispose();
this.fullscreenChanged.emit(false);
}
}
class EmptyPortal extends ComponentPortal<TbAnchorComponent> {

6
ui-ngx/src/app/shared/components/json-object-edit.component.html

@ -16,7 +16,8 @@
-->
<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center">
<label class="tb-title no-padding"
ng-class="{'tb-required': required,
@ -29,7 +30,8 @@
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div fxFlex="0%" id="tb-json-panel" class="tb-json-object-panel" fxLayout="column">
<div fxFlex="0%" id="tb-json-panel" tb-toast toastTarget="jsonObjectEditor"
class="tb-json-object-panel" fxLayout="column">
<div fxFlex #jsonEditor id="tb-json-input" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
</div>

23
ui-ngx/src/app/shared/components/json-object-edit.component.scss

@ -35,21 +35,20 @@
.fill-height {
height: 100%;
}
}
.tb-json-object-panel {
height: 100%;
margin-left: 15px;
border: 1px solid #c0c0c0;
.tb-json-object-panel {
height: 100%;
margin-left: 15px;
border: 1px solid #c0c0c0;
#tb-json-input {
width: 100%;
min-width: 200px;
height: 100%;
#tb-json-input {
width: 100%;
min-width: 200px;
height: 100%;
&:not(.fill-height) {
min-height: 200px;
}
&:not(.fill-height) {
min-height: 200px;
}
}
}

52
ui-ngx/src/app/shared/components/json-object-edit.component.ts

@ -26,6 +26,9 @@ import {
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, Validator, NG_VALIDATORS } from '@angular/forms';
import * as ace from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-json-object-edit',
@ -83,9 +86,14 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
objectValid: boolean;
validationError: string;
errorShowed = false;
private propagateChange = null;
constructor() {
constructor(public elementRef: ElementRef,
protected store: Store<AppState>) {
}
ngOnInit(): void {
@ -109,6 +117,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
this.jsonEditor.session.setUseWrapMode(false);
this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1);
this.jsonEditor.on('change', () => {
this.cleanupJsonErrors();
this.updateView();
});
}
@ -132,6 +141,33 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
};
}
validateOnSubmit(): void {
if (!this.readonly) {
this.cleanupJsonErrors();
if (!this.objectValid) {
this.store.dispatch(new ActionNotificationShow(
{
message: this.validationError,
type: 'error',
target: 'jsonObjectEditor',
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
this.errorShowed = true;
}
}
}
cleanupJsonErrors(): void {
if (this.errorShowed) {
this.store.dispatch(new ActionNotificationHide(
{
target: 'jsonObjectEditor'
}));
this.errorShowed = false;
}
}
writeValue(value: any): void {
this.modelValue = value;
this.contentValue = '';
@ -142,6 +178,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
this.objectValid = true;
} else {
this.objectValid = !this.required;
this.validationError = 'Json object is required.';
}
} catch (e) {
//
@ -161,9 +198,20 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
try {
data = JSON.parse(this.contentValue);
this.objectValid = true;
} catch (ex) {}
this.validationError = '';
} catch (ex) {
let errorInfo = 'Error:';
if (ex.name) {
errorInfo += ' ' + ex.name + ':';
}
if (ex.message) {
errorInfo += ' ' + ex.message;
}
this.validationError = errorInfo;
}
} else {
this.objectValid = !this.required;
this.validationError = this.required ? 'Json object is required.' : '';
}
this.propagateChange(data);
}

4
ui-ngx/src/app/shared/components/page.component.ts

@ -19,7 +19,7 @@ import { select, Store } from '@ngrx/store';
import { AppState } from '../../core/core.state';
import { Observable, Subscription } from 'rxjs';
import { selectIsLoading } from '../../core/interceptors/load.selectors';
import { delay } from 'rxjs/operators';
import { delay, share } from 'rxjs/operators';
import { AbstractControl } from '@angular/forms';
export abstract class PageComponent implements OnDestroy {
@ -29,7 +29,7 @@ export abstract class PageComponent implements OnDestroy {
disabledOnLoadFormControls: Array<AbstractControl> = [];
protected constructor(protected store: Store<AppState>) {
this.isLoading$ = this.store.pipe(delay(0), select(selectIsLoading), delay(100));
this.isLoading$ = this.store.pipe(delay(0), select(selectIsLoading), share());
}
protected registerDisableOnLoadFormControl(control: AbstractControl) {

15
ui-ngx/src/app/shared/components/toast.directive.ts

@ -45,6 +45,7 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
toastTarget = 'root';
private notificationSubscription: Subscription = null;
private hideNotificationSubscription: Subscription = null;
constructor(public elementRef: ElementRef,
public viewContainerRef: ViewContainerRef,
@ -78,12 +79,26 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
}
}
);
this.hideNotificationSubscription = this.notificationService.getHideNotification().subscribe(
(hideNotification) => {
if (hideNotification) {
const target = hideNotification.target || 'root';
if (this.toastTarget === target) {
this.snackBar.dismiss();
}
}
}
);
}
ngOnDestroy(): void {
if (this.notificationSubscription) {
this.notificationSubscription.unsubscribe();
}
if (this.hideNotificationSubscription) {
this.hideNotificationSubscription.unsubscribe();
}
}
}

Loading…
Cancel
Save