Commit 52ab3d6dea044edaf81b8193e2eeee7685b435fe

Authored by Igor Kulikov
1 parent b438cdd2

Relation edit dialog improvements. Minor improvements.

Showing 34 changed files with 343 additions and 99 deletions
... ... @@ -82,18 +82,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
82 82 } else {
83 83 const authority = Authority[authState.authUser.authority];
84 84 if (data.auth && data.auth.indexOf(authority) === -1) {
85   - this.dialogService.confirm(
86   - this.translate.instant('access.access-forbidden'),
87   - this.translate.instant('access.access-forbidden-text'),
88   - this.translate.instant('action.cancel'),
89   - this.translate.instant('action.sign-in'),
90   - true
91   - ).subscribe((res) => {
92   - if (res) {
93   - this.authService.logout();
94   - }
95   - }
96   - );
  85 + this.dialogService.forbidden();
97 86 return false;
98 87 } else {
99 88 return true;
... ...
... ... @@ -50,6 +50,8 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
50 50 '/api/auth/token'
51 51 ];
52 52
  53 + private activeRequests = 0;
  54 +
53 55 constructor(private store: Store<AppState>,
54 56 private dialogService: DialogService,
55 57 private translate: TranslateService,
... ... @@ -135,7 +137,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
135 137 }
136 138 } else if (errorResponse.status === 403) {
137 139 if (!ignoreErrors) {
138   - this.permissionDenied();
  140 + this.dialogService.forbidden();
139 141 }
140 142 } else if (errorResponse.status === 0 || errorResponse.status === -1) {
141 143 this.showError('Unable to connect');
... ... @@ -241,7 +243,16 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
241 243
242 244 private updateLoadingState(config: InterceptorConfig, isLoading: boolean) {
243 245 if (!config.ignoreLoading) {
244   - this.store.dispatch(isLoading ? new ActionLoadStart() : new ActionLoadFinish());
  246 + if (isLoading) {
  247 + this.activeRequests++;
  248 + } else {
  249 + this.activeRequests--;
  250 + }
  251 + if (this.activeRequests === 1) {
  252 + this.store.dispatch(new ActionLoadStart());
  253 + } else if (this.activeRequests === 0) {
  254 + this.store.dispatch(new ActionLoadFinish());
  255 + }
245 256 }
246 257 }
247 258
... ... @@ -253,14 +264,6 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
253 264 }
254 265 }
255 266
256   - private permissionDenied() {
257   - this.dialogService.alert(
258   - this.translate.instant('access.permission-denied'),
259   - this.translate.instant('access.permission-denied-text'),
260   - this.translate.instant('action.close')
261   - );
262   - }
263   -
264 267 private showError(error: string, timeout: number = 0) {
265 268 setTimeout(() => {
266 269 this.store.dispatch(new ActionNotificationShow({message: error, type: 'error'}));
... ...
... ... @@ -15,10 +15,11 @@
15 15 ///
16 16
17 17 import { Action } from '@ngrx/store';
18   -import { NotificationMessage } from '@app/core/notification/notification.models';
  18 +import { NotificationMessage, HideNotification } from '@app/core/notification/notification.models';
19 19
20 20 export enum NotificationActionTypes {
21   - SHOW_NOTIFICATION = '[Notification] Show'
  21 + SHOW_NOTIFICATION = '[Notification] Show',
  22 + HIDE_NOTIFICATION = '[Notification] Hide'
22 23 }
23 24
24 25 export class ActionNotificationShow implements Action {
... ... @@ -27,5 +28,11 @@ export class ActionNotificationShow implements Action {
27 28 constructor(readonly notification: NotificationMessage ) {}
28 29 }
29 30
  31 +export class ActionNotificationHide implements Action {
  32 + readonly type = NotificationActionTypes.HIDE_NOTIFICATION;
  33 +
  34 + constructor(readonly hideNotification: HideNotification ) {}
  35 +}
  36 +
30 37 export type NotificationActions =
31   - | ActionNotificationShow;
  38 + | ActionNotificationShow | ActionNotificationHide;
... ...
... ... @@ -44,4 +44,13 @@ export class NotificationEffects {
44 44 })
45 45 );
46 46
  47 + @Effect({dispatch: false})
  48 + hideNotification = this.actions$.pipe(
  49 + ofType(
  50 + NotificationActionTypes.HIDE_NOTIFICATION,
  51 + ),
  52 + map(({ hideNotification }) => {
  53 + this.notificationService.hideNotification(hideNotification);
  54 + })
  55 + );
47 56 }
... ...
... ... @@ -17,6 +17,7 @@
17 17
18 18 export interface NotificationState {
19 19 notification: NotificationMessage;
  20 + hideNotification: HideNotification;
20 21 }
21 22
22 23 export declare type NotificationType = 'info' | 'success' | 'error';
... ... @@ -31,3 +32,7 @@ export class NotificationMessage {
31 32 horizontalPosition?: NotificationHorizontalPosition;
32 33 verticalPosition?: NotificationVerticalPosition;
33 34 }
  35 +
  36 +export class HideNotification {
  37 + target?: string;
  38 +}
... ...
... ... @@ -18,7 +18,8 @@ import { NotificationState } from './notification.models';
18 18 import { NotificationActions, NotificationActionTypes } from './notification.actions';
19 19
20 20 export const initialState: NotificationState = {
21   - notification: null
  21 + notification: null,
  22 + hideNotification: null
22 23 };
23 24
24 25 export function notificationReducer(
... ... @@ -28,6 +29,8 @@ export function notificationReducer(
28 29 switch (action.type) {
29 30 case NotificationActionTypes.SHOW_NOTIFICATION:
30 31 return { ...state, notification: action.notification };
  32 + case NotificationActionTypes.HIDE_NOTIFICATION:
  33 + return { ...state, hideNotification: action.hideNotification };
31 34 default:
32 35 return state;
33 36 }
... ...
... ... @@ -20,7 +20,8 @@ import { MatDialog, MatDialogConfig } from '@angular/material';
20 20 import { ConfirmDialogComponent } from '@core/services/dialog/confirm-dialog.component';
21 21 import { TranslateService } from '@ngx-translate/core';
22 22 import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component';
23   -import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component";
  23 +import { TodoDialogComponent } from '@core/services/dialog/todo-dialog.component';
  24 +import { AuthService } from '@core/auth/auth.service';
24 25
25 26 @Injectable(
26 27 {
... ... @@ -31,6 +32,7 @@ export class DialogService {
31 32
32 33 constructor(
33 34 private translate: TranslateService,
  35 + private authService: AuthService,
34 36 public dialog: MatDialog
35 37 ) {
36 38 }
... ... @@ -68,6 +70,30 @@ export class DialogService {
68 70 return dialogRef.afterClosed();
69 71 }
70 72
  73 + private permissionDenied() {
  74 + this.alert(
  75 + this.translate.instant('access.permission-denied'),
  76 + this.translate.instant('access.permission-denied-text'),
  77 + this.translate.instant('action.close')
  78 + );
  79 + }
  80 +
  81 + forbidden(): Observable<boolean> {
  82 + const observable = this.confirm(
  83 + this.translate.instant('access.access-forbidden'),
  84 + this.translate.instant('access.access-forbidden-text'),
  85 + this.translate.instant('action.cancel'),
  86 + this.translate.instant('action.sign-in'),
  87 + true
  88 + );
  89 + observable.subscribe((res) => {
  90 + if (res) {
  91 + this.authService.logout();
  92 + }
  93 + });
  94 + return observable;
  95 + }
  96 +
71 97 todo(): Observable<any> {
72 98 const dialogConfig: MatDialogConfig = {
73 99 disableClose: true,
... ...
... ... @@ -15,7 +15,7 @@
15 15 ///
16 16
17 17 import { Injectable } from '@angular/core';
18   -import { NotificationMessage } from '@app/core/notification/notification.models';
  18 +import { HideNotification, NotificationMessage } from '@app/core/notification/notification.models';
19 19 import { BehaviorSubject, Observable, Subject } from 'rxjs';
20 20
21 21
... ... @@ -28,6 +28,8 @@ export class NotificationService {
28 28
29 29 private notificationSubject: Subject<NotificationMessage> = new Subject();
30 30
  31 + private hideNotificationSubject: Subject<HideNotification> = new Subject();
  32 +
31 33 constructor(
32 34 ) {
33 35 }
... ... @@ -36,8 +38,15 @@ export class NotificationService {
36 38 this.notificationSubject.next(notification);
37 39 }
38 40
  41 + hideNotification(hideNotification: HideNotification) {
  42 + this.hideNotificationSubject.next(hideNotification);
  43 + }
  44 +
39 45 getNotification(): Observable<NotificationMessage> {
40   - return this.notificationSubject;
  46 + return this.notificationSubject.asObservable();
41 47 }
42 48
  49 + getHideNotification(): Observable<HideNotification> {
  50 + return this.hideNotificationSubject.asObservable();
  51 + }
43 52 }
... ...
... ... @@ -30,6 +30,8 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
30 30 import { ActionStatus, AuditLog } from '@shared/models/audit-log.models';
31 31
32 32 import * as ace from 'ace-builds';
  33 +import { DialogComponent } from '@shared/components/dialog.component';
  34 +import { Router } from '@angular/router';
33 35
34 36 export interface AuditLogDetailsDialogData {
35 37 auditLog: AuditLog;
... ... @@ -40,7 +42,7 @@ export interface AuditLogDetailsDialogData {
40 42 templateUrl: './audit-log-details-dialog.component.html',
41 43 styleUrls: ['./audit-log-details-dialog.component.scss']
42 44 })
43   -export class AuditLogDetailsDialogComponent extends PageComponent implements OnInit {
  45 +export class AuditLogDetailsDialogComponent extends DialogComponent<AuditLogDetailsDialogComponent> implements OnInit {
44 46
45 47 @ViewChild('actionDataEditor', {static: true})
46 48 actionDataEditorElmRef: ElementRef;
... ... @@ -58,10 +60,11 @@ export class AuditLogDetailsDialogComponent extends PageComponent implements OnI
58 60 @ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent;
59 61
60 62 constructor(protected store: Store<AppState>,
  63 + protected router: Router,
61 64 @Inject(MAT_DIALOG_DATA) public data: AuditLogDetailsDialogData,
62 65 public dialogRef: MatDialogRef<AuditLogDetailsDialogComponent>,
63 66 private renderer: Renderer2) {
64   - super(store);
  67 + super(store, router, dialogRef);
65 68 }
66 69
67 70 ngOnInit(): void {
... ... @@ -114,7 +117,7 @@ export class AuditLogDetailsDialogComponent extends PageComponent implements OnI
114 117 });
115 118 newWidth = 8 * maxLineLength + 16;
116 119 }
117   - newHeight = Math.min(400, newHeight);
  120 + // newHeight = Math.min(400, newHeight);
118 121 this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px');
119 122 this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px');
120 123 this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px');
... ...
... ... @@ -27,6 +27,8 @@ import {TbAnchorComponent} from '@shared/components/tb-anchor.component';
27 27 import {EntityComponent} from './entity.component';
28 28 import {EntityTableConfig} from '@home/models/entity/entities-table-config.models';
29 29 import {AddEntityDialogData} from '@home/models/entity/entity-component.models';
  30 +import { DialogComponent } from '@shared/components/dialog.component';
  31 +import { Router } from '@angular/router';
30 32
31 33 @Component({
32 34 selector: 'tb-add-entity-dialog',
... ... @@ -34,7 +36,7 @@ import {AddEntityDialogData} from '@home/models/entity/entity-component.models';
34 36 providers: [{provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent}],
35 37 styleUrls: ['./add-entity-dialog.component.scss']
36 38 })
37   -export class AddEntityDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  39 +export class AddEntityDialogComponent extends DialogComponent<AddEntityDialogComponent, BaseData<HasId>> implements OnInit, ErrorStateMatcher {
38 40
39 41 entityComponent: EntityComponent<BaseData<HasId>>;
40 42 detailsForm: NgForm;
... ... @@ -49,11 +51,12 @@ export class AddEntityDialogComponent extends PageComponent implements OnInit, E
49 51 @ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent;
50 52
51 53 constructor(protected store: Store<AppState>,
  54 + protected router: Router,
52 55 @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<HasId>>,
53 56 public dialogRef: MatDialogRef<AddEntityDialogComponent, BaseData<HasId>>,
54 57 private componentFactoryResolver: ComponentFactoryResolver,
55 58 @SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
56   - super(store);
  59 + super(store, router, dialogRef);
57 60 }
58 61
59 62 ngOnInit(): void {
... ...
... ... @@ -41,11 +41,12 @@
41 41 required="true">
42 42 </tb-entity-list-select>
43 43 <tb-json-object-edit
44   - formControlName="additionalInfo"
  44 + #additionalInfoEdit
  45 + [formControl]="additionalInfo"
45 46 label="{{ 'relation.additional-info' | translate }}">
46 47 </tb-json-object-edit>
47 48 <div class="tb-error-messages" *ngIf="submitted &&
48   - relationFormGroup.get('additionalInfo').invalid" role="alert">
  49 + additionalInfo.invalid" role="alert">
49 50 <div translate class="tb-error-message">relation.invalid-additional-info</div>
50 51 </div>
51 52 </fieldset>
... ... @@ -54,7 +55,7 @@
54 55 <span fxFlex></span>
55 56 <button mat-button mat-raised-button color="primary"
56 57 type="submit"
57   - [disabled]="(isLoading$ | async)">
  58 + [disabled]="(isLoading$ | async) || relationFormGroup.invalid || !(relationFormGroup.dirty || additionalInfo.dirty)">
58 59 {{ (isAdd ? 'action.add' : 'action.save') | translate }}
59 60 </button>
60 61 <button mat-button color="primary"
... ...
... ... @@ -14,9 +14,8 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
  17 +import { Component, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
18 18 import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
19   -import { PageComponent } from '@shared/components/page.component';
20 19 import { Store } from '@ngrx/store';
21 20 import { AppState } from '@core/core.state';
22 21 import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
... ... @@ -28,7 +27,10 @@ import {
28 27 } from '@shared/models/relation.models';
29 28 import { EntityRelationService } from '@core/http/entity-relation.service';
30 29 import { EntityId } from '@shared/models/id/entity-id';
31   -import { Observable, forkJoin } from 'rxjs';
  30 +import { forkJoin, Observable } from 'rxjs';
  31 +import { JsonObjectEditComponent } from '@app/shared/components/json-object-edit.component';
  32 +import { Router } from '@angular/router';
  33 +import { DialogComponent } from '@app/shared/components/dialog.component';
32 34
33 35 export interface RelationDialogData {
34 36 isAdd: boolean;
... ... @@ -42,7 +44,7 @@ export interface RelationDialogData {
42 44 providers: [{provide: ErrorStateMatcher, useExisting: RelationDialogComponent}],
43 45 styleUrls: ['./relation-dialog.component.scss']
44 46 })
45   -export class RelationDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  47 +export class RelationDialogComponent extends DialogComponent<RelationDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
46 48
47 49 relationFormGroup: FormGroup;
48 50
... ... @@ -50,15 +52,20 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
50 52 direction: EntitySearchDirection;
51 53 entitySearchDirection = EntitySearchDirection;
52 54
  55 + additionalInfo: FormControl;
  56 +
  57 + @ViewChild('additionalInfoEdit', {static: true}) additionalInfoEdit: JsonObjectEditComponent;
  58 +
53 59 submitted = false;
54 60
55 61 constructor(protected store: Store<AppState>,
  62 + protected router: Router,
56 63 @Inject(MAT_DIALOG_DATA) public data: RelationDialogData,
57 64 private entityRelationService: EntityRelationService,
58 65 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
59 66 public dialogRef: MatDialogRef<RelationDialogComponent, boolean>,
60 67 public fb: FormBuilder) {
61   - super(store);
  68 + super(store, router, dialogRef);
62 69 this.isAdd = data.isAdd;
63 70 this.direction = data.direction;
64 71 }
... ... @@ -68,14 +75,14 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
68 75 type: [this.isAdd ? CONTAINS_TYPE : this.data.relation.type, [Validators.required]],
69 76 targetEntityIds: [this.isAdd ? null :
70 77 [this.direction === EntitySearchDirection.FROM ? this.data.relation.to : this.data.relation.from],
71   - [Validators.required]],
72   - additionalInfo: [this.data.relation.additionalInfo]
  78 + [Validators.required]]
73 79 });
  80 + this.additionalInfo = new FormControl(this.data.relation.additionalInfo ? {...this.data.relation.additionalInfo} : null);
74 81 if (!this.isAdd) {
75 82 this.relationFormGroup.get('type').disable();
76 83 this.relationFormGroup.get('targetEntityIds').disable();
77 84 }
78   - this.relationFormGroup.valueChanges.subscribe(
  85 + this.additionalInfo.valueChanges.subscribe(
79 86 () => {
80 87 this.submitted = false;
81 88 }
... ... @@ -94,8 +101,8 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
94 101
95 102 save(): void {
96 103 this.submitted = true;
97   - if (this.relationFormGroup.valid) {
98   - const additionalInfo = this.relationFormGroup.get('additionalInfo').value;
  104 + this.additionalInfoEdit.validateOnSubmit();
  105 + if (!this.relationFormGroup.invalid && !this.additionalInfo.invalid) {
99 106 if (this.isAdd) {
100 107 const tasks: Observable<EntityRelation>[] = [];
101 108 const type: string = this.relationFormGroup.get('type').value;
... ... @@ -103,7 +110,7 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
103 110 entityIds.forEach(entityId => {
104 111 const relation = {
105 112 type,
106   - additionalInfo,
  113 + additionalInfo: this.additionalInfo.value,
107 114 typeGroup: RelationTypeGroup.COMMON
108 115 } as EntityRelation;
109 116 if (this.direction === EntitySearchDirection.FROM) {
... ... @@ -122,7 +129,7 @@ export class RelationDialogComponent extends PageComponent implements OnInit, Er
122 129 );
123 130 } else {
124 131 const relation: EntityRelation = {...this.data.relation};
125   - relation.additionalInfo = additionalInfo;
  132 + relation.additionalInfo = this.additionalInfo.value;
126 133 this.entityRelationService.saveRelation(relation).subscribe(
127 134 () => {
128 135 this.dialogRef.close(true);
... ...
... ... @@ -26,6 +26,8 @@ import {forkJoin, Observable} from 'rxjs';
26 26 import {AssetService} from '@core/http/asset.service';
27 27 import {EntityViewService} from '@core/http/entity-view.service';
28 28 import {DashboardService} from '@core/http/dashboard.service';
  29 +import { DialogComponent } from '@shared/components/dialog.component';
  30 +import { Router } from '@angular/router';
29 31
30 32 export interface AddEntitiesToCustomerDialogData {
31 33 customerId: string;
... ... @@ -38,7 +40,8 @@ export interface AddEntitiesToCustomerDialogData {
38 40 providers: [{provide: ErrorStateMatcher, useExisting: AddEntitiesToCustomerDialogComponent}],
39 41 styleUrls: []
40 42 })
41   -export class AddEntitiesToCustomerDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  43 +export class AddEntitiesToCustomerDialogComponent extends
  44 + DialogComponent<AddEntitiesToCustomerDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
42 45
43 46 addEntitiesToCustomerFormGroup: FormGroup;
44 47
... ... @@ -50,6 +53,7 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen
50 53 assignToCustomerText: string;
51 54
52 55 constructor(protected store: Store<AppState>,
  56 + protected router: Router,
53 57 @Inject(MAT_DIALOG_DATA) public data: AddEntitiesToCustomerDialogData,
54 58 private deviceService: DeviceService,
55 59 private assetService: AssetService,
... ... @@ -58,7 +62,7 @@ export class AddEntitiesToCustomerDialogComponent extends PageComponent implemen
58 62 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
59 63 public dialogRef: MatDialogRef<AddEntitiesToCustomerDialogComponent, boolean>,
60 64 public fb: FormBuilder) {
61   - super(store);
  65 + super(store, router, dialogRef);
62 66 this.entityType = data.entityType;
63 67 }
64 68
... ...
... ... @@ -26,6 +26,8 @@ import {EntityType} from '@shared/models/entity-type.models';
26 26 import {forkJoin, Observable} from 'rxjs';
27 27 import {AssetService} from '@core/http/asset.service';
28 28 import {EntityViewService} from '@core/http/entity-view.service';
  29 +import { DialogComponent } from '@shared/components/dialog.component';
  30 +import { Router } from '@angular/router';
29 31
30 32 export interface AssignToCustomerDialogData {
31 33 entityIds: Array<EntityId>;
... ... @@ -38,7 +40,8 @@ export interface AssignToCustomerDialogData {
38 40 providers: [{provide: ErrorStateMatcher, useExisting: AssignToCustomerDialogComponent}],
39 41 styleUrls: []
40 42 })
41   -export class AssignToCustomerDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  43 +export class AssignToCustomerDialogComponent extends
  44 + DialogComponent<AssignToCustomerDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
42 45
43 46 assignToCustomerFormGroup: FormGroup;
44 47
... ... @@ -50,6 +53,7 @@ export class AssignToCustomerDialogComponent extends PageComponent implements On
50 53 assignToCustomerText: string;
51 54
52 55 constructor(protected store: Store<AppState>,
  56 + protected router: Router,
53 57 @Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData,
54 58 private deviceService: DeviceService,
55 59 private assetService: AssetService,
... ... @@ -57,7 +61,7 @@ export class AssignToCustomerDialogComponent extends PageComponent implements On
57 61 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
58 62 public dialogRef: MatDialogRef<AssignToCustomerDialogComponent, boolean>,
59 63 public fb: FormBuilder) {
60   - super(store);
  64 + super(store, router, dialogRef);
61 65 }
62 66
63 67 ngOnInit(): void {
... ...
... ... @@ -20,6 +20,10 @@
20 20 <tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id"
21 21 [entityId]="entity.id"></tb-event-table>
22 22 </mat-tab>
  23 +<mat-tab *ngIf="entity"
  24 + label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
  25 + <tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
  26 +</mat-tab>
23 27 <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
24 28 label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
25 29 <tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>
... ...
... ... @@ -20,6 +20,10 @@
20 20 <tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id"
21 21 [entityId]="entity.id"></tb-event-table>
22 22 </mat-tab>
  23 +<mat-tab *ngIf="entity"
  24 + label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
  25 + <tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
  26 +</mat-tab>
23 27 <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
24 28 label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
25 29 <tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id" detailsMode="true"></tb-audit-log-table>
... ...
... ... @@ -26,6 +26,8 @@ import {forkJoin, Observable} from 'rxjs';
26 26 import {DashboardInfo} from '@app/shared/models/dashboard.models';
27 27 import {ActionNotificationShow} from '@core/notification/notification.actions';
28 28 import {TranslateService} from '@ngx-translate/core';
  29 +import { DialogComponent } from '@shared/components/dialog.component';
  30 +import { Router } from '@angular/router';
29 31
30 32 export interface MakeDashboardPublicDialogData {
31 33 dashboard: DashboardInfo;
... ... @@ -36,19 +38,20 @@ export interface MakeDashboardPublicDialogData {
36 38 templateUrl: './make-dashboard-public-dialog.component.html',
37 39 styleUrls: []
38 40 })
39   -export class MakeDashboardPublicDialogComponent extends PageComponent implements OnInit {
  41 +export class MakeDashboardPublicDialogComponent extends DialogComponent<MakeDashboardPublicDialogComponent> implements OnInit {
40 42
41 43 dashboard: DashboardInfo;
42 44
43 45 publicLink: string;
44 46
45 47 constructor(protected store: Store<AppState>,
  48 + protected router: Router,
46 49 @Inject(MAT_DIALOG_DATA) public data: MakeDashboardPublicDialogData,
47 50 public translate: TranslateService,
48 51 private dashboardService: DashboardService,
49 52 public dialogRef: MatDialogRef<MakeDashboardPublicDialogComponent>,
50 53 public fb: FormBuilder) {
51   - super(store);
  54 + super(store, router, dialogRef);
52 55
53 56 this.dashboard = data.dashboard;
54 57 this.publicLink = dashboardService.getPublicDashboardLink(this.dashboard);
... ...
... ... @@ -23,6 +23,8 @@ import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm} from '@
23 23 import {EntityType} from '@shared/models/entity-type.models';
24 24 import {DashboardService} from '@core/http/dashboard.service';
25 25 import {forkJoin, Observable} from 'rxjs';
  26 +import { DialogComponent } from '@shared/components/dialog.component';
  27 +import { Router } from '@angular/router';
26 28
27 29 export type ManageDashboardCustomersActionType = 'assign' | 'manage' | 'unassign';
28 30
... ... @@ -38,7 +40,8 @@ export interface ManageDashboardCustomersDialogData {
38 40 providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardCustomersDialogComponent}],
39 41 styleUrls: []
40 42 })
41   -export class ManageDashboardCustomersDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  43 +export class ManageDashboardCustomersDialogComponent extends
  44 + DialogComponent<ManageDashboardCustomersDialogComponent, boolean> implements OnInit, ErrorStateMatcher {
42 45
43 46 dashboardCustomersFormGroup: FormGroup;
44 47
... ... @@ -53,12 +56,13 @@ export class ManageDashboardCustomersDialogComponent extends PageComponent imple
53 56 assignedCustomersIds: string[];
54 57
55 58 constructor(protected store: Store<AppState>,
  59 + protected router: Router,
56 60 @Inject(MAT_DIALOG_DATA) public data: ManageDashboardCustomersDialogData,
57 61 private dashboardService: DashboardService,
58 62 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
59 63 public dialogRef: MatDialogRef<ManageDashboardCustomersDialogComponent, boolean>,
60 64 public fb: FormBuilder) {
61   - super(store);
  65 + super(store, router, dialogRef);
62 66
63 67 this.assignedCustomersIds = data.assignedCustomersIds || [];
64 68 switch (data.actionType) {
... ...
... ... @@ -25,6 +25,8 @@ import { TranslateService } from '@ngx-translate/core';
25 25 import { AuthService } from '@core/auth/auth.service';
26 26 import {DeviceService} from '@core/http/device.service';
27 27 import {DeviceCredentials, DeviceCredentialsType, credentialTypeNames} from '@shared/models/device.models';
  28 +import { DialogComponent } from '@shared/components/dialog.component';
  29 +import { Router } from '@angular/router';
28 30
29 31 export interface DeviceCredentialsDialogData {
30 32 isReadOnly: boolean;
... ... @@ -37,7 +39,8 @@ export interface DeviceCredentialsDialogData {
37 39 providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}],
38 40 styleUrls: []
39 41 })
40   -export class DeviceCredentialsDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  42 +export class DeviceCredentialsDialogComponent extends
  43 + DialogComponent<DeviceCredentialsDialogComponent, DeviceCredentials> implements OnInit, ErrorStateMatcher {
41 44
42 45 deviceCredentialsFormGroup: FormGroup;
43 46
... ... @@ -54,12 +57,13 @@ export class DeviceCredentialsDialogComponent extends PageComponent implements O
54 57 credentialTypeNamesMap = credentialTypeNames;
55 58
56 59 constructor(protected store: Store<AppState>,
  60 + protected router: Router,
57 61 @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogData,
58 62 private deviceService: DeviceService,
59 63 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
60 64 public dialogRef: MatDialogRef<DeviceCredentialsDialogComponent, DeviceCredentials>,
61 65 public fb: FormBuilder) {
62   - super(store);
  66 + super(store, router, dialogRef);
63 67
64 68 this.isReadOnly = data.isReadOnly;
65 69 }
... ...
... ... @@ -20,6 +20,10 @@
20 20 <tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="entity.tenantId.id"
21 21 [entityId]="entity.id"></tb-event-table>
22 22 </mat-tab>
  23 +<mat-tab *ngIf="entity"
  24 + label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
  25 + <tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
  26 +</mat-tab>
23 27 <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
24 28 label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
25 29 <tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>
... ...
... ... @@ -23,22 +23,25 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
23 23 import { ActionNotificationShow } from '@core/notification/notification.actions';
24 24 import { TranslateService } from '@ngx-translate/core';
25 25 import { AuthService } from '@core/auth/auth.service';
  26 +import { DialogComponent } from '@shared/components/dialog.component';
  27 +import { Router } from '@angular/router';
26 28
27 29 @Component({
28 30 selector: 'tb-change-password-dialog',
29 31 templateUrl: './change-password-dialog.component.html',
30 32 styleUrls: ['./change-password-dialog.component.scss']
31 33 })
32   -export class ChangePasswordDialogComponent extends PageComponent implements OnInit {
  34 +export class ChangePasswordDialogComponent extends DialogComponent<ChangePasswordDialogComponent> implements OnInit {
33 35
34 36 changePassword: FormGroup;
35 37
36 38 constructor(protected store: Store<AppState>,
  39 + protected router: Router,
37 40 private translate: TranslateService,
38 41 private authService: AuthService,
39 42 public dialogRef: MatDialogRef<ChangePasswordDialogComponent>,
40 43 public fb: FormBuilder) {
41   - super(store);
  44 + super(store, router, dialogRef);
42 45 }
43 46
44 47 ngOnInit(): void {
... ...
... ... @@ -23,6 +23,10 @@
23 23 [tenantId]="entity.tenantId.id"
24 24 [entityId]="entity.id"></tb-event-table>
25 25 </mat-tab>
  26 +<mat-tab *ngIf="entity"
  27 + label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
  28 + <tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
  29 +</mat-tab>
26 30 <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
27 31 label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
28 32 <tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>
... ...
... ... @@ -20,3 +20,7 @@
20 20 <tb-event-table [active]="eventsTab.isActive" [defaultEventType]="eventTypes.ERROR" [tenantId]="nullUid"
21 21 [entityId]="entity.id"></tb-event-table>
22 22 </mat-tab>
  23 +<mat-tab *ngIf="entity"
  24 + label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
  25 + <tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
  26 +</mat-tab>
... ...
... ... @@ -21,6 +21,8 @@ import { Store } from '@ngrx/store';
21 21 import { AppState } from '@core/core.state';
22 22 import { TranslateService } from '@ngx-translate/core';
23 23 import { ActionNotificationShow } from '@core/notification/notification.actions';
  24 +import { DialogComponent } from '@shared/components/dialog.component';
  25 +import { Router } from '@angular/router';
24 26
25 27 export interface ActivationLinkDialogData {
26 28 activationLink: string;
... ... @@ -30,15 +32,16 @@ export interface ActivationLinkDialogData {
30 32 selector: 'tb-activation-link-dialog',
31 33 templateUrl: './activation-link-dialog.component.html'
32 34 })
33   -export class ActivationLinkDialogComponent extends PageComponent implements OnInit {
  35 +export class ActivationLinkDialogComponent extends DialogComponent<ActivationLinkDialogComponent, void> implements OnInit {
34 36
35 37 activationLink: string;
36 38
37 39 constructor(protected store: Store<AppState>,
  40 + protected router: Router,
38 41 @Inject(MAT_DIALOG_DATA) public data: ActivationLinkDialogData,
39 42 public dialogRef: MatDialogRef<ActivationLinkDialogComponent, void>,
40 43 private translate: TranslateService) {
41   - super(store);
  44 + super(store, router, dialogRef);
42 45 this.activationLink = this.data.activationLink;
43 46 }
44 47
... ...
... ... @@ -31,6 +31,8 @@ import {
31 31 ActivationLinkDialogData
32 32 } from '@modules/home/pages/user/activation-link-dialog.component';
33 33 import {TenantId} from '@app/shared/models/id/tenant-id';
  34 +import { DialogComponent } from '@shared/components/dialog.component';
  35 +import { Router } from '@angular/router';
34 36
35 37 export interface AddUserDialogData {
36 38 tenantId: string;
... ... @@ -42,7 +44,7 @@ export interface AddUserDialogData {
42 44 selector: 'tb-add-user-dialog',
43 45 templateUrl: './add-user-dialog.component.html'
44 46 })
45   -export class AddUserDialogComponent extends PageComponent implements OnInit {
  47 +export class AddUserDialogComponent extends DialogComponent<AddUserDialogComponent, User> implements OnInit {
46 48
47 49 detailsForm: NgForm;
48 50 user: User;
... ... @@ -56,11 +58,12 @@ export class AddUserDialogComponent extends PageComponent implements OnInit {
56 58 @ViewChild(UserComponent, {static: true}) userComponent: UserComponent;
57 59
58 60 constructor(protected store: Store<AppState>,
  61 + protected router: Router,
59 62 @Inject(MAT_DIALOG_DATA) public data: AddUserDialogData,
60 63 public dialogRef: MatDialogRef<AddUserDialogComponent, User>,
61 64 private userService: UserService,
62 65 private dialog: MatDialog) {
63   - super(store);
  66 + super(store, router, dialogRef);
64 67 }
65 68
66 69 ngOnInit(): void {
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { OnDestroy } from '@angular/core';
  18 +import { PageComponent } from './page.component';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { MatDialogRef } from '@angular/material/dialog';
  22 +import { NavigationStart, Router, RouterEvent } from '@angular/router';
  23 +import { Subscription } from 'rxjs';
  24 +import { filter } from 'rxjs/operators';
  25 +
  26 +export abstract class DialogComponent<T, R = any> extends PageComponent implements OnDestroy {
  27 +
  28 + routerSubscription: Subscription;
  29 +
  30 + protected constructor(protected store: Store<AppState>,
  31 + protected router: Router,
  32 + protected dialogRef: MatDialogRef<T, R>) {
  33 + super(store);
  34 + this.routerSubscription = this.router.events
  35 + .pipe(
  36 + filter((event: RouterEvent) => event instanceof NavigationStart),
  37 + filter(() => !!this.dialogRef)
  38 + )
  39 + .subscribe(() => {
  40 + this.dialogRef.close();
  41 + });
  42 + }
  43 +
  44 + ngOnDestroy(): void {
  45 + console.log('Dialog destroy called');
  46 + super.ngOnDestroy();
  47 + if (this.routerSubscription) {
  48 + this.routerSubscription.unsubscribe();
  49 + }
  50 + }
  51 +}
... ...
... ... @@ -28,6 +28,7 @@
28 28 <input matInput type="text" placeholder="{{ !disabled ? ('entity.entity-list' | translate) : '' }}"
29 29 style="max-width: 200px;"
30 30 #entityInput
  31 + (focusin)="onFocus()"
31 32 formControlName="entity"
32 33 matAutocompleteOrigin
33 34 #origin="matAutocompleteOrigin"
... ...
... ... @@ -99,6 +99,8 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
99 99
100 100 private searchText = '';
101 101
  102 + private dirty = false;
  103 +
102 104 private propagateChange = (v: any) => { };
103 105
104 106 constructor(private store: Store<AppState>,
... ... @@ -126,7 +128,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
126 128 ngOnInit() {
127 129 this.filteredEntities = this.entityListFormGroup.get('entity').valueChanges
128 130 .pipe(
129   - startWith<string | BaseData<EntityId>>(''),
  131 + // startWith<string | BaseData<EntityId>>(''),
130 132 tap((value) => {
131 133 if (value && typeof value !== 'string') {
132 134 this.add(value);
... ... @@ -145,12 +147,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
145 147 }
146 148
147 149 setDisabledState(isDisabled: boolean): void {
148   - const emitEvent = this.disabled !== isDisabled;
149 150 this.disabled = isDisabled;
150 151 if (isDisabled) {
151   - this.entityListFormGroup.disable({emitEvent});
  152 + this.entityListFormGroup.disable({emitEvent: false});
152 153 } else {
153   - this.entityListFormGroup.enable({emitEvent});
  154 + this.entityListFormGroup.enable({emitEvent: false});
154 155 }
155 156 }
156 157
... ... @@ -169,14 +170,19 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
169 170 this.entityListFormGroup.get('entities').setValue(this.entities);
170 171 this.modelValue = null;
171 172 }
  173 + this.dirty = true;
172 174 }
173 175
174 176 reset() {
175 177 this.entities = [];
176 178 this.entityListFormGroup.get('entities').setValue(this.entities);
177 179 this.modelValue = null;
178   - this.entityListFormGroup.get('entity').patchValue('', {emitEvent: true});
  180 + if (this.entityInput) {
  181 + this.entityInput.nativeElement.value = '';
  182 + }
  183 + this.entityListFormGroup.get('entity').patchValue('', {emitEvent: false});
179 184 this.propagateChange(this.modelValue);
  185 + this.dirty = true;
180 186 }
181 187
182 188 add(entity: BaseData<EntityId>): void {
... ... @@ -212,12 +218,18 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
212 218
213 219 fetchEntities(searchText?: string): Observable<Array<BaseData<EntityId>>> {
214 220 this.searchText = searchText;
215   - return this.disabled ? of([]) :
216   - this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText,
  221 + return this.entityService.getEntitiesByNameFilter(this.entityTypeValue, searchText,
217 222 50, '', false, true).pipe(
218 223 map((data) => data ? data : []));
219 224 }
220 225
  226 + onFocus() {
  227 + if (this.dirty) {
  228 + this.entityListFormGroup.get('entity').updateValueAndValidity({onlySelf: true, emitEvent: true});
  229 + this.dirty = false;
  230 + }
  231 + }
  232 +
221 233 clear(value: string = '') {
222 234 this.entityInput.nativeElement.value = value;
223 235 this.entityListFormGroup.get('entity').patchValue(value, {emitEvent: true});
... ...
... ... @@ -54,13 +54,13 @@ export class FullscreenDirective {
54 54 constructor(public elementRef: ElementRef,
55 55 private viewContainerRef: ViewContainerRef,
56 56 private overlay: Overlay) {
57   -
58 57 }
59 58
60 59 enterFullscreen() {
61   - this.parentElement = this.elementRef.nativeElement.parentElement;
62   - this.parentElement.removeChild(this.elementRef.nativeElement);
63   - this.elementRef.nativeElement.classList.add('tb-fullscreen');
  60 + const targetElement = this.elementRef;
  61 + this.parentElement = targetElement.nativeElement.parentElement;
  62 + this.parentElement.removeChild(targetElement.nativeElement);
  63 + targetElement.nativeElement.classList.add('tb-fullscreen');
64 64 const position = this.overlay.position();
65 65 const config = new OverlayConfig({
66 66 hasBackdrop: false,
... ... @@ -73,21 +73,24 @@ export class FullscreenDirective {
73 73
74 74 this.overlayRef = this.overlay.create(config);
75 75 this.overlayRef.attach(new EmptyPortal());
76   - this.overlayRef.overlayElement.append( this.elementRef.nativeElement );
  76 + this.overlayRef.overlayElement.append( targetElement.nativeElement );
77 77 this.fullscreenChanged.emit(true);
78 78 }
79 79
80 80 exitFullscreen() {
  81 + const targetElement = this.elementRef;
81 82 if (this.parentElement) {
82   - this.overlayRef.overlayElement.removeChild( this.elementRef.nativeElement );
83   - this.parentElement.append( this.elementRef.nativeElement );
  83 + this.overlayRef.overlayElement.removeChild( targetElement.nativeElement );
  84 + this.parentElement.append(targetElement.nativeElement);
84 85 this.parentElement = null;
85 86 }
86   - this.elementRef.nativeElement.classList.remove('tb-fullscreen');
  87 + targetElement.nativeElement.classList.remove('tb-fullscreen');
  88 + if (this.elementRef !== targetElement) {
  89 + this.elementRef.nativeElement.classList.remove('tb-fullscreen');
  90 + }
87 91 this.overlayRef.dispose();
88 92 this.fullscreenChanged.emit(false);
89 93 }
90   -
91 94 }
92 95
93 96 class EmptyPortal extends ComponentPortal<TbAnchorComponent> {
... ...
... ... @@ -16,7 +16,8 @@
16 16
17 17 -->
18 18 <div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
19   - tb-fullscreen [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
  19 + tb-fullscreen
  20 + [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
20 21 <div fxLayout="row" fxLayoutAlign="start center">
21 22 <label class="tb-title no-padding"
22 23 ng-class="{'tb-required': required,
... ... @@ -29,7 +30,8 @@
29 30 <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
30 31 </button>
31 32 </div>
32   - <div fxFlex="0%" id="tb-json-panel" class="tb-json-object-panel" fxLayout="column">
  33 + <div fxFlex="0%" id="tb-json-panel" tb-toast toastTarget="jsonObjectEditor"
  34 + class="tb-json-object-panel" fxLayout="column">
33 35 <div fxFlex #jsonEditor id="tb-json-input" [ngClass]="{'fill-height': fillHeight}"></div>
34 36 </div>
35 37 </div>
... ...
... ... @@ -35,21 +35,20 @@
35 35 .fill-height {
36 36 height: 100%;
37 37 }
  38 +}
38 39
39   - .tb-json-object-panel {
40   - height: 100%;
41   - margin-left: 15px;
42   - border: 1px solid #c0c0c0;
  40 +.tb-json-object-panel {
  41 + height: 100%;
  42 + margin-left: 15px;
  43 + border: 1px solid #c0c0c0;
43 44
44   - #tb-json-input {
45   - width: 100%;
46   - min-width: 200px;
47   - height: 100%;
  45 + #tb-json-input {
  46 + width: 100%;
  47 + min-width: 200px;
  48 + height: 100%;
48 49
49   - &:not(.fill-height) {
50   - min-height: 200px;
51   - }
  50 + &:not(.fill-height) {
  51 + min-height: 200px;
52 52 }
53 53 }
54   -
55 54 }
... ...
... ... @@ -26,6 +26,9 @@ import {
26 26 import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, Validator, NG_VALIDATORS } from '@angular/forms';
27 27 import * as ace from 'ace-builds';
28 28 import { coerceBooleanProperty } from '@angular/cdk/coercion';
  29 +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
  30 +import { Store } from '@ngrx/store';
  31 +import { AppState } from '@core/core.state';
29 32
30 33 @Component({
31 34 selector: 'tb-json-object-edit',
... ... @@ -83,9 +86,14 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
83 86
84 87 objectValid: boolean;
85 88
  89 + validationError: string;
  90 +
  91 + errorShowed = false;
  92 +
86 93 private propagateChange = null;
87 94
88   - constructor() {
  95 + constructor(public elementRef: ElementRef,
  96 + protected store: Store<AppState>) {
89 97 }
90 98
91 99 ngOnInit(): void {
... ... @@ -109,6 +117,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
109 117 this.jsonEditor.session.setUseWrapMode(false);
110 118 this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1);
111 119 this.jsonEditor.on('change', () => {
  120 + this.cleanupJsonErrors();
112 121 this.updateView();
113 122 });
114 123 }
... ... @@ -132,6 +141,33 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
132 141 };
133 142 }
134 143
  144 + validateOnSubmit(): void {
  145 + if (!this.readonly) {
  146 + this.cleanupJsonErrors();
  147 + if (!this.objectValid) {
  148 + this.store.dispatch(new ActionNotificationShow(
  149 + {
  150 + message: this.validationError,
  151 + type: 'error',
  152 + target: 'jsonObjectEditor',
  153 + verticalPosition: 'bottom',
  154 + horizontalPosition: 'left'
  155 + }));
  156 + this.errorShowed = true;
  157 + }
  158 + }
  159 + }
  160 +
  161 + cleanupJsonErrors(): void {
  162 + if (this.errorShowed) {
  163 + this.store.dispatch(new ActionNotificationHide(
  164 + {
  165 + target: 'jsonObjectEditor'
  166 + }));
  167 + this.errorShowed = false;
  168 + }
  169 + }
  170 +
135 171 writeValue(value: any): void {
136 172 this.modelValue = value;
137 173 this.contentValue = '';
... ... @@ -142,6 +178,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
142 178 this.objectValid = true;
143 179 } else {
144 180 this.objectValid = !this.required;
  181 + this.validationError = 'Json object is required.';
145 182 }
146 183 } catch (e) {
147 184 //
... ... @@ -161,9 +198,20 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
161 198 try {
162 199 data = JSON.parse(this.contentValue);
163 200 this.objectValid = true;
164   - } catch (ex) {}
  201 + this.validationError = '';
  202 + } catch (ex) {
  203 + let errorInfo = 'Error:';
  204 + if (ex.name) {
  205 + errorInfo += ' ' + ex.name + ':';
  206 + }
  207 + if (ex.message) {
  208 + errorInfo += ' ' + ex.message;
  209 + }
  210 + this.validationError = errorInfo;
  211 + }
165 212 } else {
166 213 this.objectValid = !this.required;
  214 + this.validationError = this.required ? 'Json object is required.' : '';
167 215 }
168 216 this.propagateChange(data);
169 217 }
... ...
... ... @@ -19,7 +19,7 @@ import { select, Store } from '@ngrx/store';
19 19 import { AppState } from '../../core/core.state';
20 20 import { Observable, Subscription } from 'rxjs';
21 21 import { selectIsLoading } from '../../core/interceptors/load.selectors';
22   -import { delay } from 'rxjs/operators';
  22 +import { delay, share } from 'rxjs/operators';
23 23 import { AbstractControl } from '@angular/forms';
24 24
25 25 export abstract class PageComponent implements OnDestroy {
... ... @@ -29,7 +29,7 @@ export abstract class PageComponent implements OnDestroy {
29 29 disabledOnLoadFormControls: Array<AbstractControl> = [];
30 30
31 31 protected constructor(protected store: Store<AppState>) {
32   - this.isLoading$ = this.store.pipe(delay(0), select(selectIsLoading), delay(100));
  32 + this.isLoading$ = this.store.pipe(delay(0), select(selectIsLoading), share());
33 33 }
34 34
35 35 protected registerDisableOnLoadFormControl(control: AbstractControl) {
... ...
... ... @@ -45,6 +45,7 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
45 45 toastTarget = 'root';
46 46
47 47 private notificationSubscription: Subscription = null;
  48 + private hideNotificationSubscription: Subscription = null;
48 49
49 50 constructor(public elementRef: ElementRef,
50 51 public viewContainerRef: ViewContainerRef,
... ... @@ -78,12 +79,26 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
78 79 }
79 80 }
80 81 );
  82 +
  83 + this.hideNotificationSubscription = this.notificationService.getHideNotification().subscribe(
  84 + (hideNotification) => {
  85 + if (hideNotification) {
  86 + const target = hideNotification.target || 'root';
  87 + if (this.toastTarget === target) {
  88 + this.snackBar.dismiss();
  89 + }
  90 + }
  91 + }
  92 + );
81 93 }
82 94
83 95 ngOnDestroy(): void {
84 96 if (this.notificationSubscription) {
85 97 this.notificationSubscription.unsubscribe();
86 98 }
  99 + if (this.hideNotificationSubscription) {
  100 + this.hideNotificationSubscription.unsubscribe();
  101 + }
87 102 }
88 103 }
89 104
... ...