diff --git a/core/app/core/src/lib/core.ts b/core/app/core/src/lib/core.ts
index ab8b035733..0a0c97a334 100644
--- a/core/app/core/src/lib/core.ts
+++ b/core/app/core/src/lib/core.ts
@@ -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';
@@ -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..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/conditional-options/conditional-options.action.ts b/core/app/core/src/lib/fields/field-logic/conditional-options/conditional-options.action.ts
new file mode 100644
index 0000000000..09052a38c7
--- /dev/null
+++ b/core/app/core/src/lib/fields/field-logic/conditional-options/conditional-options.action.ts
@@ -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 .
+ *
+ * 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 ?? [])];
+ }
+}
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 c355d444ba..7df742b893 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
@@ -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'
@@ -62,6 +63,7 @@ export class FieldLogicManager extends BaseActionManager {
updateFlexRelateModule: UpdateFlexRelateModuleAction,
updateValueBackend: UpdateValueBackendAction,
dislayTypeBackend: DisplayTypeBackendAction,
+ conditionalOptionsAction: ConditionalOptionsAction,
) {
super();
displayType.modes.forEach(mode => this.actions[mode][displayType.key] = displayType);
@@ -73,6 +75,7 @@ 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);
+ conditionalOptionsAction.modes.forEach(mode => this.actions[mode][conditionalOptionsAction.key] = conditionalOptionsAction);
}
/**
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);
+ });
+ }
+}