Commit 984e260be38ca2c16f37bbfa9ff68aa496d89084
1 parent
3700bcaa
Devices and Customers pages implementation
Showing
21 changed files
with
853 additions
and
77 deletions
... | ... | @@ -102,6 +102,11 @@ export class DeviceService { |
102 | 102 | return this.http.post<Device>(`/api/customer/public/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
103 | 103 | } |
104 | 104 | |
105 | + public assignDeviceToCustomer(customerId: string, deviceId: string, | |
106 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> { | |
107 | + return this.http.post<Device>(`/api/customer/${customerId}/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
108 | + } | |
109 | + | |
105 | 110 | public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
106 | 111 | return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
107 | 112 | } | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<form #assignToCustomerForm="ngForm" [formGroup]="assignToCustomerFormGroup" (ngSubmit)="assign()"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2>{{ assignToCustomerTitle | translate }}</h2> | |
21 | + <span fxFlex></span> | |
22 | + <button mat-button mat-icon-button | |
23 | + (click)="cancel()" | |
24 | + type="button"> | |
25 | + <mat-icon class="material-icons">close</mat-icon> | |
26 | + </button> | |
27 | + </mat-toolbar> | |
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
29 | + </mat-progress-bar> | |
30 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
31 | + <div mat-dialog-content> | |
32 | + <fieldset [disabled]="isLoading$ | async"> | |
33 | + <span>{{ assignToCustomerText | translate }}</span> | |
34 | + <tb-entity-autocomplete | |
35 | + formControlName="customerId" | |
36 | + required | |
37 | + [entityType]="entityType.CUSTOMER"> | |
38 | + </tb-entity-autocomplete> | |
39 | + </fieldset> | |
40 | + </div> | |
41 | + <div mat-dialog-actions fxLayout="row"> | |
42 | + <span fxFlex></span> | |
43 | + <button mat-button mat-raised-button color="primary" | |
44 | + type="submit" | |
45 | + [disabled]="(isLoading$ | async) || assignToCustomerForm.invalid | |
46 | + || !assignToCustomerForm.dirty"> | |
47 | + {{ 'action.assign' | translate }} | |
48 | + </button> | |
49 | + <button mat-button color="primary" | |
50 | + style="margin-right: 20px;" | |
51 | + type="button" | |
52 | + [disabled]="(isLoading$ | async)" | |
53 | + (click)="cancel()" cdkFocusInitial> | |
54 | + {{ 'action.cancel' | translate }} | |
55 | + </button> | |
56 | + </div> | |
57 | +</form> | ... | ... |
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 {Component, Inject, OnInit, SkipSelf} from '@angular/core'; | |
18 | +import {ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material'; | |
19 | +import {PageComponent} from '@shared/components/page.component'; | |
20 | +import {Store} from '@ngrx/store'; | |
21 | +import {AppState} from '@core/core.state'; | |
22 | +import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms'; | |
23 | +import {DeviceService} from '@core/http/device.service'; | |
24 | +import {EntityId} from '@shared/models/id/entity-id'; | |
25 | +import {EntityType} from '@shared/models/entity-type.models'; | |
26 | +import {forkJoin, Observable} from 'rxjs'; | |
27 | + | |
28 | +export interface AssignToCustomerDialogData { | |
29 | + entityIds: Array<EntityId>; | |
30 | + entityType: EntityType; | |
31 | +} | |
32 | + | |
33 | +@Component({ | |
34 | + selector: 'tb-assign-to-customer-dialog', | |
35 | + templateUrl: './assign-to-customer-dialog.component.html', | |
36 | + providers: [{provide: ErrorStateMatcher, useExisting: AssignToCustomerDialogComponent}], | |
37 | + styleUrls: [] | |
38 | +}) | |
39 | +export class AssignToCustomerDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher { | |
40 | + | |
41 | + assignToCustomerFormGroup: FormGroup; | |
42 | + | |
43 | + submitted = false; | |
44 | + | |
45 | + entityType = EntityType; | |
46 | + | |
47 | + assignToCustomerTitle: string; | |
48 | + assignToCustomerText: string; | |
49 | + | |
50 | + constructor(protected store: Store<AppState>, | |
51 | + @Inject(MAT_DIALOG_DATA) public data: AssignToCustomerDialogData, | |
52 | + private deviceService: DeviceService, | |
53 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
54 | + public dialogRef: MatDialogRef<AssignToCustomerDialogComponent, boolean>, | |
55 | + public fb: FormBuilder) { | |
56 | + super(store); | |
57 | + } | |
58 | + | |
59 | + ngOnInit(): void { | |
60 | + this.assignToCustomerFormGroup = this.fb.group({ | |
61 | + customerId: [null, [Validators.required]] | |
62 | + }); | |
63 | + switch (this.data.entityType) { | |
64 | + case EntityType.DEVICE: | |
65 | + this.assignToCustomerTitle = 'device.assign-device-to-customer'; | |
66 | + this.assignToCustomerText = 'device.assign-to-customer-text'; | |
67 | + break; | |
68 | + case EntityType.ASSET: | |
69 | + // TODO: | |
70 | + break; | |
71 | + case EntityType.ENTITY_VIEW: | |
72 | + // TODO: | |
73 | + break; | |
74 | + } | |
75 | + } | |
76 | + | |
77 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
78 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
79 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
80 | + return originalErrorState || customErrorState; | |
81 | + } | |
82 | + | |
83 | + cancel(): void { | |
84 | + this.dialogRef.close(false); | |
85 | + } | |
86 | + | |
87 | + assign(): void { | |
88 | + this.submitted = true; | |
89 | + const customerId: string = this.assignToCustomerFormGroup.get('customerId').value; | |
90 | + const tasks: Observable<any>[] = []; | |
91 | + this.data.entityIds.forEach( | |
92 | + (entityId) => { | |
93 | + tasks.push(this.getAssignToCustomerTask(customerId, entityId.id)); | |
94 | + } | |
95 | + ); | |
96 | + forkJoin(tasks).subscribe( | |
97 | + () => { | |
98 | + this.dialogRef.close(true); | |
99 | + } | |
100 | + ); | |
101 | + } | |
102 | + | |
103 | + private getAssignToCustomerTask(customerId: string, entityId: string): Observable<any> { | |
104 | + switch (this.data.entityType) { | |
105 | + case EntityType.DEVICE: | |
106 | + return this.deviceService.assignDeviceToCustomer(customerId, entityId); | |
107 | + break; | |
108 | + case EntityType.ASSET: | |
109 | + // TODO: | |
110 | + break; | |
111 | + case EntityType.ENTITY_VIEW: | |
112 | + // TODO: | |
113 | + break; | |
114 | + } | |
115 | + } | |
116 | + | |
117 | +} | ... | ... |
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 { NgModule } from '@angular/core'; | |
18 | +import { CommonModule } from '@angular/common'; | |
19 | +import { SharedModule } from '@app/shared/shared.module'; | |
20 | +import {AssignToCustomerDialogComponent} from '@modules/home/dialogs/assign-to-customer-dialog.component'; | |
21 | + | |
22 | +@NgModule({ | |
23 | + entryComponents: [ | |
24 | + AssignToCustomerDialogComponent | |
25 | + ], | |
26 | + declarations: | |
27 | + [ | |
28 | + AssignToCustomerDialogComponent | |
29 | + ], | |
30 | + imports: [ | |
31 | + CommonModule, | |
32 | + SharedModule | |
33 | + ], | |
34 | + exports: [ | |
35 | + AssignToCustomerDialogComponent | |
36 | + ] | |
37 | +}) | |
38 | +export class HomeDialogsModule { } | ... | ... |
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 {NgModule} from '@angular/core'; | |
18 | +import {RouterModule, Routes} from '@angular/router'; | |
19 | + | |
20 | +import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component'; | |
21 | +import {Authority} from '@shared/models/authority.enum'; | |
22 | +import {UsersTableConfigResolver} from '../user/users-table-config.resolver'; | |
23 | +import {CustomersTableConfigResolver} from './customers-table-config.resolver'; | |
24 | + | |
25 | +const routes: Routes = [ | |
26 | + { | |
27 | + path: 'customers', | |
28 | + data: { | |
29 | + breadcrumb: { | |
30 | + label: 'customer.customers', | |
31 | + icon: 'supervisor_account' | |
32 | + } | |
33 | + }, | |
34 | + children: [ | |
35 | + { | |
36 | + path: '', | |
37 | + component: EntitiesTableComponent, | |
38 | + data: { | |
39 | + auth: [Authority.TENANT_ADMIN], | |
40 | + title: 'customer.customers' | |
41 | + }, | |
42 | + resolve: { | |
43 | + entitiesTableConfig: CustomersTableConfigResolver | |
44 | + } | |
45 | + }, | |
46 | + { | |
47 | + path: ':customerId/users', | |
48 | + component: EntitiesTableComponent, | |
49 | + data: { | |
50 | + auth: [Authority.TENANT_ADMIN], | |
51 | + title: 'user.customer-users', | |
52 | + breadcrumb: { | |
53 | + label: 'user.customer-users', | |
54 | + icon: 'account_circle' | |
55 | + } | |
56 | + }, | |
57 | + resolve: { | |
58 | + entitiesTableConfig: UsersTableConfigResolver | |
59 | + } | |
60 | + } | |
61 | + ] | |
62 | + } | |
63 | +]; | |
64 | + | |
65 | +@NgModule({ | |
66 | + imports: [RouterModule.forChild(routes)], | |
67 | + exports: [RouterModule], | |
68 | + providers: [ | |
69 | + CustomersTableConfigResolver | |
70 | + ] | |
71 | +}) | |
72 | +export class CustomerRoutingModule { } | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<div class="tb-details-buttons"> | |
19 | + <button mat-raised-button color="primary" | |
20 | + [disabled]="(isLoading$ | async)" | |
21 | + (click)="onEntityAction($event, 'manageUsers')" | |
22 | + [fxShow]="!isEdit && !isPublic"> | |
23 | + {{'customer.manage-users' | translate }} | |
24 | + </button> | |
25 | + <button mat-raised-button color="primary" | |
26 | + [disabled]="(isLoading$ | async)" | |
27 | + (click)="onEntityAction($event, 'manageAssets')" | |
28 | + [fxShow]="!isEdit"> | |
29 | + {{'customer.manage-assets' | translate }} | |
30 | + </button> | |
31 | + <button mat-raised-button color="primary" | |
32 | + [disabled]="(isLoading$ | async)" | |
33 | + (click)="onEntityAction($event, 'manageDevices')" | |
34 | + [fxShow]="!isEdit"> | |
35 | + {{'customer.manage-devices' | translate }} | |
36 | + </button> | |
37 | + <button mat-raised-button color="primary" | |
38 | + [disabled]="(isLoading$ | async)" | |
39 | + (click)="onEntityAction($event, 'manageDashboards')" | |
40 | + [fxShow]="!isEdit"> | |
41 | + {{'customer.manage-dashboards' | translate }} | |
42 | + </button> | |
43 | + <button mat-raised-button color="primary" | |
44 | + [disabled]="(isLoading$ | async)" | |
45 | + (click)="onEntityAction($event, 'delete')" | |
46 | + [fxShow]="!hideDelete() && !isEdit && !isPublic"> | |
47 | + {{'customer.delete' | translate }} | |
48 | + </button> | |
49 | + <div fxLayout="row"> | |
50 | + <button mat-raised-button | |
51 | + ngxClipboard | |
52 | + (cbOnSuccess)="onCustomerIdCopied($event)" | |
53 | + [cbContent]="entity?.id?.id" | |
54 | + [fxShow]="!isEdit"> | |
55 | + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> | |
56 | + <span translate>customer.copyId</span> | |
57 | + </button> | |
58 | + </div> | |
59 | +</div> | |
60 | +<div class="mat-padding" fxLayout="column"> | |
61 | + <form #entityNgForm="ngForm" [formGroup]="entityForm"> | |
62 | + <fieldset [fxShow]="!isPublic" [disabled]="(isLoading$ | async) || !isEdit"> | |
63 | + <mat-form-field class="mat-block"> | |
64 | + <mat-label translate>customer.title</mat-label> | |
65 | + <input matInput formControlName="title" required/> | |
66 | + <mat-error *ngIf="entityForm.get('title').hasError('required')"> | |
67 | + {{ 'customer.title-required' | translate }} | |
68 | + </mat-error> | |
69 | + </mat-form-field> | |
70 | + <div formGroupName="additionalInfo" fxLayout="column"> | |
71 | + <mat-form-field class="mat-block"> | |
72 | + <mat-label translate>customer.description</mat-label> | |
73 | + <textarea matInput formControlName="description" rows="2"></textarea> | |
74 | + </mat-form-field> | |
75 | + </div> | |
76 | + <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact> | |
77 | + </fieldset> | |
78 | + </form> | |
79 | +</div> | ... | ... |
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 { Component } from '@angular/core'; | |
18 | +import { Store } from '@ngrx/store'; | |
19 | +import { AppState } from '@core/core.state'; | |
20 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
21 | +import { Customer } from '@shared/models/customer.model'; | |
22 | +import { ContactBasedComponent } from '@shared/components/entity/contact-based.component'; | |
23 | +import {Tenant} from '@app/shared/models/tenant.model'; | |
24 | +import {ActionNotificationShow} from '@app/core/notification/notification.actions'; | |
25 | +import {TranslateService} from '@ngx-translate/core'; | |
26 | + | |
27 | +@Component({ | |
28 | + selector: 'tb-customer', | |
29 | + templateUrl: './customer.component.html' | |
30 | +}) | |
31 | +export class CustomerComponent extends ContactBasedComponent<Customer> { | |
32 | + | |
33 | + isPublic = false; | |
34 | + | |
35 | + constructor(protected store: Store<AppState>, | |
36 | + protected translate: TranslateService, | |
37 | + protected fb: FormBuilder) { | |
38 | + super(store, fb); | |
39 | + } | |
40 | + | |
41 | + hideDelete() { | |
42 | + if (this.entitiesTableConfig) { | |
43 | + return !this.entitiesTableConfig.deleteEnabled(this.entity); | |
44 | + } else { | |
45 | + return false; | |
46 | + } | |
47 | + } | |
48 | + | |
49 | + buildEntityForm(entity: Customer): FormGroup { | |
50 | + return this.fb.group( | |
51 | + { | |
52 | + title: [entity ? entity.title : '', [Validators.required]], | |
53 | + additionalInfo: this.fb.group( | |
54 | + { | |
55 | + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] | |
56 | + } | |
57 | + ) | |
58 | + } | |
59 | + ); | |
60 | + } | |
61 | + | |
62 | + updateEntityForm(entity: Customer) { | |
63 | + this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic; | |
64 | + this.entityForm.patchValue({title: entity.title}); | |
65 | + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); | |
66 | + } | |
67 | + | |
68 | + onCustomerIdCopied(event) { | |
69 | + this.store.dispatch(new ActionNotificationShow( | |
70 | + { | |
71 | + message: this.translate.instant('customer.idCopiedMessage'), | |
72 | + type: 'success', | |
73 | + duration: 750, | |
74 | + verticalPosition: 'bottom', | |
75 | + horizontalPosition: 'right' | |
76 | + })); | |
77 | + } | |
78 | + | |
79 | +} | ... | ... |
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 { NgModule } from '@angular/core'; | |
18 | +import { CommonModule } from '@angular/common'; | |
19 | +import { SharedModule } from '@shared/shared.module'; | |
20 | +import {CustomerComponent} from '@modules/home/pages/customer/customer.component'; | |
21 | +import {CustomerRoutingModule} from './customer-routing.module'; | |
22 | + | |
23 | +@NgModule({ | |
24 | + entryComponents: [ | |
25 | + CustomerComponent | |
26 | + ], | |
27 | + declarations: [ | |
28 | + CustomerComponent | |
29 | + ], | |
30 | + imports: [ | |
31 | + CommonModule, | |
32 | + SharedModule, | |
33 | + CustomerRoutingModule | |
34 | + ] | |
35 | +}) | |
36 | +export class CustomerModule { } | ... | ... |
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 { Injectable } from '@angular/core'; | |
18 | + | |
19 | +import { Resolve, Router } from '@angular/router'; | |
20 | + | |
21 | +import { Tenant } from '@shared/models/tenant.model'; | |
22 | +import { | |
23 | + DateEntityTableColumn, | |
24 | + EntityTableColumn, | |
25 | + EntityTableConfig | |
26 | +} from '@shared/components/entity/entities-table-config.models'; | |
27 | +import { TranslateService } from '@ngx-translate/core'; | |
28 | +import { DatePipe } from '@angular/common'; | |
29 | +import { | |
30 | + EntityType, | |
31 | + entityTypeResources, | |
32 | + entityTypeTranslations | |
33 | +} from '@shared/models/entity-type.models'; | |
34 | +import { EntityAction } from '@shared/components/entity/entity-component.models'; | |
35 | +import {Customer} from '@app/shared/models/customer.model'; | |
36 | +import {CustomerService} from '@app/core/http/customer.service'; | |
37 | +import {CustomerComponent} from '@modules/home/pages/customer/customer.component'; | |
38 | + | |
39 | +@Injectable() | |
40 | +export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<Customer>> { | |
41 | + | |
42 | + private readonly config: EntityTableConfig<Customer> = new EntityTableConfig<Customer>(); | |
43 | + | |
44 | + constructor(private customerService: CustomerService, | |
45 | + private translate: TranslateService, | |
46 | + private datePipe: DatePipe, | |
47 | + private router: Router) { | |
48 | + | |
49 | + this.config.entityType = EntityType.CUSTOMER; | |
50 | + this.config.entityComponent = CustomerComponent; | |
51 | + this.config.entityTranslations = entityTypeTranslations.get(EntityType.CUSTOMER); | |
52 | + this.config.entityResources = entityTypeResources.get(EntityType.CUSTOMER); | |
53 | + | |
54 | + this.config.columns.push( | |
55 | + new DateEntityTableColumn<Customer>('createdTime', 'customer.created-time', this.datePipe, '150px'), | |
56 | + new EntityTableColumn<Customer>('title', 'customer.title'), | |
57 | + new EntityTableColumn<Customer>('email', 'contact.email'), | |
58 | + new EntityTableColumn<Customer>('country', 'contact.country'), | |
59 | + new EntityTableColumn<Customer>('city', 'contact.city') | |
60 | + ); | |
61 | + | |
62 | + this.config.cellActionDescriptors.push( | |
63 | + { | |
64 | + name: this.translate.instant('customer.manage-customer-users'), | |
65 | + icon: 'account_circle', | |
66 | + isEnabled: (customer) => !customer.additionalInfo || !customer.additionalInfo.isPublic, | |
67 | + onAction: ($event, entity) => this.manageCustomerUsers($event, entity) | |
68 | + } | |
69 | + ); | |
70 | + | |
71 | + this.config.deleteEntityTitle = customer => this.translate.instant('customer.delete-customer-title', { customerTitle: customer.title }); | |
72 | + this.config.deleteEntityContent = () => this.translate.instant('customer.delete-customer-text'); | |
73 | + this.config.deleteEntitiesTitle = count => this.translate.instant('customer.delete-customers-title', {count}); | |
74 | + this.config.deleteEntitiesContent = () => this.translate.instant('customer.delete-customers-text'); | |
75 | + | |
76 | + this.config.entitiesFetchFunction = pageLink => this.customerService.getCustomers(pageLink); | |
77 | + this.config.loadEntity = id => this.customerService.getCustomer(id.id); | |
78 | + this.config.saveEntity = customer => this.customerService.saveCustomer(customer); | |
79 | + this.config.deleteEntity = id => this.customerService.deleteCustomer(id.id); | |
80 | + this.config.onEntityAction = action => this.onCustomerAction(action); | |
81 | + } | |
82 | + | |
83 | + resolve(): EntityTableConfig<Customer> { | |
84 | + this.config.tableTitle = this.translate.instant('customer.customers'); | |
85 | + | |
86 | + return this.config; | |
87 | + } | |
88 | + | |
89 | + manageCustomerUsers($event: Event, customer: Customer) { | |
90 | + if ($event) { | |
91 | + $event.stopPropagation(); | |
92 | + } | |
93 | + this.router.navigateByUrl(`customers/${customer.id.id}/users`); | |
94 | + } | |
95 | + | |
96 | + onCustomerAction(action: EntityAction<Customer>): boolean { | |
97 | + switch (action.action) { | |
98 | + case 'manageUsers': | |
99 | + this.manageCustomerUsers(action.event, action.entity); | |
100 | + return true; | |
101 | + } | |
102 | + return false; | |
103 | + } | |
104 | + | |
105 | +} | ... | ... |
... | ... | @@ -21,6 +21,7 @@ import {DeviceComponent} from '@modules/home/pages/device/device.component'; |
21 | 21 | import {DeviceRoutingModule} from './device-routing.module'; |
22 | 22 | import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component'; |
23 | 23 | import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/device-credentials-dialog.component'; |
24 | +import {HomeDialogsModule} from '../../dialogs/home-dialogs.module'; | |
24 | 25 | |
25 | 26 | @NgModule({ |
26 | 27 | entryComponents: [ |
... | ... | @@ -36,6 +37,7 @@ import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/devic |
36 | 37 | imports: [ |
37 | 38 | CommonModule, |
38 | 39 | SharedModule, |
40 | + HomeDialogsModule, | |
39 | 41 | DeviceRoutingModule |
40 | 42 | ] |
41 | 43 | }) | ... | ... |
... | ... | @@ -14,35 +14,26 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Injectable } from '@angular/core'; | |
17 | +import {Injectable} from '@angular/core'; | |
18 | 18 | |
19 | 19 | import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router'; |
20 | - | |
21 | -import { Tenant } from '@shared/models/tenant.model'; | |
22 | 20 | import { |
23 | 21 | CellActionDescriptor, |
24 | 22 | checkBoxCell, |
25 | 23 | DateEntityTableColumn, |
26 | 24 | EntityTableColumn, |
27 | - EntityTableConfig, | |
25 | + EntityTableConfig, GroupActionDescriptor, | |
28 | 26 | HeaderActionDescriptor |
29 | 27 | } from '@shared/components/entity/entities-table-config.models'; |
30 | -import { TenantService } from '@core/http/tenant.service'; | |
31 | -import { TranslateService } from '@ngx-translate/core'; | |
32 | -import { DatePipe } from '@angular/common'; | |
33 | -import { | |
34 | - EntityType, | |
35 | - entityTypeResources, | |
36 | - entityTypeTranslations | |
37 | -} from '@shared/models/entity-type.models'; | |
38 | -import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; | |
39 | -import { EntityAction } from '@shared/components/entity/entity-component.models'; | |
40 | -import { User } from '@shared/models/user.model'; | |
28 | +import {TranslateService} from '@ngx-translate/core'; | |
29 | +import {DatePipe} from '@angular/common'; | |
30 | +import {EntityType, entityTypeResources, entityTypeTranslations} from '@shared/models/entity-type.models'; | |
31 | +import {EntityAction} from '@shared/components/entity/entity-component.models'; | |
41 | 32 | import {Device, DeviceCredentials, DeviceInfo} from '@app/shared/models/device.models'; |
42 | 33 | import {DeviceComponent} from '@modules/home/pages/device/device.component'; |
43 | -import {Observable, of} from 'rxjs'; | |
34 | +import {forkJoin, Observable, of} from 'rxjs'; | |
44 | 35 | import {select, Store} from '@ngrx/store'; |
45 | -import {selectAuth, selectAuthUser} from '@core/auth/auth.selectors'; | |
36 | +import {selectAuthUser} from '@core/auth/auth.selectors'; | |
46 | 37 | import {map, mergeMap, take, tap} from 'rxjs/operators'; |
47 | 38 | import {AppState} from '@core/core.state'; |
48 | 39 | import {DeviceService} from '@app/core/http/device.service'; |
... | ... | @@ -58,6 +49,11 @@ import { |
58 | 49 | DeviceCredentialsDialogData |
59 | 50 | } from '@modules/home/pages/device/device-credentials-dialog.component'; |
60 | 51 | import {DialogService} from '@core/services/dialog.service'; |
52 | +import { | |
53 | + AssignToCustomerDialogComponent, | |
54 | + AssignToCustomerDialogData | |
55 | +} from '@modules/home/dialogs/assign-to-customer-dialog.component'; | |
56 | +import {DeviceId} from '@app/shared/models/id/device-id'; | |
61 | 57 | |
62 | 58 | @Injectable() |
63 | 59 | export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> { |
... | ... | @@ -76,7 +72,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
76 | 72 | private router: Router, |
77 | 73 | private dialog: MatDialog) { |
78 | 74 | |
79 | - this.config.entityType = EntityType.CUSTOMER; | |
75 | + this.config.entityType = EntityType.DEVICE; | |
80 | 76 | this.config.entityComponent = DeviceComponent; |
81 | 77 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE); |
82 | 78 | this.config.entityResources = entityTypeResources.get(EntityType.DEVICE); |
... | ... | @@ -131,7 +127,11 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
131 | 127 | this.config.columns = this.configureColumns(this.config.componentsData.deviceScope); |
132 | 128 | this.configureEntityFunctions(this.config.componentsData.deviceScope); |
133 | 129 | this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope); |
130 | + this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope); | |
134 | 131 | this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope); |
132 | + this.config.addEnabled = this.config.componentsData.deviceScope !== 'customer_user'; | |
133 | + this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant'; | |
134 | + this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant'; | |
135 | 135 | return this.config; |
136 | 136 | }) |
137 | 137 | ); |
... | ... | @@ -175,7 +175,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
175 | 175 | } |
176 | 176 | |
177 | 177 | configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> { |
178 | - const actions: Array<CellActionDescriptor<Device>> = []; | |
178 | + const actions: Array<CellActionDescriptor<DeviceInfo>> = []; | |
179 | 179 | if (deviceScope === 'tenant') { |
180 | 180 | actions.push( |
181 | 181 | { |
... | ... | @@ -183,6 +183,87 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
183 | 183 | icon: 'share', |
184 | 184 | isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), |
185 | 185 | onAction: ($event, entity) => this.makePublic($event, entity) |
186 | + }, | |
187 | + { | |
188 | + name: this.translate.instant('device.assign-to-customer'), | |
189 | + icon: 'assignment_ind', | |
190 | + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), | |
191 | + onAction: ($event, entity) => this.assignToCustomer($event, [entity.id]) | |
192 | + }, | |
193 | + { | |
194 | + name: this.translate.instant('device.unassign-from-customer'), | |
195 | + icon: 'assignment_return', | |
196 | + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), | |
197 | + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) | |
198 | + }, | |
199 | + { | |
200 | + name: this.translate.instant('device.make-private'), | |
201 | + icon: 'reply', | |
202 | + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), | |
203 | + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) | |
204 | + }, | |
205 | + { | |
206 | + name: this.translate.instant('device.manage-credentials'), | |
207 | + icon: 'security', | |
208 | + isEnabled: (entity) => true, | |
209 | + onAction: ($event, entity) => this.manageCredentials($event, entity) | |
210 | + } | |
211 | + ); | |
212 | + } | |
213 | + if (deviceScope === 'customer') { | |
214 | + actions.push( | |
215 | + { | |
216 | + name: this.translate.instant('device.unassign-from-customer'), | |
217 | + icon: 'assignment_return', | |
218 | + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && !entity.customerIsPublic), | |
219 | + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) | |
220 | + }, | |
221 | + { | |
222 | + name: this.translate.instant('device.make-private'), | |
223 | + icon: 'reply', | |
224 | + isEnabled: (entity) => (entity.customerId && entity.customerId.id !== NULL_UUID && entity.customerIsPublic), | |
225 | + onAction: ($event, entity) => this.unassignFromCustomer($event, entity) | |
226 | + }, | |
227 | + { | |
228 | + name: this.translate.instant('device.manage-credentials'), | |
229 | + icon: 'security', | |
230 | + isEnabled: (entity) => true, | |
231 | + onAction: ($event, entity) => this.manageCredentials($event, entity) | |
232 | + } | |
233 | + ); | |
234 | + } | |
235 | + if (deviceScope === 'customer_user') { | |
236 | + actions.push( | |
237 | + { | |
238 | + name: this.translate.instant('device.view-credentials'), | |
239 | + icon: 'security', | |
240 | + isEnabled: (entity) => true, | |
241 | + onAction: ($event, entity) => this.manageCredentials($event, entity) | |
242 | + } | |
243 | + ); | |
244 | + } | |
245 | + return actions; | |
246 | + } | |
247 | + | |
248 | + configureGroupActions(deviceScope: string): Array<GroupActionDescriptor<DeviceInfo>> { | |
249 | + const actions: Array<GroupActionDescriptor<DeviceInfo>> = []; | |
250 | + if (deviceScope === 'tenant') { | |
251 | + actions.push( | |
252 | + { | |
253 | + name: this.translate.instant('device.assign-devices'), | |
254 | + icon: 'assignment_ind', | |
255 | + isEnabled: true, | |
256 | + onAction: ($event, entities) => this.assignToCustomer($event, entities.map((entity) => entity.id)) | |
257 | + } | |
258 | + ); | |
259 | + } | |
260 | + if (deviceScope === 'customer') { | |
261 | + actions.push( | |
262 | + { | |
263 | + name: this.translate.instant('device.unassign-devices'), | |
264 | + icon: 'assignment_return', | |
265 | + isEnabled: true, | |
266 | + onAction: ($event, entities) => this.unassignDevicesFromCustomer($event, entities) | |
186 | 267 | } |
187 | 268 | ); |
188 | 269 | } |
... | ... | @@ -191,20 +272,32 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
191 | 272 | |
192 | 273 | configureAddActions(deviceScope: string): Array<HeaderActionDescriptor> { |
193 | 274 | const actions: Array<HeaderActionDescriptor> = []; |
194 | - actions.push( | |
195 | - { | |
196 | - name: this.translate.instant('device.add-device-text'), | |
197 | - icon: 'insert_drive_file', | |
198 | - isEnabled: () => true, | |
199 | - onAction: ($event) => this.config.table.addEntity($event) | |
200 | - }, | |
201 | - { | |
202 | - name: this.translate.instant('device.import'), | |
203 | - icon: 'file_upload', | |
204 | - isEnabled: () => true, | |
205 | - onAction: ($event) => this.importDevices($event) | |
206 | - } | |
207 | - ); | |
275 | + if (deviceScope === 'tenant') { | |
276 | + actions.push( | |
277 | + { | |
278 | + name: this.translate.instant('device.add-device-text'), | |
279 | + icon: 'insert_drive_file', | |
280 | + isEnabled: () => true, | |
281 | + onAction: ($event) => this.config.table.addEntity($event) | |
282 | + }, | |
283 | + { | |
284 | + name: this.translate.instant('device.import'), | |
285 | + icon: 'file_upload', | |
286 | + isEnabled: () => true, | |
287 | + onAction: ($event) => this.importDevices($event) | |
288 | + } | |
289 | + ); | |
290 | + } | |
291 | + if (deviceScope === 'customer') { | |
292 | + actions.push( | |
293 | + { | |
294 | + name: this.translate.instant('device.assign-new-device'), | |
295 | + icon: 'add', | |
296 | + isEnabled: () => true, | |
297 | + onAction: ($event) => this.addDevicesToCustomer($event) | |
298 | + } | |
299 | + ); | |
300 | + } | |
208 | 301 | return actions; |
209 | 302 | } |
210 | 303 | |
... | ... | @@ -215,6 +308,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
215 | 308 | // TODO: |
216 | 309 | } |
217 | 310 | |
311 | + addDevicesToCustomer($event: Event) { | |
312 | + if ($event) { | |
313 | + $event.stopPropagation(); | |
314 | + } | |
315 | + // TODO: | |
316 | + } | |
317 | + | |
218 | 318 | makePublic($event: Event, device: Device) { |
219 | 319 | if ($event) { |
220 | 320 | $event.stopPropagation(); |
... | ... | @@ -237,11 +337,24 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
237 | 337 | ); |
238 | 338 | } |
239 | 339 | |
240 | - assignToCustomer($event: Event, device: Device) { | |
340 | + assignToCustomer($event: Event, deviceIds: Array<DeviceId>) { | |
241 | 341 | if ($event) { |
242 | 342 | $event.stopPropagation(); |
243 | 343 | } |
244 | - // TODO: | |
344 | + this.dialog.open<AssignToCustomerDialogComponent, AssignToCustomerDialogData, | |
345 | + boolean>(AssignToCustomerDialogComponent, { | |
346 | + disableClose: true, | |
347 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
348 | + data: { | |
349 | + entityIds: deviceIds, | |
350 | + entityType: EntityType.DEVICE | |
351 | + } | |
352 | + }).afterClosed() | |
353 | + .subscribe((res) => { | |
354 | + if (res) { | |
355 | + this.config.table.updateData(); | |
356 | + } | |
357 | + }); | |
245 | 358 | } |
246 | 359 | |
247 | 360 | unassignFromCustomer($event: Event, device: DeviceInfo) { |
... | ... | @@ -259,8 +372,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
259 | 372 | content = this.translate.instant('device.unassign-device-text'); |
260 | 373 | } |
261 | 374 | this.dialogService.confirm( |
262 | - this.translate.instant(title), | |
263 | - this.translate.instant(content), | |
375 | + title, | |
376 | + content, | |
264 | 377 | this.translate.instant('action.no'), |
265 | 378 | this.translate.instant('action.yes'), |
266 | 379 | true |
... | ... | @@ -276,6 +389,34 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
276 | 389 | ); |
277 | 390 | } |
278 | 391 | |
392 | + unassignDevicesFromCustomer($event: Event, devices: Array<DeviceInfo>) { | |
393 | + if ($event) { | |
394 | + $event.stopPropagation(); | |
395 | + } | |
396 | + this.dialogService.confirm( | |
397 | + this.translate.instant('device.unassign-devices-title', {count: devices.length}), | |
398 | + this.translate.instant('device.unassign-devices-text'), | |
399 | + this.translate.instant('action.no'), | |
400 | + this.translate.instant('action.yes'), | |
401 | + true | |
402 | + ).subscribe((res) => { | |
403 | + if (res) { | |
404 | + const tasks: Observable<any>[] = []; | |
405 | + devices.forEach( | |
406 | + (device) => { | |
407 | + tasks.push(this.deviceService.unassignDeviceFromCustomer(device.id.id)); | |
408 | + } | |
409 | + ); | |
410 | + forkJoin(tasks).subscribe( | |
411 | + () => { | |
412 | + this.config.table.updateData(); | |
413 | + } | |
414 | + ); | |
415 | + } | |
416 | + } | |
417 | + ); | |
418 | + } | |
419 | + | |
279 | 420 | manageCredentials($event: Event, device: Device) { |
280 | 421 | if ($event) { |
281 | 422 | $event.stopPropagation(); |
... | ... | @@ -297,7 +438,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
297 | 438 | this.makePublic(action.event, action.entity); |
298 | 439 | return true; |
299 | 440 | case 'assignToCustomer': |
300 | - this.assignToCustomer(action.event, action.entity); | |
441 | + this.assignToCustomer(action.event, [action.entity.id]); | |
301 | 442 | return true; |
302 | 443 | case 'unassignFromCustomer': |
303 | 444 | this.unassignFromCustomer(action.event, action.entity); | ... | ... |
... | ... | @@ -20,7 +20,7 @@ import { AdminModule } from './admin/admin.module'; |
20 | 20 | import { HomeLinksModule } from './home-links/home-links.module'; |
21 | 21 | import { ProfileModule } from './profile/profile.module'; |
22 | 22 | import { TenantModule } from '@modules/home/pages/tenant/tenant.module'; |
23 | -// import { CustomerModule } from '@modules/home/pages/customer/customer.module'; | |
23 | +import { CustomerModule } from '@modules/home/pages/customer/customer.module'; | |
24 | 24 | // import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module'; |
25 | 25 | import { UserModule } from '@modules/home/pages/user/user.module'; |
26 | 26 | import {DeviceModule} from '@modules/home/pages/device/device.module'; |
... | ... | @@ -32,7 +32,7 @@ import {DeviceModule} from '@modules/home/pages/device/device.module'; |
32 | 32 | ProfileModule, |
33 | 33 | TenantModule, |
34 | 34 | DeviceModule, |
35 | -// CustomerModule, | |
35 | + CustomerModule, | |
36 | 36 | // AuditLogModule, |
37 | 37 | UserModule |
38 | 38 | ] | ... | ... |
... | ... | @@ -46,7 +46,7 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten |
46 | 46 | private datePipe: DatePipe, |
47 | 47 | private router: Router) { |
48 | 48 | |
49 | - this.config.entityType = EntityType.CUSTOMER; | |
49 | + this.config.entityType = EntityType.TENANT; | |
50 | 50 | this.config.entityComponent = TenantComponent; |
51 | 51 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT); |
52 | 52 | this.config.entityResources = entityTypeResources.get(EntityType.TENANT); | ... | ... |
... | ... | @@ -145,8 +145,7 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User> |
145 | 145 | name: this.authority === Authority.TENANT_ADMIN ? |
146 | 146 | this.translate.instant('user.login-as-tenant-admin') : |
147 | 147 | this.translate.instant('user.login-as-customer-user'), |
148 | - icon: 'mdi:login', | |
149 | - isMdiIcon: true, | |
148 | + mdiIcon: 'mdi:login', | |
150 | 149 | isEnabled: () => true, |
151 | 150 | onAction: ($event, entity) => this.loginAsUser($event, entity) |
152 | 151 | } | ... | ... |
... | ... | @@ -51,8 +51,8 @@ export interface CellActionDescriptor<T extends BaseData<HasId>> { |
51 | 51 | name: string; |
52 | 52 | nameFunction?: (entity: T) => string; |
53 | 53 | icon?: string; |
54 | - isMdiIcon?: boolean; | |
55 | - color?: string; | |
54 | + mdiIcon?: string; | |
55 | + style?: any; | |
56 | 56 | isEnabled: (entity: T) => boolean; |
57 | 57 | onAction: ($event: MouseEvent, entity: T) => void; |
58 | 58 | } | ... | ... |
... | ... | @@ -52,20 +52,30 @@ |
52 | 52 | </button> |
53 | 53 | <ng-template #addActions> |
54 | 54 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
55 | - matTooltip="{{ translations.add | translate }}" | |
56 | - matTooltipPosition="above" | |
57 | - [matMenuTriggerFor]="addActionsMenu"> | |
58 | - <mat-icon>add</mat-icon> | |
55 | + *ngIf="this.entitiesTableConfig.addActionDescriptors.length === 1; else addActionsMenu" | |
56 | + [fxShow]="this.entitiesTableConfig.addActionDescriptors[0].isEnabled()" | |
57 | + (click)="this.entitiesTableConfig.addActionDescriptors[0].onAction($event)" | |
58 | + matTooltip="{{ this.entitiesTableConfig.addActionDescriptors[0].name }}" | |
59 | + matTooltipPosition="above"> | |
60 | + <mat-icon>{{this.entitiesTableConfig.addActionDescriptors[0].icon}}</mat-icon> | |
59 | 61 | </button> |
60 | - <mat-menu #addActionsMenu="matMenu" xPosition="before"> | |
61 | - <button mat-menu-item *ngFor="let actionDescriptor of this.entitiesTableConfig.addActionDescriptors" | |
62 | - [disabled]="isLoading$ | async" | |
63 | - [fxShow]="actionDescriptor.isEnabled()" | |
64 | - (click)="actionDescriptor.onAction($event)"> | |
65 | - <mat-icon>{{actionDescriptor.icon}}</mat-icon> | |
66 | - <span>{{ actionDescriptor.name }}</span> | |
62 | + <ng-template #addActionsMenu> | |
63 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | |
64 | + matTooltip="{{ translations.add | translate }}" | |
65 | + matTooltipPosition="above" | |
66 | + [matMenuTriggerFor]="addActionsMenu"> | |
67 | + <mat-icon>add</mat-icon> | |
67 | 68 | </button> |
68 | - </mat-menu> | |
69 | + <mat-menu #addActionsMenu="matMenu" xPosition="before"> | |
70 | + <button mat-menu-item *ngFor="let actionDescriptor of this.entitiesTableConfig.addActionDescriptors" | |
71 | + [disabled]="isLoading$ | async" | |
72 | + [fxShow]="actionDescriptor.isEnabled()" | |
73 | + (click)="actionDescriptor.onAction($event)"> | |
74 | + <mat-icon>{{actionDescriptor.icon}}</mat-icon> | |
75 | + <span>{{ actionDescriptor.name }}</span> | |
76 | + </button> | |
77 | + </mat-menu> | |
78 | + </ng-template> | |
69 | 79 | </ng-template> |
70 | 80 | </div> |
71 | 81 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
... | ... | @@ -140,9 +150,11 @@ |
140 | 150 | </mat-checkbox> |
141 | 151 | </mat-cell> |
142 | 152 | </ng-container> |
143 | - <ng-container [matColumnDef]="column.key" *ngFor="let column of columns"> | |
153 | + <ng-container [matColumnDef]="column.key" *ngFor="let column of columns; trackBy: trackByColumnKey; let col = index"> | |
144 | 154 | <mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell> |
145 | - <mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell> | |
155 | + <mat-cell *matCellDef="let entity; let row = index" | |
156 | + [innerHTML]="cellContent(entity, column, row, col)" | |
157 | + [ngStyle]="cellStyle(entity, column, row, col)"></mat-cell> | |
146 | 158 | </ng-container> |
147 | 159 | <ng-container matColumnDef="actions" stickyEnd> |
148 | 160 | <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }"> |
... | ... | @@ -155,10 +167,8 @@ |
155 | 167 | matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}" |
156 | 168 | matTooltipPosition="above" |
157 | 169 | (click)="actionDescriptor.onAction($event, entity)"> |
158 | - <mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"> | |
170 | + <mat-icon [svgIcon]="actionDescriptor.mdiIcon" [ngStyle]="actionDescriptor.style"> | |
159 | 171 | {{actionDescriptor.icon}}</mat-icon> |
160 | - <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}" | |
161 | - [svgIcon]="actionDescriptor.icon"></mat-icon> | |
162 | 172 | </button> |
163 | 173 | </div> |
164 | 174 | <div fxHide fxShow.lt-lg> |
... | ... | @@ -172,10 +182,8 @@ |
172 | 182 | [disabled]="isLoading$ | async" |
173 | 183 | [fxShow]="actionDescriptor.isEnabled(entity)" |
174 | 184 | (click)="actionDescriptor.onAction($event, entity)"> |
175 | - <mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"> | |
185 | + <mat-icon [svgIcon]="actionDescriptor.mdiIcon" [ngStyle]="actionDescriptor.style"> | |
176 | 186 | {{actionDescriptor.icon}}</mat-icon> |
177 | - <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}" | |
178 | - [svgIcon]="actionDescriptor.icon"></mat-icon> | |
179 | 187 | <span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span> |
180 | 188 | </button> |
181 | 189 | </mat-menu> | ... | ... |
... | ... | @@ -21,7 +21,8 @@ import { |
21 | 21 | Input, |
22 | 22 | OnInit, |
23 | 23 | Type, |
24 | - ViewChild | |
24 | + ViewChild, | |
25 | + ChangeDetectionStrategy | |
25 | 26 | } from '@angular/core'; |
26 | 27 | import { PageComponent } from '@shared/components/page.component'; |
27 | 28 | import { Store } from '@ngrx/store'; |
... | ... | @@ -51,13 +52,14 @@ import { |
51 | 52 | EntityAction |
52 | 53 | } from '@shared/components/entity/entity-component.models'; |
53 | 54 | import { Timewindow } from '@shared/models/time/time.models'; |
54 | -import { DomSanitizer } from '@angular/platform-browser'; | |
55 | +import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; | |
55 | 56 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
56 | 57 | |
57 | 58 | @Component({ |
58 | 59 | selector: 'tb-entities-table', |
59 | 60 | templateUrl: './entities-table.component.html', |
60 | - styleUrls: ['./entities-table.component.scss'] | |
61 | + styleUrls: ['./entities-table.component.scss'], | |
62 | + changeDetection: ChangeDetectionStrategy.OnPush | |
61 | 63 | }) |
62 | 64 | export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit { |
63 | 65 | |
... | ... | @@ -73,6 +75,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
73 | 75 | columns: Array<EntityTableColumn<BaseData<HasId>>>; |
74 | 76 | displayedColumns: string[] = []; |
75 | 77 | |
78 | + cellContentCache: Array<SafeHtml> = []; | |
79 | + | |
80 | + cellStyleCache: Array<any> = []; | |
81 | + | |
76 | 82 | selectionEnabled; |
77 | 83 | |
78 | 84 | pageLink: PageLink; |
... | ... | @@ -139,7 +145,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
139 | 145 | |
140 | 146 | this.columns = [...this.entitiesTableConfig.columns]; |
141 | 147 | |
142 | - this.selectionEnabled = this.entitiesTableConfig.selectionEnabled; | |
148 | + const enabledGroupActionDescriptors = | |
149 | + this.groupActionDescriptors.filter((descriptor) => descriptor.isEnabled); | |
150 | + | |
151 | + this.selectionEnabled = this.entitiesTableConfig.selectionEnabled && enabledGroupActionDescriptors.length; | |
143 | 152 | |
144 | 153 | if (this.selectionEnabled) { |
145 | 154 | this.displayedColumns.push('select'); |
... | ... | @@ -163,7 +172,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
163 | 172 | this.pageLink = new PageLink(10, 0, null, sortOrder); |
164 | 173 | } |
165 | 174 | this.dataSource = new EntitiesDataSource<BaseData<HasId>>( |
166 | - this.entitiesTableConfig.entitiesFetchFunction | |
175 | + this.entitiesTableConfig.entitiesFetchFunction, | |
176 | + () => { | |
177 | + this.dataLoaded(); | |
178 | + } | |
167 | 179 | ); |
168 | 180 | if (this.entitiesTableConfig.onLoadAction) { |
169 | 181 | this.entitiesTableConfig.onLoadAction(this.route); |
... | ... | @@ -221,6 +233,11 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
221 | 233 | this.dataSource.loadEntities(this.pageLink); |
222 | 234 | } |
223 | 235 | |
236 | + private dataLoaded() { | |
237 | + this.cellContentCache.length = 0; | |
238 | + this.cellStyleCache.length = 0; | |
239 | + } | |
240 | + | |
224 | 241 | onRowClick($event: Event, entity) { |
225 | 242 | if ($event) { |
226 | 243 | $event.stopPropagation(); |
... | ... | @@ -347,12 +364,28 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
347 | 364 | } |
348 | 365 | } |
349 | 366 | |
350 | - cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) { | |
351 | - return this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); | |
367 | + cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>, row: number, col: number) { | |
368 | + const index = row * this.columns.length + col; | |
369 | + let res = this.cellContentCache[index]; | |
370 | + if (!res) { | |
371 | + res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); | |
372 | + this.cellContentCache[index] = res; | |
373 | + } | |
374 | + return res; | |
375 | + } | |
376 | + | |
377 | + cellStyle(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>, row: number, col: number) { | |
378 | + const index = row * this.columns.length + col; | |
379 | + let res = this.cellStyleCache[index]; | |
380 | + if (!res) { | |
381 | + res = {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}}; | |
382 | + this.cellStyleCache[index] = res; | |
383 | + } | |
384 | + return res; | |
352 | 385 | } |
353 | 386 | |
354 | - cellStyle(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) { | |
355 | - return {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}}; | |
387 | + trackByColumnKey(index, column: EntityTableColumn<BaseData<HasId>>) { | |
388 | + return column.key; | |
356 | 389 | } |
357 | 390 | |
358 | 391 | } | ... | ... |
... | ... | @@ -105,7 +105,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit |
105 | 105 | } |
106 | 106 | |
107 | 107 | ngOnInit() { |
108 | - this.filteredEntities = this.selectEntityFormGroup.get('dashboard').valueChanges | |
108 | + this.filteredEntities = this.selectEntityFormGroup.get('entity').valueChanges | |
109 | 109 | .pipe( |
110 | 110 | tap(value => { |
111 | 111 | let modelValue; | ... | ... |
... | ... | @@ -15,6 +15,7 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { |
18 | + ChangeDetectionStrategy, | |
18 | 19 | Component, |
19 | 20 | ComponentFactoryResolver, |
20 | 21 | EventEmitter, |
... | ... | @@ -44,7 +45,8 @@ import { Subscription } from 'rxjs'; |
44 | 45 | @Component({ |
45 | 46 | selector: 'tb-entity-details-panel', |
46 | 47 | templateUrl: './entity-details-panel.component.html', |
47 | - styleUrls: ['./entity-details-panel.component.scss'] | |
48 | + styleUrls: ['./entity-details-panel.component.scss'], | |
49 | + changeDetection: ChangeDetectionStrategy.OnPush | |
48 | 50 | }) |
49 | 51 | export class EntityDetailsPanelComponent extends PageComponent implements OnInit, OnDestroy { |
50 | 52 | ... | ... |
... | ... | @@ -36,7 +36,8 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = |
36 | 36 | |
37 | 37 | public currentEntity: T = null; |
38 | 38 | |
39 | - constructor(private fetchFunction: EntitiesFetchFunction<T, P>) {} | |
39 | + constructor(private fetchFunction: EntitiesFetchFunction<T, P>, | |
40 | + private dataLoadedFunction: () => void) {} | |
40 | 41 | |
41 | 42 | connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { |
42 | 43 | return this.entitiesSubject.asObservable(); |
... | ... | @@ -59,6 +60,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = |
59 | 60 | this.entitiesSubject.next(pageData.data); |
60 | 61 | this.pageDataSubject.next(pageData); |
61 | 62 | result.next(pageData); |
63 | + this.dataLoadedFunction(); | |
62 | 64 | } |
63 | 65 | ); |
64 | 66 | return result; | ... | ... |
... | ... | @@ -395,6 +395,7 @@ |
395 | 395 | "manage-assets": "Manage assets", |
396 | 396 | "manage-devices": "Manage devices", |
397 | 397 | "manage-dashboards": "Manage dashboards", |
398 | + "created-time": "Created time", | |
398 | 399 | "title": "Title", |
399 | 400 | "title-required": "Title is required.", |
400 | 401 | "description": "Description", | ... | ... |