From ba2a9c17b64a59765dc55d361d7a321be94cd0a5 Mon Sep 17 00:00:00 2001 From: "Moises E. Puyosa" Date: Mon, 13 Nov 2023 09:12:57 -0500 Subject: [PATCH] Update Field Logic --- core/app/common/src/lib/record/field.model.ts | 79 +++++++++++++---- core/app/core/src/lib/core.ts | 2 + .../actionable-field-logic.action.ts | 84 +++++++++++++++++++ .../fields/field-logic/field-logic.manager.ts | 25 ++++-- .../update-field/update-field.action.ts | 48 +++++++++++ 5 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 core/app/core/src/lib/fields/field-logic/actionable-field-logic/actionable-field-logic.action.ts create mode 100644 core/app/core/src/lib/fields/field-logic/update-field/update-field.action.ts diff --git a/core/app/common/src/lib/record/field.model.ts b/core/app/common/src/lib/record/field.model.ts index 038ad645c3..8351ede492 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; } @@ -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; @@ -232,13 +244,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 +264,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 +284,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 +302,32 @@ 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; } - - diff --git a/core/app/core/src/lib/core.ts b/core/app/core/src/lib/core.ts index 1945b6e481..52deafb393 100644 --- a/core/app/core/src/lib/core.ts +++ b/core/app/core/src/lib/core.ts @@ -361,11 +361,13 @@ export * from './fields/enum/templates/edit/enum.component'; export * from './fields/enum/templates/edit/enum.module'; export * from './fields/field-logic/field-logic.action'; export * from './fields/field-logic/field-logic.manager'; +export * from './fields/field-logic/actionable-field-logic/actionable-field-logic.action'; export * from './fields/field-logic/currency-conversion/update-base-currency.action'; export * from './fields/field-logic/currency-conversion/update-currency.action'; export * from './fields/field-logic/display-type/display-type.action'; export * from './fields/field-logic/email-primary-select/email-primary-select.action'; export * from './fields/field-logic/required/required.action'; +export * from './fields/field-logic/update-field/update-field.action'; export * from './fields/field-logic/update-flex-relate-module/update-flex-relate-module.action'; export * from './fields/file/templates/detail/file.component'; export * from './fields/file/templates/detail/file.module'; diff --git a/core/app/core/src/lib/fields/field-logic/actionable-field-logic/actionable-field-logic.action.ts b/core/app/core/src/lib/fields/field-logic/actionable-field-logic/actionable-field-logic.action.ts new file mode 100644 index 0000000000..09c6f12987 --- /dev/null +++ b/core/app/core/src/lib/fields/field-logic/actionable-field-logic/actionable-field-logic.action.ts @@ -0,0 +1,84 @@ +/** + * SuiteCRM is a customer relationship management program developed by 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 + * Free Software Foundation with the addition of the following permission added + * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK + * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE + * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * In accordance with Section 7(b) of the GNU Affero General Public License + * version 3, these Appropriate Legal Notices must retain the display of the + * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably + * feasible for technical reasons, the Appropriate Legal Notices must display + * the words "Supercharged by SuiteCRM". + */ + +import {isArray, isString} from 'lodash-es'; +import { + Action, + ALL_VIEW_MODES, + Field, + Record, +} from 'common'; +import { FieldLogicActionData, FieldLogicActionHandler } from '../field-logic.action'; +import {ActiveLogicChecker} from '../../../services/logic/active-logic-checker.service'; + +export type FieldValueTypes = string | string[] | object; + +export abstract class ActionableFieldLogicActionHandler extends FieldLogicActionHandler { + modes = ALL_VIEW_MODES; + + protected constructor( + protected activeLogicChecker: ActiveLogicChecker, + ) { + super(); + } + + run(data: FieldLogicActionData, action: Action): void { + const record = data.record; + const field = data.field; + if (!record || !field) { + return; + } + const params = action.params ?? {}; + + const logicIsActive = this.activeLogicChecker.run(record, action); + + this.executeLogic(logicIsActive, params, field, record); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + shouldDisplay(data: FieldLogicActionData): boolean { + return true; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + executeLogic(logicIsActive: boolean, params: { [p: string]: any }, field: Field, record: Record): void { + } + + protected updateValue(value: FieldValueTypes, field: Field, record: Record): void { + if (isString(value)) { + field.value = value; + } else if (isArray(value)) { + field.valueList = value; + } else { + field.valueObject = value; + } + + field.formControl.setValue(value); + + // re-validate the parent form-control after value update + record.formGroup.updateValueAndValidity({onlySelf: true, emitEvent: true}); + } +} 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..22cfda37e9 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,7 @@ 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'; +import {UpdateFieldAction} from './update-field/update-field.action'; @Injectable({ providedIn: 'root' @@ -62,7 +62,8 @@ export class FieldLogicManager extends BaseActionManager { updateValue: UpdateValueAction, updateFlexRelateModule: UpdateFlexRelateModuleAction, updateValueBackend: UpdateValueBackendAction, - dislayTypeBackend: DisplayTypeBackendAction + dislayTypeBackend: DisplayTypeBackendAction, + updateFieldAction: UpdateFieldAction, ) { super(); displayType.modes.forEach(mode => this.actions[mode][displayType.key] = displayType); @@ -74,16 +75,18 @@ export class FieldLogicManager extends BaseActionManager { updateValue.modes.forEach(mode => this.actions[mode][updateValue.key] = updateValue); updateValueBackend.modes.forEach(mode => this.actions[mode][updateValueBackend.key] = updateValueBackend); dislayTypeBackend.modes.forEach(mode => this.actions[mode][dislayTypeBackend.key] = dislayTypeBackend); + updateFieldAction.modes.forEach(mode => this.actions[mode][updateFieldAction.key] = updateFieldAction); } /** * 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 +107,7 @@ export class FieldLogicManager extends BaseActionManager { /** * Run the action using given context + * * @param action * @param mode * @param context @@ -114,6 +118,7 @@ export class FieldLogicManager extends BaseActionManager { /** * Run front end action + * * @param {object} action * @param {object} mode * @param {object} context @@ -126,6 +131,7 @@ export class FieldLogicManager extends BaseActionManager { /** * Get module name + * * @param {object} context */ protected getModuleName(context?: ActionContext): string { @@ -141,6 +147,7 @@ export class FieldLogicManager extends BaseActionManager { /** * Parse mode actions + * * @param declaredActions * @param mode * @param triggeringStatus diff --git a/core/app/core/src/lib/fields/field-logic/update-field/update-field.action.ts b/core/app/core/src/lib/fields/field-logic/update-field/update-field.action.ts new file mode 100644 index 0000000000..32168d9c13 --- /dev/null +++ b/core/app/core/src/lib/fields/field-logic/update-field/update-field.action.ts @@ -0,0 +1,48 @@ +/** + * @author SalesAgility . + */ + +import {Injectable} from '@angular/core'; +import {EDITABLE_VIEW_MODES, Field, Record} from 'common'; +import {ActionableFieldLogicActionHandler} from '../actionable-field-logic/actionable-field-logic.action'; +import {ActiveLogicChecker} from '../../../services/logic/active-logic-checker.service'; + +type UpdateFieldParamType = string | string[]; + +interface UpdateFieldParams { + nonActiveValue?: UpdateFieldParamType; + activeValue?: UpdateFieldParamType; +} + +@Injectable({ + providedIn: 'root' +}) +export class UpdateFieldAction extends ActionableFieldLogicActionHandler { + + key = 'updateField'; + modes = EDITABLE_VIEW_MODES; + + constructor( + protected activeLogicChecker: ActiveLogicChecker, + ) { + super(activeLogicChecker); + } + + executeLogic(logicIsActive: boolean, params: UpdateFieldParams, field: Field, record: Record): void { + const toUpdateValue = this.getToUpdateValue(logicIsActive, params); + + if (toUpdateValue === null) { + return; + } + + this.updateValue(toUpdateValue, field, record); + } + + private getToUpdateValue(logicIsActive: boolean, params: UpdateFieldParams): UpdateFieldParamType | null { + const valueAccordingToLogicState = logicIsActive + ? params.activeValue + : params.nonActiveValue; + + return valueAccordingToLogicState ?? null; + } +}