Showing
9 changed files
with
193 additions
and
14 deletions
... | ... | @@ -242,7 +242,9 @@ |
242 | 242 | </tb-edit-widget> |
243 | 243 | </tb-details-panel> |
244 | 244 | <tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex |
245 | - headerTitle="{{'dashboard.select-widget-title' | translate}}" | |
245 | + headerTitle="{{ | |
246 | + (!widgetsBundle?.title ? 'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: widgetsBundle | |
247 | + }}" | |
246 | 248 | headerHeightPx="120" |
247 | 249 | [isReadOnly]="true" |
248 | 250 | [isEdit]="false" |
... | ... | @@ -252,17 +254,17 @@ |
252 | 254 | <div class="header-pane" *ngIf="isAddingWidget"> |
253 | 255 | <div fxLayout="row"> |
254 | 256 | <!-- <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>--> |
255 | - <tb-widgets-bundle-select fxFlex | |
256 | - required | |
257 | - [selectFirstBundle]="false" | |
258 | - [(ngModel)]="widgetsBundle" | |
259 | - (ngModelChange)="widgetsBundle = $event"> | |
260 | - </tb-widgets-bundle-select> | |
257 | + <tb-widgets-bundle-search fxFlex | |
258 | + [(ngModel)]="searchBundle" | |
259 | + [placeholder]="!widgetsBundle?.title ? 'Search widgets bundle' : 'Search widget'" | |
260 | + (ngModelChange)="searchBundle = $event"> | |
261 | + </tb-widgets-bundle-search> | |
261 | 262 | </div> |
262 | 263 | </div> |
263 | 264 | <tb-dashboard-widget-select *ngIf="isAddingWidget" |
264 | 265 | [aliasController]="dashboardCtx.aliasController" |
265 | 266 | [widgetsBundle]="widgetsBundle" |
267 | + [searchBundle]="searchBundle" | |
266 | 268 | (widgetsBundleSelected)="widgetBundleSelected($event)" |
267 | 269 | (widgetSelected)="addWidgetFromType($event)"> |
268 | 270 | </tb-dashboard-widget-select> | ... | ... |
... | ... | @@ -146,6 +146,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
146 | 146 | isAddingWidget = false; |
147 | 147 | isAddingWidgetClosed = true; |
148 | 148 | widgetsBundle: WidgetsBundle = null; |
149 | + searchBundle = ''; | |
149 | 150 | |
150 | 151 | isToolbarOpened = false; |
151 | 152 | isToolbarOpenedAnimate = false; | ... | ... |
... | ... | @@ -21,8 +21,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; |
21 | 21 | import { WidgetService } from '@core/http/widget.service'; |
22 | 22 | import { Widget } from '@shared/models/widget.models'; |
23 | 23 | import { toWidgetInfo } from '@home/models/widget-component.models'; |
24 | -import { share } from 'rxjs/operators'; | |
25 | -import { Observable } from 'rxjs'; | |
24 | +import { distinctUntilChanged, map, mergeMap, publishReplay, refCount, share } from 'rxjs/operators'; | |
25 | +import { BehaviorSubject, Observable, of } from 'rxjs'; | |
26 | 26 | import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; |
27 | 27 | import { isDefinedAndNotNull } from '@core/utils'; |
28 | 28 | |
... | ... | @@ -33,12 +33,19 @@ import { isDefinedAndNotNull } from '@core/utils'; |
33 | 33 | }) |
34 | 34 | export class DashboardWidgetSelectComponent implements OnInit, OnChanges { |
35 | 35 | |
36 | + private search$ = new BehaviorSubject<string>(''); | |
37 | + | |
36 | 38 | @Input() |
37 | 39 | widgetsBundle: WidgetsBundle; |
38 | 40 | |
39 | 41 | @Input() |
40 | 42 | aliasController: IAliasController; |
41 | 43 | |
44 | + @Input() | |
45 | + set searchBundle(search: string) { | |
46 | + this.search$.next(search); | |
47 | + } | |
48 | + | |
42 | 49 | @Output() |
43 | 50 | widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>(); |
44 | 51 | |
... | ... | @@ -49,13 +56,20 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { |
49 | 56 | |
50 | 57 | widgetsBundles$: Observable<Array<WidgetsBundle>>; |
51 | 58 | |
59 | + widgets$: Observable<Array<Widget>>; | |
60 | + | |
52 | 61 | constructor(private widgetsService: WidgetService, |
53 | 62 | private sanitizer: DomSanitizer) { |
54 | 63 | } |
55 | 64 | |
56 | 65 | ngOnInit(): void { |
57 | - this.widgetsBundles$ = this.widgetsService.getAllWidgetsBundles().pipe( | |
58 | - share() | |
66 | + this.widgetsBundles$ = this.search$.asObservable().pipe( | |
67 | + distinctUntilChanged(), | |
68 | + mergeMap(search => this.fetchWidgetBundle(search)) | |
69 | + ); | |
70 | + this.widgets$ = this.search$.asObservable().pipe( | |
71 | + distinctUntilChanged(), | |
72 | + mergeMap(search => this.fetchWidget(search)) | |
59 | 73 | ); |
60 | 74 | } |
61 | 75 | |
... | ... | @@ -121,6 +135,7 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { |
121 | 135 | selectBundle($event: Event, bundle: WidgetsBundle) { |
122 | 136 | $event.preventDefault(); |
123 | 137 | this.widgetsBundle = bundle; |
138 | + this.search$.next(''); | |
124 | 139 | this.widgetsBundleSelected.emit(bundle); |
125 | 140 | } |
126 | 141 | |
... | ... | @@ -131,4 +146,32 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { |
131 | 146 | return '/assets/widget-preview-empty.svg'; |
132 | 147 | } |
133 | 148 | |
149 | + private getWidgetsBundle(): Observable<Array<WidgetsBundle>> { | |
150 | + return this.widgetsService.getAllWidgetsBundles().pipe( | |
151 | + publishReplay(1), | |
152 | + refCount() | |
153 | + ); | |
154 | + } | |
155 | + | |
156 | + private fetchWidgetBundle(search: string): Observable<Array<WidgetsBundle>> { | |
157 | + return this.getWidgetsBundle().pipe( | |
158 | + map(bundles => search ? bundles.filter( | |
159 | + bundle => ( | |
160 | + bundle.title?.toLowerCase().includes(search.toLowerCase()) || | |
161 | + bundle.description?.toLowerCase().includes(search.toLowerCase()) | |
162 | + )) : bundles | |
163 | + ) | |
164 | + ); | |
165 | + } | |
166 | + | |
167 | + private fetchWidget(search: string): Observable<Array<Widget>> { | |
168 | + return of(this.widgets).pipe( | |
169 | + map(widgets => search ? widgets.filter( | |
170 | + widget => ( | |
171 | + widget.title?.toLowerCase().includes(search.toLowerCase()) || | |
172 | + widget.description?.toLowerCase().includes(search.toLowerCase()) | |
173 | + )) : widgets | |
174 | + ) | |
175 | + ); | |
176 | + } | |
134 | 177 | } | ... | ... |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | </span> |
32 | 32 | </div> |
33 | 33 | <ng-content select=".details-buttons"></ng-content> |
34 | - <button mat-button mat-icon-button (click)="onCloseDetails()"> | |
34 | + <button mat-button cdkFocusInitial mat-icon-button (click)="onCloseDetails()"> | |
35 | 35 | <mat-icon class="material-icons">close</mat-icon> |
36 | 36 | </button> |
37 | 37 | </div> | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2021 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="input-wrapper" fxLayoutAlign="start center" fxLayoutGap="8px"> | |
19 | + <mat-icon>search</mat-icon> | |
20 | + <input type="text" [(ngModel)]="searchText" (ngModelChange)="updateSearchText()" [placeholder]=placeholder> | |
21 | + <button mat-button *ngIf="searchText" mat-icon-button (click)="clear()"> | |
22 | + <mat-icon>close</mat-icon> | |
23 | + </button> | |
24 | +</div> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2021 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 | +.input-wrapper { | |
17 | + background: hsla(0, 0%, 100%, .2); | |
18 | + padding: 5px 0 5px 10px; | |
19 | + height: 40px; | |
20 | + | |
21 | + input { | |
22 | + width: 100%; | |
23 | + height: 100%; | |
24 | + padding: 0; | |
25 | + font-size: 20px; | |
26 | + outline: none; | |
27 | + border: none; | |
28 | + background-color: transparent; | |
29 | + color: #fff; | |
30 | + | |
31 | + &::placeholder { | |
32 | + color: #fff; | |
33 | + opacity: .8; | |
34 | + line-height: 26px; | |
35 | + } | |
36 | + } | |
37 | +} | |
38 | + | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2021 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, ElementRef, forwardRef, Input, ViewChild, ViewEncapsulation } from '@angular/core'; | |
18 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
19 | + | |
20 | +@Component({ | |
21 | + selector: 'tb-widgets-bundle-search', | |
22 | + templateUrl: './widgets-bundle-search.component.html', | |
23 | + styleUrls: ['./widgets-bundle-search.component.scss'], | |
24 | + providers: [{ | |
25 | + provide: NG_VALUE_ACCESSOR, | |
26 | + useExisting: forwardRef(() => WidgetsBundleSearchComponent), | |
27 | + multi: true | |
28 | + }], | |
29 | + encapsulation: ViewEncapsulation.None | |
30 | +}) | |
31 | +export class WidgetsBundleSearchComponent implements ControlValueAccessor { | |
32 | + | |
33 | + searchText: string; | |
34 | + | |
35 | + @Input() placeholder: string; | |
36 | + | |
37 | + @ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>; | |
38 | + | |
39 | + private propagateChange = (v: any) => { }; | |
40 | + | |
41 | + constructor() { | |
42 | + } | |
43 | + | |
44 | + registerOnChange(fn: any): void { | |
45 | + this.propagateChange = fn; | |
46 | + } | |
47 | + | |
48 | + registerOnTouched(fn: any): void { | |
49 | + } | |
50 | + | |
51 | + writeValue(value: string | null): void { | |
52 | + this.searchText = value; | |
53 | + } | |
54 | + | |
55 | + updateSearchText(): void { | |
56 | + this.updateView(); | |
57 | + } | |
58 | + | |
59 | + private updateView() { | |
60 | + this.propagateChange(this.searchText); | |
61 | + } | |
62 | + | |
63 | + clear(): void { | |
64 | + this.searchText = ''; | |
65 | + this.updateView(); | |
66 | + } | |
67 | +} | ... | ... |
... | ... | @@ -138,6 +138,7 @@ import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list |
138 | 138 | import { ContactComponent } from '@shared/components/contact.component'; |
139 | 139 | import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; |
140 | 140 | import { FileSizePipe } from '@shared/pipe/file-size.pipe'; |
141 | +import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; | |
141 | 142 | |
142 | 143 | @NgModule({ |
143 | 144 | providers: [ |
... | ... | @@ -227,7 +228,8 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; |
227 | 228 | JsonObjectEditDialogComponent, |
228 | 229 | HistorySelectorComponent, |
229 | 230 | EntityGatewaySelectComponent, |
230 | - ContactComponent | |
231 | + ContactComponent, | |
232 | + WidgetsBundleSearchComponent | |
231 | 233 | ], |
232 | 234 | imports: [ |
233 | 235 | CommonModule, |
... | ... | @@ -395,7 +397,8 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; |
395 | 397 | JsonObjectEditDialogComponent, |
396 | 398 | HistorySelectorComponent, |
397 | 399 | EntityGatewaySelectComponent, |
398 | - ContactComponent | |
400 | + ContactComponent, | |
401 | + WidgetsBundleSearchComponent | |
399 | 402 | ] |
400 | 403 | }) |
401 | 404 | export class SharedModule { } | ... | ... |
... | ... | @@ -646,6 +646,7 @@ |
646 | 646 | "add-widget": "Add new widget", |
647 | 647 | "title": "Title", |
648 | 648 | "select-widget-title": "Select widget", |
649 | + "select-widget-value": "{{title}}: select widget", | |
649 | 650 | "select-widget-subtitle": "List of available widget types", |
650 | 651 | "delete": "Delete dashboard", |
651 | 652 | "title-required": "Title is required.", | ... | ... |