Browse Source
- 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
9 changed files with 385 additions and 165 deletions
@ -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> |
|||
@ -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] |
|||
}); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue