From b11036d38571ad15fd3c9ee42dc141c64c1dd229 Mon Sep 17 00:00:00 2001 From: "Moises E. Puyosa" Date: Mon, 13 Nov 2023 07:55:25 -0500 Subject: [PATCH 1/2] Closes #395 - Include all not displayed fields in Record --- .../common/src/lib/metadata/metadata.model.ts | 9 +- .../services/record/field/field.manager.ts | 65 +++++-- .../src/lib/services/record/record.manager.ts | 12 ++ .../store/record-view/record-view.store.ts | 166 +++++++++++------- 4 files changed, 171 insertions(+), 81 deletions(-) diff --git a/core/app/common/src/lib/metadata/metadata.model.ts b/core/app/common/src/lib/metadata/metadata.model.ts index ba1134bd33..633c18366c 100644 --- a/core/app/common/src/lib/metadata/metadata.model.ts +++ b/core/app/common/src/lib/metadata/metadata.model.ts @@ -30,6 +30,7 @@ import {BehaviorSubject, Observable} from 'rxjs'; export interface ViewFieldDefinition { name?: string; + vardefBased?: boolean; label?: string; labelKey?: string; dynamicLabelKey?: string; @@ -63,7 +64,7 @@ export interface PanelCell extends ViewFieldDefinition { } export interface ViewFieldDefinitionMap { - [key: string]: ViewFieldDefinition + [key: string]: ViewFieldDefinition; } export interface TabDefinitions { @@ -86,12 +87,12 @@ export interface LogicDefinition { modes: Array; params: { activeOnFields?: { - [key:string]: LogicRuleValues[]; - } + [key: string]: LogicRuleValues[]; + }; displayState?: boolean; fieldDependencies: Array; asyncProcessHandler?: string; - } + }; } export interface LogicRuleValues{ diff --git a/core/app/core/src/lib/services/record/field/field.manager.ts b/core/app/core/src/lib/services/record/field/field.manager.ts index db9abe363e..16d85768dc 100644 --- a/core/app/core/src/lib/services/record/field/field.manager.ts +++ b/core/app/core/src/lib/services/record/field/field.manager.ts @@ -148,17 +148,18 @@ export class FieldManager { /** * Build line item and add to record - * @param {object} itemDefinition - * @param {object }item - * @param {object} parentRecord - * @param {object} parentField + * + * @param {FieldDefinition} itemDefinition Item Definition + * @param {Record} parentRecord Parent Record + * @param {Field} parentField Parent Field + * @param {Record | null} item Item */ public addLineItem( itemDefinition: FieldDefinition, parentRecord: Record, parentField: Field, item: Record = null - ) { + ): void { if (!item) { item = { id: '', @@ -183,10 +184,11 @@ export class FieldManager { /** * Remove line item - * @param {object} parentField - * @param index + * + * @param {Field} parentField Parent Field + * @param {number} index Index */ - public removeLineItem(parentField: Field, index: number) { + public removeLineItem(parentField: Field, index: number): void { const item = parentField.items[index]; if (!item) { @@ -204,13 +206,13 @@ export class FieldManager { parentField.itemFormArray.clear(); - parentField.items.forEach(item => { - const deleted = item && item.attributes && item.attributes.deleted; - if (!item || deleted) { + parentField.items.forEach(parentItem => { + const deleted = parentItem && parentItem.attributes && parentItem.attributes.deleted; + if (!parentItem || deleted) { return; } - parentField.itemFormArray.push(item.formGroup); + parentField.itemFormArray.push(parentItem.formGroup); }); parentField.itemFormArray.updateValueAndValidity(); @@ -246,6 +248,45 @@ export class FieldManager { } + /** + * Build and add vardef only field to record + * + * @param {object} record Record + * @param {object} viewField ViewFieldDefinition + * @param {object} language LanguageStore + * @returns {object}Field + */ + public addVardefOnlyField(record: Record, viewField: ViewFieldDefinition, language: LanguageStore = null): Field { + + const field = this.fieldBuilder.buildField(record, viewField, language); + + this.addVardefOnlyFieldToRecord(record, viewField.name, field); + + return field; + } + + + /** + * Add field to record + * + * @param {object} record Record + * @param {string} name string + * @param {object} field Field + */ + public addVardefOnlyFieldToRecord(record: Record, name: string, field: Field): void { + + if (!record || !name || !field) { + return; + } + + if (!record.fields) { + record.fields = {}; + } + + record.fields[name] = field; + } + + /** * Is field initialized in record * diff --git a/core/app/core/src/lib/services/record/record.manager.ts b/core/app/core/src/lib/services/record/record.manager.ts index 8da3e17ad7..04ed3af99b 100644 --- a/core/app/core/src/lib/services/record/record.manager.ts +++ b/core/app/core/src/lib/services/record/record.manager.ts @@ -81,6 +81,18 @@ export class RecordManager { if (!viewField || !viewField.name) { return; } + + if(record.fields[viewField.name]) { + return; + } + + const isVardefBased = viewField?.vardefBased ?? false; + + if (isVardefBased) { + this.fieldManager.addVardefOnlyField(record, viewField, this.language); + return; + } + this.fieldManager.addField(record, viewField, this.language); }); diff --git a/core/app/core/src/lib/views/record/store/record-view/record-view.store.ts b/core/app/core/src/lib/views/record/store/record-view/record-view.store.ts index 69fd742fca..f749eaac57 100644 --- a/core/app/core/src/lib/views/record/store/record-view/record-view.store.ts +++ b/core/app/core/src/lib/views/record/store/record-view/record-view.store.ts @@ -24,12 +24,18 @@ * the words "Supercharged by SuiteCRM". */ +import { isEmpty } from 'lodash-es'; +import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs'; +import { catchError, distinctUntilChanged, finalize, map, take, tap } from 'rxjs/operators'; import {Injectable} from '@angular/core'; -import {BehaviorSubject, combineLatest, Observable, of, Subscription} from 'rxjs'; +import { Params } from '@angular/router'; import { BooleanMap, deepClone, + Field, FieldDefinitionMap, + FieldLogicMap, + FieldMetadata, isVoid, Record, StatisticsMap, @@ -37,10 +43,10 @@ import { SubPanelMeta, ViewContext, ViewFieldDefinition, - ViewMode + ViewFieldDefinitionMap, + ViewMode, } from 'common'; -import {catchError, distinctUntilChanged, finalize, map, take, tap} from 'rxjs/operators'; -import {RecordViewData, RecordViewModel, RecordViewState} from './record-view.store.model'; +import { RecordViewData, RecordViewModel, RecordViewState } from './record-view.store.model'; import {NavigationStore} from '../../../../store/navigation/navigation.store'; import {StateStore} from '../../../../store/state'; import {RecordSaveGQL} from '../../../../store/record/graphql/api.record.save'; @@ -61,7 +67,6 @@ import {LocalStorageService} from '../../../../services/local-storage/local-stor import {SubpanelStoreFactory} from '../../../../containers/subpanel/store/subpanel/subpanel.store.factory'; import {ViewStore} from '../../../../store/view/view.store'; import {RecordFetchGQL} from '../../../../store/record/graphql/api.record.get'; -import {Params} from '@angular/router'; import {StatisticsBatch} from '../../../../store/statistics/statistics-batch.service'; import {RecordStoreFactory} from '../../../../store/record/record.store.factory'; import {UserPreferenceStore} from '../../../../store/user-preference/user-preference.store'; @@ -170,9 +175,7 @@ export class RecordViewStore extends ViewStore implements StateStore { this.subpanels$ = this.subpanelsState.asObservable(); - this.viewContext$ = this.record$.pipe(map(() => { - return this.getViewContext(); - })); + this.viewContext$ = this.record$.pipe(map(() => this.getViewContext())); } get widgets(): boolean { @@ -394,6 +397,58 @@ export class RecordViewStore extends ViewStore implements StateStore { return templates[this.getMode()] || ''; } + initValidators(record: Record): void { + if(!record || !Object.keys(record?.fields).length) { + return; + } + + Object.keys(record.fields).forEach(fieldName => { + const field = record.fields[fieldName]; + const formControl = field?.formControl ?? null; + if (!formControl) { + return; + } + + this.resetValidators(field); + + const validators = field?.validators ?? []; + const asyncValidators = field?.asyncValidators ?? []; + + if (field?.formControl && validators.length) { + field.formControl.setValidators(validators); + } + if (field?.formControl && asyncValidators.length) { + field.formControl.setAsyncValidators(asyncValidators); + } + }); + + } + + resetValidators(field: Field): void { + if (!field?.formControl) { + return; + } + + field.formControl.clearValidators(); + field.formControl.clearAsyncValidators(); + } + + resetValidatorsForAllFields(record: Record): void { + if(!record || !record?.fields?.length) { + return ; + } + Object.keys(record.fields).forEach(fieldName => { + const field = record.fields[fieldName]; + const formControl = field?.formControl ?? null; + + if (!formControl) { + return; + } + + this.resetValidators(field); + }); + } + /** * Parse query params * @@ -587,23 +642,49 @@ export class RecordViewStore extends ViewStore implements StateStore { */ protected getViewFieldsObservable(): Observable { return this.metadataStore.recordViewMetadata$.pipe(map((recordMetadata: RecordViewMetadata) => { - const fields: ViewFieldDefinition[] = []; + const fieldsMap: ViewFieldDefinitionMap = {}; recordMetadata.panels.forEach(panel => { panel.rows.forEach(row => { row.cols.forEach(col => { - fields.push(col); + const fieldName = col.name ?? col.fieldDefinition.name ?? ''; + fieldsMap[fieldName] = col; }); }); }); - return fields; + Object.keys(recordMetadata.vardefs).forEach(fieldKey => { + const vardef = recordMetadata.vardefs[fieldKey] ?? null; + if (!vardef || isEmpty(vardef)) { + return; + } + + // already defined. skip + if (fieldsMap[fieldKey]) { + return; + } + + fieldsMap[fieldKey] = { + name: fieldKey, + vardefBased: true, + label: vardef.vname ?? '', + type: vardef.type ?? '', + display: vardef.display ?? '', + fieldDefinition: vardef, + metadata: vardef.metadata ?? {} as FieldMetadata, + logic: vardef.logic ?? {} as FieldLogicMap + } as ViewFieldDefinition; + }); + + return Object.values(fieldsMap); })); } /** * Build ui user preference key - * @param storageKey + * + * @param {string} storageKey Storage Key * @protected + * @returns {string} Preference Key */ protected getPreferenceKey(storageKey: string): string { return 'recordview-' + storageKey; @@ -611,9 +692,10 @@ export class RecordViewStore extends ViewStore implements StateStore { /** * Save ui user preference - * @param module - * @param storageKey - * @param value + * + * @param {string} module Module + * @param {string} storageKey Storage Key + * @param {any} value Value * @protected */ protected savePreference(module: string, storageKey: string, value: any): void { @@ -622,60 +704,14 @@ export class RecordViewStore extends ViewStore implements StateStore { /** * Load ui user preference - * @param module - * @param storageKey + * + * @param {string} module Module + * @param {string} storageKey Storage Key * @protected + * @returns {any} User Preference */ protected loadPreference(module: string, storageKey: string): any { return this.preferences.getUi(module, this.getPreferenceKey(storageKey)); } - initValidators(record: Record): void { - if(!record || !Object.keys(record?.fields).length) { - return; - } - - Object.keys(record.fields).forEach(fieldName => { - const field = record.fields[fieldName]; - const formControl = field?.formControl ?? null; - if (!formControl) { - return; - } - - this.resetValidators(field); - - const validators = field?.validators ?? []; - const asyncValidators = field?.asyncValidators ?? []; - - if (validators.length) { - field?.formControl?.setValidators(validators); - } - if (asyncValidators.length) { - field?.formControl?.setAsyncValidators(asyncValidators); - } - }); - - } - - resetValidators(field) { - field?.formControl?.clearValidators(); - field?.formControl?.clearAsyncValidators(); - } - - resetValidatorsForAllFields(record) { - if(!record || !record?.fields?.length) { - return ; - } - Object.keys(record.fields).forEach(fieldName => { - const field = record.fields[fieldName]; - const formControl = field?.formControl ?? null; - - if (!formControl) { - return; - } - - this.resetValidators(field); - }); - } - } From 2f6c808c14ebadf5e1906bc7b65192be4bc31340 Mon Sep 17 00:00:00 2001 From: "Moises E. Puyosa" Date: Mon, 13 Nov 2023 09:22:20 -0500 Subject: [PATCH 2/2] Closes #396 - Set from Related Record Logic --- core/app/common/src/lib/record/field.model.ts | 79 ++++++-- core/app/core/src/lib/core.ts | 3 + .../actionable-field-logic.action.ts | 84 ++++++++ .../fields/field-logic/field-logic.manager.ts | 25 ++- .../set-field-from-related.action.ts | 118 +++++++++++ .../logic/active-logic-checker.service.ts | 184 ++++++++++++++++++ 6 files changed, 467 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/set-field-from-related/set-field-from-related.action.ts create mode 100644 core/app/core/src/lib/services/logic/active-logic-checker.service.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 ab8b035733..8fd2261d52 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/set-field-from-related/set-field-from-related.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'; @@ -453,6 +455,7 @@ export * from './services/formatters/number/number-formatter.service'; export * from './services/formatters/phone/phone-formatter.service'; export * from './services/language/dynamic-label.service'; export * from './services/local-storage/local-storage.service'; +export * from './services/logic/active-logic-checker.service'; export * from './services/message/message.service'; export * from './services/metadata/base-metadata.resolver'; export * from './services/metadata/base-module.resolver'; 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..243c141433 --- /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..2b9e222f28 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) 2021-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 {SetFieldFromRelatedAction} from './set-field-from-related/set-field-from-related.action'; @Injectable({ providedIn: 'root' @@ -62,7 +62,8 @@ export class FieldLogicManager extends BaseActionManager { updateValue: UpdateValueAction, updateFlexRelateModule: UpdateFlexRelateModuleAction, updateValueBackend: UpdateValueBackendAction, - dislayTypeBackend: DisplayTypeBackendAction + dislayTypeBackend: DisplayTypeBackendAction, + setFieldFromRelatedAction: SetFieldFromRelatedAction ) { 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); + setFieldFromRelatedAction.modes.forEach(mode => this.actions[mode][setFieldFromRelatedAction.key] = setFieldFromRelatedAction); } /** * 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/set-field-from-related/set-field-from-related.action.ts b/core/app/core/src/lib/fields/field-logic/set-field-from-related/set-field-from-related.action.ts new file mode 100644 index 0000000000..1cf9dedfba --- /dev/null +++ b/core/app/core/src/lib/fields/field-logic/set-field-from-related/set-field-from-related.action.ts @@ -0,0 +1,118 @@ +/** + * 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 {Injectable} from '@angular/core'; +import {AttributeMap, EDITABLE_VIEW_MODES, Field, Record} from 'common'; +import { + ActionableFieldLogicActionHandler, + FieldValueTypes +} from '../actionable-field-logic/actionable-field-logic.action'; +import {ActiveLogicChecker} from '../../../services/logic/active-logic-checker.service'; +import {map, take} from 'rxjs/operators'; +import {RecordFetchGQL} from '../../../store/record/graphql/api.record.get'; +import {Observable, of} from 'rxjs'; +import {isString} from 'lodash-es'; + + +type RelatedFieldParamType = string | { + linkFieldName: string; + toCopyFromFieldName: string; +}; + +interface RelatedFieldParams { + nonActiveState?: RelatedFieldParamType; + activeState?: RelatedFieldParamType; +} + +@Injectable({ + providedIn: 'root' +}) +export class SetFieldFromRelatedAction extends ActionableFieldLogicActionHandler { + + key = 'setFromRelated'; + modes = EDITABLE_VIEW_MODES; + + constructor( + protected activeLogicChecker: ActiveLogicChecker, + protected recordFetchGQL: RecordFetchGQL + ) { + super(activeLogicChecker); + } + + executeLogic(logicIsActive: boolean, params: RelatedFieldParams, field: Field, record: Record): void { + this.getToUpdateValue(logicIsActive, params, record) + .pipe(take(1)) + .subscribe((toUpdateValue) => { + if (toUpdateValue === null) { + return; + } + + this.updateValue(toUpdateValue, field, record); + }); + } + + private getToUpdateValue(logicIsActive: boolean, params: RelatedFieldParams, record: Record): Observable { + const paramAccordingToLogicState = logicIsActive + ? params.activeState + : params.nonActiveState; + const relatedFieldParam = paramAccordingToLogicState ?? null; + + if (relatedFieldParam === null) { + return of(null); + } + + if (isString(relatedFieldParam)) { + return of(relatedFieldParam); + } + + const toCopyFromFieldName = relatedFieldParam.toCopyFromFieldName; + if (!toCopyFromFieldName) { + return of(null); + } + + const linkFieldName = relatedFieldParam.linkFieldName; + const relatedIdNameField = record.fields[linkFieldName]; + const relatedIdField = record.fields[relatedIdNameField.definition.id_name]; + if (!relatedIdField) { + return of(null); + } + + const module: string = relatedIdField.definition.module; + const recordId: string = relatedIdField.value; + + return this.getRecordAttributes(module, recordId).pipe( + map((recordAttributes): (FieldValueTypes | null) => (recordAttributes[toCopyFromFieldName] ?? null)) + ); + } + + private getRecordAttributes(module: string, recordId: string): Observable { + const fields: string[] = ['_id', 'attributes']; + + return this.recordFetchGQL.fetch(module, recordId, {fields}).pipe( + map((result): AttributeMap => (result?.data?.getRecord?.attributes ?? {})) + ); + } +} diff --git a/core/app/core/src/lib/services/logic/active-logic-checker.service.ts b/core/app/core/src/lib/services/logic/active-logic-checker.service.ts new file mode 100644 index 0000000000..055225dca4 --- /dev/null +++ b/core/app/core/src/lib/services/logic/active-logic-checker.service.ts @@ -0,0 +1,184 @@ +/** + * 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, isEmpty } from 'lodash-es'; +import { Injectable } from '@angular/core'; +import { + Record, + Action, + Field, + StringArrayMap, + StringArrayMatrix, + ViewMode, + FieldAttribute, + ALL_VIEW_MODES, + LogicRuleValues, +} from 'common'; +import { ConditionOperatorManager } from '../condition-operators/condition-operator.manager'; + +@Injectable({ + providedIn: 'root', +}) +export class ActiveLogicChecker { + + modes: ViewMode[] = ALL_VIEW_MODES; + + constructor( + protected operatorManager: ConditionOperatorManager + ) { + } + + public run(record: Record, action: Action): boolean { + if (!record || !action) { + return true; + } + + const activeOnFields: StringArrayMap = action.params?.activeOnFields || {}; + + const activeOnAttributes: StringArrayMatrix = action.params?.activeOnAttributes || {}; + + return this.isActive(record, activeOnFields, activeOnAttributes); + } + + /** + * Check if any of the configured values is currently set + * + * @param {Record} record Record + * @param {StringArrayMap} activeOnFields Active On Fields + * @param {StringArrayMatrix} activeOnAttributes Active On Attributes + * @returns {boolean} true if any of the configured values is currently set + */ + protected isActive( + record: Record, + activeOnFields: StringArrayMap, + activeOnAttributes: StringArrayMatrix + ): boolean { + let isActive = true; + if (!isEmpty(activeOnFields)) { + isActive = isActive && this.areFieldsActive(record, activeOnFields); + } + if (!isEmpty(activeOnAttributes)) { + isActive = isActive && this.areAttributesActive(record, activeOnAttributes); + } + + return isActive; + } + + /** + * Are fields active + * + * @param {Record} record Record + * @param {StringArrayMap} activeOnFields StringArrayMap + * @returns {boolean} true are fields active + */ + protected areFieldsActive(record: Record, activeOnFields: StringArrayMap): boolean { + let areActive = true; + + Object.entries(activeOnFields).forEach(([fieldKey, activeValues]) => { + if (!areActive) { + return; + } + + const field = (record.fields ?? {})[fieldKey] ?? null; + if (!field || isEmpty(activeValues)) { + return; + } + + areActive = this.isValueActive(record, field, activeValues); + }); + + return areActive; + } + + /** + * Are attributes active + * + * @param {Record} record Record + * @param {StringArrayMatrix} activeOnAttributes Active On Attributes + * @returns {boolean} true if are attributes active + */ + protected areAttributesActive(record: Record, activeOnAttributes: StringArrayMatrix): boolean { + let areActive = true; + + Object.entries(activeOnAttributes).forEach(([fieldKey, attributesMap]) => { + if (!areActive) { + return; + } + + const field = (record.fields ?? {})[fieldKey] ?? null; + if (!field || isEmpty(attributesMap)) { + return; + } + + Object.entries(attributesMap).forEach(([attributeKey, activeValues]) => { + if (!areActive) { + return; + } + + const attribute = (field.attributes ?? {})[attributeKey] ?? null; + if (!attribute || isEmpty(activeValues)) { + return; + } + + areActive = this.isValueActive(record, attribute, activeValues); + }); + }); + + return areActive; + } + + /** + * Is value active + * + * @param {Record} record Record + * @param {Field | FieldAttribute} value Value + * @param {Array | any} activeValueOrValues Active Value Or Values + * @returns {boolean} true if is value active + */ + protected isValueActive( + record: Record, + value: Field | FieldAttribute, + activeValueOrValues: string | LogicRuleValues | Array + ): boolean { + const activeValues = isArray(activeValueOrValues) ? activeValueOrValues : [activeValueOrValues]; + + const toCompareValueList = !isEmpty(value.valueList) + ? value.valueList + : [value.value]; + + return activeValues.some(activeValue => { + if (typeof activeValue === 'string') { + return toCompareValueList.some(toCompareValue => activeValue === toCompareValue); + } + + const operatorKey = activeValue.operator ?? ''; + const operator = this.operatorManager.get(operatorKey); + if (!operator) { + console.warn(`ActiveLogicChecker.isValueActive: Operator: '${operatorKey}' not found.`); + } + return operator?.run(record, value, activeValue); + }); + } +}