Browse Source

UI: Add HTML card/HTML value card widget settings form

pull/6545/head
Igor Kulikov 4 years ago
parent
commit
323c67ff2a
  1. 10
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.html
  3. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/html-card-widget-settings.component.ts
  4. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html
  5. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts
  6. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  7. 2
      ui-ngx/src/app/shared/components/css.component.html
  8. 6
      ui-ngx/src/app/shared/components/css.component.ts
  9. 41
      ui-ngx/src/app/shared/components/html.component.html
  10. 65
      ui-ngx/src/app/shared/components/html.component.scss
  11. 211
      ui-ngx/src/app/shared/components/html.component.ts
  12. 8
      ui-ngx/src/app/shared/components/js-func.component.html
  13. 9
      ui-ngx/src/app/shared/components/js-func.component.ts
  14. 2
      ui-ngx/src/app/shared/components/markdown-editor.component.html
  15. 3
      ui-ngx/src/app/shared/shared.module.ts
  16. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

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

@ -0,0 +1,25 @@
<!--
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 [formGroup]="htmlCardWidgetSettingsForm" fxLayout="column">
<tb-html formControlName="cardHtml" required
label="{{ 'widgets.html-card.html' | translate }}">
</tb-html>
<tb-css formControlName="cardCss"
label="{{ 'widgets.html-card.css' | translate }}">
</tb-css>
</section>

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

@ -0,0 +1,54 @@
///
/// 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-html-card-widget-settings',
templateUrl: './html-card-widget-settings.component.html',
styleUrls: []
})
export class HtmlCardWidgetSettingsComponent extends WidgetSettingsComponent {
htmlCardWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.htmlCardWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
cardHtml: '<div class=\'card\'>HTML code here</div>',
cardCss: '.card {\n font-weight: bold; \n}'
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.htmlCardWidgetSettingsForm = this.fb.group({
cardHtml: [settings.cardHtml, [Validators.required]],
cardCss: [settings.cardCss, []]
});
}
}

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html

@ -26,7 +26,8 @@
{{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }}
</mat-error>
</mat-form-field>
<tb-js-func [fxShow]="qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value"
<tb-js-func required
[fxShow]="qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value"
formControlName="qrCodeTextFunction"
[globalVariables]="functionScopeVariables"
[functionArgs]="['data']"

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts

@ -50,7 +50,7 @@ export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent {
this.qrCodeWidgetSettingsForm = this.fb.group({
qrCodeTextPattern: [settings.qrCodeTextPattern, [Validators.required]],
useQrCodeTextFunction: [settings.useQrCodeTextFunction, [Validators.required]],
qrCodeTextFunction: [settings.qrCodeTextFunction, []]
qrCodeTextFunction: [settings.qrCodeTextFunction, [Validators.required]]
});
}

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

@ -44,6 +44,9 @@ import {
import {
EntitiesHierarchyWidgetSettingsComponent
} from '@home/components/widget/lib/settings/entities-hierarchy-widget-settings.component';
import {
HtmlCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/html-card-widget-settings.component';
@NgModule({
declarations: [
@ -57,7 +60,8 @@ import {
LabelWidgetSettingsComponent,
SimpleCardWidgetSettingsComponent,
DashboardStateWidgetSettingsComponent,
EntitiesHierarchyWidgetSettingsComponent
EntitiesHierarchyWidgetSettingsComponent,
HtmlCardWidgetSettingsComponent
],
imports: [
CommonModule,
@ -75,7 +79,8 @@ import {
LabelWidgetSettingsComponent,
SimpleCardWidgetSettingsComponent,
DashboardStateWidgetSettingsComponent,
EntitiesHierarchyWidgetSettingsComponent
EntitiesHierarchyWidgetSettingsComponent,
HtmlCardWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -90,5 +95,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-label-widget-settings': LabelWidgetSettingsComponent,
'tb-simple-card-widget-settings': SimpleCardWidgetSettingsComponent,
'tb-dashboard-state-widget-settings': DashboardStateWidgetSettingsComponent,
'tb-entities-hierarchy-widget-settings': EntitiesHierarchyWidgetSettingsComponent
'tb-entities-hierarchy-widget-settings': EntitiesHierarchyWidgetSettingsComponent,
'tb-html-card-widget-settings': HtmlCardWidgetSettingsComponent
};

2
ui-ngx/src/app/shared/components/css.component.html

@ -19,7 +19,7 @@
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-css-toolbar">
<label class="tb-title no-padding" [ngClass]="{'tb-error': hasErrors}">{{ label }}</label>
<label class="tb-title no-padding" [ngClass]="{'tb-error': hasErrors || required && !modelValue, 'tb-required': required}">{{ label }}</label>
<span fxFlex></span>
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyCss()">
{{'js-func.tidy' | translate }}

6
ui-ngx/src/app/shared/components/css.component.ts

@ -15,6 +15,7 @@
///
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
@ -91,7 +92,8 @@ export class CssComponent implements OnInit, OnDestroy, ControlValueAccessor, Va
private utils: UtilsService,
private translate: TranslateService,
protected store: Store<AppState>,
private raf: RafService) {
private raf: RafService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
@ -128,6 +130,7 @@ export class CssComponent implements OnInit, OnDestroy, ControlValueAccessor, Va
if (this.hasErrors !== hasErrors) {
this.hasErrors = hasErrors;
this.propagateChange(this.modelValue);
this.cd.markForCheck();
}
});
this.editorResize$ = new ResizeObserver(() => {
@ -202,6 +205,7 @@ export class CssComponent implements OnInit, OnDestroy, ControlValueAccessor, Va
if (this.modelValue !== editorValue) {
this.modelValue = editorValue;
this.propagateChange(this.modelValue);
this.cd.markForCheck();
}
}
}

41
ui-ngx/src/app/shared/components/html.component.html

@ -0,0 +1,41 @@
<!--
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.
-->
<div class="tb-html" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}"
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-html-toolbar">
<label class="tb-title no-padding" [ngClass]="{'tb-error': hasErrors || required && !modelValue, 'tb-required': required}">{{ label }}</label>
<span fxFlex></span>
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyHtml()">
{{'js-func.tidy' | translate }}
</button>
<fieldset style="width: initial">
<div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above"
style="border-radius: 50%"
(click)="fullscreen = !fullscreen">
<button type='button' mat-button mat-icon-button class="tb-mat-32">
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
</fieldset>
</div>
<div id="tb-html-panel" class="tb-html-content-panel" fxLayout="column">
<div #htmlEditor id="tb-html-input" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
</div>

65
ui-ngx/src/app/shared/components/html.component.scss

@ -0,0 +1,65 @@
/**
* 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.
*/
.tb-html {
position: relative;
&.tb-disabled {
color: rgba(0, 0, 0, .38);
}
&.fill-height {
height: 100%;
}
.tb-html-content-panel {
height: calc(100% - 40px);
border: 1px solid #c0c0c0;
#tb-html-input {
width: 100%;
min-width: 200px;
height: 100%;
&:not(.fill-height) {
min-height: 200px;
}
}
}
.tb-html-toolbar {
& > * {
&:not(:last-child) {
margin-right: 4px;
}
}
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
background: rgba(220, 220, 220, .35);
align-items: center;
vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
font-size: .8rem;
line-height: 15px;
&:not(.tb-help-popup-button) {
color: #7b7b7b;
}
}
.tb-help-popup-button-loading {
background: #f3f3f3;
}
}
}

211
ui-ngx/src/app/shared/components/html.component.ts

@ -0,0 +1,211 @@
///
/// 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 {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { Ace } from 'ace-builds';
import { getAce } from '@shared/models/ace/ace.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
import { beautifyHtml } from '@shared/models/beautify.models';
@Component({
selector: 'tb-html',
templateUrl: './html.component.html',
styleUrls: ['./html.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => HtmlComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => HtmlComponent),
multi: true,
}
],
encapsulation: ViewEncapsulation.None
})
export class HtmlComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
@ViewChild('htmlEditor', {static: true})
htmlEditorElmRef: ElementRef;
private htmlEditor: Ace.Editor;
private editorsResizeCaf: CancelAnimationFrame;
private editorResize$: ResizeObserver;
private ignoreChange = false;
@Input() label: string;
@Input() disabled: boolean;
@Input() fillHeight: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
fullscreen = false;
modelValue: string;
hasErrors = false;
private propagateChange = null;
constructor(public elementRef: ElementRef,
private utils: UtilsService,
private translate: TranslateService,
protected store: Store<AppState>,
private raf: RafService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
const editorElement = this.htmlEditorElmRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: 'ace/mode/html',
showGutter: true,
showPrintMargin: true,
readOnly: this.disabled
};
const advancedOptions = {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
};
editorOptions = {...editorOptions, ...advancedOptions};
getAce().subscribe(
(ace) => {
this.htmlEditor = ace.edit(editorElement, editorOptions);
this.htmlEditor.session.setUseWrapMode(true);
this.htmlEditor.setValue(this.modelValue ? this.modelValue : '', -1);
this.htmlEditor.setReadOnly(this.disabled);
this.htmlEditor.on('change', () => {
if (!this.ignoreChange) {
this.updateView();
}
});
// @ts-ignore
this.htmlEditor.session.on('changeAnnotation', () => {
const annotations = this.htmlEditor.session.getAnnotations();
const hasErrors = annotations.filter(annotation => annotation.type === 'error').length > 0;
if (this.hasErrors !== hasErrors) {
this.hasErrors = hasErrors;
this.propagateChange(this.modelValue);
this.cd.markForCheck();
}
});
this.editorResize$ = new ResizeObserver(() => {
this.onAceEditorResize();
});
this.editorResize$.observe(editorElement);
}
);
}
ngOnDestroy(): void {
if (this.editorResize$) {
this.editorResize$.disconnect();
}
}
private onAceEditorResize() {
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
this.editorsResizeCaf = this.raf.raf(() => {
this.htmlEditor.resize();
this.htmlEditor.renderer.updateFull();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.htmlEditor) {
this.htmlEditor.setReadOnly(this.disabled);
}
}
public validate(c: FormControl) {
return (!this.hasErrors) ? null : {
html: {
valid: false,
},
};
}
beautifyHtml() {
beautifyHtml(this.modelValue, {indent_size: 4}).subscribe(
(res) => {
if (this.modelValue !== res) {
this.htmlEditor.setValue(res ? res : '', -1);
this.updateView();
}
}
);
}
writeValue(value: string): void {
this.modelValue = value;
if (this.htmlEditor) {
this.ignoreChange = true;
this.htmlEditor.setValue(this.modelValue ? this.modelValue : '', -1);
this.ignoreChange = false;
}
}
updateView() {
const editorValue = this.htmlEditor.getValue();
if (this.modelValue !== editorValue) {
this.modelValue = editorValue;
this.propagateChange(this.modelValue);
this.cd.markForCheck();
}
}
}

8
ui-ngx/src/app/shared/components/js-func.component.html

@ -19,8 +19,10 @@
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar">
<label *ngIf="functionTitle" class="tb-title no-padding">{{functionTitle + ': f(' + functionArgsString + ')' }}</label>
<label *ngIf="!functionTitle" class="tb-title no-padding">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label>
<label *ngIf="functionTitle" class="tb-title no-padding"
[ngClass]="{'tb-error': hasErrors || !functionValid || required && !modelValue, 'tb-required': required}">{{functionTitle + ': f(' + functionArgsString + ')' }}</label>
<label *ngIf="!functionTitle" class="tb-title no-padding"
[ngClass]="{'tb-error': hasErrors || !functionValid || required && !modelValue, 'tb-required': required}">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label>
<span fxFlex></span>
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyJs()">
{{'js-func.tidy' | translate }}
@ -41,6 +43,6 @@
<div #javascriptEditor id="tb-javascript-input" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
<div *ngIf="!functionTitle" fxLayout="row" fxLayoutAlign="start center" style="height: 40px;">
<label class="tb-title no-padding">}</label>
<label class="tb-title no-padding" [ngClass]="{'tb-error': hasErrors || !functionValid || required && !modelValue}">}</label>
</div>
</div>

9
ui-ngx/src/app/shared/components/js-func.component.ts

@ -15,6 +15,7 @@
///
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
@ -125,13 +126,14 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
errorAnnotationId = -1;
private propagateChange = null;
private hasErrors = false;
public hasErrors = false;
constructor(public elementRef: ElementRef,
private utils: UtilsService,
private translate: TranslateService,
protected store: Store<AppState>,
private raf: RafService) {
private raf: RafService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
@ -185,6 +187,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
if (this.hasErrors !== hasErrors) {
this.hasErrors = hasErrors;
this.propagateChange(this.modelValue);
this.cd.markForCheck();
}
});
}
@ -273,6 +276,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
this.functionValid = this.validateJsFunc();
if (!this.functionValid) {
this.propagateChange(this.modelValue);
this.cd.markForCheck();
this.store.dispatch(new ActionNotificationShow(
{
message: this.validationError,
@ -400,6 +404,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
this.modelValue = editorValue;
this.functionValid = true;
this.propagateChange(this.modelValue);
this.cd.markForCheck();
}
}
}

2
ui-ngx/src/app/shared/components/markdown-editor.component.html

@ -18,7 +18,7 @@
<div class="markdown-content" [ngClass]="{'tb-edit-mode': !readonly}"
tb-fullscreen [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()">
<div *ngIf="label" fxLayout="row" fxLayoutAlign="start center" style="height: 40px;">
<label class="tb-title no-padding" [ngClass]="{'tb-required': required}">{{ label }}</label>
<label class="tb-title no-padding" [ngClass]="{'tb-error': required && !markdownValue, 'tb-required': required}">{{ label }}</label>
</div>
<div [fxShow]="!readonly && !disabled" class="markdown-content-editor">
<div class="buttons-panel">

3
ui-ngx/src/app/shared/shared.module.ts

@ -159,6 +159,7 @@ import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/comp
import { TbMarkdownComponent } from '@shared/components/markdown.component';
import { ProtobufContentComponent } from '@shared/components/protobuf-content.component';
import { CssComponent } from '@shared/components/css.component';
import { HtmlComponent } from '@shared/components/html.component';
import { SafePipe } from '@shared/pipe/safe.pipe';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
@ -240,6 +241,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
JsonContentComponent,
JsFuncComponent,
CssComponent,
HtmlComponent,
FabTriggerDirective,
FabActionsDirective,
FabToolbarComponent,
@ -389,6 +391,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
JsonContentComponent,
JsFuncComponent,
CssComponent,
HtmlComponent,
FabTriggerDirective,
FabActionsDirective,
FabToolbarComponent,

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

@ -3260,6 +3260,10 @@
"sort-settings": "Sort settings",
"nodes-sort-function": "Nodes sort function"
},
"html-card": {
"html": "HTML",
"css": "CSS"
},
"input-widgets": {
"attribute-not-allowed": "Attribute parameter cannot be used in this widget",
"blocked-location": "Geolocation is blocked in your browser",

Loading…
Cancel
Save