diff --git a/core/app/common/src/lib/record/field.model.ts b/core/app/common/src/lib/record/field.model.ts index 038ad645c3..c7df0a144b 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 {cloneDeep, 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; } @@ -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,45 @@ export class BaseField implements Field { } set valueObjectArray(value: ObjectMap[]) { + if (isEqual(this.valueObjectArrayState, value)) { + return; + } + this.valueObjectArrayState = value; this.emitValueChanges(); } - protected emitValueChanges() { + 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; -} + get options(): Option[] { + return this.optionsState + } + set options(options: Option[]) { + this.optionsState = options; + this.emitOptionsChanges(); + } + + protected emitOptionsChanges() { + this.optionsSubject.next(cloneDeep(this.optionsState)) + } +} 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..87d44a6130 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, FieldDefinition, 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, @@ -58,137 +53,48 @@ export class BaseEnumComponent extends BaseFieldComponent implements OnInit, OnD super(typeFormatter, logic, logicDisplay); } - ngOnInit(): void { - + /** + * + */ + public 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; - - } - - const options = this?.field?.definition?.options ?? null; - if (options) { - this.subs.push(this.languages.vm$.subscribe((strings: LanguageStrings) => { + this.subscribeValueAndOptionChanges(); - 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 { + /** + * + */ + public getInvalidClass(): string { if (this.field.formControl && this.field.formControl.invalid && this.field.formControl.touched) { return 'is-invalid'; } 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 + */ 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 +104,164 @@ 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; + /** + * + * @param value + * @protected + */ + 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); + } - let defaultVal = this.field?.definition?.default; - if (typeof defaultVal === 'string') { - defaultVal = defaultVal.trim(); + /** + * + * @param value + * @protected + */ + protected buildOptionFromValue(value: string): Option { + const option: Option = {value: '', label: ''}; + + 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; + } - this.initEnumDefaultFieldValues(defaultVal); + protected buildProvidedOptions(options: Option[]): void { + this.addExtraOptions(options); + this.addConditionalOptions(options); + + this.optionsMap = {} as LanguageStringMap; + options.forEach(option => this.addToOptionMap(option)); + + 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 ?? []; - if (this.field.type === 'multienum') { - const defaultValues = this.selectedValues.map(option => option.value); - this.field.valueList = defaultValues; - this.field.formControl.setValue(defaultValues); + extraOptions.forEach(extraOption=>options.push(extraOption)); + } - } else { - this.field.value = defaultVal; - this.field.formControl.setValue(defaultVal); - } - this.field.formControl.markAsDirty(); + protected addConditionalOptions(options: Option[]): void { + const conditionalOptions = this.field.metadata?.conditionalOptions ?? {}; + + Object.values(conditionalOptions).forEach(extraOption => options.push(