From d629989499cdc5bf89b90677240163d2c7cb0566 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Mon, 3 Feb 2025 10:26:23 +0200 Subject: [PATCH 1/2] Add collection components (#6359) --- README.md | 2 +- .../core/src/css_composer/model/CssRule.ts | 2 - .../model/ComponentDataVariable.ts | 14 +- .../model/DataResolverListener.ts | 96 ++ .../core/src/data_sources/model/DataSource.ts | 2 +- .../src/data_sources/model/DataVariable.ts | 36 +- .../model/DataVariableListenerManager.ts | 88 -- .../data_sources/model/StyleDataVariable.ts | 9 - .../data_sources/model/TraitDataVariable.ts | 8 +- .../ComponentDataCondition.ts | 36 + .../model/conditional_variables/Condition.ts | 27 +- .../ConditionalComponent.ts | 46 - .../conditional_variables/DataCondition.ts | 80 +- .../LogicalGroupStatement.ts | 7 +- .../operators/GenericOperator.ts | 2 +- .../ComponentDataCollection.ts | 284 +++++ .../ComponentDataCollectionVariable.ts | 53 + .../data_collection/DataCollectionVariable.ts | 158 +++ .../model/data_collection/constants.ts | 5 + .../model/data_collection/types.ts | 57 + packages/core/src/data_sources/model/utils.ts | 60 +- packages/core/src/data_sources/types.ts | 19 +- .../ComponentDataCollectionVariableView.ts | 21 + .../view/ComponentDataCollectionView.ts | 4 + .../view/ComponentDataConditionView.ts | 4 + .../view/ComponentDataVariableView.ts | 21 +- .../data_sources/view/ComponentDynamicView.ts | 4 - packages/core/src/dom_components/index.ts | 29 +- .../src/dom_components/model/Component.ts | 96 +- .../model/ComponentDataResolverWatchers.ts | 116 ++ .../model/ComponentDynamicValueWatcher.ts | 66 -- .../model/ComponentResolverWatcher.ts | 180 +++ .../model/DynamicValueWatcher.ts | 117 -- .../src/dom_components/model/SymbolUtils.ts | 78 +- .../core/src/dom_components/model/types.ts | 6 +- .../domain_abstract/model/StyleableModel.ts | 102 +- .../core/src/trait_manager/model/Trait.ts | 5 - packages/core/src/utils/mixins.ts | 2 + .../ComponentConditionalVariable.ts | 31 +- .../ConditionalStyles.ts | 11 +- .../ConditionalTraits.ts | 10 +- .../conditional_variables/DataCondition.ts | 39 +- .../ComponentDataCollection.ts | 1004 +++++++++++++++++ .../ComponentDataCollectionVariable.ts | 264 +++++ .../ComponentDataCollection.ts.snap | 519 +++++++++ .../ComponentDataCollectionVariable.ts.snap | 141 +++ .../nestedComponentDataCollections.ts.snap | 36 + .../nestedComponentDataCollections.ts | 430 +++++++ 48 files changed, 3780 insertions(+), 647 deletions(-) create mode 100644 packages/core/src/data_sources/model/DataResolverListener.ts delete mode 100644 packages/core/src/data_sources/model/DataVariableListenerManager.ts delete mode 100644 packages/core/src/data_sources/model/StyleDataVariable.ts create mode 100644 packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts delete mode 100644 packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts create mode 100644 packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts create mode 100644 packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts create mode 100644 packages/core/src/data_sources/model/data_collection/DataCollectionVariable.ts create mode 100644 packages/core/src/data_sources/model/data_collection/constants.ts create mode 100644 packages/core/src/data_sources/model/data_collection/types.ts create mode 100644 packages/core/src/data_sources/view/ComponentDataCollectionVariableView.ts create mode 100644 packages/core/src/data_sources/view/ComponentDataCollectionView.ts create mode 100644 packages/core/src/data_sources/view/ComponentDataConditionView.ts delete mode 100644 packages/core/src/data_sources/view/ComponentDynamicView.ts create mode 100644 packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts delete mode 100644 packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts create mode 100644 packages/core/src/dom_components/model/ComponentResolverWatcher.ts delete mode 100644 packages/core/src/dom_components/model/DynamicValueWatcher.ts create mode 100644 packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts create mode 100644 packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionVariable.ts create mode 100644 packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap create mode 100644 packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionVariable.ts.snap create mode 100644 packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap create mode 100644 packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts diff --git a/README.md b/README.md index 96a9dd409..e24cf16a1 120000 --- a/README.md +++ b/README.md @@ -1 +1 @@ -./packages/core/README.md \ No newline at end of file +./packages/core/README.md diff --git a/packages/core/src/css_composer/model/CssRule.ts b/packages/core/src/css_composer/model/CssRule.ts index d90a0d3f1..2579eba49 100644 --- a/packages/core/src/css_composer/model/CssRule.ts +++ b/packages/core/src/css_composer/model/CssRule.ts @@ -7,7 +7,6 @@ import { isEmptyObj, hasWin } from '../../utils/mixins'; import Selector, { SelectorProps } from '../../selector_manager/model/Selector'; import EditorModel from '../../editor/model/Editor'; import CssRuleView from '../view/CssRuleView'; -import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; /** @private */ export interface CssRuleProperties { @@ -95,7 +94,6 @@ export default class CssRule extends StyleableModel { em?: EditorModel; opt: any; views: CssRuleView[] = []; - dynamicVariableListeners: Record = {}; defaults() { return { diff --git a/packages/core/src/data_sources/model/ComponentDataVariable.ts b/packages/core/src/data_sources/model/ComponentDataVariable.ts index ac118f4c1..2c980d48f 100644 --- a/packages/core/src/data_sources/model/ComponentDataVariable.ts +++ b/packages/core/src/data_sources/model/ComponentDataVariable.ts @@ -1,8 +1,11 @@ import Component from '../../dom_components/model/Component'; +import { ComponentOptions } from '../../dom_components/model/types'; import { toLowerCase } from '../../utils/mixins'; -import { DataVariableType } from './DataVariable'; +import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable'; export default class ComponentDataVariable extends Component { + dataResolver: DataVariable; + get defaults() { return { // @ts-ignore @@ -13,9 +16,14 @@ export default class ComponentDataVariable extends Component { }; } + constructor(props: DataVariableProps, opt: ComponentOptions) { + super(props, opt); + const { type, path, defaultValue } = props; + this.dataResolver = new DataVariable({ type, path, defaultValue }, opt); + } + getDataValue() { - const { path, defaultValue } = this.attributes; - return this.em.DataSources.getValue(path, defaultValue); + return this.dataResolver.getDataValue(); } getInnerHTML() { diff --git a/packages/core/src/data_sources/model/DataResolverListener.ts b/packages/core/src/data_sources/model/DataResolverListener.ts new file mode 100644 index 000000000..7e7d7a4b4 --- /dev/null +++ b/packages/core/src/data_sources/model/DataResolverListener.ts @@ -0,0 +1,96 @@ +import { DataSourcesEvents, DataSourceListener } from '../types'; +import { stringToPath } from '../../utils/mixins'; +import { Model } from '../../common'; +import EditorModel from '../../editor/model/Editor'; +import DataVariable, { DataVariableType } from './DataVariable'; +import { DataResolver } from '../types'; +import { DataCondition, DataConditionType } from './conditional_variables/DataCondition'; +import { DataCollectionVariableType } from './data_collection/constants'; +import DataCollectionVariable from './data_collection/DataCollectionVariable'; + +export interface DataResolverListenerProps { + em: EditorModel; + resolver: DataResolver; + onUpdate: (value: any) => void; +} + +export default class DataResolverListener { + private listeners: DataSourceListener[] = []; + private em: EditorModel; + private onUpdate: (value: any) => void; + private model = new Model(); + resolver: DataResolver; + + constructor(props: DataResolverListenerProps) { + this.em = props.em; + this.resolver = props.resolver; + this.onUpdate = props.onUpdate; + this.listenToResolver(); + } + + private onChange = () => { + const value = this.resolver.getDataValue(); + this.onUpdate(value); + }; + + listenToResolver() { + const { resolver, model } = this; + this.removeListeners(); + let listeners: DataSourceListener[] = []; + const type = resolver.attributes.type; + + switch (type) { + case DataCollectionVariableType: + listeners = this.listenToDataCollectionVariable(resolver as DataCollectionVariable); + break; + case DataVariableType: + listeners = this.listenToDataVariable(resolver as DataVariable); + break; + case DataConditionType: + listeners = this.listenToConditionalVariable(resolver as DataCondition); + break; + } + + listeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange)); + this.listeners = listeners; + } + + private listenToConditionalVariable(dataVariable: DataCondition) { + const { em } = this; + const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { + return this.listenToDataVariable(new DataVariable(dataVariable, { em })); + }); + + return dataListeners; + } + + private listenToDataVariable(dataVariable: DataVariable) { + 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' }); + dataListeners.push( + { obj: dataVariable, event: 'change:path change:defaultValue' }, + { obj: em.DataSources.all, event: 'add remove reset' }, + { obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, + ); + + return dataListeners; + } + + private listenToDataCollectionVariable(dataVariable: DataCollectionVariable) { + return [{ obj: dataVariable, event: 'change:value' }]; + } + + private removeListeners() { + this.listeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange)); + this.listeners = []; + } + + destroy() { + this.removeListeners(); + } +} diff --git a/packages/core/src/data_sources/model/DataSource.ts b/packages/core/src/data_sources/model/DataSource.ts index 4ea82ddfb..3b8aeff94 100644 --- a/packages/core/src/data_sources/model/DataSource.ts +++ b/packages/core/src/data_sources/model/DataSource.ts @@ -31,7 +31,7 @@ import { AddOptions, collectionEvents, CombinedModelConstructorOptions, Model, RemoveOptions } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import { DataSourceTransformers, DataSourceType, DataSourceProps, RecordPropsType, DataRecordProps } from '../types'; +import { DataSourceTransformers, DataSourceType, DataSourceProps, DataRecordProps } from '../types'; import DataRecord from './DataRecord'; import DataRecords from './DataRecords'; import DataSources from './DataSources'; diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index 915642ebf..caaaa5722 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -1,15 +1,15 @@ import { Model } from '../../common'; import EditorModel from '../../editor/model/Editor'; -import { stringToPath } from '../../utils/mixins'; -export const DataVariableType = 'data-variable'; -export type DataVariableDefinition = { +export const DataVariableType = 'data-variable' as const; + +export interface DataVariableProps { type: typeof DataVariableType; path: string; defaultValue?: string; -}; +} -export default class DataVariable extends Model { +export default class DataVariable extends Model { em?: EditorModel; defaults() { @@ -20,33 +20,13 @@ export default class DataVariable extends Model { }; } - constructor(attrs: DataVariableDefinition, options: any) { - super(attrs, options); + constructor(props: DataVariableProps, options: { em?: EditorModel }) { + super(props, options); this.em = options.em; - this.listenToDataSource(); - } - - listenToDataSource() { - const { path } = this.attributes; - const resolvedPath = stringToPath(path).join('.'); - - if (this.em) { - this.listenTo(this.em.DataSources, `change:${resolvedPath}`, this.onDataSourceChange); - } - } - - onDataSourceChange() { - const newValue = this.getDataValue(); - this.set({ value: newValue }); } getDataValue() { const { path, defaultValue } = this.attributes; - if (!this.em) { - throw new Error('EditorModel instance is not provided for a data variable.'); - } - const val = this.em?.DataSources.getValue(path, defaultValue); - - return val; + return this.em?.DataSources.getValue(path!, defaultValue); } } diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts deleted file mode 100644 index ccd119aa9..000000000 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { DataSourcesEvents, DataVariableListener } from '../types'; -import { stringToPath } from '../../utils/mixins'; -import { Model } from '../../common'; -import EditorModel from '../../editor/model/Editor'; -import DataVariable, { DataVariableType } from './DataVariable'; -import { DynamicValue } from '../types'; -import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition'; -import ComponentDataVariable from './ComponentDataVariable'; - -export interface DynamicVariableListenerManagerOptions { - em: EditorModel; - dataVariable: DynamicValue; - updateValueFromDataVariable: (value: any) => void; -} - -export default class DynamicVariableListenerManager { - private dataListeners: DataVariableListener[] = []; - private em: EditorModel; - dynamicVariable: DynamicValue; - private updateValueFromDynamicVariable: (value: any) => void; - private model = new Model(); - - constructor(options: DynamicVariableListenerManagerOptions) { - this.em = options.em; - this.dynamicVariable = options.dataVariable; - this.updateValueFromDynamicVariable = options.updateValueFromDataVariable; - - this.listenToDynamicVariable(); - } - - private onChange = () => { - const value = this.dynamicVariable.getDataValue(); - this.updateValueFromDynamicVariable(value); - }; - - listenToDynamicVariable() { - const { em, dynamicVariable } = this; - this.removeListeners(); - - // @ts-ignore - const type = dynamicVariable.get('type'); - let dataListeners: DataVariableListener[] = []; - switch (type) { - case DataVariableType: - dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em); - break; - case ConditionalVariableType: - dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em); - break; - } - dataListeners.forEach((ls) => this.model.listenTo(ls.obj, ls.event, this.onChange)); - - this.dataListeners = dataListeners; - } - - private listenToConditionalVariable(dataVariable: DataCondition, em: EditorModel) { - const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { - return this.listenToDataVariable(new DataVariable(dataVariable, { em: this.em }), em); - }); - - return dataListeners; - } - - private listenToDataVariable(dataVariable: DataVariable | ComponentDataVariable, em: EditorModel) { - const dataListeners: DataVariableListener[] = []; - const { path } = dataVariable.attributes; - const normPath = stringToPath(path || '').join('.'); - const [ds, dr] = this.em.DataSources.fromPath(path); - ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); - dr && dataListeners.push({ obj: dr, event: 'change' }); - dataListeners.push( - { obj: dataVariable, event: 'change:path change:defaultValue' }, - { obj: em.DataSources.all, event: 'add remove reset' }, - { obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, - ); - - return dataListeners; - } - - private removeListeners() { - this.dataListeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange)); - this.dataListeners = []; - } - - destroy() { - this.removeListeners(); - } -} diff --git a/packages/core/src/data_sources/model/StyleDataVariable.ts b/packages/core/src/data_sources/model/StyleDataVariable.ts deleted file mode 100644 index bec65ba17..000000000 --- a/packages/core/src/data_sources/model/StyleDataVariable.ts +++ /dev/null @@ -1,9 +0,0 @@ -import DataVariable from './DataVariable'; - -export default class StyleDataVariable extends DataVariable { - defaults() { - return { - ...super.defaults(), - }; - } -} diff --git a/packages/core/src/data_sources/model/TraitDataVariable.ts b/packages/core/src/data_sources/model/TraitDataVariable.ts index f213e4b73..df26de066 100644 --- a/packages/core/src/data_sources/model/TraitDataVariable.ts +++ b/packages/core/src/data_sources/model/TraitDataVariable.ts @@ -1,14 +1,14 @@ -import DataVariable, { DataVariableDefinition } from './DataVariable'; +import DataVariable, { DataVariableProps } from './DataVariable'; import Trait from '../../trait_manager/model/Trait'; import { TraitProperties } from '../../trait_manager/types'; -export type TraitDataVariableDefinition = TraitProperties & DataVariableDefinition; +export interface TraitDataVariableProps extends Omit, DataVariableProps {} export default class TraitDataVariable extends DataVariable { trait?: Trait; - constructor(attrs: TraitDataVariableDefinition, options: any) { - super(attrs, options); + constructor(props: TraitDataVariableProps, options: any) { + super(props, options); this.trait = options.trait; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts new file mode 100644 index 000000000..a2a9e19ca --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts @@ -0,0 +1,36 @@ +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'; + +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 }); + super( + { + ...props, + type: DataConditionType, + components: dataConditionInstance.getDataValue(), + }, + opt, + ); + this.dataResolver = dataConditionInstance; + this.dataResolver.onValueChange = this.handleConditionChange.bind(this); + } + + private handleConditionChange() { + this.dataResolver.reevaluate(); + this.components(this.dataResolver.getDataValue()); + } + + static isComponent(el: HTMLElement) { + return toLowerCase(el.tagName) === DataConditionType; + } + + toJSON(): ComponentDefinition { + return this.dataResolver.toJSON(); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/Condition.ts index a17fd70bf..5f582b3e5 100644 --- a/packages/core/src/data_sources/model/conditional_variables/Condition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/Condition.ts @@ -1,7 +1,7 @@ -import { DataVariableDefinition, DataVariableType } from './../DataVariable'; +import { DataVariableProps } from './../DataVariable'; import EditorModel from '../../../editor/model/Editor'; import { evaluateVariable, isDataVariable } from '../utils'; -import { ExpressionDefinition, LogicGroupDefinition } from './DataCondition'; +import { ExpressionProps, LogicGroupProps } from './DataCondition'; import { LogicalGroupStatement } from './LogicalGroupStatement'; import { Operator } from './operators'; import { GenericOperation, GenericOperator } from './operators/GenericOperator'; @@ -10,13 +10,15 @@ import { NumberOperator, NumberOperation } from './operators/NumberOperator'; import { StringOperator, StringOperation } from './operators/StringOperations'; import { Model } from '../../../common'; +export type ConditionProps = ExpressionProps | LogicGroupProps | boolean; + export class Condition extends Model { - private condition: ExpressionDefinition | LogicGroupDefinition | boolean; + private condition: ConditionProps; private em: EditorModel; - constructor(condition: ExpressionDefinition | LogicGroupDefinition | boolean, opts: { em: EditorModel }) { - super(condition); - this.condition = condition; + constructor(props: ConditionProps, opts: { em: EditorModel }) { + super(props); + this.condition = props; this.em = opts.em; } @@ -27,7 +29,7 @@ export class Condition extends Model { /** * Recursively evaluates conditions and logic groups. */ - private evaluateCondition(condition: any): boolean { + private evaluateCondition(condition: ConditionProps): boolean { if (typeof condition === 'boolean') return condition; if (this.isLogicGroup(condition)) { @@ -68,7 +70,7 @@ export class Condition extends Model { * Extracts all data variables from the condition, including nested ones. */ getDataVariables() { - const variables: DataVariableDefinition[] = []; + const variables: DataVariableProps[] = []; this.extractVariables(this.condition, variables); return variables; } @@ -76,10 +78,7 @@ export class Condition extends Model { /** * Recursively extracts variables from expressions or logic groups. */ - private extractVariables( - condition: boolean | LogicGroupDefinition | ExpressionDefinition, - variables: DataVariableDefinition[], - ): void { + private extractVariables(condition: ConditionProps, variables: DataVariableProps[]): void { if (this.isExpression(condition)) { if (isDataVariable(condition.left)) variables.push(condition.left); if (isDataVariable(condition.right)) variables.push(condition.right); @@ -91,14 +90,14 @@ export class Condition extends Model { /** * Checks if a condition is a LogicGroup. */ - private isLogicGroup(condition: any): condition is LogicGroupDefinition { + 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 ExpressionDefinition { + private isExpression(condition: any): condition is ExpressionProps { return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts deleted file mode 100644 index c6bba9aec..000000000 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Component from '../../../dom_components/model/Component'; -import Components from '../../../dom_components/model/Components'; -import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; -import { toLowerCase } from '../../../utils/mixins'; -import { DataCondition, ConditionalVariableType, ExpressionDefinition, LogicGroupDefinition } from './DataCondition'; - -type ConditionalComponentDefinition = { - condition: ExpressionDefinition | LogicGroupDefinition | boolean; - ifTrue: any; - ifFalse: any; -}; - -export default class ComponentConditionalVariable extends Component { - dataCondition: DataCondition; - componentDefinition: ConditionalComponentDefinition; - - constructor(componentDefinition: ConditionalComponentDefinition, opt: ComponentOptions) { - const { condition, ifTrue, ifFalse } = componentDefinition; - const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); - const initialComponentsProps = dataConditionInstance.getDataValue(); - const conditionalCmptDef = { - type: ConditionalVariableType, - components: initialComponentsProps, - }; - super(conditionalCmptDef, opt); - - this.componentDefinition = componentDefinition; - this.dataCondition = dataConditionInstance; - this.dataCondition.onValueChange = this.handleConditionChange.bind(this); - } - - private handleConditionChange() { - this.dataCondition.reevaluate(); - const updatedComponents = this.dataCondition.getDataValue(); - this.components().reset(); - this.components().add(updatedComponents); - } - - static isComponent(el: HTMLElement) { - return toLowerCase(el.tagName) === ConditionalVariableType; - } - - toJSON(): ComponentDefinition { - return this.dataCondition.toJSON(); - } -} diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 6208a318f..0d46e8dda 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -1,49 +1,46 @@ -import { NumberOperation } from './operators/NumberOperator'; -import { StringOperation } from './operators/StringOperations'; -import { GenericOperation } from './operators/GenericOperator'; import { Model } from '../../../common'; -import { LogicalOperation } from './operators/LogicalOperator'; -import DynamicVariableListenerManager from '../DataVariableListenerManager'; import EditorModel from '../../../editor/model/Editor'; -import { Condition } from './Condition'; -import DataVariable, { DataVariableDefinition } from '../DataVariable'; +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 { NumberOperation } from './operators/NumberOperator'; +import { StringOperation } from './operators/StringOperations'; + +export const DataConditionType = 'data-condition'; -export const ConditionalVariableType = 'conditional-variable'; -export type ExpressionDefinition = { +export interface ExpressionProps { left: any; operator: GenericOperation | StringOperation | NumberOperation; right: any; -}; +} -export type LogicGroupDefinition = { +export interface LogicGroupProps { logicalOperator: LogicalOperation; - statements: (ExpressionDefinition | LogicGroupDefinition | boolean)[]; -}; + statements: ConditionProps[]; +} -export type ConditionDefinition = ExpressionDefinition | LogicGroupDefinition | boolean; -export type ConditionalVariableDefinition = { - type: typeof ConditionalVariableType; - condition: ConditionDefinition; +export interface DataConditionProps { + type: typeof DataConditionType; + condition: ConditionProps; ifTrue: any; ifFalse: any; -}; +} -type DataConditionType = { - type: typeof ConditionalVariableType; +interface DataConditionPropsDefined extends Omit { condition: Condition; - ifTrue: any; - ifFalse: any; -}; -export class DataCondition extends Model { +} + +export class DataCondition extends Model { lastEvaluationResult: boolean; - private condition: Condition; private em: EditorModel; - private variableListeners: DynamicVariableListenerManager[] = []; + private resolverListeners: DataResolverListener[] = []; private _onValueChange?: () => void; constructor( - condition: ExpressionDefinition | LogicGroupDefinition | boolean, + condition: ConditionProps, public ifTrue: any, public ifFalse: any, opts: { em: EditorModel; onValueChange?: () => void }, @@ -54,18 +51,21 @@ export class DataCondition extends Model { const conditionInstance = new Condition(condition, { em: opts.em }); super({ - type: ConditionalVariableType, + type: DataConditionType, condition: conditionInstance, ifTrue, ifFalse, }); - this.condition = conditionInstance; this.em = opts.em; this.lastEvaluationResult = this.evaluate(); this.listenToDataVariables(); this._onValueChange = opts.onValueChange; } + get condition() { + return this.get('condition')!; + } + evaluate() { return this.condition.evaluate(); } @@ -84,7 +84,8 @@ export class DataCondition extends Model { } private listenToDataVariables() { - if (!this.em) return; + const { em } = this; + if (!em) return; // Clear previous listeners to avoid memory leaks this.cleanupListeners(); @@ -92,22 +93,21 @@ export class DataCondition extends Model { const dataVariables = this.getDependentDataVariables(); dataVariables.forEach((variable) => { - const variableInstance = new DataVariable(variable, { em: this.em }); - const listener = new DynamicVariableListenerManager({ - em: this.em!, - dataVariable: variableInstance, - updateValueFromDataVariable: (() => { + const listener = new DataResolverListener({ + em, + resolver: new DataVariable(variable, { em: this.em }), + onUpdate: (() => { this.reevaluate(); this._onValueChange?.(); }).bind(this), }); - this.variableListeners.push(listener); + this.resolverListeners.push(listener); }); } getDependentDataVariables() { - const dataVariables: DataVariableDefinition[] = this.condition.getDataVariables(); + const dataVariables: DataVariableProps[] = this.condition.getDataVariables(); if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); @@ -115,13 +115,13 @@ export class DataCondition extends Model { } private cleanupListeners() { - this.variableListeners.forEach((listener) => listener.destroy()); - this.variableListeners = []; + this.resolverListeners.forEach((listener) => listener.destroy()); + this.resolverListeners = []; } toJSON() { return { - type: ConditionalVariableType, + type: DataConditionType, condition: this.condition, ifTrue: this.ifTrue, ifFalse: this.ifFalse, diff --git a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts index 284e99e96..c3294a02c 100644 --- a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts @@ -1,14 +1,13 @@ -import { LogicalOperator } from './operators/LogicalOperator'; -import { ExpressionDefinition, LogicGroupDefinition } from './DataCondition'; -import { Condition } from './Condition'; import EditorModel from '../../../editor/model/Editor'; +import { Condition, ConditionProps } from './Condition'; +import { LogicalOperator } from './operators/LogicalOperator'; export class LogicalGroupStatement { private em: EditorModel; constructor( private operator: LogicalOperator, - private statements: (ExpressionDefinition | LogicGroupDefinition | boolean)[], + private statements: ConditionProps[], opts: { em: EditorModel }, ) { this.em = opts.em; diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts index 45ab4943d..d4b1e035d 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts @@ -46,7 +46,7 @@ export class GenericOperator extends Operator { case 'isBoolean': return typeof left === 'boolean'; case 'isDefaultValue': - return left instanceof DataVariable && left.get('default') === right; + return left instanceof DataVariable && left.get('defaultValue') === right; default: throw new Error(`Unsupported generic operator: ${this.operator}`); } diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts new file mode 100644 index 000000000..1baf6bb62 --- /dev/null +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -0,0 +1,284 @@ +import { isArray } from 'underscore'; +import { ObjectAny } from '../../../common'; +import Component from '../../../dom_components/model/Component'; +import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; +import EditorModel from '../../../editor/model/Editor'; +import { isObject, serialize, toLowerCase } from '../../../utils/mixins'; +import DataResolverListener from '../DataResolverListener'; +import DataSource from '../DataSource'; +import DataVariable, { DataVariableProps, DataVariableType } from '../DataVariable'; +import { isDataVariable } from '../utils'; +import { DataCollectionType, keyCollectionDefinition, keyCollectionsStateMap, keyIsCollectionItem } from './constants'; +import { + ComponentDataCollectionProps, + DataCollectionConfig, + DataCollectionDataSource, + DataCollectionProps, + DataCollectionState, + DataCollectionStateMap, +} from './types'; +import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers'; + +export default class ComponentDataCollection extends Component { + 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; + } + + const em = opt.em; + const newProps = { ...props, components: undefined, droppable: false } as any; + const cmp: ComponentDataCollection = super(newProps, opt) as unknown as ComponentDataCollection; + + if (!collectionDef) { + em.logError('missing collection definition'); + return cmp; + } + + const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as DataCollectionStateMap; + const components: Component[] = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt); + cmp.components(components, opt); + + if (isDataVariable(this.collectionDataSource)) { + this.watchDataSource(parentCollectionStateMap, opt); + } + + return cmp; + } + + get collectionConfig() { + return this.get(keyCollectionDefinition).collectionConfig as DataCollectionConfig; + } + + get collectionDataSource() { + return this.collectionConfig.dataSource; + } + + 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; + } + + private getComponentDef() { + const firstChild = this.components().at(0); + const firstChildJSON = firstChild ? serialize(firstChild) : this.get(keyCollectionDefinition).componentDef; + delete firstChildJSON?.draggable; + return firstChildJSON; + } + + private watchDataSource(parentCollectionStateMap: DataCollectionStateMap, opt: ComponentOptions) { + const { em } = this; + const path = this.collectionDataSource?.path; + if (!path) return; + + 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); + }, + }); + } + + 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 []; + } + + 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), + }; + + if (parentCollectionStateMap[collectionId]) { + em.logError( + `The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`, + ); + return []; + } + + const collectionsStateMap: DataCollectionStateMap = { + ...parentCollectionStateMap, + [collectionId]: collectionState, + }; + + 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 instance = symbolMain!.clone({ symbol: true }); + setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance); + + components.push(instance); + } + + return components; +} + +function setCollectionStateMapAndPropagate( + collectionsStateMap: DataCollectionStateMap, + collectionId: string | undefined, +) { + return (cmp: Component) => { + setCollectionStateMap(collectionsStateMap)(cmp); + + const addListener = (component: Component) => { + setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(component); + }; + + 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); + + const removeListener = (component: Component) => { + component.stopListening(component.components(), 'add', addListener); + component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); + const index = component.collectionStateListeners.indexOf(listenerKey); + if (index > -1) { + component.collectionStateListeners.splice(index, 1); + } + }; + + cmp.listenTo(cmps, 'remove', removeListener); + } + + cmps?.toArray().forEach((component: Component) => { + setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(component); + }); + + cmp.on(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); + }; +} + +function handleCollectionStateMapChange(this: Component) { + const updatedCollectionsStateMap = this.get(keyCollectionsStateMap); + this.components() + ?.toArray() + .forEach((component: Component) => { + setCollectionStateMap(updatedCollectionsStateMap)(component); + }); +} + +function logErrorIfMissing(property: any, propertyPath: string, em: EditorModel) { + if (!property) { + em.logError(`The "${propertyPath}" property is required in the collection definition.`); + return false; + } + return true; +} + +function validateCollectionConfig( + collectionConfig: DataCollectionConfig, + componentDef: ComponentDefinition, + em: EditorModel, +) { + const validations = [ + { property: collectionConfig, propertyPath: 'collectionConfig' }, + { property: componentDef, propertyPath: 'componentDef' }, + { property: collectionConfig?.collectionId, propertyPath: 'collectionConfig.collectionId' }, + { property: collectionConfig?.dataSource, propertyPath: 'collectionConfig.dataSource' }, + ]; + + for (const { property, propertyPath } of validations) { + if (!logErrorIfMissing(property, propertyPath, em)) { + return []; + } + } + + return true; +} + +function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { + return (cmp: Component) => { + cmp.set(keyIsCollectionItem, true); + const updatedCollectionStateMap = { + ...cmp.get(keyCollectionsStateMap), + ...collectionsStateMap, + }; + cmp.set(keyCollectionsStateMap, updatedCollectionStateMap); + cmp.dataResolverWatchers.updateCollectionStateMap(updatedCollectionStateMap); + }; +} + +function getDataSourceItems(dataSource: DataCollectionDataSource, em: EditorModel) { + let items: DataVariableProps[] = []; + + switch (true) { + case isArray(dataSource): + items = dataSource; + break; + case isObject(dataSource) && dataSource instanceof DataSource: { + const id = dataSource.get('id')!; + items = listDataSourceVariables(id, em); + break; + } + case isDataVariable(dataSource): { + const isDataSourceId = dataSource.path.split('.').length === 1; + if (isDataSourceId) { + 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; + } + default: + } + + return items; +} + +function listDataSourceVariables(dataSource_id: string, em: EditorModel): DataVariableProps[] { + const records = em.DataSources.getValue(dataSource_id, []); + const keys = Object.keys(records); + + return keys.map((key) => ({ + type: DataVariableType, + path: dataSource_id + '.' + key, + })); +} diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts new file mode 100644 index 000000000..a4d7a7f6e --- /dev/null +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts @@ -0,0 +1,53 @@ +import Component from '../../../dom_components/model/Component'; +import { ComponentOptions } from '../../../dom_components/model/types'; +import { toLowerCase } from '../../../utils/mixins'; +import DataCollectionVariable from './DataCollectionVariable'; +import { DataCollectionVariableType, keyCollectionsStateMap } from './constants'; +import { ComponentDataCollectionVariableProps, DataCollectionStateMap } from './types'; + +export default class ComponentDataCollectionVariable extends Component { + dataResolver: DataCollectionVariable; + + get defaults() { + // @ts-expect-error + const componentDefaults = super.defaults; + + return { + ...componentDefaults, + type: DataCollectionVariableType, + collectionId: undefined, + variableType: undefined, + path: undefined, + }; + } + + constructor(props: ComponentDataCollectionVariableProps, opt: ComponentOptions) { + super(props, opt); + const { type, variableType, path, collectionId } = props; + this.dataResolver = new DataCollectionVariable( + { type, variableType, path, collectionId }, + { + ...opt, + collectionsStateMap: this.get(keyCollectionsStateMap), + }, + ); + + this.listenTo(this, `change:${keyCollectionsStateMap}`, this.handleCollectionsMapStateUpdate); + } + + private handleCollectionsMapStateUpdate(m: any, v: DataCollectionStateMap, opts = {}) { + this.dataResolver.updateCollectionsStateMap(v); + } + + getDataValue() { + return this.dataResolver.getDataValue(); + } + + getInnerHTML() { + return this.getDataValue(); + } + + static isComponent(el: HTMLElement) { + return toLowerCase(el.tagName) === DataCollectionVariableType; + } +} diff --git a/packages/core/src/data_sources/model/data_collection/DataCollectionVariable.ts b/packages/core/src/data_sources/model/data_collection/DataCollectionVariable.ts new file mode 100644 index 000000000..914531b0d --- /dev/null +++ b/packages/core/src/data_sources/model/data_collection/DataCollectionVariable.ts @@ -0,0 +1,158 @@ +import { DataCollectionVariableProps } from './types'; +import { Model } from '../../../common'; +import EditorModel from '../../../editor/model/Editor'; +import DataVariable, { DataVariableType } from '../DataVariable'; +import { DataCollectionVariableType } from './constants'; +import { DataCollectionState, DataCollectionStateMap } from './types'; +import DataResolverListener from '../DataResolverListener'; + +interface DataCollectionVariablePropsDefined extends DataCollectionVariableProps { + value?: any; +} + +export default class DataCollectionVariable extends Model { + em: EditorModel; + collectionsStateMap?: DataCollectionStateMap; + dataVariable?: DataVariable; + resolverListener?: DataResolverListener; + + defaults(): Partial { + return { + type: DataCollectionVariableType, + collectionId: undefined, + variableType: undefined, + path: undefined, + value: undefined, + }; + } + + constructor( + props: DataCollectionVariablePropsDefined, + options: { + em: EditorModel; + collectionsStateMap?: DataCollectionStateMap; + }, + ) { + super(props, options); + this.em = options.em; + this.collectionsStateMap = options.collectionsStateMap; + this.updateDataVariable(); + } + + hasDynamicValue() { + return !!this.dataVariable; + } + + getDataValue() { + const { resolvedValue } = this.updateDataVariable(); + + if (resolvedValue?.type === DataVariableType) { + return this.dataVariable!.getDataValue(); + } + + return resolvedValue; + } + + private updateDataVariable() { + if (!this.collectionsStateMap) return { resolvedValue: undefined }; + + const resolvedValue = resolveCollectionVariable( + this.attributes as DataCollectionVariableProps, + this.collectionsStateMap, + this.em, + ); + + let dataVariable; + if (resolvedValue?.type === DataVariableType) { + dataVariable = new DataVariable(resolvedValue, { em: this.em }); + this.dataVariable = dataVariable; + + this.resolverListener?.destroy(); + this.resolverListener = new DataResolverListener({ + em: this.em, + resolver: dataVariable, + onUpdate: () => { + this.set('value', this.dataVariable?.getDataValue()); + }, + }); + } + + this.set('value', resolvedValue); + return { resolvedValue, dataVariable }; + } + + updateCollectionsStateMap(collectionsStateMap: DataCollectionStateMap) { + this.collectionsStateMap = collectionsStateMap; + this.updateDataVariable(); + } + + destroy() { + this.resolverListener?.destroy(); + this.dataVariable?.destroy(); + + return super.destroy(); + } + + toJSON(options?: any) { + const json = super.toJSON(options); + delete json.value; + !json.collectionId && delete json.collectionId; + + return json; + } +} + +function resolveCollectionVariable( + collectionVariableDefinition: DataCollectionVariableProps, + collectionsStateMap: DataCollectionStateMap, + em: EditorModel, +) { + const { collectionId, variableType, path } = collectionVariableDefinition; + if (!collectionsStateMap) return; + + const collectionItem = collectionsStateMap[collectionId]; + + if (!collectionItem) { + return ''; + } + + if (!variableType) { + em.logError(`Missing collection variable type for collection: ${collectionId}`); + return ''; + } + + if (variableType === 'currentItem') { + return resolveCurrentItem(collectionItem, path, collectionId, em); + } + + return collectionItem[variableType]; +} + +function resolveCurrentItem( + collectionItem: DataCollectionState, + path: string | undefined, + collectionId: string, + em: EditorModel, +) { + const currentItem = collectionItem.currentItem; + + if (!currentItem) { + em.logError(`Current item is missing for collection: ${collectionId}`); + return ''; + } + + if (currentItem.type === DataVariableType) { + const resolvedPath = currentItem.path ? `${currentItem.path}.${path}` : path; + return { + ...currentItem, + path: resolvedPath, + }; + } + + if (path && !(currentItem as any)[path]) { + em.logError(`Path not found in current item: ${path} for collection: ${collectionId}`); + return ''; + } + + return path ? (currentItem as any)[path] : currentItem; +} diff --git a/packages/core/src/data_sources/model/data_collection/constants.ts b/packages/core/src/data_sources/model/data_collection/constants.ts new file mode 100644 index 000000000..64084cea2 --- /dev/null +++ b/packages/core/src/data_sources/model/data_collection/constants.ts @@ -0,0 +1,5 @@ +export const DataCollectionType = 'data-collection'; +export const DataCollectionVariableType = 'data-collection-variable'; +export const keyCollectionDefinition = 'collectionDef'; +export const keyIsCollectionItem = '__is_data_collection_item'; +export const keyCollectionsStateMap = '__collections_state_map'; diff --git a/packages/core/src/data_sources/model/data_collection/types.ts b/packages/core/src/data_sources/model/data_collection/types.ts new file mode 100644 index 000000000..3ce256c36 --- /dev/null +++ b/packages/core/src/data_sources/model/data_collection/types.ts @@ -0,0 +1,57 @@ +import { DataCollectionType, DataCollectionVariableType, keyCollectionDefinition } from './constants'; +import { ComponentDefinition, ComponentProperties } from '../../../dom_components/model/types'; +import { DataVariableProps } from '../DataVariable'; + +export type DataCollectionDataSource = DataVariableProps | DataCollectionVariableProps; + +export interface DataCollectionConfig { + collectionId: string; + startIndex?: number; + endIndex?: number; + dataSource: DataCollectionDataSource; +} + +export enum DataCollectionStateVariableType { + currentIndex = 'currentIndex', + startIndex = 'startIndex', + currentItem = 'currentItem', + endIndex = 'endIndex', + collectionId = 'collectionId', + totalItems = 'totalItems', + remainingItems = 'remainingItems', +} + +export interface DataCollectionState { + [DataCollectionStateVariableType.currentIndex]: number; + [DataCollectionStateVariableType.startIndex]: number; + [DataCollectionStateVariableType.currentItem]: DataVariableProps; + [DataCollectionStateVariableType.endIndex]: number; + [DataCollectionStateVariableType.collectionId]: string; + [DataCollectionStateVariableType.totalItems]: number; + [DataCollectionStateVariableType.remainingItems]: number; +} + +export interface DataCollectionStateMap { + [key: string]: DataCollectionState; +} + +export interface ComponentDataCollectionProps extends ComponentDefinition { + [keyCollectionDefinition]: DataCollectionProps; +} + +export interface ComponentDataCollectionVariableProps + extends DataCollectionVariableProps, + Omit {} + +export interface DataCollectionProps { + type: typeof DataCollectionType; + collectionConfig: DataCollectionConfig; + componentDef: ComponentDefinition; +} + +export interface DataCollectionVariableProps { + type: typeof DataCollectionVariableType; + variableType: DataCollectionStateVariableType; + collectionId: string; + path?: string; +} diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index 85451ca7f..c092a05d6 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -1,50 +1,68 @@ import EditorModel from '../../editor/model/Editor'; -import { DynamicValue, DynamicValueDefinition } from '../types'; -import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition'; -import DataVariable, { DataVariableType } from './DataVariable'; +import { DataResolver, DataResolverProps } from '../types'; +import { DataCollectionStateMap } from './data_collection/types'; +import DataCollectionVariable from './data_collection/DataCollectionVariable'; +import { DataCollectionVariableType } from './data_collection/constants'; +import { DataConditionType, DataCondition } from './conditional_variables/DataCondition'; +import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable'; -export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition { - return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type); +export function isDataResolverProps(value: any): value is DataResolverProps { + return ( + typeof value === 'object' && [DataVariableType, DataConditionType, DataCollectionVariableType].includes(value?.type) + ); } -export function isDynamicValue(value: any): value is DynamicValue { +export function isDataResolver(value: any): value is DataResolver { return value instanceof DataVariable || value instanceof DataCondition; } -export function isDataVariable(variable: any) { +export function isDataVariable(variable: any): variable is DataVariableProps { return variable?.type === DataVariableType; } export function isDataCondition(variable: any) { - return variable?.type === ConditionalVariableType; + return variable?.type === DataConditionType; } export function evaluateVariable(variable: any, em: EditorModel) { return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; } -export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue { - const dynamicType = valueDefinition.type; - let dynamicVariable: DynamicValue; +export function getDataResolverInstance( + resolverProps: DataResolverProps, + options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap }, +): DataResolver { + const { type } = resolverProps; + let resolver: DataResolver; - switch (dynamicType) { + switch (type) { case DataVariableType: - dynamicVariable = new DataVariable(valueDefinition, { em: em }); + resolver = new DataVariable(resolverProps, options); break; - case ConditionalVariableType: { - const { condition, ifTrue, ifFalse } = valueDefinition; - dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em }); + case DataConditionType: { + const { condition, ifTrue, ifFalse } = resolverProps; + resolver = new DataCondition(condition, ifTrue, ifFalse, options); + break; + } + case DataCollectionVariableType: { + resolver = new DataCollectionVariable(resolverProps, options); break; } default: - throw new Error(`Unsupported dynamic type: ${dynamicType}`); + throw new Error(`Unsupported dynamic type: ${type}`); } - return dynamicVariable; + return resolver; } -export function evaluateDynamicValueDefinition(valueDefinition: DynamicValueDefinition, em: EditorModel) { - const dynamicVariable = getDynamicValueInstance(valueDefinition, em); +export function getDataResolverInstanceValue( + resolverProps: DataResolverProps, + options: { + em: EditorModel; + collectionsStateMap?: DataCollectionStateMap; + }, +) { + const resolver = getDataResolverInstance(resolverProps, options); - return { variable: dynamicVariable, value: dynamicVariable.getDataValue() }; + return { resolver, value: resolver.getDataValue() }; } diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index 95e86123e..7b7931331 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -1,12 +1,15 @@ -import { ObjectAny } from '../common'; -import ComponentDataVariable from './model/ComponentDataVariable'; +import { Model, Collection, ObjectAny } from '../common'; +import DataCollectionVariable from './model/data_collection/DataCollectionVariable'; +import { DataCollectionVariableProps } from './model/data_collection/types'; import DataRecord from './model/DataRecord'; import DataRecords from './model/DataRecords'; -import DataVariable, { DataVariableDefinition } from './model/DataVariable'; -import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition'; +import DataVariable, { DataVariableProps } from './model/DataVariable'; +import { DataConditionProps, DataCondition } from './model/conditional_variables/DataCondition'; + +export type DataResolver = DataVariable | DataCondition | DataCollectionVariable; + +export type DataResolverProps = DataVariableProps | DataConditionProps | DataCollectionVariableProps; -export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition; -export type DynamicValueDefinition = DataVariableDefinition | ConditionalVariableDefinition; export interface DataRecordProps extends ObjectAny { /** * Record id. @@ -21,8 +24,8 @@ export interface DataRecordProps extends ObjectAny { [key: string]: any; } -export interface DataVariableListener { - obj: any; +export interface DataSourceListener { + obj: Model | Collection; event: string; } diff --git a/packages/core/src/data_sources/view/ComponentDataCollectionVariableView.ts b/packages/core/src/data_sources/view/ComponentDataCollectionVariableView.ts new file mode 100644 index 000000000..8fffd260c --- /dev/null +++ b/packages/core/src/data_sources/view/ComponentDataCollectionVariableView.ts @@ -0,0 +1,21 @@ +import ComponentView from '../../dom_components/view/ComponentView'; +import DataResolverListener from '../model/DataResolverListener'; +import ComponentDataCollectionVariable from '../model/data_collection/ComponentDataCollectionVariable'; + +export default class ComponentDataCollectionVariableView extends ComponentView { + dataResolverListener?: DataResolverListener; + + initialize(opt = {}) { + super.initialize(opt); + this.dataResolverListener = new DataResolverListener({ + em: this.em!, + resolver: this.model.dataResolver, + onUpdate: this.postRender.bind(this), + }); + } + + postRender() { + this.el.innerHTML = this.model.getDataValue(); + super.postRender(); + } +} diff --git a/packages/core/src/data_sources/view/ComponentDataCollectionView.ts b/packages/core/src/data_sources/view/ComponentDataCollectionView.ts new file mode 100644 index 000000000..c38627b0f --- /dev/null +++ b/packages/core/src/data_sources/view/ComponentDataCollectionView.ts @@ -0,0 +1,4 @@ +import ComponentView from '../../dom_components/view/ComponentView'; +import ComponentDataCollection from '../model/data_collection/ComponentDataCollection'; + +export default class ComponentDataCollectionView extends ComponentView {} diff --git a/packages/core/src/data_sources/view/ComponentDataConditionView.ts b/packages/core/src/data_sources/view/ComponentDataConditionView.ts new file mode 100644 index 000000000..c8bf42438 --- /dev/null +++ b/packages/core/src/data_sources/view/ComponentDataConditionView.ts @@ -0,0 +1,4 @@ +import ComponentView from '../../dom_components/view/ComponentView'; +import ComponentDataCondition from '../model/conditional_variables/ComponentDataCondition'; + +export default class ComponentDataConditionView extends ComponentView {} diff --git a/packages/core/src/data_sources/view/ComponentDataVariableView.ts b/packages/core/src/data_sources/view/ComponentDataVariableView.ts index 1d53474d3..5a940abd1 100644 --- a/packages/core/src/data_sources/view/ComponentDataVariableView.ts +++ b/packages/core/src/data_sources/view/ComponentDataVariableView.ts @@ -1,23 +1,26 @@ import ComponentView from '../../dom_components/view/ComponentView'; import ComponentDataVariable from '../model/ComponentDataVariable'; -import DynamicVariableListenerManager from '../model/DataVariableListenerManager'; +import DataResolverListener from '../model/DataResolverListener'; export default class ComponentDataVariableView extends ComponentView { - dynamicVariableListener?: DynamicVariableListenerManager; + dataResolverListener!: DataResolverListener; initialize(opt = {}) { super.initialize(opt); - this.dynamicVariableListener = new DynamicVariableListenerManager({ - em: this.em!, - dataVariable: this.model, - updateValueFromDataVariable: () => this.postRender(), + this.dataResolverListener = new DataResolverListener({ + em: this.em, + resolver: this.model.dataResolver, + onUpdate: () => this.postRender(), }); } + remove() { + this.dataResolverListener.destroy(); + return super.remove(); + } + postRender() { - const { model, el, em } = this; - const { path, defaultValue } = model.attributes; - el.innerHTML = em.DataSources.getValue(path, defaultValue); + this.el.innerHTML = this.model.getDataValue(); super.postRender(); } } diff --git a/packages/core/src/data_sources/view/ComponentDynamicView.ts b/packages/core/src/data_sources/view/ComponentDynamicView.ts deleted file mode 100644 index 75c287d45..000000000 --- a/packages/core/src/data_sources/view/ComponentDynamicView.ts +++ /dev/null @@ -1,4 +0,0 @@ -import ComponentView from '../../dom_components/view/ComponentView'; -import ConditionalComponent from '../model/conditional_variables/ConditionalComponent'; - -export default class ConditionalComponentView extends ComponentView {} diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 1111e1a37..224dfc585 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -53,7 +53,7 @@ * * @module Components */ -import { debounce, isArray, isBoolean, isEmpty, isFunction, isString, isSymbol, result } from 'underscore'; +import { debounce, isArray, isEmpty, isFunction, isString, isSymbol, result } from 'underscore'; import { ItemManagerModule } from '../abstract/Module'; import { ObjectAny } from '../common'; import EditorModel from '../editor/model/Editor'; @@ -125,9 +125,14 @@ import { BlockProperties } from '../block_manager/model/Block'; import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import { DataVariableType } from '../data_sources/model/DataVariable'; -import { ConditionalVariableType } from '../data_sources/model/conditional_variables/DataCondition'; -import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ConditionalComponent'; -import ConditionalComponentView from '../data_sources/view/ComponentDynamicView'; +import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition'; +import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition'; +import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView'; +import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection'; +import { DataCollectionType, DataCollectionVariableType } from '../data_sources/model/data_collection/constants'; +import ComponentDataCollectionVariable from '../data_sources/model/data_collection/ComponentDataCollectionVariable'; +import ComponentDataCollectionVariableView from '../data_sources/view/ComponentDataCollectionVariableView'; +import ComponentDataCollectionView from '../data_sources/view/ComponentDataCollectionView'; export type ComponentEvent = | 'component:create' @@ -194,9 +199,19 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ { - id: ConditionalVariableType, - model: ComponentConditionalVariable, - view: ConditionalComponentView, + id: DataCollectionVariableType, + model: ComponentDataCollectionVariable, + view: ComponentDataCollectionVariableView, + }, + { + id: DataCollectionType, + model: ComponentDataCollection, + view: ComponentDataCollectionView, + }, + { + id: DataConditionType, + model: ComponentDataCondition, + view: ComponentDataConditionView, }, { id: DataVariableType, diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index dd135c727..c16b96b68 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -51,15 +51,11 @@ import { updateSymbolComps, updateSymbolProps, } from './SymbolUtils'; -import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher'; -import { DynamicValueWatcher } from './DynamicValueWatcher'; -import { DynamicValueDefinition } from '../../data_sources/types'; +import { ComponentDataResolverWatchers } from './ComponentDataResolverWatchers'; +import { DynamicWatchersOptions } from './ComponentResolverWatcher'; +import { keyIsCollectionItem, keyCollectionsStateMap } from '../../data_sources/model/data_collection/constants'; export interface IComponent extends ExtractMethods {} -export interface DynamicWatchersOptions { - skipWatcherUpdates?: boolean; - fromDataSource?: boolean; -} export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {} export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {} @@ -262,12 +258,20 @@ export default class Component extends StyleableModel { * @private * @ts-ignore */ collection!: Components; - componentDVListener: ComponentDynamicValueWatcher; + collectionStateListeners: string[] = []; + dataResolverWatchers: ComponentDataResolverWatchers; constructor(props: ComponentProperties = {}, opt: ComponentOptions) { - super(props, opt); - this.componentDVListener = new ComponentDynamicValueWatcher(this, opt.em); - this.componentDVListener.addProps(props); + const dataResolverWatchers = new ComponentDataResolverWatchers(undefined, { + em: opt.em, + collectionsStateMap: props[keyCollectionsStateMap], + }); + super(props, { + ...opt, + dataResolverWatchers, + } as any); + dataResolverWatchers.bindComponent(this); + this.dataResolverWatchers = dataResolverWatchers; bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps'); const em = opt.em; @@ -295,9 +299,11 @@ export default class Component extends StyleableModel { this.opt = opt; this.em = em!; this.config = opt.config || {}; + const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs(); this.setAttributes({ ...(result(this, 'defaults').attributes || {}), ...(this.get('attributes') || {}), + ...dynamicAttributes, }); this.ccid = Component.createId(this, opt); this.preInit(); @@ -343,7 +349,9 @@ export default class Component extends StyleableModel { optionsOrUndefined?: ComponentSetOptions, ): this { let attributes: Partial; - let options: ComponentSetOptions = { skipWatcherUpdates: false, fromDataSource: false }; + let options: ComponentSetOptions & { + dataResolverWatchers?: ComponentDataResolverWatchers; + } = { skipWatcherUpdates: false, fromDataSource: false }; if (typeof keyOrAttributes === 'object') { attributes = keyOrAttributes; options = valueOrOptions || (options as ComponentSetOptions); @@ -355,16 +363,10 @@ export default class Component extends StyleableModel { options = optionsOrUndefined || options; } - // @ts-ignore - const em = this.em || options.em; - const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, em); - - const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; - if (!shouldSkipWatcherUpdates) { - this.componentDVListener?.addProps(attributes); - } + this.dataResolverWatchers = this.dataResolverWatchers || options.dataResolverWatchers; + const evaluatedProps = this.dataResolverWatchers.addProps(attributes, options); - return super.set(evaluatedAttributes, options); + return super.set(evaluatedProps, options); } __postAdd(opts: { recursive?: boolean } = {}) { @@ -503,8 +505,13 @@ export default class Component extends StyleableModel { * @example * component.setSymbolOverride(['children', 'classes']); */ - setSymbolOverride(value?: boolean | string | string[]) { - this.set(keySymbolOvrd, (isString(value) ? [value] : value) ?? 0); + setSymbolOverride(value: boolean | string | string[], options: DynamicWatchersOptions = {}) { + this.set( + { + [keySymbolOvrd]: (isString(value) ? [value] : value) ?? 0, + }, + options, + ); } /** @@ -685,14 +692,7 @@ export default class Component extends StyleableModel { * component.setAttributes({ id: 'test', 'data-key': 'value' }); */ setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) { - // @ts-ignore - const em = this.em || opts.em; - const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attrs, em); - const shouldSkipWatcherUpdates = opts.skipWatcherUpdates || opts.fromDataSource; - if (!shouldSkipWatcherUpdates) { - this.componentDVListener.setAttributes(attrs); - } - this.set('attributes', { ...evaluatedAttributes }, opts); + this.set('attributes', { ...attrs }, opts); return this; } @@ -706,7 +706,7 @@ export default class Component extends StyleableModel { * component.addAttributes({ 'data-key': 'value' }); */ addAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) { - const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs(); + const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs(); return this.setAttributes( { ...this.getAttributes({ noClass: true }), @@ -728,7 +728,7 @@ export default class Component extends StyleableModel { */ removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) { const attrArr = Array.isArray(attrs) ? attrs : [attrs]; - this.componentDVListener.removeAttributes(attrArr); + this.dataResolverWatchers.removeAttributes(attrArr); const compAttr = this.getAttributes(); attrArr.map((i) => delete compAttr[i]); @@ -965,12 +965,12 @@ export default class Component extends StyleableModel { const value = trait.getInitValue(); if (trait.changeProp) { - this.set(name, value); + isUndefined(this.get(name)) && this.set(name, value); } else { if (name && value) attrs[name] = value; } }); - const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs(); + const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs(); traits.length && this.setAttributes({ ...attrs, @@ -1318,13 +1318,14 @@ export default class Component extends StyleableModel { clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this { const em = this.em; const attr = { - ...this.componentDVListener.getPropsDefsOrValues(this.attributes), + ...this.attributes, + ...this.dataResolverWatchers.getDynamicPropsDefs(), }; const opts = { ...this.opt }; const id = this.getId(); const cssc = em?.Css; attr.attributes = { - ...(attr.attributes ? this.componentDVListener.getAttributesDefsOrValues(attr.attributes) : undefined), + ...(attr.attributes ? this.dataResolverWatchers.getAttributesDefsOrValues(attr.attributes) : undefined), }; // @ts-ignore attr.components = []; @@ -1353,6 +1354,7 @@ export default class Component extends StyleableModel { attr.status = ''; // @ts-ignore opts.collection = null; + opts.forCloning = true; // @ts-ignore const cloned = new this.constructor(attr, opts); @@ -1581,9 +1583,9 @@ export default class Component extends StyleableModel { */ toJSON(opts: ObjectAny = {}): ComponentDefinition { let obj = Model.prototype.toJSON.call(this, opts); - obj = { ...obj, ...this.componentDVListener.getDynamicPropsDefs() }; - obj.attributes = this.componentDVListener.getAttributesDefsOrValues(this.getAttributes()); - delete obj.componentDVListener; + obj = { ...obj, ...this.dataResolverWatchers.getDynamicPropsDefs() }; + obj.attributes = this.dataResolverWatchers.getAttributesDefsOrValues(this.getAttributes()); + delete obj.dataResolverWatchers; delete obj.attributes.class; delete obj.toolbar; delete obj.traits; @@ -1591,6 +1593,14 @@ export default class Component extends StyleableModel { delete obj.open; // used in Layers delete obj._undoexc; delete obj.delegate; + if (this.get(keyIsCollectionItem)) { + delete obj[keySymbol]; + delete obj[keySymbolOvrd]; + delete obj[keySymbols]; + delete obj[keyCollectionsStateMap]; + delete obj[keyIsCollectionItem]; + delete obj.attributes.id; + } if (!opts.fromUndo) { const symbol = obj[keySymbol]; @@ -1657,9 +1667,7 @@ export default class Component extends StyleableModel { * @return {this} */ setId(id: string, opts?: SetOptions & { idUpdate?: boolean }) { - const attrs = { ...this.get('attributes') }; - attrs.id = id; - this.set('attributes', attrs, opts); + this.addAttributes({ id }, opts); return this; } @@ -1822,7 +1830,7 @@ export default class Component extends StyleableModel { } destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { - this.componentDVListener.destroy(); + this.dataResolverWatchers.destroy(); return super.destroy(options); } diff --git a/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts b/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts new file mode 100644 index 000000000..e0974793d --- /dev/null +++ b/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts @@ -0,0 +1,116 @@ +import { ObjectAny } from '../../common'; +import { + DataCollectionVariableType, + keyCollectionsStateMap, + keyIsCollectionItem, +} from '../../data_sources/model/data_collection/constants'; +import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; +import Component from './Component'; +import { + ComponentResolverWatcher, + ComponentResolverWatcherOptions, + DynamicWatchersOptions, +} from './ComponentResolverWatcher'; +import { getSymbolsToUpdate } from './SymbolUtils'; + +export const updateFromWatcher = { fromDataSource: true, avoidStore: true }; + +export class ComponentDataResolverWatchers { + private propertyWatcher: ComponentResolverWatcher; + private attributeWatcher: ComponentResolverWatcher; + + constructor( + private component: Component | undefined, + options: ComponentResolverWatcherOptions, + ) { + this.propertyWatcher = new ComponentResolverWatcher(component, this.onPropertyUpdate, options); + this.attributeWatcher = new ComponentResolverWatcher(component, this.onAttributeUpdate, options); + } + + private onPropertyUpdate(component: Component | undefined, key: string, value: any) { + component?.set(key, value, updateFromWatcher); + } + + private onAttributeUpdate(component: Component | undefined, key: string, value: any) { + component?.addAttributes({ [key]: value }, updateFromWatcher); + } + + bindComponent(component: Component) { + this.component = component; + this.propertyWatcher.bindComponent(component); + this.attributeWatcher.bindComponent(component); + this.updateSymbolOverride(); + } + + updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { + this.propertyWatcher.updateCollectionStateMap(collectionsStateMap); + this.attributeWatcher.updateCollectionStateMap(collectionsStateMap); + } + + addProps(props: ObjectAny, options: DynamicWatchersOptions = {}) { + const excludedFromEvaluation = ['components']; + + const evaluatedProps = Object.fromEntries( + Object.entries(props).map(([key, value]) => + excludedFromEvaluation.includes(key) + ? [key, value] // Return excluded keys as they are + : [key, this.propertyWatcher.addDynamicValues({ [key]: value }, options)[key]], + ), + ); + + if (props.attributes) { + const evaluatedAttributes = this.attributeWatcher.setDynamicValues(props.attributes, options); + evaluatedProps['attributes'] = evaluatedAttributes; + } + + const skipOverrideUpdates = options.skipWatcherUpdates || options.fromDataSource; + if (!skipOverrideUpdates) { + this.updateSymbolOverride(); + } + + return evaluatedProps; + } + + removeAttributes(attributes: string[]) { + this.attributeWatcher.removeListeners(attributes); + this.updateSymbolOverride(); + } + + private updateSymbolOverride() { + if (!this.component || !this.component.get(keyIsCollectionItem)) return; + + const keys = this.propertyWatcher.getDynamicValuesOfType(DataCollectionVariableType); + const attributesKeys = this.attributeWatcher.getDynamicValuesOfType(DataCollectionVariableType); + + const combinedKeys = [keyCollectionsStateMap, ...keys]; + const haveOverridenAttributes = Object.keys(attributesKeys).length; + if (haveOverridenAttributes) combinedKeys.push('attributes'); + + const toUp = getSymbolsToUpdate(this.component); + toUp.forEach((child) => { + child.setSymbolOverride(combinedKeys, { fromDataSource: true }); + }); + this.component.setSymbolOverride(combinedKeys, { fromDataSource: true }); + } + + getDynamicPropsDefs() { + return this.propertyWatcher.getAllSerializableValues(); + } + + getDynamicAttributesDefs() { + return this.attributeWatcher.getAllSerializableValues(); + } + + getPropsDefsOrValues(props: ObjectAny) { + return this.propertyWatcher.getSerializableValues(props); + } + + getAttributesDefsOrValues(attributes: ObjectAny) { + return this.attributeWatcher.getSerializableValues(attributes); + } + + destroy() { + this.propertyWatcher.destroy(); + this.attributeWatcher.destroy(); + } +} diff --git a/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts b/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts deleted file mode 100644 index 911417833..000000000 --- a/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ObjectAny } from '../../common'; -import EditorModel from '../../editor/model/Editor'; -import Component from './Component'; -import { DynamicValueWatcher } from './DynamicValueWatcher'; - -export class ComponentDynamicValueWatcher { - private propertyWatcher: DynamicValueWatcher; - private attributeWatcher: DynamicValueWatcher; - - constructor( - private component: Component, - em: EditorModel, - ) { - this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), em); - this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), em); - } - - private createPropertyUpdater() { - return (key: string, value: any) => { - this.component.set(key, value, { fromDataSource: true, avoidStore: true }); - }; - } - - private createAttributeUpdater() { - return (key: string, value: any) => { - this.component.addAttributes({ [key]: value }, { fromDataSource: true, avoidStore: true }); - }; - } - - addProps(props: ObjectAny) { - this.propertyWatcher.addDynamicValues(props); - } - - addAttributes(attributes: ObjectAny) { - this.attributeWatcher.addDynamicValues(attributes); - } - - setAttributes(attributes: ObjectAny) { - this.attributeWatcher.setDynamicValues(attributes); - } - - removeAttributes(attributes: string[]) { - this.attributeWatcher.removeListeners(attributes); - } - - getDynamicPropsDefs() { - return this.propertyWatcher.getAllSerializableValues(); - } - - getDynamicAttributesDefs() { - return this.attributeWatcher.getAllSerializableValues(); - } - - getAttributesDefsOrValues(attributes: ObjectAny) { - return this.attributeWatcher.getSerializableValues(attributes); - } - - getPropsDefsOrValues(props: ObjectAny) { - return this.propertyWatcher.getSerializableValues(props); - } - - destroy() { - this.propertyWatcher.removeListeners(); - this.attributeWatcher.removeListeners(); - } -} diff --git a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts b/packages/core/src/dom_components/model/ComponentResolverWatcher.ts new file mode 100644 index 000000000..8b337e4b7 --- /dev/null +++ b/packages/core/src/dom_components/model/ComponentResolverWatcher.ts @@ -0,0 +1,180 @@ +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 EditorModel from '../../editor/model/Editor'; +import { DataResolverProps } from '../../data_sources/types'; +import Component from './Component'; + +export interface DynamicWatchersOptions { + skipWatcherUpdates?: boolean; + fromDataSource?: boolean; +} + +export interface ComponentResolverWatcherOptions { + em: EditorModel; + collectionsStateMap?: DataCollectionStateMap; +} + +type UpdateFn = (component: Component | undefined, key: string, value: any) => void; + +export class ComponentResolverWatcher { + private em: EditorModel; + private collectionsStateMap?: DataCollectionStateMap; + private resolverListeners: Record = {}; + + constructor( + private component: Component | undefined, + private updateFn: UpdateFn, + options: ComponentResolverWatcherOptions, + ) { + this.em = options.em; + this.collectionsStateMap = options.collectionsStateMap; + } + + bindComponent(component: Component) { + this.component = component; + } + + updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { + this.collectionsStateMap = collectionsStateMap; + + const collectionVariablesKeys = this.getDynamicValuesOfType(DataCollectionVariableType); + const collectionVariablesObject = collectionVariablesKeys.reduce( + (acc: { [key: string]: DataResolverProps | null }, key) => { + acc[key] = null; + return acc; + }, + {}, + ); + const newVariables = this.getSerializableValues(collectionVariablesObject); + const evaluatedValues = this.addDynamicValues(newVariables); + + Object.keys(evaluatedValues).forEach((key) => { + this.updateFn(this.component, key, evaluatedValues[key]); + }); + } + + setDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) { + const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; + if (!shouldSkipWatcherUpdates) { + this.removeListeners(); + } + + return this.addDynamicValues(values, options); + } + + addDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) { + if (!values) return {}; + const evaluatedValues = this.evaluateValues(values); + + const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; + if (!shouldSkipWatcherUpdates) { + this.updateListeners(values); + } + + return evaluatedValues; + } + + private updateListeners(values: { [key: string]: any }) { + const { em, collectionsStateMap } = this; + this.removeListeners(Object.keys(values)); + const propsKeys = Object.keys(values); + + for (let index = 0; index < propsKeys.length; index++) { + const key = propsKeys[index]; + const resolverProps = values[key]; + + if (!isDataResolverProps(resolverProps)) { + continue; + } + + const { resolver } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); + this.resolverListeners[key] = new DataResolverListener({ + em, + resolver, + onUpdate: (value) => this.updateFn.bind(this)(this.component, key, value), + }); + } + } + + private evaluateValues(values: ObjectAny) { + const { em, collectionsStateMap } = this; + const evaluatedValues = { ...values }; + const propsKeys = Object.keys(values); + + for (let index = 0; index < propsKeys.length; index++) { + const key = propsKeys[index]; + const resolverProps = values[key]; + + if (!isDataResolverProps(resolverProps)) { + continue; + } + + const { value } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); + evaluatedValues[key] = value; + } + + return evaluatedValues; + } + + /** + * removes listeners to stop watching for changes, + * if keys argument is omitted, remove all listeners + * @argument keys + */ + removeListeners(keys?: string[]) { + const propsKeys = keys ? keys : Object.keys(this.resolverListeners); + + propsKeys.forEach((key) => { + if (this.resolverListeners[key]) { + this.resolverListeners[key].destroy?.(); + delete this.resolverListeners[key]; + } + }); + + return propsKeys; + } + + getSerializableValues(values: ObjectAny | undefined) { + if (!values) return {}; + const serializableValues = { ...values }; + const propsKeys = Object.keys(serializableValues); + + for (let index = 0; index < propsKeys.length; index++) { + const key = propsKeys[index]; + const resolverListener = this.resolverListeners[key]; + if (resolverListener) { + serializableValues[key] = resolverListener.resolver.toJSON(); + } + } + + return serializableValues; + } + + getAllSerializableValues() { + const serializableValues: ObjectAny = {}; + const propsKeys = Object.keys(this.resolverListeners); + + for (let index = 0; index < propsKeys.length; index++) { + const key = propsKeys[index]; + serializableValues[key] = this.resolverListeners[key].resolver.toJSON(); + } + + return serializableValues; + } + + getDynamicValuesOfType(type: DataResolverProps['type']) { + const keys = Object.keys(this.resolverListeners).filter((key: string) => { + // @ts-ignore + return this.resolverListeners[key].resolver.get('type') === type; + }); + + return keys; + } + + destroy() { + this.removeListeners(); + } +} diff --git a/packages/core/src/dom_components/model/DynamicValueWatcher.ts b/packages/core/src/dom_components/model/DynamicValueWatcher.ts deleted file mode 100644 index 88af44dad..000000000 --- a/packages/core/src/dom_components/model/DynamicValueWatcher.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ObjectAny } from '../../common'; -import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; -import { evaluateDynamicValueDefinition, isDynamicValueDefinition } from '../../data_sources/model/utils'; -import { DynamicValue } from '../../data_sources/types'; -import EditorModel from '../../editor/model/Editor'; - -export class DynamicValueWatcher { - dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {}; - constructor( - private updateFn: (key: string, value: any) => void, - private em: EditorModel, - ) {} - - static getStaticValues(values: ObjectAny | undefined, em: EditorModel): ObjectAny { - if (!values) return {}; - const evaluatedValues: ObjectAny = { ...values }; - const propsKeys = Object.keys(values); - - for (const key of propsKeys) { - const valueDefinition = values[key]; - if (!isDynamicValueDefinition(valueDefinition)) continue; - - const { value } = evaluateDynamicValueDefinition(valueDefinition, em); - evaluatedValues[key] = value; - } - - return evaluatedValues; - } - - static areStaticValues(values: ObjectAny | undefined) { - if (!values) return true; - return Object.keys(values).every((key) => { - return !isDynamicValueDefinition(values[key]); - }); - } - - setDynamicValues(values: ObjectAny | undefined) { - this.removeListeners(); - return this.addDynamicValues(values); - } - - addDynamicValues(values: ObjectAny | undefined) { - if (!values) return {}; - this.removeListeners(Object.keys(values)); - const dynamicProps = this.getDynamicValues(values); - const propsKeys = Object.keys(dynamicProps); - for (let index = 0; index < propsKeys.length; index++) { - const key = propsKeys[index]; - this.dynamicVariableListeners[key] = new DynamicVariableListenerManager({ - em: this.em, - dataVariable: dynamicProps[key], - updateValueFromDataVariable: (value: any) => { - this.updateFn.bind(this)(key, value); - }, - }); - } - - return dynamicProps; - } - - private getDynamicValues(values: ObjectAny) { - const dynamicValues: { - [key: string]: DynamicValue; - } = {}; - const propsKeys = Object.keys(values); - for (let index = 0; index < propsKeys.length; index++) { - const key = propsKeys[index]; - if (!isDynamicValueDefinition(values[key])) { - continue; - } - const { variable } = evaluateDynamicValueDefinition(values[key], this.em); - dynamicValues[key] = variable; - } - - return dynamicValues; - } - - /** - * removes listeners to stop watching for changes, - * if keys argument is omitted, remove all listeners - * @argument keys - */ - removeListeners(keys?: string[]) { - const propsKeys = keys ? keys : Object.keys(this.dynamicVariableListeners); - propsKeys.forEach((key) => { - if (this.dynamicVariableListeners[key]) { - this.dynamicVariableListeners[key].destroy(); - delete this.dynamicVariableListeners[key]; - } - }); - } - - getSerializableValues(values: ObjectAny | undefined) { - if (!values) return {}; - const serializableValues = { ...values }; - const propsKeys = Object.keys(serializableValues); - for (let index = 0; index < propsKeys.length; index++) { - const key = propsKeys[index]; - if (this.dynamicVariableListeners[key]) { - serializableValues[key] = this.dynamicVariableListeners[key].dynamicVariable.toJSON(); - } - } - - return serializableValues; - } - - getAllSerializableValues() { - const serializableValues: ObjectAny = {}; - const propsKeys = Object.keys(this.dynamicVariableListeners); - for (let index = 0; index < propsKeys.length; index++) { - const key = propsKeys[index]; - serializableValues[key] = this.dynamicVariableListeners[key].dynamicVariable.toJSON(); - } - - return serializableValues; - } -} diff --git a/packages/core/src/dom_components/model/SymbolUtils.ts b/packages/core/src/dom_components/model/SymbolUtils.ts index 76ae4b139..d8c2082f1 100644 --- a/packages/core/src/dom_components/model/SymbolUtils.ts +++ b/packages/core/src/dom_components/model/SymbolUtils.ts @@ -3,6 +3,10 @@ import Component, { keySymbol, keySymbolOvrd, keySymbols } from './Component'; import { SymbolToUpOptions } from './types'; import { isEmptyObj } from '../../utils/mixins'; import Components from './Components'; +import { + DataCollectionVariableType, + keyCollectionDefinition, +} from '../../data_sources/model/data_collection/constants'; export const isSymbolMain = (cmp: Component) => isArray(cmp.get(keySymbols)); @@ -129,38 +133,58 @@ export const logSymbol = (symb: Component, type: string, toUp: Component[], opts symb.em.log(type, { model: symb, toUp, context: 'symbols', opts }); }; -export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}) => { - const changed = symbol.changedAttributes() || {}; - const attrs = changed.attributes || {}; - delete changed.status; - delete changed.open; - delete changed[keySymbols]; - delete changed[keySymbol]; - delete changed[keySymbolOvrd]; - delete changed.attributes; +export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}): void => { + const changed = symbol.dataResolverWatchers.getPropsDefsOrValues({ ...symbol.changedAttributes() }); + const attrs = symbol.dataResolverWatchers.getAttributesDefsOrValues({ ...changed.attributes }); + + cleanChangedProperties(changed, attrs); + + if (!isEmptyObj(changed)) { + const toUpdate = getSymbolsToUpdate(symbol, opts); + + // Filter properties to propagate + filterPropertiesForPropagation(changed, symbol); + + logSymbol(symbol, 'props', toUpdate, { opts, changed }); + + // Update child symbols + toUpdate.forEach((child) => { + const propsToUpdate = { ...changed }; + filterPropertiesForPropagation(propsToUpdate, child); + child.set(propsToUpdate, { fromInstance: symbol, ...opts }); + }); + } +}; + +const cleanChangedProperties = (changed: Record, attrs: Record): void => { + const keysToDelete = ['status', 'open', keySymbols, keySymbol, keySymbolOvrd, 'attributes']; + keysToDelete.forEach((key) => delete changed[key]); delete attrs.id; if (!isEmptyObj(attrs)) { changed.attributes = attrs; } +}; - if (!isEmptyObj(changed)) { - const toUp = getSymbolsToUpdate(symbol, opts); - // Avoid propagating overrides to other symbols - keys(changed).map((prop) => { - if (isSymbolOverride(symbol, prop)) delete changed[prop]; - }); +const filterPropertiesForPropagation = (props: Record, component: Component): void => { + keys(props).forEach((prop) => { + if (!shouldPropagateProperty(props, prop, component)) { + delete props[prop]; + } + }); +}; - logSymbol(symbol, 'props', toUp, { opts, changed }); - toUp.forEach((child) => { - const propsChanged = { ...changed }; - // Avoid updating those with override - keys(propsChanged).map((prop) => { - if (isSymbolOverride(child, prop)) delete propsChanged[prop]; - }); - child.set(propsChanged, { fromInstance: symbol, ...opts }); - }); - } +const shouldPropagateProperty = (props: Record, prop: string, component: Component): boolean => { + const isCollectionVariableDefinition = (() => { + if (prop === 'attributes') { + const attributes = props['attributes']; + return Object.values(attributes).some((attr: any) => attr?.type === DataCollectionVariableType); + } + + return props[prop]?.type === DataCollectionVariableType; + })(); + + return !isSymbolOverride(component, prop) || isCollectionVariableDefinition; }; export const updateSymbolCls = (symbol: Component, opts: any = {}) => { @@ -193,6 +217,9 @@ export const updateSymbolComps = (symbol: Component, m: Component, c: Components toUp.forEach((rel) => { const relCmps = rel.components(); const toReset = cmps.map((cmp, i) => { + if (symbol.get(keyCollectionDefinition)) { + return cmp.clone({ symbol: isSymbol(cmp) }); + } // This particular case here is to handle reset from `resetFromString` // where we can receive an array of regulat components or already // existing symbols (updated already before reset) @@ -202,6 +229,7 @@ export const updateSymbolComps = (symbol: Component, m: Component, c: Components } return relCmps.at(i); }); + relCmps.reset(toReset, { fromInstance: symbol, ...c } as any); }); // Add diff --git a/packages/core/src/dom_components/model/types.ts b/packages/core/src/dom_components/model/types.ts index 79b452b6b..7d7ab3942 100644 --- a/packages/core/src/dom_components/model/types.ts +++ b/packages/core/src/dom_components/model/types.ts @@ -1,3 +1,4 @@ +import { DynamicWatchersOptions } from './ComponentResolverWatcher'; import Frame from '../../canvas/model/Frame'; import { AddOptions, Nullable, OptionAsDocument } from '../../common'; import EditorModel from '../../editor/model/Editor'; @@ -11,7 +12,7 @@ import Component from './Component'; import Components from './Components'; import { ToolbarButtonProps } from './ToolbarButton'; import { ParseNodeOptions } from '../../parser/config/config'; -import { DynamicValueDefinition } from '../../data_sources/types'; +import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; export type DragMode = 'translate' | 'absolute' | ''; @@ -253,7 +254,7 @@ export interface ComponentProperties { [key: string]: any; } -export interface SymbolToUpOptions { +export interface SymbolToUpOptions extends DynamicWatchersOptions { changed?: string; fromInstance?: boolean; noPropagate?: boolean; @@ -321,4 +322,5 @@ export interface ComponentOptions { frame?: Frame; temporary?: boolean; avoidChildren?: boolean; + forCloning?: boolean; } diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 5621b4d67..80fc8fe0f 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -1,30 +1,30 @@ import { isArray, isString, keys } from 'underscore'; -import { Model, ObjectAny, ObjectHash, SetOptions, View } from '../../common'; +import { Model, ObjectAny, ObjectHash, SetOptions } from '../../common'; 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 StyleDataVariable from '../../data_sources/model/StyleDataVariable'; -import { DataVariableDefinition, DataVariableType } from '../../data_sources/model/DataVariable'; -import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; +import DataVariable, { DataVariableProps, DataVariableType } 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 { DataCondition, - ConditionalVariableType, - ConditionalVariableDefinition, + DataConditionType, + DataConditionProps, } from '../../data_sources/model/conditional_variables/DataCondition'; -import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; -import { DynamicValueDefinition } from '../../data_sources/types'; -export type StyleProps = Record; +import { isDataResolver, isDataResolverProps } from '../../data_sources/model/utils'; +import { DataResolverProps } from '../../data_sources/types'; -export type UpdateStyleOptions = SetOptions & { +export type StyleProps = Record; + +export interface UpdateStyleOptions extends SetOptions { partial?: boolean; addStyle?: StyleProps; inline?: boolean; noEvent?: boolean; -}; +} export type StyleableView = ComponentView | CssRuleView; @@ -36,8 +36,8 @@ export const getLastStyleValue = (value: string | string[]) => { export default class StyleableModel extends Model { em?: EditorModel; - dynamicVariableListeners: Record = {}; views: StyleableView[] = []; + styleResolverListeners: Record = {}; constructor(attributes: T, options: { em?: EditorModel } = {}) { super(attributes, options); @@ -71,7 +71,7 @@ export default class StyleableModel extends Model const result: ObjectAny = { ...style }; if (this.em && !opts.skipResolve) { - const resolvedStyle = this.resolveDataVariables({ ...result }); + const resolvedStyle = this.getResolvedStyles({ ...result }); // @ts-ignore return prop && isString(prop) ? resolvedStyle[prop] : resolvedStyle; } @@ -110,10 +110,12 @@ export default class StyleableModel extends Model } const styleValue = newStyle[key]; - if (isDynamicValueDefinition(styleValue)) { - const styleDynamicVariable = this.resolveDynamicValue(styleValue); - newStyle[key] = styleDynamicVariable; - this.manageDataVariableListener(styleDynamicVariable, key); + if (isDataResolverProps(styleValue)) { + const dataResolver = this.getDataResolverInstance(styleValue); + if (dataResolver) { + newStyle[key] = dataResolver; + this.listenToDataResolver(dataResolver, key); + } } }); @@ -139,38 +141,33 @@ export default class StyleableModel extends Model return newStyle; } - private resolveDynamicValue(styleValue: DynamicValueDefinition) { - const dynamicType = styleValue.type; - let styleDynamicVariable; - switch (dynamicType) { + private getDataResolverInstance(props: DataResolverProps) { + const em = this.em!; + let resolver; + + switch (props.type) { case DataVariableType: - styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em }); + resolver = new DataVariable(props, { em }); break; - case ConditionalVariableType: { - const { condition, ifTrue, ifFalse } = styleValue; - styleDynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em! }); + case DataConditionType: { + const { condition, ifTrue, ifFalse } = props; + resolver = new DataCondition(condition, ifTrue, ifFalse, { em }); break; } - default: - throw new Error( - `Unsupported dynamic value type for styles. Only '${DataVariableType}' and '${ConditionalVariableType}' are supported. Received '${dynamicType}'.`, - ); } - return styleDynamicVariable; + return resolver; } - /** - * Manage DataVariableListenerManager for a style property - */ - manageDataVariableListener(dataVar: StyleDataVariable | DataCondition, styleProp: string) { - if (this.dynamicVariableListeners[styleProp]) { - this.dynamicVariableListeners[styleProp].listenToDynamicVariable(); + listenToDataResolver(resolver: DataVariable | DataCondition, styleProp: string) { + const resolverListener = this.styleResolverListeners[styleProp]; + if (resolverListener) { + resolverListener.listenToResolver(); } else { - this.dynamicVariableListeners[styleProp] = new DynamicVariableListenerManager({ + this.styleResolverListeners[styleProp] = new DataResolverListener({ em: this.em!, - dataVariable: dataVar, - updateValueFromDataVariable: () => this.updateView(), + resolver, + onUpdate: () => this.updateView(), }); } } @@ -195,28 +192,29 @@ export default class StyleableModel extends Model this.views.forEach((view) => view.updateStyles()); } - /** - * Resolve dynamic values ( datasource variables - conditional variables ) to their actual values - */ - resolveDataVariables(style: StyleProps): StyleProps { - const resolvedStyle = { ...style }; - keys(resolvedStyle).forEach((key) => { - const styleValue = resolvedStyle[key]; + getResolvedStyles(style: StyleProps): StyleProps { + const resultStyle = { ...style }; + + keys(resultStyle).forEach((key) => { + const styleValue = resultStyle[key]; if (typeof styleValue === 'string' || Array.isArray(styleValue)) { return; } - if (isDynamicValueDefinition(styleValue)) { - const dataVar = this.resolveDynamicValue(styleValue); - resolvedStyle[key] = dataVar.getDataValue(); + if (isDataResolverProps(styleValue)) { + const resolver = this.getDataResolverInstance(styleValue); + if (resolver) { + resultStyle[key] = resolver.getDataValue(); + } } - if (isDynamicValue(styleValue)) { - resolvedStyle[key] = styleValue.getDataValue(); + if (isDataResolver(styleValue)) { + resultStyle[key] = styleValue.getDataValue(); } }); - return resolvedStyle; + + return resultStyle; } /** diff --git a/packages/core/src/trait_manager/model/Trait.ts b/packages/core/src/trait_manager/model/Trait.ts index 497a0741d..f5dd1c278 100644 --- a/packages/core/src/trait_manager/model/Trait.ts +++ b/packages/core/src/trait_manager/model/Trait.ts @@ -1,4 +1,3 @@ -import { ConditionalVariableType, DataCondition } from './../../data_sources/model/conditional_variables/DataCondition'; import { isString, isUndefined } from 'underscore'; import Category from '../../abstract/ModuleCategory'; import { LocaleOptions, Model, SetOptions } from '../../common'; @@ -8,8 +7,6 @@ import { isDef } from '../../utils/mixins'; import TraitsEvents, { TraitGetValueOptions, TraitOption, TraitProperties, TraitSetValueOptions } from '../types'; import TraitView from '../view/TraitView'; import Traits from './Traits'; -import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; -import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; /** * @property {String} id Trait id, eg. `my-trait-id`. @@ -29,8 +26,6 @@ export default class Trait extends Model { em: EditorModel; view?: TraitView; el?: HTMLElement; - dynamicVariable?: TraitDataVariable | DataCondition; - dynamicVariableListener?: DynamicVariableListenerManager; defaults() { return { diff --git a/packages/core/src/utils/mixins.ts b/packages/core/src/utils/mixins.ts index f1fa6775f..038461210 100644 --- a/packages/core/src/utils/mixins.ts +++ b/packages/core/src/utils/mixins.ts @@ -36,6 +36,8 @@ export const get = (object: ObjectAny, path: string | string[], def: any) => { return (index && index == length ? object : undefined) ?? def; }; +export const serialize = (obj: ObjectAny) => JSON.parse(JSON.stringify(obj)); + export const isBultInMethod = (key: string) => isFunction(obj[key]); export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts index 478bed43e..95710a7ac 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -1,11 +1,12 @@ import { Component, DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; -import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { + MissingConditionError, + DataConditionType, +} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; -import { DataSourceProps } from '../../../../../src/data_sources/types'; -import ConditionalComponentView from '../../../../../src/data_sources/view/ComponentDynamicView'; +import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import ComponentTableView from '../../../../../src/dom_components/view/ComponentTableView'; import ComponentTextView from '../../../../../src/dom_components/view/ComponentTextView'; @@ -28,7 +29,7 @@ describe('ComponentConditionalVariable', () => { it('should add a component with a condition that evaluates a component definition', () => { const component = cmpRoot.append({ - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -41,10 +42,10 @@ describe('ComponentConditionalVariable', () => { }, })[0]; expect(component).toBeDefined(); - expect(component.get('type')).toBe(ConditionalVariableType); + expect(component.get('type')).toBe(DataConditionType); expect(component.getInnerHTML()).toBe('

some text

'); const componentView = component.getView(); - expect(componentView).toBeInstanceOf(ConditionalComponentView); + expect(componentView).toBeInstanceOf(ComponentDataConditionView); expect(componentView?.el.textContent).toBe('some text'); const childComponent = getFirstChild(component); @@ -58,7 +59,7 @@ describe('ComponentConditionalVariable', () => { it('should add a component with a condition that evaluates a string', () => { const component = cmpRoot.append({ - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -67,10 +68,10 @@ describe('ComponentConditionalVariable', () => { ifTrue: '

some text

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

some text

'); const componentView = component.getView(); - expect(componentView).toBeInstanceOf(ConditionalComponentView); + expect(componentView).toBeInstanceOf(ComponentDataConditionView); expect(componentView?.el.textContent).toBe('some text'); const childComponent = getFirstChild(component); @@ -93,7 +94,7 @@ describe('ComponentConditionalVariable', () => { dsm.add(dataSource); const component = cmpRoot.append({ - type: ConditionalVariableType, + type: DataConditionType, condition: { left: { type: DataVariableType, @@ -142,7 +143,7 @@ describe('ComponentConditionalVariable', () => { dsm.add(dataSource); const component = cmpRoot.append({ - type: ConditionalVariableType, + type: DataConditionType, condition: { left: { type: DataVariableType, @@ -158,7 +159,7 @@ describe('ComponentConditionalVariable', () => { tagName: 'div', components: [ { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: { type: DataVariableType, @@ -189,7 +190,7 @@ describe('ComponentConditionalVariable', () => { it('should store conditional components', () => { const conditionalCmptDef = { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -215,7 +216,7 @@ describe('ComponentConditionalVariable', () => { it('should throw an error if no condition is passed', () => { const conditionalCmptDef = { - type: ConditionalVariableType, + type: DataConditionType, ifTrue: { tagName: 'h1', type: 'text', diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts index 516dd2f4d..5f7d4af0f 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,12 +1,11 @@ import { DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { - ConditionalVariableType, + DataConditionType, MissingConditionError, } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; -import { DataSourceProps } from '../../../../../src/data_sources/types'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import EditorModel from '../../../../../src/editor/model/Editor'; import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; @@ -32,7 +31,7 @@ describe('StyleConditionalVariable', () => { content: 'some text', style: { color: { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -64,7 +63,7 @@ describe('StyleConditionalVariable', () => { content: 'some text', style: { color: { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: { type: DataVariableType, @@ -96,7 +95,7 @@ describe('StyleConditionalVariable', () => { content: 'some text', style: { color: { - type: ConditionalVariableType, + type: DataConditionType, ifTrue: 'grey', ifFalse: 'red', }, @@ -112,7 +111,7 @@ describe('StyleConditionalVariable', () => { content: 'some text', style: { color: { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts index a9907207a..0a65a2321 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -1,5 +1,5 @@ import { DataSourceManager, Editor } from '../../../../../src'; -import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; 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'; @@ -34,7 +34,7 @@ describe('conditional traits', () => { label: 'Value', name: 'value', value: { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -68,7 +68,7 @@ describe('conditional traits', () => { name: 'value', changeProp: true, value: { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -103,7 +103,7 @@ describe('conditional traits', () => { name: 'value', changeProp: true, value: { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, @@ -138,7 +138,7 @@ describe('conditional traits', () => { name: 'value', changeProp: true, value: { - type: ConditionalVariableType, + type: DataConditionType, condition: { left: 0, operator: NumberOperation.greaterThan, 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 31c350ade..c90cb7250 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts @@ -1,15 +1,14 @@ import { DataSourceManager } from '../../../../../src'; import { DataCondition, - ExpressionDefinition, - LogicGroupDefinition, + 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 { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { DataSourceProps } from '../../../../../src/data_sources/types'; import Editor from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor'; @@ -52,14 +51,14 @@ describe('DataCondition', () => { describe('Operator Tests', () => { test('should evaluate using GenericOperation operators', () => { - const condition: ExpressionDefinition = { left: 5, operator: GenericOperation.equals, right: 5 }; + const condition: ExpressionProps = { left: 5, operator: GenericOperation.equals, right: 5 }; const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal', { em }); expect(dataCondition.getDataValue()).toBe('Equal'); }); test('equals (false)', () => { - const condition: ExpressionDefinition = { + const condition: ExpressionProps = { left: 'hello', operator: GenericOperation.equals, right: 'world', @@ -69,21 +68,21 @@ describe('DataCondition', () => { }); test('should evaluate using StringOperation operators', () => { - const condition: ExpressionDefinition = { left: 'apple', operator: StringOperation.contains, right: 'app' }; + const condition: ExpressionProps = { left: 'apple', operator: StringOperation.contains, right: 'app' }; const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain", { em }); expect(dataCondition.getDataValue()).toBe('Contains'); }); test('should evaluate using NumberOperation operators', () => { - const condition: ExpressionDefinition = { left: 10, operator: NumberOperation.lessThan, right: 15 }; + const condition: ExpressionProps = { left: 10, operator: NumberOperation.lessThan, right: 15 }; const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em }); expect(dataCondition.getDataValue()).toBe('Valid'); }); test('should evaluate using LogicalOperation operators', () => { - const logicGroup: LogicGroupDefinition = { + const logicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.and, statements: [ { left: true, operator: GenericOperation.equals, right: true }, @@ -103,7 +102,7 @@ describe('DataCondition', () => { }); test('should evaluate complex nested conditions', () => { - const nestedLogicGroup: LogicGroupDefinition = { + const nestedLogicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.or, statements: [ { @@ -124,7 +123,7 @@ describe('DataCondition', () => { describe('LogicalGroup Tests', () => { test('should correctly handle AND logical operator', () => { - const logicGroup: LogicGroupDefinition = { + const logicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.and, statements: [ { left: true, operator: GenericOperation.equals, right: true }, @@ -137,7 +136,7 @@ describe('DataCondition', () => { }); test('should correctly handle OR logical operator', () => { - const logicGroup: LogicGroupDefinition = { + const logicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.or, statements: [ { left: true, operator: GenericOperation.equals, right: false }, @@ -150,7 +149,7 @@ describe('DataCondition', () => { }); test('should correctly handle XOR logical operator', () => { - const logicGroup: LogicGroupDefinition = { + const logicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.xor, statements: [ { left: true, operator: GenericOperation.equals, right: true }, @@ -164,7 +163,7 @@ describe('DataCondition', () => { }); test('should handle nested logical groups', () => { - const logicGroup: LogicGroupDefinition = { + const logicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.and, statements: [ { left: true, operator: GenericOperation.equals, right: true }, @@ -183,7 +182,7 @@ describe('DataCondition', () => { }); test('should handle groups with false conditions', () => { - const logicGroup: LogicGroupDefinition = { + const logicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.and, statements: [ { left: true, operator: GenericOperation.equals, right: true }, @@ -199,7 +198,7 @@ describe('DataCondition', () => { describe('Conditions with dataVariables', () => { test('should return "Yes" when dataVariable matches expected value', () => { - const condition: ExpressionDefinition = { + const condition: ExpressionProps = { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, operator: GenericOperation.equals, right: 'active', @@ -210,7 +209,7 @@ describe('DataCondition', () => { }); test('should return "No" when dataVariable does not match expected value', () => { - const condition: ExpressionDefinition = { + const condition: ExpressionProps = { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, operator: GenericOperation.equals, right: 'inactive', @@ -222,7 +221,7 @@ describe('DataCondition', () => { // TODO: unskip after adding UndefinedOperator test.skip('should handle missing data variable gracefully', () => { - const condition: ExpressionDefinition = { + const condition: ExpressionProps = { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.not_a_user.status' }, operator: GenericOperation.isDefined, right: undefined, @@ -233,7 +232,7 @@ describe('DataCondition', () => { }); test('should correctly compare numeric values from dataVariables', () => { - const condition: ExpressionDefinition = { + const condition: ExpressionProps = { left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.age' }, operator: NumberOperation.greaterThan, right: 24, @@ -249,7 +248,7 @@ describe('DataCondition', () => { }; dsm.add(dataSource2); - const logicGroup: LogicGroupDefinition = { + const logicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.and, statements: [ { @@ -270,7 +269,7 @@ describe('DataCondition', () => { }); test('should handle nested logical conditions with data variables', () => { - const logicGroup: LogicGroupDefinition = { + const logicGroup: LogicGroupProps = { logicalOperator: LogicalOperation.or, statements: [ { diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts new file mode 100644 index 000000000..55c3dc296 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts @@ -0,0 +1,1004 @@ +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 { getSymbolMain } from '../../../../../src/dom_components/model/SymbolUtils'; +import { ProjectData } from '../../../../../src/storage_manager'; + +describe('Collection component', () => { + let em: EditorModel; + let editor: Editor; + let dsm: DataSourceManager; + let dataSource: DataSource; + let wrapper: Component; + let firstRecord: DataRecord; + let secondRecord: DataRecord; + + beforeEach(() => { + ({ em, editor, 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(); + }); + + test('Collection component should be undroppable', () => { + const cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + expect(cmp.get('droppable')).toBe(false); + }); + + test('Collection items should be undraggable', () => { + const cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + cmp.components().forEach((child) => { + expect(child.get('draggable')).toBe(false); + }); + }); + + test('Collection items should be symbols', () => { + const cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: 'default', + }, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + expect(cmp.components()).toHaveLength(3); + cmp.components().forEach((child) => expect(child.get('type')).toBe('default')); + const children = cmp.components(); + const firstChild = children.at(0); + + children.slice(1).forEach((component) => { + expect(getSymbolMain(component)).toBe(firstChild); + }); + }); + + describe('Collection variables', () => { + describe('Properties', () => { + let cmp: Component; + let firstChild!: Component; + let firstGrandchild!: Component; + let secondChild!: Component; + let secondGrandchild!: Component; + let thirdChild!: Component; + + const checkHtmlModelAndView = ({ cmp, innerHTML }: { cmp: Component; innerHTML: string }) => { + const tagName = cmp.tagName; + expect(cmp.getInnerHTML()).toBe(innerHTML); + expect(cmp.toHTML()).toBe(`<${tagName} id="${cmp.getId()}">${innerHTML}`); + expect(cmp.getEl()?.innerHTML).toBe(innerHTML); + }; + + const checkRecordsWithInnerCmp = () => { + dataSource.getRecords().forEach((record, i) => { + const innerCmp = cmp.components().at(i).components().at(1); + checkHtmlModelAndView({ cmp: innerCmp, innerHTML: record.get('firstName') }); + }); + }; + + beforeEach(() => { + cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + { + tagName: 'span', + type: DataCollectionVariableType, + variableType: 'currentItem', + collectionId: 'my_collection', + path: 'firstName', + }, + ], + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + custom_property: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + firstChild = cmp.components().at(0); + firstGrandchild = firstChild.components().at(0); + secondChild = cmp.components().at(1); + secondGrandchild = secondChild.components().at(0); + thirdChild = cmp.components().at(2); + }); + + test('Evaluating to static value', () => { + expect(firstChild.get('name')).toBe('user1'); + expect(firstChild.get('custom_property')).toBe('user1'); + expect(firstGrandchild.get('name')).toBe('user1'); + + expect(secondChild.get('name')).toBe('user2'); + expect(secondChild.get('custom_property')).toBe('user2'); + expect(secondGrandchild.get('name')).toBe('user2'); + + checkRecordsWithInnerCmp(); + }); + + test('Watching Records', async () => { + firstRecord.set('user', 'new_user1_value'); + expect(firstChild.get('name')).toBe('new_user1_value'); + expect(firstChild.get('custom_property')).toBe('new_user1_value'); + expect(firstGrandchild.get('name')).toBe('new_user1_value'); + + expect(secondChild.get('name')).toBe('user2'); + expect(secondChild.get('custom_property')).toBe('user2'); + expect(secondGrandchild.get('name')).toBe('user2'); + + const firstName = 'Name1-up'; + firstRecord.set({ firstName }); + checkRecordsWithInnerCmp(); + }); + + test('Removing a record updates the collection component correctly', () => { + dataSource.removeRecord('user1'); + + expect(cmp.components().length).toBe(2); + + const updatedFirstChild = cmp.components().at(0); + const updatedSecondChild = cmp.components().at(1); + + expect(updatedFirstChild.get('name')).toBe('user2'); + expect(updatedSecondChild.get('name')).toBe('user3'); + + const updatedFirstGrandchild = updatedFirstChild.components().at(0); + const updatedSecondGrandchild = updatedSecondChild.components().at(0); + + expect(updatedFirstGrandchild.get('name')).toBe('user2'); + expect(updatedSecondGrandchild.get('name')).toBe('user3'); + + checkRecordsWithInnerCmp(); + }); + + test('Adding a record updates the collection component correctly', () => { + dataSource.addRecord({ id: 'user4', user: 'user4', firstName: 'Name4', age: '20' }); + + expect(cmp.components().length).toBe(4); + + const newChild = cmp.components().at(3); + expect(newChild.get('name')).toBe('user4'); + + const newGrandchild = newChild.components().at(0); + expect(newGrandchild.get('name')).toBe('user4'); + + const firstChild = cmp.components().at(0); + const secondChild = cmp.components().at(1); + const thirdChild = cmp.components().at(2); + + expect(firstChild.get('name')).toBe('user1'); + expect(secondChild.get('name')).toBe('user2'); + expect(thirdChild.get('name')).toBe('user3'); + + checkRecordsWithInnerCmp(); + }); + + test('Updating the value to a static value', async () => { + firstChild.set('name', 'new_content_value'); + expect(firstChild.get('name')).toBe('new_content_value'); + expect(secondChild.get('name')).toBe('new_content_value'); + + firstRecord.set('user', 'wrong_value'); + expect(firstChild.get('name')).toBe('new_content_value'); + expect(secondChild.get('name')).toBe('new_content_value'); + + firstGrandchild.set('name', 'new_content_value'); + expect(firstGrandchild.get('name')).toBe('new_content_value'); + expect(secondGrandchild.get('name')).toBe('new_content_value'); + + firstRecord.set('user', 'wrong_value'); + expect(firstGrandchild.get('name')).toBe('new_content_value'); + expect(secondGrandchild.get('name')).toBe('new_content_value'); + }); + + test('Updating the value to a different collection variable', async () => { + firstChild.set('name', { + // @ts-ignore + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'age', + }); + expect(firstChild.get('name')).toBe('12'); + expect(secondChild.get('name')).toBe('14'); + + firstRecord.set('age', 'new_value_12'); + secondRecord.set('age', 'new_value_14'); + + firstRecord.set('user', 'wrong_value'); + secondRecord.set('user', 'wrong_value'); + + expect(firstChild.get('name')).toBe('new_value_12'); + expect(secondChild.get('name')).toBe('new_value_14'); + + firstGrandchild.set('name', { + // @ts-ignore + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'age', + }); + expect(firstGrandchild.get('name')).toBe('new_value_12'); + expect(secondGrandchild.get('name')).toBe('new_value_14'); + + firstRecord.set('age', 'most_new_value_12'); + secondRecord.set('age', 'most_new_value_14'); + + expect(firstGrandchild.get('name')).toBe('most_new_value_12'); + expect(secondGrandchild.get('name')).toBe('most_new_value_14'); + }); + + test('Updating the value to a different dynamic variable', async () => { + firstChild.set('name', { + // @ts-ignore + type: DataVariableType, + path: 'my_data_source_id.user2.user', + }); + expect(firstChild.get('name')).toBe('user2'); + expect(secondChild.get('name')).toBe('user2'); + expect(thirdChild.get('name')).toBe('user2'); + + secondRecord.set('user', 'new_value'); + expect(firstChild.get('name')).toBe('new_value'); + expect(secondChild.get('name')).toBe('new_value'); + expect(thirdChild.get('name')).toBe('new_value'); + + firstGrandchild.set('name', { + // @ts-ignore + type: DataVariableType, + path: 'my_data_source_id.user2.user', + }); + expect(firstGrandchild.get('name')).toBe('new_value'); + expect(secondGrandchild.get('name')).toBe('new_value'); + + secondRecord.set('user', 'most_new_value'); + + expect(firstGrandchild.get('name')).toBe('most_new_value'); + expect(secondGrandchild.get('name')).toBe('most_new_value'); + }); + }); + + describe('Attributes', () => { + let cmp: Component; + let firstChild!: Component; + let firstGrandchild!: Component; + let secondChild!: Component; + let secondGrandchild!: Component; + let thirdChild!: Component; + + beforeEach(() => { + cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: 'default', + attributes: { + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + }, + ], + attributes: { + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + firstChild = cmp.components().at(0); + firstGrandchild = firstChild.components().at(0); + secondChild = cmp.components().at(1); + secondGrandchild = secondChild.components().at(0); + thirdChild = cmp.components().at(2); + }); + + test('Evaluating to static value', () => { + expect(firstChild.getAttributes()['name']).toBe('user1'); + expect(firstChild.getEl()?.getAttribute('name')).toBe('user1'); + expect(firstGrandchild.getAttributes()['name']).toBe('user1'); + expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('user1'); + + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('user2'); + expect(secondGrandchild.getAttributes()['name']).toBe('user2'); + expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('user2'); + }); + + test('Watching Records', async () => { + firstRecord.set('user', 'new_user1_value'); + expect(firstChild.getEl()?.getAttribute('name')).toBe('new_user1_value'); + expect(firstChild.getAttributes()['name']).toBe('new_user1_value'); + expect(firstGrandchild.getAttributes()['name']).toBe('new_user1_value'); + expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_user1_value'); + + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondGrandchild.getAttributes()['name']).toBe('user2'); + }); + + test('Updating the value to a static value', async () => { + firstChild.setAttributes({ name: 'new_content_value' }); + expect(firstChild.getAttributes()['name']).toBe('new_content_value'); + expect(firstChild.getEl()?.getAttribute('name')).toBe('new_content_value'); + expect(secondChild.getAttributes()['name']).toBe('new_content_value'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('new_content_value'); + + firstRecord.set('user', 'wrong_value'); + expect(firstChild.getAttributes()['name']).toBe('new_content_value'); + expect(firstChild.getEl()?.getAttribute('name')).toBe('new_content_value'); + expect(secondChild.getAttributes()['name']).toBe('new_content_value'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('new_content_value'); + + firstGrandchild.setAttributes({ name: 'new_content_value' }); + expect(firstGrandchild.getAttributes()['name']).toBe('new_content_value'); + expect(secondGrandchild.getAttributes()['name']).toBe('new_content_value'); + + firstRecord.set('user', 'wrong_value'); + expect(firstGrandchild.getAttributes()['name']).toBe('new_content_value'); + expect(secondGrandchild.getAttributes()['name']).toBe('new_content_value'); + }); + + test('Updating the value to a diffirent collection variable', async () => { + firstChild.setAttributes({ + name: { + // @ts-ignore + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'age', + }, + }); + expect(firstChild.getAttributes()['name']).toBe('12'); + expect(firstChild.getEl()?.getAttribute('name')).toBe('12'); + expect(secondChild.getAttributes()['name']).toBe('14'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('14'); + + firstRecord.set('age', 'new_value_12'); + secondRecord.set('age', 'new_value_14'); + + firstRecord.set('user', 'wrong_value'); + secondRecord.set('user', 'wrong_value'); + + expect(firstChild.getAttributes()['name']).toBe('new_value_12'); + expect(firstChild.getEl()?.getAttribute('name')).toBe('new_value_12'); + expect(secondChild.getAttributes()['name']).toBe('new_value_14'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('new_value_14'); + + firstGrandchild.setAttributes({ + name: { + // @ts-ignore + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'age', + }, + }); + expect(firstGrandchild.getAttributes()['name']).toBe('new_value_12'); + expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_value_12'); + expect(secondGrandchild.getAttributes()['name']).toBe('new_value_14'); + expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('new_value_14'); + + firstRecord.set('age', 'most_new_value_12'); + secondRecord.set('age', 'most_new_value_14'); + + expect(firstGrandchild.getAttributes()['name']).toBe('most_new_value_12'); + expect(secondGrandchild.getAttributes()['name']).toBe('most_new_value_14'); + }); + + test('Updating the value to a different dynamic variable', async () => { + firstChild.setAttributes({ + name: { + // @ts-ignore + type: DataVariableType, + path: 'my_data_source_id.user2.user', + }, + }); + expect(firstChild.getAttributes()['name']).toBe('user2'); + expect(firstChild.getEl()?.getAttribute('name')).toBe('user2'); + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('user2'); + expect(thirdChild.getAttributes()['name']).toBe('user2'); + + secondRecord.set('user', 'new_value'); + expect(firstChild.getAttributes()['name']).toBe('new_value'); + expect(firstChild.getEl()?.getAttribute('name')).toBe('new_value'); + expect(secondChild.getAttributes()['name']).toBe('new_value'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('new_value'); + expect(thirdChild.getAttributes()['name']).toBe('new_value'); + + firstGrandchild.setAttributes({ + name: { + // @ts-ignore + type: DataVariableType, + path: 'my_data_source_id.user2.user', + }, + }); + expect(firstGrandchild.getAttributes()['name']).toBe('new_value'); + expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_value'); + expect(secondGrandchild.getAttributes()['name']).toBe('new_value'); + expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('new_value'); + + secondRecord.set('user', 'most_new_value'); + + expect(firstGrandchild.getAttributes()['name']).toBe('most_new_value'); + expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('most_new_value'); + expect(secondGrandchild.getAttributes()['name']).toBe('most_new_value'); + expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('most_new_value'); + }); + }); + + test('Traits', () => { + const cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + traits: [ + { + name: 'attribute_trait', + value: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + { + name: 'property_trait', + changeProp: true, + value: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + expect(cmp.components()).toHaveLength(3); + const firstChild = cmp.components().at(0); + const secondChild = cmp.components().at(1); + + expect(firstChild.getAttributes()['attribute_trait']).toBe('user1'); + expect(firstChild.getEl()?.getAttribute('attribute_trait')).toBe('user1'); + expect(firstChild.get('property_trait')).toBe('user1'); + + expect(secondChild.getAttributes()['attribute_trait']).toBe('user2'); + expect(secondChild.getEl()?.getAttribute('attribute_trait')).toBe('user2'); + expect(secondChild.get('property_trait')).toBe('user2'); + + firstRecord.set('user', 'new_user1_value'); + expect(firstChild.getAttributes()['attribute_trait']).toBe('new_user1_value'); + expect(firstChild.getEl()?.getAttribute('attribute_trait')).toBe('new_user1_value'); + expect(firstChild.get('property_trait')).toBe('new_user1_value'); + + expect(secondChild.getAttributes()['attribute_trait']).toBe('user2'); + expect(secondChild.getEl()?.getAttribute('attribute_trait')).toBe('user2'); + expect(secondChild.get('property_trait')).toBe('user2'); + }); + }); + + describe('Serialization', () => { + let cmp: Component; + + beforeEach(() => { + const cmpDefinition = { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + custom_prop: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentIndex, + collectionId: 'my_collection', + path: 'user', + }, + attributes: { + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + traits: [ + { + name: 'attribute_trait', + value: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + { + name: 'property_trait', + changeProp: true, + value: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + ], + }; + + const collectionComponentDefinition = { + type: DataCollectionType, + collectionDef: { + componentDef: { + ...cmpDefinition, + components: [cmpDefinition, cmpDefinition], + }, + collectionConfig: { + collectionId: 'my_collection', + startIndex: 0, + endIndex: 1, + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + }; + + cmp = wrapper.components(collectionComponentDefinition)[0]; + }); + + test('Serializion with Collection Variables to JSON', () => { + expect(cmp.toJSON()).toMatchSnapshot(`Collection with no grandchildren`); + + const firstChild = cmp.components().at(0); + const newChildDefinition = { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentIndex, + collectionId: 'my_collection', + path: 'user', + }, + }; + firstChild.components().at(0).components(newChildDefinition); + expect(cmp.toJSON()).toMatchSnapshot(`Collection with grandchildren`); + }); + + test('Saving', () => { + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const component = frame.component.components[0]; + + expect(component).toMatchSnapshot(`Collection with no grandchildren`); + + const firstChild = cmp.components().at(0); + const newChildDefinition = { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentIndex, + collectionId: 'my_collection', + path: 'user', + }, + }; + firstChild.components().at(0).components(newChildDefinition); + expect(cmp.toJSON()).toMatchSnapshot(`Collection with grandchildren`); + }); + + test('Loading', () => { + const componentProjectData: ProjectData = { + assets: [], + pages: [ + { + frames: [ + { + component: { + components: [ + { + collectionDef: { + componentDef: { + attributes: { + attribute_trait: { + path: 'user', + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + }, + name: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + }, + components: [ + { + attributes: { + attribute_trait: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + name: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + }, + name: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + custom_prop: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: 'currentIndex', + }, + property_trait: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + type: 'default', + }, + { + attributes: { + attribute_trait: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + name: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + }, + name: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + custom_prop: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: 'currentIndex', + }, + property_trait: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + type: 'default', + }, + ], + name: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + custom_prop: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: 'currentIndex', + }, + property_trait: { + path: 'user', + type: DataCollectionVariableType, + collectionId: 'my_collection', + variableType: DataCollectionStateVariableType.currentItem, + }, + type: 'default', + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + path: 'my_data_source_id', + type: DataVariableType, + }, + endIndex: 1, + startIndex: 0, + }, + }, + type: DataCollectionType, + }, + ], + docEl: { + tagName: 'html', + }, + head: { + type: 'head', + }, + stylable: [ + 'background', + 'background-color', + 'background-image', + 'background-repeat', + 'background-attachment', + 'background-position', + 'background-size', + ], + type: 'wrapper', + }, + id: 'frameid', + }, + ], + id: 'pageid', + type: 'main', + }, + ], + styles: [], + symbols: [], + dataSources: [dataSource], + }; + editor.loadProjectData(componentProjectData); + + const components = editor.getComponents(); + const component = components.models[0]; + const firstChild = component.components().at(0); + const firstGrandchild = firstChild.components().at(0); + const secondChild = component.components().at(1); + const secondGrandchild = secondChild.components().at(0); + + expect(firstChild.get('name')).toBe('user1'); + expect(firstChild.getAttributes()['name']).toBe('user1'); + expect(firstGrandchild.get('name')).toBe('user1'); + expect(firstGrandchild.getAttributes()['name']).toBe('user1'); + + expect(secondChild.get('name')).toBe('user2'); + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondGrandchild.get('name')).toBe('user2'); + expect(secondGrandchild.getAttributes()['name']).toBe('user2'); + + firstRecord.set('user', 'new_user1_value'); + expect(firstChild.get('name')).toBe('new_user1_value'); + expect(firstChild.getAttributes()['name']).toBe('new_user1_value'); + expect(firstGrandchild.get('name')).toBe('new_user1_value'); + expect(firstGrandchild.getAttributes()['name']).toBe('new_user1_value'); + + expect(secondChild.get('name')).toBe('user2'); + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondGrandchild.get('name')).toBe('user2'); + expect(secondGrandchild.getAttributes()['name']).toBe('user2'); + }); + }); + + describe('Configuration options', () => { + test('Collection with start and end indexes', () => { + const cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + collectionConfig: { + startIndex: 1, + endIndex: 2, + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + expect(cmp.components()).toHaveLength(2); + const firstChild = cmp.components().at(0); + const secondChild = cmp.components().at(1); + + expect(firstChild.get('name')).toBe('user2'); + expect(secondChild.get('name')).toBe('user3'); + }); + }); + + describe('Diffirent Collection variable types', () => { + const stateVariableTests = [ + { variableType: DataCollectionStateVariableType.currentIndex, expectedValues: [0, 1, 2] }, + { variableType: DataCollectionStateVariableType.startIndex, expectedValues: [0, 0, 0] }, + { variableType: DataCollectionStateVariableType.endIndex, expectedValues: [2, 2, 2] }, + { + variableType: DataCollectionStateVariableType.collectionId, + expectedValues: ['my_collection', 'my_collection', 'my_collection'], + }, + { variableType: DataCollectionStateVariableType.totalItems, expectedValues: [3, 3, 3] }, + { variableType: DataCollectionStateVariableType.remainingItems, expectedValues: [2, 1, 0] }, + ]; + + stateVariableTests.forEach(({ variableType, expectedValues }) => { + test(`Variable type: ${variableType}`, () => { + const cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: variableType, + collectionId: 'my_collection', + }, + attributes: { + custom_attribute: { + type: DataCollectionVariableType, + variableType: variableType, + collectionId: 'my_collection', + }, + }, + traits: [ + { + name: 'attribute_trait', + value: { + type: DataCollectionVariableType, + variableType: variableType, + collectionId: 'my_collection', + }, + }, + { + name: 'property_trait', + changeProp: true, + value: { + type: DataCollectionVariableType, + variableType: variableType, + collectionId: 'my_collection', + }, + }, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const children = cmp.components(); + expect(children).toHaveLength(3); + + children.each((child, index) => { + expect(child.get('name')).toBe(expectedValues[index]); + expect(child.get('property_trait')).toBe(expectedValues[index]); + expect(child.getAttributes()['custom_attribute']).toBe(expectedValues[index]); + expect(child.getAttributes()['attribute_trait']).toBe(expectedValues[index]); + }); + }); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionVariable.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionVariable.ts new file mode 100644 index 000000000..a66c0136c --- /dev/null +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionVariable.ts @@ -0,0 +1,264 @@ +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 { ProjectData } from '../../../../../src/storage_manager'; +import { setupTestEditor } from '../../../../common'; + +describe('Collection variable components', () => { + let em: EditorModel; + let editor: Editor; + let dsm: DataSourceManager; + let dataSource: DataSource; + let wrapper: Component; + let firstRecord: DataRecord; + let secondRecord: DataRecord; + + beforeEach(() => { + ({ em, editor, dsm } = setupTestEditor()); + wrapper = em.getWrapper()!; + dataSource = dsm.add({ + id: 'my_data_source_id', + records: [ + { id: 'user1', user: 'user1', age: '12' }, + { id: 'user2', user: 'user2', age: '14' }, + { id: 'user3', user: 'user3', age: '16' }, + ], + }); + + firstRecord = dataSource.getRecord('user1')!; + secondRecord = dataSource.getRecord('user2')!; + }); + + afterEach(() => { + em.destroy(); + }); + + test('Gets the correct static value', async () => { + const cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const firstGrandchild = cmp.components().at(0).components().at(0); + expect(firstGrandchild.getInnerHTML()).toContain('user1'); + expect(firstGrandchild.getEl()?.innerHTML).toContain('user1'); + + const secondGrandchild = cmp.components().at(1).components().at(0); + expect(secondGrandchild.getInnerHTML()).toContain('user2'); + expect(secondGrandchild.getEl()?.innerHTML).toContain('user2'); + }); + + test('Watches collection variable changes', async () => { + const cmp = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + firstRecord.set('user', 'new_correct_value'); + + const firstGrandchild = cmp.components().at(0).components().at(0); + expect(firstGrandchild.getInnerHTML()).toContain('new_correct_value'); + expect(firstGrandchild.getEl()?.innerHTML).toContain('new_correct_value'); + + const secondGrandchild = cmp.components().at(1).components().at(0); + expect(secondGrandchild.getInnerHTML()).toContain('user2'); + expect(secondGrandchild.getEl()?.innerHTML).toContain('user2'); + }); + + describe('Serialization', () => { + let cmp: Component; + + beforeEach(() => { + const variableCmpDef = { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }; + + const collectionComponentDefinition = { + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: 'default', + }, + variableCmpDef, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + startIndex: 0, + endIndex: 2, + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + }; + + cmp = wrapper.components(collectionComponentDefinition)[0]; + }); + + test('Serializion to JSON', () => { + expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( no grandchildren )`); + + const firstChild = cmp.components().at(0); + const newChildDefinition = { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentIndex, + collectionId: 'my_collection', + path: 'user', + }; + firstChild.components().at(0).components(newChildDefinition); + expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( with grandchildren )`); + }); + + test('Saving', () => { + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const component = frame.component.components[0]; + + expect(component).toMatchSnapshot(`Collection with collection variable component ( no grandchildren )`); + + const firstChild = cmp.components().at(0); + const newChildDefinition = { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentIndex, + collectionId: 'my_collection', + path: 'user', + }; + + firstChild.components().at(0).components(newChildDefinition); + expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( with grandchildren )`); + }); + + test('Loading', () => { + const componentProjectData: ProjectData = { + assets: [], + pages: [ + { + frames: [ + { + component: { + components: [ + { + collectionDef: { + componentDef: { + type: 'default', + components: [ + { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'my_collection', + path: 'user', + }, + ], + }, + collectionConfig: { + collectionId: 'my_collection', + dataSource: { + path: 'my_data_source_id', + type: DataVariableType, + }, + endIndex: 1, + startIndex: 0, + }, + }, + type: DataCollectionType, + }, + ], + docEl: { + tagName: 'html', + }, + head: { + type: 'head', + }, + stylable: [ + 'background', + 'background-color', + 'background-image', + 'background-repeat', + 'background-attachment', + 'background-position', + 'background-size', + ], + type: 'wrapper', + }, + id: 'frameid', + }, + ], + id: 'pageid', + type: 'main', + }, + ], + styles: [], + symbols: [], + dataSources: [dataSource], + }; + editor.loadProjectData(componentProjectData); + + const components = editor.getComponents(); + const component = components.models[0]; + const firstChild = component.components().at(0); + const firstGrandchild = firstChild.components().at(0); + const secondChild = component.components().at(1); + const secondGrandchild = secondChild.components().at(0); + + expect(firstGrandchild.getInnerHTML()).toBe('user1'); + expect(secondGrandchild.getInnerHTML()).toBe('user2'); + + firstRecord.set('user', 'new_user1_value'); + expect(firstGrandchild.getInnerHTML()).toBe('new_user1_value'); + expect(secondGrandchild.getInnerHTML()).toBe('user2'); + + secondRecord.set('user', 'new_user2_value'); + expect(firstGrandchild.getInnerHTML()).toBe('new_user1_value'); + expect(secondGrandchild.getInnerHTML()).toBe('new_user2_value'); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap new file mode 100644 index 000000000..79be166cd --- /dev/null +++ b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap @@ -0,0 +1,519 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Collection component Serialization Saving: Collection with grandchildren 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "my_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "endIndex": 1, + "startIndex": 0, + }, + "componentDef": { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "components": [ + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "components": [ + { + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "type": "default", + }, + ], + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + ], + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + }, + "type": "data-collection", +} +`; + +exports[`Collection component Serialization Saving: Collection with no grandchildren 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "my_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "endIndex": 1, + "startIndex": 0, + }, + "componentDef": { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "components": [ + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + ], + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + }, + "type": "data-collection", +} +`; + +exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with grandchildren 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "my_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "endIndex": 1, + "startIndex": 0, + }, + "componentDef": { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "components": [ + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "components": [ + { + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "type": "default", + }, + ], + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + ], + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + }, + "type": "data-collection", +} +`; + +exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with no grandchildren 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "my_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "endIndex": 1, + "startIndex": 0, + }, + "componentDef": { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "components": [ + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + ], + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + }, + "type": "data-collection", +} +`; diff --git a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionVariable.ts.snap b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionVariable.ts.snap new file mode 100644 index 000000000..8edb6a4d2 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionVariable.ts.snap @@ -0,0 +1,141 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Collection variable components Serialization Saving: Collection with collection variable component ( no grandchildren ) 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "my_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "endIndex": 2, + "startIndex": 0, + }, + "componentDef": { + "components": [ + { + "type": "default", + }, + { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + ], + "type": "default", + }, + }, + "type": "data-collection", +} +`; + +exports[`Collection variable components Serialization Saving: Collection with collection variable component ( with grandchildren ) 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "my_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "endIndex": 2, + "startIndex": 0, + }, + "componentDef": { + "components": [ + { + "components": [ + { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + ], + "type": "default", + }, + { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + ], + "type": "default", + }, + }, + "type": "data-collection", +} +`; + +exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( no grandchildren ) 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "my_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "endIndex": 2, + "startIndex": 0, + }, + "componentDef": { + "components": [ + { + "type": "default", + }, + { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + ], + "type": "default", + }, + }, + "type": "data-collection", +} +`; + +exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( with grandchildren ) 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "my_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "endIndex": 2, + "startIndex": 0, + }, + "componentDef": { + "components": [ + { + "components": [ + { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentIndex", + }, + ], + "type": "default", + }, + { + "collectionId": "my_collection", + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + ], + "type": "default", + }, + }, + "type": "data-collection", +} +`; diff --git a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap new file mode 100644 index 000000000..92ce3f6c8 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Collection component Nested collections are correctly serialized 1`] = ` +{ + "collectionDef": { + "collectionConfig": { + "collectionId": "parent_collection", + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + }, + "componentDef": { + "collectionDef": { + "collectionConfig": { + "collectionId": "nested_collection", + "dataSource": { + "path": "nested_data_source_id", + "type": "data-variable", + }, + }, + "componentDef": { + "name": { + "path": "user", + "type": "data-collection-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + }, + "type": "data-collection", + }, + }, + "type": "data-collection", +} +`; diff --git a/packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts b/packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts new file mode 100644 index 000000000..e7b3a48f2 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts @@ -0,0 +1,430 @@ +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'; + +describe('Collection component', () => { + let em: EditorModel; + let editor: Editor; + let dsm: DataSourceManager; + let dataSource: DataSource; + let nestedDataSource: DataSource; + let wrapper: Component; + let firstRecord: DataRecord; + let secondRecord: DataRecord; + let firstNestedRecord: DataRecord; + let secondNestedRecord: DataRecord; + + beforeEach(() => { + ({ em, editor, dsm } = setupTestEditor()); + wrapper = em.getWrapper()!; + dataSource = dsm.add({ + id: 'my_data_source_id', + records: [ + { id: 'user1', user: 'user1', age: '12' }, + { id: 'user2', user: 'user2', age: '14' }, + ], + }); + + nestedDataSource = dsm.add({ + id: 'nested_data_source_id', + records: [ + { id: 'nested_user1', user: 'nested_user1', age: '12' }, + { id: 'nested_user2', user: 'nested_user2', age: '14' }, + { id: 'nested_user3', user: 'nested_user3', age: '16' }, + ], + }); + + firstRecord = dataSource.getRecord('user1')!; + secondRecord = dataSource.getRecord('user2')!; + firstNestedRecord = nestedDataSource.getRecord('nested_user1')!; + secondNestedRecord = nestedDataSource.getRecord('nested_user2')!; + }); + + afterEach(() => { + em.destroy(); + }); + + test('Nested collections bind to correct data sources', () => { + const parentCollection = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'nested_collection', + path: 'user', + }, + }, + collectionConfig: { + collectionId: 'nested_collection', + dataSource: { + type: DataVariableType, + path: 'nested_data_source_id', + }, + }, + }, + }, + collectionConfig: { + collectionId: 'parent_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const nestedCollection = parentCollection.components().at(0); + const nestedFirstChild = nestedCollection.components().at(0); + const nestedSecondChild = nestedCollection.components().at(1); + + expect(nestedFirstChild.get('name')).toBe('nested_user1'); + expect(nestedSecondChild.get('name')).toBe('nested_user2'); + }); + + test('Updates in parent collection propagate to nested collections', () => { + const parentCollection = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'nested_collection', + path: 'user', + }, + }, + collectionConfig: { + collectionId: 'nested_collection', + dataSource: { + type: DataVariableType, + path: 'nested_data_source_id', + }, + }, + }, + }, + collectionConfig: { + collectionId: 'parent_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const nestedCollection = parentCollection.components().at(0); + const nestedFirstChild = nestedCollection.components().at(0); + const nestedSecondChild = nestedCollection.components().at(1); + + firstNestedRecord.set('user', 'updated_user1'); + expect(nestedFirstChild.get('name')).toBe('updated_user1'); + expect(nestedSecondChild.get('name')).toBe('nested_user2'); + }); + + test('Nested collections are correctly serialized', () => { + const parentCollection = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + path: 'user', + }, + }, + collectionConfig: { + collectionId: 'nested_collection', + dataSource: { + type: DataVariableType, + path: 'nested_data_source_id', + }, + }, + }, + }, + collectionConfig: { + collectionId: 'parent_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const serialized = parentCollection.toJSON(); + expect(serialized).toMatchSnapshot(); + }); + + test('Nested collections respect startIndex and endIndex', () => { + const parentCollection = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'nested_collection', + path: 'user', + }, + }, + collectionConfig: { + collectionId: 'nested_collection', + startIndex: 0, + endIndex: 1, + dataSource: { + type: DataVariableType, + path: 'nested_data_source_id', + }, + }, + }, + }, + collectionConfig: { + collectionId: 'parent_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const nestedCollection = parentCollection.components().at(0); + expect(nestedCollection.components().length).toBe(2); + }); + + test('Nested collection gets and watches value from the parent collection', () => { + const parentCollection = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'parent_collection', + path: 'user', + }, + }, + collectionConfig: { + collectionId: 'nested_collection', + dataSource: { + type: DataVariableType, + path: 'nested_data_source_id', + }, + }, + }, + }, + collectionConfig: { + collectionId: 'parent_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const nestedCollection = parentCollection.components().at(0); + const firstNestedChild = nestedCollection.components().at(0); + + // Verify initial value + expect(firstNestedChild.get('name')).toBe('user1'); + + // Update value in parent collection and verify nested collection updates + firstRecord.set('user', 'updated_user1'); + expect(firstNestedChild.get('name')).toBe('updated_user1'); + }); + + test('Nested collection switches to using its own collection variable', () => { + const parentCollection = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: DataCollectionType, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + path: 'user', + collectionId: 'parent_collection', + }, + }, + collectionConfig: { + collectionId: 'nested_collection', + dataSource: { + type: DataVariableType, + path: 'nested_data_source_id', + }, + }, + }, + }, + collectionConfig: { + collectionId: 'parent_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const nestedCollection = parentCollection.components().at(0); + + const firstChild = nestedCollection.components().at(0); + // Replace the collection variable with one from the inner collection + firstChild.set('name', { + // @ts-ignore + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + path: 'user', + collectionId: 'nested_collection', + }); + + expect(firstChild.get('name')).toBe('nested_user1'); + }); + + describe('Nested Collection Component with Parent and Nested Data Sources', () => { + let parentCollection: Component; + let nestedCollection: Component; + + beforeEach(() => { + // Initialize the parent and nested collections + parentCollection = wrapper.components({ + type: DataCollectionType, + collectionDef: { + componentDef: { + type: DataCollectionType, + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'parent_collection', + path: 'user', + }, + collectionDef: { + componentDef: { + type: 'default', + name: { + type: DataCollectionVariableType, + variableType: DataCollectionStateVariableType.currentItem, + collectionId: 'nested_collection', + path: 'user', + }, + }, + collectionConfig: { + collectionId: 'nested_collection', + dataSource: { + type: DataVariableType, + path: 'nested_data_source_id', + }, + }, + }, + }, + collectionConfig: { + collectionId: 'parent_collection', + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + nestedCollection = parentCollection.components().at(0); + }); + + test('Removing a record from the parent data source updates the parent collection correctly', () => { + // Verify initial state + expect(parentCollection.components().length).toBe(2); // 2 parent records initially + + // Remove a record from the parent data source + dataSource.removeRecord('user1'); + + // Verify that the parent collection updates correctly + expect(parentCollection.components().length).toBe(1); // Only 1 parent record remains + expect(parentCollection.components().at(0).get('name')).toBe('user2'); // Verify updated name + + // Verify that the nested collection is unaffected + expect(nestedCollection.components().length).toBe(3); // Nested records remain the same + expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify nested name + }); + + test('Adding a record to the parent data source updates the parent collection correctly', () => { + // Verify initial state + expect(parentCollection.components().length).toBe(2); // 2 parent records initially + + // Add a new record to the parent data source + dataSource.addRecord({ id: 'user3', user: 'user3', age: '16' }); + + // Verify that the parent collection updates correctly + expect(parentCollection.components().length).toBe(3); // 3 parent records now + expect(parentCollection.components().at(2).get('name')).toBe('user3'); // Verify new name + + // Verify that the nested collection is unaffected + expect(nestedCollection.components().length).toBe(3); // Nested records remain the same + expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify nested name + expect(parentCollection.components().at(2).components().at(0).get('name')).toBe('nested_user1'); // Verify nested name + }); + + test('Removing a record from the nested data source updates the nested collection correctly', () => { + // Verify initial state + expect(nestedCollection.components().length).toBe(3); // 3 nested records initially + + // Remove a record from the nested data source + nestedDataSource.removeRecord('nested_user1'); + + // Verify that the nested collection updates correctly + expect(nestedCollection.components().length).toBe(2); // Only 2 nested records remain + expect(nestedCollection.components().at(0).get('name')).toBe('nested_user2'); // Verify updated name + expect(nestedCollection.components().at(1).get('name')).toBe('nested_user3'); // Verify updated name + }); + + test('Adding a record to the nested data source updates the nested collection correctly', () => { + // Verify initial state + expect(nestedCollection.components().length).toBe(3); // 3 nested records initially + expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify initial name + expect(nestedCollection.components().at(1).get('name')).toBe('nested_user2'); // Verify initial name + expect(nestedCollection.components().at(2).get('name')).toBe('nested_user3'); // Verify initial name + + // Add a new record to the nested data source + nestedDataSource.addRecord({ id: 'user4', user: 'nested_user4', age: '18' }); + + // Verify that the nested collection updates correctly + expect(nestedCollection.components().length).toBe(4); // 4 nested records now + expect(nestedCollection.components().at(3).get('name')).toBe('nested_user4'); // Verify new name + + // Verify existing records are unaffected + expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify existing name + expect(nestedCollection.components().at(1).get('name')).toBe('nested_user2'); // Verify existing name + expect(nestedCollection.components().at(2).get('name')).toBe('nested_user3'); // Verify existing name + }); + }); +}); From 8298c4d69eaa9753629272f3aa65a9443edb8972 Mon Sep 17 00:00:00 2001 From: Lucas Tabis Date: Mon, 3 Feb 2025 09:54:19 +0100 Subject: [PATCH 2/2] fix: unknown pseudo element and unused rulesets in scss (#6389) fix: unused rulesets and unknown pseudo element --- packages/core/src/styles/scss/main.scss | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/core/src/styles/scss/main.scss b/packages/core/src/styles/scss/main.scss index b6969830d..5bade05a5 100644 --- a/packages/core/src/styles/scss/main.scss +++ b/packages/core/src/styles/scss/main.scss @@ -104,15 +104,9 @@ $colorsAll: (one, var(--gjs-primary-color)), (two, var(--gjs-secondary-color)), } .#{$app-prefix}test { - &::btn { + &btn { color: '#fff'; } - - &input { - } - - &header { - } } .opac50 { @include opacity(0.5);