From 62c7479a0ecc8710a8a205249a54cce9685117b0 Mon Sep 17 00:00:00 2001 From: "y.yerli" Date: Tue, 7 Nov 2023 19:59:53 +0300 Subject: [PATCH] Fix #323 - Dynamic dropdown --- core/app/common/src/lib/record/field.model.ts | 104 +++++- .../saved-filter/saved-filter-record.store.ts | 16 +- .../lib/fields/base/base-enum.component.ts | 343 ++++++++---------- .../lib/fields/base/base-field.component.ts | 77 ++-- .../fields/base/base-multienum.component.ts | 44 ++- .../templates/edit/dropdownenum.component.ts | 22 +- .../templates/edit/dynamicenum.component.ts | 24 +- .../enum/templates/edit/enum.component.ts | 41 +-- .../fields/field-logic/field-logic.manager.ts | 22 +- .../templates/edit/multienum.component.ts | 37 +- .../templates/filter/multienum.component.ts | 51 +-- .../templates/edit/radioenum.component.ts | 6 +- 12 files changed, 403 insertions(+), 384 deletions(-) diff --git a/core/app/common/src/lib/record/field.model.ts b/core/app/common/src/lib/record/field.model.ts index 038ad645c3..795db3ff3a 100644 --- a/core/app/common/src/lib/record/field.model.ts +++ b/core/app/common/src/lib/record/field.model.ts @@ -24,13 +24,15 @@ * the words "Supercharged by SuiteCRM". */ -import {SearchCriteriaFieldFilter} from '../views/list/search-criteria.model'; +import {isArray, isEqual, isObject, isString, uniq} from 'lodash-es'; import {BehaviorSubject, Observable} from 'rxjs'; import {AsyncValidatorFn, FormArray, FormControl, ValidatorFn} from '@angular/forms'; +import {SearchCriteriaFieldFilter} from '../views/list/search-criteria.model'; import {Record} from './record.model'; import {FieldLogicMap} from '../actions/field-logic-action.model'; import {ObjectMap} from '../types/object-map'; import {ViewMode} from '../views/view.model'; +import {deepClone} from '../utils/object-utils'; export type DisplayType = 'none' | 'show' | 'readonly' | 'inline' | 'disabled' | 'default'; @@ -97,7 +99,7 @@ export interface FieldDefinition { default?: string; modes?: ViewMode[]; relationship?: string; - relationshipMetadata?: RelationshipMetadata + relationshipMetadata?: RelationshipMetadata; [key: string]: any; } @@ -128,8 +130,8 @@ export interface FieldMetadata { digits?: number; isBaseCurrency?: boolean; labelDisplay?: string; - options$?: Observable; extraOptions?: Option[]; + conditionalOptions?: { [value: string]: Option }; onClick?: FieldClickCallback; tinymce?: any; date_time_format?: string; @@ -157,6 +159,16 @@ export interface AttributeDependency { types: string[]; } +export interface FieldValue { + value?: string; + valueList?: string[]; + valueObject?: any; +} + +export interface FieldValueMap { + [key: string]: FieldValue; +} + export interface Field { type: string; value?: string; @@ -185,11 +197,14 @@ export interface Field { asyncValidators?: AsyncValidatorFn[]; valueSubject?: BehaviorSubject; valueChanges$?: Observable; + options?: Option[]; + optionsState?: Option[]; + optionsSubject?: BehaviorSubject; + optionsChanges$?: Observable; fieldDependencies?: ObjectMap; attributeDependencies?: AttributeDependency[]; logic?: FieldLogicMap; displayLogic?: FieldLogicMap; - previousValue?: string; } export class BaseField implements Field { @@ -212,6 +227,9 @@ export class BaseField implements Field { attributes?: FieldAttributeMap; valueSubject?: BehaviorSubject; valueChanges$?: Observable; + optionsState?: Option[]; + optionsSubject?: BehaviorSubject; + optionsChanges$?: Observable; fieldDependencies: ObjectMap = {}; attributeDependencies: AttributeDependency[] = []; logic?: FieldLogicMap; @@ -225,6 +243,9 @@ export class BaseField implements Field { constructor() { this.valueSubject = new BehaviorSubject({} as FieldValue); this.valueChanges$ = this.valueSubject.asObservable(); + + this.optionsSubject = new BehaviorSubject(this.optionsState); + this.optionsChanges$ = this.optionsSubject.asObservable(); } get value(): string { @@ -232,13 +253,19 @@ export class BaseField implements Field { } set value(value: string) { - const changed = value !== this.valueState; + if (!isString(value)) { + this.setValue(value); + return; + } - this.valueState = value; + const valueClean: string = value.trim(); - if (changed) { - this.emitValueChanges(); + if (isEqual(this.valueState, valueClean)) { + return; } + + this.valueState = valueClean; + this.emitValueChanges(); } get valueList(): string[] { @@ -246,9 +273,18 @@ export class BaseField implements Field { } set valueList(value: string[]) { + if (!isArray(value)) { + this.setValue(value); + return; + } - this.valueListState = value; + const valueListClean: string[] = uniq(deepClone(value)); + if (isEqual(this.valueListState, valueListClean)) { + return; + } + + this.valueListState = valueListClean; this.emitValueChanges(); } @@ -257,7 +293,16 @@ export class BaseField implements Field { } set valueObject(value: any) { - this.valueObjectState = value; + if (!isObject(value)) { + this.setValue(value); + return; + } + + if (isEqual(this.valueObjectState, value)) { + return; + } + + this.valueObjectState = deepClone(value); this.emitValueChanges(); } @@ -266,23 +311,44 @@ export class BaseField implements Field { } set valueObjectArray(value: ObjectMap[]) { + if (isEqual(this.valueObjectArrayState, value)) { + return; + } + this.valueObjectArrayState = value; this.emitValueChanges(); } - protected emitValueChanges() { + get options(): Option[] { + return this.optionsState; + } + + set options(options: Option[]) { + this.optionsState = options; + this.emitOptionsChanges(); + } + + public setValue(value: string | string[] | any): void { + if (isString(value)) { + this.value = value; + } else if (isArray(value)) { + this.valueList = value; + } else if (isObject(value)) { + this.valueObject = value; + } else { + this.value = value?.toString() ?? ''; + } + } + + protected emitValueChanges(): void { this.valueSubject.next({ value: this.valueState, valueList: this.valueListState, valueObject: this.valueObjectState - }) + }); } -} -export interface FieldValue { - value?: string; - valueList?: string[]; - valueObject?: any; + protected emitOptionsChanges(): void { + this.optionsSubject.next(deepClone(this.optionsState)); + } } - - diff --git a/core/app/core/src/lib/containers/list-filter/store/saved-filter/saved-filter-record.store.ts b/core/app/core/src/lib/containers/list-filter/store/saved-filter/saved-filter-record.store.ts index 82328aea94..f07f524c30 100644 --- a/core/app/core/src/lib/containers/list-filter/store/saved-filter/saved-filter-record.store.ts +++ b/core/app/core/src/lib/containers/list-filter/store/saved-filter/saved-filter-record.store.ts @@ -96,6 +96,7 @@ export class SavedFilterRecordStore extends RecordStore { /** * Get search fields metadata + * * @returns SearchMetaFieldMap */ public getSearchFields(): SearchMetaFieldMap { @@ -104,6 +105,7 @@ export class SavedFilterRecordStore extends RecordStore { /** * Set search fields metadata + * * @param {object} searchFields SearchMetaFieldMap */ public setSearchFields(searchFields: SearchMetaFieldMap): void { @@ -112,6 +114,7 @@ export class SavedFilterRecordStore extends RecordStore { /** * Get list fields metadata + * * @returns SearchMetaFieldMap */ public getListColumns(): ColumnDefinition[] { @@ -120,6 +123,7 @@ export class SavedFilterRecordStore extends RecordStore { /** * Set list fields metadata + * * @param {object} listColumns SearchMetaFieldMap */ public setListColumns(listColumns: ColumnDefinition[]): void { @@ -153,6 +157,7 @@ export class SavedFilterRecordStore extends RecordStore { * Extract base record * * @returns {object} Record + * @param record */ extractBaseRecord(record: SavedFilter): Record { if (!record) { @@ -170,7 +175,7 @@ export class SavedFilterRecordStore extends RecordStore { module: record.module, key: record.key, searchModule: record.searchModule, - criteria: criteria, + criteria, attributes: record.attributes, } as SavedFilter); } @@ -206,11 +211,12 @@ export class SavedFilterRecordStore extends RecordStore { /** * Init Order by options using list view columns set as default + * * @param record */ protected initOrderByOptions(record: SavedFilter): void { if (!record.fields || !record.fields.orderBy) { - return + return; } record.fields.orderBy.metadata = record.fields.orderBy.metadata || {} as FieldMetadata; @@ -228,14 +234,16 @@ export class SavedFilterRecordStore extends RecordStore { options.push({ value: column.fieldDefinition.name || column.name, label - }) + }); }); - record.fields.orderBy.metadata.options$ = of(options).pipe(shareReplay()); + record.fields.orderBy.definition.options = null; + record.fields.orderBy.options = options; } /** * Get criteria from filter + * * @param filter */ protected getCriteria(filter: SavedFilter): SearchCriteria { diff --git a/core/app/core/src/lib/fields/base/base-enum.component.ts b/core/app/core/src/lib/fields/base/base-enum.component.ts index cc9f1eb5db..7df403b302 100644 --- a/core/app/core/src/lib/fields/base/base-enum.component.ts +++ b/core/app/core/src/lib/fields/base/base-enum.component.ts @@ -24,17 +24,13 @@ * the words "Supercharged by SuiteCRM". */ -import {BaseFieldComponent} from './base-field.component'; +import {isEmpty, isNull, isObject} from 'lodash-es'; +import {combineLatest, of, Subscription} from 'rxjs'; import {Component, OnDestroy, OnInit} from '@angular/core'; -import {Subscription} from 'rxjs'; -import {Field, FieldDefinition, isEmptyString, isVoid, Option} from 'common'; +import {deepClone, Field, FieldValue, isEmptyString, isVoid, Option} from 'common'; +import {BaseFieldComponent} from './base-field.component'; import {DataTypeFormatter} from '../../services/formatters/data-type.formatter.service'; -import { - LanguageListStringMap, - LanguageStore, - LanguageStringMap, - LanguageStrings -} from '../../store/language/language.store'; +import {LanguageListStringMap, LanguageStore, LanguageStringMap} from '../../store/language/language.store'; import {FieldLogicManager} from '../field-logic/field-logic.manager'; import {FieldLogicDisplayManager} from '../field-logic-display/field-logic-display.manager'; @@ -42,12 +38,11 @@ import {FieldLogicDisplayManager} from '../field-logic-display/field-logic-displ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnDestroy { selectedValues: Option[] = []; valueLabel = ''; - optionsMap: LanguageStringMap; + optionsMap: LanguageStringMap = {}; options: Option[] = []; labels: LanguageStringMap; protected subs: Subscription[] = []; protected mappedOptions: { [key: string]: Option[] }; - protected isDynamicEnum = false; constructor( protected languages: LanguageStore, @@ -59,40 +54,11 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD } ngOnInit(): void { - super.ngOnInit(); - const options$ = this?.field?.metadata?.options$ ?? null; - if (options$) { - this.subs.push(this.field.metadata.options$.subscribe((options: Option[]) => { - this.buildProvidedOptions(options); - - this.initValue(); - - })); - return; + this.subscribeValueAndOptionChanges(); - } - - const options = this?.field?.definition?.options ?? null; - if (options) { - this.subs.push(this.languages.vm$.subscribe((strings: LanguageStrings) => { - - this.buildAppStringListOptions(strings.appListStrings); - this.initValue(); - - })); - } - - if (!options && !options$) { - this.initValue(); - } - - } - - ngOnDestroy(): void { - this.isDynamicEnum = false; - this.subs.forEach(sub => sub.unsubscribe()); + this.initAsEnumAndDynamicIf(); } getInvalidClass(): string { @@ -102,93 +68,24 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD return ''; } - protected buildProvidedOptions(options: Option[]): void { - this.options = options; - this.optionsMap = {}; - - options.forEach(option => { - this.optionsMap[option.value] = option.label; - }); - - } - - protected buildAppStringListOptions(appStrings: LanguageListStringMap): void { - - this.optionsMap = {} as LanguageStringMap; - this.addExtraOptions(); - - if (appStrings && this.field.definition.options && appStrings[this.field.definition.options]) { - const options = appStrings[this.field.definition.options] as LanguageStringMap; - - if (this.options && Object.keys(this.options)) { - this.optionsMap = {...this.optionsMap, ...options}; - } - } - - this.buildOptionsArray(appStrings); - } - - protected addExtraOptions(): void { - const extraOptions = (this.field.metadata && this.field.metadata.extraOptions) || []; - - extraOptions.forEach((item: Option) => { - if (isVoid(item.value)) { - return; - } - - let label = item.label || ''; - if (item.labelKey) { - label = this.languages.getFieldLabel(item.labelKey); - } - - this.optionsMap[item.value] = label; - }); - } - - protected buildOptionsArray(appStrings: LanguageListStringMap): void { - - this.options = []; - Object.keys(this.optionsMap).forEach(key => { - - this.options.push({ - value: key, - label: this.optionsMap[key] - }); - }); - - if (this.isDynamicEnum) { - this.buildDynamicEnumOptions(appStrings); - } - } protected initValue(): void { + const fieldValue = this.field.value ?? ''; - this.selectedValues = []; - - if (!this.field.value) { + if (isEmptyString(fieldValue)) { this.initEnumDefault(); return; } - if (typeof this.field.value !== 'string') { + if ( + !isEmpty(this.field.options) + && !this.field.options.find(option => option.value === fieldValue) + ) { + this.updateInternalState(); return; } - if (!this.optionsMap) { - return; - } - - if (typeof this.optionsMap[this.field.value] !== 'string') { - return; - } - - if (this.field.value) { - this.valueLabel = this.optionsMap[this.field.value]; - this.selectedValues.push({ - value: this.field.value, - label: this.valueLabel - }); - } + this.updateInternalState(fieldValue); } /** @@ -198,104 +95,159 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD * @description set default enum value, if defined in vardefs * */ protected initEnumDefault(): void { + const defaultValue = this.field.definition?.default; - if (!isEmptyString(this.record?.id)) { + if ( + isEmptyString(this.record?.id) + && !isVoid(defaultValue) + ) { + this.updateInternalState(defaultValue); + return; + } - this.field?.formControl.setValue(''); + this.updateInternalState(); + } - return; + protected updateInternalState(value = ''): void { + this.selectedValues = []; + const option = this.buildOptionFromValue(value); + if (!isEmptyString(option.value)) { + this.selectedValues.push(option); } + this.valueLabel = option.label; + this.setFormControlValue(option.value); + } + + protected buildOptionFromValue(value: string): Option { + const option: Option = {value: '', label: ''}; - let defaultVal = this.field?.definition?.default; - if (typeof defaultVal === 'string') { - defaultVal = defaultVal.trim(); + if (isNull(value)) { + return option; } - if (!defaultVal) { - this.field.formControl.setValue(''); - return; + option.value = (typeof value !== 'string' ? JSON.stringify(value) : value).trim(); + option.label = option.value; + + const valueLabel = this.optionsMap[option.value] ?? option.label; + if (isObject(valueLabel)) { + return option; } + option.label = (typeof valueLabel !== 'string' ? JSON.stringify(valueLabel) : valueLabel).trim(); - this.selectedValues.push({ - value: defaultVal, - label: this.optionsMap[defaultVal] - }); + return option; + } + + protected buildProvidedOptions(options: Option[]): void { + this.addExtraOptions(options); + this.addConditionalOptions(options); + + this.optionsMap = {} as LanguageStringMap; + options.forEach(option => this.addToOptionMap(option)); - this.initEnumDefaultFieldValues(defaultVal); + this.options = Object.entries(this.optionsMap) + .map(([value, label]) => ({value, label})); } - protected initEnumDefaultFieldValues(defaultVal: string): void { + protected addExtraOptions(options: Option[]): void { + const extraOptions = this.field.metadata?.extraOptions ?? []; + + extraOptions.forEach(extraOption=>options.push(extraOption)); + } - if (this.field.type === 'multienum') { - const defaultValues = this.selectedValues.map(option => option.value); - this.field.valueList = defaultValues; - this.field.formControl.setValue(defaultValues); + protected addConditionalOptions(options: Option[]): void { + const conditionalOptions = this.field.metadata?.conditionalOptions ?? {}; - } else { - this.field.value = defaultVal; - this.field.formControl.setValue(defaultVal); - } - this.field.formControl.markAsDirty(); + Object.values(conditionalOptions).forEach(extraOption => options.push(extraOption)); } - protected checkAndInitAsDynamicEnum(): void { + protected addToOptionMap(option: Option): void { + if (isVoid(option.value)) { + return; + } + + let label = option.label || ''; + if (option.labelKey) { + label = this.languages.getFieldLabel(option.labelKey, this.record.module) || option.labelKey; + } + + this.optionsMap[option.value] = label; + } - const definition = (this.field && this.field.definition) || {} as FieldDefinition; - const dynamic = (definition && definition.dynamic) || false; - const parentEnumKey = (definition && definition.parentenum) || ''; - const fields = (this.record && this.record.fields) || null; + private subscribeValueAndOptionChanges(): void { + this.subscribeValueChanges(); - if (dynamic && parentEnumKey && fields) { - this.isDynamicEnum = true; - const parentEnum: Field = fields[parentEnumKey]; - if (parentEnum) { - this.subscribeToParentValueChanges(parentEnum); + const valueAndOptionChanges = combineLatest([ + this.field.valueChanges$, + this.field.optionsChanges$ + ]).subscribe(([_, options]) => { + if (options) { + this.buildProvidedOptions(options); } - } + this.initValue(); + }); + + this.subs.push(valueAndOptionChanges); } - protected buildDynamicEnumOptions(appStrings: LanguageListStringMap): void { + private initAsEnumAndDynamicIf(): void { + const definition = (this.field.definition ?? {}); + const dynamic = definition?.dynamic ?? false; + const parentEnumKey = definition?.parentenum ?? ''; + const fields = this.record?.fields ?? {}; - const parentEnum = this.record.fields[this.field.definition.parentenum]; + const parentEnum = dynamic ? fields[parentEnumKey] ?? null : null; - if (parentEnum) { + this.subscribeToParentValueChanges(parentEnum); + } - const parentOptionMap: LanguageStringMap = appStrings[parentEnum.definition.options] as LanguageStringMap; + private subscribeToParentValueChanges(parentEnum: Field | null): void { + const parentValueChangesSubscription = combineLatest([ + parentEnum?.valueChanges$ ?? of({} as FieldValue), + this.languages.vm$ + ]).subscribe(([parentFieldValue, languageStrings]) => { + const appStrings = languageStrings.appListStrings ?? {}; + const optionsKey = this.field.definition?.options ?? ''; - if (parentOptionMap && Object.keys(parentOptionMap).length !== 0) { + const optionsMap = (appStrings[optionsKey] ?? {}) as LanguageStringMap; + let options: Option[] = Object.entries(optionsMap) + .map(([value, label]) => ({value, label})); - this.mappedOptions = this.createParentChildOptionsMap(parentOptionMap, this.options); + options = this.getDynamicEnumOptions(parentEnum,appStrings,parentFieldValue,options); - let parentValues: string[] = []; - if (parentEnum.definition.type === 'multienum') { - parentValues = parentEnum.valueList; - } else { - parentValues.push(parentEnum.value); - } - this.options = this.filterMatchingOptions(parentValues); + this.field.options = options; + }); - } - } + this.subs.push(parentValueChangesSubscription); } - protected filterMatchingOptions(values: string[]): Option[] { + private getDynamicEnumOptions( + parentEnum: Field | null, + appStrings: LanguageListStringMap, + parentFieldValue: FieldValue, + prevOptions: Option[] + ): Option[] { + if (isEmpty(parentEnum)) { + return prevOptions; + } - let filteredOptions: Option[] = []; + const parentOptionsKey = parentEnum?.definition.options ?? ''; - if (!values || !values.length) { - return []; + const parentOptionMap: LanguageStringMap = (appStrings[parentOptionsKey] ?? {}) as LanguageStringMap; + if (isEmpty(parentOptionMap)) { + return prevOptions; } - values.forEach(value => { - if (!this.mappedOptions[value]) { - return; - } - filteredOptions = filteredOptions.concat([...this.mappedOptions[value]]); - }); + this.mappedOptions = this.createParentChildOptionsMap(parentOptionMap, prevOptions); + let parentValues: string[] = []; + if (!isEmpty(parentFieldValue.valueList)) { + parentValues = parentFieldValue.valueList; + } else if (parentEnum.value) { + parentValues = [parentFieldValue.value ?? '']; + } - return filteredOptions; + return this.filterMatchingOptions(parentValues); } - protected createParentChildOptionsMap(parentOptions: LanguageStringMap, childOptions: Option[]): { [key: string]: Option[] } { + private createParentChildOptionsMap(parentOptions: LanguageStringMap, childOptions: Option[]): { [key: string]: Option[] } { const mappedOptions: { [key: string]: Option[] } = {}; Object.keys(parentOptions).forEach(key => { mappedOptions[key] = childOptions.filter( @@ -305,24 +257,19 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD return mappedOptions; } - protected subscribeToParentValueChanges(parentEnum: Field): void { - if (parentEnum.formControl) { - this.subs.push(parentEnum.formControl.valueChanges.subscribe(values => { - - if (typeof values === 'string') { - values = [values]; - } - - // Reset selected values on Form Control - this.field.value = ''; - this.field.formControl.setValue(''); + private filterMatchingOptions(values: string[]): Option[] { + if (isEmpty(values)) { + return []; + } - // Rebuild available enum options - this.options = this.filterMatchingOptions(values); + let filteredOptions: Option[] = []; + values.forEach(value => { + if (!this.mappedOptions[value]) { + return; + } + filteredOptions = filteredOptions.concat([...deepClone(this.mappedOptions[value])]); + }); - this.initValue(); - })); - } + return filteredOptions; } - } diff --git a/core/app/core/src/lib/fields/base/base-field.component.ts b/core/app/core/src/lib/fields/base/base-field.component.ts index 98d1b4dc03..bac845c625 100644 --- a/core/app/core/src/lib/fields/base/base-field.component.ts +++ b/core/app/core/src/lib/fields/base/base-field.component.ts @@ -24,12 +24,13 @@ * the words "Supercharged by SuiteCRM". */ +import { isEqual } from 'lodash-es'; +import { Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import { AttributeDependency, deepClone, Field, FieldValue, isVoid, ObjectMap, Record, ViewMode } from 'common'; import {FieldComponentInterface} from './field.interface'; -import {AttributeDependency, Field, isVoid, ObjectMap, Record, ViewMode} from 'common'; -import {Subscription} from 'rxjs'; import {DataTypeFormatter} from '../../services/formatters/data-type.formatter.service'; -import {debounceTime} from 'rxjs/operators'; import {FieldLogicManager} from '../field-logic/field-logic.manager'; import {FieldLogicDisplayManager} from '../field-logic-display/field-logic-display.manager'; @@ -43,6 +44,11 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe dependentFields: ObjectMap = {}; dependentAttributes: AttributeDependency[] = []; protected subs: Subscription[] = []; + protected previousValue?: FieldValue = { + value: '', + valueList: [], + valueObject: {}, + }; constructor( protected typeFormatter: DataTypeFormatter, @@ -73,7 +79,12 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe const fieldKeys = (this.record.fields && Object.keys(this.record.fields)) || []; if (fieldKeys.length > 1) { this.calculateDependentFields(fieldKeys); - this.field.previousValue = this.field.value; + this.previousValue = deepClone({ + value: this.field.value, + valueList: this.field.valueList, + valueObject: this.field.valueObject, + forceNotEqual: 'forceNotEqual', + }); if((this.dependentFields && Object.keys(this.dependentFields).length) || this.dependentAttributes.length) { Object.keys(this.dependentFields).forEach(fieldKey => { @@ -94,7 +105,10 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe }); } - if (this.field.valueChanges$ && ((this.dependentFields && Object.keys(this.dependentFields).length) || this.dependentAttributes.length)) { + if ( + this.field.valueChanges$ + && ((this.dependentFields && Object.keys(this.dependentFields).length) || this.dependentAttributes.length) + ) { this.subs.push(this.field.valueChanges$.pipe(debounceTime(500)).subscribe((data) => { Object.keys(this.dependentFields).forEach(fieldKey => { const dependentField = this.dependentFields[fieldKey]; @@ -103,7 +117,7 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe return; } - if(this.field.previousValue != data.value) { + if (!isEqual(this.previousValue, data)) { const types = dependentField.type ?? []; if (types.includes('logic')) { @@ -115,7 +129,11 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe } } }); - this.field.previousValue = data.value; + this.previousValue = deepClone({ + value: data.value, + valueList: data.valueList, + valueObject: data.valueObject, + }); this.dependentAttributes.forEach(dependency => { const field = this.record.fields[dependency.field] || {} as Field; @@ -136,7 +154,9 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe /** * Calculate dependent fields - * @param {array} fieldKeys + * + * @param {string[]} fieldKeys Record field keys + * @protected */ protected calculateDependentFields(fieldKeys: string[]): void { fieldKeys.forEach(key => { @@ -156,9 +176,10 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe /** * Add field dependency - * @param {string} fieldKey - * @param {array} dependentFields - * @param {object} dependentAttributes + * + * @param {string} fieldKey Field key + * @param {Array} dependentFields Dependent Fields + * @param {object} dependentAttributes Dependent Attributes */ protected addFieldDependency(fieldKey: string, dependentFields: ObjectMap, dependentAttributes: AttributeDependency[]): void { const field = this.record.fields[fieldKey]; @@ -184,7 +205,7 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe dependentAttributes.push({ field: fieldKey, attribute: attributeKey, - types: dependentFields[name]['types'] ?? [] + types: dependentFields[name].types ?? [] }); } }); @@ -192,8 +213,9 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe /** * Check if field is dependency - * @param dependencies - * @returns {boolean} + * + * @param {ObjectMap} dependencies Dependencies + * @returns {boolean} field is dependency */ protected isDependencyField(dependencies: ObjectMap): boolean { const name = this.field.name || this.field.definition.name || ''; @@ -203,9 +225,10 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe /** * Add attribute dependency - * @param {string} fieldKey - * @param {array} dependentFields - * @param {object} dependentAttributes + * + * @param {string} fieldKey Field Key + * @param {ObjectMap} dependentFields Dependent Fields + * @param {AttributeDependency[]} dependentAttributes Dependent Attributes */ protected addAttributeDependency(fieldKey: string, dependentFields: ObjectMap, dependentAttributes: AttributeDependency[]): void { const field = this.record.fields[fieldKey]; @@ -231,7 +254,7 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe dependentAttributes.push({ field: fieldKey, attribute: attributeKey, - types: dependentFields[name]['types'] ?? [] + types: (dependentFields[name] ?? {}).types ?? [], }); } }); @@ -239,8 +262,9 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe /** * Check if attribute is dependency - * @param {object} attributeDependencies - * @returns {boolean} + * + * @param {object} attributeDependencies Attribute Dependencies + * @returns {boolean} attribute is dependency */ protected isDependencyAttribute(attributeDependencies: AttributeDependency[]): boolean { @@ -254,7 +278,7 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe if (this.field && this.field.formControl) { this.subs.push(this.field.formControl.valueChanges.subscribe(value => { - if (!isVoid(value)) { + if (!isVoid(value) && typeof value === 'string') { value = value.trim(); } else { value = ''; @@ -274,10 +298,19 @@ export class BaseFieldComponent implements FieldComponentInterface, OnInit, OnDe } - protected setFieldValue(newValue): void { + protected setFieldValue(newValue: string): void { this.field.value = newValue; } + protected setFormControlValue(newValue: string | string[]): void { + this.field.formControl.markAsDirty(); + + if (isEqual(this.field.formControl.value, newValue)) { + return; + } + this.field.formControl.setValue(newValue); + } + protected unsubscribeAll(): void { this.subs.forEach(sub => sub.unsubscribe()); } diff --git a/core/app/core/src/lib/fields/base/base-multienum.component.ts b/core/app/core/src/lib/fields/base/base-multienum.component.ts index 6e1c9c467e..d67864f6ed 100644 --- a/core/app/core/src/lib/fields/base/base-multienum.component.ts +++ b/core/app/core/src/lib/fields/base/base-multienum.component.ts @@ -24,6 +24,8 @@ * the words "Supercharged by SuiteCRM". */ +import { isArray, isEmpty, uniqBy } from 'lodash-es'; +import { isVoid } from 'common'; import {Component} from '@angular/core'; import {DataTypeFormatter} from '../../services/formatters/data-type.formatter.service'; import {BaseEnumComponent} from './base-enum.component'; @@ -43,22 +45,42 @@ export class BaseMultiEnumComponent extends BaseEnumComponent { super(languages, typeFormatter, logic, logicDisplay); } + protected subscribeValueChanges(): void { + if (!this.field?.formControl) { + return; + } + + const formValueChangesSubscription = this.field.formControl.valueChanges.subscribe( + (value: string[]) => this.field.valueList = value); + + this.subs.push(formValueChangesSubscription); + } + protected initValue(): void { - this.selectedValues = []; + const fieldValueList = this.field.valueList; - if (!this.field.valueList || this.field.valueList.length < 1) { + if (isVoid(fieldValueList) || isEmpty(fieldValueList)) { this.initEnumDefault(); return; } - this.field.valueList.forEach(value => { - if (typeof this.optionsMap[value] !== 'string') { - return; - } - this.selectedValues.push({ - value, - label: this.optionsMap[value] - }); - }); + this.updateInternalState(fieldValueList); + } + + protected updateInternalState(value: string | string[] = []): void { + const valueArray = isArray(value) ? value : [value]; + + this.selectedValues = valueArray.map(valueElement=>this.buildOptionFromValue(valueElement)); + this.selectedValues = uniqBy(this.selectedValues, 'value'); + + this.syncSelectedValuesWithForm(); + } + + protected syncSelectedValuesWithForm(): string[] { + const selectedValuesValueMap = this.selectedValues.map(selectedValue => selectedValue.value); + + this.setFormControlValue(selectedValuesValueMap); + + return selectedValuesValueMap; } } diff --git a/core/app/core/src/lib/fields/dropdownenum/templates/edit/dropdownenum.component.ts b/core/app/core/src/lib/fields/dropdownenum/templates/edit/dropdownenum.component.ts index 52c6e5f71b..a000018e00 100644 --- a/core/app/core/src/lib/fields/dropdownenum/templates/edit/dropdownenum.component.ts +++ b/core/app/core/src/lib/fields/dropdownenum/templates/edit/dropdownenum.component.ts @@ -24,21 +24,21 @@ * the words "Supercharged by SuiteCRM". */ +import {Option} from 'common'; import {Component} from '@angular/core'; +import {FormGroup} from '@angular/forms'; import {DataTypeFormatter} from '../../../../services/formatters/data-type.formatter.service'; -import {BaseEnumComponent} from '../../../base/base-enum.component'; import {LanguageStore} from '../../../../store/language/language.store'; -import {FormGroup} from '@angular/forms'; -import {Option} from 'common'; import {FieldLogicManager} from '../../../field-logic/field-logic.manager'; import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic-display.manager'; +import {DropdownEnumDetailFieldComponent} from '../detail/dropdownenum.component'; @Component({ selector: 'scrm-dropdownenum-edit', templateUrl: './dropdownenum.component.html', styleUrls: [] }) -export class DropdownEnumEditFieldComponent extends BaseEnumComponent { +export class DropdownEnumEditFieldComponent extends DropdownEnumDetailFieldComponent { formGroup: FormGroup; constructor( @@ -53,10 +53,8 @@ export class DropdownEnumEditFieldComponent extends BaseEnumComponent { ngOnInit(): void { super.ngOnInit(); - this.subscribeValueChanges(); - if (this.record && this.record.formGroup) { - this.formGroup = this.record.formGroup + this.formGroup = this.record.formGroup; } else { this.formGroup = new FormGroup({}); this.formGroup.addControl(this.field.name, this.field.formControl); @@ -64,7 +62,15 @@ export class DropdownEnumEditFieldComponent extends BaseEnumComponent { } - public getId(item: Option) { + public getId(item: Option): string { return this.field.name + '-' + item.value; } + + protected buildProvidedOptions(options: Option[]): void { + if (!options.find(option => option.value === '')) { + options.unshift({ value: '', label: '' }); + } + super.buildProvidedOptions(options); + } + } diff --git a/core/app/core/src/lib/fields/dynamicenum/templates/edit/dynamicenum.component.ts b/core/app/core/src/lib/fields/dynamicenum/templates/edit/dynamicenum.component.ts index c083b3d117..f634910bb2 100644 --- a/core/app/core/src/lib/fields/dynamicenum/templates/edit/dynamicenum.component.ts +++ b/core/app/core/src/lib/fields/dynamicenum/templates/edit/dynamicenum.component.ts @@ -27,17 +27,17 @@ import {Component, ViewChild} from '@angular/core'; import {TagInputComponent} from 'ngx-chips'; import {DataTypeFormatter} from '../../../../services/formatters/data-type.formatter.service'; -import {BaseEnumComponent} from '../../../base/base-enum.component'; import {LanguageStore} from '../../../../store/language/language.store'; import {FieldLogicManager} from '../../../field-logic/field-logic.manager'; import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic-display.manager'; +import { DynamicEnumDetailFieldComponent } from '../detail/dynamicenum.component'; @Component({ selector: 'scrm-dynamicenum-edit', templateUrl: './dynamicenum.component.html', styleUrls: [] }) -export class DynamicEnumEditFieldComponent extends BaseEnumComponent { +export class DynamicEnumEditFieldComponent extends DynamicEnumDetailFieldComponent { @ViewChild('tag') tag: TagInputComponent; @@ -50,30 +50,20 @@ export class DynamicEnumEditFieldComponent extends BaseEnumComponent { super(languages, typeFormatter, logic, logicDisplay); } - ngOnInit(): void { - super.ngOnInit(); - } - public onAdd(item): void { - if (item && item.value) { - this.field.value = item.value; - this.field.formControl.setValue(item.value); - this.field.formControl.markAsDirty(); + if (item?.value) { + this.updateInternalState(item.value); return; } - this.field.value = ''; - this.field.formControl.setValue(''); - this.field.formControl.markAsDirty(); - this.selectedValues = []; + this.updateInternalState(); return; } public onRemove(): void { - this.field.value = ''; - this.field.formControl.setValue(''); - this.field.formControl.markAsDirty(); + this.updateInternalState(); + setTimeout(() => { this.tag.focus(true, true); this.tag.dropdown.show(); diff --git a/core/app/core/src/lib/fields/enum/templates/edit/enum.component.ts b/core/app/core/src/lib/fields/enum/templates/edit/enum.component.ts index 9b3ba0cc13..3cc4396fb7 100644 --- a/core/app/core/src/lib/fields/enum/templates/edit/enum.component.ts +++ b/core/app/core/src/lib/fields/enum/templates/edit/enum.component.ts @@ -24,12 +24,13 @@ * the words "Supercharged by SuiteCRM". */ +import { isEmpty } from 'lodash-es'; import {Component, ViewChild} from '@angular/core'; import {TagInputComponent} from 'ngx-chips'; import {DataTypeFormatter} from '../../../../services/formatters/data-type.formatter.service'; import {BaseEnumComponent} from '../../../base/base-enum.component'; import {LanguageStore} from '../../../../store/language/language.store'; -import {TagModel} from "ngx-chips/core/accessor"; +import {TagModel} from 'ngx-chips/core/accessor'; import {FieldLogicManager} from '../../../field-logic/field-logic.manager'; import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic-display.manager'; @@ -46,37 +47,23 @@ export class EnumEditFieldComponent extends BaseEnumComponent { protected languages: LanguageStore, protected typeFormatter: DataTypeFormatter, protected logic: FieldLogicManager, - protected logicDisplay: FieldLogicDisplayManager + protected logicDisplay: FieldLogicDisplayManager, ) { super(languages, typeFormatter, logic, logicDisplay); } - ngOnInit(): void { - - this.checkAndInitAsDynamicEnum(); - super.ngOnInit(); - } - public onAdd(item): void { - if (item && item.value) { - this.field.value = item.value; - this.field.formControl.setValue(item.value); - this.field.formControl.markAsDirty(); + if (!item?.value) { + this.updateInternalState(); return; } - this.field.value = ''; - this.field.formControl.setValue(''); - this.field.formControl.markAsDirty(); - this.selectedValues = []; - - return; + this.updateInternalState(item.value); } public onRemove(): void { - this.field.value = ''; - this.field.formControl.setValue(''); - this.field.formControl.markAsDirty(); + this.updateInternalState(); + setTimeout(() => { this.tag.focus(true, true); this.tag.dropdown.show(); @@ -89,12 +76,14 @@ export class EnumEditFieldComponent extends BaseEnumComponent { public selectFirstElement(): void { const filteredElements: TagModel = this.tag.dropdown.items; - if (filteredElements.length !== 0) { - const firstElement = filteredElements[0]; - this.selectedValues.push(firstElement); - this.onAdd(firstElement); - this.tag.dropdown.hide(); + + if (isEmpty(filteredElements)) { + return; } + + const firstElement = filteredElements[0]; + this.onAdd(firstElement); + this.tag.dropdown.hide(); } } diff --git a/core/app/core/src/lib/fields/field-logic/field-logic.manager.ts b/core/app/core/src/lib/fields/field-logic/field-logic.manager.ts index 15ba9dbf11..c355d444ba 100644 --- a/core/app/core/src/lib/fields/field-logic/field-logic.manager.ts +++ b/core/app/core/src/lib/fields/field-logic/field-logic.manager.ts @@ -1,6 +1,6 @@ /** * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd. - * Copyright (C) 2021 SalesAgility Ltd. + * Copyright (C) 2023 SalesAgility Ltd. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by the @@ -27,7 +27,7 @@ import {Injectable} from '@angular/core'; import {BaseActionManager} from '../../services/actions/base-action-manager.service'; import {FieldLogicActionData, FieldLogicActionHandlerMap} from './field-logic.action'; -import {Action, ActionContext, ActionHandlerMap, Field, ModeActions, Record, ViewMode} from 'common'; +import {Action, ActionContext, Field, ModeActions, Record, ViewMode} from 'common'; import {DisplayTypeAction} from './display-type/display-type.action'; import {EmailPrimarySelectAction} from './email-primary-select/email-primary-select.action'; import {RequiredAction} from './required/required.action'; @@ -37,7 +37,6 @@ import {UpdateFlexRelateModuleAction} from './update-flex-relate-module/update-f import {UpdateValueAction} from './update-value/update-value.action'; import {UpdateValueBackendAction} from './update-value-backend/update-value-backend.action'; import {DisplayTypeBackendAction} from './display-type-backend/display-type-backend.action'; -import {RecordActionData} from '../../views/record/actions/record.action'; @Injectable({ providedIn: 'root' @@ -62,7 +61,7 @@ export class FieldLogicManager extends BaseActionManager { updateValue: UpdateValueAction, updateFlexRelateModule: UpdateFlexRelateModuleAction, updateValueBackend: UpdateValueBackendAction, - dislayTypeBackend: DisplayTypeBackendAction + dislayTypeBackend: DisplayTypeBackendAction, ) { super(); displayType.modes.forEach(mode => this.actions[mode][displayType.key] = displayType); @@ -78,12 +77,13 @@ export class FieldLogicManager extends BaseActionManager { /** * Run logic for the given field - * @param {object} field - * @param {object} mode - * @param {object} record - * @param triggeringStatus + * + * @param {Field} field Field + * @param {ViewMode} mode Mode + * @param {Record} record Record + * @param {string} triggeringStatus Triggering Status */ - runLogic(field: Field, mode: ViewMode, record: Record, triggeringStatus: string = ''): void { + runLogic(field: Field, mode: ViewMode, record: Record, triggeringStatus = ''): void { if (!field.logic) { return; } @@ -104,6 +104,7 @@ export class FieldLogicManager extends BaseActionManager { /** * Run the action using given context + * * @param action * @param mode * @param context @@ -114,6 +115,7 @@ export class FieldLogicManager extends BaseActionManager { /** * Run front end action + * * @param {object} action * @param {object} mode * @param {object} context @@ -126,6 +128,7 @@ export class FieldLogicManager extends BaseActionManager { /** * Get module name + * * @param {object} context */ protected getModuleName(context?: ActionContext): string { @@ -141,6 +144,7 @@ export class FieldLogicManager extends BaseActionManager { /** * Parse mode actions + * * @param declaredActions * @param mode * @param triggeringStatus diff --git a/core/app/core/src/lib/fields/multienum/templates/edit/multienum.component.ts b/core/app/core/src/lib/fields/multienum/templates/edit/multienum.component.ts index 06392174c3..acd1244ff4 100644 --- a/core/app/core/src/lib/fields/multienum/templates/edit/multienum.component.ts +++ b/core/app/core/src/lib/fields/multienum/templates/edit/multienum.component.ts @@ -24,21 +24,22 @@ * the words "Supercharged by SuiteCRM". */ +import { isEmpty } from 'lodash-es'; import {Component, ViewChild} from '@angular/core'; import {TagInputComponent} from 'ngx-chips'; +import { TagModel } from 'ngx-chips/core/accessor'; import {DataTypeFormatter} from '../../../../services/formatters/data-type.formatter.service'; -import {BaseMultiEnumComponent} from '../../../base/base-multienum.component'; import {LanguageStore} from '../../../../store/language/language.store'; -import {TagModel} from "ngx-chips/core/accessor"; import {FieldLogicManager} from '../../../field-logic/field-logic.manager'; import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic-display.manager'; +import { MultiEnumDetailFieldComponent } from '../detail/multienum.component'; @Component({ selector: 'scrm-multienum-edit', templateUrl: './multienum.component.html', styleUrls: [] }) -export class MultiEnumEditFieldComponent extends BaseMultiEnumComponent { +export class MultiEnumEditFieldComponent extends MultiEnumDetailFieldComponent { @ViewChild('tag') tag: TagInputComponent; @@ -51,25 +52,12 @@ export class MultiEnumEditFieldComponent extends BaseMultiEnumComponent { super(languages, typeFormatter, logic, logicDisplay); } - ngOnInit(): void { - this.checkAndInitAsDynamicEnum(); - super.ngOnInit(); - } - public onAdd(): void { - const value = this.selectedValues.map(option => option.value); - this.field.valueList = value; - this.field.formControl.setValue(value); - this.field.formControl.markAsDirty(); - - return; + this.syncSelectedValuesWithForm(); } public onRemove(): void { - const value = this.selectedValues.map(option => option.value); - this.field.valueList = value; - this.field.formControl.setValue(value); - this.field.formControl.markAsDirty(); + this.syncSelectedValuesWithForm(); setTimeout(() => { this.tag.focus(true, true); @@ -83,12 +71,15 @@ export class MultiEnumEditFieldComponent extends BaseMultiEnumComponent { public selectFirstElement(): void { const filteredElements: TagModel = this.tag.dropdown.items; - if (filteredElements.length !== 0) { - const firstElement = filteredElements[0]; - this.selectedValues.push(firstElement); - this.onAdd(); - this.tag.dropdown.hide(); + + if (isEmpty(filteredElements)) { + return; } + + const firstElement = filteredElements[0]; + this.selectedValues.push(firstElement); + this.onAdd(); + this.tag.dropdown.hide(); } } diff --git a/core/app/core/src/lib/fields/multienum/templates/filter/multienum.component.ts b/core/app/core/src/lib/fields/multienum/templates/filter/multienum.component.ts index ce9be21336..ae4c19290f 100644 --- a/core/app/core/src/lib/fields/multienum/templates/filter/multienum.component.ts +++ b/core/app/core/src/lib/fields/multienum/templates/filter/multienum.component.ts @@ -24,21 +24,21 @@ * the words "Supercharged by SuiteCRM". */ +import { isEmpty } from 'lodash-es'; import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {TagInputComponent} from 'ngx-chips'; import {DataTypeFormatter} from '../../../../services/formatters/data-type.formatter.service'; import {LanguageStore} from '../../../../store/language/language.store'; -import {BaseMultiEnumComponent} from '../../../base/base-multienum.component'; -import {TagModel} from "ngx-chips/core/accessor"; import {FieldLogicManager} from '../../../field-logic/field-logic.manager'; import {FieldLogicDisplayManager} from '../../../field-logic-display/field-logic-display.manager'; +import { MultiEnumEditFieldComponent } from '../edit/multienum.component'; @Component({ selector: 'scrm-multienum-filter', templateUrl: './multienum.component.html', styleUrls: [] }) -export class MultiEnumFilterFieldComponent extends BaseMultiEnumComponent implements OnInit, OnDestroy { +export class MultiEnumFilterFieldComponent extends MultiEnumEditFieldComponent implements OnInit, OnDestroy { @ViewChild('tag') tag: TagInputComponent; @@ -54,7 +54,7 @@ export class MultiEnumFilterFieldComponent extends BaseMultiEnumComponent implem ngOnInit(): void { this.field.valueList = []; - if (this.field.criteria.values && this.field.criteria.values.length > 0) { + if (!isEmpty(this.field.criteria.values)) { this.field.valueList = this.field.criteria.values; } @@ -62,48 +62,13 @@ export class MultiEnumFilterFieldComponent extends BaseMultiEnumComponent implem } - public onAdd(): void { + protected syncSelectedValuesWithForm(): string[] { + const selectedValuesValueMap = super.syncSelectedValuesWithForm(); - const value = this.selectedValues.map(option => option.value); - this.field.valueList = value; - this.field.formControl.setValue(value); - this.field.formControl.markAsDirty(); this.field.criteria.operator = '='; - this.field.criteria.values = value; + this.field.criteria.values = selectedValuesValueMap; - return; - } - - public onRemove(): void { - - let value = this.selectedValues.map(option => option.value); - if (!value) { - value = []; - } - - this.field.valueList = value; - this.field.formControl.setValue(value); - this.field.formControl.markAsDirty(); - this.field.criteria.operator = '='; - this.field.criteria.values = value; - setTimeout(() => { - this.tag.focus(true, true); - this.tag.dropdown.show(); - }, 200); - } - - public getPlaceholderLabel(): string { - return this.languages.getAppString('LBL_SELECT_ITEM') || ''; - } - - public selectFirstElement(): void { - const filteredElements: TagModel = this.tag.dropdown.items; - if (filteredElements.length !== 0) { - const firstElement = filteredElements[0]; - this.selectedValues.push(firstElement); - this.onAdd(); - this.tag.dropdown.hide(); - } + return selectedValuesValueMap; } } diff --git a/core/app/core/src/lib/fields/radioenum/templates/edit/radioenum.component.ts b/core/app/core/src/lib/fields/radioenum/templates/edit/radioenum.component.ts index 7327c3ec5d..32264c64cd 100644 --- a/core/app/core/src/lib/fields/radioenum/templates/edit/radioenum.component.ts +++ b/core/app/core/src/lib/fields/radioenum/templates/edit/radioenum.component.ts @@ -60,10 +60,8 @@ export class RadioEnumEditFieldComponent extends BaseEnumComponent { ngOnInit(): void { super.ngOnInit(); - this.subscribeValueChanges(); - if (this.record && this.record.formGroup) { - this.formGroup = this.record.formGroup + this.formGroup = this.record.formGroup; } else { this.formGroup = new FormGroup({}); this.formGroup.addControl(this.field.name, this.field.formControl); @@ -71,7 +69,7 @@ export class RadioEnumEditFieldComponent extends BaseEnumComponent { } - public getId(item: Option) { + public getId(item: Option): string { return this.field.name + '-' + item.value; } }