diff --git a/application/src/main/data/json/system/widget_bundles/navigation_widgets.json b/application/src/main/data/json/system/widget_bundles/navigation_widgets.json index f7d0b303b4..9d06c0122a 100644 --- a/application/src/main/data/json/system/widget_bundles/navigation_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/navigation_widgets.json @@ -19,8 +19,9 @@ "templateHtml": "", "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": "", "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}" } } ] -} +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html new file mode 100644 index 0000000000..4bb4ea7824 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.html @@ -0,0 +1,32 @@ + +
+ + widgets.navigation.title + + + + + + widgets.navigation.navigation-path + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-card-widget-settings.component.ts new file mode 100644 index 0000000000..28ff53599b --- /dev/null +++ b/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, + 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]] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html new file mode 100644 index 0000000000..18aca3645f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.html @@ -0,0 +1,56 @@ + +
+
+
widgets.navigation.filter-type
+ + {{ 'widgets.navigation.filter-type-all' | translate }} + {{ 'widgets.navigation.filter-type-include' | translate }} + {{ 'widgets.navigation.filter-type-exclude' | translate }} + +
+ + widgets.navigation.items + + + {{ filterItem }} + cancel + + + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/navigation/navigation-cards-widget-settings.component.ts new file mode 100644 index 0000000000..4bf7d36f14 --- /dev/null +++ b/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; + + filterItems: Array = ['/devices', '/assets', '/deviceProfiles']; + + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + + navigationCardsWidgetSettingsForm: FormGroup; + + filteredFilterItems: Observable>; + + filterItemSearchText = ''; + + filterItemInputChange = new Subject(); + + constructor(protected store: Store, + 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> { + 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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 6c9993b0b4..8ee0388bc8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/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
- {{materialIconFormGroup.get('icon').value}} + {{materialIconFormGroup.get('icon').value}} icon.icon - + +
diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.scss b/ui-ngx/src/app/shared/components/material-icon-select.component.scss index cd77a15453..2227bdab95 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.scss +++ b/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; diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 14c7d3e9b9..868ed79c58 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/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}); + } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 343d706ea5..c2b149ae57 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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",