src/app/shared/widgets/dynamic-form/dynamic-form-component/dynamic-form-fields.component.ts
additionalProperties |
additionalProperties:
|
Type : any
|
Optional |
Array of additional properties to view in select dialog list. used with 'select-dialog' type. |
autofocus |
autofocus:
|
Type : boolean
|
Optional |
Auto focus field. |
autosize |
autosize:
|
Type : boolean
|
Optional |
Sets autosize property for type 'textarea'. |
autosizeMinRows |
autosizeMinRows:
|
Type : string
|
Optional |
min rows for textarea field |
categoryUrl |
categoryUrl:
|
Type : string
|
Optional |
URL used to load version from. default is 'category-select-publications' for publications and 'category-select-asset-trees' for asset-trees Used with type 'category-selector'. |
chipRemovable |
chipRemovable:
|
Type : boolean
|
Optional |
Makes chips removable in 'chip-list' type. @default(true) |
clearable |
clearable:
|
Type : boolean
|
Optional |
Shows clear selection button. used with 'lookup' and 'multi-lookup' types. @default(true) |
confirmationLabel |
confirmationLabel:
|
Type : string
|
Optional |
Localized key for password confirmation lable. used with 'password' type. |
dataType |
dataType:
|
Type : string
|
Optional |
List folder data type. used with 'worklist' type. |
dateFormat |
dateFormat:
|
Type : string
|
Optional |
Date format. used with 'date' type. @default(dd.mm.yyyy) |
defaultValue |
defaultValue:
|
Type : any
|
Optional |
Initial value of the field. |
descriptionField |
descriptionField:
|
Type : string
|
Optional |
Description field of the object. it is used with 'lookup' and 'multi-lookup' types to define description field. @default(description) |
dialogContext |
dialogContext:
|
Type : any
|
Optional |
Dialog data for page-dialog type. |
dialogDescription |
dialogDescription:
|
Type : any
|
Optional |
Dialog data property used to set the dialog description when closed for page-dialog type. |
dialogIdentifier |
dialogIdentifier:
|
Type : string
|
Optional |
Dialog identifier for page-dialog type. |
dialogModule |
dialogModule:
|
Type : string
|
Optional |
Dialog module for page-dialog type. |
disabled |
disabled:
|
Type : boolean
|
Optional |
Enables\Disables the field. |
dropValueIdentifier |
dropValueIdentifier:
|
Type : string
|
Optional |
Identifier field that will be used to get the value while dropping it in the textarea field. |
enableMinimap |
enableMinimap:
|
Type : boolean
|
Optional |
Enable/Disable editor minimap for source-code-editor type. |
errors |
errors:
|
Type : ValidationError[]
|
Optional |
List of field validation errors. |
field |
field:
|
Type : string
|
Unique field identifier. |
fileUploadUrl |
fileUploadUrl:
|
Type : string
|
Optional |
File upload url. used with 'file-upload type. |
filterChannelTypes |
filterChannelTypes:
|
Type : string[]
|
Optional |
Applies channel types filter on versions. supported values are PURCHASE, SALES, ARCHIVE. used with 'category-selector' type. |
floatingLabel |
floatingLabel:
|
Type : boolean
|
Optional |
Shows floating label for the field. @default(false) |
height |
height:
|
Type : number
|
Optional |
Field height. used with 'text' type. |
hidden |
hidden:
|
Type : boolean
|
Optional |
Hides or shows the field. |
hideConfirmationField |
hideConfirmationField:
|
Type : boolean
|
Optional |
Hides\Shows second password confirmation field. used with 'password' type. @default(true) |
hint |
hint:
|
Type : string
|
Optional |
Localized key for field hint. |
hintOnFocus |
hintOnFocus:
|
Type : boolean
|
Optional |
Shows hint on focus. |
hintTranslations |
hintTranslations:
|
Type : any
|
Optional |
hint translation in other locales. |
identifierField |
identifierField:
|
Type : string
|
Optional |
Identifier field of the object. it is used with 'lookup' and 'multi-lookup' types to define identifier field. @default(identifier) |
includeSubCategories |
includeSubCategories:
|
Type : boolean
|
Optional |
Includes sub-categories value. used with 'category-selector' type. |
items |
items:
|
Type : DynamicFormField[]
|
Optional |
collection of all data for multi-components |
label |
label:
|
Type : string
|
Localized key for field label |
listfolderType |
listfolderType:
|
Type : string
|
Optional |
List folder type. used with 'worklist' type. |
localizedText |
localizedText:
|
Type : boolean
|
Optional |
Sets field as localized text. @default(false) |
lookupAllowEmpty |
lookupAllowEmpty:
|
Type : boolean
|
Optional |
Allows empty selection for lookup types. @default(false) |
lookupOptions |
lookupOptions:
|
Type : any[]
|
Optional |
Array of static lookup options. used with these types: lookup, multi-lookup, select-dialog. (See also lookupUrl) |
lookupUrl |
lookupUrl:
|
Type : string
|
Optional |
URL to load lookup options from. used with these types: lookup, multi-lookup, select-dialog. (See also lookupOptions) |
maxLength |
maxLength:
|
Type : number
|
Optional |
Sets maximum characters length for text types. |
menuInteractions |
menuInteractions:
|
Type : Selectors
|
Optional |
Identifiers of the interactions that are supposed to be loaded for the menu. |
multiCategorySelect |
multiCategorySelect:
|
Type : boolean
|
Optional |
Allows multiple category selection. Used with type 'category-selector'. @default(false) |
order |
order:
|
Type : number
|
Optional |
Order of the field in the form. @default(0) |
publication |
publication:
|
Type : string
|
Optional |
If not set the version select lookup will be shown, otherwise the provided version will be used and the category select lookup only will be shown. Used with type 'category-selector'. |
readonly |
readonly:
|
Type : boolean
|
Optional |
Makes the field Readonly or editable. @default(false) |
required |
required:
|
Type : boolean
|
Optional |
Makes the field required or optional. |
selectDialogItemType |
selectDialogItemType:
|
Type : string
|
Optional |
Item type used for select-dialog type. |
showEditorOnInit |
showEditorOnInit:
|
Type : boolean
|
Optional |
Triggers the display of Tiny editor implmentation in its onInit callback |
showEditpasswordButton |
showEditpasswordButton:
|
Type : boolean
|
Optional |
Shows\Hides edit password button. used with 'password' type. @default(false) |
showHint |
showHint:
|
Type : boolean
|
Optional |
This property shouldn't be added in configuration. It is only used internally. |
showIncludeSubCategories |
showIncludeSubCategories:
|
Type : boolean
|
Optional |
Shows\Hides include sub-categories checkbox. used with 'category-selector' type. @default(false) |
sourceCodeLanguage |
sourceCodeLanguage:
|
Type : string
|
Optional |
Source code programming language. It is used with type = 'source-code-editor'. |
supportedFileTypes |
supportedFileTypes:
|
Type : string
|
Optional |
Comma-separated list of supported file types. used with 'file-upload' type. |
supportedMimeTypes |
supportedMimeTypes:
|
Type : string[]
|
Optional |
Array of supported MIME types. used with 'file-upload' type. |
tab |
tab:
|
Type : string
|
Optional |
The target tab. used in dynamic forms with mutliple tabs. supported tabs are localizedtext, listFolder, userRights |
translations |
translations:
|
Type : any
|
Optional |
Label translation in other locales. |
type |
type:
|
Type : string
|
Type of the field. supported types are: boolean, boolean-indeterminate, category-selector, chip-list, date, date-time, file-upload, heading, lookup, multi-lookup, number, number-decimal, password, plain, select-dialog, text, textarea, toggler, multi-toggler, user-right, worklist, source-code-editor |
url |
url:
|
Type : string
|
Optional |
URL to load user rights. used with 'user-right' type. |
validations |
validations:
|
Type : DataValidation[]
|
Optional |
Validations to be applied on field input. |
value |
value:
|
Type : any
|
Optional |
This property shouldn't be added in configuration. It is only used internally. |
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
Input,
Output,
EventEmitter,
ViewChild,
} from "@angular/core";
import { EMPTY, Observable, Subject } from "rxjs";
import { NgUnsubscribe } from "../../../ng-unsubscribe";
import { map, takeUntil, withLatestFrom } from "rxjs/operators";
import { WidgetframeService } from "../../widgetframe/widgetframe.service";
import { SelectItemsDialogComponent } from "../../../components/select-items-dialog/select-items-dialog.component";
import { CurrentLocaleService } from "../../../components/i18n/currentLocale.service";
import { DEFAULT_TEXT_EDITOR_CONFIG } from "../../../components/tiny-text-editor/tiny-text-editor.component";
import { BaseConfiguration } from "../../widgetframe/widgetframe.component";
import { DataValidation } from "../../../components/validation/validation.service";
import { ValidationError } from "../../../components/validation/validators";
import { TranslateService } from "@ngx-translate/core";
import { OwlDateTimeComponent, OwlDateTimeIntl } from "ng-pick-datetime";
import { DateAdapter } from "@angular/material/core";
import { getOrDefault } from "../../widget.configuration";
import {
angularWidgetBridgeInput,
stripPasteContent,
} from "../../../components/util/util.service";
import { PageDialogComponent } from "../../../components/dialog/page-dialog.component";
import { COMMA, ENTER } from "@angular/cdk/keycodes";
import { MatChipInputEvent } from "@angular/material/chips";
import { AppContext } from "../../../components/app-context/app.context";
import { Content, Selectors } from "../../../components/app-context/api";
import { filter } from "rxjs/operators";
import { ValidationService } from "../../../components/validation/validation.service";
import { DateTimeAdapter } from "ng-pick-datetime";
import { WidgetForPipe } from "../../../widgets/container/widget-for.pipe";
import { DialogService } from "../../../components/dialog";
declare var contextPath: string;
@Component({
selector: "nm-dynamic-form-fields-component",
templateUrl: "./dynamic-form-fields.component.html",
styleUrls: ["./dynamic-form-fields.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DynamicFormFieldsComponent implements OnDestroy {
public unsubscribe = NgUnsubscribe.create();
public editorConfig: any = DEFAULT_TEXT_EDITOR_CONFIG;
public lookupOptions = {};
public dialogSelectedItems = {};
public value = {};
public description = {};
public passwordConfirmation = {};
public sourceCodeValidations = {};
public menuActions: { [key: string]: Observable<Content[]> } = {};
public buttonActions: { [key: string]: Observable<Content[]> } = {};
public resetDateValue = new Subject<any>();
public showPassword: boolean = false;
public separatorKeysCodes: number[] = [ENTER, COMMA];
public currentLocale: any;
/**
* Sets dynamic form fields.
*/
@Input()
public fields: DynamicFormField[];
/**
* Sets dynamic form configurations.
*/
@Input()
public configuration: DynamicFormConfiguration;
/**
* Sets lookup options for lookup types.
*/
@Input("lookupOptions")
public set lookupOptionsInput(value) {
angularWidgetBridgeInput(
value,
this.lookupOptionsInputChannel,
this.unsubscribe
);
}
/**
* Resets field lookup options.
*/
@Input("resetLookup")
public set resetLookup(value) {
angularWidgetBridgeInput(value, this.resetLookupChannel, this.unsubscribe);
}
/**
* Sets field value.
*/
@Input("valueSetter")
public set valueSetter(value) {
angularWidgetBridgeInput(value, this.valueSetterChannel, this.unsubscribe);
}
/**
* Sets field label.
*/
@Input("labelSetter")
public set labelSetter(value) {
angularWidgetBridgeInput(value, this.labelSetterChannel, this.unsubscribe);
}
/**
* Enables\Disables field.
*/
@Input("setDisabled")
public set setDisabled(value) {
angularWidgetBridgeInput(value, this.setDisabledChannel, this.unsubscribe);
}
/**
* Sets dialog context for field used with page-dialog type fields.
*/
@Input("dialogContext")
public set dialogContext(value) {
angularWidgetBridgeInput(
value,
this.dialogContextChannel,
this.unsubscribe
);
}
/**
* Resets all form fields.
*/
@Input("resetFormFields")
public set resetFormFields(value) {
angularWidgetBridgeInput(
value,
this.resetFormFieldsChannel,
this.unsubscribe
);
}
/**
* Shows\Hides field.
*/
@Input("setHidden")
public set setHidden(value) {
angularWidgetBridgeInput(value, this.setHiddenChannel, this.unsubscribe);
}
/**
* Resets all form fields.
*/
@Input("reset")
public set reset(value) {
angularWidgetBridgeInput(value, this.resetChannel, this.unsubscribe);
}
/**
* Reloads field lookup options.
*/
@Input("reloadLookup")
public set reloadLookup(value) {
angularWidgetBridgeInput(value, this.reloadLookupChannel, this.unsubscribe);
}
/**
* Adds fields dynamically to the form.
*/
@Input("addFields")
public set addFields(value) {
angularWidgetBridgeInput(value, this.addFieldsChannel, this.unsubscribe);
}
/**
* Removes fields dynamically from the form.
*/
@Input("removeFields")
public set removeFields(value) {
angularWidgetBridgeInput(value, this.removeFieldsChannel, this.unsubscribe);
}
/**
* Removes all fields dynamically from the form.
*/
@Input("removeAllFields")
public set removeAllFields(value) {
angularWidgetBridgeInput(
value,
this.removeAllFieldsChannel,
this.unsubscribe
);
}
/**
* Triggers validation for form fields.
*/
@Input("triggerValidation")
public set triggerValidation(value) {
angularWidgetBridgeInput(
value,
this.triggerValidationChannel,
this.unsubscribe
);
}
@ViewChild("datepicker", { static: false })
datePicker: OwlDateTimeComponent<Date>;
@ViewChild("datetimepicker", { static: false })
dateTimePicker: OwlDateTimeComponent<any>;
public lookupOptionsInputChannel = new Subject<any>();
public resetLookupChannel = new Subject<string>();
public reloadLookupChannel = new Subject<string>();
public valueSetterChannel = new Subject<{ field: string; value: any }>();
public labelSetterChannel = new Subject<{ field: string; value: any }>();
public setDisabledChannel = new Subject<{ field: string; value: boolean }>();
public dialogContextChannel = new Subject<{ field: string; context: any }>();
public resetFormFieldsChannel = new Subject<any>();
public setHiddenChannel = new Subject<{ field: string; value: boolean }>();
public resetChannel = new Subject<any>();
public addFieldsChannel = new Subject<DynamicFormField[]>();
public removeFieldsChannel = new Subject<string[]>();
public removeAllFieldsChannel = new Subject<any>();
public triggerValidationChannel = new Subject();
/**
* Emits when any field value changed.
*/
@Output("value")
public valueOutputEmitter = new EventEmitter<any>();
/**
* Emits validation result when any field value changed.
*/
@Output("valid")
public validOutputEmitter = new EventEmitter<any>();
/**
* Emits when any field is clicked.
*/
@Output("onClick")
public clickOutputEmitter = new EventEmitter<any>();
/**
* Emits when enter is pressed.
*/
@Output("onEnterPressed")
public enterPressedOutputEmitter = new EventEmitter<any>();
/**
* Emits when initialization is finished for all fields and emits also before form destroy.
*/
@Output("init")
public initOutputEmitter = new EventEmitter<any>();
/**
* Emits the dropped value when using drag and drop featuer.
*/
@Output("droppedValue")
public droppedValueEmitter = new EventEmitter<any>();
constructor(
protected cdr: ChangeDetectorRef,
protected widgetFrameService: WidgetframeService,
protected currentLocaleService: CurrentLocaleService,
protected dateAdapter: DateAdapter<Date>,
protected owldateTimeAdapter: DateTimeAdapter<any>,
protected translateService: TranslateService,
protected owlDateTimeIntl: OwlDateTimeIntl,
protected dialogService: DialogService,
protected appContext: AppContext,
protected validationService: ValidationService
) {}
public ngOnInit() {
if (!this.fields) {
return;
}
this.initFields(this.fields);
this.reloadLookupChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((identifier) => {
let field = this.fields.find((entry) => entry.field === identifier);
if (!field) {
field = this.fields.find((entry) => entry.type === "multi-toggler");
if (field) {
field = field.items.find((item) => item.field === identifier);
}
if (!field) {
console.error(
`[ReloadLookup] Cant find field with identifier ${identifier}`
);
return;
}
}
this.loadLookupData(field);
});
this.addFieldsChannel
.pipe(
withLatestFrom(this.currentLocaleService.getCurrentLocale()),
takeUntil(this.unsubscribe)
)
.subscribe((data) => {
const fields = data[0];
const locale = data[1];
this.fields.push(...fields);
this.initFields(fields);
this.updateLocale(locale, fields);
this.cdr.markForCheck();
});
this.valueSetterChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
if (!this.value) {
this.value = {};
}
const field = this.findField(data.field);
if (!field) {
return;
}
this.setFieldDescription(field, data.value);
this.value[field.field] = data.value;
this.onValueChange(field, false);
this.cdr.markForCheck();
});
this.labelSetterChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
const field = this.findField(data.field);
if (!field) {
return;
}
if (field.label != null) {
field.label = data.value;
}
this.cdr.markForCheck();
});
this.setDisabledChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
const field = this.findField(data.field);
if (!field) {
return;
}
field.disabled = data.value;
this.cdr.markForCheck();
});
this.dialogContextChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
const field = this.findField(data.field);
if (!field) {
return;
}
field.dialogContext = data.context;
this.cdr.markForCheck();
});
this.resetFormFieldsChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
this.fields.forEach((field) => {
if (field.type === "category-selector") {
field.includeSubCategories = false;
this.cdr.markForCheck();
}
});
Object.keys(this.value).forEach((field) => {
this.value[field] = null;
this.description[field] = null;
this.cdr.markForCheck();
this.resetDateValue.next(true);
});
this.dialogSelectedItems = {};
});
this.setHiddenChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
const field = this.fields.find((entry) => entry.field === data.field);
if (!field) {
console.error("Unable to find field " + data.field);
return;
}
field.hidden = data.value;
this.cdr.markForCheck();
});
this.lookupOptionsInputChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
this.setLookupOptions(data.field, data.value);
});
this.resetChannel.pipe(takeUntil(this.unsubscribe)).subscribe((reset) => {
this.value = {};
this.dialogSelectedItems = {};
this.description = {};
this.cdr.markForCheck();
});
this.resetLookupChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
this.lookupOptions[data] = [];
this.value[data] = null;
this.cdr.markForCheck();
});
this.removeFieldsChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((fields) => {
this.fields = this.fields.filter((f) => fields.indexOf(f.field) === -1);
fields.forEach((f) => delete this.value[f]);
this.cdr.markForCheck();
});
this.removeAllFieldsChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => {
this.fields = [];
this.value = {};
this.description = {};
this.cdr.markForCheck();
});
this.triggerValidationChannel
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => this.onTriggerValidation());
this.currentLocaleService
.getCurrentLocale()
.pipe(takeUntil(this.unsubscribe))
.subscribe((locale) => {
this.updateLocale(locale, this.fields);
});
this.initOutputEmitter.next({ init: true });
}
private findField(identifier: string) {
let field = this.fields.find((entry) => entry.field === identifier);
if (!field) {
field = this.fields.find((entry) => entry.type === "multi-toggler");
if (field) {
field = field.items.find((item) => item.field === identifier);
}
if (!field) {
console.error(
`Unable to find field with identifier ${identifier} `,
this.fields
);
return;
}
}
return field;
}
private initFields(fields: any[]) {
this.fields.sort((a, b) => {
if (!a.order) {
a.order = 0;
}
if (!b.order) {
b.order = 0;
}
return a.order - b.order;
});
fields.forEach((field) => {
if (field.chipRemovable === undefined || field.chipRemovable === null) {
field.chipRemovable = true;
}
if (field.clearable == null) {
field.clearable = true;
}
if (field.type === "date") {
if (!field.dateFormat) {
field.dateFormat = "dd.mm.yyyy";
}
} else if (field.type === "text") {
if (field.height) {
//This will only work if all editors are supposed to have the same height..
this.editorConfig = Object.assign(
{ height: field.height },
this.editorConfig
);
}
} else if (
field.type === "lookup" ||
field.type === "multi-lookup" ||
field.type === "context"
) {
this.loadLookupData(field);
} else if (field.type === "category-selector") {
if (field.publication) {
this.value[field.field] = {
publication: field.publication,
};
}
} else if (field.type === "boolean-indeterminate") {
this.value[field.field] = field.defaultValue;
} else if (field.type === "password-confirmation") {
if (field.hideConfirmationField === undefined) {
field.hideConfirmationField = true;
}
} else if (field.type === "radio") {
this.loadLookupData(field);
} else if (field.type === "source-code-editor") {
this.value[field.field] = field.defaultValue;
}
field.showHint = !field.hintOnFocus;
this.setFieldDefaultValue(field);
this.onValueChange(field, false);
});
this.currentLocaleService.getCurrentLocale().subscribe((locale) => {
this.currentLocale = locale;
});
}
private setFieldDefaultValue(field: DynamicFormField) {
if (!field.defaultValue) {
return;
}
this.value[field.field] = field.defaultValue;
}
private setFieldDescription(field: DynamicFormField, value: any) {
if (!field) {
return;
}
if (field.type === "user-right") {
this.description[field.field] = value
? value.identifier + " (" + value.pimRef + ")"
: null;
} else if (field.type === "worklist") {
this.description[field.field] = value ? value.name : null;
} else if (field.type === "page-dialog") {
this.description[field.field] = value
? value[field.dialogDescription]
: null;
}
}
private setLookupOptions(field, value) {
this.lookupOptions[field] = value;
this.cdr.markForCheck();
}
public getClass(field: DynamicFormField) {
return String(field.field);
}
public getIdentifierField(field: DynamicFormField) {
if (field.identifierField) {
return field.identifierField;
}
return "identifier";
}
public getDescriptionField(field: DynamicFormField) {
if (field.descriptionField) {
return field.descriptionField;
}
return "description";
}
public loadLookupData(field: DynamicFormField) {
if (field.lookupUrl) {
this.widgetFrameService.getData(field.lookupUrl).subscribe((data) => {
if (data._embedded) {
if (data.type) {
data = data._embedded[data.type];
} else {
const key = Object.keys(data._embedded)[0];
data = data._embedded[key];
}
}
if (field.lookupAllowEmpty) {
const emptyEntry = {};
emptyEntry[this.getIdentifierField(field)] = "";
emptyEntry[this.getDescriptionField(field)] = "";
data.unshift(emptyEntry);
}
this.setLookupOptions(field.field, data);
});
} else if (field.lookupOptions) {
this.setLookupOptions(field.field, field.lookupOptions);
}
}
ngOnDestroy(): void {
this.initOutputEmitter.next({ init: false, value: this.value });
this.unsubscribe.destroy();
}
onValueChange(field: DynamicFormField, isEdit: boolean = true) {
this.onTriggerValidation();
this.valueOutputEmitter.next({
value: this.value,
lastChange: field.field,
isEdit: isEdit,
});
}
onTriggerValidation() {
const valid = this.validateFields();
this.validOutputEmitter.next(valid);
}
onEditorValueChange(field: DynamicFormField, value: any) {
this.value[field.field] = value.value;
this.sourceCodeValidations[field.field] = value.isValid;
this.onValueChange(field);
}
private validateFields(): boolean {
this.validationService.validateDynamicFormFields(this.fields, this.value);
let valid = true;
for (const field of this.fields) {
// the validation service updates the errors to field.errors
// if field.errors is undefined or has no entries, the field is valid
if (Array.isArray(field.errors) && field.errors.length > 0) {
valid = false;
break;
}
if (!field.required) {
continue;
}
const value = this.value[field.field];
if (
value === undefined ||
value === null ||
value === "" ||
value.length === 0
) {
valid = false;
break;
}
if (!this.validateLocalizedText(field)) {
valid = false;
break;
}
if (!this.validateCategorySelector(field)) {
valid = false;
break;
}
if (!this.validatePasswords(field)) {
valid = false;
break;
}
if (!this.validateSourceCodeEditor(field)) {
valid = false;
break;
}
}
return valid;
}
private validateSourceCodeEditor(field) {
if (field.type !== "source-code-editor") {
return true;
}
return this.sourceCodeValidations[field.field];
}
private validateLocalizedText(field: DynamicFormField): boolean {
if (!field.localizedText) {
return true;
}
const value = this.value[field.field];
let validLocalizedText = false;
Object.keys(value).forEach((locale) => {
if (locale && value[locale].trim()) {
validLocalizedText = true;
}
});
return validLocalizedText;
}
private validateCategorySelector(field: DynamicFormField): boolean {
if (field.type !== "category-selector") {
return true;
}
const value = this.value[field.field];
if (!value.publication || !value.category || value.category.length === 0) {
return false;
}
return true;
}
private validatePasswords(field: DynamicFormField): boolean {
if (field.type !== "password") {
return true;
}
const password = this.value[field.field];
const confirmPassword = this.passwordConfirmation[field.field];
if (!password || !confirmPassword) {
return false;
}
return password === confirmPassword;
}
onClick(event, field: DynamicFormField) {
event.stopPropagation();
this.clickOutputEmitter.next(field);
}
onEnterPressed(event) {
if (event.target) {
// due to ngModelOptions: force blur event to trigger setting of asset-search´s formValue
event.target.blur();
this.enterPressedOutputEmitter.next(event.target.value);
}
}
onKeyPressedDatePicker(event) {
const key = event.key.toLowerCase();
switch (key) {
case "space":
case "spacebar":
case " ":
case "enter":
case "down":
case "arrowdown":
this.datePicker.open();
this.onEnterPressed(event);
}
}
onKeyPressedDateTimePicker(event) {
const key = event.key.toLowerCase();
switch (key) {
case "space":
case "spacebar":
case " ":
case "enter":
case "down":
case "arrowdown":
this.dateTimePicker.open();
this.onEnterPressed(event);
}
}
public onExternalModelChange(field: DynamicFormField, value: any) {
this.value[field.field] = value;
this.onValueChange(field);
}
public openSelectDialog(field: DynamicFormField) {
const dialogConfig = {
minWidth: "900px",
minHeight: "750px",
data: {
title: field.label,
itemsUrl: field.lookupUrl,
itemType: field.selectDialogItemType,
itemTypeId: this.getIdentifierField(field),
itemTypeDescription: this.getDescriptionField(field),
additionalProperties: field.additionalProperties
? field.additionalProperties
: [],
preselectedItems: this.dialogSelectedItems[field.field],
excludePreselected: false,
allowNoSelection: true,
},
};
let dialogRef = this.dialogService.open(
SelectItemsDialogComponent,
dialogConfig
);
dialogRef.afterClosed().subscribe((selectedItems) => {
if (!selectedItems) {
return;
}
if (selectedItems.length === 0) {
this.value[field.field] = null;
this.description[field.field] = null;
this.dialogSelectedItems[field.field] = null;
} else if (selectedItems.length > 0) {
this.dialogSelectedItems[field.field] = selectedItems;
let values = [];
let descriptions = [];
selectedItems.forEach((item) => {
values.push(item[this.getIdentifierField(field)]);
descriptions.push(item[this.getDescriptionField(field)]);
});
this.value[field.field] = values.toString();
this.description[field.field] = descriptions.join(", ");
this.onValueChange(field);
}
this.cdr.markForCheck();
});
}
public openUserRightDialog(field: DynamicFormField) {
const context = { userRightsUrl: field.url };
this.pageDialog(field, "core", "select-userRight", context, "650px");
}
public openWorklistDialog(field: DynamicFormField) {
this.pageDialog(field, "core", "select-worklist", null, "650px");
}
public openPageDialog(field: DynamicFormField) {
this.pageDialog(
field,
field.dialogModule,
field.dialogIdentifier,
field.dialogContext,
"900px"
);
}
private pageDialog(
field: DynamicFormField,
module: string,
identifier: string,
context: any,
minWidth: string
) {
if (field.disabled) {
return;
}
const dialogRef = this.dialogService.open(PageDialogComponent, {
minWidth: minWidth,
height: "700px",
data: {
title: field.label,
acceptEnabled: true,
customAcceptIcon: true,
acceptIcon: "check",
acceptText: "button.accept",
cancelEnabled: true,
cancelText: "button.cancel",
module: module,
identifier: identifier,
context: context,
},
});
dialogRef.afterClosed().subscribe((data) => {
if (!data) {
return;
}
this.setFieldDescription(field, data);
this.value[field.field] = data;
this.onValueChange(field);
this.cdr.markForCheck();
});
}
public clearField(field: DynamicFormField) {
this.value[field.field] = null;
this.description[field.field] = null;
this.dialogSelectedItems[field.field] = null;
this.onValueChange(field);
this.cdr.markForCheck();
}
private updateLocale(locale: string, fields: DynamicFormField[]) {
fields.forEach((field) => {
if (field.translations) {
const translations = field.translations;
if (translations[locale]) {
field.label = translations[locale];
}
}
if (field.hintTranslations) {
const translations = field.hintTranslations;
if (translations[locale]) {
field.hint = translations[locale];
}
}
});
this.dateAdapter.setLocale(locale);
this.owldateTimeAdapter.setLocale(locale);
this.owlDateTimeIntl.cancelBtnLabel =
this.translateService.instant("button.cancel");
this.owlDateTimeIntl.setBtnLabel =
this.translateService.instant("button.accept");
}
public includeSubCategoriesValueChanged(field: DynamicFormField, value: any) {
if (this.value[field.field]) {
this.value[field.field].includeSubCategories = value;
} else {
this.value[field.field] = { includeSubCategories: value };
}
field.includeSubCategories = value;
}
public getCategoryUrl(field: DynamicFormField) {
return getOrDefault(field.categoryUrl, "");
}
public booleanValueChanged(field: DynamicFormField) {
switch (this.value[field.field]) {
case true: {
this.value[field.field] = false;
break;
}
case false: {
this.value[field.field] = null;
break;
}
case undefined:
case null: {
this.value[field.field] = true;
break;
}
}
this.onValueChange(field);
}
addChip(event: MatChipInputEvent, field: DynamicFormField): void {
const input = event.input;
const value = event.value;
if ((value || "").trim()) {
if (this.value[field.field]) {
this.value[field.field].push({
identifier: value.trim(),
description: value.trim(),
});
} else {
this.value[field.field] = [
{ identifier: value.trim(), description: value.trim() },
];
}
this.onValueChange(field);
}
if (input) {
input.value = "";
}
}
removeChip(chip: any, field: DynamicFormField): void {
if (!this.value[field.field]) {
return;
}
const index = this.value[field.field].indexOf(chip);
if (index >= 0) {
this.value[field.field].splice(index, 1);
this.onValueChange(field);
}
}
public onItemDropped(ev, field) {
const dropValueIdentifier = getOrDefault(
field.dropValueIdentifier,
"identifier"
);
const myValue = ev.dragData.source[dropValueIdentifier];
this.droppedValueEmitter.emit(myValue);
const cursorPosition = ev.owner.element.nativeElement.selectionStart;
if (this.value[field.field]) {
var firstHalf = this.value[field.field].substring(0, cursorPosition);
if (firstHalf) {
firstHalf += " ";
}
var secondHalf = this.value[field.field].substring(
cursorPosition,
this.value[field.field].length
);
if (secondHalf) {
secondHalf = " " + secondHalf;
}
this.value[field.field] = firstHalf + myValue + secondHalf;
} else {
this.value[field.field] = myValue;
}
this.onValueChange(field);
}
public onFileUpload(field, uploadedFile: any) {
if (uploadedFile) {
this.value[field.field] = uploadedFile;
} else {
this.value[field.field] = null;
}
this.onValueChange(field);
}
public changePassword(field) {
field.hideConfirmationField = !field.hideConfirmationField;
field.required = !field.required;
field.disabled = !field.disabled;
if (field.hideConfirmationField) {
this.value[field.field] = null;
this.passwordConfirmation[field.field] = null;
}
this.onValueChange(field);
this.clickOutputEmitter.next(field);
}
public getLocalizedValue(field: DynamicFormField): string {
if (!field || this.value[field.field] == null) {
return "";
}
if (field.localizedText) {
return this.value[field.field][this.currentLocale];
}
return this.value[field.field];
}
private initializeMenuActions(field: DynamicFormField) {
if (field.menuInteractions) {
this.menuActions[field.field] = this.appContext.browserContext
.subscribe(field.menuInteractions)
.pipe(
takeUntil(this.unsubscribe),
filter((contents) => !!contents),
map((contents) => {
const interactions = contents
.filter(
(entry) =>
entry.showAsButton == null || entry.showAsButton === false
)
.sort((l, r) => (r.order || 0) - (l.order || 0));
return interactions;
})
);
} else {
this.menuActions[field.field] = EMPTY;
}
}
private initializeButtonActions(field: DynamicFormField) {
if (field.menuInteractions) {
this.buttonActions[field.field] = this.appContext.browserContext
.subscribe(field.menuInteractions)
.pipe(
takeUntil(this.unsubscribe),
filter((contents) => !!contents),
map((contents) => {
return contents
.filter((entry) => entry?.showAsButton === true)
.sort((l, r) => (r.order || 0) - (l.order || 0));
})
);
} else {
this.buttonActions[field.field] = EMPTY;
}
}
public getMenuActions(field: DynamicFormField): Observable<Content[]> {
if (this.menuActions[field.field]) {
return this.menuActions[field.field];
}
if (field.menuInteractions) {
this.initializeMenuActions(field);
} else {
this.menuActions[field.field] = EMPTY;
}
return this.menuActions[field.field];
}
public getButtonActions(field: DynamicFormField): Observable<Content[]> {
if (this.buttonActions[field.field]) {
return this.buttonActions[field.field];
}
if (field.menuInteractions) {
this.initializeButtonActions(field);
} else {
this.buttonActions[field.field] = EMPTY;
}
return this.buttonActions[field.field];
}
public getInteractions(
field: DynamicFormField
): { menu: Observable<Content[]>; buttons: Observable<Content[]> } | null {
if (field.menuInteractions == null) {
return null;
}
return {
menu: this.getMenuActions(field),
buttons: this.getButtonActions(field),
};
}
onKeyPressed(event: KeyboardEvent, field) {
if (field.tab && event.code !== "Tab") {
event.preventDefault();
return false;
}
return;
}
onFocusOut(field: DynamicFormField) {
field.showHint = !field.hintOnFocus;
}
onFocusIn(field: DynamicFormField) {
field.showHint = true;
}
getPreferredPosition(field: DynamicFormField) {
return field.additionalProperties &&
field.additionalProperties.preferredPosition
? field.additionalProperties.preferredPosition
: ["right-below", "right", "bottom"];
}
resetDateTime(field, event) {
event.stopPropagation();
this.value[field.field] = null;
this.onValueChange(field);
}
handlePaste(event: ClipboardEvent) {
stripPasteContent(event);
}
}
export interface DynamicFormConfiguration extends BaseConfiguration {
/**
* Array of dynamic form fields to be added in the form.
*/
fields: DynamicFormField[];
}
export interface DynamicFormField {
/**
* Type of the field. supported types are: boolean, boolean-indeterminate, category-selector,
* chip-list, date, date-time, file-upload, heading, lookup, multi-lookup, number,
* number-decimal, password, plain, select-dialog, text, textarea, toggler, multi-toggler, user-right, worklist, source-code-editor
*/
type: string;
/**
* Unique field identifier.
*/
field: string;
/**
* collection of all data for multi-components
*/
items?: DynamicFormField[];
/**
* Localized key for field label
*/
label: string;
/**
* URL to load lookup options from. used with these types: lookup, multi-lookup, select-dialog.
* (See also lookupOptions)
*/
lookupUrl?: string;
/**
* Array of static lookup options. used with these types: lookup, multi-lookup, select-dialog.
* (See also lookupUrl)
*/
lookupOptions?: any[];
/**
* Allows empty selection for lookup types. @default(false)
*/
lookupAllowEmpty?: boolean;
/**
* Item type used for select-dialog type.
*/
selectDialogItemType?: string;
/**
* Enables\Disables the field.
*/
disabled?: boolean;
/**
* Makes the field required or optional.
*/
required?: boolean;
/**
* Identifier field of the object. it is used with 'lookup' and 'multi-lookup' types
* to define identifier field. @default(identifier)
*/
identifierField?: string;
/**
* Description field of the object. it is used with 'lookup' and 'multi-lookup' types
* to define description field. @default(description)
*/
descriptionField?: string;
/**
* Field height. used with 'text' type.
*/
height?: number;
/**
* Hides or shows the field.
*/
hidden?: boolean;
/**
* Order of the field in the form. @default(0)
*/
order?: number;
/**
* If not set the version select lookup will be shown,
* otherwise the provided version will be used and the category select lookup only will be shown.
* Used with type 'category-selector'.
*/
publication?: string;
/**
* Allows multiple category selection. Used with type 'category-selector'. @default(false)
*/
multiCategorySelect?: boolean;
/**
* URL used to load version from.
* default is 'category-select-publications' for publications
* and 'category-select-asset-trees' for asset-trees
* Used with type 'category-selector'.
*/
categoryUrl?: string;
/**
* Label translation in other locales.
*/
translations?: any;
/**
* hint translation in other locales.
*/
hintTranslations?: any;
/**
* Validations to be applied on field input.
*/
validations?: DataValidation[];
/**
* Date format. used with 'date' type. @default(dd.mm.yyyy)
*/
dateFormat?: string;
/**
* Shows\Hides include sub-categories checkbox. used with 'category-selector' type. @default(false)
*/
showIncludeSubCategories?: boolean;
/**
* Includes sub-categories value. used with 'category-selector' type.
*/
includeSubCategories?: boolean;
/**
* Applies channel types filter on versions. supported values are PURCHASE, SALES, ARCHIVE.
* used with 'category-selector' type.
*/
filterChannelTypes?: string[];
/**
* Array of additional properties to view in select dialog list. used with 'select-dialog' type.
*/
additionalProperties?: any;
/**
* List folder type. used with 'worklist' type.
*/
listfolderType?: string;
/**
* List folder data type. used with 'worklist' type.
*/
dataType?: string;
/**
* Shows floating label for the field. @default(false)
*/
floatingLabel?: boolean;
/**
* The target tab. used in dynamic forms with mutliple tabs.
* supported tabs are localizedtext, listFolder, userRights
*/
tab?: string;
/**
* This property shouldn't be added in configuration. It is only used internally.
*/
value?: any;
/**
* Initial value of the field.
*/
defaultValue?: any;
/**
* Makes the field Readonly or editable. @default(false)
*/
readonly?: boolean;
/**
* Sets autosize property for type 'textarea'.
*/
autosize?: boolean;
/**
* Shows clear selection button. used with 'lookup' and 'multi-lookup' types. @default(true)
*/
clearable?: boolean;
/**
* Sets maximum characters length for text types.
*/
maxLength?: number;
/**
* URL to load user rights. used with 'user-right' type.
*/
url?: string;
/**
* Comma-separated list of supported file types. used with 'file-upload' type.
*/
supportedFileTypes?: string;
/**
* File upload url. used with 'file-upload type.
*/
fileUploadUrl?: string;
/**
* Sets field as localized text. @default(false)
*/
localizedText?: boolean;
/**
* Identifiers of the interactions that are supposed to be loaded for the menu.
*/
menuInteractions?: Selectors;
/**
* List of field validation errors.
*/
errors?: ValidationError[];
/**
* Array of supported MIME types. used with 'file-upload' type.
*/
supportedMimeTypes?: string[];
/**
* Makes chips removable in 'chip-list' type. @default(true)
*/
chipRemovable?: boolean;
/**
* Shows\Hides edit password button. used with 'password' type. @default(false)
*/
showEditpasswordButton?: boolean;
/**
* Hides\Shows second password confirmation field. used with 'password' type. @default(true)
*/
hideConfirmationField?: boolean;
/**
* Localized key for password confirmation lable. used with 'password' type.
*/
confirmationLabel?: string;
/**
* Auto focus field.
*/
autofocus?: boolean;
/**
* Localized key for field hint.
*/
hint?: string;
/**
* Shows hint on focus.
*/
hintOnFocus?: boolean;
/**
* This property shouldn't be added in configuration. It is only used internally.
*/
showHint?: boolean;
/**
* Identifier field that will be used to get the value while dropping it in the textarea field.
*/
dropValueIdentifier?: string;
/**
* min rows for textarea field
*/
autosizeMinRows?: string;
/**
* Source code programming language. It is used with type = 'source-code-editor'.
*/
sourceCodeLanguage?: string;
/**
* Dialog module for page-dialog type.
*/
dialogModule?: string;
/**
* Dialog identifier for page-dialog type.
*/
dialogIdentifier?: string;
/**
* Dialog data for page-dialog type.
*/
dialogContext?: any;
/**
* Dialog data property used to set the dialog description when closed for page-dialog type.
*/
dialogDescription?: any;
/**
* Enable/Disable editor minimap for source-code-editor type.
*/
enableMinimap?: boolean;
/**
* Triggers the display of Tiny editor implmentation in its onInit callback
*/
showEditorOnInit?: boolean;
}