Commit cf4e1a4bd93995fd4cae49c81fb86b9e1cbd1381
1 parent
a73e6be5
UI: Inline tenant profile create/edit.
Showing
10 changed files
with
277 additions
and
19 deletions
... | ... | @@ -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, '...')}) | 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"> | ... | ... |
... | ... | @@ -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} }", | ... | ... |