From a4f3d20a425c5aabce0c2f3f2c847e1e0706a6a1 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Mon, 10 Mar 2025 12:00:26 +0200 Subject: [PATCH 1/5] Dynamic values improvements (#6419) * Avoid throwing errors if no condition is passed to a DataCondition * Refactor Condition class * Remove expecting errors for conditions tests * Extend and refactor BaseOperator * Rename and refactor GenericOperator * Rename and refactor logicalOperator to BooleanOperator * Refactor StringOperator * Refactor NumberOperator * Refactor and fix DataResolverListener * Update tests for condition Operators * Rename Condition class to ConditionEvaluator * Add missing types file * Update and refactor DataCondition * Update utils * Refactor StyleableModel class * Update ComponentDataCondition * Refactor ComponentResolverWatcher * Fix conditional styles * Rename LogicalGroupStatement to LogicalGroupEvaluator * Fix tests for DataCondition * Add setter methods for component data variable * Add setters and getter to ComponentDataCondition * Add getters to ComponentDataVariable * Rename test file * Make dynamic components undroppable * Fix collection types * Update collections ( add setters and getter, first item editable, sync styles ) * Update data collection tests * Format tests * Fix some ComponentData collection bugs * Refactor setStartIndex and setEndIndex * Fix getComponentDef test * Fix bug with end index = 0 * fix test for setDataSource * fix test for HTML updates * Format * Add tests for the new option in getDataValue for conditions ( skipDynamicValueResolution ) * rename Operation to DataConditionOperation * Run __onStyleChange after style changes as before * Format * Up BooleanOperator --------- Co-authored-by: Artur Arseniev --- .../model/ComponentDataVariable.ts | 17 + .../model/DataResolverListener.ts | 48 ++- .../ComponentDataCondition.ts | 31 +- .../ConditionStatement.ts | 5 +- .../conditional_variables/DataCondition.ts | 107 ++++-- ...Condition.ts => DataConditionEvaluator.ts} | 93 ++--- ...pStatement.ts => LogicalGroupEvaluator.ts} | 10 +- ...{GenericOperator.ts => AnyTypeOperator.ts} | 16 +- .../operators/BaseOperator.ts | 14 + .../operators/BooleanOperator.ts | 25 ++ .../operators/LogicalOperator.ts | 28 -- .../operators/NumberOperator.ts | 13 +- ...{StringOperations.ts => StringOperator.ts} | 15 +- .../conditional_variables/operators/index.ts | 3 - .../conditional_variables/operators/types.ts | 6 + .../ComponentDataCollection.ts | 289 ++++++++++----- .../ComponentDataCollectionVariable.ts | 1 + .../model/data_collection/types.ts | 2 +- packages/core/src/data_sources/model/utils.ts | 5 +- .../model/ComponentDataResolverWatchers.ts | 2 +- .../model/ComponentResolverWatcher.ts | 11 +- .../domain_abstract/model/StyleableModel.ts | 43 +-- .../ComponentDataVariable.getters-setters.ts | 94 +++++ .../ComponentDataCondition.getters-setters.ts | 156 ++++++++ ...lVariable.ts => ComponentDataCondition.ts} | 36 +- .../ConditionalStyles.ts | 26 +- .../conditional_variables/DataCondition.ts | 170 +++++---- ...{GenericOperator.ts => AnyTypeOperator.ts} | 69 ++-- ...{LogicalOperator.ts => BooleanOperator.ts} | 39 +- .../operators/NumberOperator.ts | 45 ++- .../operators/StringOperator.ts | 55 +-- ...ComponentDataCollection.getters-setters.ts | 350 ++++++++++++++++++ 32 files changed, 1326 insertions(+), 498 deletions(-) rename packages/core/src/data_sources/model/conditional_variables/{Condition.ts => DataConditionEvaluator.ts} (53%) rename packages/core/src/data_sources/model/conditional_variables/{LogicalGroupStatement.ts => LogicalGroupEvaluator.ts} (54%) rename packages/core/src/data_sources/model/conditional_variables/operators/{GenericOperator.ts => AnyTypeOperator.ts} (78%) create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts delete mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/LogicalOperator.ts rename packages/core/src/data_sources/model/conditional_variables/operators/{StringOperations.ts => StringOperator.ts} (70%) delete mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/index.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/operators/types.ts create mode 100644 packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts rename packages/core/test/specs/data_sources/model/conditional_variables/{ComponentConditionalVariable.ts => ComponentDataCondition.ts} (87%) rename packages/core/test/specs/data_sources/model/conditional_variables/operators/{GenericOperator.ts => AnyTypeOperator.ts} (61%) rename packages/core/test/specs/data_sources/model/conditional_variables/operators/{LogicalOperator.ts => BooleanOperator.ts} (61%) create mode 100644 packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts 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'); + }); + }); +}); From 414cac6cedfae5dfcab4bf64de522481d50da708 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 10 Mar 2025 17:26:45 +0400 Subject: [PATCH 2/5] Add StudioSdkBannerSidebar (#6437) --- docs/.vuepress/public/studio-banner.jpg | Bin 0 -> 51143 bytes docs/.vuepress/theme/layouts/Layout.vue | 8 +++- .../theme/layouts/StudioSdkBannerSidebar.vue | 43 ++++++++++++++++++ docs/.vuepress/theme/layouts/utils.js | 7 +++ 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 docs/.vuepress/public/studio-banner.jpg create mode 100644 docs/.vuepress/theme/layouts/StudioSdkBannerSidebar.vue create mode 100644 docs/.vuepress/theme/layouts/utils.js diff --git a/docs/.vuepress/public/studio-banner.jpg b/docs/.vuepress/public/studio-banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bd9b2bcde5624fd368979e6090c7d9cb5976d694 GIT binary patch literal 51143 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 G+5ZEeC4dhA literal 0 HcmV?d00001 diff --git a/docs/.vuepress/theme/layouts/Layout.vue b/docs/.vuepress/theme/layouts/Layout.vue index 7bf01643c..20eb116af 100644 --- a/docs/.vuepress/theme/layouts/Layout.vue +++ b/docs/.vuepress/theme/layouts/Layout.vue @@ -1,6 +1,6 @@ @@ -9,13 +9,17 @@ // var CarbonAds = require('./CarbonAds.vue').default; // import Layout from '@vuepress/theme-default/layouts/Layout.vue'; // import CarbonAds from './CarbonAds.vue'; + +// Check all the default slots: +// https://github.com/vuejs/vuepress/blob/9fb4bb00925d9409e6732118e996411c6a82c85d/packages/%40vuepress/theme-default/layouts/Layout.vue import Layout from '@parent-theme/layouts/Layout.vue'; import CarbonAds from './CarbonAds.vue'; +import StudioSdkBannerSidebar from './StudioSdkBannerSidebar.vue'; export default { components: { Layout, - CarbonAds, + StudioSdkBannerSidebar, }, }; diff --git a/docs/.vuepress/theme/layouts/StudioSdkBannerSidebar.vue b/docs/.vuepress/theme/layouts/StudioSdkBannerSidebar.vue new file mode 100644 index 000000000..726258c1a --- /dev/null +++ b/docs/.vuepress/theme/layouts/StudioSdkBannerSidebar.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/docs/.vuepress/theme/layouts/utils.js b/docs/.vuepress/theme/layouts/utils.js new file mode 100644 index 000000000..a216ed46a --- /dev/null +++ b/docs/.vuepress/theme/layouts/utils.js @@ -0,0 +1,7 @@ +export const getSdkUtmParams = (medium = '') => { + return `utm_source=grapesjs-docs&utm_medium=${medium}`; +} + +export const getSdkDocsLink = (medium = '') => { + return `https://app.grapesjs.com/docs-sdk/overview/getting-started?${getSdkUtmParams(medium)}`; +} \ No newline at end of file From 39d7fbcf10f8815bd1ce49ce2233919c5b623b12 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 10 Mar 2025 17:28:06 +0400 Subject: [PATCH 3/5] Release GrapesJS docs: v0.22.4 (#6438) --- docs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/package.json b/docs/package.json index 7c810f3cc..45020bbbd 100644 --- a/docs/package.json +++ b/docs/package.json @@ -2,7 +2,7 @@ "name": "@grapesjs/docs", "private": true, "description": "Free and Open Source Web Builder Framework", - "version": "0.22.3", + "version": "0.22.4", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", "files": [ From 11882c13d2df70cefa03caf65136af133928e3e8 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 10 Mar 2025 20:18:20 +0400 Subject: [PATCH 4/5] Release GrapesJS core rc: v0.22.6-rc.0 (#6439) --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 232747eb3..0e15c7719 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.22.5", + "version": "0.22.6-rc.0", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", From 5471e57669fd8da952f6d431aabc70cd35bbf120 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Tue, 11 Mar 2025 17:48:55 +0200 Subject: [PATCH 5/5] Fix scss path error in windows (#6440) --- packages/core/src/styles/scss/main.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/styles/scss/main.scss b/packages/core/src/styles/scss/main.scss index ac767c625..68a585c15 100644 --- a/packages/core/src/styles/scss/main.scss +++ b/packages/core/src/styles/scss/main.scss @@ -3,7 +3,7 @@ @use 'codemirror/lib/codemirror'; @use 'codemirror/theme/hopscotch'; -@use 'gjs_vars.scss'; +@use 'gjs_vars'; @use 'gjs_main_mixins'; @use 'gjs_status';