Commit 6c5f840de8b4d73f1c0b20819f5600a3612287fd

Authored by Vladyslav_Prykhodko
1 parent 530c077b

UI: Added search widgets bundle

... ... @@ -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.",
... ...