Commit cf4e1a4bd93995fd4cae49c81fb86b9e1cbd1381

Authored by Igor Kulikov
1 parent a73e6be5

UI: Inline tenant profile create/edit.

... ... @@ -86,6 +86,7 @@ import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.
86 86 import { FilterPredicateValueComponent } from './filter/filter-predicate-value.component';
87 87 import { TenantProfileAutocompleteComponent } from './profile/tenant-profile-autocomplete.component';
88 88 import { TenantProfileComponent } from './profile/tenant-profile.component';
  89 +import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component';
89 90
90 91 @NgModule({
91 92 declarations:
... ... @@ -154,7 +155,8 @@ import { TenantProfileComponent } from './profile/tenant-profile.component';
154 155 FilterUserInfoDialogComponent,
155 156 FilterPredicateValueComponent,
156 157 TenantProfileAutocompleteComponent,
157   - TenantProfileComponent
  158 + TenantProfileComponent,
  159 + TenantProfileDialogComponent
158 160 ],
159 161 imports: [
160 162 CommonModule,
... ... @@ -212,7 +214,8 @@ import { TenantProfileComponent } from './profile/tenant-profile.component';
212 214 FiltersEditComponent,
213 215 UserFilterDialogComponent,
214 216 TenantProfileAutocompleteComponent,
215   - TenantProfileComponent
  217 + TenantProfileComponent,
  218 + TenantProfileDialogComponent
216 219 ],
217 220 providers: [
218 221 WidgetComponentService,
... ...
... ... @@ -20,6 +20,8 @@
20 20 #tenantProfileInput
21 21 formControlName="tenantProfile"
22 22 [required]="required"
  23 + (keydown)="tenantProfileEnter($event)"
  24 + (keypress)="tenantProfileEnter($event)"
23 25 [matAutocomplete]="tenantProfileAutocomplete">
24 26 <button *ngIf="selectTenantProfileFormGroup.get('tenantProfile').value && !disabled"
25 27 type="button"
... ... @@ -27,6 +29,14 @@
27 29 (click)="clear()">
28 30 <mat-icon class="material-icons">close</mat-icon>
29 31 </button>
  32 + <button *ngIf="selectTenantProfileFormGroup.get('tenantProfile').value && !disabled"
  33 + type="button"
  34 + matSuffix mat-button mat-icon-button aria-label="Edit"
  35 + matTooltip="{{ 'tenant-profile.edit' | translate }}"
  36 + matTooltipPosition="above"
  37 + (click)="editTenantProfile($event)">
  38 + <mat-icon class="material-icons">edit</mat-icon>
  39 + </button>
30 40 <mat-autocomplete
31 41 class="tb-autocomplete"
32 42 #tenantProfileAutocomplete="matAutocomplete"
... ... @@ -34,10 +44,21 @@
34 44 <mat-option *ngFor="let tenantProfile of filteredTenantProfiles | async" [value]="tenantProfile">
35 45 <span [innerHTML]="tenantProfile.name | highlight:searchText"></span>
36 46 </mat-option>
37   - <mat-option *ngIf="!(filteredTenantProfiles | async)?.length" [value]="null">
38   - <span>
39   - {{ translate.get('tenant-profile.no-tenant-profiles-matching', {entity: searchText}) | async }}
40   - </span>
  47 + <mat-option *ngIf="!(filteredTenantProfiles | async)?.length" [value]="null" class="tb-not-found">
  48 + <div class="tb-not-found-content" (click)="$event.stopPropagation()">
  49 + <div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
  50 + <span translate>tenant-profile.no-tenant-profiles-found</span>
  51 + </div>
  52 + <ng-template #searchNotEmpty>
  53 + <span>
  54 + {{ translate.get('tenant-profile.no-tenant-profiles-matching',
  55 + {entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
  56 + </span>
  57 + </ng-template>
  58 + <span>
  59 + <a translate (click)="createTenantProfile($event, searchText)">tenant-profile.create-new-tenant-profile</a>
  60 + </span>
  61 + </div>
41 62 </mat-option>
42 63 </mat-autocomplete>
43 64 <mat-error *ngIf="selectTenantProfileFormGroup.get('tenantProfile').hasError('required')">
... ...
... ... @@ -14,7 +14,7 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
  17 +import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core';
18 18 import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
19 19 import { Observable } from 'rxjs';
20 20 import { PageLink } from '@shared/models/page/page-link';
... ... @@ -27,7 +27,12 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
27 27 import { TenantProfileId } from '@shared/models/id/tenant-profile-id';
28 28 import { EntityInfoData } from '@shared/models/entity.models';
29 29 import { TenantProfileService } from '@core/http/tenant-profile.service';
30   -import { entityIdEquals } from '../../../../shared/models/id/entity-id';
  30 +import { entityIdEquals } from '@shared/models/id/entity-id';
  31 +import { TruncatePipe } from '@shared//pipe/truncate.pipe';
  32 +import { ENTER } from '@angular/cdk/keycodes';
  33 +import { TenantProfile } from '@shared/models/tenant.model';
  34 +import { MatDialog } from '@angular/material/dialog';
  35 +import { TenantProfileDialogComponent, TenantProfileDialogData } from './tenant-profile-dialog.component';
31 36
32 37 @Component({
33 38 selector: 'tb-tenant-profile-autocomplete',
... ... @@ -60,6 +65,9 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
60 65 @Input()
61 66 disabled: boolean;
62 67
  68 + @Output()
  69 + tenantProfileUpdated = new EventEmitter<TenantProfileId>();
  70 +
63 71 @ViewChild('tenantProfileInput', {static: true}) tenantProfileInput: ElementRef;
64 72
65 73 filteredTenantProfiles: Observable<Array<EntityInfoData>>;
... ... @@ -70,8 +78,10 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
70 78
71 79 constructor(private store: Store<AppState>,
72 80 public translate: TranslateService,
  81 + public truncate: TruncatePipe,
73 82 private tenantProfileService: TenantProfileService,
74   - private fb: FormBuilder) {
  83 + private fb: FormBuilder,
  84 + private dialog: MatDialog) {
75 85 this.selectTenantProfileFormGroup = this.fb.group({
76 86 tenantProfile: [null]
77 87 });
... ... @@ -168,4 +178,67 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
168 178 }, 0);
169 179 }
170 180
  181 + textIsNotEmpty(text: string): boolean {
  182 + return (text && text.length > 0);
  183 + }
  184 +
  185 + tenantProfileEnter($event: KeyboardEvent) {
  186 + if ($event.keyCode === ENTER) {
  187 + $event.preventDefault();
  188 + if (!this.modelValue) {
  189 + this.createTenantProfile($event, this.searchText);
  190 + }
  191 + }
  192 + }
  193 +
  194 + createTenantProfile($event: Event, profileName: string) {
  195 + $event.preventDefault();
  196 + const tenantProfile: TenantProfile = {
  197 + id: null,
  198 + name: profileName
  199 + };
  200 + this.openTenantProfileDialog(tenantProfile, true);
  201 + }
  202 +
  203 + editTenantProfile($event: Event) {
  204 + $event.preventDefault();
  205 + this.tenantProfileService.getTenantProfile(this.modelValue.id).subscribe(
  206 + (tenantProfile) => {
  207 + this.openTenantProfileDialog(tenantProfile, false);
  208 + }
  209 + );
  210 + }
  211 +
  212 + openTenantProfileDialog(tenantProfile: TenantProfile, isAdd: boolean) {
  213 + this.dialog.open<TenantProfileDialogComponent, TenantProfileDialogData,
  214 + TenantProfile>(TenantProfileDialogComponent, {
  215 + disableClose: true,
  216 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  217 + data: {
  218 + isAdd,
  219 + tenantProfile
  220 + }
  221 + }).afterClosed().subscribe(
  222 + (savedTenantProfile) => {
  223 + if (!savedTenantProfile) {
  224 + setTimeout(() => {
  225 + this.tenantProfileInput.nativeElement.blur();
  226 + this.tenantProfileInput.nativeElement.focus();
  227 + }, 0);
  228 + } else {
  229 + this.tenantProfileService.getTenantProfileInfo(savedTenantProfile.id.id).subscribe(
  230 + (profile) => {
  231 + this.modelValue = new TenantProfileId(profile.id.id);
  232 + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: true});
  233 + if (isAdd) {
  234 + this.propagateChange(this.modelValue);
  235 + } else {
  236 + this.tenantProfileUpdated.next(savedTenantProfile.id);
  237 + }
  238 + }
  239 + );
  240 + }
  241 + }
  242 + );
  243 + }
171 244 }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 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 (ngSubmit)="save()" style="min-width: 600px;">
  19 + <mat-toolbar color="primary">
  20 + <h2>{{ (isAdd ? 'tenant-profile.add' : 'tenant-profile.edit' ) | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <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 + <tb-tenant-profile
  33 + #tenantProfileComponent
  34 + [standalone]="true"
  35 + [entity]="tenantProfile"
  36 + [isEdit]="true">
  37 + </tb-tenant-profile>
  38 + </div>
  39 + <div mat-dialog-actions fxLayoutAlign="end center">
  40 + <button mat-raised-button color="primary"
  41 + type="submit"
  42 + [disabled]="(isLoading$ | async) || tenantProfileComponent.entityForm?.invalid || !tenantProfileComponent.entityForm?.dirty">
  43 + {{ (isAdd ? 'action.add' : 'action.save') | translate }}
  44 + </button>
  45 + <button mat-button color="primary"
  46 + type="button"
  47 + cdkFocusInitial
  48 + [disabled]="(isLoading$ | async)"
  49 + (click)="cancel()">
  50 + {{ 'action.cancel' | translate }}
  51 + </button>
  52 + </div>
  53 +</form>
... ...
  1 +///
  2 +/// Copyright © 2016-2020 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 {
  18 + AfterViewInit,
  19 + Component,
  20 + ComponentFactoryResolver,
  21 + Inject,
  22 + Injector,
  23 + SkipSelf,
  24 + ViewChild
  25 +} from '@angular/core';
  26 +import { ErrorStateMatcher } from '@angular/material/core';
  27 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
  28 +import { Store } from '@ngrx/store';
  29 +import { AppState } from '@core/core.state';
  30 +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
  31 +import { DialogComponent } from '@shared/components/dialog.component';
  32 +import { Router } from '@angular/router';
  33 +import { TenantProfile } from '@shared/models/tenant.model';
  34 +import { TenantProfileComponent } from './tenant-profile.component';
  35 +import { TenantProfileService } from '@core/http/tenant-profile.service';
  36 +
  37 +export interface TenantProfileDialogData {
  38 + tenantProfile: TenantProfile;
  39 + isAdd: boolean;
  40 +}
  41 +
  42 +@Component({
  43 + selector: 'tb-tenant-profile-dialog',
  44 + templateUrl: './tenant-profile-dialog.component.html',
  45 + providers: [{provide: ErrorStateMatcher, useExisting: TenantProfileDialogComponent}],
  46 + styleUrls: []
  47 +})
  48 +export class TenantProfileDialogComponent extends
  49 + DialogComponent<TenantProfileDialogComponent, TenantProfile> implements ErrorStateMatcher, AfterViewInit {
  50 +
  51 + isAdd: boolean;
  52 + tenantProfile: TenantProfile;
  53 +
  54 + submitted = false;
  55 +
  56 + @ViewChild('tenantProfileComponent', {static: true}) tenantProfileComponent: TenantProfileComponent;
  57 +
  58 + constructor(protected store: Store<AppState>,
  59 + protected router: Router,
  60 + @Inject(MAT_DIALOG_DATA) public data: TenantProfileDialogData,
  61 + public dialogRef: MatDialogRef<TenantProfileDialogComponent, TenantProfile>,
  62 + private componentFactoryResolver: ComponentFactoryResolver,
  63 + private injector: Injector,
  64 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  65 + private tenantProfileService: TenantProfileService) {
  66 + super(store, router, dialogRef);
  67 + this.isAdd = this.data.isAdd;
  68 + this.tenantProfile = this.data.tenantProfile;
  69 + }
  70 +
  71 + ngAfterViewInit(): void {
  72 + if (this.isAdd) {
  73 + setTimeout(() => {
  74 + this.tenantProfileComponent.entityForm.markAsDirty();
  75 + }, 0);
  76 + }
  77 + }
  78 +
  79 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  80 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  81 + const customErrorState = !!(control && control.invalid && this.submitted);
  82 + return originalErrorState || customErrorState;
  83 + }
  84 +
  85 + cancel(): void {
  86 + this.dialogRef.close(null);
  87 + }
  88 +
  89 + save(): void {
  90 + this.submitted = true;
  91 + if (this.tenantProfileComponent.entityForm.valid) {
  92 + this.tenantProfile = {...this.tenantProfile, ...this.tenantProfileComponent.entityFormValue()};
  93 + this.tenantProfileService.saveTenantProfile(this.tenantProfile).subscribe(
  94 + (tenantProfile) => {
  95 + this.dialogRef.close(tenantProfile);
  96 + }
  97 + );
  98 + }
  99 + }
  100 +
  101 +}
... ...
... ... @@ -14,7 +14,7 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, Inject, Input } from '@angular/core';
  17 +import { Component, Inject, Input, Optional } from '@angular/core';
18 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
... ... @@ -36,8 +36,8 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
36 36
37 37 constructor(protected store: Store<AppState>,
38 38 protected translate: TranslateService,
39   - @Inject('entity') protected entityValue: TenantProfile,
40   - @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>,
  39 + @Optional() @Inject('entity') protected entityValue: TenantProfile,
  40 + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>,
41 41 protected fb: FormBuilder) {
42 42 super(store, fb, entityValue, entitiesTableConfigValue);
43 43 }
... ...
... ... @@ -52,7 +52,8 @@
52 52 <tb-tenant-profile-autocomplete
53 53 [selectDefaultProfile]="isAdd"
54 54 required
55   - formControlName="tenantProfileId">
  55 + formControlName="tenantProfileId"
  56 + (tenantProfileUpdated)="onTenantProfileUpdated()">
56 57 </tb-tenant-profile-autocomplete>
57 58 <div formGroupName="additionalInfo" fxLayout="column">
58 59 <mat-form-field class="mat-block">
... ...
... ... @@ -88,4 +88,7 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
88 88 }));
89 89 }
90 90
  91 + onTenantProfileUpdated() {
  92 + this.entitiesTableConfig.table.updateData(false);
  93 + }
91 94 }
... ...
... ... @@ -25,11 +25,11 @@ export interface TenantProfileData {
25 25
26 26 export interface TenantProfile extends BaseData<TenantProfileId> {
27 27 name: string;
28   - description: string;
29   - default: boolean;
30   - isolatedTbCore: boolean;
31   - isolatedTbRuleEngine: boolean;
32   - profileData: TenantProfileData;
  28 + description?: string;
  29 + default?: boolean;
  30 + isolatedTbCore?: boolean;
  31 + isolatedTbRuleEngine?: boolean;
  32 + profileData?: TenantProfileData;
33 33 }
34 34
35 35 export interface Tenant extends ContactBased<TenantId> {
... ...
... ... @@ -1680,6 +1680,7 @@
1680 1680 "tenant-profile": "Tenant profile",
1681 1681 "tenant-profiles": "Tenant profiles",
1682 1682 "add": "Add tenant profile",
  1683 + "edit": "Edit tenant profile",
1683 1684 "tenant-profile-details": "Tenant profile details",
1684 1685 "no-tenant-profiles-text": "No tenant profiles found",
1685 1686 "search": "Search tenant profiles",
... ... @@ -1699,7 +1700,9 @@
1699 1700 "delete-tenant-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 tenant profile} other {# tenant profiles} }?",
1700 1701 "delete-tenant-profiles-text": "Be careful, after the confirmation all selected tenant profiles will be removed and all related data will become unrecoverable.",
1701 1702 "set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' root?",
1702   - "set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified."
  1703 + "set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified.",
  1704 + "no-tenant-profiles-found": "No tenant profiles found.",
  1705 + "create-new-tenant-profile": "Create a new one!"
1703 1706 },
1704 1707 "timeinterval": {
1705 1708 "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",
... ...