Skip to content

Commit

Permalink
Closes #392 - Conditional Options Logic
Browse files Browse the repository at this point in the history
  • Loading branch information
mpuyosa91 committed Dec 6, 2023
1 parent 62c7479 commit 301e772
Show file tree
Hide file tree
Showing 5 changed files with 348 additions and 0 deletions.
3 changes: 3 additions & 0 deletions core/app/core/src/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ 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/conditional-options/conditional-options.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';
Expand Down Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
* 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});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*
* 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 {Action, Field, Option, Record, ViewMode} from 'common';
import {ActionableFieldLogicActionHandler} from '../actionable-field-logic/actionable-field-logic.action';
import {ActiveLogicChecker} from '../../../services/logic/active-logic-checker.service';

interface ConditionalOptionsParams {
conditionalOptions?: (Option & Action)[];
}

@Injectable({
providedIn: 'root'
})
export class ConditionalOptionsAction extends ActionableFieldLogicActionHandler {

key = 'conditional-options';
modes = ['detail', 'edit', 'create'] as ViewMode[];

constructor(
protected activeLogicChecker: ActiveLogicChecker
) {
super(activeLogicChecker);
}

executeLogic(logicIsActive: boolean, params: ConditionalOptionsParams, field: Field, record: Record): void {
if (!logicIsActive) {
return;
}
const conditionalOptions = params.conditionalOptions ?? [];
const currentFieldOptions: { [key: string]: Option } = {};

conditionalOptions.forEach((conditionalOption) => {
const isActive = this.activeLogicChecker.run(record, conditionalOption);

if (!isActive) {
return;
}

currentFieldOptions[conditionalOption.value] = {
value: conditionalOption.value ?? '',
label: conditionalOption.label ?? '',
labelKey: conditionalOption.labelKey ?? ''
};
});

field.metadata.conditionalOptions = currentFieldOptions;
field.options = [...(field.options ?? [])];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +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 {ConditionalOptionsAction} from './conditional-options/conditional-options.action';

@Injectable({
providedIn: 'root'
Expand All @@ -62,6 +63,7 @@ export class FieldLogicManager extends BaseActionManager<FieldLogicActionData> {
updateFlexRelateModule: UpdateFlexRelateModuleAction,
updateValueBackend: UpdateValueBackendAction,
dislayTypeBackend: DisplayTypeBackendAction,
conditionalOptionsAction: ConditionalOptionsAction,
) {
super();
displayType.modes.forEach(mode => this.actions[mode][displayType.key] = displayType);
Expand All @@ -73,6 +75,7 @@ export class FieldLogicManager extends BaseActionManager<FieldLogicActionData> {
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);
conditionalOptionsAction.modes.forEach(mode => this.actions[mode][conditionalOptionsAction.key] = conditionalOptionsAction);
}

/**
Expand Down
184 changes: 184 additions & 0 deletions core/app/core/src/lib/services/logic/active-logic-checker.service.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
* 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<string | LogicRuleValues>
): 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);
});
}
}

0 comments on commit 301e772

Please sign in to comment.