Browse Source

UI: Implement navigation widget settings form

pull/6545/head
Igor Kulikov 4 years ago
parent
commit
03ddc2c378
  1. 8
      application/src/main/data/json/system/widget_bundles/navigation_widgets.json
  2. 32
      ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html
  3. 56
      ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.ts
  4. 56
      ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html
  5. 156
      ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.ts
  6. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  7. 10
      ui-ngx/src/app/shared/components/material-icon-select.component.html
  8. 2
      ui-ngx/src/app/shared/components/material-icon-select.component.scss
  9. 26
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  10. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

8
application/src/main/data/json/system/widget_bundles/navigation_widgets.json

@ -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}"
}
}
]
}
}

32
ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html

@ -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>

56
ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.ts

@ -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]]
});
}
}

56
ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html

@ -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>

156
ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.ts

@ -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);
}
}

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -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
};

10
ui-ngx/src/app/shared/components/material-icon-select.component.html

@ -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>

2
ui-ngx/src/app/shared/components/material-icon-select.component.scss

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

26
ui-ngx/src/app/shared/components/material-icon-select.component.ts

@ -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});
}
}

10
ui-ngx/src/assets/locale/locale.constant-en_US.json

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

Loading…
Cancel
Save