From 01285f2e070c0f3f0e819c61807cde60bfdb3eb2 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 21 Oct 2024 10:20:32 +0300 Subject: [PATCH 01/48] Add data condition variable --- .../ConditionStatement.ts | 13 ++ .../conditional_variables/DataCondition.ts | 50 ++++++ .../LogicalGroupStatement.ts | 15 ++ .../evaluateCondition.ts | 53 ++++++ .../operators/GenericOperator.ts | 54 ++++++ .../operators/LogicalOperator.ts | 28 +++ .../operators/NumberOperator.ts | 35 ++++ .../operators/StringOperations.ts | 29 +++ .../conditional_variables/operators/index.ts | 3 + .../conditional_variables/DataCondition.ts | 170 ++++++++++++++++++ .../operators/GenericOperator.ts | 147 +++++++++++++++ .../operators/LogicalOperator.ts | 58 ++++++ .../operators/NumberOperator.ts | 94 ++++++++++ .../operators/StringOperator.ts | 80 +++++++++ 14 files changed, 829 insertions(+) create mode 100644 packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/DataCondition.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/index.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts new file mode 100644 index 0000000000..bd96463175 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts @@ -0,0 +1,13 @@ +import { Operator } from "./operators"; + +export class ConditionStatement { + constructor( + private leftValue: any, + private operator: Operator, + private rightValue: any + ) { } + + evaluate(): boolean { + return this.operator.evaluate(this.leftValue, this.rightValue); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts new file mode 100644 index 0000000000..13071d75f9 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -0,0 +1,50 @@ +import { NumberOperation } from "./operators/NumberOperator"; +import { StringOperation } from "./operators/StringOperations"; +import { GenericOperation } from "./operators/GenericOperator"; +import { Model } from "../../../common"; +import { LogicalOperation } from "./operators/LogicalOperator"; +import { evaluateCondition } from "./evaluateCondition"; + +export const ConditionalVariableType = 'conditional-variable'; +export type Expression = { + left: any; + operator: GenericOperation | StringOperation | NumberOperation; + right: any; +}; + +export type LogicGroup = { + logicalOperator: LogicalOperation; + statements: (Expression | LogicGroup | boolean)[]; +}; + +export class DataCondition extends Model { + private conditionResult: boolean; + + defaults() { + return { + type: ConditionalVariableType, + condition: false, + }; + } + + constructor( + private condition: Expression | LogicGroup | boolean, + private ifTrue: any, + private ifFalse: any + ) { + super(); + this.conditionResult = this.evaluate(); + } + + evaluate() { + return evaluateCondition(this.condition); + } + + getDataValue(): any { + return this.conditionResult ? this.ifTrue : this.ifFalse; + } + + reevaluate(): void { + this.conditionResult = this.evaluate(); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts new file mode 100644 index 0000000000..7fb2348a34 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts @@ -0,0 +1,15 @@ +import { LogicalOperator } from "./operators/LogicalOperator"; +import { Expression, LogicGroup } from "./DataCondition"; +import { evaluateCondition } from "./evaluateCondition"; + +export class LogicalGroupStatement { + constructor( + private operator: LogicalOperator, + private statements: (Expression | LogicGroup | boolean)[] + ) { } + + evaluate(): boolean { + const results = this.statements.map(statement => evaluateCondition(statement)); + return this.operator.evaluate(results); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts new file mode 100644 index 0000000000..53e6dea684 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts @@ -0,0 +1,53 @@ +import { ConditionStatement } from "./ConditionStatement"; +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 { NumberOperation, NumberOperator } from "./operators/NumberOperator"; +import { StringOperation, StringOperator } from "./operators/StringOperations"; + +export function evaluateCondition(condition: any): boolean { + if (typeof condition === 'boolean') { + return condition; + } + + if (isLogicGroup(condition)) { + const { logicalOperator, statements } = condition; + const op = new LogicalOperator(logicalOperator) + const logicalGroup = new LogicalGroupStatement(op, statements); + return logicalGroup.evaluate(); + } + + if (isCondition(condition)) { + const { left, operator, right } = condition; + const op = operatorFactory(left, operator); + const statement = new ConditionStatement(left, op, right); + return statement.evaluate(); + } + + throw new Error('Invalid condition type.'); +} + +function operatorFactory(left: any, operator: string): Operator { + if (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}`); +} + +function isOperatorInEnum(operator: string, enumObject: any): boolean { + return Object.values(enumObject).includes(operator); +} + +function isLogicGroup(condition: any): condition is LogicGroup { + return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); +} + +function isCondition(condition: any): condition is Expression { + return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts new file mode 100644 index 0000000000..467ee9268d --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts @@ -0,0 +1,54 @@ +import DataVariable from "../../DataVariable"; +import { Operator } from "."; + +export enum GenericOperation { + equals = 'equals', + isTruthy = 'isTruthy', + isFalsy = 'isFalsy', + isDefined = 'isDefined', + isNull = 'isNull', + isUndefined = 'isUndefined', + isArray = 'isArray', + isObject = 'isObject', + isString = 'isString', + isNumber = 'isNumber', + isBoolean = 'isBoolean', + isDefaultValue = 'isDefaultValue' // For Datasource variables +} + +export class GenericOperator extends Operator { + constructor(private operator: GenericOperation) { + super(); + } + + evaluate(left: any, right: any): boolean { + switch (this.operator) { + case 'equals': + return left === right; + case 'isTruthy': + return !!left; + case 'isFalsy': + return !left; + case 'isDefined': + return left !== undefined && left !== null; + case 'isNull': + return left === null; + case 'isUndefined': + return left === undefined; + case 'isArray': + return Array.isArray(left); + case 'isObject': + return typeof left === 'object' && left !== null; + case 'isString': + return typeof left === 'string'; + case 'isNumber': + return typeof left === 'number'; + case 'isBoolean': + return typeof left === 'boolean'; + case 'isDefaultValue': + return left instanceof DataVariable && left.get('default') === right; + default: + throw new Error(`Unsupported generic operator: ${this.operator}`); + } + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts new file mode 100644 index 0000000000..6b921fb763 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts @@ -0,0 +1,28 @@ +import { Operator } from "."; + +export enum LogicalOperation { + and = 'and', + or = 'or', + xor = 'xor' +} + +export class LogicalOperator extends Operator { + constructor(private operator: LogicalOperation) { + super(); + } + + evaluate(statements: boolean[]): boolean { + if (!statements.length) throw new Error("Expected one or more statments, got none"); + + switch (this.operator) { + case LogicalOperation.and: + return statements.every(Boolean); + case LogicalOperation.or: + return statements.some(Boolean); + case LogicalOperation.xor: + return statements.filter(Boolean).length === 1; + default: + throw new Error(`Unsupported logical operator: ${this.operator}`); + } + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts new file mode 100644 index 0000000000..d83a240de0 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -0,0 +1,35 @@ +import { Operator } from "."; + +export enum NumberOperation { + greaterThan = '>', + lessThan = '<', + greaterThanOrEqual = '>=', + lessThanOrEqual = '<=', + equals = '=', + notEquals = '!=' +} + +export class NumberOperator extends Operator { + constructor(private operator: NumberOperation) { + super(); + } + + evaluate(left: number, right: number): boolean { + switch (this.operator) { + case NumberOperation.greaterThan: + return left > right; + case NumberOperation.lessThan: + return left < right; + case NumberOperation.greaterThanOrEqual: + return left >= right; + case NumberOperation.lessThanOrEqual: + return left <= right; + case NumberOperation.equals: + return left === right; + case NumberOperation.notEquals: + return left !== right; + default: + throw new Error(`Unsupported number operator: ${this.operator}`); + } + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts new file mode 100644 index 0000000000..318eee1ec9 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts @@ -0,0 +1,29 @@ +import { Operator } from "."; + +export enum StringOperation { + contains = 'contains', + startsWith = 'startsWith', + endsWith = 'endsWith', + matchesRegex = 'matchesRegex' +} + +export class StringOperator extends Operator { + constructor(private operator: StringOperation) { + super(); + } + + evaluate(left: string, right: string): boolean { + switch (this.operator) { + case StringOperation.contains: + return left.includes(right); + case StringOperation.startsWith: + return left.startsWith(right); + case StringOperation.endsWith: + return left.endsWith(right); + case StringOperation.matchesRegex: + return new RegExp(right).test(left); + default: + throw new Error(`Unsupported string operator: ${this.operator}`); + } + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/index.ts b/packages/core/src/data_sources/model/conditional_variables/operators/index.ts new file mode 100644 index 0000000000..9f14e8e83c --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/index.ts @@ -0,0 +1,3 @@ +export abstract class Operator { + abstract evaluate(left: any, right: any): boolean; +} diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts new file mode 100644 index 0000000000..4cefbcdf3f --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts @@ -0,0 +1,170 @@ +import { DataCondition, Expression, LogicGroup } from "../../../../../src/data_sources/model/conditional_variables/DataCondition"; +import { GenericOperation } from "../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator"; +import { LogicalOperation } from "../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator"; +import { NumberOperation } from "../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator"; +import { StringOperation } from "../../../../../src/data_sources/model/conditional_variables/operators/StringOperations"; + +describe('DataCondition', () => { + describe('Basic Functionality Tests', () => { + test('should evaluate a simple boolean condition', () => { + const condition = true; + const dataCondition = new DataCondition(condition, 'Yes', 'No'); + + expect(dataCondition.getDataValue()).toBe('Yes'); + }); + + test('should return ifFalse when condition evaluates to false', () => { + const condition = false; + const dataCondition = new DataCondition(condition, 'Yes', 'No'); + + expect(dataCondition.getDataValue()).toBe('No'); + }); + }); + + describe('Operator Tests', () => { + test('should evaluate using GenericOperation operators', () => { + const condition: Expression = { left: 5, operator: GenericOperation.equals, right: 5 }; + const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal'); + + expect(dataCondition.getDataValue()).toBe('Equal'); + }); + + test('equals (false)', () => { + const condition: Expression = { + left: 'hello', + operator: GenericOperation.equals, + right: 'world', + }; + const dataCondition = new DataCondition(condition, 'true', 'false'); + expect(dataCondition.evaluate()).toBe(false); + }); + + test('should evaluate using StringOperation operators', () => { + const condition: Expression = { left: 'apple', operator: StringOperation.contains, right: 'app' }; + const dataCondition = new DataCondition(condition, 'Contains', 'Doesn\'t contain'); + + expect(dataCondition.getDataValue()).toBe('Contains'); + }); + + test('should evaluate using NumberOperation operators', () => { + const condition: Expression = { left: 10, operator: NumberOperation.lessThan, right: 15 }; + const dataCondition = new DataCondition(condition, 'Valid', 'Invalid'); + + expect(dataCondition.getDataValue()).toBe('Valid'); + }); + + test('should evaluate using LogicalOperation operators', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 } + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'Pass', 'Fail'); + expect(dataCondition.getDataValue()).toBe('Pass'); + }); + }); + + describe('Edge Case Tests', () => { + test('should throw error for invalid condition type', () => { + const invalidCondition: any = { randomField: 'randomValue' }; + expect(() => new DataCondition(invalidCondition, 'Yes', 'No')).toThrow('Invalid condition type.'); + }); + + test('should evaluate complex nested conditions', () => { + const nestedLogicGroup: LogicGroup = { + logicalOperator: LogicalOperation.or, + statements: [ + { + logicalOperator: LogicalOperation.and, + statements: [ + { left: 1, operator: NumberOperation.lessThan, right: 5 }, + { left: 'test', operator: GenericOperation.equals, right: 'test' } + ], + }, + { left: 10, operator: NumberOperation.greaterThan, right: 100 } + ], + }; + + const dataCondition = new DataCondition(nestedLogicGroup, 'Nested Pass', 'Nested Fail'); + expect(dataCondition.getDataValue()).toBe('Nested Pass'); + }); + }); + + describe('LogicalGroup Tests', () => { + test('should correctly handle AND logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 } + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('All true'); + }); + + test('should correctly handle OR logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.or, + statements: [ + { left: true, operator: GenericOperation.equals, right: false }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 } + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'At least one true', 'All false'); + expect(dataCondition.getDataValue()).toBe('At least one true'); + }); + + test('should correctly handle XOR logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.xor, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.lessThan, right: 3 }, + { left: false, operator: GenericOperation.equals, right: true }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'Exactly one true', 'Multiple true or all false'); + expect(dataCondition.getDataValue()).toBe('Exactly one true'); + }); + + test('should handle nested logical groups', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { + logicalOperator: LogicalOperation.or, + statements: [ + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + { left: false, operator: GenericOperation.equals, right: true }, + ], + }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('All true'); + }); + + test('should handle groups with false conditions', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: false, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('One or more false'); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts new file mode 100644 index 0000000000..cb5e4ee06c --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts @@ -0,0 +1,147 @@ +import { GenericOperator, GenericOperation } from "../../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator"; + +describe('GenericOperator', () => { + describe('Operator: equals', () => { + test('should return true when values are equal', () => { + const operator = new GenericOperator(GenericOperation.equals); + expect(operator.evaluate(5, 5)).toBe(true); + }); + + test('should return false when values are not equal', () => { + const operator = new GenericOperator(GenericOperation.equals); + expect(operator.evaluate(5, 10)).toBe(false); + }); + }); + + describe('Operator: isTruthy', () => { + test('should return true for truthy value', () => { + const operator = new GenericOperator(GenericOperation.isTruthy); + expect(operator.evaluate('non-empty', null)).toBe(true); + }); + + test('should return false for falsy value', () => { + const operator = new GenericOperator(GenericOperation.isTruthy); + expect(operator.evaluate('', null)).toBe(false); + }); + }); + + describe('Operator: isFalsy', () => { + test('should return true for falsy value', () => { + const operator = new GenericOperator(GenericOperation.isFalsy); + expect(operator.evaluate(0, null)).toBe(true); + }); + + test('should return false for truthy value', () => { + const operator = new GenericOperator(GenericOperation.isFalsy); + expect(operator.evaluate(1, null)).toBe(false); + }); + }); + + describe('Operator: isDefined', () => { + test('should return true for defined value', () => { + const operator = new GenericOperator(GenericOperation.isDefined); + expect(operator.evaluate(10, null)).toBe(true); + }); + + test('should return false for undefined value', () => { + const operator = new GenericOperator(GenericOperation.isDefined); + expect(operator.evaluate(undefined, null)).toBe(false); + }); + }); + + describe('Operator: isNull', () => { + test('should return true for null value', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(null, null)).toBe(true); + }); + + test('should return false for non-null value', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(0, null)).toBe(false); + }); + }); + + describe('Operator: isUndefined', () => { + test('should return true for undefined value', () => { + const operator = new GenericOperator(GenericOperation.isUndefined); + expect(operator.evaluate(undefined, null)).toBe(true); + }); + + test('should return false for defined value', () => { + const operator = new GenericOperator(GenericOperation.isUndefined); + expect(operator.evaluate(0, null)).toBe(false); + }); + }); + + describe('Operator: isArray', () => { + test('should return true for array', () => { + const operator = new GenericOperator(GenericOperation.isArray); + expect(operator.evaluate([1, 2, 3], null)).toBe(true); + }); + + test('should return false for non-array', () => { + const operator = new GenericOperator(GenericOperation.isArray); + expect(operator.evaluate('not an array', null)).toBe(false); + }); + }); + + describe('Operator: isObject', () => { + test('should return true for object', () => { + const operator = new GenericOperator(GenericOperation.isObject); + expect(operator.evaluate({ key: 'value' }, null)).toBe(true); + }); + + test('should return false for non-object', () => { + const operator = new GenericOperator(GenericOperation.isObject); + expect(operator.evaluate(42, null)).toBe(false); + }); + }); + + describe('Operator: isString', () => { + test('should return true for string', () => { + const operator = new GenericOperator(GenericOperation.isString); + expect(operator.evaluate('Hello', null)).toBe(true); + }); + + test('should return false for non-string', () => { + const operator = new GenericOperator(GenericOperation.isString); + expect(operator.evaluate(42, null)).toBe(false); + }); + }); + + describe('Operator: isNumber', () => { + test('should return true for number', () => { + const operator = new GenericOperator(GenericOperation.isNumber); + expect(operator.evaluate(42, null)).toBe(true); + }); + + test('should return false for non-number', () => { + const operator = new GenericOperator(GenericOperation.isNumber); + expect(operator.evaluate('not a number', null)).toBe(false); + }); + }); + + describe('Operator: isBoolean', () => { + test('should return true for boolean', () => { + const operator = new GenericOperator(GenericOperation.isBoolean); + expect(operator.evaluate(true, null)).toBe(true); + }); + + test('should return false for non-boolean', () => { + const operator = new GenericOperator(GenericOperation.isBoolean); + expect(operator.evaluate(1, null)).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should handle null as input gracefully', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(null, null)).toBe(true); + }); + + test('should throw error for unsupported operator', () => { + const operator = new GenericOperator('unsupported' as GenericOperation); + expect(() => operator.evaluate(1, 2)).toThrow('Unsupported generic operator: unsupported'); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts new file mode 100644 index 0000000000..2730dcb828 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts @@ -0,0 +1,58 @@ +import { LogicalOperator, LogicalOperation } from "../../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator"; + +describe('LogicalOperator', () => { + describe('Operator: and', () => { + test('should return true when all statements are true', () => { + const operator = new LogicalOperator(LogicalOperation.and); + expect(operator.evaluate([true, true, true])).toBe(true); + }); + + test('should return false when at least one statement is false', () => { + const operator = new LogicalOperator(LogicalOperation.and); + expect(operator.evaluate([true, false, true])).toBe(false); + }); + }); + + describe('Operator: or', () => { + test('should return true when at least one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.or); + expect(operator.evaluate([false, true, false])).toBe(true); + }); + + test('should return false when all statements are false', () => { + const operator = new LogicalOperator(LogicalOperation.or); + expect(operator.evaluate([false, false, false])).toBe(false); + }); + }); + + describe('Operator: xor', () => { + test('should return true when exactly one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([true, false, false])).toBe(true); + }); + + test('should return false when more than one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([true, true, false])).toBe(false); + }); + + test('should return false when no statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([false, false, false])).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should return false for xor with all false inputs', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([false, false])).toBe(false); + }); + + test('should throw error for unsupported operator', () => { + const operator = new LogicalOperator('unsupported' as LogicalOperation); + expect(() => operator.evaluate([true, false])).toThrow( + 'Unsupported logical operator: unsupported' + ); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts new file mode 100644 index 0000000000..13ccb999ef --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -0,0 +1,94 @@ +import { NumberOperator, NumberOperation } from "../../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator"; + +describe('NumberOperator', () => { + describe('Operator: greaterThan', () => { + test('should return true when left is greater than right', () => { + const operator = new NumberOperator(NumberOperation.greaterThan); + expect(operator.evaluate(5, 3)).toBe(true); + }); + + test('should return false when left is not greater than right', () => { + const operator = new NumberOperator(NumberOperation.greaterThan); + expect(operator.evaluate(2, 3)).toBe(false); + }); + }); + + describe('Operator: lessThan', () => { + test('should return true when left is less than right', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(2, 3)).toBe(true); + }); + + test('should return false when left is not less than right', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(5, 3)).toBe(false); + }); + }); + + describe('Operator: greaterThanOrEqual', () => { + test('should return true when left is greater than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); + expect(operator.evaluate(3, 3)).toBe(true); + }); + + test('should return false when left is not greater than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); + expect(operator.evaluate(2, 3)).toBe(false); + }); + }); + + describe('Operator: lessThanOrEqual', () => { + test('should return true when left is less than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.lessThanOrEqual); + expect(operator.evaluate(3, 3)).toBe(true); + }); + + test('should return false when left is not less than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.lessThanOrEqual); + expect(operator.evaluate(5, 3)).toBe(false); + }); + }); + + describe('Operator: equals', () => { + test('should return true when numbers are equal', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(4, 4)).toBe(true); + }); + + test('should return false when numbers are not equal', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(4, 5)).toBe(false); + }); + }); + + describe('Operator: notEquals', () => { + test('should return true when numbers are not equal', () => { + const operator = new NumberOperator(NumberOperation.notEquals); + expect(operator.evaluate(4, 5)).toBe(true); + }); + + test('should return false when numbers are equal', () => { + const operator = new NumberOperator(NumberOperation.notEquals); + expect(operator.evaluate(4, 4)).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should handle boundary values correctly', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(Number.MIN_VALUE, 1)).toBe(true); + }); + + test('should return false for NaN comparisons', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(NaN, NaN)).toBe(false); + }); + + test('should throw error for unsupported operator', () => { + const operator = new NumberOperator('unsupported' as NumberOperation); + expect(() => operator.evaluate(1, 2)).toThrow( + 'Unsupported number operator: unsupported' + ); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts new file mode 100644 index 0000000000..a5d87d8909 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts @@ -0,0 +1,80 @@ +import { StringOperator, StringOperation } from "../../../../../../src/data_sources/model/conditional_variables/operators/StringOperations"; + +describe('StringOperator', () => { + describe('Operator: contains', () => { + test('should return true when left contains right', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', 'world')).toBe(true); + }); + + test('should return false when left does not contain right', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', 'moon')).toBe(false); + }); + }); + + describe('Operator: startsWith', () => { + test('should return true when left starts with right', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', 'hello')).toBe(true); + }); + + test('should return false when left does not start with right', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', 'world')).toBe(false); + }); + }); + + describe('Operator: endsWith', () => { + test('should return true when left ends with right', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', 'world')).toBe(true); + }); + + test('should return false when left does not end with right', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', 'hello')).toBe(false); + }); + }); + + describe('Operator: matchesRegex', () => { + test('should return true when left matches the regex right', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(operator.evaluate('hello world', '^hello')).toBe(true); + }); + + test('should return false when left does not match the regex right', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(operator.evaluate('hello world', '^world')).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should return false for contains with empty right string', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', '')).toBe(true); // Empty string is included in any string + }); + + test('should return true for startsWith with empty right string', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', '')).toBe(true); // Any string starts with an empty string + }); + + test('should return true for endsWith with empty right string', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', '')).toBe(true); // Any string ends with an empty string + }); + + test('should throw error for invalid regex', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(() => operator.evaluate('hello world', '[')).toThrow(); + }); + + test('should throw error for unsupported operator', () => { + const operator = new StringOperator('unsupported' as StringOperation); + expect(() => operator.evaluate('test', 'test')).toThrow( + 'Unsupported string operator: unsupported' + ); + }); + }); +}); From c32204c8fbf3ff1dfc172de1f05d20376aaaf9be Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 21 Oct 2024 18:15:32 +0300 Subject: [PATCH 02/48] Format --- .../ConditionStatement.ts | 18 +- .../conditional_variables/DataCondition.ts | 80 ++--- .../LogicalGroupStatement.ts | 22 +- .../evaluateCondition.ts | 76 ++--- .../operators/GenericOperator.ts | 92 ++--- .../operators/LogicalOperator.ts | 38 +-- .../operators/NumberOperator.ts | 54 +-- .../operators/StringOperations.ts | 42 +-- .../conditional_variables/operators/index.ts | 2 +- .../conditional_variables/DataCondition.ts | 318 +++++++++--------- .../operators/GenericOperator.ts | 235 ++++++------- .../operators/LogicalOperator.ts | 109 +++--- .../operators/NumberOperator.ts | 141 ++++---- .../operators/StringOperator.ts | 121 +++---- 14 files changed, 679 insertions(+), 669 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts index bd96463175..93fd786b94 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts @@ -1,13 +1,13 @@ -import { Operator } from "./operators"; +import { Operator } from './operators'; export class ConditionStatement { - constructor( - private leftValue: any, - private operator: Operator, - private rightValue: any - ) { } + constructor( + private leftValue: any, + private operator: Operator, + private rightValue: any, + ) {} - evaluate(): boolean { - return this.operator.evaluate(this.leftValue, this.rightValue); - } + evaluate(): boolean { + return this.operator.evaluate(this.leftValue, this.rightValue); + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 13071d75f9..fbcf891969 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -1,50 +1,50 @@ -import { NumberOperation } from "./operators/NumberOperator"; -import { StringOperation } from "./operators/StringOperations"; -import { GenericOperation } from "./operators/GenericOperator"; -import { Model } from "../../../common"; -import { LogicalOperation } from "./operators/LogicalOperator"; -import { evaluateCondition } from "./evaluateCondition"; +import { NumberOperation } from './operators/NumberOperator'; +import { StringOperation } from './operators/StringOperations'; +import { GenericOperation } from './operators/GenericOperator'; +import { Model } from '../../../common'; +import { LogicalOperation } from './operators/LogicalOperator'; +import { evaluateCondition } from './evaluateCondition'; export const ConditionalVariableType = 'conditional-variable'; export type Expression = { - left: any; - operator: GenericOperation | StringOperation | NumberOperation; - right: any; + left: any; + operator: GenericOperation | StringOperation | NumberOperation; + right: any; }; export type LogicGroup = { - logicalOperator: LogicalOperation; - statements: (Expression | LogicGroup | boolean)[]; + logicalOperator: LogicalOperation; + statements: (Expression | LogicGroup | boolean)[]; }; export class DataCondition extends Model { - private conditionResult: boolean; - - defaults() { - return { - type: ConditionalVariableType, - condition: false, - }; - } - - constructor( - private condition: Expression | LogicGroup | boolean, - private ifTrue: any, - private ifFalse: any - ) { - super(); - this.conditionResult = this.evaluate(); - } - - evaluate() { - return evaluateCondition(this.condition); - } - - getDataValue(): any { - return this.conditionResult ? this.ifTrue : this.ifFalse; - } - - reevaluate(): void { - this.conditionResult = this.evaluate(); - } + private conditionResult: boolean; + + defaults() { + return { + type: ConditionalVariableType, + condition: false, + }; + } + + constructor( + private condition: Expression | LogicGroup | boolean, + private ifTrue: any, + private ifFalse: any, + ) { + super(); + this.conditionResult = this.evaluate(); + } + + evaluate() { + return evaluateCondition(this.condition); + } + + getDataValue(): any { + return this.conditionResult ? this.ifTrue : this.ifFalse; + } + + reevaluate(): void { + this.conditionResult = this.evaluate(); + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts index 7fb2348a34..4d4796c0df 100644 --- a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts @@ -1,15 +1,15 @@ -import { LogicalOperator } from "./operators/LogicalOperator"; -import { Expression, LogicGroup } from "./DataCondition"; -import { evaluateCondition } from "./evaluateCondition"; +import { LogicalOperator } from './operators/LogicalOperator'; +import { Expression, LogicGroup } from './DataCondition'; +import { evaluateCondition } from './evaluateCondition'; export class LogicalGroupStatement { - constructor( - private operator: LogicalOperator, - private statements: (Expression | LogicGroup | boolean)[] - ) { } + constructor( + private operator: LogicalOperator, + private statements: (Expression | LogicGroup | boolean)[], + ) {} - evaluate(): boolean { - const results = this.statements.map(statement => evaluateCondition(statement)); - return this.operator.evaluate(results); - } + evaluate(): boolean { + const results = this.statements.map((statement) => evaluateCondition(statement)); + return this.operator.evaluate(results); + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts index 53e6dea684..ee5dee77cf 100644 --- a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts @@ -1,53 +1,53 @@ -import { ConditionStatement } from "./ConditionStatement"; -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 { NumberOperation, NumberOperator } from "./operators/NumberOperator"; -import { StringOperation, StringOperator } from "./operators/StringOperations"; +import { ConditionStatement } from './ConditionStatement'; +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 { NumberOperation, NumberOperator } from './operators/NumberOperator'; +import { StringOperation, StringOperator } from './operators/StringOperations'; export function evaluateCondition(condition: any): boolean { - if (typeof condition === 'boolean') { - return condition; - } - - if (isLogicGroup(condition)) { - const { logicalOperator, statements } = condition; - const op = new LogicalOperator(logicalOperator) - const logicalGroup = new LogicalGroupStatement(op, statements); - return logicalGroup.evaluate(); - } - - if (isCondition(condition)) { - const { left, operator, right } = condition; - const op = operatorFactory(left, operator); - const statement = new ConditionStatement(left, op, right); - return statement.evaluate(); - } - - throw new Error('Invalid condition type.'); + if (typeof condition === 'boolean') { + return condition; + } + + if (isLogicGroup(condition)) { + const { logicalOperator, statements } = condition; + const op = new LogicalOperator(logicalOperator); + const logicalGroup = new LogicalGroupStatement(op, statements); + return logicalGroup.evaluate(); + } + + if (isCondition(condition)) { + const { left, operator, right } = condition; + const op = operatorFactory(left, operator); + const statement = new ConditionStatement(left, op, right); + return statement.evaluate(); + } + + throw new Error('Invalid condition type.'); } function operatorFactory(left: any, operator: string): Operator { - if (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}`); + if (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}`); } function isOperatorInEnum(operator: string, enumObject: any): boolean { - return Object.values(enumObject).includes(operator); + return Object.values(enumObject).includes(operator); } function isLogicGroup(condition: any): condition is LogicGroup { - return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); + return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); } function isCondition(condition: any): condition is Expression { - return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; + return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts index 467ee9268d..45ab4943db 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts @@ -1,54 +1,54 @@ -import DataVariable from "../../DataVariable"; -import { Operator } from "."; +import DataVariable from '../../DataVariable'; +import { Operator } from '.'; export enum GenericOperation { - equals = 'equals', - isTruthy = 'isTruthy', - isFalsy = 'isFalsy', - isDefined = 'isDefined', - isNull = 'isNull', - isUndefined = 'isUndefined', - isArray = 'isArray', - isObject = 'isObject', - isString = 'isString', - isNumber = 'isNumber', - isBoolean = 'isBoolean', - isDefaultValue = 'isDefaultValue' // For Datasource variables + equals = 'equals', + isTruthy = 'isTruthy', + isFalsy = 'isFalsy', + isDefined = 'isDefined', + isNull = 'isNull', + isUndefined = 'isUndefined', + isArray = 'isArray', + isObject = 'isObject', + isString = 'isString', + isNumber = 'isNumber', + isBoolean = 'isBoolean', + isDefaultValue = 'isDefaultValue', // For Datasource variables } export class GenericOperator extends Operator { - constructor(private operator: GenericOperation) { - super(); - } + constructor(private operator: GenericOperation) { + super(); + } - evaluate(left: any, right: any): boolean { - switch (this.operator) { - case 'equals': - return left === right; - case 'isTruthy': - return !!left; - case 'isFalsy': - return !left; - case 'isDefined': - return left !== undefined && left !== null; - case 'isNull': - return left === null; - case 'isUndefined': - return left === undefined; - case 'isArray': - return Array.isArray(left); - case 'isObject': - return typeof left === 'object' && left !== null; - case 'isString': - return typeof left === 'string'; - case 'isNumber': - return typeof left === 'number'; - case 'isBoolean': - return typeof left === 'boolean'; - case 'isDefaultValue': - return left instanceof DataVariable && left.get('default') === right; - default: - throw new Error(`Unsupported generic operator: ${this.operator}`); - } + evaluate(left: any, right: any): boolean { + switch (this.operator) { + case 'equals': + return left === right; + case 'isTruthy': + return !!left; + case 'isFalsy': + return !left; + case 'isDefined': + return left !== undefined && left !== null; + case 'isNull': + return left === null; + case 'isUndefined': + return left === undefined; + case 'isArray': + return Array.isArray(left); + case 'isObject': + return typeof left === 'object' && left !== null; + case 'isString': + return typeof left === 'string'; + case 'isNumber': + return typeof left === 'number'; + case 'isBoolean': + return typeof left === 'boolean'; + case 'isDefaultValue': + return left instanceof DataVariable && left.get('default') === right; + default: + throw new Error(`Unsupported generic operator: ${this.operator}`); } + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts index 6b921fb763..5c3aced384 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts @@ -1,28 +1,28 @@ -import { Operator } from "."; +import { Operator } from '.'; export enum LogicalOperation { - and = 'and', - or = 'or', - xor = 'xor' + and = 'and', + or = 'or', + xor = 'xor', } export class LogicalOperator extends Operator { - constructor(private operator: LogicalOperation) { - super(); - } + constructor(private operator: LogicalOperation) { + super(); + } - evaluate(statements: boolean[]): boolean { - if (!statements.length) throw new Error("Expected one or more statments, got none"); + evaluate(statements: boolean[]): boolean { + if (!statements.length) throw new Error('Expected one or more statments, got none'); - switch (this.operator) { - case LogicalOperation.and: - return statements.every(Boolean); - case LogicalOperation.or: - return statements.some(Boolean); - case LogicalOperation.xor: - return statements.filter(Boolean).length === 1; - default: - throw new Error(`Unsupported logical operator: ${this.operator}`); - } + switch (this.operator) { + case LogicalOperation.and: + return statements.every(Boolean); + case LogicalOperation.or: + return statements.some(Boolean); + case LogicalOperation.xor: + return statements.filter(Boolean).length === 1; + default: + throw new Error(`Unsupported logical operator: ${this.operator}`); } + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts index d83a240de0..db054680b0 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -1,35 +1,35 @@ -import { Operator } from "."; +import { Operator } from '.'; export enum NumberOperation { - greaterThan = '>', - lessThan = '<', - greaterThanOrEqual = '>=', - lessThanOrEqual = '<=', - equals = '=', - notEquals = '!=' + greaterThan = '>', + lessThan = '<', + greaterThanOrEqual = '>=', + lessThanOrEqual = '<=', + equals = '=', + notEquals = '!=', } export class NumberOperator extends Operator { - constructor(private operator: NumberOperation) { - super(); - } + constructor(private operator: NumberOperation) { + super(); + } - evaluate(left: number, right: number): boolean { - switch (this.operator) { - case NumberOperation.greaterThan: - return left > right; - case NumberOperation.lessThan: - return left < right; - case NumberOperation.greaterThanOrEqual: - return left >= right; - case NumberOperation.lessThanOrEqual: - return left <= right; - case NumberOperation.equals: - return left === right; - case NumberOperation.notEquals: - return left !== right; - default: - throw new Error(`Unsupported number operator: ${this.operator}`); - } + evaluate(left: number, right: number): boolean { + switch (this.operator) { + case NumberOperation.greaterThan: + return left > right; + case NumberOperation.lessThan: + return left < right; + case NumberOperation.greaterThanOrEqual: + return left >= right; + case NumberOperation.lessThanOrEqual: + return left <= right; + case NumberOperation.equals: + return left === right; + case NumberOperation.notEquals: + return left !== right; + default: + throw new Error(`Unsupported number operator: ${this.operator}`); } + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts index 318eee1ec9..d7ac89ab63 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts @@ -1,29 +1,29 @@ -import { Operator } from "."; +import { Operator } from '.'; export enum StringOperation { - contains = 'contains', - startsWith = 'startsWith', - endsWith = 'endsWith', - matchesRegex = 'matchesRegex' + contains = 'contains', + startsWith = 'startsWith', + endsWith = 'endsWith', + matchesRegex = 'matchesRegex', } export class StringOperator extends Operator { - constructor(private operator: StringOperation) { - super(); - } + constructor(private operator: StringOperation) { + super(); + } - evaluate(left: string, right: string): boolean { - switch (this.operator) { - case StringOperation.contains: - return left.includes(right); - case StringOperation.startsWith: - return left.startsWith(right); - case StringOperation.endsWith: - return left.endsWith(right); - case StringOperation.matchesRegex: - return new RegExp(right).test(left); - default: - throw new Error(`Unsupported string operator: ${this.operator}`); - } + evaluate(left: string, right: string): boolean { + switch (this.operator) { + case StringOperation.contains: + return left.includes(right); + case StringOperation.startsWith: + return left.startsWith(right); + case StringOperation.endsWith: + return left.endsWith(right); + case StringOperation.matchesRegex: + return new RegExp(right).test(left); + default: + throw new Error(`Unsupported string operator: ${this.operator}`); } + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/index.ts b/packages/core/src/data_sources/model/conditional_variables/operators/index.ts index 9f14e8e83c..5ae1f2879f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/index.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/index.ts @@ -1,3 +1,3 @@ export abstract class Operator { - abstract evaluate(left: any, right: any): boolean; + abstract evaluate(left: any, right: any): boolean; } diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts index 4cefbcdf3f..c3f7da595b 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts @@ -1,170 +1,174 @@ -import { DataCondition, Expression, LogicGroup } from "../../../../../src/data_sources/model/conditional_variables/DataCondition"; -import { GenericOperation } from "../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator"; -import { LogicalOperation } from "../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator"; -import { NumberOperation } from "../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator"; -import { StringOperation } from "../../../../../src/data_sources/model/conditional_variables/operators/StringOperations"; +import { + DataCondition, + Expression, + LogicGroup, +} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { LogicalOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; describe('DataCondition', () => { - describe('Basic Functionality Tests', () => { - test('should evaluate a simple boolean condition', () => { - const condition = true; - const dataCondition = new DataCondition(condition, 'Yes', 'No'); + describe('Basic Functionality Tests', () => { + test('should evaluate a simple boolean condition', () => { + const condition = true; + const dataCondition = new DataCondition(condition, 'Yes', 'No'); - expect(dataCondition.getDataValue()).toBe('Yes'); - }); + expect(dataCondition.getDataValue()).toBe('Yes'); + }); + + test('should return ifFalse when condition evaluates to false', () => { + const condition = false; + const dataCondition = new DataCondition(condition, 'Yes', 'No'); + + expect(dataCondition.getDataValue()).toBe('No'); + }); + }); + + describe('Operator Tests', () => { + test('should evaluate using GenericOperation operators', () => { + const condition: Expression = { left: 5, operator: GenericOperation.equals, right: 5 }; + const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal'); - test('should return ifFalse when condition evaluates to false', () => { - const condition = false; - const dataCondition = new DataCondition(condition, 'Yes', 'No'); + expect(dataCondition.getDataValue()).toBe('Equal'); + }); + + test('equals (false)', () => { + const condition: Expression = { + left: 'hello', + operator: GenericOperation.equals, + right: 'world', + }; + const dataCondition = new DataCondition(condition, 'true', 'false'); + expect(dataCondition.evaluate()).toBe(false); + }); + + test('should evaluate using StringOperation operators', () => { + const condition: Expression = { left: 'apple', operator: StringOperation.contains, right: 'app' }; + const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain"); + + expect(dataCondition.getDataValue()).toBe('Contains'); + }); + + test('should evaluate using NumberOperation operators', () => { + const condition: Expression = { left: 10, operator: NumberOperation.lessThan, right: 15 }; + const dataCondition = new DataCondition(condition, 'Valid', 'Invalid'); + + expect(dataCondition.getDataValue()).toBe('Valid'); + }); + + test('should evaluate using LogicalOperation operators', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'Pass', 'Fail'); + expect(dataCondition.getDataValue()).toBe('Pass'); + }); + }); + + describe('Edge Case Tests', () => { + test('should throw error for invalid condition type', () => { + const invalidCondition: any = { randomField: 'randomValue' }; + expect(() => new DataCondition(invalidCondition, 'Yes', 'No')).toThrow('Invalid condition type.'); + }); + + test('should evaluate complex nested conditions', () => { + const nestedLogicGroup: LogicGroup = { + logicalOperator: LogicalOperation.or, + statements: [ + { + logicalOperator: LogicalOperation.and, + statements: [ + { left: 1, operator: NumberOperation.lessThan, right: 5 }, + { left: 'test', operator: GenericOperation.equals, right: 'test' }, + ], + }, + { left: 10, operator: NumberOperation.greaterThan, right: 100 }, + ], + }; + + const dataCondition = new DataCondition(nestedLogicGroup, 'Nested Pass', 'Nested Fail'); + expect(dataCondition.getDataValue()).toBe('Nested Pass'); + }); + }); + + describe('LogicalGroup Tests', () => { + test('should correctly handle AND logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('All true'); + }); - expect(dataCondition.getDataValue()).toBe('No'); - }); + test('should correctly handle OR logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.or, + statements: [ + { left: true, operator: GenericOperation.equals, right: false }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'At least one true', 'All false'); + expect(dataCondition.getDataValue()).toBe('At least one true'); }); - describe('Operator Tests', () => { - test('should evaluate using GenericOperation operators', () => { - const condition: Expression = { left: 5, operator: GenericOperation.equals, right: 5 }; - const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal'); - - expect(dataCondition.getDataValue()).toBe('Equal'); - }); - - test('equals (false)', () => { - const condition: Expression = { - left: 'hello', - operator: GenericOperation.equals, - right: 'world', - }; - const dataCondition = new DataCondition(condition, 'true', 'false'); - expect(dataCondition.evaluate()).toBe(false); - }); - - test('should evaluate using StringOperation operators', () => { - const condition: Expression = { left: 'apple', operator: StringOperation.contains, right: 'app' }; - const dataCondition = new DataCondition(condition, 'Contains', 'Doesn\'t contain'); - - expect(dataCondition.getDataValue()).toBe('Contains'); - }); - - test('should evaluate using NumberOperation operators', () => { - const condition: Expression = { left: 10, operator: NumberOperation.lessThan, right: 15 }; - const dataCondition = new DataCondition(condition, 'Valid', 'Invalid'); - - expect(dataCondition.getDataValue()).toBe('Valid'); - }); - - test('should evaluate using LogicalOperation operators', () => { - const logicGroup: LogicGroup = { - logicalOperator: LogicalOperation.and, - statements: [ - { left: true, operator: GenericOperation.equals, right: true }, - { left: 5, operator: NumberOperation.greaterThan, right: 3 } - ], - }; - - const dataCondition = new DataCondition(logicGroup, 'Pass', 'Fail'); - expect(dataCondition.getDataValue()).toBe('Pass'); - }); + test('should correctly handle XOR logical operator', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.xor, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.lessThan, right: 3 }, + { left: false, operator: GenericOperation.equals, right: true }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'Exactly one true', 'Multiple true or all false'); + expect(dataCondition.getDataValue()).toBe('Exactly one true'); }); - describe('Edge Case Tests', () => { - test('should throw error for invalid condition type', () => { - const invalidCondition: any = { randomField: 'randomValue' }; - expect(() => new DataCondition(invalidCondition, 'Yes', 'No')).toThrow('Invalid condition type.'); - }); - - test('should evaluate complex nested conditions', () => { - const nestedLogicGroup: LogicGroup = { - logicalOperator: LogicalOperation.or, - statements: [ - { - logicalOperator: LogicalOperation.and, - statements: [ - { left: 1, operator: NumberOperation.lessThan, right: 5 }, - { left: 'test', operator: GenericOperation.equals, right: 'test' } - ], - }, - { left: 10, operator: NumberOperation.greaterThan, right: 100 } - ], - }; - - const dataCondition = new DataCondition(nestedLogicGroup, 'Nested Pass', 'Nested Fail'); - expect(dataCondition.getDataValue()).toBe('Nested Pass'); - }); + test('should handle nested logical groups', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { + logicalOperator: LogicalOperation.or, + statements: [ + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + { left: false, operator: GenericOperation.equals, right: true }, + ], + }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('All true'); }); - describe('LogicalGroup Tests', () => { - test('should correctly handle AND logical operator', () => { - const logicGroup: LogicGroup = { - logicalOperator: LogicalOperation.and, - statements: [ - { left: true, operator: GenericOperation.equals, right: true }, - { left: 5, operator: NumberOperation.greaterThan, right: 3 } - ], - }; - - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); - expect(dataCondition.getDataValue()).toBe('All true'); - }); - - test('should correctly handle OR logical operator', () => { - const logicGroup: LogicGroup = { - logicalOperator: LogicalOperation.or, - statements: [ - { left: true, operator: GenericOperation.equals, right: false }, - { left: 5, operator: NumberOperation.greaterThan, right: 3 } - ], - }; - - const dataCondition = new DataCondition(logicGroup, 'At least one true', 'All false'); - expect(dataCondition.getDataValue()).toBe('At least one true'); - }); - - test('should correctly handle XOR logical operator', () => { - const logicGroup: LogicGroup = { - logicalOperator: LogicalOperation.xor, - statements: [ - { left: true, operator: GenericOperation.equals, right: true }, - { left: 5, operator: NumberOperation.lessThan, right: 3 }, - { left: false, operator: GenericOperation.equals, right: true }, - ], - }; - - const dataCondition = new DataCondition(logicGroup, 'Exactly one true', 'Multiple true or all false'); - expect(dataCondition.getDataValue()).toBe('Exactly one true'); - }); - - test('should handle nested logical groups', () => { - const logicGroup: LogicGroup = { - logicalOperator: LogicalOperation.and, - statements: [ - { left: true, operator: GenericOperation.equals, right: true }, - { - logicalOperator: LogicalOperation.or, - statements: [ - { left: 5, operator: NumberOperation.greaterThan, right: 3 }, - { left: false, operator: GenericOperation.equals, right: true }, - ], - }, - ], - }; - - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); - expect(dataCondition.getDataValue()).toBe('All true'); - }); - - test('should handle groups with false conditions', () => { - const logicGroup: LogicGroup = { - logicalOperator: LogicalOperation.and, - statements: [ - { left: true, operator: GenericOperation.equals, right: true }, - { left: false, operator: GenericOperation.equals, right: true }, - { left: 5, operator: NumberOperation.greaterThan, right: 3 }, - ], - }; - - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); - expect(dataCondition.getDataValue()).toBe('One or more false'); - }); + test('should handle groups with false conditions', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { left: true, operator: GenericOperation.equals, right: true }, + { left: false, operator: GenericOperation.equals, right: true }, + { left: 5, operator: NumberOperation.greaterThan, right: 3 }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + expect(dataCondition.getDataValue()).toBe('One or more false'); }); + }); }); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts index cb5e4ee06c..516bb5ce4d 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts @@ -1,147 +1,150 @@ -import { GenericOperator, GenericOperation } from "../../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator"; +import { + GenericOperator, + GenericOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; describe('GenericOperator', () => { - describe('Operator: equals', () => { - test('should return true when values are equal', () => { - const operator = new GenericOperator(GenericOperation.equals); - expect(operator.evaluate(5, 5)).toBe(true); - }); - - test('should return false when values are not equal', () => { - const operator = new GenericOperator(GenericOperation.equals); - expect(operator.evaluate(5, 10)).toBe(false); - }); + describe('Operator: equals', () => { + test('should return true when values are equal', () => { + const operator = new GenericOperator(GenericOperation.equals); + expect(operator.evaluate(5, 5)).toBe(true); }); - - describe('Operator: isTruthy', () => { - test('should return true for truthy value', () => { - const operator = new GenericOperator(GenericOperation.isTruthy); - expect(operator.evaluate('non-empty', null)).toBe(true); - }); - - test('should return false for falsy value', () => { - const operator = new GenericOperator(GenericOperation.isTruthy); - expect(operator.evaluate('', null)).toBe(false); - }); + + test('should return false when values are not equal', () => { + const operator = new GenericOperator(GenericOperation.equals); + expect(operator.evaluate(5, 10)).toBe(false); }); - - describe('Operator: isFalsy', () => { - test('should return true for falsy value', () => { - const operator = new GenericOperator(GenericOperation.isFalsy); - expect(operator.evaluate(0, null)).toBe(true); - }); - - test('should return false for truthy value', () => { - const operator = new GenericOperator(GenericOperation.isFalsy); - expect(operator.evaluate(1, null)).toBe(false); - }); + }); + + describe('Operator: isTruthy', () => { + test('should return true for truthy value', () => { + const operator = new GenericOperator(GenericOperation.isTruthy); + expect(operator.evaluate('non-empty', null)).toBe(true); }); - - describe('Operator: isDefined', () => { - test('should return true for defined value', () => { - const operator = new GenericOperator(GenericOperation.isDefined); - expect(operator.evaluate(10, null)).toBe(true); - }); - - test('should return false for undefined value', () => { - const operator = new GenericOperator(GenericOperation.isDefined); - expect(operator.evaluate(undefined, null)).toBe(false); - }); + + test('should return false for falsy value', () => { + const operator = new GenericOperator(GenericOperation.isTruthy); + expect(operator.evaluate('', null)).toBe(false); }); + }); - describe('Operator: isNull', () => { - test('should return true for null value', () => { - const operator = new GenericOperator(GenericOperation.isNull); - expect(operator.evaluate(null, null)).toBe(true); - }); + describe('Operator: isFalsy', () => { + test('should return true for falsy value', () => { + const operator = new GenericOperator(GenericOperation.isFalsy); + expect(operator.evaluate(0, null)).toBe(true); + }); - test('should return false for non-null value', () => { - const operator = new GenericOperator(GenericOperation.isNull); - expect(operator.evaluate(0, null)).toBe(false); - }); + test('should return false for truthy value', () => { + const operator = new GenericOperator(GenericOperation.isFalsy); + expect(operator.evaluate(1, null)).toBe(false); }); + }); - describe('Operator: isUndefined', () => { - test('should return true for undefined value', () => { - const operator = new GenericOperator(GenericOperation.isUndefined); - expect(operator.evaluate(undefined, null)).toBe(true); - }); + describe('Operator: isDefined', () => { + test('should return true for defined value', () => { + const operator = new GenericOperator(GenericOperation.isDefined); + expect(operator.evaluate(10, null)).toBe(true); + }); - test('should return false for defined value', () => { - const operator = new GenericOperator(GenericOperation.isUndefined); - expect(operator.evaluate(0, null)).toBe(false); - }); + test('should return false for undefined value', () => { + const operator = new GenericOperator(GenericOperation.isDefined); + expect(operator.evaluate(undefined, null)).toBe(false); }); + }); - describe('Operator: isArray', () => { - test('should return true for array', () => { - const operator = new GenericOperator(GenericOperation.isArray); - expect(operator.evaluate([1, 2, 3], null)).toBe(true); - }); + describe('Operator: isNull', () => { + test('should return true for null value', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(null, null)).toBe(true); + }); - test('should return false for non-array', () => { - const operator = new GenericOperator(GenericOperation.isArray); - expect(operator.evaluate('not an array', null)).toBe(false); - }); + test('should return false for non-null value', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(0, null)).toBe(false); }); + }); - describe('Operator: isObject', () => { - test('should return true for object', () => { - const operator = new GenericOperator(GenericOperation.isObject); - expect(operator.evaluate({ key: 'value' }, null)).toBe(true); - }); + describe('Operator: isUndefined', () => { + test('should return true for undefined value', () => { + const operator = new GenericOperator(GenericOperation.isUndefined); + expect(operator.evaluate(undefined, null)).toBe(true); + }); - test('should return false for non-object', () => { - const operator = new GenericOperator(GenericOperation.isObject); - expect(operator.evaluate(42, null)).toBe(false); - }); + test('should return false for defined value', () => { + const operator = new GenericOperator(GenericOperation.isUndefined); + expect(operator.evaluate(0, null)).toBe(false); }); + }); - describe('Operator: isString', () => { - test('should return true for string', () => { - const operator = new GenericOperator(GenericOperation.isString); - expect(operator.evaluate('Hello', null)).toBe(true); - }); + describe('Operator: isArray', () => { + test('should return true for array', () => { + const operator = new GenericOperator(GenericOperation.isArray); + expect(operator.evaluate([1, 2, 3], null)).toBe(true); + }); - test('should return false for non-string', () => { - const operator = new GenericOperator(GenericOperation.isString); - expect(operator.evaluate(42, null)).toBe(false); - }); + test('should return false for non-array', () => { + const operator = new GenericOperator(GenericOperation.isArray); + expect(operator.evaluate('not an array', null)).toBe(false); }); + }); - describe('Operator: isNumber', () => { - test('should return true for number', () => { - const operator = new GenericOperator(GenericOperation.isNumber); - expect(operator.evaluate(42, null)).toBe(true); - }); + describe('Operator: isObject', () => { + test('should return true for object', () => { + const operator = new GenericOperator(GenericOperation.isObject); + expect(operator.evaluate({ key: 'value' }, null)).toBe(true); + }); - test('should return false for non-number', () => { - const operator = new GenericOperator(GenericOperation.isNumber); - expect(operator.evaluate('not a number', null)).toBe(false); - }); + test('should return false for non-object', () => { + const operator = new GenericOperator(GenericOperation.isObject); + expect(operator.evaluate(42, null)).toBe(false); }); + }); - describe('Operator: isBoolean', () => { - test('should return true for boolean', () => { - const operator = new GenericOperator(GenericOperation.isBoolean); - expect(operator.evaluate(true, null)).toBe(true); - }); + describe('Operator: isString', () => { + test('should return true for string', () => { + const operator = new GenericOperator(GenericOperation.isString); + expect(operator.evaluate('Hello', null)).toBe(true); + }); - test('should return false for non-boolean', () => { - const operator = new GenericOperator(GenericOperation.isBoolean); - expect(operator.evaluate(1, null)).toBe(false); - }); + test('should return false for non-string', () => { + const operator = new GenericOperator(GenericOperation.isString); + expect(operator.evaluate(42, null)).toBe(false); }); + }); - describe('Edge Case Tests', () => { - test('should handle null as input gracefully', () => { - const operator = new GenericOperator(GenericOperation.isNull); - expect(operator.evaluate(null, null)).toBe(true); - }); + describe('Operator: isNumber', () => { + test('should return true for number', () => { + const operator = new GenericOperator(GenericOperation.isNumber); + expect(operator.evaluate(42, null)).toBe(true); + }); + + test('should return false for non-number', () => { + const operator = new GenericOperator(GenericOperation.isNumber); + expect(operator.evaluate('not a number', null)).toBe(false); + }); + }); + + describe('Operator: isBoolean', () => { + test('should return true for boolean', () => { + const operator = new GenericOperator(GenericOperation.isBoolean); + expect(operator.evaluate(true, null)).toBe(true); + }); + + test('should return false for non-boolean', () => { + const operator = new GenericOperator(GenericOperation.isBoolean); + expect(operator.evaluate(1, null)).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should handle null as input gracefully', () => { + const operator = new GenericOperator(GenericOperation.isNull); + expect(operator.evaluate(null, null)).toBe(true); + }); - test('should throw error for unsupported operator', () => { - const operator = new GenericOperator('unsupported' as GenericOperation); - expect(() => operator.evaluate(1, 2)).toThrow('Unsupported generic operator: unsupported'); - }); + test('should throw error for unsupported operator', () => { + const operator = new GenericOperator('unsupported' as GenericOperation); + expect(() => operator.evaluate(1, 2)).toThrow('Unsupported generic operator: unsupported'); }); + }); }); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts index 2730dcb828..a81809858a 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts @@ -1,58 +1,59 @@ -import { LogicalOperator, LogicalOperation } from "../../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator"; +import { + LogicalOperator, + LogicalOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; describe('LogicalOperator', () => { - describe('Operator: and', () => { - test('should return true when all statements are true', () => { - const operator = new LogicalOperator(LogicalOperation.and); - expect(operator.evaluate([true, true, true])).toBe(true); - }); - - test('should return false when at least one statement is false', () => { - const operator = new LogicalOperator(LogicalOperation.and); - expect(operator.evaluate([true, false, true])).toBe(false); - }); - }); - - describe('Operator: or', () => { - test('should return true when at least one statement is true', () => { - const operator = new LogicalOperator(LogicalOperation.or); - expect(operator.evaluate([false, true, false])).toBe(true); - }); - - test('should return false when all statements are false', () => { - const operator = new LogicalOperator(LogicalOperation.or); - expect(operator.evaluate([false, false, false])).toBe(false); - }); - }); - - describe('Operator: xor', () => { - test('should return true when exactly one statement is true', () => { - const operator = new LogicalOperator(LogicalOperation.xor); - expect(operator.evaluate([true, false, false])).toBe(true); - }); - - test('should return false when more than one statement is true', () => { - const operator = new LogicalOperator(LogicalOperation.xor); - expect(operator.evaluate([true, true, false])).toBe(false); - }); - - test('should return false when no statement is true', () => { - const operator = new LogicalOperator(LogicalOperation.xor); - expect(operator.evaluate([false, false, false])).toBe(false); - }); - }); - - describe('Edge Case Tests', () => { - test('should return false for xor with all false inputs', () => { - const operator = new LogicalOperator(LogicalOperation.xor); - expect(operator.evaluate([false, false])).toBe(false); - }); - - test('should throw error for unsupported operator', () => { - const operator = new LogicalOperator('unsupported' as LogicalOperation); - expect(() => operator.evaluate([true, false])).toThrow( - 'Unsupported logical operator: unsupported' - ); - }); + describe('Operator: and', () => { + test('should return true when all statements are true', () => { + const operator = new LogicalOperator(LogicalOperation.and); + expect(operator.evaluate([true, true, true])).toBe(true); }); + + test('should return false when at least one statement is false', () => { + const operator = new LogicalOperator(LogicalOperation.and); + expect(operator.evaluate([true, false, true])).toBe(false); + }); + }); + + describe('Operator: or', () => { + test('should return true when at least one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.or); + expect(operator.evaluate([false, true, false])).toBe(true); + }); + + test('should return false when all statements are false', () => { + const operator = new LogicalOperator(LogicalOperation.or); + expect(operator.evaluate([false, false, false])).toBe(false); + }); + }); + + describe('Operator: xor', () => { + test('should return true when exactly one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([true, false, false])).toBe(true); + }); + + test('should return false when more than one statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([true, true, false])).toBe(false); + }); + + test('should return false when no statement is true', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([false, false, false])).toBe(false); + }); + }); + + describe('Edge Case Tests', () => { + test('should return false for xor with all false inputs', () => { + const operator = new LogicalOperator(LogicalOperation.xor); + expect(operator.evaluate([false, false])).toBe(false); + }); + + test('should throw error for unsupported operator', () => { + const operator = new LogicalOperator('unsupported' as LogicalOperation); + expect(() => operator.evaluate([true, false])).toThrow('Unsupported logical operator: unsupported'); + }); + }); }); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts index 13ccb999ef..2c719338c9 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -1,94 +1,95 @@ -import { NumberOperator, NumberOperation } from "../../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator"; +import { + NumberOperator, + NumberOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; describe('NumberOperator', () => { - describe('Operator: greaterThan', () => { - test('should return true when left is greater than right', () => { - const operator = new NumberOperator(NumberOperation.greaterThan); - expect(operator.evaluate(5, 3)).toBe(true); - }); + describe('Operator: greaterThan', () => { + test('should return true when left is greater than right', () => { + const operator = new NumberOperator(NumberOperation.greaterThan); + expect(operator.evaluate(5, 3)).toBe(true); + }); - test('should return false when left is not greater than right', () => { - const operator = new NumberOperator(NumberOperation.greaterThan); - expect(operator.evaluate(2, 3)).toBe(false); - }); + test('should return false when left is not greater than right', () => { + const operator = new NumberOperator(NumberOperation.greaterThan); + expect(operator.evaluate(2, 3)).toBe(false); }); + }); - describe('Operator: lessThan', () => { - test('should return true when left is less than right', () => { - const operator = new NumberOperator(NumberOperation.lessThan); - expect(operator.evaluate(2, 3)).toBe(true); - }); + describe('Operator: lessThan', () => { + test('should return true when left is less than right', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(2, 3)).toBe(true); + }); - test('should return false when left is not less than right', () => { - const operator = new NumberOperator(NumberOperation.lessThan); - expect(operator.evaluate(5, 3)).toBe(false); - }); + test('should return false when left is not less than right', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(5, 3)).toBe(false); }); + }); - describe('Operator: greaterThanOrEqual', () => { - test('should return true when left is greater than or equal to right', () => { - const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); - expect(operator.evaluate(3, 3)).toBe(true); - }); + describe('Operator: greaterThanOrEqual', () => { + test('should return true when left is greater than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); + expect(operator.evaluate(3, 3)).toBe(true); + }); - test('should return false when left is not greater than or equal to right', () => { - const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); - expect(operator.evaluate(2, 3)).toBe(false); - }); + test('should return false when left is not greater than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.greaterThanOrEqual); + expect(operator.evaluate(2, 3)).toBe(false); }); + }); - describe('Operator: lessThanOrEqual', () => { - test('should return true when left is less than or equal to right', () => { - const operator = new NumberOperator(NumberOperation.lessThanOrEqual); - expect(operator.evaluate(3, 3)).toBe(true); - }); + describe('Operator: lessThanOrEqual', () => { + test('should return true when left is less than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.lessThanOrEqual); + expect(operator.evaluate(3, 3)).toBe(true); + }); - test('should return false when left is not less than or equal to right', () => { - const operator = new NumberOperator(NumberOperation.lessThanOrEqual); - expect(operator.evaluate(5, 3)).toBe(false); - }); + test('should return false when left is not less than or equal to right', () => { + const operator = new NumberOperator(NumberOperation.lessThanOrEqual); + expect(operator.evaluate(5, 3)).toBe(false); }); + }); - describe('Operator: equals', () => { - test('should return true when numbers are equal', () => { - const operator = new NumberOperator(NumberOperation.equals); - expect(operator.evaluate(4, 4)).toBe(true); - }); + describe('Operator: equals', () => { + test('should return true when numbers are equal', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(4, 4)).toBe(true); + }); - test('should return false when numbers are not equal', () => { - const operator = new NumberOperator(NumberOperation.equals); - expect(operator.evaluate(4, 5)).toBe(false); - }); + test('should return false when numbers are not equal', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(4, 5)).toBe(false); }); + }); - describe('Operator: notEquals', () => { - test('should return true when numbers are not equal', () => { - const operator = new NumberOperator(NumberOperation.notEquals); - expect(operator.evaluate(4, 5)).toBe(true); - }); + describe('Operator: notEquals', () => { + test('should return true when numbers are not equal', () => { + const operator = new NumberOperator(NumberOperation.notEquals); + expect(operator.evaluate(4, 5)).toBe(true); + }); - test('should return false when numbers are equal', () => { - const operator = new NumberOperator(NumberOperation.notEquals); - expect(operator.evaluate(4, 4)).toBe(false); - }); + test('should return false when numbers are equal', () => { + const operator = new NumberOperator(NumberOperation.notEquals); + expect(operator.evaluate(4, 4)).toBe(false); }); + }); - describe('Edge Case Tests', () => { - test('should handle boundary values correctly', () => { - const operator = new NumberOperator(NumberOperation.lessThan); - expect(operator.evaluate(Number.MIN_VALUE, 1)).toBe(true); - }); + describe('Edge Case Tests', () => { + test('should handle boundary values correctly', () => { + const operator = new NumberOperator(NumberOperation.lessThan); + expect(operator.evaluate(Number.MIN_VALUE, 1)).toBe(true); + }); - test('should return false for NaN comparisons', () => { - const operator = new NumberOperator(NumberOperation.equals); - expect(operator.evaluate(NaN, NaN)).toBe(false); - }); + test('should return false for NaN comparisons', () => { + const operator = new NumberOperator(NumberOperation.equals); + expect(operator.evaluate(NaN, NaN)).toBe(false); + }); - test('should throw error for unsupported operator', () => { - const operator = new NumberOperator('unsupported' as NumberOperation); - expect(() => operator.evaluate(1, 2)).toThrow( - 'Unsupported number operator: unsupported' - ); - }); + test('should throw error for unsupported operator', () => { + const operator = new NumberOperator('unsupported' as NumberOperation); + expect(() => operator.evaluate(1, 2)).toThrow('Unsupported number operator: unsupported'); }); + }); }); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts b/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts index a5d87d8909..6e291cf942 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/operators/StringOperator.ts @@ -1,80 +1,81 @@ -import { StringOperator, StringOperation } from "../../../../../../src/data_sources/model/conditional_variables/operators/StringOperations"; +import { + StringOperator, + StringOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; describe('StringOperator', () => { - describe('Operator: contains', () => { - test('should return true when left contains right', () => { - const operator = new StringOperator(StringOperation.contains); - expect(operator.evaluate('hello world', 'world')).toBe(true); - }); + describe('Operator: contains', () => { + test('should return true when left contains right', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', 'world')).toBe(true); + }); - test('should return false when left does not contain right', () => { - const operator = new StringOperator(StringOperation.contains); - expect(operator.evaluate('hello world', 'moon')).toBe(false); - }); + test('should return false when left does not contain right', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', 'moon')).toBe(false); }); + }); - describe('Operator: startsWith', () => { - test('should return true when left starts with right', () => { - const operator = new StringOperator(StringOperation.startsWith); - expect(operator.evaluate('hello world', 'hello')).toBe(true); - }); + describe('Operator: startsWith', () => { + test('should return true when left starts with right', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', 'hello')).toBe(true); + }); - test('should return false when left does not start with right', () => { - const operator = new StringOperator(StringOperation.startsWith); - expect(operator.evaluate('hello world', 'world')).toBe(false); - }); + test('should return false when left does not start with right', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', 'world')).toBe(false); }); + }); - describe('Operator: endsWith', () => { - test('should return true when left ends with right', () => { - const operator = new StringOperator(StringOperation.endsWith); - expect(operator.evaluate('hello world', 'world')).toBe(true); - }); + describe('Operator: endsWith', () => { + test('should return true when left ends with right', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', 'world')).toBe(true); + }); - test('should return false when left does not end with right', () => { - const operator = new StringOperator(StringOperation.endsWith); - expect(operator.evaluate('hello world', 'hello')).toBe(false); - }); + test('should return false when left does not end with right', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', 'hello')).toBe(false); }); + }); - describe('Operator: matchesRegex', () => { - test('should return true when left matches the regex right', () => { - const operator = new StringOperator(StringOperation.matchesRegex); - expect(operator.evaluate('hello world', '^hello')).toBe(true); - }); + describe('Operator: matchesRegex', () => { + test('should return true when left matches the regex right', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(operator.evaluate('hello world', '^hello')).toBe(true); + }); - test('should return false when left does not match the regex right', () => { - const operator = new StringOperator(StringOperation.matchesRegex); - expect(operator.evaluate('hello world', '^world')).toBe(false); - }); + test('should return false when left does not match the regex right', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(operator.evaluate('hello world', '^world')).toBe(false); }); + }); - describe('Edge Case Tests', () => { - test('should return false for contains with empty right string', () => { - const operator = new StringOperator(StringOperation.contains); - expect(operator.evaluate('hello world', '')).toBe(true); // Empty string is included in any string - }); + describe('Edge Case Tests', () => { + test('should return false for contains with empty right string', () => { + const operator = new StringOperator(StringOperation.contains); + expect(operator.evaluate('hello world', '')).toBe(true); // Empty string is included in any string + }); - test('should return true for startsWith with empty right string', () => { - const operator = new StringOperator(StringOperation.startsWith); - expect(operator.evaluate('hello world', '')).toBe(true); // Any string starts with an empty string - }); + test('should return true for startsWith with empty right string', () => { + const operator = new StringOperator(StringOperation.startsWith); + expect(operator.evaluate('hello world', '')).toBe(true); // Any string starts with an empty string + }); - test('should return true for endsWith with empty right string', () => { - const operator = new StringOperator(StringOperation.endsWith); - expect(operator.evaluate('hello world', '')).toBe(true); // Any string ends with an empty string - }); + test('should return true for endsWith with empty right string', () => { + const operator = new StringOperator(StringOperation.endsWith); + expect(operator.evaluate('hello world', '')).toBe(true); // Any string ends with an empty string + }); - test('should throw error for invalid regex', () => { - const operator = new StringOperator(StringOperation.matchesRegex); - expect(() => operator.evaluate('hello world', '[')).toThrow(); - }); + test('should throw error for invalid regex', () => { + const operator = new StringOperator(StringOperation.matchesRegex); + expect(() => operator.evaluate('hello world', '[')).toThrow(); + }); - test('should throw error for unsupported operator', () => { - const operator = new StringOperator('unsupported' as StringOperation); - expect(() => operator.evaluate('test', 'test')).toThrow( - 'Unsupported string operator: unsupported' - ); - }); + test('should throw error for unsupported operator', () => { + const operator = new StringOperator('unsupported' as StringOperation); + expect(() => operator.evaluate('test', 'test')).toThrow('Unsupported string operator: unsupported'); }); + }); }); From 8bc3b3999125795efdb730d5a723ac37d81c539d Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Thu, 24 Oct 2024 23:15:34 +0300 Subject: [PATCH 03/48] Add support for data variables in data conditions --- .../model/conditional_variables/Condition.ts | 103 ++++++++++++++++++ .../conditional_variables/DataCondition.ts | 47 +++++++- .../LogicalGroupStatement.ts | 7 +- .../evaluateCondition.ts | 53 --------- packages/core/src/data_sources/model/utils.ts | 14 +++ 5 files changed, 163 insertions(+), 61 deletions(-) create mode 100644 packages/core/src/data_sources/model/conditional_variables/Condition.ts delete mode 100644 packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts create mode 100644 packages/core/src/data_sources/model/utils.ts diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/Condition.ts new file mode 100644 index 0000000000..416d2a81e4 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/Condition.ts @@ -0,0 +1,103 @@ +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; + + constructor(condition: Expression | LogicGroup | boolean) { + this.condition = condition; + } + + 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); + return logicalGroup.evaluate(); + } + + if (this.isExpression(condition)) { + const { left, operator, right } = condition; + const op = this.getOperator(left, operator); + + const evaluateLeft = evaluateVariable(left); + const evaluateRight = evaluateVariable(right); + + return op.evaluate(evaluateLeft, evaluateRight); + } + + 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); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index fbcf891969..1bdf9e9b5f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -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; @@ -19,32 +23,63 @@ export type LogicGroup = { export class DataCondition extends Model { private conditionResult: boolean; + private condition: Condition; + em: EditorModel | undefined; 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.conditionResult = this.evaluate(); + this.condition = new Condition(condition); + this.em = opts.em; + 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) : evaluateVariable(this.ifFalse); } reevaluate(): void { this.conditionResult = this.evaluate(); } + + toJSON() { + return { + condition: this.condition, + ifTrue: this.ifTrue, + ifFalse: this.ifFalse, + }; + } + + private listenToDataVariables() { + if (!this.em) return; + + 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, {}); + new DynamicVariableListenerManager({ + model: this, + em: this.em!, + dataVariable: variableInstance, + updateValueFromDataVariable: this.reevaluate.bind(this), + }); + }); + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts index 4d4796c0df..0daa140e0e 100644 --- a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts @@ -1,6 +1,6 @@ import { LogicalOperator } from './operators/LogicalOperator'; import { Expression, LogicGroup } from './DataCondition'; -import { evaluateCondition } from './evaluateCondition'; +import { Condition } from './Condition'; export class LogicalGroupStatement { constructor( @@ -9,7 +9,10 @@ export class LogicalGroupStatement { ) {} evaluate(): boolean { - const results = this.statements.map((statement) => evaluateCondition(statement)); + const results = this.statements.map((statement) => { + const condition = new Condition(statement); + return condition.evaluate(); + }); return this.operator.evaluate(results); } } diff --git a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts deleted file mode 100644 index ee5dee77cf..0000000000 --- a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ConditionStatement } from './ConditionStatement'; -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 { NumberOperation, NumberOperator } from './operators/NumberOperator'; -import { StringOperation, StringOperator } from './operators/StringOperations'; - -export function evaluateCondition(condition: any): boolean { - if (typeof condition === 'boolean') { - return condition; - } - - if (isLogicGroup(condition)) { - const { logicalOperator, statements } = condition; - const op = new LogicalOperator(logicalOperator); - const logicalGroup = new LogicalGroupStatement(op, statements); - return logicalGroup.evaluate(); - } - - if (isCondition(condition)) { - const { left, operator, right } = condition; - const op = operatorFactory(left, operator); - const statement = new ConditionStatement(left, op, right); - return statement.evaluate(); - } - - throw new Error('Invalid condition type.'); -} - -function operatorFactory(left: any, operator: string): Operator { - if (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}`); -} - -function isOperatorInEnum(operator: string, enumObject: any): boolean { - return Object.values(enumObject).includes(operator); -} - -function isLogicGroup(condition: any): condition is LogicGroup { - return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); -} - -function isCondition(condition: any): condition is Expression { - return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; -} diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts new file mode 100644 index 0000000000..695e0766ea --- /dev/null +++ b/packages/core/src/data_sources/model/utils.ts @@ -0,0 +1,14 @@ +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) { + return isDataVariable(variable) ? new DataVariable(variable, {}).getDataValue() : variable; +} From 40d2e078d104cdae54faf2237d1b88deff94db33 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Fri, 25 Oct 2024 09:12:12 +0300 Subject: [PATCH 04/48] Add data variables support to data conditions --- .../model/conditional_variables/Condition.ts | 17 +- .../conditional_variables/DataCondition.ts | 10 +- .../LogicalGroupStatement.ts | 10 +- packages/core/src/data_sources/model/utils.ts | 5 +- .../conditional_variables/DataCondition.ts | 167 ++++++++++++++++-- 5 files changed, 179 insertions(+), 30 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/Condition.ts index 416d2a81e4..4586f804b2 100644 --- a/packages/core/src/data_sources/model/conditional_variables/Condition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/Condition.ts @@ -1,3 +1,4 @@ +import EditorModel from '../../../editor/model/Editor'; import DataVariable from '../DataVariable'; import { evaluateVariable, isDataVariable } from '../utils'; import { Expression, LogicGroup } from './DataCondition'; @@ -10,9 +11,11 @@ import { StringOperator, StringOperation } from './operators/StringOperations'; export class Condition { private condition: Expression | LogicGroup | boolean; + private em: EditorModel; - constructor(condition: Expression | LogicGroup | boolean) { + constructor(condition: Expression | LogicGroup | boolean, opts: { em: EditorModel }) { this.condition = condition; + this.em = opts.em; } evaluate(): boolean { @@ -28,18 +31,18 @@ export class Condition { if (this.isLogicGroup(condition)) { const { logicalOperator, statements } = condition; const operator = new LogicalOperator(logicalOperator); - const logicalGroup = new LogicalGroupStatement(operator, statements); + const logicalGroup = new LogicalGroupStatement(operator, statements, { em: this.em }); return logicalGroup.evaluate(); } if (this.isExpression(condition)) { const { left, operator, right } = condition; - const op = this.getOperator(left, operator); + const evaluateLeft = evaluateVariable(left, this.em); + const evaluateRight = evaluateVariable(right, this.em); + const op = this.getOperator(evaluateLeft, operator); - const evaluateLeft = evaluateVariable(left); - const evaluateRight = evaluateVariable(right); - - return op.evaluate(evaluateLeft, evaluateRight); + const evaluated = op.evaluate(evaluateLeft, evaluateRight); + return evaluated; } throw new Error('Invalid condition type.'); diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 1bdf9e9b5f..db8d06464f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -24,7 +24,7 @@ export type LogicGroup = { export class DataCondition extends Model { private conditionResult: boolean; private condition: Condition; - em: EditorModel | undefined; + private em: EditorModel; defaults() { return { @@ -37,12 +37,12 @@ export class DataCondition extends Model { condition: Expression | LogicGroup | boolean, private ifTrue: any, private ifFalse: any, - opts: { em?: EditorModel } = {}, + opts: { em: EditorModel }, ) { super(); - this.conditionResult = this.evaluate(); - this.condition = new Condition(condition); + this.condition = new Condition(condition, { em: opts.em }); this.em = opts.em; + this.conditionResult = this.evaluate(); this.listenToDataVariables(); } @@ -51,7 +51,7 @@ export class DataCondition extends Model { } getDataValue(): any { - return this.conditionResult ? evaluateVariable(this.ifTrue) : evaluateVariable(this.ifFalse); + return this.conditionResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em); } reevaluate(): void { diff --git a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts index 0daa140e0e..cba3764955 100644 --- a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts @@ -1,16 +1,22 @@ import { LogicalOperator } from './operators/LogicalOperator'; import { Expression, LogicGroup } from './DataCondition'; 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) => { - const condition = new Condition(statement); + const condition = new Condition(statement, { em: this.em }); return condition.evaluate(); }); return this.operator.evaluate(results); diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index 695e0766ea..44aaf738b5 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -1,3 +1,4 @@ +import EditorModel from '../../editor/model/Editor'; import { DataConditionType } from './conditional_variables/DataCondition'; import DataVariable, { DataVariableType } from './DataVariable'; @@ -9,6 +10,6 @@ export function isDataCondition(variable: any) { return variable?.type === DataConditionType; } -export function evaluateVariable(variable: any) { - return isDataVariable(variable) ? new DataVariable(variable, {}).getDataValue() : variable; +export function evaluateVariable(variable: any, em: EditorModel) { + return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; } diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts index c3f7da595b..a5313aad0d 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts @@ -1,3 +1,4 @@ +import { DataSourceManager } from '../../../../../src'; import { DataCondition, Expression, @@ -7,19 +8,43 @@ import { GenericOperation } from '../../../../../src/data_sources/model/conditio import { LogicalOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; +import { DataVariableType } from "../../../../../src/data_sources/model/DataVariable"; +import { DataSourceProps } from "../../../../../src/data_sources/types"; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { setupTestEditor } from '../../../../common'; describe('DataCondition', () => { + let em: EditorModel; + let dsm: DataSourceManager; + const dataSource: DataSourceProps = { + id: 'USER_STATUS_SOURCE', + records: [ + { id: 'USER_1', age: 25, status: 'active', }, + { id: 'USER_2', age: 12, status: 'inactive' }, + ], + }; + + beforeEach(() => { + ({ em, dsm } = setupTestEditor()); + dsm.add(dataSource); + }); + + afterEach(() => { + dsm.clear(); + em.destroy(); + }); + describe('Basic Functionality Tests', () => { test('should evaluate a simple boolean condition', () => { const condition = true; - const dataCondition = new DataCondition(condition, 'Yes', 'No'); + const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); expect(dataCondition.getDataValue()).toBe('Yes'); }); test('should return ifFalse when condition evaluates to false', () => { const condition = false; - const dataCondition = new DataCondition(condition, 'Yes', 'No'); + const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); expect(dataCondition.getDataValue()).toBe('No'); }); @@ -28,7 +53,7 @@ describe('DataCondition', () => { describe('Operator Tests', () => { test('should evaluate using GenericOperation operators', () => { const condition: Expression = { left: 5, operator: GenericOperation.equals, right: 5 }; - const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal'); + const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal', { em }); expect(dataCondition.getDataValue()).toBe('Equal'); }); @@ -39,20 +64,20 @@ describe('DataCondition', () => { operator: GenericOperation.equals, right: 'world', }; - const dataCondition = new DataCondition(condition, 'true', 'false'); + const dataCondition = new DataCondition(condition, 'true', 'false', { em }); expect(dataCondition.evaluate()).toBe(false); }); test('should evaluate using StringOperation operators', () => { const condition: Expression = { left: 'apple', operator: StringOperation.contains, right: 'app' }; - const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain"); + const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain", { em }); expect(dataCondition.getDataValue()).toBe('Contains'); }); test('should evaluate using NumberOperation operators', () => { const condition: Expression = { left: 10, operator: NumberOperation.lessThan, right: 15 }; - const dataCondition = new DataCondition(condition, 'Valid', 'Invalid'); + const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em }); expect(dataCondition.getDataValue()).toBe('Valid'); }); @@ -66,7 +91,7 @@ describe('DataCondition', () => { ], }; - const dataCondition = new DataCondition(logicGroup, 'Pass', 'Fail'); + const dataCondition = new DataCondition(logicGroup, 'Pass', 'Fail', { em }); expect(dataCondition.getDataValue()).toBe('Pass'); }); }); @@ -74,7 +99,7 @@ describe('DataCondition', () => { describe('Edge Case Tests', () => { test('should throw error for invalid condition type', () => { const invalidCondition: any = { randomField: 'randomValue' }; - expect(() => new DataCondition(invalidCondition, 'Yes', 'No')).toThrow('Invalid condition type.'); + expect(() => new DataCondition(invalidCondition, 'Yes', 'No', { em })).toThrow('Invalid condition type.'); }); test('should evaluate complex nested conditions', () => { @@ -92,7 +117,7 @@ describe('DataCondition', () => { ], }; - const dataCondition = new DataCondition(nestedLogicGroup, 'Nested Pass', 'Nested Fail'); + const dataCondition = new DataCondition(nestedLogicGroup, 'Nested Pass', 'Nested Fail', { em }); expect(dataCondition.getDataValue()).toBe('Nested Pass'); }); }); @@ -107,7 +132,7 @@ describe('DataCondition', () => { ], }; - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); expect(dataCondition.getDataValue()).toBe('All true'); }); @@ -120,7 +145,7 @@ describe('DataCondition', () => { ], }; - const dataCondition = new DataCondition(logicGroup, 'At least one true', 'All false'); + const dataCondition = new DataCondition(logicGroup, 'At least one true', 'All false', { em }); expect(dataCondition.getDataValue()).toBe('At least one true'); }); @@ -134,7 +159,7 @@ describe('DataCondition', () => { ], }; - const dataCondition = new DataCondition(logicGroup, 'Exactly one true', 'Multiple true or all false'); + const dataCondition = new DataCondition(logicGroup, 'Exactly one true', 'Multiple true or all false', { em }); expect(dataCondition.getDataValue()).toBe('Exactly one true'); }); @@ -153,7 +178,7 @@ describe('DataCondition', () => { ], }; - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); expect(dataCondition.getDataValue()).toBe('All true'); }); @@ -167,8 +192,122 @@ describe('DataCondition', () => { ], }; - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false'); + const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); expect(dataCondition.getDataValue()).toBe('One or more false'); }); }); + + describe('Conditions with dataVariables', () => { + test('should return "Yes" when dataVariable matches expected value', () => { + const condition: Expression = { + left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, + operator: GenericOperation.equals, + right: 'active', + }; + + const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); + expect(dataCondition.getDataValue()).toBe('Yes'); + }); + + test('should return "No" when dataVariable does not match expected value', () => { + const condition: Expression = { + left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, + operator: GenericOperation.equals, + right: 'inactive', + }; + + const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); + expect(dataCondition.getDataValue()).toBe('No'); + }); + + // TODO: unskip after adding UndefinedOperator + test.skip('should handle missing data variable gracefully', () => { + const condition: Expression = { + left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.not_a_user.status' }, + operator: GenericOperation.isDefined, + right: undefined, + }; + + const dataCondition = new DataCondition(condition, 'Found', 'Not Found', { em }); + expect(dataCondition.getDataValue()).toBe('Not Found'); + }); + + test('should correctly compare numeric values from dataVariables', () => { + const condition: Expression = { + left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.age' }, + operator: NumberOperation.greaterThan, + right: 24, + }; + const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em }); + expect(dataCondition.getDataValue()).toBe('Valid'); + }); + + test('should evaluate logical operators with multiple data sources', () => { + const dataSource2: DataSourceProps = { + id: 'SECOND_DATASOURCE_ID', + records: [{ id: 'RECORD_2', status: 'active', age: 22 }], + }; + dsm.add(dataSource2); + + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.and, + statements: [ + { + left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, + operator: GenericOperation.equals, + right: 'active', + }, + { + left: { type: DataVariableType, path: 'SECOND_DATASOURCE_ID.RECORD_2.age' }, + operator: NumberOperation.greaterThan, + right: 18, + }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'All conditions met', 'Some conditions failed', { em }); + expect(dataCondition.getDataValue()).toBe('All conditions met'); + }); + + test('should handle nested logical conditions with data variables', () => { + const logicGroup: LogicGroup = { + logicalOperator: LogicalOperation.or, + statements: [ + { + logicalOperator: LogicalOperation.and, + statements: [ + { + left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_2.status' }, + operator: GenericOperation.equals, + right: 'inactive', + }, + { + left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_2.age' }, + operator: NumberOperation.lessThan, + right: 14, + }, + ], + }, + { + left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, + operator: GenericOperation.equals, + right: 'inactive', + }, + ], + }; + + const dataCondition = new DataCondition(logicGroup, 'Condition met', 'Condition failed', { em }); + expect(dataCondition.getDataValue()).toBe('Condition met'); + }); + + test('should handle data variables as an ifTrue return value', () => { + const dataCondition = new DataCondition(true, { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, 'No', { em }); + expect(dataCondition.getDataValue()).toBe('active'); + }); + + test('should handle data variables as an ifFalse return value', () => { + const dataCondition = new DataCondition(false, 'Yes', { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, { em }); + expect(dataCondition.getDataValue()).toBe('active'); + }); + }); }); From 04f7eade82b33bd097c06b8336dc71157437e062 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Fri, 25 Oct 2024 10:26:42 +0300 Subject: [PATCH 05/48] fix datacondition tests --- .../conditional_variables/DataCondition.ts | 4 ++- .../conditional_variables/DataCondition.ts | 26 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index db8d06464f..9cf5f1c08e 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -72,8 +72,10 @@ export class DataCondition extends Model { const dataVariables = this.condition.getDataVariables(); if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); + + // TODO avoid memory leaks dataVariables.forEach((variable) => { - const variableInstance = new DataVariable(variable, {}); + const variableInstance = new DataVariable(variable, { em: this.em }); new DynamicVariableListenerManager({ model: this, em: this.em!, diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts index a5313aad0d..59ac056040 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts @@ -8,10 +8,10 @@ import { GenericOperation } from '../../../../../src/data_sources/model/conditio import { LogicalOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; -import { DataVariableType } from "../../../../../src/data_sources/model/DataVariable"; -import { DataSourceProps } from "../../../../../src/data_sources/types"; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { DataSourceProps } from '../../../../../src/data_sources/types'; +import Editor from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor'; -import { setupTestEditor } from '../../../../common'; describe('DataCondition', () => { let em: EditorModel; @@ -19,18 +19,18 @@ describe('DataCondition', () => { const dataSource: DataSourceProps = { id: 'USER_STATUS_SOURCE', records: [ - { id: 'USER_1', age: 25, status: 'active', }, + { id: 'USER_1', age: 25, status: 'active' }, { id: 'USER_2', age: 12, status: 'inactive' }, ], }; beforeEach(() => { - ({ em, dsm } = setupTestEditor()); + em = new Editor(); + dsm = em.DataSources; dsm.add(dataSource); }); afterEach(() => { - dsm.clear(); em.destroy(); }); @@ -301,12 +301,22 @@ describe('DataCondition', () => { }); test('should handle data variables as an ifTrue return value', () => { - const dataCondition = new DataCondition(true, { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, 'No', { em }); + const dataCondition = new DataCondition( + true, + { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, + 'No', + { em }, + ); expect(dataCondition.getDataValue()).toBe('active'); }); test('should handle data variables as an ifFalse return value', () => { - const dataCondition = new DataCondition(false, 'Yes', { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, { em }); + const dataCondition = new DataCondition( + false, + 'Yes', + { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, + { em }, + ); expect(dataCondition.getDataValue()).toBe('active'); }); }); From 7927b1ea515565560b63a0ed302cf4ef95ffe974 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Fri, 25 Oct 2024 10:36:13 +0300 Subject: [PATCH 06/48] Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into conditional-variables --- .github/workflows/publish-docs.yml | 53 +++++++++++++++++++ docs/deploy.sh | 28 ---------- docs/package.json | 5 +- package.json | 1 + .../evaluateCondition.ts | 53 +++++++++++++++++++ scripts/releaseDocs.ts | 32 +++++++++++ 6 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/publish-docs.yml delete mode 100755 docs/deploy.sh create mode 100644 packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts create mode 100644 scripts/releaseDocs.ts diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 0000000000..fa5c24b996 --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -0,0 +1,53 @@ +name: Publish GrapesJS Docs + +on: + push: + branches: [dev] + paths: + - 'docs/**' + +jobs: + publish-docs: + runs-on: ubuntu-latest + if: "startsWith(github.event.head_commit.message, 'Release GrapesJS docs:')" + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-project + - name: Setup Git + run: | + git config --global user.name 'GrapesJSBot' + git config --global user.email 'services@grapesjs.com' + - name: Build and Deploy Docs + env: + GRAPESJS_BOT_TOKEN: ${{ secrets.GRAPESJS_BOT_TOKEN }} + working-directory: ./docs + run: | + # abort on errors + set -e + # navigate into the build output directory + cd .vuepress/dist + + # Need to deploy all the documentation inside docs folder + mkdir docs-new + + # move all the files from the current directory in docs + mv `ls -1 ./ | grep -v docs-new` ./docs-new + + # fetch the current site, remove the old docs dir and make current the new one + git clone -b main https://github.com/GrapesJS/website.git tmp + mv tmp/[^.]* . # Move all non-hidden files + mv tmp/.[^.]* . 2>/dev/null || true # Move hidden files, ignore errors if none exist + rm -rf tmp + rm -fR public/docs + mv ./docs-new ./public/docs + + # stage all and commit + git add -A + git commit -m 'deploy docs' + + # Push using PAT + git push https://$GRAPESJS_BOT_TOKEN@github.com/GrapesJS/website.git main + + cd - diff --git a/docs/deploy.sh b/docs/deploy.sh deleted file mode 100755 index 2720c49e30..0000000000 --- a/docs/deploy.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env sh - -# abort on errors -set -e - -# build -# npm run docs:build - -# navigate into the build output directory -cd docs/.vuepress/dist - -# Need to deploy all the documentation inside docs folder -mkdir docs-new - -# move all the files from the current directory in docs -mv `\ls -1 ./ | grep -v docs-new` ./docs-new - -# fetch the current site, remove the old docs dir and make current the new one -git clone -b main https://github.com/GrapesJS/website.git tmp && mv tmp/* tmp/.* . && rm -rf tmp -rm -fR public/docs -mv ./docs-new ./public/docs - -# stage all and commit -git add -A -git commit -m 'deploy docs' -git push https://artf@github.com/GrapesJS/website.git main - -cd - diff --git a/docs/package.json b/docs/package.json index a8dba0314d..cae4e693de 100644 --- a/docs/package.json +++ b/docs/package.json @@ -2,7 +2,7 @@ "name": "@grapesjs/docs", "private": true, "description": "Free and Open Source Web Builder Framework", - "version": "0.21.13", + "version": "0.21.14", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", "files": [ @@ -34,7 +34,6 @@ "scripts": { "docs": "vuepress dev .", "docs:api": "node ./api.mjs", - "build": "npm run docs:api && vuepress build .", - "docs:deploy": "./deploy.sh" + "build": "npm run docs:api && vuepress build ." } } diff --git a/package.json b/package.json index 10489dea05..9d1357a7d4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "publish:core:rc": "cd packages/core && npm publish --tag rc --access public", "publish:core:latest": "cd packages/core && npm publish --access public", "build:core": "pnpm --filter grapesjs build", + "release:docs": "ts-node scripts/releaseDocs latest", "build:cli": "pnpm --filter grapesjs-cli build", "build:docs:api": "pnpm --filter @grapesjs/docs docs:api", "build:docs": "pnpm --filter @grapesjs/docs build" diff --git a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts new file mode 100644 index 0000000000..ee5dee77cf --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts @@ -0,0 +1,53 @@ +import { ConditionStatement } from './ConditionStatement'; +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 { NumberOperation, NumberOperator } from './operators/NumberOperator'; +import { StringOperation, StringOperator } from './operators/StringOperations'; + +export function evaluateCondition(condition: any): boolean { + if (typeof condition === 'boolean') { + return condition; + } + + if (isLogicGroup(condition)) { + const { logicalOperator, statements } = condition; + const op = new LogicalOperator(logicalOperator); + const logicalGroup = new LogicalGroupStatement(op, statements); + return logicalGroup.evaluate(); + } + + if (isCondition(condition)) { + const { left, operator, right } = condition; + const op = operatorFactory(left, operator); + const statement = new ConditionStatement(left, op, right); + return statement.evaluate(); + } + + throw new Error('Invalid condition type.'); +} + +function operatorFactory(left: any, operator: string): Operator { + if (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}`); +} + +function isOperatorInEnum(operator: string, enumObject: any): boolean { + return Object.values(enumObject).includes(operator); +} + +function isLogicGroup(condition: any): condition is LogicGroup { + return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); +} + +function isCondition(condition: any): condition is Expression { + return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; +} diff --git a/scripts/releaseDocs.ts b/scripts/releaseDocs.ts new file mode 100644 index 0000000000..9bb3fea348 --- /dev/null +++ b/scripts/releaseDocs.ts @@ -0,0 +1,32 @@ +import fs from 'fs'; +import { resolve } from 'path'; +import { runCommand } from './common'; + +const pathLib = resolve(__dirname, '../docs'); + +async function prepareCoreRelease() { + try { + // Check if the current branch is clean (no staged changes) + runCommand( + 'git diff-index --quiet HEAD --', + 'You have uncommitted changes. Please commit or stash them before running the release script.', + ); + + // Increment the docs version + runCommand(`pnpm --filter @grapesjs/docs exec npm version patch --no-git-tag-version --no-commit-hooks`); + + // Create a new release branch + const newVersion = JSON.parse(fs.readFileSync(`${pathLib}/package.json`, 'utf8')).version; + const newBranch = `release-docs-v${newVersion}`; + runCommand(`git checkout -b ${newBranch}`); + runCommand('git add .'); + runCommand(`git commit -m "Release GrapesJS docs: v${newVersion}"`); + + console.log(`Release prepared! Push the current "${newBranch}" branch and open a new PR targeting 'dev'`); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +prepareCoreRelease(); From cab64de7a5d24d7323282014c38e9a52d3bd3de8 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Fri, 25 Oct 2024 10:47:05 +0300 Subject: [PATCH 07/48] Avoid memory leak in data conditions --- .../model/DataVariableListenerManager.ts | 4 ++++ .../model/conditional_variables/DataCondition.ts | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index e2941b523b..45a5b1e8fa 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -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(); + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 9cf5f1c08e..745caae342 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -25,6 +25,7 @@ export class DataCondition extends Model { private conditionResult: boolean; private condition: Condition; private em: EditorModel; + private variableListeners: DynamicVariableListenerManager[] = []; defaults() { return { @@ -69,19 +70,28 @@ export class DataCondition extends Model { 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); - // TODO avoid memory leaks dataVariables.forEach((variable) => { const variableInstance = new DataVariable(variable, { em: this.em }); - new DynamicVariableListenerManager({ - model: this, + 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 = []; + } } From 7f67370cf882e469e40aa47d3a8a088c3dfe841f Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 28 Oct 2024 17:12:00 +0300 Subject: [PATCH 08/48] Add support for conditional variables in traits --- .../model/DataVariableListenerManager.ts | 10 ++-- .../conditional_variables/DataCondition.ts | 8 --- .../evaluateCondition.ts | 53 ------------------- packages/core/src/data_sources/types.ts | 4 ++ .../src/dom_components/model/Component.ts | 6 +-- .../core/src/trait_manager/model/Trait.ts | 7 ++- 6 files changed, 18 insertions(+), 70 deletions(-) delete mode 100644 packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index 45a5b1e8fa..79ca0b63f5 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -2,14 +2,14 @@ import { DataSourcesEvents, DataVariableListener } from '../types'; import { stringToPath } from '../../utils/mixins'; import { Model } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import DataVariable, { DataVariableType } from './DataVariable'; +import { DataVariableType } from './DataVariable'; import ComponentView from '../../dom_components/view/ComponentView'; -import ComponentDataVariable from './ComponentDataVariable'; +import { DynamicValue } from '../types'; export interface DynamicVariableListenerManagerOptions { model: Model | ComponentView; em: EditorModel; - dataVariable: DataVariable | ComponentDataVariable; + dataVariable: DynamicValue; updateValueFromDataVariable: (value: any) => void; } @@ -17,7 +17,7 @@ export default class DynamicVariableListenerManager { private dataListeners: DataVariableListener[] = []; private em: EditorModel; private model: Model | ComponentView; - private dynamicVariable: DataVariable | ComponentDataVariable; + private dynamicVariable: DynamicValue; private updateValueFromDynamicVariable: (value: any) => void; constructor(options: DynamicVariableListenerManagerOptions) { @@ -50,7 +50,7 @@ export default class DynamicVariableListenerManager { this.dataListeners = dataListeners; } - private listenToDataVariable(dataVariable: DataVariable | ComponentDataVariable, em: EditorModel) { + private listenToDataVariable(dataVariable: DynamicValue, em: EditorModel) { const dataListeners: DataVariableListener[] = []; const { path } = dataVariable.attributes; const normPath = stringToPath(path || '').join('.'); diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 745caae342..aab5652af5 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -59,14 +59,6 @@ export class DataCondition extends Model { this.conditionResult = this.evaluate(); } - toJSON() { - return { - condition: this.condition, - ifTrue: this.ifTrue, - ifFalse: this.ifFalse, - }; - } - private listenToDataVariables() { if (!this.em) return; diff --git a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts b/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts deleted file mode 100644 index ee5dee77cf..0000000000 --- a/packages/core/src/data_sources/model/conditional_variables/evaluateCondition.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ConditionStatement } from './ConditionStatement'; -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 { NumberOperation, NumberOperator } from './operators/NumberOperator'; -import { StringOperation, StringOperator } from './operators/StringOperations'; - -export function evaluateCondition(condition: any): boolean { - if (typeof condition === 'boolean') { - return condition; - } - - if (isLogicGroup(condition)) { - const { logicalOperator, statements } = condition; - const op = new LogicalOperator(logicalOperator); - const logicalGroup = new LogicalGroupStatement(op, statements); - return logicalGroup.evaluate(); - } - - if (isCondition(condition)) { - const { left, operator, right } = condition; - const op = operatorFactory(left, operator); - const statement = new ConditionStatement(left, op, right); - return statement.evaluate(); - } - - throw new Error('Invalid condition type.'); -} - -function operatorFactory(left: any, operator: string): Operator { - if (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}`); -} - -function isOperatorInEnum(operator: string, enumObject: any): boolean { - return Object.values(enumObject).includes(operator); -} - -function isLogicGroup(condition: any): condition is LogicGroup { - return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); -} - -function isCondition(condition: any): condition is Expression { - return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; -} diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index 3b23326e64..e230e418e2 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -1,7 +1,11 @@ import { ObjectAny } from '../common'; +import ComponentDataVariable from './model/ComponentDataVariable'; import DataRecord from './model/DataRecord'; import DataRecords from './model/DataRecords'; +import DataVariable from './model/DataVariable'; +import { DataCondition } from './model/conditional_variables/DataCondition'; +export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition; export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 7abb1af35b..55afeeac88 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -915,7 +915,7 @@ export default class Component extends StyleableModel { this.off(event, this.initTraits); this.__loadTraits(); const attrs = { ...this.get('attributes') }; - const traitDataVariableAttr: ObjectAny = {}; + const traitDynamicValueAttr: ObjectAny = {}; const traits = this.traits; traits.each((trait) => { const name = trait.getName(); @@ -928,11 +928,11 @@ export default class Component extends StyleableModel { } if (trait.dynamicVariable) { - traitDataVariableAttr[name] = trait.dynamicVariable; + traitDynamicValueAttr[name] = trait.dynamicVariable; } }); traits.length && this.set('attributes', attrs); - Object.keys(traitDataVariableAttr).length && this.set('attributes-data-variable', traitDataVariableAttr); + Object.keys(traitDynamicValueAttr).length && this.set('attributes-data-variable', traitDynamicValueAttr); this.on(event, this.initTraits); changed && em && em.trigger('component:toggled'); return this; diff --git a/packages/core/src/trait_manager/model/Trait.ts b/packages/core/src/trait_manager/model/Trait.ts index 17dd6c7732..8338dbbed4 100644 --- a/packages/core/src/trait_manager/model/Trait.ts +++ b/packages/core/src/trait_manager/model/Trait.ts @@ -1,3 +1,4 @@ +import { DataConditionType, DataCondition } from './../../data_sources/model/conditional_variables/DataCondition'; import { isString, isUndefined } from 'underscore'; import Category from '../../abstract/ModuleCategory'; import { LocaleOptions, Model, SetOptions } from '../../common'; @@ -29,7 +30,7 @@ export default class Trait extends Model { em: EditorModel; view?: TraitView; el?: HTMLElement; - dynamicVariable?: TraitDataVariable; + dynamicVariable?: TraitDataVariable | DataCondition; dynamicVariableListener?: DynamicVariableListenerManager; defaults() { @@ -63,6 +64,10 @@ export default class Trait extends Model { case DataVariableType: this.dynamicVariable = new TraitDataVariable(this.attributes.value, { em: this.em, trait: this }); break; + case DataConditionType: + const { condition, ifTrue, ifFalse } = this.attributes.value; + this.dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em }); + break; default: throw new Error(`Invalid data variable type. Expected '${DataVariableType}', but found '${dataType}'.`); } From b213fc59c4ca0d9a060dabeb7218c0de916deb75 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 28 Oct 2024 17:33:00 +0300 Subject: [PATCH 09/48] Add conditional value support for styles --- .../domain_abstract/model/StyleableModel.ts | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index e325d8751c..e11d447333 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -10,6 +10,7 @@ import DynamicVariableListenerManager from '../../data_sources/model/DataVariabl import CssRuleView from '../../css_composer/view/CssRuleView'; import ComponentView from '../../dom_components/view/ComponentView'; import Frame from '../../canvas/model/Frame'; +import { DataCondition, DataConditionType } from '../../data_sources/model/conditional_variables/DataCondition'; export type StyleProps = Record< string, @@ -113,16 +114,8 @@ export default class StyleableModel extends Model } const styleValue = newStyle[key]; - if (typeof styleValue === 'object' && styleValue.type === DataVariableType) { - const dynamicType = styleValue.type; - let styleDynamicVariable; - switch (dynamicType) { - case DataVariableType: - styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em }); - break; - default: - throw new Error(`Invalid data variable type. Expected '${DataVariableType}', but found '${dynamicType}'.`); - } + if (this.isDynamicValue(styleValue)) { + const styleDynamicVariable = this.resolveDynamicValue(styleValue); newStyle[key] = styleDynamicVariable; this.manageDataVariableListener(styleDynamicVariable, key); } @@ -150,10 +143,34 @@ export default class StyleableModel extends Model return newStyle; } + private isDynamicValue(styleValue: any) { + return typeof styleValue === 'object' && [DataVariableType, DataConditionType].includes(styleValue.type); + } + + private resolveDynamicValue(styleValue: any) { + const dynamicType = styleValue.type; + let styleDynamicVariable; + switch (dynamicType) { + case DataVariableType: + styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em }); + break; + case DataConditionType: + const { condition, ifTrue, ifFalse } = styleValue; + styleDynamicVariable = new DataCondition(true, 'red', ifFalse, { em: this.em! }); + break; + default: + throw new Error( + `Invalid data variable type. Expected '${DataVariableType} or ${DataConditionType}', but found '${dynamicType}'.`, + ); + } + + return styleDynamicVariable; + } + /** * Manage DataVariableListenerManager for a style property */ - manageDataVariableListener(dataVar: StyleDataVariable, styleProp: string) { + manageDataVariableListener(dataVar: StyleDataVariable | DataCondition, styleProp: string) { if (this.dynamicVariableListeners[styleProp]) { this.dynamicVariableListeners[styleProp].listenToDynamicVariable(); } else { @@ -187,7 +204,7 @@ export default class StyleableModel extends Model } /** - * Resolve data variables to their actual values + * Resolve dynamic values ( datasource variables - conditional variables ) to their actual values */ resolveDataVariables(style: StyleProps): StyleProps { const resolvedStyle = { ...style }; @@ -198,16 +215,12 @@ export default class StyleableModel extends Model return; } - if ( - typeof styleValue === 'object' && - styleValue.type === DataVariableType && - !(styleValue instanceof StyleDataVariable) - ) { + if (this.isDynamicValue(styleValue)) { const dataVar = new StyleDataVariable(styleValue, { em: this.em }); resolvedStyle[key] = dataVar.getDataValue(); } - if (styleValue instanceof StyleDataVariable) { + if (styleValue instanceof StyleDataVariable || styleValue instanceof DataCondition) { resolvedStyle[key] = styleValue.getDataValue(); } }); From b1f8acb49d5f3bb3e3eeb2059f974efbe6bdd786 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 28 Oct 2024 17:33:08 +0300 Subject: [PATCH 10/48] Fix message for trait --- packages/core/src/trait_manager/model/Trait.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/trait_manager/model/Trait.ts b/packages/core/src/trait_manager/model/Trait.ts index 8338dbbed4..09cfa00144 100644 --- a/packages/core/src/trait_manager/model/Trait.ts +++ b/packages/core/src/trait_manager/model/Trait.ts @@ -69,7 +69,9 @@ export default class Trait extends Model { this.dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em }); break; default: - throw new Error(`Invalid data variable type. Expected '${DataVariableType}', but found '${dataType}'.`); + throw new Error( + `Invalid data variable type. Expected '${DataVariableType} or ${DataConditionType}', but found '${dataType}'.`, + ); } const dv = this.dynamicVariable.getDataValue(); From 589e6e0c567c0f5389dc25ba0d7c5fb13c34c292 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 28 Oct 2024 17:39:06 +0300 Subject: [PATCH 11/48] Fix hard coded values in conditional styles --- packages/core/src/domain_abstract/model/StyleableModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index e11d447333..619d516f57 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -156,7 +156,7 @@ export default class StyleableModel extends Model break; case DataConditionType: const { condition, ifTrue, ifFalse } = styleValue; - styleDynamicVariable = new DataCondition(true, 'red', ifFalse, { em: this.em! }); + styleDynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em! }); break; default: throw new Error( @@ -216,7 +216,7 @@ export default class StyleableModel extends Model } if (this.isDynamicValue(styleValue)) { - const dataVar = new StyleDataVariable(styleValue, { em: this.em }); + const dataVar = this.resolveDynamicValue(styleValue); resolvedStyle[key] = dataVar.getDataValue(); } From b21d0d2a554d624ca09ae899b66f7e3316c206fb Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 28 Oct 2024 17:53:38 +0300 Subject: [PATCH 12/48] Add support for conditional values for component definition --- .../ComponentConditionalVariable.ts | 25 +++++++++++++++++++ packages/core/src/data_sources/types.ts | 3 ++- .../view/ComponentConditionalVariableView.ts | 10 ++++++++ packages/core/src/dom_components/index.ts | 8 ++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts create mode 100644 packages/core/src/data_sources/view/ComponentConditionalVariableView.ts diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts new file mode 100644 index 0000000000..7e1e50a216 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -0,0 +1,25 @@ +import Component from '../../../dom_components/model/Component'; +import { toLowerCase } from '../../../utils/mixins'; +import { evaluateVariable } from '../utils'; +import { Condition } from './Condition'; +import { DataConditionType } from './DataCondition'; + +export default class ComponentConditionalVariable extends Component { + condition?: Condition; + + getDataValue(): any { + const { condition, ifTrue, ifFalse, em } = this.attributes; + this.condition = new Condition(condition, { em }); + return this.condition.evaluate() ? evaluateVariable(ifTrue, em) : evaluateVariable(ifFalse, em); + } + + getInnerHTML() { + const val = this.getDataValue(); + + return val; + } + + static isComponent(el: HTMLElement) { + return toLowerCase(el.tagName) === DataConditionType; + } +} diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index e230e418e2..a1d1ebcbbd 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -3,9 +3,10 @@ import ComponentDataVariable from './model/ComponentDataVariable'; import DataRecord from './model/DataRecord'; import DataRecords from './model/DataRecords'; import DataVariable from './model/DataVariable'; +import ComponentConditionalVariable from './model/conditional_variables/ComponentConditionalVariable'; import { DataCondition } from './model/conditional_variables/DataCondition'; -export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition; +export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition | ComponentConditionalVariable; export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/data_sources/view/ComponentConditionalVariableView.ts b/packages/core/src/data_sources/view/ComponentConditionalVariableView.ts new file mode 100644 index 0000000000..437c5249d8 --- /dev/null +++ b/packages/core/src/data_sources/view/ComponentConditionalVariableView.ts @@ -0,0 +1,10 @@ +import ComponentView from '../../dom_components/view/ComponentView'; +import ComponentConditionalVariable from '../model/conditional_variables/ComponentConditionalVariable'; + +export default class ComponentComponentVariableView extends ComponentView { + postRender() { + const { model, el } = this; + el.innerHTML = model.getDataValue(); + super.postRender(); + } +} diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 008a0380ad..2583901b29 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -125,6 +125,9 @@ import { BlockProperties } from '../block_manager/model/Block'; import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import { DataVariableType } from '../data_sources/model/DataVariable'; +import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition'; +import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ComponentConditionalVariable'; +import ComponentComponentVariableView from '../data_sources/view/ComponentConditionalVariableView'; export type ComponentEvent = | 'component:create' @@ -190,6 +193,11 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ + { + id: DataConditionType, + model: ComponentConditionalVariable, + view: ComponentComponentVariableView, + }, { id: DataVariableType, model: ComponentDataVariable, From 8f2a75656e5cbe817dff912c83e40477f40662c4 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 28 Oct 2024 17:57:14 +0300 Subject: [PATCH 13/48] Simplify component conditional variable --- .../conditional_variables/ComponentConditionalVariable.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 7e1e50a216..a1e9b6470a 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -5,12 +5,10 @@ import { Condition } from './Condition'; import { DataConditionType } from './DataCondition'; export default class ComponentConditionalVariable extends Component { - condition?: Condition; - getDataValue(): any { - const { condition, ifTrue, ifFalse, em } = this.attributes; - this.condition = new Condition(condition, { em }); - return this.condition.evaluate() ? evaluateVariable(ifTrue, em) : evaluateVariable(ifFalse, em); + const { condition: conditionAttribute, ifTrue, ifFalse, em } = this.attributes; + const condition = new Condition(conditionAttribute, { em }); + return condition.evaluate() ? evaluateVariable(ifTrue, em) : evaluateVariable(ifFalse, em); } getInnerHTML() { From 0bd16e0b019ec6ae766d31c8004c2913047e8ec8 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 28 Oct 2024 18:02:02 +0300 Subject: [PATCH 14/48] Remove redundency --- .../core/src/data_sources/model/ComponentDataVariable.ts | 7 ++----- .../conditional_variables/ComponentConditionalVariable.ts | 4 +--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/core/src/data_sources/model/ComponentDataVariable.ts b/packages/core/src/data_sources/model/ComponentDataVariable.ts index e75d57a60b..ac118f4c19 100644 --- a/packages/core/src/data_sources/model/ComponentDataVariable.ts +++ b/packages/core/src/data_sources/model/ComponentDataVariable.ts @@ -1,5 +1,4 @@ import Component from '../../dom_components/model/Component'; -import { ToHTMLOptions } from '../../dom_components/model/types'; import { toLowerCase } from '../../utils/mixins'; import { DataVariableType } from './DataVariable'; @@ -19,10 +18,8 @@ export default class ComponentDataVariable extends Component { return this.em.DataSources.getValue(path, defaultValue); } - getInnerHTML(opts: ToHTMLOptions) { - const val = this.getDataValue(); - - return val; + getInnerHTML() { + return this.getDataValue(); } static isComponent(el: HTMLElement) { diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index a1e9b6470a..32e13d411f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -12,9 +12,7 @@ export default class ComponentConditionalVariable extends Component { } getInnerHTML() { - const val = this.getDataValue(); - - return val; + return this.getDataValue(); } static isComponent(el: HTMLElement) { From bbcea7b9f9b8f6a9aa732227104f08490f80b074 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 28 Oct 2024 18:23:57 +0300 Subject: [PATCH 15/48] Watch for changes in data variable listed in conditional variables --- .../model/DataVariableListenerManager.ts | 23 ++++++++++++++++--- .../ComponentConditionalVariable.ts | 20 ++++++++++++---- .../conditional_variables/DataCondition.ts | 12 +++++++--- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index 79ca0b63f5..4508beffe3 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -2,9 +2,12 @@ import { DataSourcesEvents, DataVariableListener } from '../types'; import { stringToPath } from '../../utils/mixins'; import { Model } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import { DataVariableType } from './DataVariable'; +import DataVariable, { DataVariableType } from './DataVariable'; import ComponentView from '../../dom_components/view/ComponentView'; import { DynamicValue } from '../types'; +import { DataCondition, DataConditionType } from './conditional_variables/DataCondition'; +import ComponentDataVariable from './ComponentDataVariable'; +import ComponentConditionalVariable from './conditional_variables/ComponentConditionalVariable'; export interface DynamicVariableListenerManagerOptions { model: Model | ComponentView; @@ -42,7 +45,13 @@ export default class DynamicVariableListenerManager { let dataListeners: DataVariableListener[] = []; switch (type) { case DataVariableType: - dataListeners = this.listenToDataVariable(dynamicVariable, em); + dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em); + break; + case DataConditionType: + dataListeners = this.listenToConditionalVariable( + dynamicVariable as DataCondition | ComponentConditionalVariable, + em, + ); break; } dataListeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange)); @@ -50,7 +59,15 @@ export default class DynamicVariableListenerManager { this.dataListeners = dataListeners; } - private listenToDataVariable(dataVariable: DynamicValue, em: EditorModel) { + private listenToConditionalVariable(dataVariable: DataCondition | ComponentConditionalVariable, em: EditorModel) { + const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { + return this.listenToDataVariable(dataVariable, em); + }); + + return dataListeners; + } + + private listenToDataVariable(dataVariable: DataVariable | ComponentDataVariable, em: EditorModel) { const dataListeners: DataVariableListener[] = []; const { path } = dataVariable.attributes; const normPath = stringToPath(path || '').join('.'); diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 32e13d411f..e94adf08e5 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -1,14 +1,24 @@ import Component from '../../../dom_components/model/Component'; import { toLowerCase } from '../../../utils/mixins'; -import { evaluateVariable } from '../utils'; +import { evaluateVariable, isDataVariable } from '../utils'; import { Condition } from './Condition'; -import { DataConditionType } from './DataCondition'; +import { DataCondition, DataConditionType } from './DataCondition'; export default class ComponentConditionalVariable extends Component { getDataValue(): any { - const { condition: conditionAttribute, ifTrue, ifFalse, em } = this.attributes; - const condition = new Condition(conditionAttribute, { em }); - return condition.evaluate() ? evaluateVariable(ifTrue, em) : evaluateVariable(ifFalse, em); + const dataCondtion = this.getDataCondition(); + return dataCondtion.getDataValue(); + } + + getDependentDataVariables() { + const dataCondtion = this.getDataCondition(); + return dataCondtion.getDependentDataVariables(); + } + + private getDataCondition() { + const { condition, ifTrue, ifFalse, em } = this.attributes; + const dataCondtion = new DataCondition(condition, ifTrue, ifFalse, { em }); + return dataCondtion; } getInnerHTML() { diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index aab5652af5..c6f62b877f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -65,9 +65,7 @@ export class DataCondition extends Model { // 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); + const dataVariables = this.getDependentDataVariables(); dataVariables.forEach((variable) => { const variableInstance = new DataVariable(variable, { em: this.em }); @@ -82,6 +80,14 @@ export class DataCondition extends Model { }); } + getDependentDataVariables() { + const dataVariables = this.condition.getDataVariables(); + if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); + if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); + + return dataVariables; + } + private cleanupListeners() { this.variableListeners.forEach((listener) => listener.destroy()); this.variableListeners = []; From b10663deb71291322804ff605fe51e18becef98a Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 06:16:52 +0300 Subject: [PATCH 16/48] Add dynamic view to components with dynamic value --- .../model/DataVariableListenerManager.ts | 7 ++--- .../ComponentConditionalVariable.ts | 27 +++++++------------ packages/core/src/data_sources/types.ts | 3 +-- .../view/ComponentConditionalVariableView.ts | 10 ------- .../data_sources/view/ComponentDynamicView.ts | 15 +++++++++++ packages/core/src/dom_components/index.ts | 4 +-- 6 files changed, 29 insertions(+), 37 deletions(-) delete mode 100644 packages/core/src/data_sources/view/ComponentConditionalVariableView.ts create mode 100644 packages/core/src/data_sources/view/ComponentDynamicView.ts diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index 4508beffe3..fccf349a57 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -48,10 +48,7 @@ export default class DynamicVariableListenerManager { dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em); break; case DataConditionType: - dataListeners = this.listenToConditionalVariable( - dynamicVariable as DataCondition | ComponentConditionalVariable, - em, - ); + dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em); break; } dataListeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange)); @@ -59,7 +56,7 @@ export default class DynamicVariableListenerManager { this.dataListeners = dataListeners; } - private listenToConditionalVariable(dataVariable: DataCondition | ComponentConditionalVariable, em: EditorModel) { + private listenToConditionalVariable(dataVariable: DataCondition, em: EditorModel) { const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { return this.listenToDataVariable(dataVariable, em); }); diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index e94adf08e5..051594208d 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -1,28 +1,19 @@ import Component from '../../../dom_components/model/Component'; +import { ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; -import { evaluateVariable, isDataVariable } from '../utils'; -import { Condition } from './Condition'; import { DataCondition, DataConditionType } from './DataCondition'; export default class ComponentConditionalVariable extends Component { - getDataValue(): any { - const dataCondtion = this.getDataCondition(); - return dataCondtion.getDataValue(); - } - - getDependentDataVariables() { - const dataCondtion = this.getDataCondition(); - return dataCondtion.getDependentDataVariables(); - } + dataCondition: DataCondition; - private getDataCondition() { - const { condition, ifTrue, ifFalse, em } = this.attributes; - const dataCondtion = new DataCondition(condition, ifTrue, ifFalse, { em }); - return dataCondtion; - } + constructor(props: ComponentProperties = {}, opt: ComponentOptions) { + let componentProperties = props; + const { condition, ifTrue, ifFalse } = props; + const dataCondtion = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); + componentProperties = dataCondtion.getDataValue(); - getInnerHTML() { - return this.getDataValue(); + super(componentProperties, opt); + this.dataCondition = dataCondtion; } static isComponent(el: HTMLElement) { diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index a1d1ebcbbd..e230e418e2 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -3,10 +3,9 @@ import ComponentDataVariable from './model/ComponentDataVariable'; import DataRecord from './model/DataRecord'; import DataRecords from './model/DataRecords'; import DataVariable from './model/DataVariable'; -import ComponentConditionalVariable from './model/conditional_variables/ComponentConditionalVariable'; import { DataCondition } from './model/conditional_variables/DataCondition'; -export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition | ComponentConditionalVariable; +export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition; export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/data_sources/view/ComponentConditionalVariableView.ts b/packages/core/src/data_sources/view/ComponentConditionalVariableView.ts deleted file mode 100644 index 437c5249d8..0000000000 --- a/packages/core/src/data_sources/view/ComponentConditionalVariableView.ts +++ /dev/null @@ -1,10 +0,0 @@ -import ComponentView from '../../dom_components/view/ComponentView'; -import ComponentConditionalVariable from '../model/conditional_variables/ComponentConditionalVariable'; - -export default class ComponentComponentVariableView extends ComponentView { - postRender() { - const { model, el } = this; - el.innerHTML = model.getDataValue(); - super.postRender(); - } -} diff --git a/packages/core/src/data_sources/view/ComponentDynamicView.ts b/packages/core/src/data_sources/view/ComponentDynamicView.ts new file mode 100644 index 0000000000..a35732ca01 --- /dev/null +++ b/packages/core/src/data_sources/view/ComponentDynamicView.ts @@ -0,0 +1,15 @@ +import ComponentView from '../../dom_components/view/ComponentView'; +import EditorModel from '../../editor/model/Editor'; +import ComponentConditionalVariable from '../model/conditional_variables/ComponentConditionalVariable'; + +export default class DynamicView extends ComponentView { + initialize(opt: any = {}) { + const model = this.model; + const config = opt.config || {}; + const em: EditorModel = config.em; + const viewId = this.model.dataCondition.getDataValue()?.type || 'default'; + const view = new (em.Components.getType(viewId).view)(opt); + + return view; + } +} diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 2583901b29..698b23af2b 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -127,7 +127,7 @@ import ComponentDataVariableView from '../data_sources/view/ComponentDataVariabl import { DataVariableType } from '../data_sources/model/DataVariable'; import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition'; import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ComponentConditionalVariable'; -import ComponentComponentVariableView from '../data_sources/view/ComponentConditionalVariableView'; +import DynamicView from '../data_sources/view/ComponentDynamicView'; export type ComponentEvent = | 'component:create' @@ -196,7 +196,7 @@ export default class ComponentManager extends ItemManagerModule Date: Wed, 30 Oct 2024 07:55:47 +0300 Subject: [PATCH 17/48] Update conditional component view --- .../ComponentConditionalVariable.ts | 3 +-- .../src/data_sources/view/ComponentDynamicView.ts | 13 +------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 051594208d..100106e723 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -7,10 +7,9 @@ export default class ComponentConditionalVariable extends Component { dataCondition: DataCondition; constructor(props: ComponentProperties = {}, opt: ComponentOptions) { - let componentProperties = props; const { condition, ifTrue, ifFalse } = props; const dataCondtion = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); - componentProperties = dataCondtion.getDataValue(); + const componentProperties = dataCondtion.getDataValue(); super(componentProperties, opt); this.dataCondition = dataCondtion; diff --git a/packages/core/src/data_sources/view/ComponentDynamicView.ts b/packages/core/src/data_sources/view/ComponentDynamicView.ts index a35732ca01..b632ba42bb 100644 --- a/packages/core/src/data_sources/view/ComponentDynamicView.ts +++ b/packages/core/src/data_sources/view/ComponentDynamicView.ts @@ -1,15 +1,4 @@ import ComponentView from '../../dom_components/view/ComponentView'; -import EditorModel from '../../editor/model/Editor'; import ComponentConditionalVariable from '../model/conditional_variables/ComponentConditionalVariable'; -export default class DynamicView extends ComponentView { - initialize(opt: any = {}) { - const model = this.model; - const config = opt.config || {}; - const em: EditorModel = config.em; - const viewId = this.model.dataCondition.getDataValue()?.type || 'default'; - const view = new (em.Components.getType(viewId).view)(opt); - - return view; - } -} +export default class DynamicView extends ComponentView {} From 2e3c33dcf03d0f8802a6734d8c6911149f5f68fc Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 08:18:26 +0300 Subject: [PATCH 18/48] Add updating components after datasource changes --- .../ComponentConditionalVariable.ts | 7 +++++++ .../model/conditional_variables/DataCondition.ts | 13 ++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 100106e723..db6868d988 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -13,6 +13,13 @@ export default class ComponentConditionalVariable extends Component { super(componentProperties, opt); this.dataCondition = dataCondtion; + this.dataCondition.onValueChange = this.onValueChange.bind(this); + } + + private onValueChange() { + this.dataCondition.reevaluate(); + const componentProperties = this.dataCondition.getDataValue(); + this.set(componentProperties); } static isComponent(el: HTMLElement) { diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index c6f62b877f..77fb820219 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -26,6 +26,7 @@ export class DataCondition extends Model { private condition: Condition; private em: EditorModel; private variableListeners: DynamicVariableListenerManager[] = []; + private _onValueChange?: () => void; defaults() { return { @@ -38,13 +39,14 @@ export class DataCondition extends Model { condition: Expression | LogicGroup | boolean, private ifTrue: any, private ifFalse: any, - opts: { em: EditorModel }, + opts: { em: EditorModel; onValueChange?: () => void }, ) { super(); this.condition = new Condition(condition, { em: opts.em }); this.em = opts.em; this.conditionResult = this.evaluate(); this.listenToDataVariables(); + this._onValueChange = opts.onValueChange; } evaluate() { @@ -59,8 +61,13 @@ export class DataCondition extends Model { this.conditionResult = this.evaluate(); } + set onValueChange(newFunction: () => void) { + this._onValueChange = newFunction; + this.listenToDataVariables(); + } + private listenToDataVariables() { - if (!this.em) return; + if (!this.em || !this._onValueChange) return; // Clear previous listeners to avoid memory leaks this.cleanupListeners(); @@ -73,7 +80,7 @@ export class DataCondition extends Model { model: this as any, em: this.em!, dataVariable: variableInstance, - updateValueFromDataVariable: this.reevaluate.bind(this), + updateValueFromDataVariable: this._onValueChange!, }); this.variableListeners.push(listener); From a5eedca9d3b935a5a626f3bbeea22a0c24c45ce6 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 08:47:04 +0300 Subject: [PATCH 19/48] Throw an error if no condition is passed to a conditional component --- .../ComponentConditionalVariable.ts | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index db6868d988..cd9e9ebed2 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -1,17 +1,35 @@ +import { Model, ObjectAny } from '../../../common'; import Component from '../../../dom_components/model/Component'; -import { ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; +import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; -import { DataCondition, DataConditionType } from './DataCondition'; +import { DataCondition, DataConditionType, Expression, LogicGroup } from './DataCondition'; + +type ConditionalComponentDefinition = { + condition?: Expression | LogicGroup | boolean; + ifTrue: any; + ifFalse: any; +}; +export class MissingConditionError extends Error { + constructor() { + super('No condition was provided to a conditional component.'); + } +} export default class ComponentConditionalVariable extends Component { dataCondition: DataCondition; + componentDefinition: ConditionalComponentDefinition; + + constructor(componentDefinition: ConditionalComponentDefinition, opt: ComponentOptions) { + if (!componentDefinition.condition) { + throw new MissingConditionError; + } - constructor(props: ComponentProperties = {}, opt: ComponentOptions) { - const { condition, ifTrue, ifFalse } = props; + const { condition, ifTrue, ifFalse } = componentDefinition; const dataCondtion = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); - const componentProperties = dataCondtion.getDataValue(); + const props = dataCondtion.getDataValue(); - super(componentProperties, opt); + super(props, opt); + this.componentDefinition = componentDefinition; this.dataCondition = dataCondtion; this.dataCondition.onValueChange = this.onValueChange.bind(this); } From f3ef99af9363ed4a5c2aaf6abe42f3dec566214f Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 08:47:27 +0300 Subject: [PATCH 20/48] Add serialization to conditional components --- .../conditional_variables/ComponentConditionalVariable.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index cd9e9ebed2..1391b7599a 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -43,4 +43,8 @@ export default class ComponentConditionalVariable extends Component { static isComponent(el: HTMLElement) { return toLowerCase(el.tagName) === DataConditionType; } + + toJSON(opts?: ObjectAny): ComponentDefinition { + return Model.prototype.toJSON.call(this.componentDefinition, opts) + } } From d78e393b9f6940b8d3b2a43d2a9c0724a8785189 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 08:50:49 +0300 Subject: [PATCH 21/48] Add tests for conditional components --- .../ComponentConditionalVariable.ts | 7 +- .../ComponentConditionalVariable.ts | 237 ++++++++++++++++++ 2 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 1391b7599a..9b0e5eabba 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -1,4 +1,3 @@ -import { Model, ObjectAny } from '../../../common'; import Component from '../../../dom_components/model/Component'; import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; @@ -21,7 +20,7 @@ export default class ComponentConditionalVariable extends Component { constructor(componentDefinition: ConditionalComponentDefinition, opt: ComponentOptions) { if (!componentDefinition.condition) { - throw new MissingConditionError; + throw new MissingConditionError(); } const { condition, ifTrue, ifFalse } = componentDefinition; @@ -44,7 +43,7 @@ export default class ComponentConditionalVariable extends Component { return toLowerCase(el.tagName) === DataConditionType; } - toJSON(opts?: ObjectAny): ComponentDefinition { - return Model.prototype.toJSON.call(this.componentDefinition, opts) + toJSON(): ComponentDefinition { + return this.componentDefinition; } } diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts new file mode 100644 index 0000000000..acc96ba102 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -0,0 +1,237 @@ +import { DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/ComponentConditionalVariable'; +import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { DataSourceProps } from '../../../../../src/data_sources/types'; +import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { setupTestEditor } from '../../../../common'; + +describe('ComponentConditionalVariable', () => { + let editor: Editor; + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + }); + + afterEach(() => { + em.destroy(); + }); + + it('should add a component with a condition that evaluates a component definition', () => { + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'some text', + }, + })[0]; + + expect(component).toBeDefined(); + expect(component.get('type')).toBe('text'); + expect(component.getInnerHTML()).toBe('some text'); + }); + + // TODO + it.skip('should add a component with a condition that evaluates a string', () => { + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: '
some text
', + })[0]; + + expect(component).toBeDefined(); + expect(component.get('type')).toBe('text'); + expect(component.getInnerHTML()).toBe('some text'); + }); + + it('should test component variable with data-source', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Name1' }, + { id: 'right_id', right: 'Name1' }, + ], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'Some value', + }, + })[0]; + + expect(component).toBeDefined(); + expect(component.get('type')).toBe('text'); + expect(component.getInnerHTML()).toBe('Some value'); + }); + + it('should test a conditional component with a child that is also a conditional component', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Name1' }, + { id: 'right_id', right: 'Name1' }, + ], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'Some value', + components: [ + { + type: DataConditionType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'Some child value', + }, + }, + ], + }, + })[0]; + const childComponent = component.components().at(0); + + expect(component).toBeDefined(); + expect(component.get('type')).toBe('text'); + expect(component.getInnerHTML()).toBe('

Some child value

'); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('Some child value'); + }); + + it('should test component variable with changing value of data-source', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Name1' }, + { id: 'right_id', right: 'Name1' }, + ], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'True value', + }, + ifFalse: { + tagName: 'h1', + type: 'text', + content: 'False value', + }, + })[0]; + dsm.get('ds1').getRecord('left_id')?.set('left', 'Diffirent value'); + + expect(component).toBeDefined(); + expect(component.get('type')).toBe('text'); + expect(component.getInnerHTML()).toBe('False value'); + }); + + it('should test storage for conditional components', () => { + const conditionalCmptDef = { + type: DataConditionType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'some text', + }, + }; + + cmpRoot.append(conditionalCmptDef)[0]; + + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const storageCmptDef = frame.component.components[0]; + expect(storageCmptDef).toEqual(conditionalCmptDef); + }); + + it('should throw an error if no condition is passed', () => { + const conditionalCmptDef = { + type: DataConditionType, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'some text', + }, + }; + + expect(() => { + cmpRoot.append(conditionalCmptDef); + }).toThrow(MissingConditionError); + }); +}); From 493821029212eab6738f7db6712cecac46c2e44a Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 09:23:12 +0300 Subject: [PATCH 22/48] Move missing condition error to DataCondition class --- .../data_sources/model/DataVariableListenerManager.ts | 1 - .../model/conditional_variables/DataCondition.ts | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index fccf349a57..35931f2777 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -7,7 +7,6 @@ import ComponentView from '../../dom_components/view/ComponentView'; import { DynamicValue } from '../types'; import { DataCondition, DataConditionType } from './conditional_variables/DataCondition'; import ComponentDataVariable from './ComponentDataVariable'; -import ComponentConditionalVariable from './conditional_variables/ComponentConditionalVariable'; export interface DynamicVariableListenerManagerOptions { model: Model | ComponentView; diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 77fb820219..0ea78dff2b 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -41,6 +41,10 @@ export class DataCondition extends Model { private ifFalse: any, opts: { em: EditorModel; onValueChange?: () => void }, ) { + if (!condition) { + throw new MissingConditionError(); + } + super(); this.condition = new Condition(condition, { em: opts.em }); this.em = opts.em; @@ -100,3 +104,9 @@ export class DataCondition extends Model { this.variableListeners = []; } } +export class MissingConditionError extends Error { + constructor() { + super('No condition was provided to a conditional component.'); + } +} + From 1bbcc0b9c14b1018f5fd8ffd7b940dd4c7a24653 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 09:23:44 +0300 Subject: [PATCH 23/48] Rename conditional component model and view --- ...ConditionalVariable.ts => ConditionalComponent.ts} | 11 +---------- .../src/data_sources/view/ComponentDynamicView.ts | 4 ++-- packages/core/src/dom_components/index.ts | 6 +++--- 3 files changed, 6 insertions(+), 15 deletions(-) rename packages/core/src/data_sources/model/conditional_variables/{ComponentConditionalVariable.ts => ConditionalComponent.ts} (83%) diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts similarity index 83% rename from packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts rename to packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts index 9b0e5eabba..208048d0e1 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts @@ -4,25 +4,16 @@ import { toLowerCase } from '../../../utils/mixins'; import { DataCondition, DataConditionType, Expression, LogicGroup } from './DataCondition'; type ConditionalComponentDefinition = { - condition?: Expression | LogicGroup | boolean; + condition: Expression | LogicGroup | boolean; ifTrue: any; ifFalse: any; }; -export class MissingConditionError extends Error { - constructor() { - super('No condition was provided to a conditional component.'); - } -} export default class ComponentConditionalVariable extends Component { dataCondition: DataCondition; componentDefinition: ConditionalComponentDefinition; constructor(componentDefinition: ConditionalComponentDefinition, opt: ComponentOptions) { - if (!componentDefinition.condition) { - throw new MissingConditionError(); - } - const { condition, ifTrue, ifFalse } = componentDefinition; const dataCondtion = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); const props = dataCondtion.getDataValue(); diff --git a/packages/core/src/data_sources/view/ComponentDynamicView.ts b/packages/core/src/data_sources/view/ComponentDynamicView.ts index b632ba42bb..2916780c00 100644 --- a/packages/core/src/data_sources/view/ComponentDynamicView.ts +++ b/packages/core/src/data_sources/view/ComponentDynamicView.ts @@ -1,4 +1,4 @@ import ComponentView from '../../dom_components/view/ComponentView'; -import ComponentConditionalVariable from '../model/conditional_variables/ComponentConditionalVariable'; +import ConditionalComponent from '../model/conditional_variables/ConditionalComponent'; -export default class DynamicView extends ComponentView {} +export default class ConditionalComponentView extends ComponentView { } diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 698b23af2b..26f98d02ee 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -126,8 +126,8 @@ import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import { DataVariableType } from '../data_sources/model/DataVariable'; import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition'; -import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ComponentConditionalVariable'; -import DynamicView from '../data_sources/view/ComponentDynamicView'; +import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ConditionalComponent'; +import ConditionalComponentView from '../data_sources/view/ComponentDynamicView'; export type ComponentEvent = | 'component:create' @@ -196,7 +196,7 @@ export default class ComponentManager extends ItemManagerModule Date: Wed, 30 Oct 2024 09:50:03 +0300 Subject: [PATCH 24/48] Make conditional variable value as a children components --- .../model/DataVariableListenerManager.ts | 4 +- .../ConditionalComponent.ts | 52 ++++++++++--- .../conditional_variables/DataCondition.ts | 31 +++++--- packages/core/src/data_sources/model/utils.ts | 4 +- .../data_sources/view/ComponentDynamicView.ts | 2 +- packages/core/src/dom_components/index.ts | 4 +- .../domain_abstract/model/StyleableModel.ts | 8 +- .../core/src/trait_manager/model/Trait.ts | 6 +- .../ComponentConditionalVariable.ts | 74 ++++++++++--------- 9 files changed, 115 insertions(+), 70 deletions(-) diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index 35931f2777..5920086aaf 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -5,7 +5,7 @@ import EditorModel from '../../editor/model/Editor'; import DataVariable, { DataVariableType } from './DataVariable'; import ComponentView from '../../dom_components/view/ComponentView'; import { DynamicValue } from '../types'; -import { DataCondition, DataConditionType } from './conditional_variables/DataCondition'; +import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition'; import ComponentDataVariable from './ComponentDataVariable'; export interface DynamicVariableListenerManagerOptions { @@ -46,7 +46,7 @@ export default class DynamicVariableListenerManager { case DataVariableType: dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em); break; - case DataConditionType: + case ConditionalVariableType: dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em); break; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts index 208048d0e1..86aca0ebee 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts @@ -1,7 +1,8 @@ import Component from '../../../dom_components/model/Component'; -import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; +import Components from '../../../dom_components/model/Components'; +import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; -import { DataCondition, DataConditionType, Expression, LogicGroup } from './DataCondition'; +import { DataCondition, ConditionalVariableType, Expression, LogicGroup } from './DataCondition'; type ConditionalComponentDefinition = { condition: Expression | LogicGroup | boolean; @@ -15,23 +16,52 @@ export default class ComponentConditionalVariable extends Component { constructor(componentDefinition: ConditionalComponentDefinition, opt: ComponentOptions) { const { condition, ifTrue, ifFalse } = componentDefinition; - const dataCondtion = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); - const props = dataCondtion.getDataValue(); + const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); + const initialComponentsProps = dataConditionInstance.getDataValue(); + const conditionalCmptDef = { + type: ConditionalVariableType, + components: initialComponentsProps, + }; + super(conditionalCmptDef, opt); - super(props, opt); this.componentDefinition = componentDefinition; - this.dataCondition = dataCondtion; - this.dataCondition.onValueChange = this.onValueChange.bind(this); + this.dataCondition = dataConditionInstance; + this.dataCondition.onValueChange = this.handleConditionChange.bind(this); + this.refreshComponentState(); } - private onValueChange() { + private handleConditionChange() { this.dataCondition.reevaluate(); - const componentProperties = this.dataCondition.getDataValue(); - this.set(componentProperties); + this.refreshComponentState(); + const updatedProperties = this.dataCondition.getDataValue(); + this.set(updatedProperties); + } + + refreshComponentState() { + if (this.dataCondition.lastEvaluationResult) { + this.assignComponents({ newIfTrueComponents: this.components() }); + } else { + this.assignComponents({ newIfFalseComponents: this.components() }); + } + } + + private assignComponents({ + newIfTrueComponents, + newIfFalseComponents, + }: { + newIfTrueComponents?: Components; + newIfFalseComponents?: Components; + }) { + if (newIfTrueComponents) { + this.dataCondition.ifTrue = newIfTrueComponents; + } + if (newIfFalseComponents) { + this.dataCondition.ifFalse = newIfFalseComponents; + } } static isComponent(el: HTMLElement) { - return toLowerCase(el.tagName) === DataConditionType; + return toLowerCase(el.tagName) === ConditionalVariableType; } toJSON(): ComponentDefinition { diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 0ea78dff2b..7272c1fef7 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -9,7 +9,7 @@ import { Condition } from './Condition'; import DataVariable from '../DataVariable'; import { evaluateVariable, isDataVariable } from '../utils'; -export const DataConditionType = 'conditional-variable'; +export const ConditionalVariableType = 'conditional-variable'; export type Expression = { left: any; operator: GenericOperation | StringOperation | NumberOperation; @@ -22,7 +22,7 @@ export type LogicGroup = { }; export class DataCondition extends Model { - private conditionResult: boolean; + lastEvaluationResult: boolean; private condition: Condition; private em: EditorModel; private variableListeners: DynamicVariableListenerManager[] = []; @@ -30,15 +30,15 @@ export class DataCondition extends Model { defaults() { return { - type: DataConditionType, + type: ConditionalVariableType, condition: false, }; } constructor( condition: Expression | LogicGroup | boolean, - private ifTrue: any, - private ifFalse: any, + private _ifTrue: any, + private _ifFalse: any, opts: { em: EditorModel; onValueChange?: () => void }, ) { if (!condition) { @@ -48,7 +48,7 @@ export class DataCondition extends Model { super(); this.condition = new Condition(condition, { em: opts.em }); this.em = opts.em; - this.conditionResult = this.evaluate(); + this.lastEvaluationResult = this.evaluate(); this.listenToDataVariables(); this._onValueChange = opts.onValueChange; } @@ -58,11 +58,21 @@ export class DataCondition extends Model { } getDataValue(): any { - return this.conditionResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em); + return this.lastEvaluationResult + ? evaluateVariable(this._ifTrue, this.em) + : evaluateVariable(this._ifFalse, this.em); } reevaluate(): void { - this.conditionResult = this.evaluate(); + this.lastEvaluationResult = this.evaluate(); + } + + set ifFalse(newValue: any) { + this._ifFalse = newValue; + } + + set ifTrue(newValue: any) { + this._ifTrue = newValue; } set onValueChange(newFunction: () => void) { @@ -93,8 +103,8 @@ export class DataCondition extends Model { getDependentDataVariables() { const dataVariables = this.condition.getDataVariables(); - if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); - if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); + if (isDataVariable(this._ifTrue)) dataVariables.push(this._ifTrue); + if (isDataVariable(this._ifFalse)) dataVariables.push(this._ifFalse); return dataVariables; } @@ -109,4 +119,3 @@ export class MissingConditionError extends Error { super('No condition was provided to a conditional component.'); } } - diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index 44aaf738b5..e6a8ba736e 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -1,5 +1,5 @@ import EditorModel from '../../editor/model/Editor'; -import { DataConditionType } from './conditional_variables/DataCondition'; +import { ConditionalVariableType } from './conditional_variables/DataCondition'; import DataVariable, { DataVariableType } from './DataVariable'; export function isDataVariable(variable: any) { @@ -7,7 +7,7 @@ export function isDataVariable(variable: any) { } export function isDataCondition(variable: any) { - return variable?.type === DataConditionType; + return variable?.type === ConditionalVariableType; } export function evaluateVariable(variable: any, em: EditorModel) { diff --git a/packages/core/src/data_sources/view/ComponentDynamicView.ts b/packages/core/src/data_sources/view/ComponentDynamicView.ts index 2916780c00..75c287d456 100644 --- a/packages/core/src/data_sources/view/ComponentDynamicView.ts +++ b/packages/core/src/data_sources/view/ComponentDynamicView.ts @@ -1,4 +1,4 @@ import ComponentView from '../../dom_components/view/ComponentView'; import ConditionalComponent from '../model/conditional_variables/ConditionalComponent'; -export default class ConditionalComponentView extends ComponentView { } +export default class ConditionalComponentView extends ComponentView {} diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 26f98d02ee..81102c44d3 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -125,7 +125,7 @@ import { BlockProperties } from '../block_manager/model/Block'; import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import { DataVariableType } from '../data_sources/model/DataVariable'; -import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition'; +import { ConditionalVariableType } from '../data_sources/model/conditional_variables/DataCondition'; import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ConditionalComponent'; import ConditionalComponentView from '../data_sources/view/ComponentDynamicView'; @@ -194,7 +194,7 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ { - id: DataConditionType, + id: ConditionalVariableType, model: ComponentConditionalVariable, view: ConditionalComponentView, }, diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 619d516f57..3784c2c238 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -10,7 +10,7 @@ import DynamicVariableListenerManager from '../../data_sources/model/DataVariabl import CssRuleView from '../../css_composer/view/CssRuleView'; import ComponentView from '../../dom_components/view/ComponentView'; import Frame from '../../canvas/model/Frame'; -import { DataCondition, DataConditionType } from '../../data_sources/model/conditional_variables/DataCondition'; +import { DataCondition, ConditionalVariableType } from '../../data_sources/model/conditional_variables/DataCondition'; export type StyleProps = Record< string, @@ -144,7 +144,7 @@ export default class StyleableModel extends Model } private isDynamicValue(styleValue: any) { - return typeof styleValue === 'object' && [DataVariableType, DataConditionType].includes(styleValue.type); + return typeof styleValue === 'object' && [DataVariableType, ConditionalVariableType].includes(styleValue.type); } private resolveDynamicValue(styleValue: any) { @@ -154,13 +154,13 @@ export default class StyleableModel extends Model case DataVariableType: styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em }); break; - case DataConditionType: + case ConditionalVariableType: const { condition, ifTrue, ifFalse } = styleValue; styleDynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em! }); break; default: throw new Error( - `Invalid data variable type. Expected '${DataVariableType} or ${DataConditionType}', but found '${dynamicType}'.`, + `Invalid data variable type. Expected '${DataVariableType} or ${ConditionalVariableType}', but found '${dynamicType}'.`, ); } diff --git a/packages/core/src/trait_manager/model/Trait.ts b/packages/core/src/trait_manager/model/Trait.ts index 09cfa00144..1d6a666c51 100644 --- a/packages/core/src/trait_manager/model/Trait.ts +++ b/packages/core/src/trait_manager/model/Trait.ts @@ -1,4 +1,4 @@ -import { DataConditionType, DataCondition } from './../../data_sources/model/conditional_variables/DataCondition'; +import { ConditionalVariableType, DataCondition } from './../../data_sources/model/conditional_variables/DataCondition'; import { isString, isUndefined } from 'underscore'; import Category from '../../abstract/ModuleCategory'; import { LocaleOptions, Model, SetOptions } from '../../common'; @@ -64,13 +64,13 @@ export default class Trait extends Model { case DataVariableType: this.dynamicVariable = new TraitDataVariable(this.attributes.value, { em: this.em, trait: this }); break; - case DataConditionType: + case ConditionalVariableType: const { condition, ifTrue, ifFalse } = this.attributes.value; this.dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em }); break; default: throw new Error( - `Invalid data variable type. Expected '${DataVariableType} or ${DataConditionType}', but found '${dataType}'.`, + `Invalid data variable type. Expected '${DataVariableType} or ${ConditionalVariableType}', but found '${dataType}'.`, ); } diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index acc96ba102..7732181ab3 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -1,7 +1,7 @@ import { DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/ComponentConditionalVariable'; -import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { DataSourceProps } from '../../../../../src/data_sources/types'; @@ -25,7 +25,7 @@ describe('ComponentConditionalVariable', () => { it('should add a component with a condition that evaluates a component definition', () => { const component = cmpRoot.append({ - type: DataConditionType, + type: ConditionalVariableType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -39,25 +39,33 @@ describe('ComponentConditionalVariable', () => { })[0]; expect(component).toBeDefined(); - expect(component.get('type')).toBe('text'); - expect(component.getInnerHTML()).toBe('some text'); + expect(component.get('type')).toBe(ConditionalVariableType); + expect(component.getInnerHTML()).toBe('

some text

'); + + const childComponent = component.components().at(0); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('some text'); }); - // TODO - it.skip('should add a component with a condition that evaluates a string', () => { + it('should add a component with a condition that evaluates a string', () => { const component = cmpRoot.append({ - type: DataConditionType, + type: ConditionalVariableType, condition: { left: 0, operator: NumberOperation.greaterThan, right: -1, }, - ifTrue: '
some text
', + ifTrue: '

some text

', })[0]; - expect(component).toBeDefined(); - expect(component.get('type')).toBe('text'); - expect(component.getInnerHTML()).toBe('some text'); + expect(component.get('type')).toBe(ConditionalVariableType); + expect(component.getInnerHTML()).toBe('

some text

'); + + const childComponent = component.components().at(0); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('some text'); }); it('should test component variable with data-source', () => { @@ -71,7 +79,7 @@ describe('ComponentConditionalVariable', () => { dsm.add(dataSource); const component = cmpRoot.append({ - type: DataConditionType, + type: ConditionalVariableType, condition: { left: { type: DataVariableType, @@ -90,9 +98,10 @@ describe('ComponentConditionalVariable', () => { }, })[0]; - expect(component).toBeDefined(); - expect(component.get('type')).toBe('text'); - expect(component.getInnerHTML()).toBe('Some value'); + const childComponent = component.components().at(0); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('Some value'); }); it('should test a conditional component with a child that is also a conditional component', () => { @@ -106,7 +115,7 @@ describe('ComponentConditionalVariable', () => { dsm.add(dataSource); const component = cmpRoot.append({ - type: DataConditionType, + type: ConditionalVariableType, condition: { left: { type: DataVariableType, @@ -119,12 +128,10 @@ describe('ComponentConditionalVariable', () => { }, }, ifTrue: { - tagName: 'h1', - type: 'text', - content: 'Some value', + tagName: 'div', components: [ { - type: DataConditionType, + type: ConditionalVariableType, condition: { left: { type: DataVariableType, @@ -145,14 +152,12 @@ describe('ComponentConditionalVariable', () => { ], }, })[0]; - const childComponent = component.components().at(0); - expect(component).toBeDefined(); - expect(component.get('type')).toBe('text'); - expect(component.getInnerHTML()).toBe('

Some child value

'); - expect(childComponent).toBeDefined(); - expect(childComponent.get('type')).toBe('text'); - expect(childComponent.getInnerHTML()).toBe('Some child value'); + const childComponent = component.components().at(0); + const innerComponent = childComponent.components().at(0); + expect(innerComponent).toBeDefined(); + expect(innerComponent.get('type')).toBe(ConditionalVariableType); + expect(innerComponent.getInnerHTML()).toBe('

Some child value

'); }); it('should test component variable with changing value of data-source', () => { @@ -166,7 +171,7 @@ describe('ComponentConditionalVariable', () => { dsm.add(dataSource); const component = cmpRoot.append({ - type: DataConditionType, + type: ConditionalVariableType, condition: { left: { type: DataVariableType, @@ -191,14 +196,15 @@ describe('ComponentConditionalVariable', () => { })[0]; dsm.get('ds1').getRecord('left_id')?.set('left', 'Diffirent value'); - expect(component).toBeDefined(); - expect(component.get('type')).toBe('text'); - expect(component.getInnerHTML()).toBe('False value'); + const childComponent = component.components().at(0); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('False value'); }); it('should test storage for conditional components', () => { const conditionalCmptDef = { - type: DataConditionType, + type: ConditionalVariableType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -222,7 +228,7 @@ describe('ComponentConditionalVariable', () => { it('should throw an error if no condition is passed', () => { const conditionalCmptDef = { - type: DataConditionType, + type: ConditionalVariableType, ifTrue: { tagName: 'h1', type: 'text', From b31633d00c6d7d09c9bf7525d58590ac885b6a67 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 09:50:42 +0300 Subject: [PATCH 25/48] Make a method private --- .../model/conditional_variables/ConditionalComponent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts index 86aca0ebee..a5c5def653 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts @@ -37,7 +37,7 @@ export default class ComponentConditionalVariable extends Component { this.set(updatedProperties); } - refreshComponentState() { + private refreshComponentState() { if (this.dataCondition.lastEvaluationResult) { this.assignComponents({ newIfTrueComponents: this.components() }); } else { From 5c422fa6a9fa705e70be0c202a5cc487c0e7539a Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 10:04:20 +0300 Subject: [PATCH 26/48] Allow switching between true and false states for conditional variables --- .../ConditionalComponent.ts | 11 ++++- .../ComponentConditionalVariable.ts | 44 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts index a5c5def653..a69d62137f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts @@ -32,9 +32,16 @@ export default class ComponentConditionalVariable extends Component { private handleConditionChange() { this.dataCondition.reevaluate(); + const updatedComponents = this.dataCondition.getDataValue(); + if (updatedComponents instanceof Components) { + const componentsArray = updatedComponents.map((cmp) => cmp); + this.components().set(componentsArray); + } else { + this.components().reset(); + this.components().add(updatedComponents); + } + this.refreshComponentState(); - const updatedProperties = this.dataCondition.getDataValue(); - this.set(updatedProperties); } private refreshComponentState() { diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 7732181ab3..03d67c69fd 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -202,7 +202,49 @@ describe('ComponentConditionalVariable', () => { expect(childComponent.getInnerHTML()).toBe('False value'); }); - it('should test storage for conditional components', () => { + it('should change with changing value of data-source', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Name1' }, + { id: 'right_id', right: 'Name1' }, + ], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'True value', + }, + ifFalse: { + tagName: 'h1', + type: 'text', + content: 'False value', + }, + })[0]; + dsm.get('ds1').getRecord('left_id')?.set('left', 'Diffirent value'); + + const childComponent = component.components().at(0); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('False value'); + }); + + it('should store conditional components', () => { const conditionalCmptDef = { type: ConditionalVariableType, condition: { From d29f6d2e47c148cfbcdb99cbc849d84b26998c3c Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 10:09:39 +0300 Subject: [PATCH 27/48] Rename a variable --- .../ConditionalComponent.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts index a69d62137f..1598cdeeee 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts @@ -46,24 +46,24 @@ export default class ComponentConditionalVariable extends Component { private refreshComponentState() { if (this.dataCondition.lastEvaluationResult) { - this.assignComponents({ newIfTrueComponents: this.components() }); + this.assignComponents({ positiveCaseComponents: this.components() }); } else { - this.assignComponents({ newIfFalseComponents: this.components() }); + this.assignComponents({ negativeCaseComponents: this.components() }); } } private assignComponents({ - newIfTrueComponents, - newIfFalseComponents, + positiveCaseComponents, + negativeCaseComponents, }: { - newIfTrueComponents?: Components; - newIfFalseComponents?: Components; + positiveCaseComponents?: Components; + negativeCaseComponents?: Components; }) { - if (newIfTrueComponents) { - this.dataCondition.ifTrue = newIfTrueComponents; + if (positiveCaseComponents) { + this.dataCondition.ifTrue = positiveCaseComponents; } - if (newIfFalseComponents) { - this.dataCondition.ifFalse = newIfFalseComponents; + if (negativeCaseComponents) { + this.dataCondition.ifFalse = negativeCaseComponents; } } From b024bc33b0f183c4747cc40f93aafb181767af02 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 10:24:44 +0300 Subject: [PATCH 28/48] Add tests for conditional styles --- .../ComponentConditionalVariable.ts | 1 - .../ConditionalStyles.ts | 133 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 03d67c69fd..a007c214a2 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -37,7 +37,6 @@ describe('ComponentConditionalVariable', () => { content: 'some text', }, })[0]; - expect(component).toBeDefined(); expect(component.get('type')).toBe(ConditionalVariableType); expect(component.getInnerHTML()).toBe('

some text

'); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts new file mode 100644 index 0000000000..8318f22d83 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts @@ -0,0 +1,133 @@ +import { DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { ConditionalVariableType, MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { DataSourceProps } from '../../../../../src/data_sources/types'; +import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; + +describe('StyleConditionalVariable', () => { + let editor: Editor; + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + }); + + afterEach(() => { + em.destroy(); + }); + + it('should add a component with a conditionally styled attribute', () => { + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'red', + ifFalse: 'black', + }, + }, + })[0]; + + expect(component).toBeDefined(); + expect(component.getStyle().color).toBe('red'); + }); + + it('should change style based on data source changes', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Value1' }, + { id: 'right_id', right: 'Value2' }, + ], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: 'green', + ifFalse: 'blue', + }, + }, + })[0]; + + expect(component.getStyle().color).toBe('blue'); + + dsm.get('ds1').getRecord('right_id')?.set('right', 'Value1'); + expect(component.getStyle().color).toBe('green'); + }); + + it('should throw an error if no condition is passed in style', () => { + expect(() => { + cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + ifTrue: 'green', + ifFalse: 'red', + }, + }, + }); + }).toThrow(MissingConditionError); + }); + + // TODO + it.skip('should store components with conditional styles correctly', () => { + const conditionalStyleDef = { + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'yellow', + ifFalse: 'black', + }, + }, + }; + + cmpRoot.append(conditionalStyleDef)[0]; + + const projectData = filterObjectForSnapshot(editor.getProjectData()); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const storedComponent = frame.component.components[0]; + expect(storedComponent).toEqual(expect.objectContaining({ style: conditionalStyleDef.style })); + }); +}); From b2c9d614c949008de0b8dc190eee69ec61c2a9dd Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 10:40:26 +0300 Subject: [PATCH 29/48] Fix throwing an error when passing "false" as a condition value --- .../data_sources/model/conditional_variables/DataCondition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 7272c1fef7..e2c47f92e9 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -41,7 +41,7 @@ export class DataCondition extends Model { private _ifFalse: any, opts: { em: EditorModel; onValueChange?: () => void }, ) { - if (!condition) { + if (typeof condition === 'undefined') { throw new MissingConditionError(); } From ee08f60149f2d78c91750c04778b0bab6347899c Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 12:15:41 +0300 Subject: [PATCH 30/48] Allow serilzation of traits --- .../model/conditional_variables/Condition.ts | 4 +- .../ConditionalComponent.ts | 2 +- .../conditional_variables/DataCondition.ts | 29 ++- .../src/dom_components/model/Component.ts | 29 ++- .../ConditionalStyles.ts | 236 ++++++++++-------- .../test/specs/data_sources/serialization.ts | 6 +- 6 files changed, 173 insertions(+), 133 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/Condition.ts index 4586f804b2..d8e3e562bf 100644 --- a/packages/core/src/data_sources/model/conditional_variables/Condition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/Condition.ts @@ -1,3 +1,4 @@ +import { Model } from 'backbone'; import EditorModel from '../../../editor/model/Editor'; import DataVariable from '../DataVariable'; import { evaluateVariable, isDataVariable } from '../utils'; @@ -9,11 +10,12 @@ import { LogicalOperator } from './operators/LogicalOperator'; import { NumberOperator, NumberOperation } from './operators/NumberOperator'; import { StringOperator, StringOperation } from './operators/StringOperations'; -export class Condition { +export class Condition extends Model { private condition: Expression | LogicGroup | boolean; private em: EditorModel; constructor(condition: Expression | LogicGroup | boolean, opts: { em: EditorModel }) { + super(condition); this.condition = condition; this.em = opts.em; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts index 1598cdeeee..51fcae5253 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts @@ -72,6 +72,6 @@ export default class ComponentConditionalVariable extends Component { } toJSON(): ComponentDefinition { - return this.componentDefinition; + return this.dataCondition.toJSON(); } } diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index e2c47f92e9..8d0325a4e4 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -37,8 +37,8 @@ export class DataCondition extends Model { constructor( condition: Expression | LogicGroup | boolean, - private _ifTrue: any, - private _ifFalse: any, + public ifTrue: any, + public ifFalse: any, opts: { em: EditorModel; onValueChange?: () => void }, ) { if (typeof condition === 'undefined') { @@ -58,23 +58,13 @@ export class DataCondition extends Model { } getDataValue(): any { - return this.lastEvaluationResult - ? evaluateVariable(this._ifTrue, this.em) - : evaluateVariable(this._ifFalse, this.em); + return this.lastEvaluationResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em); } reevaluate(): void { this.lastEvaluationResult = this.evaluate(); } - set ifFalse(newValue: any) { - this._ifFalse = newValue; - } - - set ifTrue(newValue: any) { - this._ifTrue = newValue; - } - set onValueChange(newFunction: () => void) { this._onValueChange = newFunction; this.listenToDataVariables(); @@ -103,8 +93,8 @@ export class DataCondition extends Model { getDependentDataVariables() { const dataVariables = this.condition.getDataVariables(); - if (isDataVariable(this._ifTrue)) dataVariables.push(this._ifTrue); - if (isDataVariable(this._ifFalse)) dataVariables.push(this._ifFalse); + if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); + if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); return dataVariables; } @@ -113,6 +103,15 @@ export class DataCondition extends Model { this.variableListeners.forEach((listener) => listener.destroy()); this.variableListeners = []; } + + toJSON() { + return { + type: ConditionalVariableType, + condition: this.condition, + ifTrue: this.ifTrue, + ifFalse: this.ifFalse, + }; + } } export class MissingConditionError extends Error { constructor() { diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 55afeeac88..d553c3ab24 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -52,6 +52,8 @@ import { updateSymbolProps, } from './SymbolUtils'; import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; +import { ConditionalVariableType, DataCondition } from '../../data_sources/model/conditional_variables/DataCondition'; +import { DataVariableType } from '../../data_sources/model/DataVariable'; export interface IComponent extends ExtractMethods {} @@ -769,10 +771,31 @@ export default class Component extends StyleableModel { } } - const attrDataVariable = this.get('attributes-data-variable'); + const attrDataVariable = this.get('attributes-dynamic-value'); if (attrDataVariable) { Object.entries(attrDataVariable).forEach(([key, value]) => { - const dataVariable = value instanceof TraitDataVariable ? value : new TraitDataVariable(value, { em }); + let dataVariable: TraitDataVariable | DataCondition; + + switch (true) { + case value instanceof DataCondition: + case value instanceof TraitDataVariable: + dataVariable = value; + break; + + case (value as any).type === ConditionalVariableType: { + const { condition, ifTrue, ifFalse } = value as any; + dataVariable = new DataCondition(condition, ifTrue, ifFalse, { em }); + break; + } + + case (value as any).type === DataVariableType: + dataVariable = new TraitDataVariable(value, { em }); + break; + + default: + throw new Error(`Unexpected data type for key: ${key}`); + } + attributes[key] = dataVariable.getDataValue(); }); } @@ -932,7 +955,7 @@ export default class Component extends StyleableModel { } }); traits.length && this.set('attributes', attrs); - Object.keys(traitDynamicValueAttr).length && this.set('attributes-data-variable', traitDynamicValueAttr); + Object.keys(traitDynamicValueAttr).length && this.set('attributes-dynamic-value', traitDynamicValueAttr); this.on(event, this.initTraits); changed && em && em.trigger('component:toggled'); return this; diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts index 8318f22d83..7024bb9ce8 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts @@ -1,6 +1,9 @@ import { DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { ConditionalVariableType, MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { + ConditionalVariableType, + MissingConditionError, +} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { DataSourceProps } from '../../../../../src/data_sources/types'; @@ -9,125 +12,138 @@ import EditorModel from '../../../../../src/editor/model/Editor'; import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; describe('StyleConditionalVariable', () => { - let editor: Editor; - let em: EditorModel; - let dsm: DataSourceManager; - let cmpRoot: ComponentWrapper; + let editor: Editor; + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; - beforeEach(() => { - ({ editor, em, dsm, cmpRoot } = setupTestEditor()); - }); + beforeEach(() => { + ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + }); - afterEach(() => { - em.destroy(); - }); + afterEach(() => { + em.destroy(); + }); - it('should add a component with a conditionally styled attribute', () => { - const component = cmpRoot.append({ - tagName: 'h1', - type: 'text', - content: 'some text', - style: { - color: { - type: ConditionalVariableType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: 'red', - ifFalse: 'black', - }, - }, - })[0]; + it.skip('should add a component with a conditionally styled attribute', () => { + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'red', + ifFalse: 'black', + }, + }, + })[0]; - expect(component).toBeDefined(); - expect(component.getStyle().color).toBe('red'); - }); + expect(component).toBeDefined(); + expect(component.getStyle().color).toBe('red'); + }); - it('should change style based on data source changes', () => { - const dataSource: DataSourceProps = { - id: 'ds1', - records: [ - { id: 'left_id', left: 'Value1' }, - { id: 'right_id', right: 'Value2' }, - ], - }; - dsm.add(dataSource); + it('should change style based on data source changes', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Value1' }, + { id: 'right_id', right: 'Value2' }, + ], + }; + dsm.add(dataSource); - const component = cmpRoot.append({ - tagName: 'h1', - type: 'text', - content: 'some text', - style: { - color: { - type: ConditionalVariableType, - condition: { - left: { - type: DataVariableType, - path: 'ds1.left_id.left', - }, - operator: GenericOperation.equals, - right: { - type: DataVariableType, - path: 'ds1.right_id.right', - }, - }, - ifTrue: 'green', - ifFalse: 'blue', - }, - }, - })[0]; + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: false, + ifTrue: 'green', + ifFalse: 'blue', + }, + }, + })[0]; + // const component = cmpRoot.append({ + // tagName: 'h1', + // type: 'text', + // content: 'some text', + // style: { + // color: { + // type: ConditionalVariableType, + // condition: { + // left: { + // type: DataVariableType, + // path: 'ds1.left_id.left', + // }, + // operator: GenericOperation.equals, + // right: { + // type: DataVariableType, + // path: 'ds1.right_id.right', + // }, + // }, + // ifTrue: 'green', + // ifFalse: 'blue', + // }, + // }, + // })[0]; - expect(component.getStyle().color).toBe('blue'); + expect(component.getStyle().color).toBe('blue'); - dsm.get('ds1').getRecord('right_id')?.set('right', 'Value1'); - expect(component.getStyle().color).toBe('green'); - }); + dsm.get('ds1').getRecord('right_id')?.set('right', 'Value1'); + expect(component.getStyle().color).toBe('green'); + }); - it('should throw an error if no condition is passed in style', () => { - expect(() => { - cmpRoot.append({ - tagName: 'h1', - type: 'text', - content: 'some text', - style: { - color: { - type: ConditionalVariableType, - ifTrue: 'green', - ifFalse: 'red', - }, - }, - }); - }).toThrow(MissingConditionError); - }); + it.skip('should throw an error if no condition is passed in style', () => { + expect(() => { + cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + ifTrue: 'grey', + ifFalse: 'red', + }, + }, + }); + }).toThrow(MissingConditionError); + }); - // TODO - it.skip('should store components with conditional styles correctly', () => { - const conditionalStyleDef = { - tagName: 'h1', - type: 'text', - content: 'some text', - style: { - color: { - type: ConditionalVariableType, - condition: { - left: 0, - operator: NumberOperation.greaterThan, - right: -1, - }, - ifTrue: 'yellow', - ifFalse: 'black', - }, - }, - }; + // TODO + it.skip('should store components with conditional styles correctly', () => { + const conditionalStyleDef = { + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'yellow', + ifFalse: 'black', + }, + }, + }; - cmpRoot.append(conditionalStyleDef)[0]; + cmpRoot.append(conditionalStyleDef)[0]; - const projectData = filterObjectForSnapshot(editor.getProjectData()); - const page = projectData.pages[0]; - const frame = page.frames[0]; - const storedComponent = frame.component.components[0]; - expect(storedComponent).toEqual(expect.objectContaining({ style: conditionalStyleDef.style })); - }); + const projectData = filterObjectForSnapshot(editor.getProjectData()); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const storedComponent = frame.component.components[0]; + expect(storedComponent).toEqual(expect.objectContaining({ style: conditionalStyleDef.style })); + }); }); diff --git a/packages/core/test/specs/data_sources/serialization.ts b/packages/core/test/specs/data_sources/serialization.ts index 9a9c463a11..9999b37ec8 100644 --- a/packages/core/test/specs/data_sources/serialization.ts +++ b/packages/core/test/specs/data_sources/serialization.ts @@ -143,8 +143,8 @@ describe('DataSource Serialization', () => { const page = projectData.pages[0]; const frame = page.frames[0]; const component = frame.component.components[0]; - expect(component).toHaveProperty('attributes-data-variable'); - expect(component['attributes-data-variable']).toEqual({ + expect(component).toHaveProperty('attributes-dynamic-value'); + expect(component['attributes-dynamic-value']).toEqual({ value: dataVariable, }); expect(component.attributes).toEqual({ @@ -297,7 +297,7 @@ describe('DataSource Serialization', () => { attributes: { value: 'default', }, - 'attributes-data-variable': { + 'attributes-dynamic-value': { value: { path: 'test-input.id1.value', type: 'data-variable', From ec6f318ffb2688ee35218880241efabf34093928 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 12:52:12 +0300 Subject: [PATCH 31/48] Fix listening to traits --- .../data_sources/model/DataVariableListenerManager.ts | 2 +- .../model/conditional_variables/Condition.ts | 11 +++++++---- .../model/conditional_variables/DataCondition.ts | 7 +++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index 5920086aaf..46d1528b91 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -57,7 +57,7 @@ export default class DynamicVariableListenerManager { private listenToConditionalVariable(dataVariable: DataCondition, em: EditorModel) { const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { - return this.listenToDataVariable(dataVariable, em); + return this.listenToDataVariable(new DataVariable(dataVariable, { em: this.em }), em); }); return dataListeners; diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/Condition.ts index d8e3e562bf..8a4a2f6c67 100644 --- a/packages/core/src/data_sources/model/conditional_variables/Condition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/Condition.ts @@ -1,6 +1,6 @@ +import { DataVariableType } from './../DataVariable'; import { Model } from 'backbone'; import EditorModel from '../../../editor/model/Editor'; -import DataVariable from '../DataVariable'; import { evaluateVariable, isDataVariable } from '../utils'; import { Expression, LogicGroup } from './DataCondition'; import { LogicalGroupStatement } from './LogicalGroupStatement'; @@ -67,8 +67,8 @@ export class Condition extends Model { /** * Extracts all data variables from the condition, including nested ones. */ - getDataVariables(): DataVariable[] { - const variables: DataVariable[] = []; + getDataVariables() { + const variables: { type: typeof DataVariableType }[] = []; this.extractVariables(this.condition, variables); return variables; } @@ -76,7 +76,10 @@ export class Condition extends Model { /** * Recursively extracts variables from expressions or logic groups. */ - private extractVariables(condition: boolean | LogicGroup | Expression, variables: DataVariable[]): void { + private extractVariables( + condition: boolean | LogicGroup | Expression, + variables: { type: typeof DataVariableType }[], + ): void { if (this.isExpression(condition)) { if (isDataVariable(condition.left)) variables.push(condition.left); if (isDataVariable(condition.right)) variables.push(condition.right); diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 8d0325a4e4..543335ae51 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -71,7 +71,7 @@ export class DataCondition extends Model { } private listenToDataVariables() { - if (!this.em || !this._onValueChange) return; + if (!this.em) return; // Clear previous listeners to avoid memory leaks this.cleanupListeners(); @@ -84,7 +84,10 @@ export class DataCondition extends Model { model: this as any, em: this.em!, dataVariable: variableInstance, - updateValueFromDataVariable: this._onValueChange!, + updateValueFromDataVariable: (() => { + this.reevaluate(); + this._onValueChange?.(); + }).bind(this), }); this.variableListeners.push(listener); From 80baf2cba2d5d094ca3959679873096d79870100 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 12:52:23 +0300 Subject: [PATCH 32/48] Add tests for conditional traits --- .../ConditionalTraits.ts | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts new file mode 100644 index 0000000000..ac51a84c2e --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -0,0 +1,174 @@ +import { DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { DataSourceProps } from '../../../../../src/data_sources/types'; +import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { setupTestEditor } from '../../../../common'; + +describe('TraitConditionalVariable', () => { + let editor: Editor; + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + }); + + afterEach(() => { + em.destroy(); + }); + + it('should add a trait with a condition evaluating to a string', () => { + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'title', + value: { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'Some title', + }, + }, + ], + })[0]; + + expect(component).toBeDefined(); + expect(component.getTrait('title').get('value')).toBe('Some title'); + }); + + it('should add a trait with a data-source condition', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [{ id: 'left_id', left: 'Name1' }], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'value', + value: { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: 'Name1', + }, + ifTrue: 'Valid name', + ifFalse: 'Invalid name', + }, + }, + ], + })[0]; + + const traitValue = component.getTrait('value').get('value'); + expect(traitValue).toBe('Valid name'); + }); + + it('should change trait value with changing data-source value', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [{ id: 'left_id', left: 'Name1' }], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'value', + value: { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: 'Name1', + }, + ifTrue: 'Correct name', + ifFalse: 'Incorrect name', + }, + }, + ], + })[0]; + + const traitValueBefore = component.getTrait('value').get('value'); + expect(traitValueBefore).toBe('Correct name'); + + dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name'); + const traitValueAfter = component.getTrait('value').get('value'); + expect(traitValueAfter).toBe('Incorrect name'); + }); + + it('should throw an error if no condition is passed in trait', () => { + expect(() => { + cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'invalidTrait', + value: { + type: ConditionalVariableType, + }, + }, + ], + }); + }).toThrow(MissingConditionError); + }); + + it('should store traits with conditional values correctly', () => { + const conditionalTrait = { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'Positive', + }; + cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'dynamicTrait', + value: conditionalTrait, + }, + ], + })[0]; + + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const storedComponent = frame.component.components[0]; + + expect(storedComponent['attributes-dynamic-value']).toEqual({ + dynamicTrait: conditionalTrait, + }); + }); +}); From 9d6ec5854c94b203755df659d21b2e488e21bcc4 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 13:07:13 +0300 Subject: [PATCH 33/48] Unskip all tests for conditional styles --- .../ConditionalStyles.ts | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts index 7024bb9ce8..ad9bd4ef25 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts @@ -25,7 +25,7 @@ describe('StyleConditionalVariable', () => { em.destroy(); }); - it.skip('should add a component with a conditionally styled attribute', () => { + it('should add a component with a conditionally styled attribute', () => { const component = cmpRoot.append({ tagName: 'h1', type: 'text', @@ -65,35 +65,22 @@ describe('StyleConditionalVariable', () => { style: { color: { type: ConditionalVariableType, - condition: false, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, ifTrue: 'green', ifFalse: 'blue', }, }, })[0]; - // const component = cmpRoot.append({ - // tagName: 'h1', - // type: 'text', - // content: 'some text', - // style: { - // color: { - // type: ConditionalVariableType, - // condition: { - // left: { - // type: DataVariableType, - // path: 'ds1.left_id.left', - // }, - // operator: GenericOperation.equals, - // right: { - // type: DataVariableType, - // path: 'ds1.right_id.right', - // }, - // }, - // ifTrue: 'green', - // ifFalse: 'blue', - // }, - // }, - // })[0]; expect(component.getStyle().color).toBe('blue'); @@ -101,7 +88,7 @@ describe('StyleConditionalVariable', () => { expect(component.getStyle().color).toBe('green'); }); - it.skip('should throw an error if no condition is passed in style', () => { + it('should throw an error if no condition is passed in style', () => { expect(() => { cmpRoot.append({ tagName: 'h1', @@ -118,7 +105,6 @@ describe('StyleConditionalVariable', () => { }).toThrow(MissingConditionError); }); - // TODO it.skip('should store components with conditional styles correctly', () => { const conditionalStyleDef = { tagName: 'h1', @@ -144,6 +130,6 @@ describe('StyleConditionalVariable', () => { const page = projectData.pages[0]; const frame = page.frames[0]; const storedComponent = frame.component.components[0]; - expect(storedComponent).toEqual(expect.objectContaining({ style: conditionalStyleDef.style })); + expect(storedComponent).toEqual(expect.objectContaining(conditionalStyleDef)); }); }); From 5f2a00d52c76d8803459e190b570324b4fc6966c Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 13:14:00 +0300 Subject: [PATCH 34/48] Fix a unit test --- .../specs/data_sources/__snapshots__/serialization.ts.snap | 2 +- .../conditional_variables/ComponentConditionalVariable.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap b/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap index 0a0d726e00..cdb56af488 100644 --- a/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap +++ b/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap @@ -126,7 +126,7 @@ exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = ` "attributes": { "value": "test-value", }, - "attributes-data-variable": { + "attributes-dynamic-value": { "value": { "defaultValue": "default", "path": "test-input.id1.value", diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index a007c214a2..ba43a347d7 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -251,11 +251,11 @@ describe('ComponentConditionalVariable', () => { operator: NumberOperation.greaterThan, right: -1, }, - ifTrue: { + ifTrue: [{ tagName: 'h1', type: 'text', content: 'some text', - }, + }], }; cmpRoot.append(conditionalCmptDef)[0]; From 25ff13405815de72710e360721593dbf40d918dc Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 30 Oct 2024 13:16:57 +0300 Subject: [PATCH 35/48] Format --- .../ComponentConditionalVariable.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index ba43a347d7..b792212fec 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -251,11 +251,13 @@ describe('ComponentConditionalVariable', () => { operator: NumberOperation.greaterThan, right: -1, }, - ifTrue: [{ - tagName: 'h1', - type: 'text', - content: 'some text', - }], + ifTrue: [ + { + tagName: 'h1', + type: 'text', + content: 'some text', + }, + ], }; cmpRoot.append(conditionalCmptDef)[0]; From 1151f43e44449436d7afd20e1fdde2e8c3040c8d Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Thu, 31 Oct 2024 12:40:11 +0300 Subject: [PATCH 36/48] Import Model from common instead of backbone --- .../src/data_sources/model/conditional_variables/Condition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/Condition.ts index 8a4a2f6c67..9c0005278a 100644 --- a/packages/core/src/data_sources/model/conditional_variables/Condition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/Condition.ts @@ -1,5 +1,4 @@ import { DataVariableType } from './../DataVariable'; -import { Model } from 'backbone'; import EditorModel from '../../../editor/model/Editor'; import { evaluateVariable, isDataVariable } from '../utils'; import { Expression, LogicGroup } from './DataCondition'; @@ -9,6 +8,7 @@ import { GenericOperation, GenericOperator } from './operators/GenericOperator'; import { LogicalOperator } from './operators/LogicalOperator'; import { NumberOperator, NumberOperation } from './operators/NumberOperator'; import { StringOperator, StringOperation } from './operators/StringOperations'; +import { Model } from '../../../common'; export class Condition extends Model { private condition: Expression | LogicGroup | boolean; From 747bb9f6d9d9346979db0cdd728658f0d64310b7 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Thu, 31 Oct 2024 13:01:44 +0300 Subject: [PATCH 37/48] enclose a case block in braces --- packages/core/src/domain_abstract/model/StyleableModel.ts | 5 +++-- packages/core/src/trait_manager/model/Trait.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 3784c2c238..e23f3e14da 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -154,13 +154,14 @@ export default class StyleableModel extends Model case DataVariableType: styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em }); break; - case ConditionalVariableType: + case ConditionalVariableType: { const { condition, ifTrue, ifFalse } = styleValue; styleDynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em! }); break; + } default: throw new Error( - `Invalid data variable type. Expected '${DataVariableType} or ${ConditionalVariableType}', but found '${dynamicType}'.`, + `Invalid data variable type. Expected '${DataVariableType}' or '${ConditionalVariableType}', but found '${dynamicType}'.`, ); } diff --git a/packages/core/src/trait_manager/model/Trait.ts b/packages/core/src/trait_manager/model/Trait.ts index 1d6a666c51..60f8def263 100644 --- a/packages/core/src/trait_manager/model/Trait.ts +++ b/packages/core/src/trait_manager/model/Trait.ts @@ -64,10 +64,11 @@ export default class Trait extends Model { case DataVariableType: this.dynamicVariable = new TraitDataVariable(this.attributes.value, { em: this.em, trait: this }); break; - case ConditionalVariableType: + case ConditionalVariableType: { const { condition, ifTrue, ifFalse } = this.attributes.value; this.dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em }); break; + } default: throw new Error( `Invalid data variable type. Expected '${DataVariableType} or ${ConditionalVariableType}', but found '${dataType}'.`, From 01752faf6d0a98b52125b03b2ef92b540cbd7542 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 12:00:39 +0200 Subject: [PATCH 38/48] Replace "attributes-dynamic-value" with a constant --- packages/core/src/dom_components/model/Component.ts | 5 +++-- .../model/conditional_variables/ConditionalTraits.ts | 3 ++- packages/core/test/specs/data_sources/serialization.ts | 7 ++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index d553c3ab24..169024eb48 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -71,6 +71,7 @@ export const keySymbol = '__symbol'; export const keySymbolOvrd = '__symbol_ovrd'; export const keyUpdate = ComponentsEvents.update; export const keyUpdateInside = ComponentsEvents.updateInside; +export const dynamicAttrKey = 'attributes-dynamic-value'; /** * The Component object represents a single node of our template structure, so when you update its properties the changes are @@ -771,7 +772,7 @@ export default class Component extends StyleableModel { } } - const attrDataVariable = this.get('attributes-dynamic-value'); + const attrDataVariable = this.get(dynamicAttrKey); if (attrDataVariable) { Object.entries(attrDataVariable).forEach(([key, value]) => { let dataVariable: TraitDataVariable | DataCondition; @@ -955,7 +956,7 @@ export default class Component extends StyleableModel { } }); traits.length && this.set('attributes', attrs); - Object.keys(traitDynamicValueAttr).length && this.set('attributes-dynamic-value', traitDynamicValueAttr); + Object.keys(traitDynamicValueAttr).length && this.set(dynamicAttrKey, traitDynamicValueAttr); this.on(event, this.initTraits); changed && em && em.trigger('component:toggled'); return this; diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts index ac51a84c2e..393ed4d9a0 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -5,6 +5,7 @@ import { ConditionalVariableType } from '../../../../../src/data_sources/model/c import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { DataSourceProps } from '../../../../../src/data_sources/types'; +import { dynamicAttrKey } from '../../../../../src/dom_components/model/Component'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import EditorModel from '../../../../../src/editor/model/Editor'; import { setupTestEditor } from '../../../../common'; @@ -167,7 +168,7 @@ describe('TraitConditionalVariable', () => { const frame = page.frames[0]; const storedComponent = frame.component.components[0]; - expect(storedComponent['attributes-dynamic-value']).toEqual({ + expect(storedComponent[dynamicAttrKey]).toEqual({ dynamicTrait: conditionalTrait, }); }); diff --git a/packages/core/test/specs/data_sources/serialization.ts b/packages/core/test/specs/data_sources/serialization.ts index 9999b37ec8..26d95f56d1 100644 --- a/packages/core/test/specs/data_sources/serialization.ts +++ b/packages/core/test/specs/data_sources/serialization.ts @@ -6,6 +6,7 @@ import EditorModel from '../../../src/editor/model/Editor'; import { ProjectData } from '../../../src/storage_manager'; import { DataSourceProps } from '../../../src/data_sources/types'; import { filterObjectForSnapshot, setupTestEditor } from '../../common'; +import { dynamicAttrKey } from '../../../src/dom_components/model/Component'; describe('DataSource Serialization', () => { let editor: Editor; @@ -143,8 +144,8 @@ describe('DataSource Serialization', () => { const page = projectData.pages[0]; const frame = page.frames[0]; const component = frame.component.components[0]; - expect(component).toHaveProperty('attributes-dynamic-value'); - expect(component['attributes-dynamic-value']).toEqual({ + expect(component).toHaveProperty(dynamicAttrKey); + expect(component[dynamicAttrKey]).toEqual({ value: dataVariable, }); expect(component.attributes).toEqual({ @@ -297,7 +298,7 @@ describe('DataSource Serialization', () => { attributes: { value: 'default', }, - 'attributes-dynamic-value': { + [dynamicAttrKey]: { value: { path: 'test-input.id1.value', type: 'data-variable', From 82ee3eb99bd59c11f1d1de068a9c07d1316052c4 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 12:15:16 +0200 Subject: [PATCH 39/48] Allow objects as trait values ( object other than dynamic values ) --- packages/core/src/data_sources/model/utils.ts | 4 ++++ .../core/src/domain_abstract/model/StyleableModel.ts | 10 +++------- packages/core/src/trait_manager/model/Trait.ts | 7 +++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index e6a8ba736e..09fd2202b8 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -2,6 +2,10 @@ import EditorModel from '../../editor/model/Editor'; import { ConditionalVariableType } from './conditional_variables/DataCondition'; import DataVariable, { DataVariableType } from './DataVariable'; +export function isDynamicValue(value: any) { + return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value.type); +} + export function isDataVariable(variable: any) { return variable?.type === DataVariableType; } diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index e23f3e14da..13614045f3 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -11,7 +11,7 @@ import CssRuleView from '../../css_composer/view/CssRuleView'; import ComponentView from '../../dom_components/view/ComponentView'; import Frame from '../../canvas/model/Frame'; import { DataCondition, ConditionalVariableType } from '../../data_sources/model/conditional_variables/DataCondition'; - +import { isDynamicValue } from '../../data_sources/model/utils'; export type StyleProps = Record< string, | string @@ -114,7 +114,7 @@ export default class StyleableModel extends Model } const styleValue = newStyle[key]; - if (this.isDynamicValue(styleValue)) { + if (isDynamicValue(styleValue)) { const styleDynamicVariable = this.resolveDynamicValue(styleValue); newStyle[key] = styleDynamicVariable; this.manageDataVariableListener(styleDynamicVariable, key); @@ -143,10 +143,6 @@ export default class StyleableModel extends Model return newStyle; } - private isDynamicValue(styleValue: any) { - return typeof styleValue === 'object' && [DataVariableType, ConditionalVariableType].includes(styleValue.type); - } - private resolveDynamicValue(styleValue: any) { const dynamicType = styleValue.type; let styleDynamicVariable; @@ -216,7 +212,7 @@ export default class StyleableModel extends Model return; } - if (this.isDynamicValue(styleValue)) { + if (isDynamicValue(styleValue)) { const dataVar = this.resolveDynamicValue(styleValue); resolvedStyle[key] = dataVar.getDataValue(); } diff --git a/packages/core/src/trait_manager/model/Trait.ts b/packages/core/src/trait_manager/model/Trait.ts index 60f8def263..ba044cf753 100644 --- a/packages/core/src/trait_manager/model/Trait.ts +++ b/packages/core/src/trait_manager/model/Trait.ts @@ -11,6 +11,7 @@ import Traits from './Traits'; import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import { DataVariableType } from '../../data_sources/model/DataVariable'; import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; +import { isDynamicValue } from '../../data_sources/model/utils'; /** * @property {String} id Trait id, eg. `my-trait-id`. @@ -58,7 +59,7 @@ export default class Trait extends Model { } this.em = em; - if (this.attributes.value && typeof this.attributes.value === 'object') { + if (isDynamicValue(this.attributes.value)) { const dataType = this.attributes.value.type; switch (dataType) { case DataVariableType: @@ -70,9 +71,7 @@ export default class Trait extends Model { break; } default: - throw new Error( - `Invalid data variable type. Expected '${DataVariableType} or ${ConditionalVariableType}', but found '${dataType}'.`, - ); + return; } const dv = this.dynamicVariable.getDataValue(); From a5e36fe33ac55c1d3a0b971d126e5f6eefbf1b07 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 13:12:09 +0200 Subject: [PATCH 40/48] Improve types for conditional and datasource variables --- .../src/data_sources/model/DataVariable.ts | 7 +++- .../data_sources/model/TraitDataVariable.ts | 7 ++-- .../model/conditional_variables/Condition.ts | 9 ++---- .../conditional_variables/DataCondition.ts | 11 +++++-- packages/core/src/data_sources/model/utils.ts | 9 ++++-- packages/core/src/data_sources/types.ts | 5 +-- .../src/dom_components/model/Component.ts | 32 +++++++++---------- .../domain_abstract/model/StyleableModel.ts | 32 ++++++++----------- .../core/src/trait_manager/model/Trait.ts | 4 +-- 9 files changed, 64 insertions(+), 52 deletions(-) diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index c84ef0b3c6..915642ebf3 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -3,6 +3,11 @@ import EditorModel from '../../editor/model/Editor'; import { stringToPath } from '../../utils/mixins'; export const DataVariableType = 'data-variable'; +export type DataVariableDefinition = { + type: typeof DataVariableType; + path: string; + defaultValue?: string; +}; export default class DataVariable extends Model { em?: EditorModel; @@ -15,7 +20,7 @@ export default class DataVariable extends Model { }; } - constructor(attrs: any, options: any) { + constructor(attrs: DataVariableDefinition, options: any) { super(attrs, options); this.em = options.em; this.listenToDataSource(); diff --git a/packages/core/src/data_sources/model/TraitDataVariable.ts b/packages/core/src/data_sources/model/TraitDataVariable.ts index b8aedc1f5a..f213e4b733 100644 --- a/packages/core/src/data_sources/model/TraitDataVariable.ts +++ b/packages/core/src/data_sources/model/TraitDataVariable.ts @@ -1,10 +1,13 @@ -import DataVariable from './DataVariable'; +import DataVariable, { DataVariableDefinition } from './DataVariable'; import Trait from '../../trait_manager/model/Trait'; +import { TraitProperties } from '../../trait_manager/types'; + +export type TraitDataVariableDefinition = TraitProperties & DataVariableDefinition; export default class TraitDataVariable extends DataVariable { trait?: Trait; - constructor(attrs: any, options: any) { + constructor(attrs: TraitDataVariableDefinition, options: any) { super(attrs, options); this.trait = options.trait; } diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/Condition.ts index 9c0005278a..6af9152925 100644 --- a/packages/core/src/data_sources/model/conditional_variables/Condition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/Condition.ts @@ -1,4 +1,4 @@ -import { DataVariableType } from './../DataVariable'; +import { DataVariableDefinition, DataVariableType } from './../DataVariable'; import EditorModel from '../../../editor/model/Editor'; import { evaluateVariable, isDataVariable } from '../utils'; import { Expression, LogicGroup } from './DataCondition'; @@ -68,7 +68,7 @@ export class Condition extends Model { * Extracts all data variables from the condition, including nested ones. */ getDataVariables() { - const variables: { type: typeof DataVariableType }[] = []; + const variables: DataVariableDefinition[] = []; this.extractVariables(this.condition, variables); return variables; } @@ -76,10 +76,7 @@ export class Condition extends Model { /** * Recursively extracts variables from expressions or logic groups. */ - private extractVariables( - condition: boolean | LogicGroup | Expression, - variables: { type: typeof DataVariableType }[], - ): void { + private extractVariables(condition: boolean | LogicGroup | Expression, variables: DataVariableDefinition[]): void { if (this.isExpression(condition)) { if (isDataVariable(condition.left)) variables.push(condition.left); if (isDataVariable(condition.right)) variables.push(condition.right); diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 543335ae51..5630198f17 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -6,7 +6,7 @@ import { LogicalOperation } from './operators/LogicalOperator'; import DynamicVariableListenerManager from '../DataVariableListenerManager'; import EditorModel from '../../../editor/model/Editor'; import { Condition } from './Condition'; -import DataVariable from '../DataVariable'; +import DataVariable, { DataVariableDefinition } from '../DataVariable'; import { evaluateVariable, isDataVariable } from '../utils'; export const ConditionalVariableType = 'conditional-variable'; @@ -21,6 +21,13 @@ export type LogicGroup = { statements: (Expression | LogicGroup | boolean)[]; }; +export type ConditionalVariableDefinition = { + type: typeof ConditionalVariableType; + condition: Expression | LogicGroup | boolean; + ifTrue: any; + ifFalse: any; +}; + export class DataCondition extends Model { lastEvaluationResult: boolean; private condition: Condition; @@ -95,7 +102,7 @@ export class DataCondition extends Model { } getDependentDataVariables() { - const dataVariables = this.condition.getDataVariables(); + const dataVariables: DataVariableDefinition[] = this.condition.getDataVariables(); if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index 09fd2202b8..fe835cb684 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -1,11 +1,16 @@ import EditorModel from '../../editor/model/Editor'; -import { ConditionalVariableType } from './conditional_variables/DataCondition'; +import { DynamicValue, DynamicValueDefinition } from '../types'; +import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition'; import DataVariable, { DataVariableType } from './DataVariable'; -export function isDynamicValue(value: any) { +export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition { return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value.type); } +export function isDynamicValue(value: any): value is DynamicValue { + return value instanceof DataVariable || value instanceof DataCondition; +} + export function isDataVariable(variable: any) { return variable?.type === DataVariableType; } diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index e230e418e2..f1873fd3ec 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -2,10 +2,11 @@ import { ObjectAny } from '../common'; import ComponentDataVariable from './model/ComponentDataVariable'; import DataRecord from './model/DataRecord'; import DataRecords from './model/DataRecords'; -import DataVariable from './model/DataVariable'; -import { DataCondition } from './model/conditional_variables/DataCondition'; +import DataVariable, { DataVariableDefinition } from './model/DataVariable'; +import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition'; export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition; +export type DynamicValueDefinition = DataVariableDefinition | ConditionalVariableDefinition; export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 169024eb48..f2ca1be8f8 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -54,6 +54,8 @@ import { import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import { ConditionalVariableType, DataCondition } from '../../data_sources/model/conditional_variables/DataCondition'; import { DataVariableType } from '../../data_sources/model/DataVariable'; +import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; +import { DynamicValue, DynamicValueDefinition } from '../../data_sources/types'; export interface IComponent extends ExtractMethods {} @@ -772,32 +774,28 @@ export default class Component extends StyleableModel { } } - const attrDataVariable = this.get(dynamicAttrKey); + const attrDataVariable = this.get(dynamicAttrKey) as { + [key: string]: TraitDataVariable | DynamicValueDefinition; + }; if (attrDataVariable) { Object.entries(attrDataVariable).forEach(([key, value]) => { let dataVariable: TraitDataVariable | DataCondition; + if (isDynamicValue(value)) { + dataVariable = value; + } - switch (true) { - case value instanceof DataCondition: - case value instanceof TraitDataVariable: - dataVariable = value; - break; + if (isDynamicValueDefinition(value)) { + const type = value.type; - case (value as any).type === ConditionalVariableType: { - const { condition, ifTrue, ifFalse } = value as any; + if (type === ConditionalVariableType) { + const { condition, ifTrue, ifFalse } = value; dataVariable = new DataCondition(condition, ifTrue, ifFalse, { em }); - break; - } - - case (value as any).type === DataVariableType: + } else { dataVariable = new TraitDataVariable(value, { em }); - break; - - default: - throw new Error(`Unexpected data type for key: ${key}`); + } } - attributes[key] = dataVariable.getDataValue(); + attributes[key] = dataVariable!.getDataValue(); }); } diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 13614045f3..4e36417281 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -5,23 +5,19 @@ import Selectors from '../../selector_manager/model/Selectors'; import { shallowDiff } from '../../utils/mixins'; import EditorModel from '../../editor/model/Editor'; import StyleDataVariable from '../../data_sources/model/StyleDataVariable'; -import { DataVariableType } from '../../data_sources/model/DataVariable'; +import { DataVariableDefinition, DataVariableType } from '../../data_sources/model/DataVariable'; import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; import CssRuleView from '../../css_composer/view/CssRuleView'; import ComponentView from '../../dom_components/view/ComponentView'; import Frame from '../../canvas/model/Frame'; -import { DataCondition, ConditionalVariableType } from '../../data_sources/model/conditional_variables/DataCondition'; -import { isDynamicValue } from '../../data_sources/model/utils'; -export type StyleProps = Record< - string, - | string - | string[] - | { - type: typeof DataVariableType; - defaultValue: string; - path: string; - } ->; +import { + DataCondition, + ConditionalVariableType, + ConditionalVariableDefinition, +} from '../../data_sources/model/conditional_variables/DataCondition'; +import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; +import { DynamicValueDefinition } from '../../data_sources/types'; +export type StyleProps = Record; export type UpdateStyleOptions = SetOptions & { partial?: boolean; @@ -114,7 +110,7 @@ export default class StyleableModel extends Model } const styleValue = newStyle[key]; - if (isDynamicValue(styleValue)) { + if (isDynamicValueDefinition(styleValue)) { const styleDynamicVariable = this.resolveDynamicValue(styleValue); newStyle[key] = styleDynamicVariable; this.manageDataVariableListener(styleDynamicVariable, key); @@ -143,7 +139,7 @@ export default class StyleableModel extends Model return newStyle; } - private resolveDynamicValue(styleValue: any) { + private resolveDynamicValue(styleValue: DynamicValueDefinition) { const dynamicType = styleValue.type; let styleDynamicVariable; switch (dynamicType) { @@ -157,7 +153,7 @@ export default class StyleableModel extends Model } default: throw new Error( - `Invalid data variable type. Expected '${DataVariableType}' or '${ConditionalVariableType}', but found '${dynamicType}'.`, + `Unsupported dynamic value type for styles. Only '${DataVariableType}' and '${ConditionalVariableType}' are supported. Received '${dynamicType}'.`, ); } @@ -212,12 +208,12 @@ export default class StyleableModel extends Model return; } - if (isDynamicValue(styleValue)) { + if (isDynamicValueDefinition(styleValue)) { const dataVar = this.resolveDynamicValue(styleValue); resolvedStyle[key] = dataVar.getDataValue(); } - if (styleValue instanceof StyleDataVariable || styleValue instanceof DataCondition) { + if (isDynamicValue(styleValue)) { resolvedStyle[key] = styleValue.getDataValue(); } }); diff --git a/packages/core/src/trait_manager/model/Trait.ts b/packages/core/src/trait_manager/model/Trait.ts index ba044cf753..185bbce1df 100644 --- a/packages/core/src/trait_manager/model/Trait.ts +++ b/packages/core/src/trait_manager/model/Trait.ts @@ -11,7 +11,7 @@ import Traits from './Traits'; import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import { DataVariableType } from '../../data_sources/model/DataVariable'; import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; -import { isDynamicValue } from '../../data_sources/model/utils'; +import { isDynamicValueDefinition } from '../../data_sources/model/utils'; /** * @property {String} id Trait id, eg. `my-trait-id`. @@ -59,7 +59,7 @@ export default class Trait extends Model { } this.em = em; - if (isDynamicValue(this.attributes.value)) { + if (isDynamicValueDefinition(this.attributes.value)) { const dataType = this.attributes.value.type; switch (dataType) { case DataVariableType: From 3bff47310f7d6a36b877283e9d55fb16c1345f00 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 13:37:03 +0200 Subject: [PATCH 41/48] Add unit test for objects as trait value --- .../src/dom_components/model/Component.ts | 4 +--- .../ConditionalTraits.ts | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index f2ca1be8f8..4553a0a2d3 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -782,9 +782,7 @@ export default class Component extends StyleableModel { let dataVariable: TraitDataVariable | DataCondition; if (isDynamicValue(value)) { dataVariable = value; - } - - if (isDynamicValueDefinition(value)) { + } else if (isDynamicValueDefinition(value)) { const type = value.type; if (type === ConditionalVariableType) { diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts index 393ed4d9a0..d1467e51db 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -172,4 +172,25 @@ describe('TraitConditionalVariable', () => { dynamicTrait: conditionalTrait, }); }); + + it('should handle objects as traits (other than dynamic values)', () => { + const traitValue = { + type: 'UNKNOWN_TYPE', + condition: "This's not a condition", + value: 'random value', + }; + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'some trait', + value: traitValue, + }, + ], + })[0]; + expect(component.getTrait('some trait').get('value')).toEqual(traitValue); + }); }); From 123d8bffb05d4b0b2d933d9002cd99b4d254b06c Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 14:48:55 +0200 Subject: [PATCH 42/48] Remove persisting of components in conditional components --- .../ConditionalComponent.ts | 35 +------ .../ComponentConditionalVariable.ts | 95 +++---------------- 2 files changed, 13 insertions(+), 117 deletions(-) diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts index 51fcae5253..ccf4142b93 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts @@ -27,44 +27,13 @@ export default class ComponentConditionalVariable extends Component { this.componentDefinition = componentDefinition; this.dataCondition = dataConditionInstance; this.dataCondition.onValueChange = this.handleConditionChange.bind(this); - this.refreshComponentState(); } private handleConditionChange() { this.dataCondition.reevaluate(); const updatedComponents = this.dataCondition.getDataValue(); - if (updatedComponents instanceof Components) { - const componentsArray = updatedComponents.map((cmp) => cmp); - this.components().set(componentsArray); - } else { - this.components().reset(); - this.components().add(updatedComponents); - } - - this.refreshComponentState(); - } - - private refreshComponentState() { - if (this.dataCondition.lastEvaluationResult) { - this.assignComponents({ positiveCaseComponents: this.components() }); - } else { - this.assignComponents({ negativeCaseComponents: this.components() }); - } - } - - private assignComponents({ - positiveCaseComponents, - negativeCaseComponents, - }: { - positiveCaseComponents?: Components; - negativeCaseComponents?: Components; - }) { - if (positiveCaseComponents) { - this.dataCondition.ifTrue = positiveCaseComponents; - } - if (negativeCaseComponents) { - this.dataCondition.ifFalse = negativeCaseComponents; - } + this.components().reset(); + this.components().add(updatedComponents); } static isComponent(el: HTMLElement) { diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index b792212fec..9ba5163658 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -95,12 +95,23 @@ describe('ComponentConditionalVariable', () => { type: 'text', content: 'Some value', }, + ifFalse: { + tagName: 'h1', + type: 'text', + content: 'False value', + }, })[0]; const childComponent = component.components().at(0); expect(childComponent).toBeDefined(); expect(childComponent.get('type')).toBe('text'); expect(childComponent.getInnerHTML()).toBe('Some value'); + + /* Test changing datasources */ + dsm.get('ds1').getRecord('left_id')?.set('left', 'Diffirent value'); + expect(component.components().at(0).getInnerHTML()).toBe('False value'); + dsm.get('ds1').getRecord('left_id')?.set('left', 'Name1'); + expect(component.components().at(0).getInnerHTML()).toBe('Some value'); }); it('should test a conditional component with a child that is also a conditional component', () => { @@ -159,90 +170,6 @@ describe('ComponentConditionalVariable', () => { expect(innerComponent.getInnerHTML()).toBe('

Some child value

'); }); - it('should test component variable with changing value of data-source', () => { - const dataSource: DataSourceProps = { - id: 'ds1', - records: [ - { id: 'left_id', left: 'Name1' }, - { id: 'right_id', right: 'Name1' }, - ], - }; - dsm.add(dataSource); - - const component = cmpRoot.append({ - type: ConditionalVariableType, - condition: { - left: { - type: DataVariableType, - path: 'ds1.left_id.left', - }, - operator: GenericOperation.equals, - right: { - type: DataVariableType, - path: 'ds1.right_id.right', - }, - }, - ifTrue: { - tagName: 'h1', - type: 'text', - content: 'True value', - }, - ifFalse: { - tagName: 'h1', - type: 'text', - content: 'False value', - }, - })[0]; - dsm.get('ds1').getRecord('left_id')?.set('left', 'Diffirent value'); - - const childComponent = component.components().at(0); - expect(childComponent).toBeDefined(); - expect(childComponent.get('type')).toBe('text'); - expect(childComponent.getInnerHTML()).toBe('False value'); - }); - - it('should change with changing value of data-source', () => { - const dataSource: DataSourceProps = { - id: 'ds1', - records: [ - { id: 'left_id', left: 'Name1' }, - { id: 'right_id', right: 'Name1' }, - ], - }; - dsm.add(dataSource); - - const component = cmpRoot.append({ - type: ConditionalVariableType, - condition: { - left: { - type: DataVariableType, - path: 'ds1.left_id.left', - }, - operator: GenericOperation.equals, - right: { - type: DataVariableType, - path: 'ds1.right_id.right', - }, - }, - ifTrue: { - tagName: 'h1', - type: 'text', - content: 'True value', - }, - ifFalse: { - tagName: 'h1', - type: 'text', - content: 'False value', - }, - })[0]; - dsm.get('ds1').getRecord('left_id')?.set('left', 'Diffirent value'); - - const childComponent = component.components().at(0); - expect(childComponent).toBeDefined(); - expect(childComponent.get('type')).toBe('text'); - expect(childComponent.getInnerHTML()).toBe('False value'); - }); - it('should store conditional components', () => { const conditionalCmptDef = { type: ConditionalVariableType, From ee7c8b698a578d92f18f6c5526699a2c4b6a9de8 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 16:04:43 +0200 Subject: [PATCH 43/48] Add view tests to conditional components --- .../ComponentConditionalVariable.ts | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 9ba5163658..efa95e1c35 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -1,11 +1,14 @@ -import { DataSourceManager, Editor } from '../../../../../src'; +import { Component, DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { DataSourceProps } from '../../../../../src/data_sources/types'; +import ConditionalComponentView from '../../../../../src/data_sources/view/ComponentDynamicView'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; +import ComponentTableView from '../../../../../src/dom_components/view/ComponentTableView'; +import ComponentTextView from '../../../../../src/dom_components/view/ComponentTextView'; import EditorModel from '../../../../../src/editor/model/Editor'; import { setupTestEditor } from '../../../../common'; @@ -40,11 +43,17 @@ describe('ComponentConditionalVariable', () => { expect(component).toBeDefined(); expect(component.get('type')).toBe(ConditionalVariableType); expect(component.getInnerHTML()).toBe('

some text

'); + const componentView = component.getView(); + expect(componentView).toBeInstanceOf(ConditionalComponentView); + expect(componentView?.el.textContent).toBe('some text'); - const childComponent = component.components().at(0); + const childComponent = getFirstChild(component); + const childView = getFirstChildView(component); expect(childComponent).toBeDefined(); expect(childComponent.get('type')).toBe('text'); expect(childComponent.getInnerHTML()).toBe('some text'); + expect(childView).toBeInstanceOf(ComponentTextView); + expect(childView?.el.innerHTML).toBe('some text'); }); it('should add a component with a condition that evaluates a string', () => { @@ -60,11 +69,17 @@ describe('ComponentConditionalVariable', () => { expect(component).toBeDefined(); expect(component.get('type')).toBe(ConditionalVariableType); expect(component.getInnerHTML()).toBe('

some text

'); + const componentView = component.getView(); + expect(componentView).toBeInstanceOf(ConditionalComponentView); + expect(componentView?.el.textContent).toBe('some text'); - const childComponent = component.components().at(0); + const childComponent = getFirstChild(component); + const childView = getFirstChildView(component); expect(childComponent).toBeDefined(); expect(childComponent.get('type')).toBe('text'); expect(childComponent.getInnerHTML()).toBe('some text'); + expect(childView).toBeInstanceOf(ComponentTextView); + expect(childView?.el.innerHTML).toBe('some text'); }); it('should test component variable with data-source', () => { @@ -102,16 +117,18 @@ describe('ComponentConditionalVariable', () => { }, })[0]; - const childComponent = component.components().at(0); + const childComponent = getFirstChild(component); expect(childComponent).toBeDefined(); expect(childComponent.get('type')).toBe('text'); expect(childComponent.getInnerHTML()).toBe('Some value'); /* Test changing datasources */ - dsm.get('ds1').getRecord('left_id')?.set('left', 'Diffirent value'); - expect(component.components().at(0).getInnerHTML()).toBe('False value'); - dsm.get('ds1').getRecord('left_id')?.set('left', 'Name1'); - expect(component.components().at(0).getInnerHTML()).toBe('Some value'); + updatedsmLeftValue(dsm, 'Diffirent value'); + expect(getFirstChild(component).getInnerHTML()).toBe('False value'); + expect(getFirstChildView(component)?.el.innerHTML).toBe('False value'); + updatedsmLeftValue(dsm, 'Name1'); + expect(getFirstChild(component).getInnerHTML()).toBe('Some value'); + expect(getFirstChildView(component)?.el.innerHTML).toBe('Some value'); }); it('should test a conditional component with a child that is also a conditional component', () => { @@ -154,20 +171,20 @@ describe('ComponentConditionalVariable', () => { }, }, ifTrue: { - tagName: 'h1', - type: 'text', - content: 'Some child value', + tagName: 'table', + type: 'table', }, }, ], }, })[0]; - const childComponent = component.components().at(0); - const innerComponent = childComponent.components().at(0); - expect(innerComponent).toBeDefined(); - expect(innerComponent.get('type')).toBe(ConditionalVariableType); - expect(innerComponent.getInnerHTML()).toBe('

Some child value

'); + const innerComponent = getFirstChild(getFirstChild(component)); + const innerComponentView = getFirstChildView(innerComponent); + const innerHTML = '
'; + expect(innerComponent.getInnerHTML()).toBe(innerHTML); + expect(innerComponentView).toBeInstanceOf(ComponentTableView); + expect(innerComponentView?.el.tagName).toBe('TABLE'); }); it('should store conditional components', () => { @@ -211,3 +228,15 @@ describe('ComponentConditionalVariable', () => { }).toThrow(MissingConditionError); }); }); + +function updatedsmLeftValue(dsm: DataSourceManager, newValue: string) { + dsm.get('ds1').getRecord('left_id')?.set('left', newValue); +} + +function getFirstChildView(component: Component) { + return getFirstChild(component).getView(); +} + +function getFirstChild(component: Component) { + return component.components().at(0); +} From ee44c1a0b954aa9151e422abba96f4d4459cd465 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 16:12:43 +0200 Subject: [PATCH 44/48] Add attribute model check for conditional traits --- .../ConditionalTraits.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts index d1467e51db..64895e7919 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -47,6 +47,7 @@ describe('TraitConditionalVariable', () => { expect(component).toBeDefined(); expect(component.getTrait('title').get('value')).toBe('Some title'); + expect(component.getAttributes().title).toBe('Some title'); }); it('should add a trait with a data-source condition', () => { @@ -62,7 +63,7 @@ describe('TraitConditionalVariable', () => { traits: [ { type: 'text', - name: 'value', + name: 'title', value: { type: ConditionalVariableType, condition: { @@ -80,8 +81,9 @@ describe('TraitConditionalVariable', () => { ], })[0]; - const traitValue = component.getTrait('value').get('value'); + const traitValue = component.getTrait('title').get('value'); expect(traitValue).toBe('Valid name'); + expect(component.getAttributes().title).toBe('Valid name'); }); it('should change trait value with changing data-source value', () => { @@ -97,7 +99,7 @@ describe('TraitConditionalVariable', () => { traits: [ { type: 'text', - name: 'value', + name: 'title', value: { type: ConditionalVariableType, condition: { @@ -115,12 +117,16 @@ describe('TraitConditionalVariable', () => { ], })[0]; - const traitValueBefore = component.getTrait('value').get('value'); + const traitValueBefore = component.getTrait('title').get('value'); expect(traitValueBefore).toBe('Correct name'); + const titleBefore = component.getAttributes().title; + expect(titleBefore).toBe('Correct name'); dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name'); - const traitValueAfter = component.getTrait('value').get('value'); + const traitValueAfter = component.getTrait('title').get('value'); expect(traitValueAfter).toBe('Incorrect name'); + const titleAfter = component.getAttributes().title; + expect(titleAfter).toBe('Incorrect name'); }); it('should throw an error if no condition is passed in trait', () => { @@ -186,11 +192,12 @@ describe('TraitConditionalVariable', () => { traits: [ { type: 'text', - name: 'some trait', + name: 'title', value: traitValue, }, ], })[0]; - expect(component.getTrait('some trait').get('value')).toEqual(traitValue); + expect(component.getTrait('title').get('value')).toEqual(traitValue); + expect(component.getAttributes().title).toEqual(traitValue); }); }); From 4fa0625fe62c117c31392e8f716496413b29e93f Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 16:15:34 +0200 Subject: [PATCH 45/48] Check the view for conditional traits --- .../model/conditional_variables/ConditionalTraits.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts index 64895e7919..f94a808bf1 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -84,6 +84,7 @@ describe('TraitConditionalVariable', () => { const traitValue = component.getTrait('title').get('value'); expect(traitValue).toBe('Valid name'); expect(component.getAttributes().title).toBe('Valid name'); + expect(component.getView()?.el.getAttribute('title')).toBe('Valid name'); }); it('should change trait value with changing data-source value', () => { @@ -121,12 +122,14 @@ describe('TraitConditionalVariable', () => { expect(traitValueBefore).toBe('Correct name'); const titleBefore = component.getAttributes().title; expect(titleBefore).toBe('Correct name'); + expect(component.getView()?.el.getAttribute('title')).toBe('Correct name'); dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name'); const traitValueAfter = component.getTrait('title').get('value'); expect(traitValueAfter).toBe('Incorrect name'); const titleAfter = component.getAttributes().title; expect(titleAfter).toBe('Incorrect name'); + expect(component.getView()?.el.getAttribute('title')).toBe('Incorrect name'); }); it('should throw an error if no condition is passed in trait', () => { From ccd2f4ce8b80033b96c0451bb55271779fd2521a Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 16:31:47 +0200 Subject: [PATCH 46/48] Refactor conditional traits --- .../ConditionalTraits.ts | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts index f94a808bf1..38e080b2d1 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -5,7 +5,7 @@ import { ConditionalVariableType } from '../../../../../src/data_sources/model/c import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { DataSourceProps } from '../../../../../src/data_sources/types'; -import { dynamicAttrKey } from '../../../../../src/dom_components/model/Component'; +import Component, { dynamicAttrKey } from '../../../../../src/dom_components/model/Component'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import EditorModel from '../../../../../src/editor/model/Editor'; import { setupTestEditor } from '../../../../common'; @@ -45,9 +45,7 @@ describe('TraitConditionalVariable', () => { ], })[0]; - expect(component).toBeDefined(); - expect(component.getTrait('title').get('value')).toBe('Some title'); - expect(component.getAttributes().title).toBe('Some title'); + testComponentAttr(component, 'title', 'Some title'); }); it('should add a trait with a data-source condition', () => { @@ -81,10 +79,7 @@ describe('TraitConditionalVariable', () => { ], })[0]; - const traitValue = component.getTrait('title').get('value'); - expect(traitValue).toBe('Valid name'); - expect(component.getAttributes().title).toBe('Valid name'); - expect(component.getView()?.el.getAttribute('title')).toBe('Valid name'); + testComponentAttr(component, 'title', 'Valid name'); }); it('should change trait value with changing data-source value', () => { @@ -118,18 +113,9 @@ describe('TraitConditionalVariable', () => { ], })[0]; - const traitValueBefore = component.getTrait('title').get('value'); - expect(traitValueBefore).toBe('Correct name'); - const titleBefore = component.getAttributes().title; - expect(titleBefore).toBe('Correct name'); - expect(component.getView()?.el.getAttribute('title')).toBe('Correct name'); - + testComponentAttr(component, 'title', 'Correct name'); dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name'); - const traitValueAfter = component.getTrait('title').get('value'); - expect(traitValueAfter).toBe('Incorrect name'); - const titleAfter = component.getAttributes().title; - expect(titleAfter).toBe('Incorrect name'); - expect(component.getView()?.el.getAttribute('title')).toBe('Incorrect name'); + testComponentAttr(component, 'title', 'Incorrect name'); }); it('should throw an error if no condition is passed in trait', () => { @@ -204,3 +190,10 @@ describe('TraitConditionalVariable', () => { expect(component.getAttributes().title).toEqual(traitValue); }); }); + +function testComponentAttr(component: Component, trait: string, value: string) { + expect(component).toBeDefined(); + expect(component.getTrait(trait).get('value')).toBe(value); + expect(component.getAttributes()[trait]).toBe(value); + expect(component.getView()?.el.getAttribute(trait)).toBe(value); +} From 48de30d8fa197d73759eecf3a8a8b139ec80f6ba Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 1 Nov 2024 17:06:49 +0200 Subject: [PATCH 47/48] Add test for conditional properties ( traits with `changeProp:true` ) --- .../src/dom_components/model/Component.ts | 3 +- .../ConditionalTraits.ts | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 4553a0a2d3..fe93d6931d 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -53,9 +53,8 @@ import { } from './SymbolUtils'; import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import { ConditionalVariableType, DataCondition } from '../../data_sources/model/conditional_variables/DataCondition'; -import { DataVariableType } from '../../data_sources/model/DataVariable'; import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; -import { DynamicValue, DynamicValueDefinition } from '../../data_sources/types'; +import { DynamicValueDefinition } from '../../data_sources/types'; export interface IComponent extends ExtractMethods {} diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts index 38e080b2d1..d0c713bb13 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -168,6 +168,47 @@ describe('TraitConditionalVariable', () => { }); }); + it('should be property on the component with `changeProp:true`', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [{ id: 'left_id', left: 'Name1' }], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'title', + changeProp: true, + value: { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: 'Name1', + }, + ifTrue: 'Correct name', + ifFalse: 'Incorrect name', + }, + }, + ], + })[0]; + + // TODO: make dynamic values not to change the attributes if `changeProp:true` + // expect(component.getView()?.el.getAttribute('title')).toBeNull(); + expect(component.get('title')).toBe('Correct name'); + + dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name'); + // expect(component.getView()?.el.getAttribute('title')).toBeNull(); + expect(component.get('title')).toBe('Incorrect name'); + }); + it('should handle objects as traits (other than dynamic values)', () => { const traitValue = { type: 'UNKNOWN_TYPE', From 1bc4baa8206e6ba4246554a4c71fbbcb94134e6a Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 4 Nov 2024 10:09:39 +0200 Subject: [PATCH 48/48] Add test for loading conditional traits --- .../ConditionalTraits.ts | 45 +++++++++++++- .../__snapshots__/ConditionalTraits.ts.snap | 59 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts index d0c713bb13..8e109aac6c 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -8,7 +8,7 @@ import { DataSourceProps } from '../../../../../src/data_sources/types'; import Component, { dynamicAttrKey } from '../../../../../src/dom_components/model/Component'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import EditorModel from '../../../../../src/editor/model/Editor'; -import { setupTestEditor } from '../../../../common'; +import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; describe('TraitConditionalVariable', () => { let editor: Editor; @@ -159,6 +159,8 @@ describe('TraitConditionalVariable', () => { })[0]; const projectData = editor.getProjectData(); + const snapshot = filterObjectForSnapshot(projectData); + expect(snapshot).toMatchSnapshot(``); const page = projectData.pages[0]; const frame = page.frames[0]; const storedComponent = frame.component.components[0]; @@ -168,6 +170,47 @@ describe('TraitConditionalVariable', () => { }); }); + it('should load traits with conditional values correctly', () => { + const projectData = { + pages: [ + { + frames: [ + { + component: { + components: [ + { + attributes: { + dynamicTrait: 'Default', + }, + [dynamicAttrKey]: { + dynamicTrait: { + condition: { + left: 0, + operator: '>', + right: -1, + }, + ifTrue: 'Positive', + type: 'conditional-variable', + }, + }, + type: 'text', + }, + ], + type: 'wrapper', + }, + }, + ], + type: 'main', + }, + ], + }; + + editor.loadProjectData(projectData); + const components = editor.getComponents(); + const component = components.models[0]; + expect(component.getAttributes()).toEqual({ dynamicTrait: 'Positive' }); + }); + it('should be property on the component with `changeProp:true`', () => { const dataSource: DataSourceProps = { id: 'ds1', diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap b/packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap new file mode 100644 index 0000000000..34066fa8c1 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TraitConditionalVariable should store traits with conditional values correctly 1`] = ` +{ + "assets": [], + "dataSources": [], + "pages": [ + { + "frames": [ + { + "component": { + "components": [ + { + "attributes": { + "dynamicTrait": "Positive", + }, + "attributes-dynamic-value": { + "dynamicTrait": { + "condition": { + "left": 0, + "operator": ">", + "right": -1, + }, + "ifTrue": "Positive", + "type": "conditional-variable", + }, + }, + "tagName": "h1", + "type": "text", + }, + ], + "docEl": { + "tagName": "html", + }, + "head": { + "type": "head", + }, + "stylable": [ + "background", + "background-color", + "background-image", + "background-repeat", + "background-attachment", + "background-position", + "background-size", + ], + "type": "wrapper", + }, + "id": "data-variable-id", + }, + ], + "id": "data-variable-id", + "type": "main", + }, + ], + "styles": [], + "symbols": [], +} +`;