Browse Source

UI: Dynamic form implementation. Migration from JSON schema form.

pull/12325/head
Igor Kulikov 2 years ago
parent
commit
9fe5f7199e
  1. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  2. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  3. 3
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html
  4. 3
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  5. 3
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html
  6. 99
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  7. 74
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html
  8. 72
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.scss
  9. 161
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts
  10. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html
  11. 15
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts
  12. 422
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html
  13. 21
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss
  14. 131
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts
  15. 41
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts
  16. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts
  17. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html
  18. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts
  19. 203
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html
  20. 21
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss
  21. 70
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts
  22. 9
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  23. 7
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  24. 4
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  25. 18
      ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html
  26. 1
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html
  27. 4
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts
  28. 10
      ui-ngx/src/app/shared/components/js-func.component.ts
  29. 3
      ui-ngx/src/app/shared/components/string-items-list.component.html
  30. 3
      ui-ngx/src/app/shared/components/string-items-list.component.ts
  31. 46
      ui-ngx/src/app/shared/components/time/datetime.component.html
  32. 60
      ui-ngx/src/app/shared/components/time/datetime.component.scss
  33. 32
      ui-ngx/src/app/shared/components/time/datetime.component.ts
  34. 421
      ui-ngx/src/app/shared/models/dynamic-form.models.ts
  35. 1
      ui-ngx/src/app/shared/models/widget.models.ts
  36. 35
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  37. 10
      ui-ngx/src/form.scss

3
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -109,7 +109,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction; const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources; const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsForm = jsonFormSchemaToFormProperties(rawSettingsSchema); const rawSettingsForm = widgetInfo.typeSettingsForm || widgetInfo.settingsForm;
const settingsForm = rawSettingsForm?.length ? rawSettingsForm : jsonFormSchemaToFormProperties(rawSettingsSchema);
let settingsSchema; let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') { if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {}; settingsSchema = {};

3
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -147,7 +147,8 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction; const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources; const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
const settingsForm = jsonFormSchemaToFormProperties(rawSettingsSchema); const rawSettingsForm = widgetInfo.typeSettingsForm || widgetInfo.settingsForm;
const settingsForm = rawSettingsForm?.length ? rawSettingsForm : jsonFormSchemaToFormProperties(rawSettingsSchema);
let settingsSchema; let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') { if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {}; settingsSchema = {};

3
ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html

@ -31,8 +31,7 @@
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME"> <ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<tb-datetime formControlName="defaultValue" <tb-datetime formControlName="defaultValue"
dateText="filter.date" dateText="{{ 'filter.date' | translate }}"
timeText="filter.time"
required [showLabel]="false"></tb-datetime> required [showLabel]="false"></tb-datetime>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN"> <ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">

3
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html

@ -101,8 +101,7 @@
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.DATE_TIME"> <ng-template [ngSwitchCase]="entityKeyValueTypeEnum.DATE_TIME">
<tb-datetime formControlName="value" <tb-datetime formControlName="value"
dateText="filter.date" dateText="{{ 'filter.date' | translate }}"
timeText="filter.time"
required></tb-datetime> required></tb-datetime>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.BOOLEAN"> <ng-template [ngSwitchCase]="entityKeyValueTypeEnum.BOOLEAN">

3
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html

@ -47,8 +47,7 @@
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME"> <ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value }}</label> <label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value }}</label>
<tb-datetime [formControl]="userInputControl.get('value')" <tb-datetime [formControl]="userInputControl.get('value')"
dateText="filter.date" dateText="{{ 'filter.date' | translate }}"
timeText="filter.time"
required [showLabel]="false"></tb-datetime> required [showLabel]="false"></tb-datetime>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN"> <ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">

99
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -65,7 +65,12 @@ import { catchError, map, take, takeUntil } from 'rxjs/operators';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { MatIconRegistry } from '@angular/material/icon'; import { MatIconRegistry } from '@angular/material/icon';
import { RafService } from '@core/services/raf.service'; import { RafService } from '@core/services/raf.service';
import { defaultFormPropertyValue, FormProperty, FormPropertyType } from '@shared/models/dynamic-form.models'; import {
defaultFormPropertyValue,
defaultPropertyValue,
FormProperty,
FormPropertyType
} from '@shared/models/dynamic-form.models';
export interface ScadaSymbolApi { export interface ScadaSymbolApi {
generateElementId: () => string; generateElementId: () => string;
@ -630,7 +635,9 @@ export class ScadaSymbolObject {
elements.push(element); elements.push(element);
} }
for (const property of this.metadata.properties) { for (const property of this.metadata.properties) {
this.context.properties[property.id] = this.getPropertyValue(property.id); if (property.type !== FormPropertyType.htmlSection) {
this.context.properties[property.id] = this.getPropertyValue(this.metadata.properties, this.settings.properties, property.id);
}
} }
for (const tag of this.metadata.tags) { for (const tag of this.metadata.tags) {
if (tag.actions) { if (tag.actions) {
@ -967,9 +974,8 @@ export class ScadaSymbolObject {
return Array.isArray(element) ? element : [element]; return Array.isArray(element) ? element : [element];
} }
private getProperty(...ids: string[]): FormProperty { private getProperty(properties: FormProperty[], ...ids: string[]): FormProperty {
let found: FormProperty; let found: FormProperty;
let properties = this.metadata.properties;
for (const id of ids) { for (const id of ids) {
if (properties) { if (properties) {
found = properties.find(p => p.id === id); found = properties.find(p => p.id === id);
@ -985,9 +991,9 @@ export class ScadaSymbolObject {
return found; return found;
} }
private getSettingsValue(...ids: string[]): any { private getSettingsValue(settings: {[id: string]: any}, ...ids: string[]): any {
let found: any; let found: any;
let properties = this.settings.properties; let properties = settings;
for (const id of ids) { for (const id of ids) {
if (properties) { if (properties) {
found = properties[id]; found = properties[id];
@ -1003,43 +1009,72 @@ export class ScadaSymbolObject {
return found; return found;
} }
private getPropertyValue(...ids: string[]): any { private getPropertyValue(properties: FormProperty[], settings: {[id: string]: any}, ...ids: string[]): any {
const property = this.getProperty(...ids); const property = this.getProperty(properties, ...ids);
if (property) { if (property) {
if (property.type === FormPropertyType.fieldset) { if (property.type === FormPropertyType.array) {
const arrayValue = [];
if (property.arrayItemType !== FormPropertyType.htmlSection) {
const settingsValue = this.getSettingsValue(settings, ...ids);
if (settingsValue && Array.isArray(settingsValue)) {
for (const settingsElement of settingsValue) {
let value: any;
if (property.arrayItemType === FormPropertyType.fieldset) {
const propertyValue: {[id: string]: any} = {};
for (const childProperty of property.properties) {
if (childProperty.type !== FormPropertyType.htmlSection) {
propertyValue[childProperty.id] = this.getPropertyValue(property.properties, settingsElement, childProperty.id);
}
}
value = propertyValue;
} else {
value = this.convertPropertyValue(property, settingsElement);
}
arrayValue.push(value);
}
}
}
return arrayValue;
} else if (property.type === FormPropertyType.fieldset) {
const propertyValue: {[id: string]: any} = {}; const propertyValue: {[id: string]: any} = {};
for (const childProperty of property.properties) { for (const childProperty of property.properties) {
propertyValue[childProperty.id] = this.getPropertyValue(...ids, childProperty.id); if (childProperty.type !== FormPropertyType.htmlSection) {
propertyValue[childProperty.id] = this.getPropertyValue(properties, settings, ...ids, childProperty.id);
}
} }
return propertyValue; return propertyValue;
} else { } else {
const value = this.getSettingsValue(...ids); const value = this.getSettingsValue(settings, ...ids);
if (isDefinedAndNotNull(value)) { return this.convertPropertyValue(property, value);
if (property.type === FormPropertyType.color_settings) {
return ColorProcessor.fromSettings(value);
} else if (property.type === FormPropertyType.text) {
const result = this.ctx.utilsService.customTranslation(value, value);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
return createLabelFromSubscriptionEntityInfo(entityInfo, result);
}
return value;
} else {
switch (property.type) {
case FormPropertyType.text:
return '';
case FormPropertyType.number:
return 0;
case FormPropertyType.color:
return '#000';
case FormPropertyType.color_settings:
return ColorProcessor.fromSettings(constantColor('#000'));
}
}
} }
} else { } else {
return ''; return '';
} }
} }
private convertPropertyValue(property: FormProperty, value: any): any {
if (isDefinedAndNotNull(value)) {
if (property.type === FormPropertyType.color_settings) {
return ColorProcessor.fromSettings(value);
} else if ([FormPropertyType.text, FormPropertyType.textarea].includes(property.type)) {
const result = this.ctx.utilsService.customTranslation(value, value);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
return createLabelFromSubscriptionEntityInfo(entityInfo, result);
}
return value;
} else {
switch (property.type) {
case FormPropertyType.color_settings:
return ColorProcessor.fromSettings(constantColor('#000'));
case FormPropertyType.font:
case FormPropertyType.units:
case FormPropertyType.icon:
return null;
default:
return defaultPropertyValue(property.type);
}
}
}
} }
const scadaSymbolAnimationId = 'scadaSymbolAnimation'; const scadaSymbolAnimationId = 'scadaSymbolAnimation';

74
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html

@ -0,0 +1,74 @@
<!--
Copyright © 2016-2024 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-dynamic-form-array tb-form-panel stroked" [class.no-padding-right]="!disabled || !propertiesFormArray().controls.length">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<div class="tb-form-panel-title">{{ title }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div *ngIf="propertiesFormArray().controls.length; else noProperties" class="tb-dynamic-form-array-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="propertyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-draggable tb-dynamic-form-array-row"
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;">
<div class="tb-dynamic-form-item-index-container">
<div class="tb-dynamic-form-item-index">{{$index + 1}}</div>
</div>
<tb-dynamic-form class="flex-1"
[properties]="[itemProperty]"
[formControl]="propertyControl"
isArrayItem noPadding noBorder>
</tb-dynamic-form>
<div class="tb-dynamic-form-array-buttons">
<button mat-icon-button
*ngIf="!disabled"
type="button"
(click)="removeProperty($index)"
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button
*ngIf="!disabled"
type="button"
cdkDragHandle
[class.tb-hidden]="!dragEnabled"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
<div *ngIf="!disabled || !propertiesFormArray().controls.length" [class.invisible]="disabled">
<button type="button" mat-stroked-button color="primary" (click)="addProperty()">
<mat-icon>add</mat-icon>
{{ 'action.add' | translate }}
</button>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<ng-template #noProperties>
<span class="tb-prompt flex items-center justify-center">{{ 'dynamic-form.property.no-items' | translate }}</span>
</ng-template>

72
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.scss

@ -0,0 +1,72 @@
/**
* Copyright © 2016-2024 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 "../../../../../../../../../scss/constants";
.tb-dynamic-form-array {
.tb-dynamic-form-array-body {
display: flex;
flex-direction: column;
gap: 12px;
}
}
.tb-dynamic-form-array-row {
background: #fff;
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: flex-start;
.tb-dynamic-form-item-index-container {
width: 36px;
height: 56px;
display: flex;
flex-direction: row;
place-content: flex-start;
align-items: center;
.tb-dynamic-form-item-index {
width: 24px;
height: 24px;
position: relative;
font-weight: 400;
font-size: 16px;
display: flex;
align-items: center;
place-content: center;
color: $tb-primary-color;
&:before {
content: "";
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: $tb-primary-color;
opacity: 0.06;
border-radius: 100%;
}
}
}
.tb-dynamic-form-array-buttons {
display: flex;
flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base {
color: rgba(0, 0, 0, 0.38);
&.tb-hidden {
visibility: hidden;
}
}
}
}

161
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts

@ -0,0 +1,161 @@
///
/// Copyright © 2016-2024 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, ViewEncapsulation } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator
} from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { defaultFormPropertyValue, FormProperty } from '@shared/models/dynamic-form.models';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
@Component({
selector: 'tb-dynamic-form-array',
templateUrl: './dynamic-form-array.component.html',
styleUrls: ['./dynamic-form-array.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DynamicFormArrayComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DynamicFormArrayComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class DynamicFormArrayComponent implements ControlValueAccessor, OnInit, Validator {
@Input()
disabled: boolean;
@Input()
itemProperty: FormProperty;
@Input()
title: string;
propertiesFormGroup: UntypedFormGroup;
get dragEnabled(): boolean {
return !this.disabled && this.propertiesFormArray().controls.length > 1;
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit() {
this.propertiesFormGroup = this.fb.group({
properties: this.fb.array([])
});
this.propertiesFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
const properties: {[id: string]: any}[] = this.propertiesFormGroup.get('properties').value;
const value = properties.map(prop => prop[this.itemProperty.id]);
this.propagateChange(value);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.propertiesFormGroup.disable({emitEvent: false});
} else {
this.propertiesFormGroup.enable({emitEvent: false});
}
}
writeValue(values: any[] | undefined): void {
this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(values || []), {emitEvent: false});
}
public validate(_c: UntypedFormControl) {
const valid = this.propertiesFormGroup.valid;
return valid ? null : {
properties: {
valid: false,
},
};
}
propertyDrop(event: CdkDragDrop<string[]>) {
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray;
const property = propertiesArray.at(event.previousIndex);
propertiesArray.removeAt(event.previousIndex, {emitEvent: false});
propertiesArray.insert(event.currentIndex, property, {emitEvent: true});
}
propertiesFormArray(): UntypedFormArray {
return this.propertiesFormGroup.get('properties') as UntypedFormArray;
}
trackByProperty(_index: number, propertyControl: AbstractControl): any {
return propertyControl;
}
removeProperty(index: number, emitEvent = true) {
(this.propertiesFormGroup.get('properties') as UntypedFormArray).removeAt(index, {emitEvent});
}
addProperty() {
const property = {
[this.itemProperty.id]: defaultFormPropertyValue(this.itemProperty)
};
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray;
const propertyControl = this.fb.control(property, []);
propertiesArray.push(propertyControl);
setTimeout(() => {
propertyControl.updateValueAndValidity();
});
}
private preparePropertiesFormArray(values: any[] | undefined): UntypedFormArray {
const propertiesControls: Array<AbstractControl> = [];
if (values) {
values.forEach((value) => {
const property = {
[this.itemProperty.id]: value
};
propertiesControls.push(this.fb.control(property, []));
});
}
return this.fb.array(propertiesControls);
}
}

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html

@ -28,6 +28,7 @@
[cdkDropListDisabled]="!dragEnabled" [cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="propertyDrop($event)"> (cdkDropListDropped)="propertyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled" <div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled" [class.tb-draggable-form-table-row]="!disabled"
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;"> *ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;">
<tb-dynamic-form-property-row class="flex-1" <tb-dynamic-form-property-row class="flex-1"

15
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts

@ -37,10 +37,9 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { FormProperty, FormPropertyType } from '@shared/models/dynamic-form.models'; import { FormProperty, FormPropertyType, propertyValid } from '@shared/models/dynamic-form.models';
import { import {
DynamicFormPropertyRowComponent, DynamicFormPropertyRowComponent
propertyValid
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component'; } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -136,7 +135,7 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
@ -154,7 +153,7 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI
this.booleanPropertyIds = properties.filter(p => p.type === FormPropertyType.switch).map(p => p.id); this.booleanPropertyIds = properties.filter(p => p.type === FormPropertyType.switch).map(p => p.id);
} }
public validate(c: UntypedFormControl) { public validate(_c: UntypedFormControl) {
this.errorText = ''; this.errorText = '';
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray;
const notUniqueControls = const notUniqueControls =
@ -189,15 +188,15 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI
propertyDrop(event: CdkDragDrop<string[]>) { propertyDrop(event: CdkDragDrop<string[]>) {
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray;
const property = propertiesArray.at(event.previousIndex); const property = propertiesArray.at(event.previousIndex);
propertiesArray.removeAt(event.previousIndex); propertiesArray.removeAt(event.previousIndex, {emitEvent: false});
propertiesArray.insert(event.currentIndex, property); propertiesArray.insert(event.currentIndex, property, {emitEvent: true});
} }
propertiesFormArray(): UntypedFormArray { propertiesFormArray(): UntypedFormArray {
return this.propertiesFormGroup.get('properties') as UntypedFormArray; return this.propertiesFormGroup.get('properties') as UntypedFormArray;
} }
trackByProperty(index: number, propertyControl: AbstractControl): any { trackByProperty(_index: number, propertyControl: AbstractControl): any {
return propertyControl; return propertyControl;
} }

422
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html

@ -46,7 +46,139 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="propertyFormGroup.get('type').value === FormPropertyType.number" class="tb-form-row space-between"> <div *ngIf="isArray" class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dynamic-form.property.array-item</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.item-type</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="arrayItemType">
<mat-option *ngFor="let type of arrayItemFormPropertyTypes" [value]="type">
{{ formPropertyTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.item-name</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="arrayItemName" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<ng-container *ngTemplateOutlet="itemSettings">
</ng-container>
</div>
<ng-container *ngIf="!isArray">
<ng-container *ngTemplateOutlet="itemSettings">
</ng-container>
</ng-container>
<div *ngIf="![FormPropertyType.fieldset, FormPropertyType.htmlSection, FormPropertyType.array].includes(propertyFormGroup.get('type').value)" class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="required">
{{ 'dynamic-form.property.value-required' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ 'dynamic-form.property.advanced-ui-settings' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<ng-container *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)">
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.sub-label</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="divider">
{{ 'dynamic-form.property.vertical-divider-after' | translate }}
</mat-slide-toggle>
</div>
</ng-container>
<div *ngIf="[FormPropertyType.number, FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)"
class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.input-field-suffix</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.htmlSection" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.disable-on-property</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="null"></mat-option>
<mat-option *ngFor="let prop of booleanPropertyIds" [value]="prop">
{{ prop }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<tb-js-func
class="flex-1"
formControlName="condition"
[functionArgs]="['property', 'model']"
functionTitle="{{ 'dynamic-form.property.display-condition-function' | translate }}">
</tb-js-func>
</div>
<tb-string-items-list *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)"
class="flex-1"
fieldClass="flex"
floatLabel="always"
label="{{ 'dynamic-form.property.property-row-classes' | translate }}"
placeholder="{{ 'widget-config.set' | translate }}"
[predefinedValues]="formPropertyRowClasses"
formControlName="rowClass"
appearance="outline"
subscriptSizing="dynamic">
</tb-string-items-list>
<tb-string-items-list *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value) && FormPropertyType.switch !== propertyFormGroup.get('type').value"
class="flex-1"
fieldClass="flex"
floatLabel="always"
label="{{ 'dynamic-form.property.property-field-classes' | translate }}"
placeholder="{{ 'widget-config.set' | translate }}"
[predefinedValues]="formPropertyFieldClasses"
formControlName="fieldClass"
appearance="outline"
subscriptSizing="dynamic">
</tb-string-items-list>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div class="tb-dynamic-form-property-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ (disabled ? 'action.close' : 'action.cancel') | translate }}
</button>
<button *ngIf="!disabled"
mat-raised-button
color="primary"
type="button"
(click)="applyPropertySettings()"
[disabled]="propertyFormGroup.invalid || !propertyFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.apply') | translate }}
</button>
</div>
</div>
<ng-template #itemSettings>
<ng-container [formGroup]="propertyFormGroup">
<div *ngIf="propertyItemType === FormPropertyType.textarea" class="tb-form-row space-between">
<div translate>dynamic-form.property.textarea-rows</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="rows" min="1" step="1"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="propertyItemType === FormPropertyType.number" class="tb-form-row space-between">
<div translate>dynamic-form.property.number-settings</div> <div translate>dynamic-form.property.number-settings</div>
<div class="flex flex-1 flex-row items-center justify-end gap-2"> <div class="flex flex-1 flex-row items-center justify-end gap-2">
<div class="tb-small-label" translate>dynamic-form.property.min</div> <div class="tb-small-label" translate>dynamic-form.property.min</div>
@ -68,7 +200,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="propertyFormGroup.get('type').value === FormPropertyType.fieldset" class="tb-form-panel stroked"> <div *ngIf="propertyItemType === FormPropertyType.fieldset" class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded> <mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap"> <mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title> <mat-panel-title>
@ -81,20 +213,60 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
<ng-container *ngIf="propertyFormGroup.get('type').value === FormPropertyType.select"> <div *ngIf="propertyItemType === FormPropertyType.htmlSection" class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ 'dynamic-form.property.html-section-settings' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<tb-html
class="flex-1"
formControlName="htmlContent"
label="{{ 'dynamic-form.property.html-section-content' | translate }}">
</tb-html>
</div>
<tb-string-items-list class="flex-1"
fieldClass="flex"
floatLabel="always"
label="{{ 'dynamic-form.property.html-section-classes' | translate }}"
formControlName="htmlClassList"
appearance="outline"
subscriptSizing="dynamic">
</tb-string-items-list>
</ng-template>
</mat-expansion-panel>
</div>
<ng-container *ngIf="[FormPropertyType.select, FormPropertyType.radios].includes(propertyItemType)">
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded> <mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap"> <mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title> <mat-panel-title>
{{ 'dynamic-form.property.select-options' | translate }} {{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="tb-form-row space-between"> <div *ngIf="propertyItemType === FormPropertyType.radios" class="tb-form-row space-between">
<div translate>dynamic-form.property.buttons-direction</div>
<mat-chip-listbox class="center-stretch" formControlName="direction">
<mat-chip-option [selectable]="propertyFormGroup.get('direction').value !== 'column'"
value="column">{{ 'dynamic-form.property.direction-column' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="propertyFormGroup.get('direction').value !== 'row'"
value="row">{{ 'dynamic-form.property.direction-row' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div *ngIf="propertyItemType === FormPropertyType.select" class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="multiple"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="multiple">
{{ 'dynamic-form.property.enable-multiple-select' | translate }} {{ 'dynamic-form.property.enable-multiple-select' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div *ngIf="propertyItemType === FormPropertyType.select && !propertyFormGroup.get('multiple').value" class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="allowEmptyOption">
{{ 'dynamic-form.property.allow-empty-select-option' | translate }}
</mat-slide-toggle>
</div>
<tb-dynamic-form-select-items <tb-dynamic-form-select-items
formControlName="items"> formControlName="items">
</tb-dynamic-form-select-items> </tb-dynamic-form-select-items>
@ -102,23 +274,113 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
</ng-container> </ng-container>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between"> <div *ngIf="[FormPropertyType.javascript, FormPropertyType.markdown].includes(propertyItemType)" class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.help-id</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="helpId" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<ng-container *ngIf="propertyItemType === FormPropertyType.datetime">
<div class="tb-form-row space-between">
<div translate>dynamic-form.property.datetime-type</div>
<mat-chip-listbox class="center-stretch" formControlName="dateTimeType">
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'date'"
value="date">{{ 'dynamic-form.property.datetime-type-date' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'time'"
value="time">{{ 'dynamic-form.property.datetime-type-time' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'datetime'"
value="datetime">{{ 'dynamic-form.property.datetime-type-datetime' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="allowClear">
{{ 'dynamic-form.property.enable-clear-button' | translate }}
</mat-slide-toggle>
</div>
</ng-container>
<div *ngIf="propertyItemType === FormPropertyType.textarea" class="tb-form-row no-border no-padding">
<mat-form-field class="flex"
floatLabel="always"
appearance="outline" subscriptSizing="dynamic">
<mat-label translate>dynamic-form.property.default-value</mat-label>
<textarea matInput
[rows]="propertyFormGroup.get('rows').value || 2"
formControlName="default" placeholder="{{ 'widget-config.set' | translate }}"></textarea>
</mat-form-field>
</div>
<div *ngIf="propertyItemType === FormPropertyType.javascript" class="tb-form-row" >
<tb-js-func
class="flex-1"
disableUndefinedCheck
formControlName="default"
[helpId]="propertyFormGroup.get('helpId').value"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-js-func>
</div>
<div *ngIf="propertyItemType === FormPropertyType.json" class="tb-form-row" >
<tb-json-content
class="flex-1"
formControlName="default"
[contentType]="ContentType.JSON"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-json-content>
</div>
<div *ngIf="propertyItemType === FormPropertyType.html" class="tb-form-row" >
<tb-html
class="flex-1"
formControlName="default"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-html>
</div>
<div *ngIf="propertyItemType === FormPropertyType.css" class="tb-form-row" >
<tb-css
class="flex-1"
formControlName="default"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-css>
</div>
<div *ngIf="propertyItemType === FormPropertyType.markdown" class="tb-form-row" >
<tb-markdown-editor
class="flex-1"
formControlName="default"
[helpId]="propertyFormGroup.get('helpId').value"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-markdown-editor>
</div>
<div *ngIf="propertyItemType === FormPropertyType.image && !isArray" class="tb-form-row" >
<tb-gallery-image-input
class="flex-1"
formControlName="default"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-gallery-image-input>
</div>
<div *ngIf="propertyItemType === FormPropertyType.radios"
class="tb-form-row tb-radios-property" [class.direction-column]="propertyFormGroup.get('direction').value === 'column'">
<div tbTruncateWithTooltip translate class="fixed-title-height">dynamic-form.property.default-value</div>
<mat-radio-group class="flex flex-1 flex-wrap" [class.flex-col]="propertyFormGroup.get('direction').value === 'column'"
formControlName="default">
<mat-radio-button *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value">
<div tbTruncateWithTooltip>{{ item.label | customTranslate }}</div>
</mat-radio-button>
</mat-radio-group>
</div>
<div *ngIf="isPropertyTypeAllowedForRow(propertyItemType)" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.default-value</div> <div class="fixed-title-width" translate>dynamic-form.property.default-value</div>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === FormPropertyType.text" <mat-form-field *ngIf="[FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)"
class="flex" class="flex"
appearance="outline" subscriptSizing="dynamic"> appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="default" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput [type]="propertyFormGroup.get('type').value" formControlName="default" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<tb-color-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.color" <tb-color-input *ngIf="propertyItemType === FormPropertyType.color"
asBoxInput asBoxInput
colorClearButton colorClearButton
formControlName="default"> formControlName="default">
</tb-color-input> </tb-color-input>
<tb-color-settings *ngIf="propertyFormGroup.get('type').value === FormPropertyType.color_settings" <tb-color-settings *ngIf="propertyItemType === FormPropertyType.color_settings"
formControlName="default" formControlName="default"
settingsKey="{{ propertyFormGroup.get('id').value }}"> settingsKey="{{ propertyFormGroup.get('id').value }}">
</tb-color-settings> </tb-color-settings>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === FormPropertyType.number" <mat-form-field *ngIf="propertyItemType === FormPropertyType.number"
appearance="outline" appearance="outline"
class="number" class="number"
subscriptSizing="dynamic"> subscriptSizing="dynamic">
@ -128,23 +390,24 @@
[step]="propertyFormGroup.get('step').value" [step]="propertyFormGroup.get('step').value"
type="number" placeholder="{{ 'widget-config.set' | translate }}"> type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<tb-font-settings *ngIf="propertyFormGroup.get('type').value === FormPropertyType.font" <tb-font-settings *ngIf="propertyItemType === FormPropertyType.font"
formControlName="default" formControlName="default"
clearButton clearButton
disabledLineHeight> disabledLineHeight>
</tb-font-settings> </tb-font-settings>
<tb-unit-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.units" <tb-unit-input *ngIf="propertyItemType === FormPropertyType.units"
formControlName="default"></tb-unit-input> formControlName="default"></tb-unit-input>
<tb-value-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.switch" <tb-value-input *ngIf="propertyItemType === FormPropertyType.switch"
[valueType]="ValueType.BOOLEAN" [valueType]="ValueType.BOOLEAN"
formControlName="default"></tb-value-input> formControlName="default"></tb-value-input>
<tb-material-icon-select *ngIf="propertyFormGroup.get('type').value === FormPropertyType.icon" <tb-material-icon-select *ngIf="propertyItemType === FormPropertyType.icon"
asBoxInput asBoxInput
formControlName="default"> formControlName="default">
</tb-material-icon-select> </tb-material-icon-select>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === FormPropertyType.select" class="medium-width" <mat-form-field *ngIf="propertyItemType === FormPropertyType.select" class="medium-width"
appearance="outline" subscriptSizing="dynamic"> appearance="outline" subscriptSizing="dynamic">
<mat-select *ngIf="!propertyFormGroup.get('multiple').value" formControlName="default"> <mat-select *ngIf="!propertyFormGroup.get('multiple').value" formControlName="default">
<mat-option *ngIf="propertyFormGroup.get('allowEmptyOption').value" [value]="null"></mat-option>
<mat-option *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value"> <mat-option *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value">
{{ item.label | customTranslate }} {{ item.label | customTranslate }}
</mat-option> </mat-option>
@ -155,97 +418,38 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<ng-container *ngIf="propertyItemType === FormPropertyType.datetime">
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'date'"
class="flex-1"
showLabel="false"
fieldClass="flex"
[allowClear]="propertyFormGroup.get('allowClear').value"
appearance="outline"
subscriptSizing="dynamic"
type="date"
formControlName="default"
></tb-datetime>
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'time'"
class="flex-1"
showLabel="false"
fieldClass="flex"
[allowClear]="propertyFormGroup.get('allowClear').value"
appearance="outline"
subscriptSizing="dynamic"
type="time"
formControlName="default"
></tb-datetime>
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'datetime'"
class="flex-1"
showLabel="false"
fieldClass="flex"
[allowClear]="propertyFormGroup.get('allowClear').value"
appearance="outline"
subscriptSizing="dynamic"
type="datetime"
formControlName="default"
></tb-datetime>
</ng-container>
</div> </div>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between"> </ng-container>
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="required"> </ng-template>
{{ 'dynamic-form.property.value-required' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ 'dynamic-form.property.advanced-ui-settings' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.sub-label</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="divider">
{{ 'dynamic-form.property.vertical-divider-after' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="[FormPropertyType.number, FormPropertyType.text].includes(propertyFormGroup.get('type').value)"
class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.input-field-suffix</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.disable-on-property</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="null"></mat-option>
<mat-option *ngFor="let prop of booleanPropertyIds" [value]="prop">
{{ prop }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<tb-js-func
class="flex-1"
formControlName="condition"
[functionArgs]="['property', 'model']"
functionTitle="{{ 'dynamic-form.property.display-condition-function' | translate }}">
</tb-js-func>
</div>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.property-row-classes</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="rowClass" multiple placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let clazz of formPropertyRowClasses" [value]="clazz">
{{ clazz }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="![FormPropertyType.fieldset, FormPropertyType.switch].includes(propertyFormGroup.get('type').value)" class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.property-field-classes</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="fieldClass" multiple placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let clazz of formPropertyFieldClasses" [value]="clazz">
{{ clazz }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div class="tb-dynamic-form-property-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ (disabled ? 'action.close' : 'action.cancel') | translate }}
</button>
<button *ngIf="!disabled"
mat-raised-button
color="primary"
type="button"
(click)="applyPropertySettings()"
[disabled]="propertyFormGroup.invalid || !propertyFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.apply') | translate }}
</button>
</div>
</div>

21
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss

@ -46,4 +46,25 @@
justify-content: flex-end; justify-content: flex-end;
align-items: flex-end; align-items: flex-end;
} }
.tb-form-row.tb-radios-property {
&.direction-column {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.mat-mdc-radio-group {
overflow: hidden;
.mat-mdc-radio-button {
overflow: hidden;
.mdc-form-field {
overflow: hidden;
width: 100%;
.mdc-label {
overflow: hidden;
}
}
}
}
}
} }

131
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts

@ -17,20 +17,21 @@
import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ValueType } from '@shared/models/constants'; import { ContentType, ValueType } from '@shared/models/constants';
import { import {
defaultPropertyValue,
FormProperty, FormProperty,
formPropertyFieldClasses, formPropertyFieldClasses,
formPropertyRowClasses, formPropertyRowClasses,
FormPropertyType, FormPropertyType,
formPropertyTypes, formPropertyTypes,
formPropertyTypeTranslations, formPropertyTypeTranslations,
FormSelectItem FormSelectItem,
isPropertyTypeAllowedForRow
} from '@shared/models/dynamic-form.models'; } from '@shared/models/dynamic-form.models';
import {
defaultPropertyValue
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isUndefinedOrNull } from '@core/utils';
import { StringItemsOption } from '@shared/components/string-items-list.component';
@Component({ @Component({
selector: 'tb-dynamic-form-property-panel', selector: 'tb-dynamic-form-property-panel',
@ -44,12 +45,29 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
FormPropertyType = FormPropertyType; FormPropertyType = FormPropertyType;
ContentType = ContentType;
formPropertyTypes = formPropertyTypes; formPropertyTypes = formPropertyTypes;
arrayItemFormPropertyTypes = formPropertyTypes.filter(t => t !== FormPropertyType.array);
formPropertyTypeTranslations = formPropertyTypeTranslations; formPropertyTypeTranslations = formPropertyTypeTranslations;
formPropertyRowClasses = formPropertyRowClasses; formPropertyRowClasses: StringItemsOption[] = formPropertyRowClasses.map(clazz => ({name: clazz, value: clazz}));
formPropertyFieldClasses: StringItemsOption[] = formPropertyFieldClasses.map(clazz => ({name: clazz, value: clazz}));
formPropertyFieldClasses = formPropertyFieldClasses; isPropertyTypeAllowedForRow = isPropertyTypeAllowedForRow;
get propertyItemType(): FormPropertyType | any {
if (this.isArray) {
return this.propertyFormGroup.get('arrayItemType').value;
} else {
return this.propertyFormGroup.get('type').value;
}
}
get isArray(): boolean {
return this.propertyFormGroup.get('type').value === FormPropertyType.array;
}
@Input() @Input()
isAdd = false; isAdd = false;
@ -81,13 +99,15 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.panelTitle = this.isAdd ? 'dynamic-form.property.add-property' : 'dynamic-form.property.property-settings'; this.panelTitle = this.isAdd ? 'dynamic-form.property.add-property' : 'dynamic-form.property.property-settings';
this.propertyType = this.property.type; this.propertyType = this.property.type === FormPropertyType.array ? this.property.arrayItemType : this.property.type;
this.propertyFormGroup = this.fb.group( this.propertyFormGroup = this.fb.group(
{ {
id: [this.property.id, [Validators.required]], id: [this.property.id, [Validators.required]],
name: [this.property.name, [Validators.required]], name: [this.property.name, [Validators.required]],
group: [this.property.group, []], group: [this.property.group, []],
type: [this.property.type, [Validators.required]], type: [this.property.type, [Validators.required]],
arrayItemType: [this.property.arrayItemType, [Validators.required]],
arrayItemName: [this.property.arrayItemName, []],
default: [this.property.default, []], default: [this.property.default, []],
required: [this.property.required, []], required: [this.property.required, []],
subLabel: [this.property.subLabel, []], subLabel: [this.property.subLabel, []],
@ -95,14 +115,22 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
fieldSuffix: [this.property.fieldSuffix, []], fieldSuffix: [this.property.fieldSuffix, []],
disableOnProperty: [this.property.disableOnProperty, []], disableOnProperty: [this.property.disableOnProperty, []],
condition: [this.property.condition, []], condition: [this.property.condition, []],
rowClass: [(this.property.rowClass || '').split(' '), []], rowClass: [this.property.rowClass ? this.property.rowClass.split(' ') : [], []],
fieldClass: [(this.property.fieldClass || '').split(' '), []], fieldClass: [this.property.fieldClass ? this.property.fieldClass.split(' ') : [], []],
rows: [this.property.rows, [Validators.min(1)]],
min: [this.property.min, []], min: [this.property.min, []],
max: [this.property.max, []], max: [this.property.max, []],
step: [this.property.step, [Validators.min(0)]], step: [this.property.step, [Validators.min(0)]],
properties: [this.property.properties, []], properties: [this.property.properties, []],
multiple: [this.property.multiple, []], multiple: [this.property.multiple, []],
items: [this.property.items, []] allowEmptyOption: [this.property.allowEmptyOption, []],
items: [this.property.items, []],
helpId: [this.property.helpId, []],
direction: [this.property.direction || 'column', []],
allowClear: [this.property.allowClear || true, []],
dateTimeType: [this.property.dateTimeType || 'datetime', []],
htmlContent: [this.property.htmlContent || '', []],
htmlClassList: [this.property.htmlClassList || [], []]
} }
); );
if (this.disabled) { if (this.disabled) {
@ -113,7 +141,11 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
).subscribe(() => { ).subscribe(() => {
this.updateValidators(); this.updateValidators();
}); });
this.propertyFormGroup.get('arrayItemType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateValidators();
});
this.propertyFormGroup.get('items').valueChanges.pipe( this.propertyFormGroup.get('items').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(() => { ).subscribe(() => {
@ -140,7 +172,19 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
} }
private updateValidators() { private updateValidators() {
const type: FormPropertyType = this.propertyFormGroup.get('type').value; if (this.isArray) {
this.propertyFormGroup.get('arrayItemType').enable({emitEvent: false});
this.propertyFormGroup.get('arrayItemName').enable({emitEvent: false});
} else {
this.propertyFormGroup.get('arrayItemType').disable({emitEvent: false});
this.propertyFormGroup.get('arrayItemName').disable({emitEvent: false});
}
const type = this.propertyItemType;
if (type === FormPropertyType.textarea) {
this.propertyFormGroup.get('rows').enable({emitEvent: false});
} else {
this.propertyFormGroup.get('rows').disable({emitEvent: false});
}
if (type === FormPropertyType.number) { if (type === FormPropertyType.number) {
this.propertyFormGroup.get('min').enable({emitEvent: false}); this.propertyFormGroup.get('min').enable({emitEvent: false});
this.propertyFormGroup.get('max').enable({emitEvent: false}); this.propertyFormGroup.get('max').enable({emitEvent: false});
@ -157,27 +201,73 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
this.propertyFormGroup.get('default').enable({emitEvent: false}); this.propertyFormGroup.get('default').enable({emitEvent: false});
this.propertyFormGroup.get('properties').disable({emitEvent: false}); this.propertyFormGroup.get('properties').disable({emitEvent: false});
} }
if (type === FormPropertyType.select) { if ([FormPropertyType.select, FormPropertyType.radios].includes(type)) {
this.propertyFormGroup.get('multiple').enable({emitEvent: false});
this.propertyFormGroup.get('items').enable({emitEvent: false}); this.propertyFormGroup.get('items').enable({emitEvent: false});
if (type === FormPropertyType.select) {
this.propertyFormGroup.get('multiple').enable({emitEvent: false});
const multiple: boolean = this.propertyFormGroup.get('multiple').value;
if (multiple) {
this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false});
} else {
this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false});
}
}
} else { } else {
this.propertyFormGroup.get('multiple').disable({emitEvent: false}); this.propertyFormGroup.get('multiple').disable({emitEvent: false});
this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false});
this.propertyFormGroup.get('items').disable({emitEvent: false}); this.propertyFormGroup.get('items').disable({emitEvent: false});
} }
if (type === FormPropertyType.datetime) {
this.propertyFormGroup.get('allowClear').enable({emitEvent: false});
this.propertyFormGroup.get('dateTimeType').enable({emitEvent: false});
} else {
this.propertyFormGroup.get('allowClear').disable({emitEvent: false});
this.propertyFormGroup.get('dateTimeType').disable({emitEvent: false});
}
if (type === FormPropertyType.htmlSection) {
this.propertyFormGroup.get('htmlContent').enable({emitEvent: false});
this.propertyFormGroup.get('htmlClassList').enable({emitEvent: false});
} else {
this.propertyFormGroup.get('htmlContent').disable({emitEvent: false});
this.propertyFormGroup.get('htmlClassList').disable({emitEvent: false});
}
if ([FormPropertyType.javascript, FormPropertyType.markdown].includes(type)) {
this.propertyFormGroup.get('helpId').enable({emitEvent: false});
} else {
this.propertyFormGroup.get('helpId').disable({emitEvent: false});
}
if (this.propertyType !== type) { if (this.propertyType !== type) {
const defaultValue = defaultPropertyValue(type); const defaultValue = defaultPropertyValue(type);
this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false}); this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false});
this.propertyType = type; this.propertyType = type;
if (type === FormPropertyType.textarea) {
if (isUndefinedOrNull(this.propertyFormGroup.get('rows').value)) {
this.propertyFormGroup.get('rows').patchValue(2, {emitEvent: false});
}
}
if (type === FormPropertyType.radios) {
if (isUndefinedOrNull(this.propertyFormGroup.get('direction').value)) {
this.propertyFormGroup.get('direction').patchValue('column', {emitEvent: false});
}
}
if (type === FormPropertyType.datetime) {
if (isUndefinedOrNull(this.propertyFormGroup.get('dateTimeType').value)) {
this.propertyFormGroup.get('dateTimeType').patchValue('datetime', {emitEvent: false});
}
if (isUndefinedOrNull(this.propertyFormGroup.get('allowClear').value)) {
this.propertyFormGroup.get('allowClear').patchValue(true, {emitEvent: false});
}
}
} }
} }
private onSelectItemsChange() { private onSelectItemsChange() {
const type: FormPropertyType = this.propertyFormGroup.get('type').value; const type = this.propertyItemType;
const multiple: boolean = this.propertyFormGroup.get('multiple').value; const multiple: boolean = this.propertyFormGroup.get('multiple').value;
const defaultValue: any = this.propertyFormGroup.get('default').value; const defaultValue: any = this.propertyFormGroup.get('default').value;
const items: FormSelectItem[] = this.propertyFormGroup.get('items').value; const items: FormSelectItem[] = this.propertyFormGroup.get('items').value;
if (defaultValue && type === FormPropertyType.select) { if (defaultValue && [FormPropertyType.select, FormPropertyType.radios].includes(type)) {
if (multiple) { if (multiple && FormPropertyType.select === type) {
let targetValue: any[] = defaultValue; let targetValue: any[] = defaultValue;
targetValue = targetValue.filter(valItem => !!items.find(item => item.value === valItem)); targetValue = targetValue.filter(valItem => !!items.find(item => item.value === valItem));
this.propertyFormGroup.get('default').patchValue(targetValue, {emitEvent: false}); this.propertyFormGroup.get('default').patchValue(targetValue, {emitEvent: false});
@ -190,7 +280,7 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
} }
private onMultipleSelectChange() { private onMultipleSelectChange() {
const type: FormPropertyType = this.propertyFormGroup.get('type').value; const type = this.propertyItemType;
const multiple: boolean = this.propertyFormGroup.get('multiple').value; const multiple: boolean = this.propertyFormGroup.get('multiple').value;
const defaultValue: any = this.propertyFormGroup.get('default').value; const defaultValue: any = this.propertyFormGroup.get('default').value;
if (type === FormPropertyType.select) { if (type === FormPropertyType.select) {
@ -199,6 +289,8 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
const newVal = [defaultValue]; const newVal = [defaultValue];
this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false}); this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false});
} }
this.propertyFormGroup.get('allowEmptyOption').patchValue(false, {emitEvent: false});
this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false});
} else { } else {
if (defaultValue && Array.isArray(defaultValue)) { if (defaultValue && Array.isArray(defaultValue)) {
const newVal = defaultValue.length ? defaultValue[0] : null; const newVal = defaultValue.length ? defaultValue[0] : null;
@ -206,6 +298,7 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false}); this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false});
}); });
} }
this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false});
} }
} }
} }

41
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts

@ -42,12 +42,12 @@ import {
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { constantColor, Font } from '@shared/models/widget-settings.models';
import { import {
defaultPropertyValue,
FormProperty, FormProperty,
FormPropertyType, FormPropertyType,
formPropertyTypes, formPropertyTypes,
formPropertyTypeTranslations formPropertyTypeTranslations, propertyValid
} from '@shared/models/dynamic-form.models'; } from '@shared/models/dynamic-form.models';
import { import {
DynamicFormPropertiesComponent DynamicFormPropertiesComponent
@ -57,39 +57,6 @@ import {
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component'; } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export const propertyValid = (property: FormProperty): boolean => !(!property.id || !property.name || !property.type);
export const defaultPropertyValue = (type: FormPropertyType): any => {
switch (type) {
case FormPropertyType.text:
return '';
case FormPropertyType.number:
return 0;
case FormPropertyType.switch:
return false;
case FormPropertyType.color:
return '#000';
case FormPropertyType.color_settings:
return constantColor('#000');
case FormPropertyType.font:
return {
size: 12,
sizeUnit: 'px',
family: 'Roboto',
weight: 'normal',
style: 'normal',
lineHeight: '1'
} as Font;
case FormPropertyType.units:
return '';
case FormPropertyType.icon:
return 'star';
case FormPropertyType.fieldset:
case FormPropertyType.select:
return null;
}
};
@Component({ @Component({
selector: 'tb-dynamic-form-property-row', selector: 'tb-dynamic-form-property-row',
templateUrl: './dynamic-form-property-row.component.html', templateUrl: './dynamic-form-property-row.component.html',
@ -168,7 +135,7 @@ export class DynamicFormPropertyRowComponent implements ControlValueAccessor, On
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
@ -242,7 +209,7 @@ export class DynamicFormPropertyRowComponent implements ControlValueAccessor, On
this.editProperty(null, this.editButton, true, onCanceled); this.editProperty(null, this.editButton, true, onCanceled);
} }
public validate(c: UntypedFormControl) { public validate(_c: UntypedFormControl) {
const idControl = this.propertyRowFormGroup.get('id'); const idControl = this.propertyRowFormGroup.get('id');
if (idControl.hasError('propertyIdNotUnique')) { if (idControl.hasError('propertyIdNotUnique')) {
idControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); idControl.updateValueAndValidity({onlySelf: false, emitEvent: false});

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts

@ -40,7 +40,6 @@ import { FormSelectItem } from '@shared/models/dynamic-form.models';
import { import {
DynamicFormSelectItemsComponent DynamicFormSelectItemsComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component'; } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component';
import { TimeSeriesChartStateSourceType } from '@home/components/widget/lib/chart/time-series-chart.models';
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -105,7 +104,7 @@ export class DynamicFormSelectItemRowComponent implements ControlValueAccessor,
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
@ -128,7 +127,7 @@ export class DynamicFormSelectItemRowComponent implements ControlValueAccessor,
this.cd.markForCheck(); this.cd.markForCheck();
} }
public validate(c: UntypedFormControl) { public validate(_c: UntypedFormControl) {
const valueControl = this.selectItemRowFormGroup.get('value'); const valueControl = this.selectItemRowFormGroup.get('value');
if (valueControl.hasError('itemValueNotUnique')) { if (valueControl.hasError('itemValueNotUnique')) {
valueControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); valueControl.updateValueAndValidity({onlySelf: false, emitEvent: false});

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html

@ -27,6 +27,7 @@
[cdkDropListDisabled]="!dragEnabled" [cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="selectItemDrop($event)"> (cdkDropListDropped)="selectItemDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled" <div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled" [class.tb-draggable-form-table-row]="!disabled"
*ngFor="let selectItemControl of selectItemsFormArray().controls; trackBy: trackBySelectItem; let $index = index;"> *ngFor="let selectItemControl of selectItemsFormArray().controls; trackBy: trackBySelectItem; let $index = index;">
<tb-dynamic-form-select-item-row class="flex-1" <tb-dynamic-form-select-item-row class="flex-1"

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts

@ -109,7 +109,7 @@ export class DynamicFormSelectItemsComponent implements ControlValueAccessor, On
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
@ -126,7 +126,7 @@ export class DynamicFormSelectItemsComponent implements ControlValueAccessor, On
this.selectItemsFormGroup.setControl('selectItems', this.prepareSelectItemsFormArray(items), {emitEvent: false}); this.selectItemsFormGroup.setControl('selectItems', this.prepareSelectItemsFormArray(items), {emitEvent: false});
} }
public validate(c: UntypedFormControl) { public validate(_c: UntypedFormControl) {
this.errorText = ''; this.errorText = '';
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
const notUniqueControls = const notUniqueControls =
@ -166,15 +166,15 @@ export class DynamicFormSelectItemsComponent implements ControlValueAccessor, On
selectItemDrop(event: CdkDragDrop<string[]>) { selectItemDrop(event: CdkDragDrop<string[]>) {
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
const item = itemsArray.at(event.previousIndex); const item = itemsArray.at(event.previousIndex);
itemsArray.removeAt(event.previousIndex); itemsArray.removeAt(event.previousIndex, {emitEvent: false});
itemsArray.insert(event.currentIndex, item); itemsArray.insert(event.currentIndex, item, {emitEvent: true});
} }
selectItemsFormArray(): UntypedFormArray { selectItemsFormArray(): UntypedFormArray {
return this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; return this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
} }
trackBySelectItem(index: number, selectItemControl: AbstractControl): any { trackBySelectItem(_index: number, selectItemControl: AbstractControl): any {
return selectItemControl; return selectItemControl;
} }

203
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html

@ -15,7 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<div [class.!hidden]="!propertyGroups?.length && !propertiesContent.children?.length" class="tb-form-panel" [class.stroked]="stroked" <div [class.!hidden]="!propertyGroups?.length && !propertiesContent.children?.length" class="tb-form-panel"
[class.stroked]="stroked" [class.no-padding]="noPadding" [class.no-border]="noBorder"
[formGroup]="propertiesFormGroup"> [formGroup]="propertiesFormGroup">
<div *ngIf="title" class="tb-form-panel-title">{{ title }}</div> <div *ngIf="title" class="tb-form-panel-title">{{ title }}</div>
<ng-container #propertiesContent><ng-content select=".tb-properties-content"></ng-content></ng-container> <ng-container #propertiesContent><ng-content select=".tb-properties-content"></ng-content></ng-container>
@ -42,19 +43,50 @@
<ng-template #propertyContainersTpl let-propertyContainers="propertyContainers"> <ng-template #propertyContainersTpl let-propertyContainers="propertyContainers">
<ng-container *ngFor="let propertyContainer of propertyContainers" [formGroup]="propertiesFormGroup"> <ng-container *ngFor="let propertyContainer of propertyContainers" [formGroup]="propertiesFormGroup">
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.row && propertyContainer.visible"> <ng-container *ngIf="propertyContainer.visible">
<ng-container *ngTemplateOutlet="propertyRowTpl;context:{propertyRow: propertyContainer}"> <ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.field">
<ng-container *ngTemplateOutlet="propertyFieldTpl;context:{property: propertyContainer.property}">
</ng-container>
</ng-container>
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.row">
<ng-container *ngTemplateOutlet="propertyRowTpl;context:{propertyRow: propertyContainer}">
</ng-container>
</ng-container>
<tb-dynamic-form *ngIf="propertyContainer.type === FormPropertyContainerType.fieldset"
formControlName="{{ propertyContainer.property.id }}"
title="{{ propertyContainer.label | customTranslate }}"
stroked
[properties]="propertyContainer.properties">
</tb-dynamic-form>
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.array">
<div *ngIf="propertyContainer.arrayItemProperty.type === FormPropertyType.image" class="tb-form-row flex-1">
<tb-multiple-gallery-image-input
class="flex-1"
label="{{ propertyContainer.property.name | customTranslate }}"
formControlName="{{ propertyContainer.property.id }}"
[required]="propertyContainer.property.required">
</tb-multiple-gallery-image-input>
</div>
<tb-dynamic-form-array *ngIf="propertyContainer.arrayItemProperty.type !== FormPropertyType.image"
formControlName="{{ propertyContainer.property.id }}"
[itemProperty]="propertyContainer.arrayItemProperty"
title="{{ propertyContainer.label | customTranslate }}">
</tb-dynamic-form-array>
</ng-container>
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.htmlSection">
<div [innerHTML]="propertyContainer.safeHtml" [class]="propertyContainer.htmlClass"></div>
</ng-container> </ng-container>
</ng-container> </ng-container>
<tb-dynamic-form *ngIf="propertyContainer.type === FormPropertyContainerType.fieldset && propertyContainer.visible"
formControlName="{{ propertyContainer.property.id }}"
title="{{ propertyContainer.label | customTranslate }}"
stroked
[properties]="propertyContainer.properties">
</tb-dynamic-form>
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-template #propertyFieldTpl let-property="property">
<div [formGroup]="propertiesFormGroup" class="tb-form-row no-border no-padding tb-standard-fields">
<ng-container *ngTemplateOutlet="inputFieldTpl;context:{property: property, singleField: true}">
</ng-container>
</div>
</ng-template>
<ng-template #propertyRowTpl let-propertyRow="propertyRow"> <ng-template #propertyRowTpl let-propertyRow="propertyRow">
<div [formGroup]="propertiesFormGroup" class="tb-form-row space-between flex-wrap overflow-auto" [class]="propertyRow.rowClass"> <div [formGroup]="propertiesFormGroup" class="tb-form-row space-between flex-wrap overflow-auto" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch && propertyRow.switch.visible" <mat-slide-toggle *ngIf="propertyRow.switch && propertyRow.switch.visible"
@ -62,21 +94,14 @@
{{ propertyRow.label | customTranslate }} {{ propertyRow.label | customTranslate }}
</mat-slide-toggle> </mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">{{ propertyRow.label | customTranslate }}</div> <div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">{{ propertyRow.label | customTranslate }}</div>
<div *ngIf="propertyRow.properties.length" class="tb-flex row flex-end align-center"> <div *ngIf="propertyRow.properties.length" class="tb-flex" [class]="propertyRow.propertiesRowClass">
<ng-container *ngFor="let property of propertyRow.properties"> <ng-container *ngFor="let property of propertyRow.properties">
<ng-container *ngIf="property.visible"> <ng-container *ngIf="property.visible">
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div> <div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div>
<mat-form-field *ngIf="property.type === FormPropertyType.text" [class]="property.fieldClass" appearance="outline" subscriptSizing="dynamic"> <ng-container *ngIf="isInputFieldPropertyType(property.type)">
<input matInput formControlName="{{ property.id }}" [required]="property.required" placeholder="{{ 'widget-config.set' | translate }}"> <ng-container *ngTemplateOutlet="inputFieldTpl;context:{property: property, singleField: false}">
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div> </ng-container>
</mat-form-field> </ng-container>
<mat-form-field *ngIf="property.type === FormPropertyType.select" [class]="property.fieldClass || 'medium-width'" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="{{ property.id }}" [multiple]="property.multiple">
<mat-option *ngFor="let item of property.items" [value]="item.value">
{{ item.label | customTranslate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-color-input *ngIf="property.type === FormPropertyType.color" <tb-color-input *ngIf="property.type === FormPropertyType.color"
[required]="property.required" [required]="property.required"
[class]="property.fieldClass" [class]="property.fieldClass"
@ -89,12 +114,6 @@
formControlName="{{ property.id }}" formControlName="{{ property.id }}"
settingsKey="{{ property.name | customTranslate }}"> settingsKey="{{ property.name | customTranslate }}">
</tb-color-settings> </tb-color-settings>
<mat-form-field *ngIf="property.type === FormPropertyType.number" [class]="property.fieldClass" appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required"
[min]="property.min" [max]="property.max" [step]="property.step"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<tb-font-settings *ngIf="property.type === FormPropertyType.font" <tb-font-settings *ngIf="property.type === FormPropertyType.font"
[class]="property.fieldClass" [class]="property.fieldClass"
formControlName="{{ property.id }}" formControlName="{{ property.id }}"
@ -106,6 +125,7 @@
[required]="property.required" [required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input> formControlName="{{ property.id }}"></tb-unit-input>
<tb-material-icon-select *ngIf="property.type === FormPropertyType.icon" <tb-material-icon-select *ngIf="property.type === FormPropertyType.icon"
[required]="property.required"
asBoxInput asBoxInput
formControlName="{{ property.id }}"> formControlName="{{ property.id }}">
</tb-material-icon-select> </tb-material-icon-select>
@ -115,3 +135,132 @@
</div> </div>
</div> </div>
</ng-template> </ng-template>
<ng-template #inputFieldTpl let-property="property" let-singleField="singleField">
<ng-container [formGroup]="propertiesFormGroup">
<mat-form-field *ngIf="property.type === FormPropertyType.textarea"
[class]="singleField ? 'flex' : property.fieldClass"
[floatLabel]="singleField ? 'always' : 'auto'"
appearance="outline" subscriptSizing="dynamic">
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label>
<textarea matInput
[rows]="property.rows || 2"
formControlName="{{ property.id }}"
[required]="property.required"
placeholder="{{ 'widget-config.set' | translate }}"></textarea>
</mat-form-field>
<mat-form-field *ngIf="[FormPropertyType.text, FormPropertyType.password].includes(property.type)"
[class]="singleField ? 'flex' : property.fieldClass"
[floatLabel]="singleField ? 'always' : 'auto'"
appearance="outline" subscriptSizing="dynamic">
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label>
<input matInput
[type]="property.type"
formControlName="{{ property.id }}"
[required]="property.required"
placeholder="{{ 'widget-config.set' | translate }}">
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<mat-form-field *ngIf="property.type === FormPropertyType.number"
[class]="singleField ? 'flex' : property.fieldClass"
class="number"
[floatLabel]="singleField ? 'always' : 'auto'"
appearance="outline" subscriptSizing="dynamic">
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label>
<input matInput
type="number"
formControlName="{{ property.id }}"
[required]="property.required"
[min]="property.min"
[max]="property.max"
[step]="property.step"
placeholder="{{ 'widget-config.set' | translate }}">
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<mat-form-field *ngIf="property.type === FormPropertyType.select"
[class]="singleField ? 'flex' : (property.fieldClass || 'medium-width')"
[floatLabel]="singleField ? 'always' : 'auto'"
appearance="outline" subscriptSizing="dynamic">
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label>
<mat-select formControlName="{{ property.id }}"
[required]="property.required"
[multiple]="property.multiple">
<mat-option *ngIf="!property.multiple && property.allowEmptyOption" [value]="null"></mat-option>
<mat-option *ngFor="let item of property.items" [value]="item.value">
{{ item.label | customTranslate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-datetime *ngIf="property.type === FormPropertyType.datetime"
[fieldClass]="singleField ? 'flex' : property.fieldClass"
[class.flex-1]="singleField || property.fieldClass?.includes('flex')"
[floatLabel]="singleField ? 'always' : 'auto'"
[showLabel]="singleField"
[dateText]="singleField ? (property.name | customTranslate) : ''"
[allowClear]="property.allowClear"
[required]="property.required"
appearance="outline"
subscriptSizing="dynamic"
[type]="property.dateTimeType"
formControlName="{{ property.id }}">
</tb-datetime>
<div *ngIf="property.type === FormPropertyType.javascript" class="tb-form-row flex-1">
<tb-js-func class="flex-1"
label="{{ property.name | customTranslate }}"
disableUndefinedCheck
[required]="property.required"
[helpId]="property.helpId"
formControlName="{{ property.id }}">
</tb-js-func>
</div>
<div *ngIf="property.type === FormPropertyType.json" class="tb-form-row flex-1">
<tb-json-content class="flex-1"
label="{{ property.name | customTranslate }}"
[required]="property.required"
[contentType]="ContentType.JSON"
formControlName="{{ property.id }}">
</tb-json-content>
</div>
<div *ngIf="property.type === FormPropertyType.html" class="tb-form-row flex-1">
<tb-html class="flex-1"
label="{{ property.name | customTranslate }}"
[required]="property.required"
formControlName="{{ property.id }}">
</tb-html>
</div>
<div *ngIf="property.type === FormPropertyType.css" class="tb-form-row flex-1">
<tb-css class="flex-1"
label="{{ property.name | customTranslate }}"
[required]="property.required"
formControlName="{{ property.id }}">
</tb-css>
</div>
<div *ngIf="property.type === FormPropertyType.markdown" class="tb-form-row flex-1">
<tb-markdown-editor class="flex-1"
label="{{ property.name | customTranslate }}"
[required]="property.required"
[helpId]="property.helpId"
formControlName="{{ property.id }}">
</tb-markdown-editor>
</div>
<div *ngIf="property.type === FormPropertyType.image" class="tb-form-row flex-1">
<tb-gallery-image-input
class="flex-1"
label="{{ property.name | customTranslate }}"
formControlName="{{ property.id }}"
[required]="property.required">
</tb-gallery-image-input>
</div>
<div *ngIf="property.type === FormPropertyType.radios"
class="tb-form-row tb-radios-property column-xs flex-1" [class.direction-column]="property.direction === 'column'">
<div tbTruncateWithTooltip class="fixed-title-width fixed-title-height">{{ property.name | customTranslate }}</div>
<mat-radio-group class="flex flex-1 flex-wrap" [class.flex-col]="property.direction === 'column'"
[required]="property.required"
formControlName="{{ property.id }}">
<mat-radio-button *ngFor="let item of property.items" [value]="item.value">
<div tbTruncateWithTooltip>{{ item.label | customTranslate }}</div>
</mat-radio-button>
</mat-radio-group>
</div>
</ng-container>
</ng-template>

21
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss

@ -13,10 +13,31 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { :host ::ng-deep {
.tb-properties-content { .tb-properties-content {
gap: 16px; gap: 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.tb-form-row.tb-radios-property {
&.direction-column {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.mat-mdc-radio-group {
overflow: hidden;
.mat-mdc-radio-button {
overflow: hidden;
.mdc-form-field {
overflow: hidden;
width: 100%;
.mdc-label {
overflow: hidden;
}
}
}
}
}
} }

70
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts

@ -44,11 +44,15 @@ import {
FormPropertyContainerType, FormPropertyContainerType,
FormPropertyGroup, FormPropertyGroup,
FormPropertyType, FormPropertyType,
isInputFieldPropertyType,
PropertyConditionFunction, PropertyConditionFunction,
toPropertyGroups toPropertyGroups
} from '@shared/models/dynamic-form.models'; } from '@shared/models/dynamic-form.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { ContentType } from '@shared/models/constants';
import { DomSanitizer } from '@angular/platform-browser';
@Component({ @Component({
selector: 'tb-dynamic-form', selector: 'tb-dynamic-form',
@ -69,10 +73,14 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
}) })
export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
isInputFieldPropertyType = isInputFieldPropertyType;
FormPropertyContainerType = FormPropertyContainerType; FormPropertyContainerType = FormPropertyContainerType;
FormPropertyType = FormPropertyType; FormPropertyType = FormPropertyType;
ContentType = ContentType;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -82,10 +90,22 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
@Input() @Input()
title: string; title: string;
@Input()
@coerceBoolean()
isArrayItem = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
stroked = false; stroked = false;
@Input()
@coerceBoolean()
noPadding = false;
@Input()
@coerceBoolean()
noBorder = false;
private modelValue: {[id: string]: any}; private modelValue: {[id: string]: any};
private propagateChange = null; private propagateChange = null;
@ -97,6 +117,8 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
propertyGroups: FormPropertyGroup[]; propertyGroups: FormPropertyGroup[];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private customTranslate: CustomTranslatePipe,
private sanitizer: DomSanitizer,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
@ -173,26 +195,28 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
} }
} }
} }
this.propertyGroups = toPropertyGroups(this.properties); this.propertyGroups = toPropertyGroups(this.properties, this.isArrayItem, this.customTranslate, this.sanitizer);
for (const property of this.properties) { for (const property of this.properties) {
if (property.disableOnProperty) { if (property.type !== FormPropertyType.htmlSection) {
if (!this.validatorTriggers.includes(property.disableOnProperty)) { if (property.disableOnProperty) {
this.validatorTriggers.push(property.disableOnProperty); if (!this.validatorTriggers.includes(property.disableOnProperty)) {
this.validatorTriggers.push(property.disableOnProperty);
}
} }
} const validators: ValidatorFn[] = [];
const validators: ValidatorFn[] = []; if (property.required) {
if (property.required) { validators.push(Validators.required);
validators.push(Validators.required);
}
if (property.type === FormPropertyType.number) {
if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min));
} }
if (isDefinedAndNotNull(property.max)) { if (property.type === FormPropertyType.number) {
validators.push(Validators.max(property.max)); if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min));
}
if (isDefinedAndNotNull(property.max)) {
validators.push(Validators.max(property.max));
}
} }
this.propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
} }
this.propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
} }
} }
this.setupValue(); this.setupValue();
@ -216,7 +240,7 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
} }
this.propertyGroups.forEach(g => { this.propertyGroups.forEach(g => {
g.containers.forEach(container => { g.containers.forEach(container => {
if (container.type === FormPropertyContainerType.fieldset) { if ([FormPropertyContainerType.fieldset, FormPropertyContainerType.field, FormPropertyContainerType.htmlSection, FormPropertyContainerType.array].includes(container.type)) {
container.visible = container.property.visible; container.visible = container.property.visible;
} else { } else {
container.visible = container.switch?.visible || container.properties.some(p => p.visible); container.visible = container.switch?.visible || container.properties.some(p => p.visible);
@ -233,12 +257,14 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
private updateControlsState() { private updateControlsState() {
if (this.properties) { if (this.properties) {
for (let property of this.properties) { for (let property of this.properties) {
const control = this.propertiesFormGroup.get(property.id); if (property.type !== FormPropertyType.htmlSection) {
if (property.visible && !property.disabled) { const control = this.propertiesFormGroup.get(property.id);
control.enable({emitEvent: false}); if (property.visible && !property.disabled) {
control.updateValueAndValidity({emitEvent: false}); control.enable({emitEvent: false});
} else { control.updateValueAndValidity({emitEvent: false});
control.disable({emitEvent: false}); } else {
control.disable({emitEvent: false});
}
} }
} }
} }

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

@ -180,6 +180,9 @@ import {
import { import {
DynamicFormSelectItemRowComponent DynamicFormSelectItemRowComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component'; } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component';
import {
DynamicFormArrayComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -248,7 +251,8 @@ import {
DynamicFormPropertyPanelComponent, DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent, DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent, DynamicFormSelectItemRowComponent,
DynamicFormComponent DynamicFormComponent,
DynamicFormArrayComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -321,7 +325,8 @@ import {
DynamicFormPropertyPanelComponent, DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent, DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent, DynamicFormSelectItemRowComponent,
DynamicFormComponent DynamicFormComponent,
DynamicFormArrayComponent
], ],
providers: [ providers: [
ColorSettingsComponentService, ColorSettingsComponentService,

7
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -112,6 +112,7 @@ export class WidgetComponentService {
templateHtml: this.utils.editWidgetInfo.templateHtml, templateHtml: this.utils.editWidgetInfo.templateHtml,
templateCss: this.utils.editWidgetInfo.templateCss, templateCss: this.utils.editWidgetInfo.templateCss,
controllerScript: this.utils.editWidgetInfo.controllerScript, controllerScript: this.utils.editWidgetInfo.controllerScript,
settingsForm: this.utils.editWidgetInfo.settingsForm,
settingsSchema: this.utils.editWidgetInfo.settingsSchema, settingsSchema: this.utils.editWidgetInfo.settingsSchema,
dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
latestDataKeySettingsSchema: this.utils.editWidgetInfo.latestDataKeySettingsSchema, latestDataKeySettingsSchema: this.utils.editWidgetInfo.latestDataKeySettingsSchema,
@ -298,6 +299,9 @@ export class WidgetComponentService {
this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule, this.homeComponentsModule]).subscribe( this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule, this.homeComponentsModule]).subscribe(
{ {
next: () => { next: () => {
if (widgetControllerDescriptor.settingsForm) {
widgetInfo.typeSettingsForm = widgetControllerDescriptor.settingsForm;
}
if (widgetControllerDescriptor.settingsSchema) { if (widgetControllerDescriptor.settingsSchema) {
widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
} }
@ -539,6 +543,9 @@ export class WidgetComponentService {
const result: WidgetControllerDescriptor = { const result: WidgetControllerDescriptor = {
widgetTypeFunction: widgetType widgetTypeFunction: widgetType
}; };
if (isFunction(widgetTypeInstance.getSettingsForm)) {
result.settingsForm = widgetTypeInstance.getSettingsForm();
}
if (isFunction(widgetTypeInstance.getSettingsSchema)) { if (isFunction(widgetTypeInstance.getSettingsSchema)) {
result.settingsSchema = widgetTypeInstance.getSettingsSchema(); result.settingsSchema = widgetTypeInstance.getSettingsSchema();
} }

4
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -561,6 +561,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
fullFqn: string; fullFqn: string;
deprecated: boolean; deprecated: boolean;
scada: boolean; scada: boolean;
typeSettingsForm?: FormProperty[];
typeSettingsSchema?: string | any; typeSettingsSchema?: string | any;
typeDataKeySettingsSchema?: string | any; typeDataKeySettingsSchema?: string | any;
typeLatestDataKeySettingsSchema?: string | any; typeLatestDataKeySettingsSchema?: string | any;
@ -644,6 +645,7 @@ export const ErrorWidgetType: WidgetInfo = {
}; };
export interface WidgetTypeInstance { export interface WidgetTypeInstance {
getSettingsForm?: () => FormProperty[];
getSettingsSchema?: () => string; getSettingsSchema?: () => string;
getDataKeySettingsSchema?: () => string; getDataKeySettingsSchema?: () => string;
getLatestDataKeySettingsSchema?: () => string; getLatestDataKeySettingsSchema?: () => string;
@ -672,6 +674,7 @@ export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({
templateHtml: widgetTypeEntity.descriptor.templateHtml, templateHtml: widgetTypeEntity.descriptor.templateHtml,
templateCss: widgetTypeEntity.descriptor.templateCss, templateCss: widgetTypeEntity.descriptor.templateCss,
controllerScript: widgetTypeEntity.descriptor.controllerScript, controllerScript: widgetTypeEntity.descriptor.controllerScript,
settingsForm: widgetTypeEntity.descriptor.settingsForm,
settingsSchema: widgetTypeEntity.descriptor.settingsSchema, settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
latestDataKeySettingsSchema: widgetTypeEntity.descriptor.latestDataKeySettingsSchema, latestDataKeySettingsSchema: widgetTypeEntity.descriptor.latestDataKeySettingsSchema,
@ -701,6 +704,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
templateHtml: widgetInfo.templateHtml, templateHtml: widgetInfo.templateHtml,
templateCss: widgetInfo.templateCss, templateCss: widgetInfo.templateCss,
controllerScript: widgetInfo.controllerScript, controllerScript: widgetInfo.controllerScript,
settingsForm: widgetInfo.settingsForm,
settingsSchema: widgetInfo.settingsSchema, settingsSchema: widgetInfo.settingsSchema,
dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema,
latestDataKeySettingsSchema: widgetInfo.latestDataKeySettingsSchema, latestDataKeySettingsSchema: widgetInfo.latestDataKeySettingsSchema,

18
ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html

@ -148,17 +148,15 @@
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>
</div> </div>
<tb-datetime <tb-datetime class="block"
dateText="entity-view.start-date" dateText="{{ 'entity-view.start-ts' | translate }}"
timeText="entity-view.start-ts" [maxDate]="maxStartTimeMs | async"
[maxDate]="maxStartTimeMs | async" formControlName="startTimeMs"
formControlName="startTimeMs"
></tb-datetime> ></tb-datetime>
<tb-datetime <tb-datetime class="block"
dateText="entity-view.end-date" dateText="{{ 'entity-view.end-ts' | translate }}"
timeText="entity-view.end-ts" [minDate]="minEndTimeMs | async"
[minDate]="minEndTimeMs | async" formControlName="endTimeMs"
formControlName="endTimeMs"
></tb-datetime> ></tb-datetime>
<div formGroupName="additionalInfo"> <div formGroupName="additionalInfo">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">

1
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html

@ -28,6 +28,7 @@
[cdkDropListDisabled]="!dragEnabled" [cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="behaviorDrop($event)"> (cdkDropListDropped)="behaviorDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled" <div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled" [class.tb-draggable-form-table-row]="!disabled"
*ngFor="let behaviorControl of behaviorsFormArray().controls; trackBy: trackByBehavior; let $index = index;"> *ngFor="let behaviorControl of behaviorsFormArray().controls; trackBy: trackByBehavior; let $index = index;">
<tb-scada-symbol-metadata-behavior-row class="flex-1" <tb-scada-symbol-metadata-behavior-row class="flex-1"

4
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts

@ -172,8 +172,8 @@ export class ScadaSymbolBehaviorsComponent implements ControlValueAccessor, OnIn
behaviorDrop(event: CdkDragDrop<string[]>) { behaviorDrop(event: CdkDragDrop<string[]>) {
const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray;
const behavior = behaviorsArray.at(event.previousIndex); const behavior = behaviorsArray.at(event.previousIndex);
behaviorsArray.removeAt(event.previousIndex); behaviorsArray.removeAt(event.previousIndex, {emitEvent: false});
behaviorsArray.insert(event.currentIndex, behavior); behaviorsArray.insert(event.currentIndex, behavior, {emitEvent: true});
} }
behaviorsFormArray(): UntypedFormArray { behaviorsFormArray(): UntypedFormArray {

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

@ -80,6 +80,8 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
toastTargetId = `jsFuncEditor-${guid()}`; toastTargetId = `jsFuncEditor-${guid()}`;
@Input() label: string;
@Input() functionTitle: string; @Input() functionTitle: string;
@Input() functionName: string; @Input() functionName: string;
@ -102,7 +104,9 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
@Input() globalVariables: Array<string>; @Input() globalVariables: Array<string>;
@Input() disableUndefinedCheck = false; @Input()
@coerceBoolean()
disableUndefinedCheck = false;
@Input() helpId: string; @Input() helpId: string;
@ -174,7 +178,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
} }
ngOnInit(): void { ngOnInit(): void {
if (this.functionTitle) { if (this.functionTitle || this.label) {
this.hideBrackets = true; this.hideBrackets = true;
} }
if (!this.resultType || this.resultType.length === 0) { if (!this.resultType || this.resultType.length === 0) {
@ -190,6 +194,8 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
} }
if (this.functionTitle) { if (this.functionTitle) {
this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`; this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`;
} else if (this.label) {
this.functionLabel = this.label;
} else { } else {
this.functionLabel = this.functionLabel =
`function ${this.functionName ? this.functionName : ''}(${this.functionArgsString})${this.hideBrackets ? '' : ' {'}`; `function ${this.functionName ? this.functionName : ''}(${this.functionArgsString})${this.hideBrackets ? '' : ' {'}`;

3
ui-ngx/src/app/shared/components/string-items-list.component.html

@ -15,7 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="stringItemsForm" class="mat-block flex-1" <mat-form-field [class]="fieldClass"
[formGroup]="stringItemsForm" class="mat-block flex-1"
[floatLabel]="floatLabel" [floatLabel]="floatLabel"
[appearance]="appearance" [appearance]="appearance"
[subscriptSizing]="subscriptSizing"> [subscriptSizing]="subscriptSizing">

3
ui-ngx/src/app/shared/components/string-items-list.component.ts

@ -108,6 +108,9 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
@Input() @Input()
subscriptSizing: SubscriptSizing = 'fixed'; subscriptSizing: SubscriptSizing = 'fixed';
@Input()
fieldClass: string;
@Input() @Input()
@coerceArray() @coerceArray()
predefinedValues: StringItemsOption[]; predefinedValues: StringItemsOption[];

46
ui-ngx/src/app/shared/components/time/datetime.component.html

@ -15,26 +15,26 @@
limitations under the License. limitations under the License.
--> -->
<section class="flex flex-row items-start justify-start gap-4"> <mat-form-field [class]="fieldClass"
<mat-form-field> [class.tb-datetime-field]="!fieldClass && type === 'datetime'"
<mat-label *ngIf="showLabel">{{ dateText | translate }}</mat-label> [class.tb-date-field]="!fieldClass && type === 'date'"
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle> [class.tb-time-field]="!fieldClass && type === 'time'"
<mat-datetimepicker #datePicker type="date" openOnFocus="true"></mat-datetimepicker> [class.with-clear]="allowClear"
<input [min]="minDateValue" [max]="maxDateValue" [subscriptSizing]="subscriptSizing" [appearance]="appearance" [floatLabel]="floatLabel">
[disabled]="disabled" <mat-label *ngIf="showLabel">{{ dateText }}</mat-label>
[required]="required" <div matSuffix class="mat-mdc-form-field-icon-suffix flex flex-row">
matInput [(ngModel)]="date" <button *ngIf="allowClear && date && !disabled"
[matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()"> type="button"
</mat-form-field> mat-icon-button aria-label="Clear"
<mat-form-field> (click)="clear($event)">
<mat-label *ngIf="showLabel">{{ timeText | translate }}</mat-label> <mat-icon>close</mat-icon>
<mat-datetimepicker-toggle [for]="timePicker" matPrefix></mat-datetimepicker-toggle> </button>
<mat-datetimepicker #timePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker-toggle [for]="datePicker"></mat-datetimepicker-toggle>
<input [min]="minDateValue" [max]="maxDateValue" </div>
[disabled]="disabled" <mat-datetimepicker #datePicker [type]="type" openOnFocus="true"></mat-datetimepicker>
[required]="required" <input [min]="minDateValue" [max]="maxDateValue"
matInput [disabled]="disabled"
[(ngModel)]="date" [required]="required"
[matDatetimepicker]="timePicker" (ngModelChange)="onDateChange()"> matInput [(ngModel)]="date"
</mat-form-field> [matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()">
</section> </mat-form-field>

60
ui-ngx/src/app/shared/components/time/datetime.component.scss

@ -0,0 +1,60 @@
/**
* Copyright © 2016-2024 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 '../../../../scss/constants';
:host-context(.tb-form-row) {
:host {
display: flex;
}
.mat-mdc-form-field {
&.tb-datetime-field {
width: 185px;
&.with-clear {
width: 225px;
}
}
&.tb-date-field {
width: 140px;
&.with-clear {
width: 180px;
}
}
&.tb-time-field {
width: 100px;
&.with-clear {
width: 140px;
}
}
}
}
:host-context(.tb-form-row.column-xs) {
@media #{$mat-xs} {
.mat-mdc-form-field {
&.tb-datetime-field, &.tb-date-field, &.tb-time-field {
width: auto;
flex: 1;
}
}
}
}
:host {
.mat-mdc-form-field-icon-suffix,
.mat-datetimepicker-toggle {
color: rgba(0, 0, 0, 0.38);
}
}

32
ui-ngx/src/app/shared/components/time/datetime.component.ts

@ -17,11 +17,14 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { MatDatetimepickerType } from '@mat-datetimepicker/core/datetimepicker/datetimepicker-type';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-datetime', selector: 'tb-datetime',
templateUrl: './datetime.component.html', templateUrl: './datetime.component.html',
styleUrls: [], styleUrls: ['./datetime.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -41,6 +44,18 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor {
this.requiredValue = coerceBooleanProperty(value); this.requiredValue = coerceBooleanProperty(value);
} }
@Input()
floatLabel: FloatLabelType = 'auto';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
type: MatDatetimepickerType = 'datetime';
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -48,10 +63,15 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor {
dateText: string; dateText: string;
@Input() @Input()
timeText: string; @coerceBoolean()
showLabel = true;
@Input() @Input()
showLabel = true; @coerceBoolean()
allowClear = false;
@Input()
fieldClass: string;
minDateValue: Date | null; minDateValue: Date | null;
@ -111,4 +131,10 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor {
this.updateView(value); this.updateView(value);
} }
clear($event: Event) {
$event.stopPropagation();
this.date = null;
this.updateView(null);
}
} }

421
ui-ngx/src/app/shared/models/dynamic-form.models.ts

@ -16,22 +16,36 @@
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models'; import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { isString } from '@core/utils'; import { deepClone, isDefinedAndNotNull, isString } from '@core/utils';
import { JsonSchema, JsonSettingsSchema } from '@shared/models/widget.models'; import { JsonSchema, JsonSettingsSchema } from '@shared/models/widget.models';
import JsonFormUtils from '@shared/components/json-form/react/json-form-utils'; import JsonFormUtils from '@shared/components/json-form/react/json-form-utils';
import { JsonFormData, KeyLabelItem } from '@shared/components/json-form/react/json-form.models'; import { JsonFormData, KeyLabelItem } from '@shared/components/json-form/react/json-form.models';
import { constantColor, Font } from '@shared/models/widget-settings.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
export enum FormPropertyType { export enum FormPropertyType {
text = 'text', text = 'text',
number = 'number', number = 'number',
password = 'password',
textarea = 'textarea',
switch = 'switch', switch = 'switch',
select = 'select', select = 'select',
radios = 'radios',
datetime = 'datetime',
image = 'image',
javascript = 'javascript',
json = 'json',
html = 'html',
css = 'css',
markdown = 'markdown',
color = 'color', color = 'color',
color_settings = 'color_settings', color_settings = 'color_settings',
font = 'font', font = 'font',
units = 'units', units = 'units',
icon = 'icon', icon = 'icon',
fieldset = 'fieldset' fieldset = 'fieldset',
array = 'array',
htmlSection = 'htmlSection'
} }
export const formPropertyTypes = Object.keys(FormPropertyType) as FormPropertyType[]; export const formPropertyTypes = Object.keys(FormPropertyType) as FormPropertyType[];
@ -40,14 +54,26 @@ export const formPropertyTypeTranslations = new Map<FormPropertyType, string>(
[ [
[FormPropertyType.text, 'dynamic-form.property.type-text'], [FormPropertyType.text, 'dynamic-form.property.type-text'],
[FormPropertyType.number, 'dynamic-form.property.type-number'], [FormPropertyType.number, 'dynamic-form.property.type-number'],
[FormPropertyType.password, 'dynamic-form.property.type-password'],
[FormPropertyType.textarea, 'dynamic-form.property.type-textarea'],
[FormPropertyType.switch, 'dynamic-form.property.type-switch'], [FormPropertyType.switch, 'dynamic-form.property.type-switch'],
[FormPropertyType.select, 'dynamic-form.property.type-select'], [FormPropertyType.select, 'dynamic-form.property.type-select'],
[FormPropertyType.radios, 'dynamic-form.property.type-radios'],
[FormPropertyType.datetime, 'dynamic-form.property.type-datetime'],
[FormPropertyType.image, 'dynamic-form.property.type-image'],
[FormPropertyType.javascript, 'dynamic-form.property.type-javascript'],
[FormPropertyType.json, 'dynamic-form.property.type-json'],
[FormPropertyType.html, 'dynamic-form.property.type-html'],
[FormPropertyType.css, 'dynamic-form.property.type-css'],
[FormPropertyType.markdown, 'dynamic-form.property.type-markdown'],
[FormPropertyType.color, 'dynamic-form.property.type-color'], [FormPropertyType.color, 'dynamic-form.property.type-color'],
[FormPropertyType.color_settings, 'dynamic-form.property.type-color-settings'], [FormPropertyType.color_settings, 'dynamic-form.property.type-color-settings'],
[FormPropertyType.font, 'dynamic-form.property.type-font'], [FormPropertyType.font, 'dynamic-form.property.type-font'],
[FormPropertyType.units, 'dynamic-form.property.type-units'], [FormPropertyType.units, 'dynamic-form.property.type-units'],
[FormPropertyType.icon, 'dynamic-form.property.type-icon'], [FormPropertyType.icon, 'dynamic-form.property.type-icon'],
[FormPropertyType.fieldset, 'dynamic-form.property.type-fieldset'] [FormPropertyType.fieldset, 'dynamic-form.property.type-fieldset'],
[FormPropertyType.array, 'dynamic-form.property.type-array'],
[FormPropertyType.htmlSection, 'dynamic-form.property.type-html-section']
] ]
); );
@ -78,6 +104,10 @@ export interface FormPropertyBase {
fieldClass?: string; fieldClass?: string;
} }
export interface FormTextareaProperty extends FormPropertyBase {
rows?: number;
}
export interface FormNumberProperty extends FormPropertyBase { export interface FormNumberProperty extends FormPropertyBase {
min?: number; min?: number;
max?: number; max?: number;
@ -88,6 +118,11 @@ export interface FormFieldSetProperty extends FormPropertyBase {
properties?: FormProperty[]; properties?: FormProperty[];
} }
export interface FormArrayProperty extends FormPropertyBase {
arrayItemName?: string;
arrayItemType?: FormPropertyType;
}
export interface FormSelectItem { export interface FormSelectItem {
value: any; value: any;
label: string; label: string;
@ -95,33 +130,82 @@ export interface FormSelectItem {
export interface FormSelectProperty extends FormPropertyBase { export interface FormSelectProperty extends FormPropertyBase {
multiple?: boolean; multiple?: boolean;
allowEmptyOption?: boolean;
items?: FormSelectItem[]; items?: FormSelectItem[];
} }
export type FormProperty = FormPropertyBase & FormNumberProperty & FormSelectProperty & FormFieldSetProperty; export type FormPropertyDirection = 'row' | 'column';
export interface FormRadiosProperty extends FormPropertyBase {
direction?: FormPropertyDirection;
items?: FormSelectItem[];
}
export type FormPropertyDateTimeType = 'date' | 'time' | 'datetime';
export interface FormDateTimeProperty extends FormPropertyBase {
allowClear?: boolean;
dateTimeType?: FormPropertyDateTimeType;
}
export interface FormJavascriptProperty extends FormPropertyBase {
helpId?: string;
}
export interface FormMarkdownProperty extends FormPropertyBase {
helpId?: string;
}
export interface FormHtmlSection extends FormPropertyBase {
htmlClassList?: string[];
htmlContent?: string;
}
export type FormProperty = FormPropertyBase & FormTextareaProperty & FormNumberProperty & FormSelectProperty & FormRadiosProperty
& FormDateTimeProperty & FormJavascriptProperty & FormMarkdownProperty & FormFieldSetProperty & FormArrayProperty & FormHtmlSection;
export enum FormPropertyContainerType { export enum FormPropertyContainerType {
field = 'field',
row = 'row', row = 'row',
fieldset = 'fieldset' fieldset = 'fieldset',
array = 'array',
htmlSection = 'htmlSection'
} }
export interface FormPropertyContainerBase { export interface FormPropertyContainerBase {
type: FormPropertyContainerType; type: FormPropertyContainerType;
label: string; label: string;
properties: FormProperty[];
visible: boolean; visible: boolean;
} }
export interface FormPropertyRow extends FormPropertyContainerBase { export interface FormPropertyRow extends FormPropertyContainerBase {
properties?: FormProperty[];
switch?: FormProperty; switch?: FormProperty;
rowClass?: string; rowClass?: string;
propertiesRowClass?: string;
}
export interface FormPropertyField extends FormPropertyContainerBase {
property?: FormProperty;
} }
export interface FormPropertyFieldset extends FormPropertyContainerBase { export interface FormPropertyFieldset extends FormPropertyContainerBase {
property?: FormProperty; property?: FormProperty;
properties?: FormProperty[];
} }
export type FormPropertyContainer = FormPropertyRow & FormPropertyFieldset; export interface FormPropertyArray extends FormPropertyContainerBase {
property?: FormProperty;
arrayItemProperty?: FormProperty;
}
export interface FormPropertyHtml extends FormPropertyContainerBase {
property?: FormProperty;
safeHtml?: SafeHtml;
htmlClass?: string;
}
export type FormPropertyContainer = FormPropertyField & FormPropertyRow & FormPropertyFieldset & FormPropertyHtml;
export interface FormPropertyGroup { export interface FormPropertyGroup {
title?: string; title?: string;
@ -129,14 +213,22 @@ export interface FormPropertyGroup {
visible: boolean; visible: boolean;
} }
export const toPropertyGroups = (properties: FormProperty[]): FormPropertyGroup[] => { export const toPropertyGroups = (properties: FormProperty[],
isArrayItem: boolean,
customTranslate: CustomTranslatePipe,
sanitizer: DomSanitizer): FormPropertyGroup[] => {
const groups: {title: string, properties: FormProperty[]}[] = []; const groups: {title: string, properties: FormProperty[]}[] = [];
for (let property of properties) { for (let property of properties) {
if (!property.group) { if (!property.group) {
groups.push({ let group = groups.length ? groups[groups.length - 1] : null;
title: null, if (group && !group.title) {
properties: [property] group.properties.push(property);
}); } else {
groups.push({
title: null,
properties: [property]
});
}
} else { } else {
let propertyGroup = groups.find(g => g.title === property.group); let propertyGroup = groups.find(g => g.title === property.group);
if (!propertyGroup) { if (!propertyGroup) {
@ -151,15 +243,35 @@ export const toPropertyGroups = (properties: FormProperty[]): FormPropertyGroup[
} }
return groups.map(g => ({ return groups.map(g => ({
title: g.title, title: g.title,
containers: toPropertyContainers(g.properties), containers: toPropertyContainers(g.properties, isArrayItem, customTranslate, sanitizer),
visible: true visible: true
})); }));
}; };
const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer[] => { const toPropertyContainers = (properties: FormProperty[],
isArrayItem: boolean,
customTranslate: CustomTranslatePipe,
sanitizer: DomSanitizer): FormPropertyContainer[] => {
const result: FormPropertyContainer[] = []; const result: FormPropertyContainer[] = [];
for (let property of properties) { for (let property of properties) {
if (property.type === FormPropertyType.fieldset) { if (property.type === FormPropertyType.array) {
const propertyArray: FormPropertyArray = {
property,
label: property.name,
type: FormPropertyContainerType.array,
visible: true
};
const arrayItemProperty = deepClone(property);
arrayItemProperty.name = property.arrayItemName;
arrayItemProperty.type = property.arrayItemType;
arrayItemProperty.required = true;
delete arrayItemProperty.disableOnProperty;
delete arrayItemProperty.condition;
delete arrayItemProperty.conditionFunction;
delete arrayItemProperty.group;
propertyArray.arrayItemProperty = arrayItemProperty;
result.push(propertyArray);
} else if (property.type === FormPropertyType.fieldset) {
const propertyFieldset: FormPropertyFieldset = { const propertyFieldset: FormPropertyFieldset = {
property, property,
label: property.name, label: property.name,
@ -168,6 +280,24 @@ const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer
visible: true visible: true
}; };
result.push(propertyFieldset); result.push(propertyFieldset);
} else if (property.type === FormPropertyType.htmlSection) {
const propertyHtml: FormPropertyHtml = {
property,
label: property.name,
type: FormPropertyContainerType.htmlSection,
htmlClass: property.htmlClassList ? property.htmlClassList.join(' ') : '',
safeHtml: sanitizer.bypassSecurityTrustHtml(property.htmlContent),
visible: true
};
result.push(propertyHtml);
} else if (isSingleFieldPropertyType(property.type) || isArrayItem) {
const propertyField: FormPropertyField = {
property,
label: property.name,
type: FormPropertyContainerType.field,
visible: true
};
result.push(propertyField);
} else { } else {
let propertyRow = let propertyRow =
result.find(r => r.type === FormPropertyContainerType.row && r.label === property.name); result.find(r => r.type === FormPropertyContainerType.row && r.label === property.name);
@ -177,6 +307,7 @@ const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer
type: FormPropertyContainerType.row, type: FormPropertyContainerType.row,
properties: [], properties: [],
rowClass: property.rowClass, rowClass: property.rowClass,
propertiesRowClass: 'row flex-end align-center',
visible: true visible: true
}; };
result.push(propertyRow); result.push(propertyRow);
@ -188,22 +319,60 @@ const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer
} }
} }
} }
for (let container of result.filter(c =>
c.type === FormPropertyContainerType.row && !c.switch && c.properties?.length === 1)) {
const property = container.properties[0];
if (isInputFieldPropertyType(property.type)) {
const labelText = customTranslate.transform(property.name);
if (property.type !== FormPropertyType.number && labelText.length > 40) {
container.type = FormPropertyContainerType.field;
container.property = property;
delete container.properties;
delete container.rowClass;
} else {
if (!container.rowClass) {
container.rowClass = 'column-xs';
container.propertiesRowClass = 'gt-xs:align-center xs:flex-col gt-xs:flex-row gt-xs:justify-end';
}
}
}
}
return result; return result;
} }
export const isPropertyTypeAllowedForRow = (type: FormPropertyType): boolean => {
return !isSingleFieldPropertyType(type) && ![FormPropertyType.fieldset, FormPropertyType.array, FormPropertyType.htmlSection].includes(type);
}
export const isSingleFieldPropertyType = (type: FormPropertyType): boolean => {
return [FormPropertyType.radios, FormPropertyType.textarea, FormPropertyType.image, FormPropertyType.javascript, FormPropertyType.json, FormPropertyType.html,
FormPropertyType.css, FormPropertyType.markdown].includes(type);
}
export const isInputFieldPropertyType = (type: FormPropertyType): boolean => {
return [FormPropertyType.text, FormPropertyType.password, FormPropertyType.number, FormPropertyType.select, FormPropertyType.datetime,
FormPropertyType.textarea].includes(type);
}
export const defaultFormProperties = (properties: FormProperty[]): {[id: string]: any} => { export const defaultFormProperties = (properties: FormProperty[]): {[id: string]: any} => {
const formProperties: {[id: string]: any} = {}; const formProperties: {[id: string]: any} = {};
for (const property of properties) { for (const property of properties) {
formProperties[property.id] = defaultFormPropertyValue(property); if (property.type !== FormPropertyType.htmlSection) {
formProperties[property.id] = defaultFormPropertyValue(property);
}
} }
return formProperties; return formProperties;
}; };
export const defaultFormPropertyValue = (property: FormProperty): any => { export const defaultFormPropertyValue = (property: FormProperty): any => {
if (property.type === FormPropertyType.fieldset) { if (property.type === FormPropertyType.array) {
return [];
} else if (property.type === FormPropertyType.fieldset) {
const propertyValue: {[id: string]: any} = {}; const propertyValue: {[id: string]: any} = {};
for (const childProperty of property.properties) { for (const childProperty of property.properties) {
propertyValue[childProperty.id] = defaultFormPropertyValue(childProperty); if (childProperty.type !== FormPropertyType.htmlSection) {
propertyValue[childProperty.id] = defaultFormPropertyValue(childProperty);
}
} }
return propertyValue; return propertyValue;
} else { } else {
@ -211,10 +380,58 @@ export const defaultFormPropertyValue = (property: FormProperty): any => {
} }
} }
export const propertyValid = (property: FormProperty): boolean =>
!(!property.id || !property.name || !property.type || (property.type === FormPropertyType.array && !property.arrayItemType));
export const defaultPropertyValue = (type: FormPropertyType): any => {
switch (type) {
case FormPropertyType.text:
case FormPropertyType.textarea:
case FormPropertyType.password:
case FormPropertyType.javascript:
case FormPropertyType.json:
case FormPropertyType.html:
case FormPropertyType.css:
case FormPropertyType.markdown:
return '';
case FormPropertyType.number:
return 0;
case FormPropertyType.switch:
return false;
case FormPropertyType.color:
return '#000';
case FormPropertyType.color_settings:
return constantColor('#000');
case FormPropertyType.font:
return {
size: 12,
sizeUnit: 'px',
family: 'Roboto',
weight: 'normal',
style: 'normal',
lineHeight: '1'
} as Font;
case FormPropertyType.units:
return '';
case FormPropertyType.icon:
return 'star';
case FormPropertyType.fieldset:
case FormPropertyType.array:
case FormPropertyType.select:
case FormPropertyType.radios:
case FormPropertyType.datetime:
case FormPropertyType.htmlSection:
case FormPropertyType.image:
return null;
}
};
export const formPropertyCompletions = (properties: FormProperty[], customTranslate: CustomTranslatePipe): TbEditorCompletions => { export const formPropertyCompletions = (properties: FormProperty[], customTranslate: CustomTranslatePipe): TbEditorCompletions => {
const propertiesCompletions: TbEditorCompletions = {}; const propertiesCompletions: TbEditorCompletions = {};
for (const property of properties) { for (const property of properties) {
propertiesCompletions[property.id] = formPropertyCompletion(property, customTranslate); if (property.type !== FormPropertyType.htmlSection) {
propertiesCompletions[property.id] = formPropertyCompletion(property, customTranslate);
}
} }
return propertiesCompletions; return propertiesCompletions;
} }
@ -224,37 +441,63 @@ export const formPropertyCompletion = (property: FormProperty, customTranslate:
if (property.subLabel) { if (property.subLabel) {
description += ` <small>${customTranslate.transform(property.subLabel, property.subLabel)}</small>`; description += ` <small>${customTranslate.transform(property.subLabel, property.subLabel)}</small>`;
} }
if (property.type === FormPropertyType.select) { const isArray = property.type === FormPropertyType.array;
if (property.multiple) { const type = isArray ? property.arrayItemType : property.type;
if (type === FormPropertyType.select) {
if (property.multiple || isArray) {
description += '<br/><br/><code class="title"><b>Possible values of array element:</b></code>'; description += '<br/><br/><code class="title"><b>Possible values of array element:</b></code>';
} else { } else {
description += '<br/><br/><code class="title"><b>Possible values:</b></code>'; description += '<br/><br/><code class="title"><b>Possible values:</b></code>';
} }
description += `<ul>${property.items.map(item => `<li>${item.value} <code style="font-size: 11px;">${typeof item.value}</code></li>`).join('\n')}</ul>`; description += `<ul>${property.items.map(item => `<li>${item.value} <code style="font-size: 11px;">${typeof item.value}</code></li>`).join('\n')}</ul>`;
} }
if (type === FormPropertyType.datetime) {
if (isArray) {
description += '<br/><br/><code class="title">Stores array of time values in milliseconds since midnight, January 1, 1970 UTC.</code>';
} else {
description += '<br/><br/><code class="title">Stores time value in milliseconds since midnight, January 1, 1970 UTC.</code>';
}
}
const completion: TbEditorCompletion = { const completion: TbEditorCompletion = {
meta: 'property', meta: 'property',
description, description,
type: formPropertyCompletionType(property) type: formPropertyCompletionType(property)
}; };
if (property.type === FormPropertyType.fieldset) { if (type === FormPropertyType.fieldset && !isArray) {
completion.children = {}; completion.children = {};
for (const childProperty of property.properties) { for (const childProperty of property.properties) {
completion.children[childProperty.id] = formPropertyCompletion(childProperty, customTranslate); if (childProperty.type !== FormPropertyType.htmlSection) {
completion.children[childProperty.id] = formPropertyCompletion(childProperty, customTranslate);
}
} }
} }
return completion; return completion;
}; };
const formPropertyCompletionType = (property: FormProperty): string => { const formPropertyCompletionType = (property: FormProperty): string => {
switch (property.type) { const isArray = property.type === FormPropertyType.array;
const type = isArray ? property.arrayItemType : property.type;
let typeStr: string;
switch (type) {
case FormPropertyType.text: case FormPropertyType.text:
return 'string'; case FormPropertyType.password:
case FormPropertyType.textarea:
typeStr = 'string';
break;
case FormPropertyType.number: case FormPropertyType.number:
return 'number'; typeStr = 'number';
break;
case FormPropertyType.switch: case FormPropertyType.switch:
return 'boolean'; typeStr = 'boolean';
break;
case FormPropertyType.datetime:
typeStr = 'number';
break;
case FormPropertyType.image:
typeStr = 'image URL string';
break;
case FormPropertyType.select: case FormPropertyType.select:
case FormPropertyType.radios:
const items = property.items || []; const items = property.items || [];
const types: string[] = []; const types: string[] = [];
items.forEach(item => { items.forEach(item => {
@ -264,24 +507,53 @@ const formPropertyCompletionType = (property: FormProperty): string => {
} }
}); });
const typesString = types.length ? types.join(' | ') : 'string'; const typesString = types.length ? types.join(' | ') : 'string';
if (property.multiple) { if (property.type === FormPropertyType.select && property.multiple) {
return `Array&lt;${typesString}&gt;`; typeStr = `Array&lt;${typesString}&gt;`;
} else { } else {
return typesString; typeStr = typesString;
} }
break;
case FormPropertyType.color: case FormPropertyType.color:
return 'color string'; typeStr = 'color string';
break;
case FormPropertyType.color_settings: case FormPropertyType.color_settings:
return 'ColorProcessor'; typeStr = 'ColorProcessor';
break;
case FormPropertyType.font: case FormPropertyType.font:
return 'Font'; typeStr = 'Font';
break;
case FormPropertyType.units: case FormPropertyType.units:
return 'units string'; typeStr = 'units string';
break;
case FormPropertyType.icon: case FormPropertyType.icon:
return 'icon string'; typeStr = 'icon string';
break;
case FormPropertyType.fieldset: case FormPropertyType.fieldset:
return 'object'; typeStr = 'object';
break;
case FormPropertyType.javascript:
typeStr = 'JavaScript function body string';
break;
case FormPropertyType.json:
typeStr = 'JSON string';
break;
case FormPropertyType.html:
typeStr = 'HTML string';
break;
case FormPropertyType.css:
typeStr = 'CSS string';
break;
case FormPropertyType.markdown:
typeStr = 'Markdown string';
break;
default:
typeStr = 'unknown';
break;
}
if (isArray) {
typeStr = `Array&lt;${typeStr}&gt;`;
} }
return typeStr;
}; };
@ -322,13 +594,14 @@ const schemaFormToProperties = (schema: JsonSchema, theForm: any[], groupTitle?:
const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: string): FormProperty => { const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: string): FormProperty => {
if (form.key && form.key.length > level) { if (form.key && form.key.length > level) {
const property: FormProperty = { const id = form.key[level] + '';
id: form.key[level] + '', let property: FormProperty = {
name: form.title, id,
name: form.title || id,
group: groupTitle, group: groupTitle,
type: null, type: null,
default: form.schema.default, default: form.schema?.default || null,
required: form.required required: isDefinedAndNotNull(form.required) ? form.required : false
}; };
if (form.condition?.length) { if (form.condition?.length) {
property.condition = `return ${form.condition};`; property.condition = `return ${form.condition};`;
@ -341,6 +614,14 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?:
property.type = FormPropertyType.text; property.type = FormPropertyType.text;
property.fieldClass = 'flex'; property.fieldClass = 'flex';
break; break;
case 'password':
property.type = FormPropertyType.password;
property.fieldClass = 'flex';
break;
case 'textarea':
property.type = FormPropertyType.textarea;
property.rows = form.rows || form.rowsMax || 2;
break;
case 'checkbox': case 'checkbox':
property.type = FormPropertyType.switch; property.type = FormPropertyType.switch;
break; break;
@ -353,6 +634,7 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?:
} }
property.multiple = form.multiple; property.multiple = form.multiple;
property.fieldClass = 'flex'; property.fieldClass = 'flex';
property.allowEmptyOption = isDefinedAndNotNull(form.allowClear) ? form.allowClear : false;
break; break;
case 'select': case 'select':
property.type = FormPropertyType.select; property.type = FormPropertyType.select;
@ -363,6 +645,25 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?:
} }
property.multiple = false; property.multiple = false;
property.fieldClass = 'flex'; property.fieldClass = 'flex';
property.allowEmptyOption = false;
break;
case 'radios':
property.type = FormPropertyType.radios;
if (form.titleMap?.length) {
property.items = form.titleMap.map(item => ({value: item.value, label: item.name}));
} else {
property.items = [];
}
property.direction = form.direction === 'row' ? 'row' : 'column';
break;
case 'date':
property.type = FormPropertyType.datetime;
property.dateTimeType = 'date';
property.fieldClass = 'flex';
property.allowClear = true;
break;
case 'image':
property.type = FormPropertyType.image;
break; break;
case 'color': case 'color':
property.type = FormPropertyType.color; property.type = FormPropertyType.color;
@ -375,6 +676,44 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?:
property.properties = form.items ? (form.items as JsonFormData[]).map(item => property.properties = form.items ? (form.items as JsonFormData[]).map(item =>
jsonFormDataToProperty(item, level+1)).filter(p => p !== null) : []; jsonFormDataToProperty(item, level+1)).filter(p => p !== null) : [];
break; break;
case 'javascript':
property.type = FormPropertyType.javascript;
property.helpId = form.helpId;
break;
case 'json':
property.type = FormPropertyType.json;
break;
case 'html':
property.type = FormPropertyType.html;
break;
case 'css':
property.type = FormPropertyType.css;
break;
case 'markdown':
property.type = FormPropertyType.markdown;
break;
case 'help':
property.type = FormPropertyType.htmlSection;
property.htmlContent = form.description || '';
property.htmlClassList = form.htmlClass ? form.htmlClass.split(' ') : [];
break;
case 'array':
if (form.items?.length) {
const item: JsonFormData = form.items[0] as JsonFormData;
if (item.type !== 'array') {
const arrayProperty = jsonFormDataToProperty(item, 0);
arrayProperty.arrayItemType = arrayProperty.type;
arrayProperty.arrayItemName = arrayProperty.name;
arrayProperty.id = property.id;
arrayProperty.name = property.name;
arrayProperty.group = property.group;
arrayProperty.condition = property.condition;
arrayProperty.required = property.required;
property = arrayProperty;
property.type = FormPropertyType.array;
}
}
break;
} }
if (!property.type) { if (!property.type) {
return null; return null;

1
ui-ngx/src/app/shared/models/widget.models.ts

@ -195,6 +195,7 @@ export interface WidgetTypeParameters {
export interface WidgetControllerDescriptor { export interface WidgetControllerDescriptor {
widgetTypeFunction?: any; widgetTypeFunction?: any;
settingsForm?: FormProperty[];
settingsSchema?: string | any; settingsSchema?: string | any;
dataKeySettingsSchema?: string | any; dataKeySettingsSchema?: string | any;
latestDataKeySettingsSchema?: string | any; latestDataKeySettingsSchema?: string | any;

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

@ -1625,15 +1625,27 @@
"name": "Name", "name": "Name",
"type": "Type", "type": "Type",
"type-text": "Text", "type-text": "Text",
"type-password": "Password",
"type-textarea": "Text area",
"type-number": "Number", "type-number": "Number",
"type-switch": "Switch", "type-switch": "Switch",
"type-select": "Select", "type-select": "Select",
"type-radios": "Radio buttons",
"type-datetime": "Date/Time",
"type-image": "Image",
"type-javascript": "JavaScript",
"type-json": "JSON",
"type-html": "HTML",
"type-css": "CSS",
"type-markdown": "Markdown",
"type-color": "Color", "type-color": "Color",
"type-color-settings": "Color settings", "type-color-settings": "Color settings",
"type-font": "Font", "type-font": "Font",
"type-units": "Units", "type-units": "Units",
"type-icon": "Icon", "type-icon": "Icon",
"type-fieldset": "Fieldset", "type-fieldset": "Fieldset",
"type-array": "Array",
"type-html-section": "HTML section",
"group-title": "Group title", "group-title": "Group title",
"no-properties": "No properties configured", "no-properties": "No properties configured",
"add-property": "Add property", "add-property": "Add property",
@ -1655,13 +1667,32 @@
"property-field-classes": "Property field classes", "property-field-classes": "Property field classes",
"not-unique-property-ids-error": "Property Ids must be unique!", "not-unique-property-ids-error": "Property Ids must be unique!",
"enable-multiple-select": "Enable multiple select", "enable-multiple-select": "Enable multiple select",
"allow-empty-select-option": "Allow empty option",
"select-options": "Select options", "select-options": "Select options",
"not-unique-select-option-value-error": "Select option values must be unique!", "not-unique-select-option-value-error": "Select option values must be unique!",
"value": "Value", "value": "Value",
"label": "Label", "label": "Label",
"add-option": "Add option", "add-option": "Add option",
"no-options": "No options configured", "no-options": "No options configured",
"remove-option": "Remove option" "remove-option": "Remove option",
"textarea-rows": "Textarea rows",
"help-id": "Help id",
"buttons-direction": "Buttons direction",
"direction-row": "Row",
"direction-column": "Column",
"radio-button-options": "Radio button options",
"datetime-type": "Date/Time field type",
"datetime-type-date": "Date",
"datetime-type-time": "Time",
"datetime-type-datetime": "Date/Time",
"enable-clear-button": "Enable clear button",
"html-section-settings": "HTML section settings",
"html-section-classes": "HTML section classes",
"html-section-content": "HTML section content",
"array-item": "Array item",
"item-type": "Item type",
"item-name": "Item name",
"no-items": "No items"
} }
}, },
"asset-profile": { "asset-profile": {
@ -2548,9 +2579,7 @@
"select-entity-view": "Select entity view", "select-entity-view": "Select entity view",
"make-public": "Make entity view public", "make-public": "Make entity view public",
"make-private": "Make entity view private", "make-private": "Make entity view private",
"start-date": "Start date",
"start-ts": "Start time", "start-ts": "Start time",
"end-date": "End date",
"end-ts": "End time", "end-ts": "End time",
"date-limits": "Date limits", "date-limits": "Date limits",
"client-attributes": "Client attributes", "client-attributes": "Client attributes",

10
ui-ngx/src/form.scss

@ -54,6 +54,16 @@
padding: 12px; padding: 12px;
gap: 8px; gap: 8px;
} }
&.no-padding-right {
padding-right: 0;
.mat-expansion-panel {
&.tb-settings {
> .mat-expansion-panel-header {
padding-right: 16px;
}
}
}
}
&.no-padding-bottom { &.no-padding-bottom {
padding-bottom: 0; padding-bottom: 0;
} }

Loading…
Cancel
Save