Browse Source

feat(widgets): add HTML Container widget settings and metadata

- Fill description and tags for the HTML Container widget type JSON.
- Add basic config component (plain HTML / Angular mode editor).
- Add advanced settings component and shared common settings.
pull/15556/head
Igor Kulikov 5 months ago
parent
commit
910be7d611
  1. 6
      application/src/main/data/json/system/widget_types/html_container.json
  2. 79
      ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html
  3. 85
      ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts
  4. 97
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.html
  5. 183
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.ts
  6. 9
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  7. 20
      ui-ngx/src/app/modules/home/components/widget/lib/settings/html/html-container-widget-settings.component.html
  8. 62
      ui-ngx/src/app/modules/home/components/widget/lib/settings/html/html-container-widget-settings.component.ts
  9. 9
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

6
application/src/main/data/json/system/widget_types/html_container.json

@ -3,7 +3,7 @@
"name": "HTML Container", "name": "HTML Container",
"deprecated": false, "deprecated": false,
"image": "tb-image;/api/images/system/html_card_system_widget_image.png", "image": "tb-image;/api/images/system/html_card_system_widget_image.png",
"description": null, "description": "Configurable HTML, CSS and JavaScript widget with access to the Widget API via WidgetContext and the ability to load external resources or modules. Supports two modes: plain HTML (regular HTML/JS bound to the widget container) and Angular (Angular template with bound variables and functions). Use for custom complex visualizations or actions when system widgets are not enough.",
"descriptor": { "descriptor": {
"type": "static", "type": "static",
"sizeX": 9.5, "sizeX": 9.5,
@ -12,7 +12,7 @@
"templateHtml": "<tb-html-container-widget \n [ctx]=\"ctx\">\n</tb-html-container-widget>", "templateHtml": "<tb-html-container-widget \n [ctx]=\"ctx\">\n</tb-html-container-widget>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n \n}\n", "controllerScript": "self.onInit = function() {\n \n}\n",
"settingsDirective": "", "settingsDirective": "tb-html-container-widget-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-html-container-basic-config", "basicModeDirective": "tb-html-container-basic-config",
"defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255, 255, 255, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{\"type\":\"PLAIN\",\"html\":\"\",\"css\":\"\",\"js\":\"\",\"resources\":[]},\"title\":\"HTML Container\",\"dropShadow\":false,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}" "defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255, 255, 255, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{\"type\":\"PLAIN\",\"html\":\"\",\"css\":\"\",\"js\":\"\",\"resources\":[]},\"title\":\"HTML Container\",\"dropShadow\":false,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}"
@ -31,5 +31,5 @@
} }
], ],
"scada": false, "scada": false,
"tags": null "tags": ["html", "css", "javascript", "custom", "script", "code", "container", "angular", "template", "external resources", "widget api", "advanced", "custom visualization", "custom action", "web", "markup"]
} }

79
ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html

@ -16,82 +16,5 @@
--> -->
<ng-container [formGroup]="htmlContainerWidgetConfigForm"> <ng-container [formGroup]="htmlContainerWidgetConfigForm">
<div class="tb-form-panel no-padding no-border"> <tb-html-container-settings formControlName="settings"></tb-html-container-settings>
<div class="flex flex-row items-center gap-4">
<div class="tb-form-panel-title" translate>widgets.html-container.container-type</div>
<tb-toggle-select formControlName="type">
<tb-toggle-option [value]="HtmlContainerWidgetType.PLAIN">{{ 'widgets.html-container.type-plain' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="HtmlContainerWidgetType.ANGULAR">{{ 'widgets.html-container.type-angular' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel stroked !py-2">
<mat-expansion-panel #resourcesPanel="matExpansionPanel" class="tb-settings" [expanded]="false" >
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title class="h-10">
<div [matBadge]="resourcesFormArray.length" [matBadgeHidden]="resourcesPanel.expanded || !resourcesFormArray.length"
matBadgeSize="small">{{ 'widgets.html-container.resources' | translate }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
@if (resourcesFormArray.length) {
@for (resourceControl of resourcesControls; track resourceControl; let i = $index) {
<div class="tb-form-row no-border no-padding" [formGroup]="resourceControl">
<tb-resource-autocomplete class="flex-1"
formControlName="url"
inlineField
hideRequiredMarker required
[allowAutocomplete]="resourceControl.get('isModule').value && htmlContainerWidgetConfigForm.get('type').value === HtmlContainerWidgetType.ANGULAR"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
@if (htmlContainerWidgetConfigForm.get('type').value === HtmlContainerWidgetType.ANGULAR) {
<mat-checkbox formControlName="isModule">
{{ 'widget.resource-is-extension' | translate }}
</mat-checkbox>
}
<button mat-icon-button color="primary"
(click)="removeResource(i)"
matTooltip="{{'widget.remove-resource' | translate}}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
}
} @else {
<span translate
class="tb-prompt flex items-center justify-center">widgets.html-container.no-resources</span>
}
<div class="mt-2">
<button mat-raised-button color="primary"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-row">
<tb-html class="flex-1"
formControlName="html"
label="{{ (htmlContainerWidgetConfigForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? 'widgets.html-container.angular-html-template' : 'widgets.html-container.html') | translate }}">
</tb-html>
</div>
<div class="tb-form-row">
<tb-css class="flex-1"
formControlName="css"
label="{{ 'widgets.html-container.css' | translate }}">
</tb-css>
</div>
<div class="tb-form-row">
<tb-js-func class="flex-1"
formControlName="js"
[globalVariables]="functionScopeVariables"
[editorCompleter]="containerFunctionEditorCompleter"
[functionArgs]="htmlContainerWidgetConfigForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? ['ctx'] : ['ctx', 'container']"
withModules
functionTitle="{{ 'widgets.html-container.js-function' | translate }}">
</tb-js-func>
</div>
</div>
</ng-container> </ng-container>

85
ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts

@ -14,22 +14,17 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, Injector } from '@angular/core'; import { Component } from '@angular/core';
import { UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { import {
ContainerFunctionEditorCompleter,
htmlContainerDefaultSettings, htmlContainerDefaultSettings,
HtmlContainerWidgetSettings, HtmlContainerWidgetType HtmlContainerWidgetSettings
} from '@home/components/widget/lib/html/html-container-widget.models'; } from '@home/components/widget/lib/html/html-container-widget.models';
import { WidgetService } from '@core/http/widget.service';
import { WidgetResource } from '@shared/models/widget.models';
import { isJSResource, ResourceSubType } from '@shared/models/resource.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'tb-html-container-basic-config', selector: 'tb-html-container-basic-config',
@ -39,27 +34,10 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
}) })
export class HtmlContainerBasicConfigComponent extends BasicWidgetConfigComponent { export class HtmlContainerBasicConfigComponent extends BasicWidgetConfigComponent {
HtmlContainerWidgetType = HtmlContainerWidgetType;
htmlContainerWidgetConfigForm: UntypedFormGroup; htmlContainerWidgetConfigForm: UntypedFormGroup;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
containerFunctionEditorCompleter = ContainerFunctionEditorCompleter;
get resourcesFormArray(): UntypedFormArray {
return this.htmlContainerWidgetConfigForm.get('resources') as UntypedFormArray;
}
get resourcesControls(): UntypedFormGroup[] {
return this.resourcesFormArray.controls as UntypedFormGroup[];
}
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent, protected widgetConfigComponent: WidgetConfigComponent,
private widgetService: WidgetService,
private cd: ChangeDetectorRef,
private $injector: Injector,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent); super(store, widgetConfigComponent);
} }
@ -71,65 +49,12 @@ export class HtmlContainerBasicConfigComponent extends BasicWidgetConfigComponen
protected onConfigSet(configData: WidgetConfigComponentData) { protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: HtmlContainerWidgetSettings = {...htmlContainerDefaultSettings, ...(configData.config.settings || {})}; const settings: HtmlContainerWidgetSettings = {...htmlContainerDefaultSettings, ...(configData.config.settings || {})};
this.htmlContainerWidgetConfigForm = this.fb.group({ this.htmlContainerWidgetConfigForm = this.fb.group({
type: [settings.type, []], settings: [settings, []]
html: [settings.html, []],
css: [settings.css, []],
js: [settings.js, []],
resources: this.fb.array(settings.resources.map(r => this.buildResourceFormGroup(r)))
}); });
this.htmlContainerWidgetConfigForm.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.updateResources());
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings};
this.widgetConfig.config.settings.type = config.type;
this.widgetConfig.config.settings.html = config.html;
this.widgetConfig.config.settings.css = config.css;
this.widgetConfig.config.settings.js = config.js;
this.widgetConfig.config.settings.resources = config.resources;
return this.widgetConfig; return this.widgetConfig;
} }
private updateResources() {
if (this.htmlContainerWidgetConfigForm.get('type').value === HtmlContainerWidgetType.PLAIN) {
const resources: WidgetResource[] = this.resourcesFormArray.value;
const filtered = resources.filter(r => !isJSResource(r.url));
let updated = filtered.length !== resources.length;
filtered.forEach((r) => {
if (r.isModule) {
r.isModule = false;
updated = true;
}
});
if (updated) {
this.resourcesFormArray.clear();
filtered.forEach(r => {
this.resourcesFormArray.push(this.buildResourceFormGroup(r));
});
}
}
}
addResource() {
const newResource: WidgetResource = {
url: '',
isModule: false
};
this.resourcesFormArray.push(this.buildResourceFormGroup(newResource));
}
removeResource(index: number) {
this.resourcesFormArray.removeAt(index);
}
private buildResourceFormGroup(resource: WidgetResource): UntypedFormGroup {
return this.fb.group({
url: [resource.url, [Validators.required]],
isModule: [resource.isModule]
});
}
protected readonly ResourceSubType = ResourceSubType;
} }

97
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.html

@ -0,0 +1,97 @@
<!--
Copyright © 2016-2026 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.
-->
<ng-container [formGroup]="htmlContainerSettingsForm">
<div class="tb-form-panel no-padding no-border">
<div class="flex flex-row items-center gap-4">
<div class="tb-form-panel-title" translate>widgets.html-container.container-type</div>
<tb-toggle-select formControlName="type">
<tb-toggle-option [value]="HtmlContainerWidgetType.PLAIN">{{ 'widgets.html-container.type-plain' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="HtmlContainerWidgetType.ANGULAR">{{ 'widgets.html-container.type-angular' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel stroked !py-2">
<mat-expansion-panel #resourcesPanel="matExpansionPanel" class="tb-settings" [expanded]="false" >
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title class="h-10">
<div [matBadge]="resourcesFormArray.length" [matBadgeHidden]="resourcesPanel.expanded || !resourcesFormArray.length"
matBadgeSize="small">{{ 'widgets.html-container.resources' | translate }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
@if (resourcesFormArray.length) {
@for (resourceControl of resourcesControls; track resourceControl; let i = $index) {
<div class="tb-form-row no-border no-padding" [formGroup]="resourceControl">
<tb-resource-autocomplete class="flex-1"
formControlName="url"
inlineField
hideRequiredMarker required
[allowAutocomplete]="resourceControl.get('isModule').value && htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
@if (htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR) {
<mat-checkbox formControlName="isModule">
{{ 'widget.resource-is-extension' | translate }}
</mat-checkbox>
}
<button mat-icon-button color="primary"
(click)="removeResource(i)"
matTooltip="{{'widget.remove-resource' | translate}}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
}
} @else {
<span translate
class="tb-prompt flex items-center justify-center">widgets.html-container.no-resources</span>
}
<div class="mt-2">
<button mat-raised-button color="primary"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-row">
<tb-html class="flex-1"
formControlName="html"
label="{{ (htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? 'widgets.html-container.angular-html-template' : 'widgets.html-container.html') | translate }}">
</tb-html>
</div>
<div class="tb-form-row">
<tb-css class="flex-1"
formControlName="css"
label="{{ 'widgets.html-container.css' | translate }}">
</tb-css>
</div>
<div class="tb-form-row">
<tb-js-func class="flex-1"
formControlName="js"
[globalVariables]="functionScopeVariables"
[editorCompleter]="containerFunctionEditorCompleter"
[functionArgs]="htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? ['ctx'] : ['ctx', 'container']"
withModules
functionTitle="{{ 'widgets.html-container.js-function' | translate }}">
</tb-js-func>
</div>
</div>
</ng-container>

183
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.ts

@ -0,0 +1,183 @@
///
/// Copyright © 2016-2026 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, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import { WidgetResource } from '@shared/models/widget.models';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
Validators
} from '@angular/forms';
import {
ContainerFunctionEditorCompleter,
HtmlContainerWidgetSettings,
HtmlContainerWidgetType
} from '@home/components/widget/lib/html/html-container-widget.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isJSResource } from '@shared/models/resource.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-html-container-settings',
templateUrl: './html-container-settings.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => HtmlContainerSettingsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => HtmlContainerSettingsComponent),
multi: true,
}
],
standalone: false
})
export class HtmlContainerSettingsComponent implements OnInit, ControlValueAccessor, Validator {
HtmlContainerWidgetType = HtmlContainerWidgetType;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
containerFunctionEditorCompleter = ContainerFunctionEditorCompleter;
@Input()
disabled: boolean;
htmlContainerSettingsForm: UntypedFormGroup;
private modelValue: HtmlContainerWidgetSettings;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
private destroyRef: DestroyRef) {
}
get resourcesFormArray(): UntypedFormArray {
return this.htmlContainerSettingsForm.get('resources') as UntypedFormArray;
}
get resourcesControls(): UntypedFormGroup[] {
return this.resourcesFormArray.controls as UntypedFormGroup[];
}
ngOnInit(): void {
this.htmlContainerSettingsForm = this.fb.group({
type: [null, []],
html: [null, []],
css: [null, []],
js: [null, []],
resources: this.fb.array([])
});
this.htmlContainerSettingsForm.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.updateResources());
this.htmlContainerSettingsForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.htmlContainerSettingsForm.disable({emitEvent: false});
} else {
this.htmlContainerSettingsForm.enable({emitEvent: false});
}
}
writeValue(value: HtmlContainerWidgetSettings): void {
this.modelValue = value;
this.htmlContainerSettingsForm.get('type').patchValue(value.type, {emitEvent: false});
this.htmlContainerSettingsForm.get('html').patchValue(value.html, {emitEvent: false});
this.htmlContainerSettingsForm.get('css').patchValue(value.css, {emitEvent: false});
this.htmlContainerSettingsForm.get('js').patchValue(value.js, {emitEvent: false});
this.resourcesFormArray.clear({emitEvent: false});
value.resources.forEach(r => {
this.resourcesFormArray.push(this.buildResourceFormGroup(r), {emitEvent: false});
});
}
validate(_c: UntypedFormControl) {
return this.htmlContainerSettingsForm.valid ? null : {
htmlContainerSettings: {
valid: false,
}
};
}
addResource() {
const newResource: WidgetResource = {
url: '',
isModule: false
};
this.resourcesFormArray.push(this.buildResourceFormGroup(newResource));
}
removeResource(index: number) {
this.resourcesFormArray.removeAt(index);
}
private propagateChange = (v: any) => { };
private updateModel() {
this.modelValue = this.htmlContainerSettingsForm.value;
this.propagateChange(this.modelValue);
}
private updateResources() {
if (this.htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.PLAIN) {
const resources: WidgetResource[] = this.resourcesFormArray.value;
const filtered = resources.filter(r => !isJSResource(r.url));
let updated = filtered.length !== resources.length;
filtered.forEach((r) => {
if (r.isModule) {
r.isModule = false;
updated = true;
}
});
if (updated) {
this.resourcesFormArray.clear();
filtered.forEach(r => {
this.resourcesFormArray.push(this.buildResourceFormGroup(r));
});
}
}
}
private buildResourceFormGroup(resource: WidgetResource): UntypedFormGroup {
return this.fb.group({
url: [resource.url, [Validators.required]],
isModule: [resource.isModule]
});
}
}

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

@ -267,6 +267,9 @@ import {
import { import {
ShapeFillStripeSettingsPanelComponent ShapeFillStripeSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; } from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component';
import {
HtmlContainerSettingsComponent
} from '@home/components/widget/lib/settings/common/html/html-container-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -372,7 +375,8 @@ import {
DataKeysComponent, DataKeysComponent,
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
DataKeyConfigComponent, DataKeyConfigComponent,
WidgetSettingsComponent WidgetSettingsComponent,
HtmlContainerSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -453,7 +457,8 @@ import {
DataKeysComponent, DataKeysComponent,
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
DataKeyConfigComponent, DataKeyConfigComponent,
WidgetSettingsComponent WidgetSettingsComponent,
HtmlContainerSettingsComponent
], ],
providers: [ providers: [
ColorSettingsComponentService, ColorSettingsComponentService,

20
ui-ngx/src/app/modules/home/components/widget/lib/settings/html/html-container-widget-settings.component.html

@ -0,0 +1,20 @@
<!--
Copyright © 2016-2026 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.
-->
<ng-container [formGroup]="htmlContainerWidgetSettingsForm">
<tb-html-container-settings formControlName="htmlContainerSettings"></tb-html-container-settings>
</ng-container>

62
ui-ngx/src/app/modules/home/components/widget/lib/settings/html/html-container-widget-settings.component.ts

@ -0,0 +1,62 @@
///
/// Copyright © 2016-2026 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 { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { htmlContainerDefaultSettings } from '@home/components/widget/lib/html/html-container-widget.models';
@Component({
selector: 'tb-html-container-widget-settings',
templateUrl: './html-container-widget-settings.component.html',
styleUrls: [],
standalone: false
})
export class HtmlContainerWidgetSettingsComponent extends WidgetSettingsComponent {
htmlContainerWidgetSettingsForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.htmlContainerWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return htmlContainerDefaultSettings;
}
protected onSettingsSet(settings: WidgetSettings) {
this.htmlContainerWidgetSettingsForm = this.fb.group({
htmlContainerSettings: [settings.htmlContainerSettings, []]
});
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
return {
htmlContainerSettings: settings
};
}
protected prepareOutputSettings(settings: any): WidgetSettings {
return settings.htmlContainerSettings;
}
}

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

@ -375,6 +375,9 @@ import {
ValueStepperWidgetSettingsComponent ValueStepperWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component'; } from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component';
import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings/map/map-widget-settings.component'; import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings/map/map-widget-settings.component';
import {
HtmlContainerWidgetSettingsComponent
} from '@home/components/widget/lib/settings/html/html-container-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -508,7 +511,8 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings
LabelValueCardWidgetSettingsComponent, LabelValueCardWidgetSettingsComponent,
UnreadNotificationWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent,
ScadaSymbolWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent,
MapWidgetSettingsComponent MapWidgetSettingsComponent,
HtmlContainerWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -647,7 +651,8 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings
LabelValueCardWidgetSettingsComponent, LabelValueCardWidgetSettingsComponent,
UnreadNotificationWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent,
ScadaSymbolWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent,
MapWidgetSettingsComponent MapWidgetSettingsComponent,
HtmlContainerWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {

Loading…
Cancel
Save