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 actionSources = widgetInfo.actionSources;
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;
if (!rawSettingsSchema || rawSettingsSchema === '') {
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 actionSources = widgetInfo.actionSources;
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;
if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {};

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

@ -31,8 +31,7 @@
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<tb-datetime formControlName="defaultValue"
dateText="filter.date"
timeText="filter.time"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
</ng-template>
<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 [ngSwitchCase]="entityKeyValueTypeEnum.DATE_TIME">
<tb-datetime formControlName="value"
dateText="filter.date"
timeText="filter.time"
dateText="{{ 'filter.date' | translate }}"
required></tb-datetime>
</ng-template>
<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">
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value }}</label>
<tb-datetime [formControl]="userInputControl.get('value')"
dateText="filter.date"
timeText="filter.time"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
</ng-template>
<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 { MatIconRegistry } from '@angular/material/icon';
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 {
generateElementId: () => string;
@ -630,7 +635,9 @@ export class ScadaSymbolObject {
elements.push(element);
}
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) {
if (tag.actions) {
@ -967,9 +974,8 @@ export class ScadaSymbolObject {
return Array.isArray(element) ? element : [element];
}
private getProperty(...ids: string[]): FormProperty {
private getProperty(properties: FormProperty[], ...ids: string[]): FormProperty {
let found: FormProperty;
let properties = this.metadata.properties;
for (const id of ids) {
if (properties) {
found = properties.find(p => p.id === id);
@ -985,9 +991,9 @@ export class ScadaSymbolObject {
return found;
}
private getSettingsValue(...ids: string[]): any {
private getSettingsValue(settings: {[id: string]: any}, ...ids: string[]): any {
let found: any;
let properties = this.settings.properties;
let properties = settings;
for (const id of ids) {
if (properties) {
found = properties[id];
@ -1003,43 +1009,72 @@ export class ScadaSymbolObject {
return found;
}
private getPropertyValue(...ids: string[]): any {
const property = this.getProperty(...ids);
private getPropertyValue(properties: FormProperty[], settings: {[id: string]: any}, ...ids: string[]): any {
const property = this.getProperty(properties, ...ids);
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} = {};
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;
} else {
const value = this.getSettingsValue(...ids);
if (isDefinedAndNotNull(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'));
}
}
const value = this.getSettingsValue(settings, ...ids);
return this.convertPropertyValue(property, value);
}
} else {
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';

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"
(cdkDropListDropped)="propertyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled"
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;">
<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';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
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 {
DynamicFormPropertyRowComponent,
propertyValid
DynamicFormPropertyRowComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -136,7 +135,7 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): 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);
}
public validate(c: UntypedFormControl) {
public validate(_c: UntypedFormControl) {
this.errorText = '';
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray;
const notUniqueControls =
@ -189,15 +188,15 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI
propertyDrop(event: CdkDragDrop<string[]>) {
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray;
const property = propertiesArray.at(event.previousIndex);
propertiesArray.removeAt(event.previousIndex);
propertiesArray.insert(event.currentIndex, property);
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 {
trackByProperty(_index: number, propertyControl: AbstractControl): any {
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-form-field>
</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 class="flex flex-1 flex-row items-center justify-end gap-2">
<div class="tb-small-label" translate>dynamic-form.property.min</div>
@ -68,7 +200,7 @@
</mat-form-field>
</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-header class="flex flex-row flex-wrap">
<mat-panel-title>
@ -81,20 +213,60 @@
</ng-template>
</mat-expansion-panel>
</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">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<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-expansion-panel-header>
<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">
{{ 'dynamic-form.property.enable-multiple-select' | translate }}
</mat-slide-toggle>
</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
formControlName="items">
</tb-dynamic-form-select-items>
@ -102,23 +274,113 @@
</mat-expansion-panel>
</div>
</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>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === FormPropertyType.text"
<mat-form-field *ngIf="[FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)"
class="flex"
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>
<tb-color-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.color"
<tb-color-input *ngIf="propertyItemType === FormPropertyType.color"
asBoxInput
colorClearButton
formControlName="default">
</tb-color-input>
<tb-color-settings *ngIf="propertyFormGroup.get('type').value === FormPropertyType.color_settings"
<tb-color-settings *ngIf="propertyItemType === FormPropertyType.color_settings"
formControlName="default"
settingsKey="{{ propertyFormGroup.get('id').value }}">
</tb-color-settings>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === FormPropertyType.number"
<mat-form-field *ngIf="propertyItemType === FormPropertyType.number"
appearance="outline"
class="number"
subscriptSizing="dynamic">
@ -128,23 +390,24 @@
[step]="propertyFormGroup.get('step').value"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings *ngIf="propertyFormGroup.get('type').value === FormPropertyType.font"
<tb-font-settings *ngIf="propertyItemType === FormPropertyType.font"
formControlName="default"
clearButton
disabledLineHeight>
</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>
<tb-value-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.switch"
<tb-value-input *ngIf="propertyItemType === FormPropertyType.switch"
[valueType]="ValueType.BOOLEAN"
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
formControlName="default">
</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">
<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">
{{ item.label | customTranslate }}
</mat-option>
@ -155,97 +418,38 @@
</mat-option>
</mat-select>
</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 *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" 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>
<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>
</ng-container>
</ng-template>

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;
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 { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ValueType } from '@shared/models/constants';
import { ContentType, ValueType } from '@shared/models/constants';
import {
defaultPropertyValue,
FormProperty,
formPropertyFieldClasses,
formPropertyRowClasses,
FormPropertyType,
formPropertyTypes,
formPropertyTypeTranslations,
FormSelectItem
FormSelectItem,
isPropertyTypeAllowedForRow
} 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 { isUndefinedOrNull } from '@core/utils';
import { StringItemsOption } from '@shared/components/string-items-list.component';
@Component({
selector: 'tb-dynamic-form-property-panel',
@ -44,12 +45,29 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
FormPropertyType = FormPropertyType;
ContentType = ContentType;
formPropertyTypes = formPropertyTypes;
arrayItemFormPropertyTypes = formPropertyTypes.filter(t => t !== FormPropertyType.array);
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()
isAdd = false;
@ -81,13 +99,15 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
ngOnInit(): void {
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(
{
id: [this.property.id, [Validators.required]],
name: [this.property.name, [Validators.required]],
group: [this.property.group, []],
type: [this.property.type, [Validators.required]],
arrayItemType: [this.property.arrayItemType, [Validators.required]],
arrayItemName: [this.property.arrayItemName, []],
default: [this.property.default, []],
required: [this.property.required, []],
subLabel: [this.property.subLabel, []],
@ -95,14 +115,22 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
fieldSuffix: [this.property.fieldSuffix, []],
disableOnProperty: [this.property.disableOnProperty, []],
condition: [this.property.condition, []],
rowClass: [(this.property.rowClass || '').split(' '), []],
fieldClass: [(this.property.fieldClass || '').split(' '), []],
rowClass: [this.property.rowClass ? this.property.rowClass.split(' ') : [], []],
fieldClass: [this.property.fieldClass ? this.property.fieldClass.split(' ') : [], []],
rows: [this.property.rows, [Validators.min(1)]],
min: [this.property.min, []],
max: [this.property.max, []],
step: [this.property.step, [Validators.min(0)]],
properties: [this.property.properties, []],
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) {
@ -113,7 +141,11 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
).subscribe(() => {
this.updateValidators();
});
this.propertyFormGroup.get('arrayItemType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateValidators();
});
this.propertyFormGroup.get('items').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
@ -140,7 +172,19 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
}
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) {
this.propertyFormGroup.get('min').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('properties').disable({emitEvent: false});
}
if (type === FormPropertyType.select) {
this.propertyFormGroup.get('multiple').enable({emitEvent: false});
if ([FormPropertyType.select, FormPropertyType.radios].includes(type)) {
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 {
this.propertyFormGroup.get('multiple').disable({emitEvent: false});
this.propertyFormGroup.get('allowEmptyOption').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) {
const defaultValue = defaultPropertyValue(type);
this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false});
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() {
const type: FormPropertyType = this.propertyFormGroup.get('type').value;
const type = this.propertyItemType;
const multiple: boolean = this.propertyFormGroup.get('multiple').value;
const defaultValue: any = this.propertyFormGroup.get('default').value;
const items: FormSelectItem[] = this.propertyFormGroup.get('items').value;
if (defaultValue && type === FormPropertyType.select) {
if (multiple) {
if (defaultValue && [FormPropertyType.select, FormPropertyType.radios].includes(type)) {
if (multiple && FormPropertyType.select === type) {
let targetValue: any[] = defaultValue;
targetValue = targetValue.filter(valItem => !!items.find(item => item.value === valItem));
this.propertyFormGroup.get('default').patchValue(targetValue, {emitEvent: false});
@ -190,7 +280,7 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
}
private onMultipleSelectChange() {
const type: FormPropertyType = this.propertyFormGroup.get('type').value;
const type = this.propertyItemType;
const multiple: boolean = this.propertyFormGroup.get('multiple').value;
const defaultValue: any = this.propertyFormGroup.get('default').value;
if (type === FormPropertyType.select) {
@ -199,6 +289,8 @@ export class DynamicFormPropertyPanelComponent implements OnInit {
const newVal = [defaultValue];
this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false});
}
this.propertyFormGroup.get('allowEmptyOption').patchValue(false, {emitEvent: false});
this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false});
} else {
if (defaultValue && Array.isArray(defaultValue)) {
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('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 { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { constantColor, Font } from '@shared/models/widget-settings.models';
import {
defaultPropertyValue,
FormProperty,
FormPropertyType,
formPropertyTypes,
formPropertyTypeTranslations
formPropertyTypeTranslations, propertyValid
} from '@shared/models/dynamic-form.models';
import {
DynamicFormPropertiesComponent
@ -57,39 +57,6 @@ import {
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component';
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({
selector: 'tb-dynamic-form-property-row',
templateUrl: './dynamic-form-property-row.component.html',
@ -168,7 +135,7 @@ export class DynamicFormPropertyRowComponent implements ControlValueAccessor, On
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
@ -242,7 +209,7 @@ export class DynamicFormPropertyRowComponent implements ControlValueAccessor, On
this.editProperty(null, this.editButton, true, onCanceled);
}
public validate(c: UntypedFormControl) {
public validate(_c: UntypedFormControl) {
const idControl = this.propertyRowFormGroup.get('id');
if (idControl.hasError('propertyIdNotUnique')) {
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 {
DynamicFormSelectItemsComponent
} 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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -105,7 +104,7 @@ export class DynamicFormSelectItemRowComponent implements ControlValueAccessor,
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
@ -128,7 +127,7 @@ export class DynamicFormSelectItemRowComponent implements ControlValueAccessor,
this.cd.markForCheck();
}
public validate(c: UntypedFormControl) {
public validate(_c: UntypedFormControl) {
const valueControl = this.selectItemRowFormGroup.get('value');
if (valueControl.hasError('itemValueNotUnique')) {
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"
(cdkDropListDropped)="selectItemDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled"
*ngFor="let selectItemControl of selectItemsFormArray().controls; trackBy: trackBySelectItem; let $index = index;">
<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;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
@ -126,7 +126,7 @@ export class DynamicFormSelectItemsComponent implements ControlValueAccessor, On
this.selectItemsFormGroup.setControl('selectItems', this.prepareSelectItemsFormArray(items), {emitEvent: false});
}
public validate(c: UntypedFormControl) {
public validate(_c: UntypedFormControl) {
this.errorText = '';
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
const notUniqueControls =
@ -166,15 +166,15 @@ export class DynamicFormSelectItemsComponent implements ControlValueAccessor, On
selectItemDrop(event: CdkDragDrop<string[]>) {
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
const item = itemsArray.at(event.previousIndex);
itemsArray.removeAt(event.previousIndex);
itemsArray.insert(event.currentIndex, item);
itemsArray.removeAt(event.previousIndex, {emitEvent: false});
itemsArray.insert(event.currentIndex, item, {emitEvent: true});
}
selectItemsFormArray(): UntypedFormArray {
return this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
}
trackBySelectItem(index: number, selectItemControl: AbstractControl): any {
trackBySelectItem(_index: number, selectItemControl: AbstractControl): any {
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.
-->
<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">
<div *ngIf="title" class="tb-form-panel-title">{{ title }}</div>
<ng-container #propertiesContent><ng-content select=".tb-properties-content"></ng-content></ng-container>
@ -42,19 +43,50 @@
<ng-template #propertyContainersTpl let-propertyContainers="propertyContainers">
<ng-container *ngFor="let propertyContainer of propertyContainers" [formGroup]="propertiesFormGroup">
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.row && propertyContainer.visible">
<ng-container *ngTemplateOutlet="propertyRowTpl;context:{propertyRow: propertyContainer}">
<ng-container *ngIf="propertyContainer.visible">
<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>
<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-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">
<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"
@ -62,21 +94,14 @@
{{ propertyRow.label | customTranslate }}
</mat-slide-toggle>
<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 *ngIf="property.visible">
<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">
<input matInput 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.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>
<ng-container *ngIf="isInputFieldPropertyType(property.type)">
<ng-container *ngTemplateOutlet="inputFieldTpl;context:{property: property, singleField: false}">
</ng-container>
</ng-container>
<tb-color-input *ngIf="property.type === FormPropertyType.color"
[required]="property.required"
[class]="property.fieldClass"
@ -89,12 +114,6 @@
formControlName="{{ property.id }}"
settingsKey="{{ property.name | customTranslate }}">
</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"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
@ -106,6 +125,7 @@
[required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input>
<tb-material-icon-select *ngIf="property.type === FormPropertyType.icon"
[required]="property.required"
asBoxInput
formControlName="{{ property.id }}">
</tb-material-icon-select>
@ -115,3 +135,132 @@
</div>
</div>
</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
* limitations under the License.
*/
:host ::ng-deep {
.tb-properties-content {
gap: 16px;
display: flex;
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,
FormPropertyGroup,
FormPropertyType,
isInputFieldPropertyType,
PropertyConditionFunction,
toPropertyGroups
} from '@shared/models/dynamic-form.models';
import { coerceBoolean } from '@shared/decorators/coercion';
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({
selector: 'tb-dynamic-form',
@ -69,10 +73,14 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
})
export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
isInputFieldPropertyType = isInputFieldPropertyType;
FormPropertyContainerType = FormPropertyContainerType;
FormPropertyType = FormPropertyType;
ContentType = ContentType;
@Input()
disabled: boolean;
@ -82,10 +90,22 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
@Input()
title: string;
@Input()
@coerceBoolean()
isArrayItem = false;
@Input()
@coerceBoolean()
stroked = false;
@Input()
@coerceBoolean()
noPadding = false;
@Input()
@coerceBoolean()
noBorder = false;
private modelValue: {[id: string]: any};
private propagateChange = null;
@ -97,6 +117,8 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
propertyGroups: FormPropertyGroup[];
constructor(protected store: Store<AppState>,
private customTranslate: CustomTranslatePipe,
private sanitizer: DomSanitizer,
private destroyRef: DestroyRef,
private fb: UntypedFormBuilder,
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) {
if (property.disableOnProperty) {
if (!this.validatorTriggers.includes(property.disableOnProperty)) {
this.validatorTriggers.push(property.disableOnProperty);
if (property.type !== FormPropertyType.htmlSection) {
if (property.disableOnProperty) {
if (!this.validatorTriggers.includes(property.disableOnProperty)) {
this.validatorTriggers.push(property.disableOnProperty);
}
}
}
const validators: ValidatorFn[] = [];
if (property.required) {
validators.push(Validators.required);
}
if (property.type === FormPropertyType.number) {
if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min));
const validators: ValidatorFn[] = [];
if (property.required) {
validators.push(Validators.required);
}
if (isDefinedAndNotNull(property.max)) {
validators.push(Validators.max(property.max));
if (property.type === FormPropertyType.number) {
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();
@ -216,7 +240,7 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
}
this.propertyGroups.forEach(g => {
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;
} else {
container.visible = container.switch?.visible || container.properties.some(p => p.visible);
@ -233,12 +257,14 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce
private updateControlsState() {
if (this.properties) {
for (let property of this.properties) {
const control = this.propertiesFormGroup.get(property.id);
if (property.visible && !property.disabled) {
control.enable({emitEvent: false});
control.updateValueAndValidity({emitEvent: false});
} else {
control.disable({emitEvent: false});
if (property.type !== FormPropertyType.htmlSection) {
const control = this.propertiesFormGroup.get(property.id);
if (property.visible && !property.disabled) {
control.enable({emitEvent: false});
control.updateValueAndValidity({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 {
DynamicFormSelectItemRowComponent
} 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({
declarations: [
@ -248,7 +251,8 @@ import {
DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent,
DynamicFormComponent
DynamicFormComponent,
DynamicFormArrayComponent
],
imports: [
CommonModule,
@ -321,7 +325,8 @@ import {
DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent,
DynamicFormComponent
DynamicFormComponent,
DynamicFormArrayComponent
],
providers: [
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,
templateCss: this.utils.editWidgetInfo.templateCss,
controllerScript: this.utils.editWidgetInfo.controllerScript,
settingsForm: this.utils.editWidgetInfo.settingsForm,
settingsSchema: this.utils.editWidgetInfo.settingsSchema,
dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
latestDataKeySettingsSchema: this.utils.editWidgetInfo.latestDataKeySettingsSchema,
@ -298,6 +299,9 @@ export class WidgetComponentService {
this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule, this.homeComponentsModule]).subscribe(
{
next: () => {
if (widgetControllerDescriptor.settingsForm) {
widgetInfo.typeSettingsForm = widgetControllerDescriptor.settingsForm;
}
if (widgetControllerDescriptor.settingsSchema) {
widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
}
@ -539,6 +543,9 @@ export class WidgetComponentService {
const result: WidgetControllerDescriptor = {
widgetTypeFunction: widgetType
};
if (isFunction(widgetTypeInstance.getSettingsForm)) {
result.settingsForm = widgetTypeInstance.getSettingsForm();
}
if (isFunction(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;
deprecated: boolean;
scada: boolean;
typeSettingsForm?: FormProperty[];
typeSettingsSchema?: string | any;
typeDataKeySettingsSchema?: string | any;
typeLatestDataKeySettingsSchema?: string | any;
@ -644,6 +645,7 @@ export const ErrorWidgetType: WidgetInfo = {
};
export interface WidgetTypeInstance {
getSettingsForm?: () => FormProperty[];
getSettingsSchema?: () => string;
getDataKeySettingsSchema?: () => string;
getLatestDataKeySettingsSchema?: () => string;
@ -672,6 +674,7 @@ export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({
templateHtml: widgetTypeEntity.descriptor.templateHtml,
templateCss: widgetTypeEntity.descriptor.templateCss,
controllerScript: widgetTypeEntity.descriptor.controllerScript,
settingsForm: widgetTypeEntity.descriptor.settingsForm,
settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
latestDataKeySettingsSchema: widgetTypeEntity.descriptor.latestDataKeySettingsSchema,
@ -701,6 +704,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
templateHtml: widgetInfo.templateHtml,
templateCss: widgetInfo.templateCss,
controllerScript: widgetInfo.controllerScript,
settingsForm: widgetInfo.settingsForm,
settingsSchema: widgetInfo.settingsSchema,
dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema,
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-accordion>
</div>
<tb-datetime
dateText="entity-view.start-date"
timeText="entity-view.start-ts"
[maxDate]="maxStartTimeMs | async"
formControlName="startTimeMs"
<tb-datetime class="block"
dateText="{{ 'entity-view.start-ts' | translate }}"
[maxDate]="maxStartTimeMs | async"
formControlName="startTimeMs"
></tb-datetime>
<tb-datetime
dateText="entity-view.end-date"
timeText="entity-view.end-ts"
[minDate]="minEndTimeMs | async"
formControlName="endTimeMs"
<tb-datetime class="block"
dateText="{{ 'entity-view.end-ts' | translate }}"
[minDate]="minEndTimeMs | async"
formControlName="endTimeMs"
></tb-datetime>
<div formGroupName="additionalInfo">
<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"
(cdkDropListDropped)="behaviorDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled"
*ngFor="let behaviorControl of behaviorsFormArray().controls; trackBy: trackByBehavior; let $index = index;">
<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[]>) {
const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray;
const behavior = behaviorsArray.at(event.previousIndex);
behaviorsArray.removeAt(event.previousIndex);
behaviorsArray.insert(event.currentIndex, behavior);
behaviorsArray.removeAt(event.previousIndex, {emitEvent: false});
behaviorsArray.insert(event.currentIndex, behavior, {emitEvent: true});
}
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()}`;
@Input() label: string;
@Input() functionTitle: string;
@Input() functionName: string;
@ -102,7 +104,9 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
@Input() globalVariables: Array<string>;
@Input() disableUndefinedCheck = false;
@Input()
@coerceBoolean()
disableUndefinedCheck = false;
@Input() helpId: string;
@ -174,7 +178,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
}
ngOnInit(): void {
if (this.functionTitle) {
if (this.functionTitle || this.label) {
this.hideBrackets = true;
}
if (!this.resultType || this.resultType.length === 0) {
@ -190,6 +194,8 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
}
if (this.functionTitle) {
this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`;
} else if (this.label) {
this.functionLabel = this.label;
} else {
this.functionLabel =
`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.
-->
<mat-form-field [formGroup]="stringItemsForm" class="mat-block flex-1"
<mat-form-field [class]="fieldClass"
[formGroup]="stringItemsForm" class="mat-block flex-1"
[floatLabel]="floatLabel"
[appearance]="appearance"
[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()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
fieldClass: string;
@Input()
@coerceArray()
predefinedValues: StringItemsOption[];

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

@ -15,26 +15,26 @@
limitations under the License.
-->
<section class="flex flex-row items-start justify-start gap-4">
<mat-form-field>
<mat-label *ngIf="showLabel">{{ dateText | translate }}</mat-label>
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="date" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue"
[disabled]="disabled"
[required]="required"
matInput [(ngModel)]="date"
[matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()">
</mat-form-field>
<mat-form-field>
<mat-label *ngIf="showLabel">{{ timeText | translate }}</mat-label>
<mat-datetimepicker-toggle [for]="timePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #timePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue"
[disabled]="disabled"
[required]="required"
matInput
[(ngModel)]="date"
[matDatetimepicker]="timePicker" (ngModelChange)="onDateChange()">
</mat-form-field>
</section>
<mat-form-field [class]="fieldClass"
[class.tb-datetime-field]="!fieldClass && type === 'datetime'"
[class.tb-date-field]="!fieldClass && type === 'date'"
[class.tb-time-field]="!fieldClass && type === 'time'"
[class.with-clear]="allowClear"
[subscriptSizing]="subscriptSizing" [appearance]="appearance" [floatLabel]="floatLabel">
<mat-label *ngIf="showLabel">{{ dateText }}</mat-label>
<div matSuffix class="mat-mdc-form-field-icon-suffix flex flex-row">
<button *ngIf="allowClear && date && !disabled"
type="button"
mat-icon-button aria-label="Clear"
(click)="clear($event)">
<mat-icon>close</mat-icon>
</button>
<mat-datetimepicker-toggle [for]="datePicker"></mat-datetimepicker-toggle>
</div>
<mat-datetimepicker #datePicker [type]="type" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue"
[disabled]="disabled"
[required]="required"
matInput [(ngModel)]="date"
[matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()">
</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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
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({
selector: 'tb-datetime',
templateUrl: './datetime.component.html',
styleUrls: [],
styleUrls: ['./datetime.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -41,6 +44,18 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
floatLabel: FloatLabelType = 'auto';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
type: MatDatetimepickerType = 'datetime';
@Input()
disabled: boolean;
@ -48,10 +63,15 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor {
dateText: string;
@Input()
timeText: string;
@coerceBoolean()
showLabel = true;
@Input()
showLabel = true;
@coerceBoolean()
allowClear = false;
@Input()
fieldClass: string;
minDateValue: Date | null;
@ -111,4 +131,10 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor {
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 { 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 JsonFormUtils from '@shared/components/json-form/react/json-form-utils';
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 {
text = 'text',
number = 'number',
password = 'password',
textarea = 'textarea',
switch = 'switch',
select = 'select',
radios = 'radios',
datetime = 'datetime',
image = 'image',
javascript = 'javascript',
json = 'json',
html = 'html',
css = 'css',
markdown = 'markdown',
color = 'color',
color_settings = 'color_settings',
font = 'font',
units = 'units',
icon = 'icon',
fieldset = 'fieldset'
fieldset = 'fieldset',
array = 'array',
htmlSection = 'htmlSection'
}
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.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.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_settings, 'dynamic-form.property.type-color-settings'],
[FormPropertyType.font, 'dynamic-form.property.type-font'],
[FormPropertyType.units, 'dynamic-form.property.type-units'],
[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;
}
export interface FormTextareaProperty extends FormPropertyBase {
rows?: number;
}
export interface FormNumberProperty extends FormPropertyBase {
min?: number;
max?: number;
@ -88,6 +118,11 @@ export interface FormFieldSetProperty extends FormPropertyBase {
properties?: FormProperty[];
}
export interface FormArrayProperty extends FormPropertyBase {
arrayItemName?: string;
arrayItemType?: FormPropertyType;
}
export interface FormSelectItem {
value: any;
label: string;
@ -95,33 +130,82 @@ export interface FormSelectItem {
export interface FormSelectProperty extends FormPropertyBase {
multiple?: boolean;
allowEmptyOption?: boolean;
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 {
field = 'field',
row = 'row',
fieldset = 'fieldset'
fieldset = 'fieldset',
array = 'array',
htmlSection = 'htmlSection'
}
export interface FormPropertyContainerBase {
type: FormPropertyContainerType;
label: string;
properties: FormProperty[];
visible: boolean;
}
export interface FormPropertyRow extends FormPropertyContainerBase {
properties?: FormProperty[];
switch?: FormProperty;
rowClass?: string;
propertiesRowClass?: string;
}
export interface FormPropertyField extends FormPropertyContainerBase {
property?: FormProperty;
}
export interface FormPropertyFieldset extends FormPropertyContainerBase {
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 {
title?: string;
@ -129,14 +213,22 @@ export interface FormPropertyGroup {
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[]}[] = [];
for (let property of properties) {
if (!property.group) {
groups.push({
title: null,
properties: [property]
});
let group = groups.length ? groups[groups.length - 1] : null;
if (group && !group.title) {
group.properties.push(property);
} else {
groups.push({
title: null,
properties: [property]
});
}
} else {
let propertyGroup = groups.find(g => g.title === property.group);
if (!propertyGroup) {
@ -151,15 +243,35 @@ export const toPropertyGroups = (properties: FormProperty[]): FormPropertyGroup[
}
return groups.map(g => ({
title: g.title,
containers: toPropertyContainers(g.properties),
containers: toPropertyContainers(g.properties, isArrayItem, customTranslate, sanitizer),
visible: true
}));
};
const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer[] => {
const toPropertyContainers = (properties: FormProperty[],
isArrayItem: boolean,
customTranslate: CustomTranslatePipe,
sanitizer: DomSanitizer): FormPropertyContainer[] => {
const result: FormPropertyContainer[] = [];
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 = {
property,
label: property.name,
@ -168,6 +280,24 @@ const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer
visible: true
};
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 {
let propertyRow =
result.find(r => r.type === FormPropertyContainerType.row && r.label === property.name);
@ -177,6 +307,7 @@ const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer
type: FormPropertyContainerType.row,
properties: [],
rowClass: property.rowClass,
propertiesRowClass: 'row flex-end align-center',
visible: true
};
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;
}
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} => {
const formProperties: {[id: string]: any} = {};
for (const property of properties) {
formProperties[property.id] = defaultFormPropertyValue(property);
if (property.type !== FormPropertyType.htmlSection) {
formProperties[property.id] = defaultFormPropertyValue(property);
}
}
return formProperties;
};
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} = {};
for (const childProperty of property.properties) {
propertyValue[childProperty.id] = defaultFormPropertyValue(childProperty);
if (childProperty.type !== FormPropertyType.htmlSection) {
propertyValue[childProperty.id] = defaultFormPropertyValue(childProperty);
}
}
return propertyValue;
} 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 => {
const propertiesCompletions: TbEditorCompletions = {};
for (const property of properties) {
propertiesCompletions[property.id] = formPropertyCompletion(property, customTranslate);
if (property.type !== FormPropertyType.htmlSection) {
propertiesCompletions[property.id] = formPropertyCompletion(property, customTranslate);
}
}
return propertiesCompletions;
}
@ -224,37 +441,63 @@ export const formPropertyCompletion = (property: FormProperty, customTranslate:
if (property.subLabel) {
description += ` <small>${customTranslate.transform(property.subLabel, property.subLabel)}</small>`;
}
if (property.type === FormPropertyType.select) {
if (property.multiple) {
const isArray = property.type === FormPropertyType.array;
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>';
} else {
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>`;
}
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 = {
meta: 'property',
description,
type: formPropertyCompletionType(property)
};
if (property.type === FormPropertyType.fieldset) {
if (type === FormPropertyType.fieldset && !isArray) {
completion.children = {};
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;
};
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:
return 'string';
case FormPropertyType.password:
case FormPropertyType.textarea:
typeStr = 'string';
break;
case FormPropertyType.number:
return 'number';
typeStr = 'number';
break;
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.radios:
const items = property.items || [];
const types: string[] = [];
items.forEach(item => {
@ -264,24 +507,53 @@ const formPropertyCompletionType = (property: FormProperty): string => {
}
});
const typesString = types.length ? types.join(' | ') : 'string';
if (property.multiple) {
return `Array&lt;${typesString}&gt;`;
if (property.type === FormPropertyType.select && property.multiple) {
typeStr = `Array&lt;${typesString}&gt;`;
} else {
return typesString;
typeStr = typesString;
}
break;
case FormPropertyType.color:
return 'color string';
typeStr = 'color string';
break;
case FormPropertyType.color_settings:
return 'ColorProcessor';
typeStr = 'ColorProcessor';
break;
case FormPropertyType.font:
return 'Font';
typeStr = 'Font';
break;
case FormPropertyType.units:
return 'units string';
typeStr = 'units string';
break;
case FormPropertyType.icon:
return 'icon string';
typeStr = 'icon string';
break;
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 => {
if (form.key && form.key.length > level) {
const property: FormProperty = {
id: form.key[level] + '',
name: form.title,
const id = form.key[level] + '';
let property: FormProperty = {
id,
name: form.title || id,
group: groupTitle,
type: null,
default: form.schema.default,
required: form.required
default: form.schema?.default || null,
required: isDefinedAndNotNull(form.required) ? form.required : false
};
if (form.condition?.length) {
property.condition = `return ${form.condition};`;
@ -341,6 +614,14 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?:
property.type = FormPropertyType.text;
property.fieldClass = 'flex';
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':
property.type = FormPropertyType.switch;
break;
@ -353,6 +634,7 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?:
}
property.multiple = form.multiple;
property.fieldClass = 'flex';
property.allowEmptyOption = isDefinedAndNotNull(form.allowClear) ? form.allowClear : false;
break;
case 'select':
property.type = FormPropertyType.select;
@ -363,6 +645,25 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?:
}
property.multiple = false;
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;
case '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 =>
jsonFormDataToProperty(item, level+1)).filter(p => p !== null) : [];
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) {
return null;

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

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

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

@ -1625,15 +1625,27 @@
"name": "Name",
"type": "Type",
"type-text": "Text",
"type-password": "Password",
"type-textarea": "Text area",
"type-number": "Number",
"type-switch": "Switch",
"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-settings": "Color settings",
"type-font": "Font",
"type-units": "Units",
"type-icon": "Icon",
"type-fieldset": "Fieldset",
"type-array": "Array",
"type-html-section": "HTML section",
"group-title": "Group title",
"no-properties": "No properties configured",
"add-property": "Add property",
@ -1655,13 +1667,32 @@
"property-field-classes": "Property field classes",
"not-unique-property-ids-error": "Property Ids must be unique!",
"enable-multiple-select": "Enable multiple select",
"allow-empty-select-option": "Allow empty option",
"select-options": "Select options",
"not-unique-select-option-value-error": "Select option values must be unique!",
"value": "Value",
"label": "Label",
"add-option": "Add option",
"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": {
@ -2548,9 +2579,7 @@
"select-entity-view": "Select entity view",
"make-public": "Make entity view public",
"make-private": "Make entity view private",
"start-date": "Start date",
"start-ts": "Start time",
"end-date": "End date",
"end-ts": "End time",
"date-limits": "Date limits",
"client-attributes": "Client attributes",

10
ui-ngx/src/form.scss

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

Loading…
Cancel
Save