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