Browse Source

UI: added search to rule chain selector

pull/9880/head
rusikv 3 years ago
parent
commit
98086153dd
  1. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  2. 42
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html
  3. 18
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.scss
  4. 143
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.ts
  5. 20
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html
  6. 27
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss
  7. 93
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts
  8. 3
      ui-ngx/src/app/shared/shared.module.ts

2
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -34,7 +34,7 @@
*ngIf="!isImport"
[ruleChainType]="ruleChainType"
[disabled]="isDirtyValue"
[(ngModel)]="ruleChain.id.id"
[(ngModel)]="ruleChain"
(ngModelChange)="currentRuleChainIdChanged(ruleChain.id?.id)">
</tb-rule-chain-select>
</mat-toolbar>

42
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.html

@ -0,0 +1,42 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="selectRuleChainGroup" class="mat-block">
<input matInput type="text" placeholder="{{ 'rulechain.search' | translate }}"
#ruleChainInput
formControlName="ruleChainInput"
(focusin)="onFocus()"
[matAutocomplete]="ruleChainAutocomplete">
<mat-icon matSuffix>search</mat-icon>
<mat-autocomplete class="tb-autocomplete"
#ruleChainAutocomplete="matAutocomplete"
(optionSelected)="selected($event)">
<mat-option *ngFor="let ruleChain of filteredRuleChains | async"
[fxHide]="ruleChainId === ruleChain.id.id"
[value]="ruleChain">
<span [innerHTML]="ruleChain.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredRuleChains | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<span>
{{ translate.get('rulechain.no-rulechains-matching', {entity: searchText}) | async }}
</span>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>

18
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.scss

@ -0,0 +1,18 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
}

143
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select-panel.component.ts

@ -0,0 +1,143 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component,
ElementRef,
Inject,
InjectionToken,
OnDestroy,
ViewChild
} from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, switchMap, takeUntil, startWith, share } from 'rxjs/operators';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { RuleChain, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { TranslateService } from '@ngx-translate/core';
export const RULE_CHAIN_SELECT_PANEL_DATA = new InjectionToken<any>('RuleChainSelectPanelData');
export interface RuleChainSelectPanelData {
ruleChainId: string | null;
ruleChainType: RuleChainType;
}
@Component({
selector: 'tb-rule-chain-select-panel',
templateUrl: './rule-chain-select-panel.component.html',
styleUrls: ['./rule-chain-select-panel.component.scss']
})
export class RuleChainSelectPanelComponent implements AfterViewInit, OnDestroy {
ruleChainId: string;
ruleChainType: RuleChainType;
selectRuleChainGroup: FormGroup;
@ViewChild('ruleChainInput', {static: true}) userInput: ElementRef;
filteredRuleChains: Observable<Array<RuleChain>>;
searchText = '';
ruleChainSelected = false;
result?: RuleChain;
private dirty = false;
private destroy$ = new Subject<void>();
constructor(@Inject(RULE_CHAIN_SELECT_PANEL_DATA) public data: RuleChainSelectPanelData,
public overlayRef: OverlayRef,
public translate: TranslateService,
private fb: FormBuilder,
private ruleChainService: RuleChainService) {
this.ruleChainId = data.ruleChainId;
this.ruleChainType = data.ruleChainType;
this.selectRuleChainGroup = this.fb.group({
ruleChainInput: ['', {nonNullable: true}]
});
this.filteredRuleChains = this.selectRuleChainGroup.get('ruleChainInput').valueChanges
.pipe(
debounceTime(150),
startWith(''),
distinctUntilChanged((a: string, b: string) => a.trim() === b.trim()),
switchMap(name => this.fetchRuleChains(name)),
share(),
takeUntil(this.destroy$)
);
}
ngAfterViewInit() {
setTimeout(() => {
this.userInput.nativeElement.focus();
}, 0);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
selected(event: MatAutocompleteSelectedEvent): void {
this.clear();
this.ruleChainSelected = true;
if (event.option.value?.id) {
this.result = event.option.value;
}
this.overlayRef.dispose();
}
fetchRuleChains(searchText?: string): Observable<Array<RuleChain>> {
this.searchText = searchText;
const pageLink = new PageLink(50, 0, searchText, {
property: 'name',
direction: Direction.ASC
});
return this.getRuleChains(pageLink)
.pipe(
catchError(() => of(emptyPageData<RuleChain>())),
map(pageData => pageData.data)
);
}
onFocus(): void {
if (!this.dirty) {
this.selectRuleChainGroup.get('ruleChainInput').updateValueAndValidity({onlySelf: true});
this.dirty = true;
}
}
clear() {
this.selectRuleChainGroup.get('ruleChainInput').patchValue('', {emitEvent: true});
setTimeout(() => {
this.userInput.nativeElement.blur();
this.userInput.nativeElement.focus();
}, 0);
}
private getRuleChains(pageLink: PageLink): Observable<PageData<RuleChain>> {
return this.ruleChainService.getRuleChains(pageLink, this.ruleChainType, {ignoreLoading: true});
}
}

20
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html

@ -15,17 +15,13 @@
limitations under the License.
-->
<mat-form-field class="tb-rule-select" subscriptSizing="dynamic">
<mat-form-field class="tb-rule-select" subscriptSizing="dynamic" appearance="fill"
[ngClass]="{'cursor-pointer': !disabled}"
(click)="openRuleChainSelectPanel($event)">
<mat-icon matPrefix>settings_ethernet</mat-icon>
<mat-select fxFlex
class="tb-rule-chain-select"
hideSingleSelectionIndicator="false"
[required]="required"
[disabled]="disabled"
[(ngModel)]="ruleChainId"
(ngModelChange)="ruleChainIdChanged()">
<mat-option *ngFor="let ruleChain of ruleChains$ | async" [value]="ruleChain.id.id">
{{ruleChain.name}}
</mat-option>
</mat-select>
<input matInput readonly
[required]="required"
[disabled]="disabled"
[value]="ruleChain?.name">
<mat-icon [ngClass]="{'disabled': disabled || panelOpened}" matSuffix>arrow_drop_down</mat-icon>
</mat-form-field>

27
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss

@ -19,19 +19,22 @@
padding: 0;
.tb-rule-select {
width: 100%;
}
.tb-rule-chain-select {
display: flex;
min-height: 100%;
pointer-events: all;
}
}
:host ::ng-deep {
.mat-mdc-form-field.tb-rule-select .mdc-text-field {
.mat-mdc-form-field-infix {
min-height: 48px;
padding: 12px 0;
input {
color: #fff;
cursor: pointer;
&:disabled {
cursor: unset;
}
}
}
}
.cursor-pointer {
cursor: pointer;
}
.disabled {
opacity: 0.5;
}

93
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts

@ -14,20 +14,20 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Injector, Input, StaticProvider, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { map, share } from 'rxjs/operators';
import { PageData } from '@shared/models/page/page-data';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TooltipPosition } from '@angular/material/tooltip';
import { RuleChain, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Direction } from '@shared/models/page/sort-order';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import {
RULE_CHAIN_SELECT_PANEL_DATA, RuleChainSelectPanelComponent,
RuleChainSelectPanelData
} from '@shared/components/rule-chain/rule-chain-select-panel.component';
import { ComponentPortal } from '@angular/cdk/portal';
import { POSITION_MAP } from '@shared/models/overlay.models';
@Component({
selector: 'tb-rule-chain-select',
@ -39,7 +39,7 @@ import { Direction } from '@shared/models/page/sort-order';
multi: true
}]
})
export class RuleChainSelectComponent implements ControlValueAccessor, OnInit {
export class RuleChainSelectComponent implements ControlValueAccessor {
@Input()
tooltipPosition: TooltipPosition = 'above';
@ -55,25 +55,15 @@ export class RuleChainSelectComponent implements ControlValueAccessor, OnInit {
@Input()
ruleChainType: RuleChainType = RuleChainType.CORE;
ruleChains$: Observable<Array<RuleChain>>;
ruleChain: RuleChain | null;
ruleChainId: string | null;
panelOpened = false;
private propagateChange = (v: any) => { };
constructor(private ruleChainService: RuleChainService) {
}
ngOnInit() {
const pageLink = new PageLink(1024, 0, null, {
property: 'name',
direction: Direction.ASC
});
this.ruleChains$ = this.getRuleChains(pageLink).pipe(
map((pageData) => pageData.data),
share()
);
constructor(private ruleChainService: RuleChainService,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef) {
}
registerOnChange(fn: any): void {
@ -83,14 +73,13 @@ export class RuleChainSelectComponent implements ControlValueAccessor, OnInit {
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string | null): void {
writeValue(value: RuleChain): void {
if (isDefinedAndNotNull(value)) {
this.ruleChainId = value;
this.ruleChain = value;
}
}
@ -98,12 +87,54 @@ export class RuleChainSelectComponent implements ControlValueAccessor, OnInit {
this.updateView();
}
private updateView() {
this.propagateChange(this.ruleChainId);
openRuleChainSelectPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
if (!this.disabled) {
const target = $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
width: (target as HTMLElement).offsetWidth
});
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(target as HTMLElement)
.withPositions([POSITION_MAP.bottom]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: RULE_CHAIN_SELECT_PANEL_DATA,
useValue: {
ruleChainId: this.ruleChain.id?.id,
ruleChainType: this.ruleChainType
} as RuleChainSelectPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(RuleChainSelectPanelComponent,
this.viewContainerRef, injector));
this.panelOpened = true;
componentRef.onDestroy(() => {
this.panelOpened = false;
if (componentRef.instance.ruleChainSelected) {
this.ruleChain = componentRef.instance.result;
this.updateView();
}
});
}
}
private getRuleChains(pageLink: PageLink): Observable<PageData<RuleChain>> {
return this.ruleChainService.getRuleChains(pageLink, this.ruleChainType, {ignoreLoading: true});
private updateView() {
this.propagateChange(this.ruleChain);
}
}

3
ui-ngx/src/app/shared/shared.module.ts

@ -215,6 +215,7 @@ import { ImagesInUseDialogComponent } from '@shared/components/image/images-in-u
import { GalleryImageInputComponent } from '@shared/components/image/gallery-image-input.component';
import { MultipleGalleryImageInputComponent } from '@shared/components/image/multiple-gallery-image-input.component';
import { EmbedImageDialogComponent } from '@shared/components/image/embed-image-dialog.component';
import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -396,6 +397,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
StringAutocompleteComponent,
MaterialIconsComponent,
RuleChainSelectComponent,
RuleChainSelectPanelComponent,
TbIconComponent,
HintTooltipIconComponent,
ImportDialogComponent,
@ -646,6 +648,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
StringAutocompleteComponent,
MaterialIconsComponent,
RuleChainSelectComponent,
RuleChainSelectPanelComponent,
TbIconComponent,
HintTooltipIconComponent,
ImportDialogComponent,

Loading…
Cancel
Save