diff --git a/packages/core/src/data_sources/model/ComponentDataVariable.ts b/packages/core/src/data_sources/model/ComponentDataVariable.ts index 2c980d48f..504956c40 100644 --- a/packages/core/src/data_sources/model/ComponentDataVariable.ts +++ b/packages/core/src/data_sources/model/ComponentDataVariable.ts @@ -13,6 +13,7 @@ export default class ComponentDataVariable extends Component { type: DataVariableType, path: '', defaultValue: '', + droppable: false, }; } @@ -22,6 +23,14 @@ export default class ComponentDataVariable extends Component { this.dataResolver = new DataVariable({ type, path, defaultValue }, opt); } + getPath() { + return this.dataResolver.get('path'); + } + + getDefaultValue() { + return this.dataResolver.get('defaultValue'); + } + getDataValue() { return this.dataResolver.getDataValue(); } @@ -30,6 +39,14 @@ export default class ComponentDataVariable extends Component { return this.getDataValue(); } + setPath(newPath: string) { + this.dataResolver.set('path', newPath); + } + + setDefaultValue(newValue: string) { + this.dataResolver.set('defaultValue', newValue); + } + static isComponent(el: HTMLElement) { return toLowerCase(el.tagName) === DataVariableType; } diff --git a/packages/core/src/data_sources/model/DataResolverListener.ts b/packages/core/src/data_sources/model/DataResolverListener.ts index 7e7d7a4b4..0b13f8a43 100644 --- a/packages/core/src/data_sources/model/DataResolverListener.ts +++ b/packages/core/src/data_sources/model/DataResolverListener.ts @@ -14,8 +14,12 @@ export interface DataResolverListenerProps { onUpdate: (value: any) => void; } +interface ListenerWithCallback extends DataSourceListener { + callback: () => void; +} + export default class DataResolverListener { - private listeners: DataSourceListener[] = []; + private listeners: ListenerWithCallback[] = []; private em: EditorModel; private onUpdate: (value: any) => void; private model = new Model(); @@ -33,10 +37,14 @@ export default class DataResolverListener { this.onUpdate(value); }; + private createListener(obj: any, event: string, callback: () => void = this.onChange): ListenerWithCallback { + return { obj, event, callback }; + } + listenToResolver() { const { resolver, model } = this; this.removeListeners(); - let listeners: DataSourceListener[] = []; + let listeners: ListenerWithCallback[] = []; const type = resolver.attributes.type; switch (type) { @@ -51,11 +59,11 @@ export default class DataResolverListener { break; } - listeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange)); + listeners.forEach((ls) => model.listenTo(ls.obj, ls.event, ls.callback)); this.listeners = listeners; } - private listenToConditionalVariable(dataVariable: DataCondition) { + private listenToConditionalVariable(dataVariable: DataCondition): ListenerWithCallback[] { const { em } = this; const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { return this.listenToDataVariable(new DataVariable(dataVariable, { em })); @@ -64,29 +72,41 @@ export default class DataResolverListener { return dataListeners; } - private listenToDataVariable(dataVariable: DataVariable) { + private listenToDataVariable(dataVariable: DataVariable): ListenerWithCallback[] { const { em } = this; - const dataListeners: DataSourceListener[] = []; const { path } = dataVariable.attributes; const normPath = stringToPath(path || '').join('.'); const [ds, dr] = em.DataSources.fromPath(path!); - ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); - dr && dataListeners.push({ obj: dr, event: 'change' }); + + const dataListeners: ListenerWithCallback[] = []; + + if (ds) { + dataListeners.push(this.createListener(ds.records, 'add remove reset')); + } + + if (dr) { + dataListeners.push(this.createListener(dr, 'change')); + } + dataListeners.push( - { obj: dataVariable, event: 'change:path change:defaultValue' }, - { obj: em.DataSources.all, event: 'add remove reset' }, - { obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, + this.createListener(dataVariable, 'change:path', () => { + this.listenToResolver(); + this.onChange(); + }), + this.createListener(dataVariable, 'change:defaultValue'), + this.createListener(em.DataSources.all, 'add remove reset'), + this.createListener(em, `${DataSourcesEvents.path}:${normPath}`), ); return dataListeners; } - private listenToDataCollectionVariable(dataVariable: DataCollectionVariable) { - return [{ obj: dataVariable, event: 'change:value' }]; + private listenToDataCollectionVariable(dataVariable: DataCollectionVariable): ListenerWithCallback[] { + return [this.createListener(dataVariable, 'change:value')]; } private removeListeners() { - this.listeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange)); + this.listeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, ls.callback)); this.listeners = []; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts index a2a9e19ca..755262883 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts @@ -2,18 +2,20 @@ import Component from '../../../dom_components/model/Component'; import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition'; +import { ConditionProps } from './DataConditionEvaluator'; export default class ComponentDataCondition extends Component { dataResolver: DataCondition; constructor(props: DataConditionProps, opt: ComponentOptions) { - const { condition, ifTrue, ifFalse } = props; - const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); + const dataConditionInstance = new DataCondition(props, { em: opt.em }); + super( { ...props, type: DataConditionType, components: dataConditionInstance.getDataValue(), + droppable: false, }, opt, ); @@ -21,8 +23,19 @@ export default class ComponentDataCondition extends Component { this.dataResolver.onValueChange = this.handleConditionChange.bind(this); } + getCondition() { + return this.dataResolver.getCondition(); + } + + getIfTrue() { + return this.dataResolver.getIfTrue(); + } + + getIfFalse() { + return this.dataResolver.getIfFalse(); + } + private handleConditionChange() { - this.dataResolver.reevaluate(); this.components(this.dataResolver.getDataValue()); } @@ -30,6 +43,18 @@ export default class ComponentDataCondition extends Component { return toLowerCase(el.tagName) === DataConditionType; } + setCondition(newCondition: ConditionProps) { + this.dataResolver.setCondition(newCondition); + } + + setIfTrue(newIfTrue: any) { + this.dataResolver.setIfTrue(newIfTrue); + } + + setIfFalse(newIfFalse: any) { + this.dataResolver.setIfFalse(newIfFalse); + } + toJSON(): ComponentDefinition { return this.dataResolver.toJSON(); } 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 93fd786b9..812275fa4 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts @@ -1,9 +1,10 @@ -import { Operator } from './operators'; +import { Operator } from './operators/BaseOperator'; +import { DataConditionOperation } from './operators/types'; export class ConditionStatement { constructor( private leftValue: any, - private operator: Operator, + private operator: Operator, private rightValue: any, ) {} 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 0d46e8dda..fc72ef2f0 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -3,22 +3,23 @@ import EditorModel from '../../../editor/model/Editor'; import DataVariable, { DataVariableProps } from '../DataVariable'; import DataResolverListener from '../DataResolverListener'; import { evaluateVariable, isDataVariable } from '../utils'; -import { Condition, ConditionProps } from './Condition'; -import { GenericOperation } from './operators/GenericOperator'; -import { LogicalOperation } from './operators/LogicalOperator'; +import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator'; +import { AnyTypeOperation } from './operators/AnyTypeOperator'; +import { BooleanOperation } from './operators/BooleanOperator'; import { NumberOperation } from './operators/NumberOperator'; -import { StringOperation } from './operators/StringOperations'; +import { StringOperation } from './operators/StringOperator'; +import { isUndefined } from 'underscore'; export const DataConditionType = 'data-condition'; export interface ExpressionProps { left: any; - operator: GenericOperation | StringOperation | NumberOperation; + operator: AnyTypeOperation | StringOperation | NumberOperation; right: any; } export interface LogicGroupProps { - logicalOperator: LogicalOperation; + logicalOperator: BooleanOperation; statements: ConditionProps[]; } @@ -30,57 +31,90 @@ export interface DataConditionProps { } interface DataConditionPropsDefined extends Omit { - condition: Condition; + condition: DataConditionEvaluator; } export class DataCondition extends Model { - lastEvaluationResult: boolean; private em: EditorModel; private resolverListeners: DataResolverListener[] = []; private _onValueChange?: () => void; constructor( - condition: ConditionProps, - public ifTrue: any, - public ifFalse: any, + props: { + condition: ConditionProps; + ifTrue: any; + ifFalse: any; + }, opts: { em: EditorModel; onValueChange?: () => void }, ) { - if (typeof condition === 'undefined') { - throw new MissingConditionError(); + if (isUndefined(props.condition)) { + opts.em.logError('No condition was provided to a conditional component.'); } - const conditionInstance = new Condition(condition, { em: opts.em }); + const conditionInstance = new DataConditionEvaluator({ condition: props.condition }, { em: opts.em }); + super({ type: DataConditionType, + ...props, condition: conditionInstance, - ifTrue, - ifFalse, }); this.em = opts.em; - this.lastEvaluationResult = this.evaluate(); this.listenToDataVariables(); this._onValueChange = opts.onValueChange; + + this.on('change:condition change:ifTrue change:ifFalse', () => { + this.listenToDataVariables(); + this._onValueChange?.(); + }); } - get condition() { + private get conditionEvaluator() { return this.get('condition')!; } - evaluate() { - return this.condition.evaluate(); + getCondition(): ConditionProps { + return this.get('condition')?.get('condition')!; + } + + getIfTrue() { + return this.get('ifTrue')!; + } + + getIfFalse() { + return this.get('ifFalse')!; } - getDataValue(): any { - return this.lastEvaluationResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em); + isTrue(): boolean { + return this.conditionEvaluator.evaluate(); } - reevaluate(): void { - this.lastEvaluationResult = this.evaluate(); + getDataValue(skipDynamicValueResolution: boolean = false): any { + const ifTrue = this.get('ifTrue'); + const ifFalse = this.get('ifFalse'); + + const isConditionTrue = this.isTrue(); + if (skipDynamicValueResolution) { + return isConditionTrue ? ifTrue : ifFalse; + } + + return isConditionTrue ? evaluateVariable(ifTrue, this.em) : evaluateVariable(ifFalse, this.em); } set onValueChange(newFunction: () => void) { this._onValueChange = newFunction; - this.listenToDataVariables(); + } + + setCondition(newCondition: ConditionProps) { + const newConditionInstance = new DataConditionEvaluator({ condition: newCondition }, { em: this.em }); + this.set('condition', newConditionInstance); + } + + setIfTrue(newIfTrue: any) { + this.set('ifTrue', newIfTrue); + } + + setIfFalse(newIfFalse: any) { + this.set('ifFalse', newIfFalse); } private listenToDataVariables() { @@ -97,7 +131,6 @@ export class DataCondition extends Model { em, resolver: new DataVariable(variable, { em: this.em }), onUpdate: (() => { - this.reevaluate(); this._onValueChange?.(); }).bind(this), }); @@ -107,9 +140,11 @@ export class DataCondition extends Model { } getDependentDataVariables() { - const dataVariables: DataVariableProps[] = this.condition.getDataVariables(); - if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); - if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); + const dataVariables: DataVariableProps[] = this.conditionEvaluator.getDependentDataVariables(); + const ifTrue = this.get('ifTrue'); + const ifFalse = this.get('ifFalse'); + if (isDataVariable(ifTrue)) dataVariables.push(ifTrue); + if (isDataVariable(ifFalse)) dataVariables.push(ifFalse); return dataVariables; } @@ -120,16 +155,14 @@ export class DataCondition extends Model { } toJSON() { + const ifTrue = this.get('ifTrue'); + const ifFalse = this.get('ifFalse'); + return { type: DataConditionType, - condition: this.condition, - ifTrue: this.ifTrue, - ifFalse: this.ifFalse, + condition: this.conditionEvaluator, + ifTrue, + ifFalse, }; } } -export class MissingConditionError extends Error { - constructor() { - super('No condition was provided to a conditional component.'); - } -} diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts similarity index 53% rename from packages/core/src/data_sources/model/conditional_variables/Condition.ts rename to packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts index 5f582b3e5..b8f5aba9b 100644 --- a/packages/core/src/data_sources/model/conditional_variables/Condition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts @@ -1,41 +1,39 @@ -import { DataVariableProps } from './../DataVariable'; +import { DataVariableProps } from '../DataVariable'; import EditorModel from '../../../editor/model/Editor'; import { evaluateVariable, isDataVariable } from '../utils'; import { ExpressionProps, LogicGroupProps } from './DataCondition'; -import { LogicalGroupStatement } from './LogicalGroupStatement'; -import { Operator } from './operators'; -import { GenericOperation, GenericOperator } from './operators/GenericOperator'; -import { LogicalOperator } from './operators/LogicalOperator'; +import { LogicalGroupEvaluator } from './LogicalGroupEvaluator'; +import { Operator } from './operators/BaseOperator'; +import { AnyTypeOperation, AnyTypeOperator } from './operators/AnyTypeOperator'; +import { BooleanOperator } from './operators/BooleanOperator'; import { NumberOperator, NumberOperation } from './operators/NumberOperator'; -import { StringOperator, StringOperation } from './operators/StringOperations'; +import { StringOperator, StringOperation } from './operators/StringOperator'; import { Model } from '../../../common'; +import { DataConditionOperation } from './operators/types'; export type ConditionProps = ExpressionProps | LogicGroupProps | boolean; -export class Condition extends Model { - private condition: ConditionProps; +interface DataConditionEvaluatorProps { + condition: ConditionProps; +} + +export class DataConditionEvaluator extends Model { private em: EditorModel; - constructor(props: ConditionProps, opts: { em: EditorModel }) { + constructor(props: DataConditionEvaluatorProps, opts: { em: EditorModel }) { super(props); - this.condition = props; this.em = opts.em; } evaluate(): boolean { - return this.evaluateCondition(this.condition); - } - - /** - * Recursively evaluates conditions and logic groups. - */ - private evaluateCondition(condition: ConditionProps): boolean { + const em = this.em; + const condition = this.get('condition'); if (typeof condition === 'boolean') return condition; if (this.isLogicGroup(condition)) { const { logicalOperator, statements } = condition; - const operator = new LogicalOperator(logicalOperator); - const logicalGroup = new LogicalGroupStatement(operator, statements, { em: this.em }); + const operator = new BooleanOperator(logicalOperator, { em }); + const logicalGroup = new LogicalGroupEvaluator(operator, statements, { em }); return logicalGroup.evaluate(); } @@ -49,62 +47,65 @@ export class Condition extends Model { return evaluated; } - throw new Error('Invalid condition type.'); + this.em.logError('Invalid condition type.'); + return false; } /** * 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); + private getOperator(left: any, operator: string): Operator { + const em = this.em; + + if (this.isOperatorInEnum(operator, AnyTypeOperation)) { + return new AnyTypeOperator(operator as AnyTypeOperation, { em }); } else if (typeof left === 'number') { - return new NumberOperator(operator as NumberOperation); + return new NumberOperator(operator as NumberOperation, { em }); } else if (typeof left === 'string') { - return new StringOperator(operator as StringOperation); + return new StringOperator(operator as StringOperation, { em }); } throw new Error(`Unsupported data type: ${typeof left}`); } - /** - * Extracts all data variables from the condition, including nested ones. - */ - getDataVariables() { - const variables: DataVariableProps[] = []; - this.extractVariables(this.condition, variables); - return variables; + getDependentDataVariables(): DataVariableProps[] { + const condition = this.get('condition'); + if (!condition) return []; + + return this.extractDataVariables(condition); } - /** - * Recursively extracts variables from expressions or logic groups. - */ - private extractVariables(condition: ConditionProps, variables: DataVariableProps[]): void { + private extractDataVariables(condition: ConditionProps): DataVariableProps[] { + const variables: DataVariableProps[] = []; + 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)); + condition.statements.forEach((stmt) => variables.push(...this.extractDataVariables(stmt))); } + + return variables; } - /** - * Checks if a condition is a LogicGroup. - */ private isLogicGroup(condition: any): condition is LogicGroupProps { return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); } - /** - * Checks if a condition is an Expression. - */ private isExpression(condition: any): condition is ExpressionProps { 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); } + + toJSON(options?: any) { + const condition = this.get('condition'); + if (typeof condition === 'object') { + const json = JSON.parse(JSON.stringify(condition)); + return json; + } + + return condition; + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts similarity index 54% rename from packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts rename to packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts index c3294a02c..1a0800878 100644 --- a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts @@ -1,12 +1,12 @@ import EditorModel from '../../../editor/model/Editor'; -import { Condition, ConditionProps } from './Condition'; -import { LogicalOperator } from './operators/LogicalOperator'; +import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator'; +import { BooleanOperator } from './operators/BooleanOperator'; -export class LogicalGroupStatement { +export class LogicalGroupEvaluator { private em: EditorModel; constructor( - private operator: LogicalOperator, + private operator: BooleanOperator, private statements: ConditionProps[], opts: { em: EditorModel }, ) { @@ -15,7 +15,7 @@ export class LogicalGroupStatement { evaluate(): boolean { const results = this.statements.map((statement) => { - const condition = new Condition(statement, { em: this.em }); + const condition = new DataConditionEvaluator({ condition: statement }, { em: this.em }); return condition.evaluate(); }); return this.operator.evaluate(results); diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts similarity index 78% rename from packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts rename to packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts index d4b1e035d..128f44d8f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts @@ -1,7 +1,8 @@ import DataVariable from '../../DataVariable'; -import { Operator } from '.'; +import { Operator } from './BaseOperator'; +import EditorModel from '../../../../editor/model/Editor'; -export enum GenericOperation { +export enum AnyTypeOperation { equals = 'equals', isTruthy = 'isTruthy', isFalsy = 'isFalsy', @@ -16,13 +17,9 @@ export enum GenericOperation { isDefaultValue = 'isDefaultValue', // For Datasource variables } -export class GenericOperator extends Operator { - constructor(private operator: GenericOperation) { - super(); - } - +export class AnyTypeOperator extends Operator { evaluate(left: any, right: any): boolean { - switch (this.operator) { + switch (this.operation) { case 'equals': return left === right; case 'isTruthy': @@ -48,7 +45,8 @@ export class GenericOperator extends Operator { case 'isDefaultValue': return left instanceof DataVariable && left.get('defaultValue') === right; default: - throw new Error(`Unsupported generic operator: ${this.operator}`); + this.em?.logError(`Unsupported generic operation: ${this.operation}`); + return false; } } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts new file mode 100644 index 000000000..4cde7626d --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts @@ -0,0 +1,14 @@ +import EditorModel from '../../../../editor/model/Editor'; +import { DataConditionOperation } from './types'; + +export abstract class Operator { + protected em: EditorModel; + protected operation: OperationType; + + constructor(operation: any, opts: { em: EditorModel }) { + this.operation = operation; + this.em = opts.em; + } + + abstract evaluate(left: any, right: any): boolean; +} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts new file mode 100644 index 000000000..e06b95a7e --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts @@ -0,0 +1,25 @@ +import { Operator } from './BaseOperator'; + +export enum BooleanOperation { + and = 'and', + or = 'or', + xor = 'xor', +} + +export class BooleanOperator extends Operator { + evaluate(statements: boolean[]): boolean { + if (!statements?.length) return false; + + switch (this.operation) { + case BooleanOperation.and: + return statements.every(Boolean); + case BooleanOperation.or: + return statements.some(Boolean); + case BooleanOperation.xor: + return statements.filter(Boolean).length === 1; + default: + this.em.logError(`Unsupported boolean operation: ${this.operation}`); + return false; + } + } +} 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 deleted file mode 100644 index 75f979b45..000000000 --- a/packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 statements, 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 index db054680b..52fc256af 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,4 +1,4 @@ -import { Operator } from '.'; +import { Operator } from './BaseOperator'; export enum NumberOperation { greaterThan = '>', @@ -9,13 +9,9 @@ export enum NumberOperation { notEquals = '!=', } -export class NumberOperator extends Operator { - constructor(private operator: NumberOperation) { - super(); - } - +export class NumberOperator extends Operator { evaluate(left: number, right: number): boolean { - switch (this.operator) { + switch (this.operation) { case NumberOperation.greaterThan: return left > right; case NumberOperation.lessThan: @@ -29,7 +25,8 @@ export class NumberOperator extends Operator { case NumberOperation.notEquals: return left !== right; default: - throw new Error(`Unsupported number operator: ${this.operator}`); + this.em.logError(`Unsupported number operation: ${this.operation}`); + return false; } } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts similarity index 70% rename from packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts rename to packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts index 52c5eb6fb..7dead9629 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperations.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts @@ -1,4 +1,4 @@ -import { Operator } from '.'; +import { Operator } from './BaseOperator'; export enum StringOperation { contains = 'contains', @@ -9,13 +9,9 @@ export enum StringOperation { trimEquals = 'trimEquals', } -export class StringOperator extends Operator { - constructor(private operator: StringOperation) { - super(); - } - +export class StringOperator extends Operator { evaluate(left: string, right: string) { - switch (this.operator) { + switch (this.operation) { case StringOperation.contains: return left.includes(right); case StringOperation.startsWith: @@ -23,14 +19,15 @@ export class StringOperator extends Operator { case StringOperation.endsWith: return left.endsWith(right); case StringOperation.matchesRegex: - if (!right) throw new Error('Regex pattern must be provided.'); + if (!right) this.em.logError('Regex pattern must be provided.'); return new RegExp(right).test(left); case StringOperation.equalsIgnoreCase: return left.toLowerCase() === right.toLowerCase(); case StringOperation.trimEquals: return left.trim() === right.trim(); default: - throw new Error(`Unsupported string operator: ${this.operator}`); + this.em.logError(`Unsupported string operation: ${this.operation}`); + return false; } } } 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 deleted file mode 100644 index 5ae1f2879..000000000 --- a/packages/core/src/data_sources/model/conditional_variables/operators/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export abstract class Operator { - abstract evaluate(left: any, right: any): boolean; -} diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/types.ts b/packages/core/src/data_sources/model/conditional_variables/operators/types.ts new file mode 100644 index 000000000..483ca91bc --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/operators/types.ts @@ -0,0 +1,6 @@ +import { AnyTypeOperation } from './AnyTypeOperator'; +import { BooleanOperation } from './BooleanOperator'; +import { NumberOperation } from './NumberOperator'; +import { StringOperation } from './StringOperator'; + +export type DataConditionOperation = AnyTypeOperation | StringOperation | NumberOperation | BooleanOperation; diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 1baf6bb62..1fa19bc3e 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -1,4 +1,4 @@ -import { isArray } from 'underscore'; +import { bindAll, isArray } from 'underscore'; import { ObjectAny } from '../../../common'; import Component from '../../../dom_components/model/Component'; import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; @@ -17,12 +17,16 @@ import { DataCollectionState, DataCollectionStateMap, } from './types'; +import { getSymbolsToUpdate } from '../../../dom_components/model/SymbolUtils'; +import { StyleProps, UpdateStyleOptions } from '../../../domain_abstract/model/StyleableModel'; import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers'; export default class ComponentDataCollection extends Component { + dataSourceWatcher?: DataResolverListener; + constructor(props: ComponentDataCollectionProps, opt: ComponentOptions) { const collectionDef = props[keyCollectionDefinition]; - // If we are cloning, leave setting the collection items to the main symbol collection + if (opt.forCloning) { return super(props as any, opt) as unknown as ComponentDataCollection; } @@ -36,129 +40,202 @@ export default class ComponentDataCollection extends Component { return cmp; } - const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as DataCollectionStateMap; - const components: Component[] = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt); - cmp.components(components, opt); + this.rebuildChildrenFromCollection(); + bindAll(this, 'rebuildChildrenFromCollection'); + this.listenTo(this, `change:${keyCollectionDefinition}`, this.rebuildChildrenFromCollection); + this.listenToDataSource(); + + return cmp; + } + + getItemsCount() { + const items = this.getDataSourceItems(); + const startIndex = Math.max(0, this.getConfigStartIndex() ?? 0); + const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE; + const endIndex = Math.min(items.length - 1, configEndIndex); + + const count = endIndex - startIndex + 1; + return Math.max(0, count); + } + + getConfigStartIndex() { + return this.collectionConfig.startIndex; + } + + getConfigEndIndex() { + return this.collectionConfig.endIndex; + } + + getComponentDef(): ComponentDefinition { + return this.getFirstChildJSON(); + } + + getDataSource(): DataCollectionDataSource { + return this.collectionDef?.collectionConfig?.dataSource; + } + + getCollectionId(): string { + return this.collectionDef?.collectionConfig?.collectionId; + } + + setComponentDef(componentDef: ComponentDefinition) { + this.set(keyCollectionDefinition, { ...this.collectionDef, componentDef }); + } - if (isDataVariable(this.collectionDataSource)) { - this.watchDataSource(parentCollectionStateMap, opt); + setStartIndex(startIndex: number): void { + if (startIndex < 0) { + this.em.logError('Start index should be greater than or equal to 0'); + return; } - return cmp; + this.updateCollectionConfig({ startIndex }); } - get collectionConfig() { - return this.get(keyCollectionDefinition).collectionConfig as DataCollectionConfig; + setEndIndex(endIndex: number): void { + this.updateCollectionConfig({ endIndex }); } - get collectionDataSource() { - return this.collectionConfig.dataSource; + private updateCollectionConfig(updates: Partial): void { + this.set(keyCollectionDefinition, { + ...this.collectionDef, + collectionConfig: { + ...this.collectionConfig, + ...updates, + }, + }); } - toJSON(opts?: ObjectAny) { - const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps; - json[keyCollectionDefinition].componentDef = this.getComponentDef(); - delete json.components; - delete json.droppable; - return json; + setDataSource(dataSource: DataCollectionDataSource) { + this.set(keyCollectionDefinition, { + ...this.collectionDef, + collectionConfig: { ...this.collectionConfig, dataSource }, + }); + } + + private getDataSourceItems() { + return this.collectionDef?.collectionConfig ? getDataSourceItems(this.collectionConfig.dataSource, this.em) : []; + } + + private getCollectionStateMap() { + return (this.get(keyCollectionsStateMap) || {}) as DataCollectionStateMap; + } + + private get collectionDef() { + return (this.get(keyCollectionDefinition) || {}) as DataCollectionProps; + } + + private get collectionConfig() { + return (this.collectionDef?.collectionConfig || {}) as DataCollectionConfig; + } + + private get collectionDataSource() { + return this.collectionConfig.dataSource; } - private getComponentDef() { + private getFirstChildJSON() { const firstChild = this.components().at(0); - const firstChildJSON = firstChild ? serialize(firstChild) : this.get(keyCollectionDefinition).componentDef; + const firstChildJSON = firstChild ? serialize(firstChild) : this.collectionDef.componentDef; delete firstChildJSON?.draggable; + delete firstChildJSON?.removable; return firstChildJSON; } - private watchDataSource(parentCollectionStateMap: DataCollectionStateMap, opt: ComponentOptions) { + private listenToDataSource() { const { em } = this; const path = this.collectionDataSource?.path; if (!path) return; - - new DataResolverListener({ + this.dataSourceWatcher = new DataResolverListener({ em, resolver: new DataVariable({ type: DataVariableType, path }, { em }), - onUpdate: () => { - const collectionDef = { ...this.get(keyCollectionDefinition), componentDef: this.getComponentDef() }; - const collectionItems = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt); - this.components().reset(collectionItems, updateFromWatcher as any); - }, + onUpdate: this.rebuildChildrenFromCollection, }); } - static isComponent(el: HTMLElement) { - return toLowerCase(el.tagName) === DataCollectionType; - } -} - -function getCollectionItems( - em: EditorModel, - collectionDef: DataCollectionProps, - parentCollectionStateMap: DataCollectionStateMap, - opt: ComponentOptions, -) { - const { componentDef, collectionConfig } = collectionDef; - const result = validateCollectionConfig(collectionConfig, componentDef, em); - if (!result) { - return []; + private rebuildChildrenFromCollection() { + this.components().reset(this.getCollectionItems(), updateFromWatcher as any); } - const components: Component[] = []; - const collectionId = collectionConfig.collectionId; - const items = getDataSourceItems(collectionConfig.dataSource, em); - const startIndex = Math.max(0, collectionConfig.startIndex || 0); - const endIndex = Math.min( - items.length - 1, - collectionConfig.endIndex !== undefined ? collectionConfig.endIndex : Number.MAX_VALUE, - ); - const totalItems = endIndex - startIndex + 1; - let symbolMain: Component; - - for (let index = startIndex; index <= endIndex; index++) { - const item = items[index]; - const collectionState: DataCollectionState = { - collectionId, - currentIndex: index, - currentItem: item, - startIndex: startIndex, - endIndex: endIndex, - totalItems: totalItems, - remainingItems: totalItems - (index + 1), - }; + getCollectionItems() { + const { componentDef, collectionConfig } = this.collectionDef; + const result = validateCollectionConfig(collectionConfig, componentDef, this.em); - if (parentCollectionStateMap[collectionId]) { - em.logError( - `The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`, - ); + if (!result) { return []; } - const collectionsStateMap: DataCollectionStateMap = { - ...parentCollectionStateMap, - [collectionId]: collectionState, - }; + const components: Component[] = []; + const collectionId = collectionConfig.collectionId; + const items = this.getDataSourceItems(); + + const startIndex = this.getConfigStartIndex() ?? 0; + const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE; + const endIndex = Math.min(items.length - 1, configEndIndex); + const totalItems = endIndex - startIndex + 1; + const parentCollectionStateMap = this.getCollectionStateMap(); + + let symbolMain: Component; + for (let index = startIndex; index <= endIndex; index++) { + const item = items[index]; + const isFirstItem = index === startIndex; + const collectionState: DataCollectionState = { + collectionId, + currentIndex: index, + currentItem: item, + startIndex: startIndex, + endIndex: endIndex, + totalItems: totalItems, + remainingItems: totalItems - (index + 1), + }; + + if (parentCollectionStateMap[collectionId]) { + this.em.logError( + `The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`, + ); + return []; + } - if (index === startIndex) { - const componentType = (componentDef?.type as string) || 'default'; - let type = em.Components.getType(componentType) || em.Components.getType('default'); - const Model = type.model; - symbolMain = new Model({ ...serialize(componentDef), draggable: false }, opt); - setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(symbolMain); + const collectionsStateMap: DataCollectionStateMap = { + ...parentCollectionStateMap, + [collectionId]: collectionState, + }; + + if (isFirstItem) { + const componentType = (componentDef?.type as string) || 'default'; + let type = this.em.Components.getType(componentType) || this.em.Components.getType('default'); + const Model = type.model; + symbolMain = new Model( + { + ...serialize(componentDef), + draggable: false, + removable: false, + }, + this.opt, + ); + } + + const instance = symbolMain!.clone({ symbol: true }); + !isFirstItem && instance.set('locked', true); + setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance); + components.push(instance); } - const instance = symbolMain!.clone({ symbol: true }); - setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance); + return components; + } - components.push(instance); + static isComponent(el: HTMLElement) { + return toLowerCase(el.tagName) === DataCollectionType; } - return components; + toJSON(opts?: ObjectAny) { + const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps; + json[keyCollectionDefinition].componentDef = this.getFirstChildJSON(); + delete json.components; + delete json.droppable; + return json; + } } -function setCollectionStateMapAndPropagate( - collectionsStateMap: DataCollectionStateMap, - collectionId: string | undefined, -) { +function setCollectionStateMapAndPropagate(collectionsStateMap: DataCollectionStateMap, collectionId: string) { return (cmp: Component) => { setCollectionStateMap(collectionsStateMap)(cmp); @@ -169,7 +246,6 @@ function setCollectionStateMapAndPropagate( const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`; const cmps = cmp.components(); - // Add the 'add' listener if not already in the listeners array if (!cmp.collectionStateListeners.includes(listenerKey)) { cmp.listenTo(cmps, 'add', addListener); cmp.collectionStateListeners.push(listenerKey); @@ -178,9 +254,15 @@ function setCollectionStateMapAndPropagate( component.stopListening(component.components(), 'add', addListener); component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); const index = component.collectionStateListeners.indexOf(listenerKey); - if (index > -1) { + if (index !== -1) { component.collectionStateListeners.splice(index, 1); } + + const collectionsStateMap = component.get(keyCollectionsStateMap); + component.set(keyCollectionsStateMap, { + ...collectionsStateMap, + [collectionId]: undefined, + }); }; cmp.listenTo(cmps, 'remove', removeListener); @@ -229,6 +311,12 @@ function validateCollectionConfig( } } + const startIndex = collectionConfig?.startIndex; + + if (startIndex !== undefined && (startIndex < 0 || !Number.isInteger(startIndex))) { + em.logError(`Invalid startIndex: ${startIndex}. It must be a non-negative integer.`); + } + return true; } @@ -241,6 +329,28 @@ function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { }; cmp.set(keyCollectionsStateMap, updatedCollectionStateMap); cmp.dataResolverWatchers.updateCollectionStateMap(updatedCollectionStateMap); + + const parentCollectionsId = Object.keys(updatedCollectionStateMap); + const isFirstItem = parentCollectionsId.every( + (key) => updatedCollectionStateMap[key].currentIndex === updatedCollectionStateMap[key].startIndex, + ); + + if (isFirstItem) { + const __onStyleChange = cmp.__onStyleChange.bind(cmp); + + cmp.__onStyleChange = (newStyles: StyleProps, opts: UpdateStyleOptions = {}) => { + __onStyleChange(newStyles); + const cmps = getSymbolsToUpdate(cmp); + + cmps.forEach((cmp) => { + cmp.addStyle(newStyles, opts); + }); + }; + + cmp.on(`change:${keyIsCollectionItem}`, () => { + cmp.__onStyleChange = __onStyleChange; + }); + } }; } @@ -262,7 +372,6 @@ function getDataSourceItems(dataSource: DataCollectionDataSource, em: EditorMode const id = dataSource.path; items = listDataSourceVariables(id, em); } else { - // Path points to a record in the data source items = em.DataSources.getValue(dataSource.path, []); } break; diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts index a4d7a7f6e..c268ae3b1 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts @@ -18,6 +18,7 @@ export default class ComponentDataCollectionVariable extends Component { collectionId: undefined, variableType: undefined, path: undefined, + droppable: false, }; } diff --git a/packages/core/src/data_sources/model/data_collection/types.ts b/packages/core/src/data_sources/model/data_collection/types.ts index 3ce256c36..1431b53d6 100644 --- a/packages/core/src/data_sources/model/data_collection/types.ts +++ b/packages/core/src/data_sources/model/data_collection/types.ts @@ -36,6 +36,7 @@ export interface DataCollectionStateMap { } export interface ComponentDataCollectionProps extends ComponentDefinition { + type: typeof DataCollectionType; [keyCollectionDefinition]: DataCollectionProps; } @@ -44,7 +45,6 @@ export interface ComponentDataCollectionVariableProps Omit {} export interface DataCollectionProps { - type: typeof DataCollectionType; collectionConfig: DataCollectionConfig; componentDef: ComponentDefinition; } diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index c092a05d6..ae2130ef0 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -40,8 +40,7 @@ export function getDataResolverInstance( resolver = new DataVariable(resolverProps, options); break; case DataConditionType: { - const { condition, ifTrue, ifFalse } = resolverProps; - resolver = new DataCondition(condition, ifTrue, ifFalse, options); + resolver = new DataCondition(resolverProps, options); break; } case DataCollectionVariableType: { @@ -64,5 +63,5 @@ export function getDataResolverInstanceValue( ) { const resolver = getDataResolverInstance(resolverProps, options); - return { resolver, value: resolver.getDataValue() }; + return resolver.getDataValue(); } diff --git a/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts b/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts index e0974793d..3b774cae4 100644 --- a/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts +++ b/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts @@ -82,7 +82,7 @@ export class ComponentDataResolverWatchers { const keys = this.propertyWatcher.getDynamicValuesOfType(DataCollectionVariableType); const attributesKeys = this.attributeWatcher.getDynamicValuesOfType(DataCollectionVariableType); - const combinedKeys = [keyCollectionsStateMap, ...keys]; + const combinedKeys = [keyCollectionsStateMap, 'locked', ...keys]; const haveOverridenAttributes = Object.keys(attributesKeys).length; if (haveOverridenAttributes) combinedKeys.push('attributes'); diff --git a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts b/packages/core/src/dom_components/model/ComponentResolverWatcher.ts index 8b337e4b7..11ca8ba74 100644 --- a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts +++ b/packages/core/src/dom_components/model/ComponentResolverWatcher.ts @@ -2,7 +2,11 @@ import { ObjectAny } from '../../common'; import { DataCollectionVariableType } from '../../data_sources/model/data_collection/constants'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import DataResolverListener from '../../data_sources/model/DataResolverListener'; -import { getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/model/utils'; +import { + getDataResolverInstance, + getDataResolverInstanceValue, + isDataResolverProps, +} from '../../data_sources/model/utils'; import EditorModel from '../../editor/model/Editor'; import { DataResolverProps } from '../../data_sources/types'; import Component from './Component'; @@ -90,7 +94,7 @@ export class ComponentResolverWatcher { continue; } - const { resolver } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); + const resolver = getDataResolverInstance(resolverProps, { em, collectionsStateMap }); this.resolverListeners[key] = new DataResolverListener({ em, resolver, @@ -112,8 +116,7 @@ export class ComponentResolverWatcher { continue; } - const { value } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); - evaluatedValues[key] = value; + evaluatedValues[key] = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); } return evaluatedValues; diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 80fc8fe0f..4aa41e892 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -4,18 +4,19 @@ import ParserHtml from '../../parser/model/ParserHtml'; import Selectors from '../../selector_manager/model/Selectors'; import { shallowDiff } from '../../utils/mixins'; import EditorModel from '../../editor/model/Editor'; -import DataVariable, { DataVariableProps, DataVariableType } from '../../data_sources/model/DataVariable'; +import { DataVariableProps } from '../../data_sources/model/DataVariable'; import DataResolverListener from '../../data_sources/model/DataResolverListener'; import CssRuleView from '../../css_composer/view/CssRuleView'; import ComponentView from '../../dom_components/view/ComponentView'; import Frame from '../../canvas/model/Frame'; +import { DataConditionProps } from '../../data_sources/model/conditional_variables/DataCondition'; import { - DataCondition, - DataConditionType, - DataConditionProps, -} from '../../data_sources/model/conditional_variables/DataCondition'; -import { isDataResolver, isDataResolverProps } from '../../data_sources/model/utils'; -import { DataResolverProps } from '../../data_sources/types'; + getDataResolverInstance, + getDataResolverInstanceValue, + isDataResolver, + isDataResolverProps, +} from '../../data_sources/model/utils'; +import { DataResolver } from '../../data_sources/types'; export type StyleProps = Record; @@ -111,7 +112,8 @@ export default class StyleableModel extends Model const styleValue = newStyle[key]; if (isDataResolverProps(styleValue)) { - const dataResolver = this.getDataResolverInstance(styleValue); + const dataResolver = getDataResolverInstance(styleValue, { em: this.em! }); + if (dataResolver) { newStyle[key] = dataResolver; this.listenToDataResolver(dataResolver, key); @@ -141,25 +143,7 @@ export default class StyleableModel extends Model return newStyle; } - private getDataResolverInstance(props: DataResolverProps) { - const em = this.em!; - let resolver; - - switch (props.type) { - case DataVariableType: - resolver = new DataVariable(props, { em }); - break; - case DataConditionType: { - const { condition, ifTrue, ifFalse } = props; - resolver = new DataCondition(condition, ifTrue, ifFalse, { em }); - break; - } - } - - return resolver; - } - - listenToDataResolver(resolver: DataVariable | DataCondition, styleProp: string) { + listenToDataResolver(resolver: DataResolver, styleProp: string) { const resolverListener = this.styleResolverListeners[styleProp]; if (resolverListener) { resolverListener.listenToResolver(); @@ -203,10 +187,7 @@ export default class StyleableModel extends Model } if (isDataResolverProps(styleValue)) { - const resolver = this.getDataResolverInstance(styleValue); - if (resolver) { - resultStyle[key] = resolver.getDataValue(); - } + resultStyle[key] = getDataResolverInstanceValue(styleValue, { em: this.em! }); } if (isDataResolver(styleValue)) { diff --git a/packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts b/packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts new file mode 100644 index 000000000..de8cfd768 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts @@ -0,0 +1,94 @@ +import { DataSourceManager } from '../../../../src'; +import ComponentDataVariable from '../../../../src/data_sources/model/ComponentDataVariable'; +import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; +import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; +import EditorModel from '../../../../src/editor/model/Editor'; +import { setupTestEditor } from '../../../common'; + +describe('ComponentDataVariable - setPath and setDefaultValue', () => { + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + ({ em, dsm, cmpRoot } = setupTestEditor()); + const dataSource = { + id: 'ds_id', + records: [ + { id: 'id1', name: 'Name1' }, + { id: 'id2', name: 'Name2' }, + ], + }; + + dsm.add(dataSource); + }); + + afterEach(() => { + em.destroy(); + }); + + test('component updates when path is changed using setPath', () => { + const cmp = cmpRoot.append({ + type: DataVariableType, + defaultValue: 'default', + path: 'ds_id.id1.name', + })[0] as ComponentDataVariable; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + expect(cmp.getPath()).toBe('ds_id.id1.name'); + + cmp.setPath('ds_id.id2.name'); + expect(cmp.getEl()?.innerHTML).toContain('Name2'); + expect(cmp.getPath()).toBe('ds_id.id2.name'); + }); + + test('component updates when default value is changed using setDefaultValue', () => { + const cmp = cmpRoot.append({ + type: DataVariableType, + defaultValue: 'default', + path: 'unknown.id1.name', + })[0] as ComponentDataVariable; + + expect(cmp.getEl()?.innerHTML).toContain('default'); + expect(cmp.getDefaultValue()).toBe('default'); + + cmp.setDefaultValue('new default'); + expect(cmp.getEl()?.innerHTML).toContain('new default'); + expect(cmp.getDefaultValue()).toBe('new default'); + }); + + test('component updates correctly after path and default value are changed', () => { + const cmp = cmpRoot.append({ + type: DataVariableType, + defaultValue: 'default', + path: 'ds_id.id1.name', + })[0] as ComponentDataVariable; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + + cmp.setPath('ds_id.id2.name'); + expect(cmp.getEl()?.innerHTML).toContain('Name2'); + + cmp.setDefaultValue('new default'); + dsm.all.reset(); + expect(cmp.getEl()?.innerHTML).toContain('new default'); + expect(cmp.getDefaultValue()).toBe('new default'); + }); + + test('component updates correctly after path is changed and data is updated', () => { + const cmp = cmpRoot.append({ + type: DataVariableType, + defaultValue: 'default', + path: 'ds_id.id1.name', + })[0] as ComponentDataVariable; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + + cmp.setPath('ds_id.id2.name'); + expect(cmp.getEl()?.innerHTML).toContain('Name2'); + + const ds = dsm.get('ds_id'); + ds.getRecord('id2')?.set({ name: 'Name2-UP' }); + expect(cmp.getEl()?.innerHTML).toContain('Name2-UP'); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts new file mode 100644 index 000000000..96eab39ff --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts @@ -0,0 +1,156 @@ +import { Component, DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition'; +import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; +import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { setupTestEditor } from '../../../../common'; + +describe('ComponentDataCondition Setters', () => { + let editor: Editor; + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + }); + + afterEach(() => { + em.destroy(); + }); + + it('should update the condition using setCondition', () => { + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: '

some text

', + ifFalse: '

false text

', + })[0] as ComponentDataCondition; + + const newCondition = { + left: 1, + operator: NumberOperation.lessThan, + right: 0, + }; + + component.setCondition(newCondition); + expect(component.getCondition()).toEqual(newCondition); + expect(component.getInnerHTML()).toBe('

false text

'); + }); + + it('should update the ifTrue value using setIfTrue', () => { + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: '

some text

', + ifFalse: '

false text

', + })[0] as ComponentDataCondition; + + const newIfTrue = '

new true text

'; + component.setIfTrue(newIfTrue); + expect(component.getIfTrue()).toEqual(newIfTrue); + expect(component.getInnerHTML()).toBe(newIfTrue); + }); + + it('should update the ifFalse value using setIfFalse', () => { + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: '

some text

', + ifFalse: '

false text

', + })[0] as ComponentDataCondition; + + const newIfFalse = '

new false text

'; + component.setIfFalse(newIfFalse); + expect(component.getIfFalse()).toEqual(newIfFalse); + + component.setCondition({ + left: 0, + operator: NumberOperation.lessThan, + right: -1, + }); + expect(component.getInnerHTML()).toBe(newIfFalse); + }); + + it('should update the data sources and re-evaluate the condition', () => { + const dataSource = { + 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: AnyTypeOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: '

True value

', + ifFalse: '

False value

', + })[0] as ComponentDataCondition; + + expect(component.getInnerHTML()).toBe('

True value

'); + + changeDataSourceValue(dsm, 'Different value'); + expect(component.getInnerHTML()).toBe('

False value

'); + + changeDataSourceValue(dsm, 'Name1'); + expect(component.getInnerHTML()).toBe('

True value

'); + }); + + it('should re-render the component when condition, ifTrue, or ifFalse changes', () => { + const component = cmpRoot.append({ + type: DataConditionType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: '

some text

', + ifFalse: '

false text

', + })[0] as ComponentDataCondition; + + const componentView = component.getView() as ComponentDataConditionView; + + component.setIfTrue('

new true text

'); + expect(componentView.el.innerHTML).toContain('new true text'); + + component.setIfFalse('

new false text

'); + component.setCondition({ + left: 0, + operator: NumberOperation.lessThan, + right: -1, + }); + expect(componentView.el.innerHTML).toContain('new false text'); + }); +}); + +function changeDataSourceValue(dsm: DataSourceManager, newValue: string) { + dsm.get('ds1').getRecord('left_id')?.set('left', newValue); +} diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts similarity index 87% rename from packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts rename to packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts index 95710a7ac..222e1bcd7 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts @@ -1,10 +1,7 @@ import { Component, DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { - MissingConditionError, - DataConditionType, -} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; -import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; @@ -13,7 +10,7 @@ import ComponentTextView from '../../../../../src/dom_components/view/ComponentT import EditorModel from '../../../../../src/editor/model/Editor'; import { setupTestEditor } from '../../../../common'; -describe('ComponentConditionalVariable', () => { +describe('ComponentDataCondition', () => { let editor: Editor; let em: EditorModel; let dsm: DataSourceManager; @@ -100,7 +97,7 @@ describe('ComponentConditionalVariable', () => { type: DataVariableType, path: 'ds1.left_id.left', }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: { type: DataVariableType, path: 'ds1.right_id.right', @@ -124,10 +121,10 @@ describe('ComponentConditionalVariable', () => { expect(childComponent.getInnerHTML()).toBe('Some value'); /* Test changing datasources */ - updatedsmLeftValue(dsm, 'Diffirent value'); + changeDataSourceValue(dsm, 'Diffirent value'); expect(getFirstChild(component).getInnerHTML()).toBe('False value'); expect(getFirstChildView(component)?.el.innerHTML).toBe('False value'); - updatedsmLeftValue(dsm, 'Name1'); + changeDataSourceValue(dsm, 'Name1'); expect(getFirstChild(component).getInnerHTML()).toBe('Some value'); expect(getFirstChildView(component)?.el.innerHTML).toBe('Some value'); }); @@ -149,7 +146,7 @@ describe('ComponentConditionalVariable', () => { type: DataVariableType, path: 'ds1.left_id.left', }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: { type: DataVariableType, path: 'ds1.right_id.right', @@ -165,7 +162,7 @@ describe('ComponentConditionalVariable', () => { type: DataVariableType, path: 'ds1.left_id.left', }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: { type: DataVariableType, path: 'ds1.right_id.right', @@ -213,24 +210,9 @@ describe('ComponentConditionalVariable', () => { 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); - }); }); -function updatedsmLeftValue(dsm: DataSourceManager, newValue: string) { +function changeDataSourceValue(dsm: DataSourceManager, newValue: string) { dsm.get('ds1').getRecord('left_id')?.set('left', newValue); } 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 5f7d4af0f..456dcc060 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,10 +1,7 @@ import { DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { - DataConditionType, - MissingConditionError, -} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; -import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import EditorModel from '../../../../../src/editor/model/Editor'; @@ -69,7 +66,7 @@ describe('StyleConditionalVariable', () => { type: DataVariableType, path: 'ds1.left_id.left', }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: { type: DataVariableType, path: 'ds1.right_id.right', @@ -87,23 +84,6 @@ describe('StyleConditionalVariable', () => { 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: DataConditionType, - ifTrue: 'grey', - ifFalse: 'red', - }, - }, - }); - }).toThrow(MissingConditionError); - }); - it.skip('should store components with conditional styles correctly', () => { const conditionalStyleDef = { tagName: 'h1', 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 c90cb7250..02f129be9 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 @@ -4,10 +4,10 @@ import { ExpressionProps, LogicGroupProps, } 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 { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; +import { BooleanOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/BooleanOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; -import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; +import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperator'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import Editor from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor'; @@ -36,23 +36,41 @@ describe('DataCondition', () => { describe('Basic Functionality Tests', () => { test('should evaluate a simple boolean condition', () => { const condition = true; - const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); + const dataCondition = new DataCondition({ condition, ifTrue: 'Yes', ifFalse: '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', { em }); + const dataCondition = new DataCondition({ condition, ifTrue: 'Yes', ifFalse: 'No' }, { em }); expect(dataCondition.getDataValue()).toBe('No'); }); + + test('should return raw ifTrue value when skipDynamicValueResolution is true and condition is true', () => { + const condition = true; + const ifTrue = { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }; + const ifFalse = 'No'; + + const dataCondition = new DataCondition({ condition, ifTrue, ifFalse }, { em }); + expect(dataCondition.getDataValue(true)).toEqual(ifTrue); + }); + + test('should return raw ifFalse value when skipDynamicValueResolution is true and condition is false', () => { + const condition = false; + const ifTrue = 'Yes'; + const ifFalse = { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }; + + const dataCondition = new DataCondition({ condition, ifTrue, ifFalse }, { em }); + expect(dataCondition.getDataValue(true)).toEqual(ifFalse); + }); }); describe('Operator Tests', () => { test('should evaluate using GenericOperation operators', () => { - const condition: ExpressionProps = { left: 5, operator: GenericOperation.equals, right: 5 }; - const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal', { em }); + const condition: ExpressionProps = { left: 5, operator: AnyTypeOperation.equals, right: 5 }; + const dataCondition = new DataCondition({ condition, ifTrue: 'Equal', ifFalse: 'Not Equal' }, { em }); expect(dataCondition.getDataValue()).toBe('Equal'); }); @@ -60,63 +78,61 @@ describe('DataCondition', () => { test('equals (false)', () => { const condition: ExpressionProps = { left: 'hello', - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: 'world', }; - const dataCondition = new DataCondition(condition, 'true', 'false', { em }); - expect(dataCondition.evaluate()).toBe(false); + const dataCondition = new DataCondition({ condition, ifTrue: 'true', ifFalse: 'false' }, { em }); + expect(dataCondition.isTrue()).toBe(false); }); test('should evaluate using StringOperation operators', () => { const condition: ExpressionProps = { left: 'apple', operator: StringOperation.contains, right: 'app' }; - const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain", { em }); + const dataCondition = new DataCondition({ condition, ifTrue: 'Contains', ifFalse: "Doesn't contain" }, { em }); expect(dataCondition.getDataValue()).toBe('Contains'); }); test('should evaluate using NumberOperation operators', () => { const condition: ExpressionProps = { left: 10, operator: NumberOperation.lessThan, right: 15 }; - const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em }); + const dataCondition = new DataCondition({ condition, ifTrue: 'Valid', ifFalse: 'Invalid' }, { em }); expect(dataCondition.getDataValue()).toBe('Valid'); }); test('should evaluate using LogicalOperation operators', () => { const logicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.and, + logicalOperator: BooleanOperation.and, statements: [ - { left: true, operator: GenericOperation.equals, right: true }, + { left: true, operator: AnyTypeOperation.equals, right: true }, { left: 5, operator: NumberOperation.greaterThan, right: 3 }, ], }; - const dataCondition = new DataCondition(logicGroup, 'Pass', 'Fail', { em }); + const dataCondition = new DataCondition({ condition: logicGroup, ifTrue: 'Pass', ifFalse: 'Fail' }, { em }); 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', { em })).toThrow('Invalid condition type.'); - }); - test('should evaluate complex nested conditions', () => { const nestedLogicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.or, + logicalOperator: BooleanOperation.or, statements: [ { - logicalOperator: LogicalOperation.and, + logicalOperator: BooleanOperation.and, statements: [ { left: 1, operator: NumberOperation.lessThan, right: 5 }, - { left: 'test', operator: GenericOperation.equals, right: 'test' }, + { left: 'test', operator: AnyTypeOperation.equals, right: 'test' }, ], }, { left: 10, operator: NumberOperation.greaterThan, right: 100 }, ], }; - const dataCondition = new DataCondition(nestedLogicGroup, 'Nested Pass', 'Nested Fail', { em }); + const dataCondition = new DataCondition( + { condition: nestedLogicGroup, ifTrue: 'Nested Pass', ifFalse: 'Nested Fail' }, + { em }, + ); expect(dataCondition.getDataValue()).toBe('Nested Pass'); }); }); @@ -124,74 +140,89 @@ describe('DataCondition', () => { describe('LogicalGroup Tests', () => { test('should correctly handle AND logical operator', () => { const logicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.and, + logicalOperator: BooleanOperation.and, statements: [ - { left: true, operator: GenericOperation.equals, right: true }, + { left: true, operator: AnyTypeOperation.equals, right: true }, { left: 5, operator: NumberOperation.greaterThan, right: 3 }, ], }; - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); + const dataCondition = new DataCondition( + { condition: logicGroup, ifTrue: 'All true', ifFalse: 'One or more false' }, + { em }, + ); expect(dataCondition.getDataValue()).toBe('All true'); }); test('should correctly handle OR logical operator', () => { const logicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.or, + logicalOperator: BooleanOperation.or, statements: [ - { left: true, operator: GenericOperation.equals, right: false }, + { left: true, operator: AnyTypeOperation.equals, right: false }, { left: 5, operator: NumberOperation.greaterThan, right: 3 }, ], }; - const dataCondition = new DataCondition(logicGroup, 'At least one true', 'All false', { em }); + const dataCondition = new DataCondition( + { condition: logicGroup, ifTrue: 'At least one true', ifFalse: 'All false' }, + { em }, + ); expect(dataCondition.getDataValue()).toBe('At least one true'); }); test('should correctly handle XOR logical operator', () => { const logicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.xor, + logicalOperator: BooleanOperation.xor, statements: [ - { left: true, operator: GenericOperation.equals, right: true }, + { left: true, operator: AnyTypeOperation.equals, right: true }, { left: 5, operator: NumberOperation.lessThan, right: 3 }, - { left: false, operator: GenericOperation.equals, right: true }, + { left: false, operator: AnyTypeOperation.equals, right: true }, ], }; - const dataCondition = new DataCondition(logicGroup, 'Exactly one true', 'Multiple true or all false', { em }); + const dataCondition = new DataCondition( + { condition: logicGroup, ifTrue: 'Exactly one true', ifFalse: 'Multiple true or all false' }, + { em }, + ); expect(dataCondition.getDataValue()).toBe('Exactly one true'); }); test('should handle nested logical groups', () => { const logicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.and, + logicalOperator: BooleanOperation.and, statements: [ - { left: true, operator: GenericOperation.equals, right: true }, + { left: true, operator: AnyTypeOperation.equals, right: true }, { - logicalOperator: LogicalOperation.or, + logicalOperator: BooleanOperation.or, statements: [ { left: 5, operator: NumberOperation.greaterThan, right: 3 }, - { left: false, operator: GenericOperation.equals, right: true }, + { left: false, operator: AnyTypeOperation.equals, right: true }, ], }, ], }; - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); + const dataCondition = new DataCondition( + { condition: logicGroup, ifTrue: 'All true', ifFalse: 'One or more false' }, + { em }, + ); expect(dataCondition.getDataValue()).toBe('All true'); }); test('should handle groups with false conditions', () => { const logicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.and, + logicalOperator: BooleanOperation.and, statements: [ - { left: true, operator: GenericOperation.equals, right: true }, - { left: false, operator: GenericOperation.equals, right: true }, + { left: true, operator: AnyTypeOperation.equals, right: true }, + { left: false, operator: AnyTypeOperation.equals, right: true }, { left: 5, operator: NumberOperation.greaterThan, right: 3 }, ], }; - const dataCondition = new DataCondition(logicGroup, 'All true', 'One or more false', { em }); + const dataCondition = new DataCondition( + { condition: logicGroup, ifTrue: 'All true', ifFalse: 'One or more false' }, + { em }, + ); expect(dataCondition.getDataValue()).toBe('One or more false'); }); }); @@ -200,22 +231,22 @@ describe('DataCondition', () => { test('should return "Yes" when dataVariable matches expected value', () => { const condition: ExpressionProps = { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: 'active', }; - const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); + const dataCondition = new DataCondition({ condition, ifTrue: 'Yes', ifFalse: 'No' }, { em }); expect(dataCondition.getDataValue()).toBe('Yes'); }); test('should return "No" when dataVariable does not match expected value', () => { const condition: ExpressionProps = { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: 'inactive', }; - const dataCondition = new DataCondition(condition, 'Yes', 'No', { em }); + const dataCondition = new DataCondition({ condition, ifTrue: 'Yes', ifFalse: 'No' }, { em }); expect(dataCondition.getDataValue()).toBe('No'); }); @@ -223,11 +254,11 @@ describe('DataCondition', () => { test.skip('should handle missing data variable gracefully', () => { const condition: ExpressionProps = { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.not_a_user.status' }, - operator: GenericOperation.isDefined, + operator: AnyTypeOperation.isDefined, right: undefined, }; - const dataCondition = new DataCondition(condition, 'Found', 'Not Found', { em }); + const dataCondition = new DataCondition({ condition, ifTrue: 'Found', ifFalse: 'Not Found' }, { em }); expect(dataCondition.getDataValue()).toBe('Not Found'); }); @@ -237,7 +268,7 @@ describe('DataCondition', () => { operator: NumberOperation.greaterThan, right: 24, }; - const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em }); + const dataCondition = new DataCondition({ condition, ifTrue: 'Valid', ifFalse: 'Invalid' }, { em }); expect(dataCondition.getDataValue()).toBe('Valid'); }); @@ -249,11 +280,11 @@ describe('DataCondition', () => { dsm.add(dataSource2); const logicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.and, + logicalOperator: BooleanOperation.and, statements: [ { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: 'active', }, { @@ -264,20 +295,23 @@ describe('DataCondition', () => { ], }; - const dataCondition = new DataCondition(logicGroup, 'All conditions met', 'Some conditions failed', { em }); + const dataCondition = new DataCondition( + { condition: logicGroup, ifTrue: 'All conditions met', ifFalse: 'Some conditions failed' }, + { em }, + ); expect(dataCondition.getDataValue()).toBe('All conditions met'); }); test('should handle nested logical conditions with data variables', () => { const logicGroup: LogicGroupProps = { - logicalOperator: LogicalOperation.or, + logicalOperator: BooleanOperation.or, statements: [ { - logicalOperator: LogicalOperation.and, + logicalOperator: BooleanOperation.and, statements: [ { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_2.status' }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: 'inactive', }, { @@ -289,21 +323,26 @@ describe('DataCondition', () => { }, { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, - operator: GenericOperation.equals, + operator: AnyTypeOperation.equals, right: 'inactive', }, ], }; - const dataCondition = new DataCondition(logicGroup, 'Condition met', 'Condition failed', { em }); + const dataCondition = new DataCondition( + { condition: logicGroup, ifTrue: 'Condition met', ifFalse: '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', + { + condition: true, + ifTrue: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, + ifFalse: 'No', + }, { em }, ); expect(dataCondition.getDataValue()).toBe('active'); @@ -311,11 +350,14 @@ describe('DataCondition', () => { 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' }, + { + condition: false, + ifTrue: 'Yes', + ifFalse: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, + }, { em }, ); + expect(dataCondition.getDataValue()).toBe('active'); }); }); 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/AnyTypeOperator.ts similarity index 61% rename from packages/core/test/specs/data_sources/model/conditional_variables/operators/GenericOperator.ts rename to packages/core/test/specs/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts index 516bb5ce4..9e93cc42e 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/AnyTypeOperator.ts @@ -1,150 +1,157 @@ import { - GenericOperator, - GenericOperation, -} from '../../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; + AnyTypeOperator, + AnyTypeOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; +import Editor from '../../../../../../src/editor/model/Editor'; +import EditorModel from '../../../../../../src/editor/model/Editor'; describe('GenericOperator', () => { + let em: EditorModel; + + beforeEach(() => { + em = new Editor(); + }); + + afterEach(() => { + em.destroy(); + }); + describe('Operator: equals', () => { test('should return true when values are equal', () => { - const operator = new GenericOperator(GenericOperation.equals); + const operator = new AnyTypeOperator(AnyTypeOperation.equals, { em }); expect(operator.evaluate(5, 5)).toBe(true); }); test('should return false when values are not equal', () => { - const operator = new GenericOperator(GenericOperation.equals); + const operator = new AnyTypeOperator(AnyTypeOperation.equals, { em }); expect(operator.evaluate(5, 10)).toBe(false); }); }); describe('Operator: isTruthy', () => { test('should return true for truthy value', () => { - const operator = new GenericOperator(GenericOperation.isTruthy); + const operator = new AnyTypeOperator(AnyTypeOperation.isTruthy, { em }); expect(operator.evaluate('non-empty', null)).toBe(true); }); test('should return false for falsy value', () => { - const operator = new GenericOperator(GenericOperation.isTruthy); + const operator = new AnyTypeOperator(AnyTypeOperation.isTruthy, { em }); expect(operator.evaluate('', null)).toBe(false); }); }); describe('Operator: isFalsy', () => { test('should return true for falsy value', () => { - const operator = new GenericOperator(GenericOperation.isFalsy); + const operator = new AnyTypeOperator(AnyTypeOperation.isFalsy, { em }); expect(operator.evaluate(0, null)).toBe(true); }); test('should return false for truthy value', () => { - const operator = new GenericOperator(GenericOperation.isFalsy); + const operator = new AnyTypeOperator(AnyTypeOperation.isFalsy, { em }); expect(operator.evaluate(1, null)).toBe(false); }); }); describe('Operator: isDefined', () => { test('should return true for defined value', () => { - const operator = new GenericOperator(GenericOperation.isDefined); + const operator = new AnyTypeOperator(AnyTypeOperation.isDefined, { em }); expect(operator.evaluate(10, null)).toBe(true); }); test('should return false for undefined value', () => { - const operator = new GenericOperator(GenericOperation.isDefined); + const operator = new AnyTypeOperator(AnyTypeOperation.isDefined, { em }); expect(operator.evaluate(undefined, null)).toBe(false); }); }); describe('Operator: isNull', () => { test('should return true for null value', () => { - const operator = new GenericOperator(GenericOperation.isNull); + const operator = new AnyTypeOperator(AnyTypeOperation.isNull, { em }); expect(operator.evaluate(null, null)).toBe(true); }); test('should return false for non-null value', () => { - const operator = new GenericOperator(GenericOperation.isNull); + const operator = new AnyTypeOperator(AnyTypeOperation.isNull, { em }); expect(operator.evaluate(0, null)).toBe(false); }); }); describe('Operator: isUndefined', () => { test('should return true for undefined value', () => { - const operator = new GenericOperator(GenericOperation.isUndefined); + const operator = new AnyTypeOperator(AnyTypeOperation.isUndefined, { em }); expect(operator.evaluate(undefined, null)).toBe(true); }); test('should return false for defined value', () => { - const operator = new GenericOperator(GenericOperation.isUndefined); + const operator = new AnyTypeOperator(AnyTypeOperation.isUndefined, { em }); expect(operator.evaluate(0, null)).toBe(false); }); }); describe('Operator: isArray', () => { test('should return true for array', () => { - const operator = new GenericOperator(GenericOperation.isArray); + const operator = new AnyTypeOperator(AnyTypeOperation.isArray, { em }); expect(operator.evaluate([1, 2, 3], null)).toBe(true); }); test('should return false for non-array', () => { - const operator = new GenericOperator(GenericOperation.isArray); + const operator = new AnyTypeOperator(AnyTypeOperation.isArray, { em }); expect(operator.evaluate('not an array', null)).toBe(false); }); }); describe('Operator: isObject', () => { test('should return true for object', () => { - const operator = new GenericOperator(GenericOperation.isObject); + const operator = new AnyTypeOperator(AnyTypeOperation.isObject, { em }); expect(operator.evaluate({ key: 'value' }, null)).toBe(true); }); test('should return false for non-object', () => { - const operator = new GenericOperator(GenericOperation.isObject); + const operator = new AnyTypeOperator(AnyTypeOperation.isObject, { em }); expect(operator.evaluate(42, null)).toBe(false); }); }); describe('Operator: isString', () => { test('should return true for string', () => { - const operator = new GenericOperator(GenericOperation.isString); + const operator = new AnyTypeOperator(AnyTypeOperation.isString, { em }); expect(operator.evaluate('Hello', null)).toBe(true); }); test('should return false for non-string', () => { - const operator = new GenericOperator(GenericOperation.isString); + const operator = new AnyTypeOperator(AnyTypeOperation.isString, { em }); expect(operator.evaluate(42, null)).toBe(false); }); }); describe('Operator: isNumber', () => { test('should return true for number', () => { - const operator = new GenericOperator(GenericOperation.isNumber); + const operator = new AnyTypeOperator(AnyTypeOperation.isNumber, { em }); expect(operator.evaluate(42, null)).toBe(true); }); test('should return false for non-number', () => { - const operator = new GenericOperator(GenericOperation.isNumber); + const operator = new AnyTypeOperator(AnyTypeOperation.isNumber, { em }); expect(operator.evaluate('not a number', null)).toBe(false); }); }); describe('Operator: isBoolean', () => { test('should return true for boolean', () => { - const operator = new GenericOperator(GenericOperation.isBoolean); + const operator = new AnyTypeOperator(AnyTypeOperation.isBoolean, { em }); expect(operator.evaluate(true, null)).toBe(true); }); test('should return false for non-boolean', () => { - const operator = new GenericOperator(GenericOperation.isBoolean); + const operator = new AnyTypeOperator(AnyTypeOperation.isBoolean, { em }); expect(operator.evaluate(1, null)).toBe(false); }); }); describe('Edge Case Tests', () => { test('should handle null as input gracefully', () => { - const operator = new GenericOperator(GenericOperation.isNull); + const operator = new AnyTypeOperator(AnyTypeOperation.isNull, { em }); 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/BooleanOperator.ts similarity index 61% rename from packages/core/test/specs/data_sources/model/conditional_variables/operators/LogicalOperator.ts rename to packages/core/test/specs/data_sources/model/conditional_variables/operators/BooleanOperator.ts index a81809858..da2d1af97 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/BooleanOperator.ts @@ -1,59 +1,66 @@ import { - LogicalOperator, - LogicalOperation, -} from '../../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; + BooleanOperator, + BooleanOperation, +} from '../../../../../../src/data_sources/model/conditional_variables/operators/BooleanOperator'; +import Editor from '../../../../../../src/editor/model/Editor'; +import EditorModel from '../../../../../../src/editor/model/Editor'; describe('LogicalOperator', () => { + let em: EditorModel; + + beforeEach(() => { + em = new Editor(); + }); + + afterEach(() => { + em.destroy(); + }); + describe('Operator: and', () => { test('should return true when all statements are true', () => { - const operator = new LogicalOperator(LogicalOperation.and); + const operator = new BooleanOperator(BooleanOperation.and, { em }); 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); + const operator = new BooleanOperator(BooleanOperation.and, { em }); 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); + const operator = new BooleanOperator(BooleanOperation.or, { em }); expect(operator.evaluate([false, true, false])).toBe(true); }); test('should return false when all statements are false', () => { - const operator = new LogicalOperator(LogicalOperation.or); + const operator = new BooleanOperator(BooleanOperation.or, { em }); 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); + const operator = new BooleanOperator(BooleanOperation.xor, { em }); 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); + const operator = new BooleanOperator(BooleanOperation.xor, { em }); expect(operator.evaluate([true, true, false])).toBe(false); }); test('should return false when no statement is true', () => { - const operator = new LogicalOperator(LogicalOperation.xor); + const operator = new BooleanOperator(BooleanOperation.xor, { em }); 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); + const operator = new BooleanOperator(BooleanOperation.xor, { em }); 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 2c719338c..531261046 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 @@ -2,94 +2,101 @@ import { NumberOperator, NumberOperation, } from '../../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import Editor from '../../../../../../src/editor/model/Editor'; +import EditorModel from '../../../../../../src/editor/model/Editor'; describe('NumberOperator', () => { + let em: EditorModel; + + beforeEach(() => { + em = new Editor(); + }); + + afterEach(() => { + em.destroy(); + }); + describe('Operator: greaterThan', () => { test('should return true when left is greater than right', () => { - const operator = new NumberOperator(NumberOperation.greaterThan); + const operator = new NumberOperator(NumberOperation.greaterThan, { em }); expect(operator.evaluate(5, 3)).toBe(true); }); test('should return false when left is not greater than right', () => { - const operator = new NumberOperator(NumberOperation.greaterThan); + const operator = new NumberOperator(NumberOperation.greaterThan, { em }); 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); + const operator = new NumberOperator(NumberOperation.lessThan, { em }); expect(operator.evaluate(2, 3)).toBe(true); }); test('should return false when left is not less than right', () => { - const operator = new NumberOperator(NumberOperation.lessThan); + const operator = new NumberOperator(NumberOperation.lessThan, { em }); 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); + const operator = new NumberOperator(NumberOperation.greaterThanOrEqual, { em }); 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); + const operator = new NumberOperator(NumberOperation.greaterThanOrEqual, { em }); 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); + const operator = new NumberOperator(NumberOperation.lessThanOrEqual, { em }); 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); + const operator = new NumberOperator(NumberOperation.lessThanOrEqual, { em }); expect(operator.evaluate(5, 3)).toBe(false); }); }); describe('Operator: equals', () => { test('should return true when numbers are equal', () => { - const operator = new NumberOperator(NumberOperation.equals); + const operator = new NumberOperator(NumberOperation.equals, { em }); expect(operator.evaluate(4, 4)).toBe(true); }); test('should return false when numbers are not equal', () => { - const operator = new NumberOperator(NumberOperation.equals); + const operator = new NumberOperator(NumberOperation.equals, { em }); 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); + const operator = new NumberOperator(NumberOperation.notEquals, { em }); expect(operator.evaluate(4, 5)).toBe(true); }); test('should return false when numbers are equal', () => { - const operator = new NumberOperator(NumberOperation.notEquals); + const operator = new NumberOperator(NumberOperation.notEquals, { em }); expect(operator.evaluate(4, 4)).toBe(false); }); }); describe('Edge Case Tests', () => { test('should handle boundary values correctly', () => { - const operator = new NumberOperator(NumberOperation.lessThan); + const operator = new NumberOperator(NumberOperation.lessThan, { em }); expect(operator.evaluate(Number.MIN_VALUE, 1)).toBe(true); }); test('should return false for NaN comparisons', () => { - const operator = new NumberOperator(NumberOperation.equals); + const operator = new NumberOperator(NumberOperation.equals, { em }); 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 index 6767aaeeb..29a2a5468 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,70 +1,82 @@ import { StringOperator, StringOperation, -} from '../../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; +} from '../../../../../../src/data_sources/model/conditional_variables/operators/StringOperator'; +import Editor from '../../../../../../src/editor/model/Editor'; +import EditorModel from '../../../../../../src/editor/model/Editor'; describe('StringOperator', () => { + let em: EditorModel; + + beforeEach(() => { + em = new Editor(); + }); + + afterEach(() => { + em.destroy(); + }); + describe('Operator: contains', () => { test('should return true when left contains right', () => { - const operator = new StringOperator(StringOperation.contains); + const operator = new StringOperator(StringOperation.contains, { em }); expect(operator.evaluate('hello world', 'world')).toBe(true); }); test('should return false when left does not contain right', () => { - const operator = new StringOperator(StringOperation.contains); + const operator = new StringOperator(StringOperation.contains, { em }); 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); + const operator = new StringOperator(StringOperation.startsWith, { em }); 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); + const operator = new StringOperator(StringOperation.startsWith, { em }); 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); + const operator = new StringOperator(StringOperation.endsWith, { em }); 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); + const operator = new StringOperator(StringOperation.endsWith, { em }); 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); + const operator = new StringOperator(StringOperation.matchesRegex, { em }); 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); + const operator = new StringOperator(StringOperation.matchesRegex, { em }); expect(operator.evaluate('hello world', '^world')).toBe(false); }); }); describe('Operator: equalsIgnoreCase', () => { test('should return true when left equals right ignoring case', () => { - const operator = new StringOperator(StringOperation.equalsIgnoreCase); + const operator = new StringOperator(StringOperation.equalsIgnoreCase, { em }); expect(operator.evaluate('Hello World', 'hello world')).toBe(true); }); test('should return false when left does not equal right ignoring case', () => { - const operator = new StringOperator(StringOperation.equalsIgnoreCase); + const operator = new StringOperator(StringOperation.equalsIgnoreCase, { em }); expect(operator.evaluate('Hello World', 'hello there')).toBe(false); }); test('should handle empty strings correctly', () => { - const operator = new StringOperator(StringOperation.equalsIgnoreCase); + const operator = new StringOperator(StringOperation.equalsIgnoreCase, { em }); expect(operator.evaluate('', '')).toBe(true); expect(operator.evaluate('Hello', '')).toBe(false); expect(operator.evaluate('', 'Hello')).toBe(false); @@ -73,17 +85,17 @@ describe('StringOperator', () => { describe('Operator: trimEquals', () => { test('should return true when left equals right after trimming', () => { - const operator = new StringOperator(StringOperation.trimEquals); + const operator = new StringOperator(StringOperation.trimEquals, { em }); expect(operator.evaluate(' Hello World ', 'Hello World')).toBe(true); }); test('should return false when left does not equal right after trimming', () => { - const operator = new StringOperator(StringOperation.trimEquals); + const operator = new StringOperator(StringOperation.trimEquals, { em }); expect(operator.evaluate(' Hello World ', 'Hello there')).toBe(false); }); test('should handle cases with only whitespace', () => { - const operator = new StringOperator(StringOperation.trimEquals); + const operator = new StringOperator(StringOperation.trimEquals, { em }); expect(operator.evaluate(' ', '')).toBe(true); // Both should trim to empty expect(operator.evaluate(' ', 'non-empty')).toBe(false); }); @@ -91,28 +103,23 @@ describe('StringOperator', () => { describe('Edge Case Tests', () => { test('should return false for contains with empty right string', () => { - const operator = new StringOperator(StringOperation.contains); + const operator = new StringOperator(StringOperation.contains, { em }); 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); + const operator = new StringOperator(StringOperation.startsWith, { em }); 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); + const operator = new StringOperator(StringOperation.endsWith, { em }); 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); + const operator = new StringOperator(StringOperation.matchesRegex, { em }); 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'); - }); }); }); diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts new file mode 100644 index 000000000..6f889555c --- /dev/null +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts @@ -0,0 +1,350 @@ +import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { + DataCollectionType, + DataCollectionVariableType, +} from '../../../../../src/data_sources/model/data_collection/constants'; +import { DataCollectionStateVariableType } from '../../../../../src/data_sources/model/data_collection/types'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { setupTestEditor } from '../../../../common'; +import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection'; + +describe('Collection component getters and setters', () => { + let em: EditorModel; + let dsm: DataSourceManager; + let dataSource: DataSource; + let wrapper: Component; + let firstRecord: DataRecord; + let secondRecord: DataRecord; + + beforeEach(() => { + ({ em, dsm } = setupTestEditor()); + wrapper = em.getWrapper()!; + dataSource = dsm.add({ + id: 'my_data_source_id', + records: [ + { id: 'user1', user: 'user1', firstName: 'Name1', age: '12' }, + { id: 'user2', user: 'user2', firstName: 'Name2', age: '14' }, + { id: 'user3', user: 'user3', firstName: 'Name3', age: '16' }, + ], + }); + firstRecord = dataSource.getRecord('user1')!; + secondRecord = dataSource.getRecord('user2')!; + }); + + afterEach(() => { + em.destroy(); + }); + + describe('Getters', () => { + let cmp: ComponentDataCollection; + + beforeEach(() => { + cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: 'default', + tagName: 'div', + attributes: { + dataUser: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + }, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + startIndex: 1, + endIndex: 2, + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0] as ComponentDataCollection; + }); + + test('getItemsCount should return the correct number of items', () => { + expect(cmp.getItemsCount()).toBe(2); + }); + + test('getConfigStartIndex should return the correct start index', () => { + expect(cmp.getConfigStartIndex()).toBe(1); + }); + + test('getConfigEndIndex should return the correct end index', () => { + expect(cmp.getConfigEndIndex()).toBe(2); + }); + + test('getComponentDef should return the correct component definition', () => { + const componentDef = cmp.getComponentDef(); + + expect(componentDef.type).toBe('default'); + expect(componentDef.components).toHaveLength(1); + expect(componentDef?.components?.[0].attributes?.['dataUser']).toEqual({ + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }); + }); + + test('getDataSource should return the correct data source', () => { + const ds = cmp.getDataSource(); + + expect(ds).toEqual({ + type: DataVariableType, + path: 'my_data_source_id', + }); + }); + + test('getCollectionId should return the correct collection ID', () => { + expect(cmp.getCollectionId()).toBe('my_collection'); + }); + + test('getItemsCount should return 0 when no records are present', () => { + dataSource.removeRecord('user1'); + dataSource.removeRecord('user2'); + dataSource.removeRecord('user3'); + expect(cmp.getItemsCount()).toBe(0); + }); + + test('getConfigStartIndex should handle zero as a valid start index', () => { + cmp.setStartIndex(0); + + expect(cmp.getConfigStartIndex()).toBe(0); + expect(cmp.getItemsCount()).toBe(3); + }); + + test('getConfigEndIndex should handle zero as a valid end index', () => { + cmp.setEndIndex(0); + + expect(cmp.getConfigStartIndex()).toBe(1); + expect(cmp.getConfigEndIndex()).toBe(0); + expect(cmp.getItemsCount()).toBe(0); + }); + }); + + describe('Setters', () => { + let cmp: ComponentDataCollection; + + beforeEach(() => { + cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: 'default', + tagName: 'div', + attributes: { + dataUser: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + }, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + startIndex: 1, + endIndex: 2, + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0] as ComponentDataCollection; + }); + + test('setComponentDef should update the component definition and reflect in children', () => { + const newComponentDef = { + type: 'newType', + components: [ + { + type: 'default', + tagName: 'span', + attributes: { + 'data-name': { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'firstName', + }, + }, + }, + ], + }; + cmp.setComponentDef(newComponentDef); + + const children = cmp.components(); + expect(children).toHaveLength(2); + expect(children.at(0).get('type')).toBe('newType'); + expect(children.at(0).components().at(0).get('tagName')).toBe('span'); + expect(children.at(0).components().at(0).getAttributes()['data-name']).toBe('Name2'); + }); + + test('setStartIndex should update the start index and reflect in children', () => { + cmp.setStartIndex(0); + expect(cmp.getConfigStartIndex()).toBe(0); + + const children = cmp.components(); + expect(children).toHaveLength(3); + expect(children.at(0).components().at(0).getAttributes()['dataUser']).toBe('user1'); + expect(children.at(1).components().at(0).getAttributes()['dataUser']).toBe('user2'); + expect(children.at(2).components().at(0).getAttributes()['dataUser']).toBe('user3'); + }); + + test('setEndIndex should update the end index and reflect in children', () => { + cmp.setEndIndex(3); + expect(cmp.getConfigEndIndex()).toBe(3); + + const children = cmp.components(); + expect(children).toHaveLength(2); + expect(children.at(0).components().at(0).getAttributes()['dataUser']).toBe('user2'); + expect(children.at(1).components().at(0).getAttributes()['dataUser']).toBe('user3'); + }); + + test('setDataSource should update the data source and reflect in children', () => { + dsm.add({ + id: 'new_data_source_id', + records: [ + { id: 'user4', user: 'user4', firstName: 'Name4', age: '20' }, + { id: 'user5', user: 'user5', firstName: 'Name5', age: '21' }, + ], + }); + + cmp.setDataSource({ + type: DataVariableType, + path: 'new_data_source_id', + }); + + const children = cmp.components(); + expect(children).toHaveLength(1); + expect(children.at(0).components().at(0).getAttributes()['dataUser']).toBe('user5'); + }); + + test('setStartIndex with zero should include the first record', () => { + cmp.setStartIndex(0); + + const children = cmp.components(); + expect(children).toHaveLength(3); + expect(children.at(0).components().at(0).getAttributes()['dataUser']).toBe('user1'); + }); + + test('setEndIndex with zero should result in no children', () => { + cmp.setEndIndex(0); + + const children = cmp.components(); + expect(children).toHaveLength(0); + }); + + test('setDataSource with an empty data source should result in no children', () => { + dsm.add({ + id: 'empty_data_source_id', + records: [], + }); + + cmp.setDataSource({ + type: DataVariableType, + path: 'empty_data_source_id', + }); + + const children = cmp.components(); + expect(children).toHaveLength(0); + }); + }); + + describe('Impact on HTML output', () => { + let cmp: ComponentDataCollection; + + beforeEach(() => { + cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: 'default', + tagName: 'div', + attributes: { + dataUser: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + }, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + startIndex: 1, + endIndex: 2, + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0] as ComponentDataCollection; + }); + + test('HTML output should reflect changes in startIndex', () => { + cmp.setStartIndex(0); + + const html = cmp.toHTML(); + expect(html).toContain('dataUser="user1"'); + expect(html).toContain('dataUser="user2"'); + expect(html).toContain('dataUser="user3"'); + }); + + test('HTML output should reflect changes in endIndex', () => { + cmp.setEndIndex(3); + + const html = cmp.toHTML(); + expect(html).toContain('dataUser="user2"'); + expect(html).toContain('dataUser="user3"'); + }); + + test('HTML output should reflect changes in dataSource', () => { + dsm.add({ + id: 'new_data_source_id', + records: [ + { id: 'user4', user: 'user4', firstName: 'Name4', age: '20' }, + { id: 'user5', user: 'user5', firstName: 'Name5', age: '21' }, + ], + }); + cmp.setDataSource({ + type: DataVariableType, + path: 'new_data_source_id', + }); + + const html = cmp.toHTML(); + expect(html).toContain('dataUser="user5"'); + }); + + test('HTML output should be empty when endIndex is zero', () => { + cmp.setEndIndex(0); + + const html = cmp.toHTML(); + expect(html).not.toContain('dataUser'); + }); + }); +});