Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add datasource support to conditional variables #6270

Merged
merged 10 commits into from
Oct 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ export default class DynamicVariableListenerManager {
this.dataListeners.forEach((ls) => model.stopListening(ls.obj, ls.event, this.onChange));
this.dataListeners = [];
}

destroy() {
this.removeListeners();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import EditorModel from '../../../editor/model/Editor';
import DataVariable from '../DataVariable';
import { evaluateVariable, isDataVariable } from '../utils';
import { Expression, LogicGroup } from './DataCondition';
import { LogicalGroupStatement } from './LogicalGroupStatement';
import { Operator } from './operators';
import { GenericOperation, GenericOperator } from './operators/GenericOperator';
import { LogicalOperator } from './operators/LogicalOperator';
import { NumberOperator, NumberOperation } from './operators/NumberOperator';
import { StringOperator, StringOperation } from './operators/StringOperations';

export class Condition {
private condition: Expression | LogicGroup | boolean;
private em: EditorModel;

constructor(condition: Expression | LogicGroup | boolean, opts: { em: EditorModel }) {
this.condition = condition;
this.em = opts.em;
}

evaluate(): boolean {
return this.evaluateCondition(this.condition);
}

/**
* Recursively evaluates conditions and logic groups.
*/
private evaluateCondition(condition: any): boolean {
if (typeof condition === 'boolean') return condition;

if (this.isLogicGroup(condition)) {
const { logicalOperator, statements } = condition;
const operator = new LogicalOperator(logicalOperator);
const logicalGroup = new LogicalGroupStatement(operator, statements, { em: this.em });
return logicalGroup.evaluate();
}

if (this.isExpression(condition)) {
const { left, operator, right } = condition;
const evaluateLeft = evaluateVariable(left, this.em);
const evaluateRight = evaluateVariable(right, this.em);
const op = this.getOperator(evaluateLeft, operator);

const evaluated = op.evaluate(evaluateLeft, evaluateRight);
return evaluated;
}

throw new Error('Invalid condition type.');
}

/**
* Factory method for creating operators based on the data type.
*/
private getOperator(left: any, operator: string): Operator {
if (this.isOperatorInEnum(operator, GenericOperation)) {
return new GenericOperator(operator as GenericOperation);
} else if (typeof left === 'number') {
return new NumberOperator(operator as NumberOperation);
} else if (typeof left === 'string') {
return new StringOperator(operator as StringOperation);
}
throw new Error(`Unsupported data type: ${typeof left}`);
}

/**
* Extracts all data variables from the condition, including nested ones.
*/
getDataVariables(): DataVariable[] {
const variables: DataVariable[] = [];
this.extractVariables(this.condition, variables);
return variables;
}

/**
* Recursively extracts variables from expressions or logic groups.
*/
private extractVariables(condition: boolean | LogicGroup | Expression, variables: DataVariable[]): void {
if (this.isExpression(condition)) {
if (isDataVariable(condition.left)) variables.push(condition.left);
if (isDataVariable(condition.right)) variables.push(condition.right);
} else if (this.isLogicGroup(condition)) {
condition.statements.forEach((stmt) => this.extractVariables(stmt, variables));
}
}

/**
* Checks if a condition is a LogicGroup.
*/
private isLogicGroup(condition: any): condition is LogicGroup {
return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements);
}

/**
* Checks if a condition is an Expression.
*/
private isExpression(condition: any): condition is Expression {
return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string';
}

/**
* Checks if an operator exists in a specific enum.
*/
private isOperatorInEnum(operator: string, enumObject: any): boolean {
return Object.values(enumObject).includes(operator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { StringOperation } from './operators/StringOperations';
import { GenericOperation } from './operators/GenericOperator';
import { Model } from '../../../common';
import { LogicalOperation } from './operators/LogicalOperator';
import { evaluateCondition } from './evaluateCondition';
import DynamicVariableListenerManager from '../DataVariableListenerManager';
import EditorModel from '../../../editor/model/Editor';
import { Condition } from './Condition';
import DataVariable from '../DataVariable';
import { evaluateVariable, isDataVariable } from '../utils';

export const ConditionalVariableType = 'conditional-variable';
export const DataConditionType = 'conditional-variable';
export type Expression = {
left: any;
operator: GenericOperation | StringOperation | NumberOperation;
Expand All @@ -19,32 +23,75 @@ export type LogicGroup = {

export class DataCondition extends Model {
private conditionResult: boolean;
private condition: Condition;
private em: EditorModel;
private variableListeners: DynamicVariableListenerManager[] = [];

defaults() {
return {
type: ConditionalVariableType,
type: DataConditionType,
condition: false,
};
}

constructor(
private condition: Expression | LogicGroup | boolean,
condition: Expression | LogicGroup | boolean,
private ifTrue: any,
private ifFalse: any,
opts: { em: EditorModel },
) {
super();
this.condition = new Condition(condition, { em: opts.em });
this.em = opts.em;
this.conditionResult = this.evaluate();
this.listenToDataVariables();
}

evaluate() {
return evaluateCondition(this.condition);
return this.condition.evaluate();
}

getDataValue(): any {
return this.conditionResult ? this.ifTrue : this.ifFalse;
return this.conditionResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em);
}

reevaluate(): void {
this.conditionResult = this.evaluate();
}

toJSON() {
return {
condition: this.condition,
ifTrue: this.ifTrue,
ifFalse: this.ifFalse,
};
}

private listenToDataVariables() {
if (!this.em) return;

// Clear previous listeners to avoid memory leaks
this.cleanupListeners();

const dataVariables = this.condition.getDataVariables();
if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue);
if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse);

dataVariables.forEach((variable) => {
const variableInstance = new DataVariable(variable, { em: this.em });
const listener = new DynamicVariableListenerManager({
model: this as any,
em: this.em!,
dataVariable: variableInstance,
updateValueFromDataVariable: this.reevaluate.bind(this),
});

this.variableListeners.push(listener);
});
}

private cleanupListeners() {
this.variableListeners.forEach((listener) => listener.destroy());
this.variableListeners = [];
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { LogicalOperator } from './operators/LogicalOperator';
import { Expression, LogicGroup } from './DataCondition';
import { evaluateCondition } from './evaluateCondition';
import { Condition } from './Condition';
import EditorModel from '../../../editor/model/Editor';

export class LogicalGroupStatement {
private em: EditorModel;

constructor(
private operator: LogicalOperator,
private statements: (Expression | LogicGroup | boolean)[],
) {}
opts: { em: EditorModel },
) {
this.em = opts.em;
}

evaluate(): boolean {
const results = this.statements.map((statement) => evaluateCondition(statement));
const results = this.statements.map((statement) => {
const condition = new Condition(statement, { em: this.em });
return condition.evaluate();
});
return this.operator.evaluate(results);
}
}
15 changes: 15 additions & 0 deletions packages/core/src/data_sources/model/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import EditorModel from '../../editor/model/Editor';
import { DataConditionType } from './conditional_variables/DataCondition';
import DataVariable, { DataVariableType } from './DataVariable';

export function isDataVariable(variable: any) {
return variable?.type === DataVariableType;
}

export function isDataCondition(variable: any) {
return variable?.type === DataConditionType;
}

export function evaluateVariable(variable: any, em: EditorModel) {
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable;
}
Loading