UI: Implement navigation widget settings form

This commit is contained in:
Igor Kulikov 2022-04-28 17:57:32 +03:00
parent aebe4c5b20
commit 03ddc2c378
10 changed files with 365 additions and 9 deletions

View File

@ -19,8 +19,9 @@
"templateHtml": "<tb-navigation-cards-widget [ctx]=\"ctx\"></tb-navigation-cards-widget>",
"templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"filterType\": {\n \"title\": \"Filter type\",\n \"type\": \"string\",\n \"default\": \"all\"\n },\n \"filter\": {\n \"title\": \"Items\",\n \"type\": \"array\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"filterType\",\n \"type\": \"radios\",\n \"direction\": \"row\",\n \"titleMap\": [\n {\n \"value\": \"all\",\n \"name\": \"All items\"\n },\n {\n \"value\": \"include\",\n \"name\": \"Include items\"\n },\n {\n \"value\": \"exclude\",\n \"name\": \"Exclude items\"\n }\n ]\n },\n {\n \"key\": \"filter\",\n \"type\": \"rc-select\",\n \"condition\": \"model.filterType !== 'all'\",\n \"tags\": true,\n \"placeholder\": \"Enter urls to filter\",\n \"items\": [{\"value\": \"/devices\", \"label\": \"/devices\"}, {\"value\": \"/assets\", \"label\": \"/assets\"}, {\"value\": \"/deviceProfiles\", \"label\": \"/deviceProfiles\"}]\n }\n ]\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-navigation-cards-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
}
},
@ -37,10 +38,11 @@
"templateHtml": "<tb-navigation-card-widget [ctx]=\"ctx\"></tb-navigation-card-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"name\": {\n \"title\": \"Title\",\n \"type\": \"string\",\n \"default\": \"{i18n:device.devices}\"\n },\n \"icon\": {\n \"title\": \"icon\",\n \"type\": \"string\",\n \"default\": \"devices_other\"\n },\n \"path\": {\n \"title\": \"Navigation path\",\n \"type\": \"string\",\n \"default\": \"/devices\"\n }\n },\n \"required\": [\"name\", \"icon\", \"path\"]\n },\n \"form\": [\n \"name\",\n {\n \"key\": \"icon\",\n \"type\": \"icon\"\n },\n \"path\"\n ]\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-navigation-card-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
}
}
]
}
}

View File

@ -0,0 +1,32 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="navigationCardWidgetSettingsForm" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.navigation.title</mat-label>
<input required matInput formControlName="name">
</mat-form-field>
<tb-material-icon-select fxFlex
iconClearButton
required
formControlName="icon">
</tb-material-icon-select>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.navigation.navigation-path</mat-label>
<input required matInput formControlName="path">
</mat-form-field>
</section>

View File

@ -0,0 +1,56 @@
///
/// Copyright © 2016-2022 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 { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-navigation-card-widget-settings',
templateUrl: './navigation-card-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class NavigationCardWidgetSettingsComponent extends WidgetSettingsComponent {
navigationCardWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.navigationCardWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
name: '{i18n:device.devices}',
icon: 'devices_other',
path: '/devices'
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.navigationCardWidgetSettingsForm = this.fb.group({
name: [settings.name, [Validators.required]],
icon: [settings.icon, [Validators.required]],
path: [settings.path, [Validators.required]]
});
}
}

View File

@ -0,0 +1,56 @@
<!--
Copyright © 2016-2022 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.
-->
<section class="tb-widget-settings" [formGroup]="navigationCardsWidgetSettingsForm" fxLayout="column">
<section style="margin-bottom: 16px;">
<div class="mat-caption" style="margin: -8px 0 8px;" translate>widgets.navigation.filter-type</div>
<mat-radio-group formControlName="filterType" fxLayoutGap="16px">
<mat-radio-button [value]="'all'">{{ 'widgets.navigation.filter-type-all' | translate }}</mat-radio-button>
<mat-radio-button [value]="'include'">{{ 'widgets.navigation.filter-type-include' | translate }}</mat-radio-button>
<mat-radio-button [value]="'exclude'">{{ 'widgets.navigation.filter-type-exclude' | translate }}</mat-radio-button>
</mat-radio-group>
</section>
<mat-form-field [fxShow]="navigationCardsWidgetSettingsForm.get('filterType').value !== 'all'" fxFlex class="mat-block" floatLabel="always">
<mat-label translate>widgets.navigation.items</mat-label>
<mat-chip-list #filterItemsChipList>
<mat-chip *ngFor="let filterItem of navigationCardsWidgetSettingsForm.get('filter').value"
[removable]="true" (removed)="onFilterItemRemoved(filterItem)">
{{ filterItem }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{ 'widgets.navigation.enter-urls-to-filter' | translate }}"
style="max-width: 200px;"
#filterItemInput
(focusin)="onFilterItemInputFocus()"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
(input)="filterItemInputChange.next(filterItemInput.value)"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="filterItemAutocomplete"
[matChipInputFor]="filterItemsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addFilterItemFromChipInput($event)">
</mat-chip-list>
<mat-autocomplete #filterItemAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="filterItemSelected($event)">
<mat-option *ngFor="let filterItem of filteredFilterItems | async" [value]="filterItem">
<span [innerHTML]="filterItem | highlight:filterItemSearchText"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</section>

View File

@ -0,0 +1,156 @@
///
/// Copyright © 2016-2022 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 { Component, ElementRef, ViewChild } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { MatChipInputEvent, MatChipList } from '@angular/material/chips';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { Observable, of, Subject } from 'rxjs';
import { map, mergeMap, share, startWith } from 'rxjs/operators';
@Component({
selector: 'tb-navigation-cards-widget-settings',
templateUrl: './navigation-cards-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class NavigationCardsWidgetSettingsComponent extends WidgetSettingsComponent {
@ViewChild('filterItemsChipList') filterItemsChipList: MatChipList;
@ViewChild('filterItemAutocomplete') filterItemAutocomplete: MatAutocomplete;
@ViewChild('filterItemInput') filterItemInput: ElementRef<HTMLInputElement>;
filterItems: Array<string> = ['/devices', '/assets', '/deviceProfiles'];
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
navigationCardsWidgetSettingsForm: FormGroup;
filteredFilterItems: Observable<Array<string>>;
filterItemSearchText = '';
filterItemInputChange = new Subject<string>();
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
this.filteredFilterItems = this.filterItemInputChange
.pipe(
startWith(''),
map((value) => value ? value : ''),
mergeMap(name => this.fetchFilterItems(name) ),
share()
);
}
protected settingsForm(): FormGroup {
return this.navigationCardsWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
filterType: 'all',
filter: []
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.navigationCardsWidgetSettingsForm = this.fb.group({
filterType: [settings.filterType, []],
filter: [settings.filter, []]
});
}
protected validatorTriggers(): string[] {
return ['filterType'];
}
protected updateValidators(emitEvent: boolean) {
const filterType: string = this.navigationCardsWidgetSettingsForm.get('filterType').value;
if (filterType === 'all') {
this.navigationCardsWidgetSettingsForm.get('filter').disable();
} else {
this.navigationCardsWidgetSettingsForm.get('filter').enable();
}
this.navigationCardsWidgetSettingsForm.get('filter').updateValueAndValidity({emitEvent});
}
private fetchFilterItems(searchText?: string): Observable<Array<string>> {
this.filterItemSearchText = searchText;
let result = [...this.filterItems];
if (this.filterItemSearchText && this.filterItemSearchText.length) {
result.unshift(this.filterItemSearchText);
result = result.filter(item => item.includes(this.filterItemSearchText));
}
return of(result);
}
private addFilterItem(filterItem: string): boolean {
if (filterItem) {
const filterItems: string[] = this.navigationCardsWidgetSettingsForm.get('filter').value;
const index = filterItems.indexOf(filterItem);
if (index === -1) {
filterItems.push(filterItem);
this.navigationCardsWidgetSettingsForm.get('filter').setValue(filterItems);
this.navigationCardsWidgetSettingsForm.get('filter').markAsDirty();
return true;
}
}
return false;
}
onFilterItemRemoved(filterItem: string): void {
const filterItems: string[] = this.navigationCardsWidgetSettingsForm.get('filter').value;
const index = filterItems.indexOf(filterItem);
if (index > -1) {
filterItems.splice(index, 1);
this.navigationCardsWidgetSettingsForm.get('filter').setValue(filterItems);
this.navigationCardsWidgetSettingsForm.get('filter').markAsDirty();
}
}
onFilterItemInputFocus() {
this.filterItemInputChange.next(this.filterItemInput.nativeElement.value);
}
addFilterItemFromChipInput(event: MatChipInputEvent): void {
const value = event.value;
if ((value || '').trim()) {
const filterItem = value.trim();
if (this.addFilterItem(filterItem)) {
this.clearFilterItemInput('');
}
}
}
filterItemSelected(event: MatAutocompleteSelectedEvent): void {
this.addFilterItem(event.option.value);
this.clearFilterItemInput('');
}
clearFilterItemInput(value: string = '') {
this.filterItemInput.nativeElement.value = value;
this.filterItemInputChange.next(null);
setTimeout(() => {
this.filterItemInput.nativeElement.blur();
this.filterItemInput.nativeElement.focus();
}, 0);
}
}

View File

@ -163,6 +163,12 @@ import {
import {
GpioPanelWidgetSettingsComponent
} from '@home/components/widget/lib/settings/gpio/gpio-panel-widget-settings.component';
import {
NavigationCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component';
import {
NavigationCardsWidgetSettingsComponent
} from '@home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component';
@NgModule({
declarations: [
@ -223,7 +229,9 @@ import {
GatewayEventsWidgetSettingsComponent,
GpioItemComponent,
GpioControlWidgetSettingsComponent,
GpioPanelWidgetSettingsComponent
GpioPanelWidgetSettingsComponent,
NavigationCardWidgetSettingsComponent,
NavigationCardsWidgetSettingsComponent
],
imports: [
CommonModule,
@ -288,7 +296,9 @@ import {
GatewayEventsWidgetSettingsComponent,
GpioItemComponent,
GpioControlWidgetSettingsComponent,
GpioPanelWidgetSettingsComponent
GpioPanelWidgetSettingsComponent,
NavigationCardWidgetSettingsComponent,
NavigationCardsWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -338,5 +348,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-gateway-config-single-device-widget-settings': GatewayConfigSingleDeviceWidgetSettingsComponent,
'tb-gateway-events-widget-settings': GatewayEventsWidgetSettingsComponent,
'tb-gpio-control-widget-settings': GpioControlWidgetSettingsComponent,
'tb-gpio-panel-widget-settings': GpioPanelWidgetSettingsComponent
'tb-gpio-panel-widget-settings': GpioPanelWidgetSettingsComponent,
'tb-navigation-card-widget-settings': NavigationCardWidgetSettingsComponent,
'tb-navigation-cards-widget-settings': NavigationCardsWidgetSettingsComponent
};

View File

@ -16,9 +16,15 @@
-->
<div fxLayout="row" [formGroup]="materialIconFormGroup">
<mat-icon (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
<mat-icon class="icon-value" (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
<mat-form-field fxFlex>
<mat-label translate>icon.icon</mat-label>
<input matInput formControlName="icon" (mousedown)="openIconDialog()">
<input [required]="required" matInput formControlName="icon" (mousedown)="openIconDialog()">
<button *ngIf="iconClearButton"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-form-field>
</div>

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
:host {
.mat-icon {
.mat-icon.icon-value {
padding: 4px;
margin: 8px 4px 4px;
cursor: pointer;

View File

@ -20,6 +20,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@Component({
selector: 'tb-material-icon-select',
@ -38,6 +39,27 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
@Input()
disabled: boolean;
private iconClearButtonValue: boolean;
get iconClearButton(): boolean {
return this.iconClearButtonValue;
}
@Input()
set iconClearButton(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.iconClearButtonValue !== newVal) {
this.iconClearButtonValue = newVal;
}
}
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
private modelValue: string;
private propagateChange = null;
@ -104,4 +126,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
);
}
}
clear() {
this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true});
}
}

View File

@ -3615,6 +3615,16 @@
"no-labels": "No labels configured",
"add-label": "Add label"
},
"navigation": {
"title": "Title",
"navigation-path": "Navigation path",
"filter-type": "Filter type",
"filter-type-all": "All items",
"filter-type-include": "Include items",
"filter-type-exclude": "Exclude items",
"items": "Items",
"enter-urls-to-filter": "Enter urls to filter..."
},
"persistent-table": {
"rpc-id": "RPC ID",
"message-type": "Message type",