diff --git a/packages/core/src/data_sources/model/ComponentDataVariable.ts b/packages/core/src/data_sources/model/ComponentDataVariable.ts index 31805f0f2..fbb4309dc 100644 --- a/packages/core/src/data_sources/model/ComponentDataVariable.ts +++ b/packages/core/src/data_sources/model/ComponentDataVariable.ts @@ -1,20 +1,16 @@ -import { ModelDestroyOptions } from 'backbone'; -import { ObjectAny } from '../../common'; -import Component from '../../dom_components/model/Component'; -import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../dom_components/model/types'; +import { ComponentOptions, ComponentProperties } from '../../dom_components/model/types'; import { toLowerCase } from '../../utils/mixins'; import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable'; +import { ComponentWithDataResolver } from './ComponentWithDataResolver'; +import { DataResolver } from '../types'; import { DataCollectionStateMap } from './data_collection/types'; -import { keyCollectionsStateMap } from './data_collection/constants'; export interface ComponentDataVariableProps extends ComponentProperties { type?: typeof DataVariableType; dataResolver?: DataVariableProps; } -export default class ComponentDataVariable extends Component { - dataResolver: DataVariable; - +export default class ComponentDataVariable extends ComponentWithDataResolver { get defaults() { return { // @ts-ignore @@ -25,19 +21,16 @@ export default class ComponentDataVariable extends Component { }; } - constructor(props: ComponentDataVariableProps, opt: ComponentOptions) { - super(props, opt); - - this.dataResolver = new DataVariable(props.dataResolver ?? {}, { - ...opt, - collectionsStateMap: this.get(keyCollectionsStateMap), - }); + getPath() { + return this.dataResolver.get('path'); + } - this.listenToPropsChange(); + getCollectionId() { + return this.dataResolver.get('collectionId'); } - getPath() { - return this.dataResolver.get('path'); + getVariableType() { + return this.dataResolver.get('variableType'); } getDefaultValue() { @@ -48,12 +41,12 @@ export default class ComponentDataVariable extends Component { return this.dataResolver.getDataValue(); } - getInnerHTML() { - return this.getDataValue(); + resolvesFromCollection() { + return this.dataResolver.resolvesFromCollection(); } - getCollectionsStateMap() { - return this.get(keyCollectionsStateMap) ?? {}; + getInnerHTML() { + return this.getDataValue(); } setPath(newPath: string) { @@ -64,10 +57,6 @@ export default class ComponentDataVariable extends Component { this.dataResolver.set('defaultValue', newValue); } - setDataResolver(props: DataVariableProps) { - this.dataResolver.set(props); - } - /** * Sets the data source path and resets related properties. * This will set collectionId and variableType to undefined as it's typically @@ -82,38 +71,11 @@ export default class ComponentDataVariable extends Component { }); } - private listenToPropsChange() { - this.listenTo( - this.dataResolver, - 'change', - (() => { - this.__changesUp({ m: this }); - }).bind(this), - ); - this.on('change:dataResolver', () => { - this.dataResolver.set(this.get('dataResolver')); - }); - this.on(`change:${keyCollectionsStateMap}`, (_: Component, value: DataCollectionStateMap) => { - this.dataResolver.updateCollectionsStateMap(value); - }); - } - - toJSON(opts?: ObjectAny): ComponentDefinition { - const json = super.toJSON(opts); - const dataResolver: DataVariableProps = this.dataResolver.toJSON(); - delete dataResolver.type; - - return { - ...json, - dataResolver, - }; - } - - destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { - this.stopListening(this.dataResolver, 'change'); - this.off('change:dataResolver'); - this.off(`change:${keyCollectionsStateMap}`); - return super.destroy(options); + protected createResolverInstance( + props: DataVariableProps, + options: ComponentOptions & { collectionsStateMap: DataCollectionStateMap }, + ): DataResolver { + return new DataVariable(props, options); } static isComponent(el: HTMLElement) { diff --git a/packages/core/src/data_sources/model/ComponentWithDataResolver.ts b/packages/core/src/data_sources/model/ComponentWithDataResolver.ts new file mode 100644 index 000000000..3eb144e41 --- /dev/null +++ b/packages/core/src/data_sources/model/ComponentWithDataResolver.ts @@ -0,0 +1,93 @@ +import { ModelDestroyOptions } from 'backbone'; +import { ObjectAny } from '../../common'; +import Component from '../../dom_components/model/Component'; +import { DataResolver, DataResolverProps, ResolverFromProps } from '../types'; +import { ComponentOptions, ComponentProperties } from '../../dom_components/model/types'; +import { DataCollectionStateMap } from './data_collection/types'; + +interface ComponentWithDataResolverProps extends ComponentProperties { + type: T['type']; + dataResolver: T; +} + +export abstract class ComponentWithDataResolver extends Component { + dataResolver: ResolverFromProps; + + constructor(props: ComponentWithDataResolverProps, opt: ComponentOptions) { + super(props, opt); + + this.dataResolver = this.initializeDataResolver(props, opt); + this.listenToPropsChange(); + } + + private initializeDataResolver( + props: ComponentWithDataResolverProps, + opt: ComponentOptions, + ): ResolverFromProps { + const resolverProps = props.dataResolver ?? { + type: props.type, + }; + + const resolver = this.createResolverInstance(resolverProps, { + ...opt, + collectionsStateMap: this.collectionsStateMap, + }); + + return resolver as ResolverFromProps; + } + + protected abstract createResolverInstance( + props: T, + options: ComponentOptions & { collectionsStateMap: DataCollectionStateMap }, + ): DataResolver; + + getDataResolver() { + return this.get('dataResolver'); + } + + setDataResolver(props: any) { + return this.set('dataResolver', props); + } + + onCollectionsStateMapUpdate(collectionsStateMap: DataCollectionStateMap): void { + this.dataResolver.updateCollectionsStateMap(collectionsStateMap); + super.onCollectionsStateMapUpdate(collectionsStateMap); + } + + protected listenToPropsChange() { + this.listenTo( + this.dataResolver, + 'change', + (() => { + this.__changesUp({ m: this }); + }).bind(this), + ); + + this.on('change:dataResolver', () => { + // @ts-ignore + this.dataResolver.set(this.get('dataResolver')); + }); + } + + protected removePropsListeners() { + this.stopListening(this.dataResolver); + this.off('change:dataResolver'); + this.off(`change:${this.collectionsStateMap}`); + } + + destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { + this.removePropsListeners(); + return super.destroy(options); + } + + toJSON(opts?: ObjectAny): any { + const json = super.toJSON(opts); + const dataResolver = this.dataResolver.toJSON(); + delete dataResolver.type; + + return { + ...json, + dataResolver, + }; + } +} diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index b866270d4..3a60e4084 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -13,6 +13,11 @@ export interface DataVariableProps { variableType?: DataCollectionStateType; } +interface DataVariableOptions { + em: EditorModel; + collectionsStateMap: DataCollectionStateMap; +} + export default class DataVariable extends Model { private em: EditorModel; private collectionsStateMap: DataCollectionStateMap; @@ -27,7 +32,7 @@ export default class DataVariable extends Model { }; } - constructor(props: DataVariableProps, options: { em: EditorModel; collectionsStateMap: DataCollectionStateMap }) { + constructor(props: DataVariableProps, options: DataVariableOptions) { super(props, options); this.em = options.em; this.collectionsStateMap = options.collectionsStateMap; @@ -49,18 +54,6 @@ export default class DataVariable extends Model { return this.get('variableType'); } - getDataValue() { - if (this.resolvesFromCollection()) { - const valueOrDataVariableProps = this.resolveCollectionVariable(); - if (!isDataVariable(valueOrDataVariableProps)) return valueOrDataVariableProps; - const { path = '' } = valueOrDataVariableProps; - - return this.resolveDataSourcePath(path); - } - - return this.resolveDataSourcePath(this.path); - } - resolvesFromCollection(): boolean { return !!this.collectionId; } @@ -73,11 +66,8 @@ export default class DataVariable extends Model { getResolverPath(): string | false { if (this.resolvesFromCollection()) { const value = this.resolveCollectionVariable(); - if (!isDataVariable(value)) return false; - - return value.path ?? ''; + return isDataVariable(value) ? (value.path ?? '') : false; } - return this.path; } @@ -87,42 +77,102 @@ export default class DataVariable extends Model { const filteredJson = Object.fromEntries( Object.entries(json).filter(([key, value]) => value !== defaults[key as keyof DataVariableProps]), ) as Partial; + return { ...filteredJson, type: DataVariableType }; + } - return { - ...filteredJson, - type: DataVariableType, + getDataValue() { + const opts = { + em: this.em, + collectionsStateMap: this.collectionsStateMap, }; + + return DataVariable.resolveDataResolver( + { + path: this.path, + defaultValue: this.defaultValue, + collectionId: this.collectionId, + variableType: this.variableType, + }, + opts, + ); } - private resolveDataSourcePath(path: string) { - return this.em.DataSources.getValue(path, this.defaultValue); + static resolveDataSourceVariable( + props: { + path?: string; + defaultValue?: string; + }, + opts: { + em: EditorModel; + }, + ) { + return opts.em.DataSources.getValue(props.path ?? '', props.defaultValue ?? ''); + } + + static resolveDataResolver( + props: { + path?: string; + defaultValue?: string; + collectionId?: string; + variableType?: DataCollectionStateType; + }, + opts: DataVariableOptions, + ) { + if (props.collectionId) { + const value = DataVariable.resolveCollectionVariable(props, opts); + if (!isDataVariable(value)) return value; + return DataVariable.resolveDataSourceVariable( + { path: value.path ?? '', defaultValue: props.defaultValue ?? '' }, + { em: opts.em }, + ); + } + return DataVariable.resolveDataSourceVariable( + { path: props.path ?? '', defaultValue: props.defaultValue ?? '' }, + { em: opts.em }, + ); } private resolveCollectionVariable(): unknown { - const { collectionId = '', variableType, path, defaultValue = '' } = this.attributes; - if (!this.collectionsStateMap) return defaultValue; + const { em, collectionsStateMap } = this; + return DataVariable.resolveCollectionVariable(this.attributes, { em, collectionsStateMap }); + } + + static resolveCollectionVariable( + dataResolverProps: { + collectionId?: string; + variableType?: DataCollectionStateType; + path?: string; + defaultValue?: string; + }, + opts: DataVariableOptions, + ): unknown { + const { collectionId = '', variableType, path, defaultValue = '' } = dataResolverProps; + const { em, collectionsStateMap } = opts; + + if (!collectionsStateMap) return defaultValue; - const collectionItem = this.collectionsStateMap[collectionId]; + const collectionItem = collectionsStateMap[collectionId]; if (!collectionItem) return defaultValue; if (!variableType) { - this.em.logError(`Missing collection variable type for collection: ${collectionId}`); + em.logError(`Missing collection variable type for collection: ${collectionId}`); return defaultValue; } return variableType === 'currentItem' - ? this.resolveCurrentItem(collectionItem, path, collectionId) + ? DataVariable.resolveCurrentItem(collectionItem, path, collectionId, em) : collectionItem[variableType]; } - private resolveCurrentItem( + private static resolveCurrentItem( collectionItem: DataCollectionState, path: string | undefined, collectionId: string, + em: EditorModel, ): unknown { const currentItem = collectionItem.currentItem; if (!currentItem) { - this.em.logError(`Current item is missing for collection: ${collectionId}`); + em.logError(`Current item is missing for collection: ${collectionId}`); return ''; } @@ -132,7 +182,7 @@ export default class DataVariable extends Model { } if (path && !(currentItem as any)[path]) { - this.em.logError(`Path not found in current item: ${path} for collection: ${collectionId}`); + em.logError(`Path not found in current item: ${path} for collection: ${collectionId}`); return ''; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts index f5e21d52d..bccb49fdb 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts @@ -1,17 +1,19 @@ -import Component from '../../../dom_components/model/Component'; import { - ComponentDefinition as ComponentProperties, + ComponentAddType, ComponentDefinitionDefined, ComponentOptions, + ComponentProperties, ToHTMLOptions, - ComponentAddType, } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; -import { DataCondition, DataConditionOutputChangedEvent, DataConditionProps, DataConditionType } from './DataCondition'; +import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition'; import { ConditionProps } from './DataConditionEvaluator'; import { StringOperation } from './operators/StringOperator'; -import { ObjectAny } from '../../../common'; import { DataConditionIfTrueType, DataConditionIfFalseType } from './constants'; +import { ComponentWithDataResolver } from '../ComponentWithDataResolver'; +import Component from '../../../dom_components/model/Component'; +import { DataResolver } from '../../types'; +import { DataCollectionStateMap } from '../data_collection/types'; export type DataConditionDisplayType = typeof DataConditionIfTrueType | typeof DataConditionIfFalseType; @@ -20,9 +22,7 @@ export interface ComponentDataConditionProps extends ComponentProperties { dataResolver: DataConditionProps; } -export default class ComponentDataCondition extends Component { - dataResolver: DataCondition; - +export default class ComponentDataCondition extends ComponentWithDataResolver { get defaults(): ComponentDefinitionDefined { return { // @ts-ignore @@ -47,16 +47,6 @@ export default class ComponentDataCondition extends Component { }; } - constructor(props: ComponentDataConditionProps, opt: ComponentOptions) { - // @ts-ignore - super(props, opt); - - const { condition } = props.dataResolver; - this.dataResolver = new DataCondition({ condition }, { em: opt.em }); - - this.listenToPropsChange(); - } - isTrue() { return this.dataResolver.isTrue(); } @@ -93,30 +83,18 @@ export default class ComponentDataCondition extends Component { return this.getOutputContent()?.getInnerHTML(opts) ?? ''; } + protected createResolverInstance( + props: DataConditionProps, + options: ComponentOptions & { collectionsStateMap: DataCollectionStateMap }, + ): DataResolver { + return new DataCondition(props, options); + } + private setComponentsAtIndex(index: number, newContent: ComponentAddType) { const component = this.components().at(index); component?.components(newContent); } - private listenToPropsChange() { - this.on('change:dataResolver', () => { - this.dataResolver.set(this.get('dataResolver')); - }); - } - - toJSON(opts?: ObjectAny): ComponentProperties { - const json = super.toJSON(opts); - const dataResolver = this.dataResolver.toJSON(); - delete dataResolver.type; - delete dataResolver.ifTrue; - delete dataResolver.ifFalse; - - return { - ...json, - dataResolver, - }; - } - static isComponent(el: HTMLElement) { return toLowerCase(el.tagName) === DataConditionType; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts index 812275fa4..49dff319c 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts @@ -1,10 +1,10 @@ -import { Operator } from './operators/BaseOperator'; -import { DataConditionOperation } from './operators/types'; +import { SimpleOperator } from './operators/BaseOperator'; +import { DataConditionSimpleOperation } from './types'; export class ConditionStatement { constructor( private leftValue: any, - private operator: Operator, + private operator: SimpleOperator, private rightValue: any, ) {} diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 765b27d9d..d636e27f7 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -2,14 +2,13 @@ import { Model } from '../../../common'; import EditorModel from '../../../editor/model/Editor'; import DataVariable, { DataVariableProps } from '../DataVariable'; import DataResolverListener from '../DataResolverListener'; -import { resolveDynamicValue, isDataVariable } from '../../utils'; +import { valueOrResolve, isDataVariable } from '../../utils'; import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator'; -import { AnyTypeOperation } from './operators/AnyTypeOperator'; import { BooleanOperation } from './operators/BooleanOperator'; -import { NumberOperation } from './operators/NumberOperator'; import { StringOperation } from './operators/StringOperator'; import { isUndefined } from 'underscore'; import { DataCollectionStateMap } from '../data_collection/types'; +import { DataConditionSimpleOperation } from './types'; export const DataConditionType = 'data-condition' as const; export const DataConditionEvaluationChangedEvent = 'data-condition-evaluation-changed'; @@ -17,7 +16,7 @@ export const DataConditionOutputChangedEvent = 'data-condition-output-changed'; export interface ExpressionProps { left?: any; - operator?: AnyTypeOperation | StringOperation | NumberOperation; + operator?: DataConditionSimpleOperation; right?: any; } @@ -33,6 +32,12 @@ export interface DataConditionProps { ifFalse?: any; } +type DataConditionOptions = { + em: EditorModel; + onValueChange?: () => void; + collectionsStateMap?: DataCollectionStateMap; +}; + export class DataCondition extends Model { private em: EditorModel; private collectionsStateMap: DataCollectionStateMap = {}; @@ -53,13 +58,14 @@ export class DataCondition extends Model { }; } - constructor(props: DataConditionProps, opts: { em: EditorModel; onValueChange?: () => void }) { + constructor(props: DataConditionProps, opts: DataConditionOptions) { if (isUndefined(props.condition)) { opts.em.logError('No condition was provided to a conditional component.'); } super(props, opts); this.em = opts.em; + this.collectionsStateMap = opts.collectionsStateMap ?? {}; const { condition = {} } = props; const instance = new DataConditionEvaluator({ condition }, { em: this.em }); @@ -80,7 +86,12 @@ export class DataCondition extends Model { return this.get('ifFalse'); } + getOperations() { + return this._conditionEvaluator.getOperations(); + } + setCondition(condition: ConditionProps) { + this.set('condition', condition); this._conditionEvaluator.set('condition', condition); this.trigger(DataConditionOutputChangedEvent, this.getDataValue()); } @@ -98,6 +109,8 @@ export class DataCondition extends Model { } getDataValue(skipDynamicValueResolution: boolean = false): any { + const { em, collectionsStateMap } = this; + const options = { em, collectionsStateMap }; const ifTrue = this.getIfTrue(); const ifFalse = this.getIfFalse(); @@ -106,7 +119,7 @@ export class DataCondition extends Model { return isConditionTrue ? ifTrue : ifFalse; } - return isConditionTrue ? resolveDynamicValue(ifTrue, this.em) : resolveDynamicValue(ifFalse, this.em); + return isConditionTrue ? valueOrResolve(ifTrue, options) : valueOrResolve(ifFalse, options); } resolvesFromCollection() { @@ -115,6 +128,9 @@ export class DataCondition extends Model { updateCollectionsStateMap(collectionsStateMap: DataCollectionStateMap) { this.collectionsStateMap = collectionsStateMap; + this._conditionEvaluator.updateCollectionStateMap(collectionsStateMap); + this.listenToDataVariables(); + this.emitConditionEvaluationChange(); } private listenToPropsChange() { diff --git a/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts b/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts index a51b666a7..9e4a3f234 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts @@ -1,15 +1,17 @@ import { DataVariableProps } from '../DataVariable'; import EditorModel from '../../../editor/model/Editor'; -import { resolveDynamicValue, isDataVariable } from '../../utils'; +import { valueOrResolve, isDataVariable, getDataResolverInstanceValue } from '../../utils'; import { ExpressionProps, LogicGroupProps } from './DataCondition'; import { LogicalGroupEvaluator } from './LogicalGroupEvaluator'; -import { Operator } from './operators/BaseOperator'; +import { SimpleOperator } from './operators/BaseOperator'; import { AnyTypeOperation, AnyTypeOperator } from './operators/AnyTypeOperator'; import { BooleanOperator } from './operators/BooleanOperator'; import { NumberOperator, NumberOperation } from './operators/NumberOperator'; import { StringOperator, StringOperation } from './operators/StringOperator'; import { Model } from '../../../common'; -import { DataConditionOperation } from './operators/types'; +import { DataConditionSimpleOperation } from './types'; +import { isBoolean } from 'underscore'; +import { DataCollectionStateMap } from '../data_collection/types'; export type ConditionProps = ExpressionProps | LogicGroupProps | boolean; @@ -17,45 +19,77 @@ interface DataConditionEvaluatorProps { condition: ConditionProps; } +interface DataConditionEvaluatorOptions { + em: EditorModel; + collectionsStateMap?: DataCollectionStateMap; +} + export class DataConditionEvaluator extends Model { private em: EditorModel; + private collectionsStateMap: DataCollectionStateMap = {}; - constructor(props: DataConditionEvaluatorProps, opts: { em: EditorModel }) { + constructor(props: DataConditionEvaluatorProps, opts: DataConditionEvaluatorOptions) { super(props); this.em = opts.em; + this.collectionsStateMap = opts.collectionsStateMap ?? {}; } evaluate(): boolean { - const em = this.em; const condition = this.get('condition'); - if (typeof condition === 'boolean') return condition; + if (!condition || isBoolean(condition)) return !!condition; + + const resolvedOperator = this.getOperator(); + if (!resolvedOperator) return false; + return resolvedOperator.evaluate(this.getResolvedLeftValue(), this.getResolvedRightValue()); + } + + getDependentDataVariables(): DataVariableProps[] { + const condition = this.get('condition'); + if (!condition) return []; + + return this.extractDataVariables(condition); + } + + getOperations() { + const operator = this.getOperator(); + if (!operator || operator instanceof LogicalGroupEvaluator) return []; + + return operator.getOperations(); + } + + updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { + this.collectionsStateMap = collectionsStateMap; + } + + private getOperator() { + const opts = { em: this.em, collectionsStateMap: this.collectionsStateMap }; + const condition = this.get('condition'); + if (!condition || isBoolean(condition)) return; + let resolvedOperator: SimpleOperator | LogicalGroupEvaluator | undefined; if (this.isLogicGroup(condition)) { const { logicalOperator, statements } = condition; - const operator = new BooleanOperator(logicalOperator, { em }); - const logicalGroup = new LogicalGroupEvaluator(operator, statements, { em }); - return logicalGroup.evaluate(); + const operator = new BooleanOperator(logicalOperator, opts); + resolvedOperator = new LogicalGroupEvaluator(operator, statements, opts); } if (this.isExpression(condition)) { - const { left, operator, right } = condition; - const evaluateLeft = resolveDynamicValue(left, this.em); - const evaluateRight = resolveDynamicValue(right, this.em); - const op = this.getOperator(evaluateLeft, operator); - if (!op) return false; - - const evaluated = op.evaluate(evaluateLeft, evaluateRight); - return evaluated; + const { left, operator } = condition; + const evaluatedLeft = valueOrResolve(left, opts); + + resolvedOperator = this.resolveOperator(evaluatedLeft, operator); } - this.em.logError('Invalid condition type.'); - return false; + return resolvedOperator; } /** * Factory method for creating operators based on the data type. */ - private getOperator(left: any, operator: string | undefined): Operator | undefined { + private resolveOperator( + left: any, + operator: string | undefined, + ): SimpleOperator | undefined { const em = this.em; if (this.isOperatorInEnum(operator, AnyTypeOperation)) { @@ -66,17 +100,9 @@ export class DataConditionEvaluator extends Model { return new StringOperator(operator as StringOperation, { em }); } - this.em?.logError(`Unsupported data type: ${typeof left}`); return; } - getDependentDataVariables(): DataVariableProps[] { - const condition = this.get('condition'); - if (!condition) return []; - - return this.extractDataVariables(condition); - } - private extractDataVariables(condition: ConditionProps): DataVariableProps[] { const variables: DataVariableProps[] = []; @@ -102,6 +128,30 @@ export class DataConditionEvaluator extends Model { return Object.values(enumObject).includes(operator); } + private resolveExpressionSide(property: 'left' | 'right'): any { + const condition = this.get('condition'); + const { em, collectionsStateMap } = this; + + if (!condition || typeof condition === 'boolean') { + return condition; + } + + if (condition && typeof condition === 'object' && property in condition) { + const value = (condition as ExpressionProps)[property]; + return valueOrResolve(value, { em, collectionsStateMap }); + } + + return undefined; + } + + private getResolvedLeftValue(): any { + return this.resolveExpressionSide('left'); + } + + private getResolvedRightValue(): any { + return this.resolveExpressionSide('right'); + } + toJSON(options?: any) { const condition = this.get('condition'); if (typeof condition === 'object') { diff --git a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts index 1a0800878..aa311ef3f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts @@ -1,23 +1,21 @@ import EditorModel from '../../../editor/model/Editor'; +import { DataCollectionStateMap } from '../data_collection/types'; import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator'; import { BooleanOperator } from './operators/BooleanOperator'; export class LogicalGroupEvaluator { - private em: EditorModel; - constructor( private operator: BooleanOperator, private statements: ConditionProps[], - opts: { em: EditorModel }, - ) { - this.em = opts.em; - } + private opts: { em: EditorModel; collectionsStateMap: DataCollectionStateMap }, + ) {} evaluate(): boolean { const results = this.statements.map((statement) => { - const condition = new DataConditionEvaluator({ condition: statement }, { em: this.em }); + const condition = new DataConditionEvaluator({ condition: statement }, this.opts); return condition.evaluate(); }); + return this.operator.evaluate(results); } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts index 58eacaea3..a3b07fdc0 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts @@ -1,5 +1,5 @@ import DataVariable from '../../DataVariable'; -import { Operator } from './BaseOperator'; +import { SimpleOperator } from './BaseOperator'; export enum AnyTypeOperation { equals = 'equals', @@ -16,9 +16,11 @@ export enum AnyTypeOperation { isDefaultValue = 'isDefaultValue', // For Datasource variables } -export class AnyTypeOperator extends Operator { +export class AnyTypeOperator extends SimpleOperator { + protected operationsEnum = AnyTypeOperation; + evaluate(left: any, right: any): boolean { - switch (this.operation) { + switch (this.operationString) { case 'equals': return left === right; case 'isTruthy': @@ -44,7 +46,7 @@ export class AnyTypeOperator extends Operator { case 'isDefaultValue': return left instanceof DataVariable && left.get('defaultValue') === right; default: - this.em?.logError(`Unsupported generic operation: ${this.operation}`); + this.em?.logWarning(`Unsupported generic operation: ${this.operationString}`); return false; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts index 4cde7626d..68ae9660c 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts @@ -1,14 +1,20 @@ import EditorModel from '../../../../editor/model/Editor'; -import { DataConditionOperation } from './types'; +import { enumToArray } from '../../../utils'; +import { DataConditionSimpleOperation } from '../types'; -export abstract class Operator { +export abstract class SimpleOperator { protected em: EditorModel; - protected operation: OperationType; + protected operationString: OperationType; + protected abstract operationsEnum: Record; - constructor(operation: any, opts: { em: EditorModel }) { - this.operation = operation; + constructor(operationString: any, opts: { em: EditorModel }) { + this.operationString = operationString; this.em = opts.em; } abstract evaluate(left: any, right: any): boolean; + + getOperations(): DataConditionSimpleOperation[] { + return enumToArray(this.operationsEnum); + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts index e06b95a7e..413495694 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts @@ -1,4 +1,6 @@ -import { Operator } from './BaseOperator'; +import { enumToArray } from '../../../utils'; +import { DataConditionSimpleOperation } from '../types'; +import { SimpleOperator } from './BaseOperator'; export enum BooleanOperation { and = 'and', @@ -6,11 +8,13 @@ export enum BooleanOperation { xor = 'xor', } -export class BooleanOperator extends Operator { +export class BooleanOperator extends SimpleOperator { + protected operationsEnum = BooleanOperation; + evaluate(statements: boolean[]): boolean { if (!statements?.length) return false; - switch (this.operation) { + switch (this.operationString) { case BooleanOperation.and: return statements.every(Boolean); case BooleanOperation.or: @@ -18,7 +22,7 @@ export class BooleanOperator extends Operator { case BooleanOperation.xor: return statements.filter(Boolean).length === 1; default: - this.em.logError(`Unsupported boolean operation: ${this.operation}`); + this.em.logWarning(`Unsupported boolean operation: ${this.operationString}`); return false; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts index 52fc256af..04f4da498 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -1,4 +1,5 @@ -import { Operator } from './BaseOperator'; +import { enumToArray } from '../../../utils'; +import { SimpleOperator } from './BaseOperator'; export enum NumberOperation { greaterThan = '>', @@ -9,9 +10,13 @@ export enum NumberOperation { notEquals = '!=', } -export class NumberOperator extends Operator { +export class NumberOperator extends SimpleOperator { + protected operationsEnum = NumberOperation; + evaluate(left: number, right: number): boolean { - switch (this.operation) { + if (typeof left !== 'number') return false; + + switch (this.operationString) { case NumberOperation.greaterThan: return left > right; case NumberOperation.lessThan: @@ -25,7 +30,7 @@ export class NumberOperator extends Operator { case NumberOperation.notEquals: return left !== right; default: - this.em.logError(`Unsupported number operation: ${this.operation}`); + this.em.logWarning(`Unsupported number operation: ${this.operationString}`); return false; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts index 7dead9629..eb5279867 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts @@ -1,4 +1,4 @@ -import { Operator } from './BaseOperator'; +import { SimpleOperator } from './BaseOperator'; export enum StringOperation { contains = 'contains', @@ -9,9 +9,13 @@ export enum StringOperation { trimEquals = 'trimEquals', } -export class StringOperator extends Operator { +export class StringOperator extends SimpleOperator { + protected operationsEnum = StringOperation; + evaluate(left: string, right: string) { - switch (this.operation) { + if (typeof left !== 'string') return false; + + switch (this.operationString) { case StringOperation.contains: return left.includes(right); case StringOperation.startsWith: @@ -19,14 +23,14 @@ export class StringOperator extends Operator { case StringOperation.endsWith: return left.endsWith(right); case StringOperation.matchesRegex: - if (!right) this.em.logError('Regex pattern must be provided.'); - return new RegExp(right).test(left); + if (!right) this.em.logWarning('Regex pattern must be provided.'); + return new RegExp(right ?? '').test(left); case StringOperation.equalsIgnoreCase: return left.toLowerCase() === right.toLowerCase(); case StringOperation.trimEquals: return left.trim() === right.trim(); default: - this.em.logError(`Unsupported string operation: ${this.operation}`); + this.em.logWarning(`Unsupported string operation: ${this.operationString}`); return false; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/types.ts b/packages/core/src/data_sources/model/conditional_variables/operators/types.ts deleted file mode 100644 index 483ca91bc..000000000 --- a/packages/core/src/data_sources/model/conditional_variables/operators/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AnyTypeOperation } from './AnyTypeOperator'; -import { BooleanOperation } from './BooleanOperator'; -import { NumberOperation } from './NumberOperator'; -import { StringOperation } from './StringOperator'; - -export type DataConditionOperation = AnyTypeOperation | StringOperation | NumberOperation | BooleanOperation; diff --git a/packages/core/src/data_sources/model/conditional_variables/types.ts b/packages/core/src/data_sources/model/conditional_variables/types.ts new file mode 100644 index 000000000..346a76c67 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/types.ts @@ -0,0 +1,7 @@ +import { AnyTypeOperation } from './operators/AnyTypeOperator'; +import { BooleanOperation } from './operators/BooleanOperator'; +import { NumberOperation } from './operators/NumberOperator'; +import { StringOperation } from './operators/StringOperator'; + +export type DataConditionSimpleOperation = AnyTypeOperation | StringOperation | NumberOperation | BooleanOperation; +export type DataConditionCompositeOperation = DataConditionSimpleOperation; diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 6a893768d..ab0a535e6 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -1,4 +1,4 @@ -import { bindAll, isArray } from 'underscore'; +import { isArray } from 'underscore'; import { ObjectAny } from '../../../common'; import Component, { keySymbol } from '../../../dom_components/model/Component'; import { ComponentAddType, ComponentDefinitionDefined, ComponentOptions } from '../../../dom_components/model/types'; @@ -8,23 +8,17 @@ import DataResolverListener from '../DataResolverListener'; import DataSource from '../DataSource'; import DataVariable, { DataVariableProps, DataVariableType } from '../DataVariable'; import { isDataVariable } from '../../utils'; -import { - DataCollectionItemType, - DataCollectionType, - keyCollectionDefinition, - keyCollectionsStateMap, - keyIsCollectionItem, -} from './constants'; +import { DataCollectionItemType, DataCollectionType, keyCollectionDefinition } from './constants'; import { ComponentDataCollectionProps, DataCollectionDataSource, DataCollectionProps, - DataCollectionState, DataCollectionStateMap, } from './types'; -import { getSymbolsToUpdate } from '../../../dom_components/model/SymbolUtils'; -import { StyleProps, UpdateStyleOptions } from '../../../domain_abstract/model/StyleableModel'; +import { detachSymbolInstance, getSymbolInstances } from '../../../dom_components/model/SymbolUtils'; import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers'; +import { ModelDestroyOptions } from 'backbone'; +import Components from '../../../dom_components/model/Components'; const AvoidStoreOptions = { avoidStore: true, partial: true }; export default class ComponentDataCollection extends Component { @@ -59,14 +53,17 @@ export default class ComponentDataCollection extends Component { return cmp; } - bindAll(this, 'rebuildChildrenFromCollection'); - this.listenTo(this, `change:${keyCollectionDefinition}`, this.rebuildChildrenFromCollection); + this.rebuildChildrenFromCollection = this.rebuildChildrenFromCollection.bind(this); + this.listenToPropsChange(); this.rebuildChildrenFromCollection(); - this.listenToDataSource(); return cmp; } + getDataResolver() { + return this.get('dataResolver'); + } + getItemsCount() { const items = this.getDataSourceItems(); const startIndex = Math.max(0, this.getConfigStartIndex() ?? 0); @@ -97,6 +94,10 @@ export default class ComponentDataCollection extends Component { return this.firstChild.components(); } + setDataResolver(props: DataCollectionProps) { + return this.set('dataResolver', props); + } + setCollectionId(collectionId: string) { this.updateCollectionConfig({ collectionId }); } @@ -114,13 +115,6 @@ export default class ComponentDataCollection extends Component { this.updateCollectionConfig({ endIndex }); } - private updateCollectionConfig(updates: Partial): void { - this.set(keyCollectionDefinition, { - ...this.dataResolver, - ...updates, - }); - } - setDataSource(dataSource: DataCollectionDataSource) { this.set(keyCollectionDefinition, { ...this.dataResolver, @@ -136,12 +130,15 @@ export default class ComponentDataCollection extends Component { return this.components().at(0); } - private getDataSourceItems() { - return getDataSourceItems(this.dataResolver.dataSource, this.em); + private updateCollectionConfig(updates: Partial): void { + this.set(keyCollectionDefinition, { + ...this.dataResolver, + ...updates, + }); } - private getCollectionStateMap() { - return (this.get(keyCollectionsStateMap) || {}) as DataCollectionStateMap; + private getDataSourceItems() { + return getDataSourceItems(this.dataResolver.dataSource, this.em); } private get dataResolver() { @@ -160,7 +157,7 @@ export default class ComponentDataCollection extends Component { em, resolver: new DataVariable( { type: DataVariableType, path }, - { em, collectionsStateMap: this.get(keyCollectionsStateMap) }, + { em, collectionsStateMap: this.collectionsStateMap }, ), onUpdate: this.rebuildChildrenFromCollection, }); @@ -172,6 +169,7 @@ export default class ComponentDataCollection extends Component { private getCollectionItems() { const firstChild = this.ensureFirstChild(); + const initialDisplayValue = firstChild.getStyle()['display'] ?? ''; // TODO: Move to component view firstChild.addStyle({ display: 'none' }, AvoidStoreOptions); const components: Component[] = [firstChild]; @@ -181,15 +179,12 @@ export default class ComponentDataCollection extends Component { return components; } - const collectionId = this.dataResolver.collectionId; + const collectionId = this.collectionId; const items = this.getDataSourceItems(); + const { startIndex, endIndex } = this.resolveCollectionConfig(items); - const startIndex = this.getConfigStartIndex() ?? 0; - const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE; - const endIndex = Math.min(items.length - 1, configEndIndex); - const totalItems = endIndex - startIndex + 1; - const parentCollectionStateMap = this.getCollectionStateMap(); - if (parentCollectionStateMap[collectionId]) { + const isDuplicatedId = this.hasDuplicateCollectionId(); + if (isDuplicatedId) { this.em.logError( `The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`, ); @@ -198,40 +193,69 @@ export default class ComponentDataCollection extends Component { } for (let index = startIndex; index <= endIndex; index++) { - const item = items[index]; const isFirstItem = index === startIndex; - const collectionState: DataCollectionState = { - collectionId, - currentIndex: index, - currentItem: item, - startIndex: startIndex, - endIndex: endIndex, - totalItems: totalItems, - remainingItems: totalItems - (index + 1), - }; - - const collectionsStateMap: DataCollectionStateMap = { - ...parentCollectionStateMap, - [collectionId]: collectionState, - }; + const collectionsStateMap = this.getCollectionsStateMapForItem(items, index); if (isFirstItem) { - setCollectionStateMapAndPropagate(firstChild, collectionsStateMap, collectionId); + getSymbolInstances(firstChild)?.forEach((cmp) => detachSymbolInstance(cmp)); + + setCollectionStateMapAndPropagate(firstChild, collectionsStateMap); // TODO: Move to component view - firstChild.addStyle({ display: '' }, AvoidStoreOptions); + firstChild.addStyle({ display: initialDisplayValue }, AvoidStoreOptions); continue; } - const instance = firstChild!.clone({ symbol: true }); - instance.set('locked', true, AvoidStoreOptions); - setCollectionStateMapAndPropagate(instance, collectionsStateMap, collectionId); + const instance = firstChild!.clone({ symbol: true, symbolInv: true }); + instance.set({ locked: true, layerable: false }, AvoidStoreOptions); + setCollectionStateMapAndPropagate(instance, collectionsStateMap); components.push(instance); } return components; } + private getCollectionsStateMapForItem(items: DataVariableProps[], index: number) { + const { startIndex, endIndex, totalItems } = this.resolveCollectionConfig(items); + const collectionId = this.collectionId; + const item = items[index]; + const parentCollectionStateMap = this.collectionsStateMap; + + const offset = index - startIndex; + const remainingItems = totalItems - (1 + offset); + const collectionState = { + collectionId, + currentIndex: index, + currentItem: item, + startIndex, + endIndex, + totalItems, + remainingItems, + }; + + const collectionsStateMap: DataCollectionStateMap = { + ...parentCollectionStateMap, + [collectionId]: collectionState, + }; + + return collectionsStateMap; + } + + private hasDuplicateCollectionId() { + const collectionId = this.collectionId; + const parentCollectionStateMap = this.collectionsStateMap; + + return !!parentCollectionStateMap[collectionId]; + } + + private resolveCollectionConfig(items: DataVariableProps[]) { + const startIndex = this.getConfigStartIndex() ?? 0; + const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE; + const endIndex = Math.min(items.length - 1, configEndIndex); + const totalItems = endIndex - startIndex + 1; + return { startIndex, endIndex, totalItems }; + } + private ensureFirstChild() { const dataConditionItemModel = this.em.Components.getType(DataCollectionItemType)!.model; @@ -246,6 +270,51 @@ export default class ComponentDataCollection extends Component { ); } + private listenToPropsChange() { + this.on(`change:${keyCollectionDefinition}`, () => { + this.rebuildChildrenFromCollection(); + this.listenToDataSource(); + }); + this.listenToDataSource(); + } + + private removePropsListeners() { + this.off(`change:${keyCollectionDefinition}`); + this.dataSourceWatcher?.destroy(); + } + + onCollectionsStateMapUpdate(collectionsStateMap: DataCollectionStateMap) { + this.collectionsStateMap = collectionsStateMap; + this.dataResolverWatchers.onCollectionsStateMapUpdate(); + + const items = this.getDataSourceItems(); + const { startIndex } = this.resolveCollectionConfig(items); + const cmps = this.components(); + cmps.forEach((cmp, index) => { + const collectionsStateMap = this.getCollectionsStateMapForItem(items, startIndex + index); + cmp.onCollectionsStateMapUpdate(collectionsStateMap); + }); + } + + stopSyncComponentCollectionState() { + this.stopListening(this.components(), 'add remove reset', this.syncOnComponentChange); + this.onCollectionsStateMapUpdate({}); + } + + syncOnComponentChange(model: Component, collection: Components, opts: any) { + const collectionsStateMap = this.collectionsStateMap; + // Avoid assigning wrong collectionsStateMap value to children components + this.collectionsStateMap = {}; + + super.syncOnComponentChange(model, collection, opts); + this.collectionsStateMap = collectionsStateMap; + this.onCollectionsStateMapUpdate(collectionsStateMap); + } + + private get collectionId() { + return this.getDataResolver().collectionId as string; + } + static isComponent(el: HTMLElement) { return toLowerCase(el.tagName) === DataCollectionType; } @@ -259,66 +328,17 @@ export default class ComponentDataCollection extends Component { const firstChild = this.firstChild as any; return { ...json, components: [firstChild] }; } -} - -function applyToComponentAndChildren(operation: (cmp: Component) => void, component: Component) { - operation(component); - component.components().forEach((child) => { - applyToComponentAndChildren(operation, child); - }); -} - -function setCollectionStateMapAndPropagate( - cmp: Component, - collectionsStateMap: DataCollectionStateMap, - collectionId: string, -) { - applyToComponentAndChildren(() => { - setCollectionStateMap(collectionsStateMap)(cmp); - - const addListener = (component: Component) => { - setCollectionStateMapAndPropagate(component, collectionsStateMap, collectionId); - }; - - const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`; - const cmps = cmp.components(); - - 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); - } - - const collectionsStateMap = component.get(keyCollectionsStateMap); - component.set(keyCollectionsStateMap, { - ...collectionsStateMap, - [collectionId]: undefined, - }); - }; - - cmp.listenTo(cmps, 'remove', removeListener); - } - - cmps.forEach((cmp) => setCollectionStateMapAndPropagate(cmp, collectionsStateMap, collectionId)); - - cmp.on(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); - }, cmp); + destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { + this.removePropsListeners(); + return super.destroy(options); + } } -function handleCollectionStateMapChange(this: Component) { - const updatedCollectionsStateMap = this.get(keyCollectionsStateMap); - this.components() - ?.toArray() - .forEach((component: Component) => { - setCollectionStateMap(updatedCollectionsStateMap)(component); - }); +function setCollectionStateMapAndPropagate(cmp: Component, collectionsStateMap: DataCollectionStateMap) { + cmp.setSymbolOverride(['locked', 'layerable']); + cmp.syncComponentsCollectionState(); + cmp.onCollectionsStateMapUpdate(collectionsStateMap); } function logErrorIfMissing(property: any, propertyPath: string, em: EditorModel) { @@ -350,40 +370,6 @@ function validateCollectionDef(dataResolver: DataCollectionProps, em: EditorMode 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); - - const parentCollectionsId = Object.keys(updatedCollectionStateMap); - const isFirstItem = parentCollectionsId.every( - (key) => updatedCollectionStateMap[key].currentIndex === updatedCollectionStateMap[key].startIndex, - ); - - if (isFirstItem) { - const __onStyleChange = cmp.__onStyleChange.bind(cmp); - - cmp.__onStyleChange = (newStyles: StyleProps, opts: UpdateStyleOptions = {}) => { - __onStyleChange(newStyles); - const cmps = getSymbolsToUpdate(cmp); - - cmps.forEach((cmp) => { - cmp.addStyle(newStyles, opts); - }); - }; - - cmp.on(`change:${keyIsCollectionItem}`, () => { - cmp.__onStyleChange = __onStyleChange; - }); - } - }; -} - function getDataSourceItems(dataSource: DataCollectionDataSource, em: EditorModel) { let items: DataVariableProps[] = []; diff --git a/packages/core/src/data_sources/model/data_collection/constants.ts b/packages/core/src/data_sources/model/data_collection/constants.ts index 5ef251ea8..ba009085d 100644 --- a/packages/core/src/data_sources/model/data_collection/constants.ts +++ b/packages/core/src/data_sources/model/data_collection/constants.ts @@ -1,5 +1,3 @@ export const DataCollectionType = 'data-collection'; export const DataCollectionItemType = 'data-collection-item'; export const keyCollectionDefinition = 'dataResolver'; -export const keyIsCollectionItem = '__is_data_collection_item'; -export const keyCollectionsStateMap = '__collections_state_map'; diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index 7dad137a8..ad88c6409 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -5,8 +5,12 @@ import DataVariable, { DataVariableProps } from './model/DataVariable'; import { DataConditionProps, DataCondition } from './model/conditional_variables/DataCondition'; export type DataResolver = DataVariable | DataCondition; - export type DataResolverProps = DataVariableProps | DataConditionProps; +export type ResolverFromProps = T extends DataVariableProps + ? DataVariable + : T extends DataConditionProps + ? DataCondition + : never; export interface DataRecordProps extends ObjectAny { /** diff --git a/packages/core/src/data_sources/utils.ts b/packages/core/src/data_sources/utils.ts index 101a041da..598dc36aa 100644 --- a/packages/core/src/data_sources/utils.ts +++ b/packages/core/src/data_sources/utils.ts @@ -1,13 +1,14 @@ import EditorModel from '../editor/model/Editor'; -import { DataResolver, DataResolverProps } from './types'; +import { DataResolver, DataResolverProps, ResolverFromProps } from './types'; import { DataCollectionStateMap } from './model/data_collection/types'; import { DataCollectionItemType } from './model/data_collection/constants'; import { DataConditionType, DataCondition } from './model/conditional_variables/DataCondition'; import DataVariable, { DataVariableProps, DataVariableType } from './model/DataVariable'; -import Component from '../dom_components/model/Component'; import { ComponentDefinition, ComponentOptions } from '../dom_components/model/types'; import { serialize } from '../utils/mixins'; import { DataConditionIfFalseType, DataConditionIfTrueType } from './model/conditional_variables/constants'; +import { getSymbolMain } from '../dom_components/model/SymbolUtils'; +import Component from '../dom_components/model/Component'; export function isDataResolverProps(value: any): value is DataResolverProps { return typeof value === 'object' && [DataVariableType, DataConditionType].includes(value?.type); @@ -25,16 +26,17 @@ export function isDataCondition(variable: any) { return variable?.type === DataConditionType; } -export function resolveDynamicValue(variable: any, em: EditorModel) { - return isDataResolverProps(variable) - ? getDataResolverInstanceValue(variable, { em, collectionsStateMap: {} }) - : variable; +export function valueOrResolve(variable: any, opts: { em: EditorModel; collectionsStateMap: DataCollectionStateMap }) { + if (!isDataResolverProps(variable)) return variable; + if (isDataVariable(variable)) DataVariable.resolveDataResolver(variable, opts); + + return getDataResolverInstanceValue(variable, opts); } export function getDataResolverInstance( resolverProps: DataResolverProps, options: { em: EditorModel; collectionsStateMap: DataCollectionStateMap }, -) { +): ResolverFromProps | undefined { const { type } = resolverProps; let resolver: DataResolver; @@ -47,7 +49,7 @@ export function getDataResolverInstance( break; } default: - options.em?.logError(`Unsupported dynamic type: ${type}`); + options.em?.logWarning(`Unsupported dynamic type: ${type}`); return; } @@ -83,3 +85,39 @@ export const ensureComponentInstance = ( export const isComponentDataOutputType = (type: string | undefined) => { return !!type && [DataCollectionItemType, DataConditionIfTrueType, DataConditionIfFalseType].includes(type); }; + +export function enumToArray(enumObj: any) { + return Object.keys(enumObj) + .filter((key) => isNaN(Number(key))) + .map((key) => enumObj[key]); +} + +function shouldSyncCollectionSymbol(component: Component): boolean { + const componentCollectionMap = component.collectionsStateMap; + if (!componentCollectionMap) return false; + + const parentCollectionIds = Object.keys(componentCollectionMap); + if (!parentCollectionIds.length) return false; + + const mainSymbolComponent = getSymbolMain(component); + + if (!mainSymbolComponent || mainSymbolComponent === component) return false; + + const mainSymbolCollectionMap = mainSymbolComponent.collectionsStateMap; + const mainSymbolParentIds = Object.keys(mainSymbolCollectionMap); + + const isSubsetOfOriginalCollections = mainSymbolParentIds.every((id) => parentCollectionIds.includes(id)); + + return isSubsetOfOriginalCollections; +} + +function getIdFromCollectionSymbol(component: Component): string { + const mainSymbolComponent = getSymbolMain(component); + return mainSymbolComponent ? mainSymbolComponent.getId() : ''; +} + +export function checkAndGetSyncableCollectionItemId(component: Component) { + const shouldSync = shouldSyncCollectionSymbol(component); + const itemId = shouldSync ? getIdFromCollectionSymbol(component) : null; + return { shouldSync, itemId }; +} diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index c9f0cbf26..8e04c5091 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -50,10 +50,12 @@ import { updateSymbolCls, updateSymbolComps, updateSymbolProps, + getSymbolsToUpdate, } from './SymbolUtils'; import { ComponentDataResolverWatchers } from './ComponentDataResolverWatchers'; import { DynamicWatchersOptions } from './ComponentResolverWatcher'; -import { keyIsCollectionItem, keyCollectionsStateMap } from '../../data_sources/model/data_collection/constants'; +import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; +import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils'; export interface IComponent extends ExtractMethods {} export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {} @@ -258,14 +260,12 @@ export default class Component extends StyleableModel { * @private * @ts-ignore */ collection!: Components; - collectionStateListeners: string[] = []; dataResolverWatchers: ComponentDataResolverWatchers; + collectionsStateMap: DataCollectionStateMap = {}; constructor(props: ComponentProperties = {}, opt: ComponentOptions) { - const dataResolverWatchers = new ComponentDataResolverWatchers(undefined, { - em: opt.em, - collectionsStateMap: props[keyCollectionsStateMap], - }); + const em = opt.em; + const dataResolverWatchers = new ComponentDataResolverWatchers(undefined, { em }); super(props, { ...opt, dataResolverWatchers, @@ -273,8 +273,7 @@ export default class Component extends StyleableModel { dataResolverWatchers.bindComponent(this); this.dataResolverWatchers = dataResolverWatchers; - bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps'); - const em = opt.em; + bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps', 'syncOnComponentChange'); // Propagate properties from parent if indicated const parent = this.parent(); @@ -369,6 +368,50 @@ export default class Component extends StyleableModel { return super.set(evaluatedProps, options); } + onCollectionsStateMapUpdate(collectionsStateMap: DataCollectionStateMap) { + this.collectionsStateMap = collectionsStateMap; + this.dataResolverWatchers.onCollectionsStateMapUpdate(); + + const cmps = this.components(); + cmps.forEach((cmp) => cmp.onCollectionsStateMapUpdate(collectionsStateMap)); + } + + syncComponentsCollectionState() { + this.stopListening(this.components(), 'add remove reset', this.syncOnComponentChange); + this.listenTo(this.components(), 'add remove reset', this.syncOnComponentChange); + this.components().forEach((cmp) => cmp.syncComponentsCollectionState()); + } + + stopSyncComponentCollectionState() { + this.stopListening(this.components(), 'add remove reset', this.syncOnComponentChange); + this.collectionsStateMap = {}; + this.components().forEach((cmp) => cmp.stopSyncComponentCollectionState()); + } + + syncOnComponentChange(model: Component, collection: Components, opts: any) { + if (!this.collectionsStateMap || !Object.keys(this.collectionsStateMap).length) return; + const options = opts || collection || {}; + + // Reset (in reset, 'model' is Collection, 'collection' is opts ) + if (!opts) { + const modelsRemoved = options.previousModels || []; + modelsRemoved.forEach((cmp: Component) => cmp.stopSyncComponentCollectionState()); + this.components().forEach((cmp) => { + cmp.syncComponentsCollectionState(); + cmp.onCollectionsStateMapUpdate(this.collectionsStateMap); + }); + } else if (options.add) { + // Add + const modelAdded = model; + modelAdded.syncComponentsCollectionState(); + modelAdded.onCollectionsStateMapUpdate(this.collectionsStateMap); + } else { + // Remove + const modelRemoved = model; + modelRemoved.stopSyncComponentCollectionState(); + } + } + __postAdd(opts: { recursive?: boolean } = {}) { const { em } = this; const um = em?.UndoManager; @@ -412,6 +455,29 @@ export default class Component extends StyleableModel { em.trigger(event, this, pros); styleKeys.forEach((key) => em.trigger(`${event}:${key}`, this, pros)); + + const collectionsStateMap = this.collectionsStateMap; + const allParentCollectionIds = Object.keys(collectionsStateMap); + if (!allParentCollectionIds.length) return; + + const isAtInitialPosition = allParentCollectionIds.every( + (key) => collectionsStateMap[key].currentIndex === collectionsStateMap[key].startIndex, + ); + if (!isAtInitialPosition) return; + + const componentsToUpdate = getSymbolsToUpdate(this); + componentsToUpdate.forEach((component) => { + const componentCollectionsState = component.collectionsStateMap; + const componentParentCollectionIds = Object.keys(componentCollectionsState); + + const isChildOfOriginalCollections = componentParentCollectionIds.every((id) => + allParentCollectionIds.includes(id), + ); + + if (isChildOfOriginalCollections) { + component.addStyle(newStyles); + } + }); } __changesUp(opts: any) { @@ -450,6 +516,8 @@ export default class Component extends StyleableModel { updateSymbolComps(this, m, c, o); } + __onDestroy() {} + /** * Check component's type * @param {string} type Component type @@ -1594,12 +1662,10 @@ export default class Component extends StyleableModel { delete obj.open; // used in Layers delete obj._undoexc; delete obj.delegate; - if (this.get(keyIsCollectionItem)) { + if (this.collectionsStateMap && Object.getOwnPropertyNames(this.collectionsStateMap).length > 0) { delete obj[keySymbol]; delete obj[keySymbolOvrd]; delete obj[keySymbols]; - delete obj[keyCollectionsStateMap]; - delete obj[keyIsCollectionItem]; delete obj.attributes.id; } @@ -1659,6 +1725,10 @@ export default class Component extends StyleableModel { */ getId(): string { let attrs = this.get('attributes') || {}; + const { shouldSync, itemId } = checkAndGetSyncableCollectionItemId(this); + if (shouldSync) { + attrs.id = itemId; + } return attrs.id || this.ccid || this.cid; } @@ -1831,7 +1901,9 @@ export default class Component extends StyleableModel { } destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { + this.stopListening(this.components(), 'add remove reset', this.syncOnComponentChange); this.dataResolverWatchers.destroy(); + this.__onDestroy(); return super.destroy(options); } diff --git a/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts b/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts index 721453bc9..e6ccc0993 100644 --- a/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts +++ b/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts @@ -1,6 +1,4 @@ import { ObjectAny } from '../../common'; -import { keyCollectionsStateMap, keyIsCollectionItem } from '../../data_sources/model/data_collection/constants'; -import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import Component from './Component'; import { ComponentResolverWatcher, @@ -38,11 +36,6 @@ export class ComponentDataResolverWatchers { this.updateSymbolOverride(); } - updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { - this.propertyWatcher.updateCollectionStateMap(collectionsStateMap); - this.attributeWatcher.updateCollectionStateMap(collectionsStateMap); - } - addProps(props: ObjectAny, options: DynamicWatchersOptions = {}) { const excludedFromEvaluation = ['components']; @@ -73,12 +66,13 @@ export class ComponentDataResolverWatchers { } private updateSymbolOverride() { - if (!this.component || !this.component.get(keyIsCollectionItem)) return; + const isCollectionItem = !!Object.keys(this.component?.collectionsStateMap ?? {}).length; + if (!this.component || !isCollectionItem) return; const keys = this.propertyWatcher.getValuesResolvingFromCollections(); const attributesKeys = this.attributeWatcher.getValuesResolvingFromCollections(); - const combinedKeys = [keyCollectionsStateMap, 'locked', ...keys]; + const combinedKeys = ['locked', 'layerable', ...keys]; const haveOverridenAttributes = Object.keys(attributesKeys).length; if (haveOverridenAttributes) combinedKeys.push('attributes'); @@ -89,6 +83,11 @@ export class ComponentDataResolverWatchers { this.component.setSymbolOverride(combinedKeys, { fromDataSource: true }); } + onCollectionsStateMapUpdate() { + this.propertyWatcher.onCollectionsStateMapUpdate(); + this.attributeWatcher.onCollectionsStateMapUpdate(); + } + getDynamicPropsDefs() { return this.propertyWatcher.getAllSerializableValues(); } diff --git a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts b/packages/core/src/dom_components/model/ComponentResolverWatcher.ts index 05f67ac66..fd26d3270 100644 --- a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts +++ b/packages/core/src/dom_components/model/ComponentResolverWatcher.ts @@ -1,9 +1,7 @@ import { ObjectAny } from '../../common'; -import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import DataResolverListener from '../../data_sources/model/DataResolverListener'; import { getDataResolverInstance, getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/utils'; import EditorModel from '../../editor/model/Editor'; -import { DataResolverProps } from '../../data_sources/types'; import Component from './Component'; export interface DynamicWatchersOptions { @@ -13,14 +11,12 @@ export interface DynamicWatchersOptions { 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( @@ -29,30 +25,12 @@ export class ComponentResolverWatcher { 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.getValuesResolvingFromCollections(); - const collectionVariablesObject: { [key: string]: DataResolverProps | null } = {}; - collectionVariablesKeys.forEach((key) => { - this.resolverListeners[key].resolver.updateCollectionsStateMap(collectionsStateMap); - collectionVariablesObject[key] = null; - }); - 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) { @@ -74,6 +52,27 @@ export class ComponentResolverWatcher { return evaluatedValues; } + onCollectionsStateMapUpdate() { + const resolvesFromCollections = this.getValuesResolvingFromCollections(); + if (!resolvesFromCollections.length) return; + resolvesFromCollections.forEach((key) => + this.resolverListeners[key].resolver.updateCollectionsStateMap(this.collectionsStateMap), + ); + + const evaluatedValues = this.addDynamicValues( + this.getSerializableValues(Object.fromEntries(resolvesFromCollections.map((key) => [key, null]))), + ); + + Object.entries(evaluatedValues).forEach(([key, value]) => this.updateFn(this.component, key, value)); + } + + private get collectionsStateMap() { + const component = this.component; + if (!component) return {}; + + return component.collectionsStateMap; + } + private updateListeners(values: { [key: string]: any }) { const { em, collectionsStateMap } = this; this.removeListeners(Object.keys(values)); diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 03e148d6c..fcf5fa952 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -17,7 +17,6 @@ import { isDataResolverProps, } from '../../data_sources/utils'; import { DataResolver } from '../../data_sources/types'; -import { keyCollectionsStateMap } from '../../data_sources/model/data_collection/constants'; export type StyleProps = Record; @@ -115,7 +114,7 @@ export default class StyleableModel extends Model if (isDataResolverProps(styleValue)) { const dataResolver = getDataResolverInstance(styleValue, { em: this.em!, - collectionsStateMap: this.get(keyCollectionsStateMap) ?? {}, + collectionsStateMap: {}, }); if (dataResolver) { @@ -193,7 +192,7 @@ export default class StyleableModel extends Model if (isDataResolverProps(styleValue)) { resultStyle[key] = getDataResolverInstanceValue(styleValue, { em: this.em!, - collectionsStateMap: this.get(keyCollectionsStateMap) ?? {}, + collectionsStateMap: {}, }); } 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 index 9be911ba3..ffc214d74 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts @@ -625,8 +625,13 @@ describe('Collection component', () => { type: DataCollectionType, components: [ { - ...childCmpDef, - components: [childCmpDef, childCmpDef], + type: DataCollectionItemType, + components: [ + { + ...childCmpDef, + components: [childCmpDef, childCmpDef], + }, + ], }, ], dataResolver: { @@ -656,7 +661,7 @@ describe('Collection component', () => { path: 'user', }, }; - firstItemCmp.components(newChildDefinition); + firstItemCmp.components().at(0).components(newChildDefinition); expect(cmp.toJSON()).toMatchSnapshot(`Collection with grandchildren`); }); diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts index 774027f5f..764667c76 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts @@ -1,6 +1,9 @@ import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { DataCollectionType } from '../../../../../src/data_sources/model/data_collection/constants'; +import { + DataCollectionItemType, + DataCollectionType, +} from '../../../../../src/data_sources/model/data_collection/constants'; import { ComponentDataCollectionProps, DataCollectionStateType, @@ -123,7 +126,7 @@ describe('Collection variable components', () => { const collectionCmpDef = { type: DataCollectionType, components: { - type: 'default', + type: DataCollectionItemType, components: [ { type: 'default', 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 index 4ff379359..6af45f11c 100644 --- 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 @@ -4,20 +4,6 @@ exports[`Collection component Serialization Saving: Collection with grandchildre { "components": [ { - "attributes": { - "attribute_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - }, "components": [ { "attributes": { @@ -65,61 +51,8 @@ exports[`Collection component Serialization Saving: Collection with grandchildre }, "type": "default", }, - { - "attributes": { - "attribute_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - }, - "custom_prop": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentIndex", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "property_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "type": "default", - }, ], - "custom_prop": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentIndex", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "property_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "type": "default", + "type": "data-collection-item", }, ], "dataResolver": { @@ -139,20 +72,6 @@ exports[`Collection component Serialization Saving: Collection with no grandchil { "components": [ { - "attributes": { - "attribute_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - }, "components": [ { "attributes": { @@ -169,41 +88,78 @@ exports[`Collection component Serialization Saving: Collection with no grandchil "variableType": "currentItem", }, }, - "custom_prop": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentIndex", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "property_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "type": "default", - }, - { - "attributes": { - "attribute_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", + "components": [ + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "type": "default", }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "type": "default", }, - }, + ], "custom_prop": { "collectionId": "my_collection", "path": "user", @@ -225,25 +181,7 @@ exports[`Collection component Serialization Saving: Collection with no grandchil "type": "default", }, ], - "custom_prop": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentIndex", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "property_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "type": "default", + "type": "data-collection-item", }, ], "dataResolver": { @@ -263,20 +201,6 @@ exports[`Collection component Serialization Serializion with Collection Variable { "components": [ { - "attributes": { - "attribute_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - }, "components": [ { "attributes": { @@ -295,12 +219,84 @@ exports[`Collection component Serialization Serializion with Collection Variable }, "components": [ { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + }, + "components": [ + { + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentIndex", + }, + "type": "default", + }, + ], + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentIndex", + }, "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { "collectionId": "my_collection", "path": "user", "type": "data-variable", "variableType": "currentIndex", }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, "type": "default", }, ], @@ -324,61 +320,8 @@ exports[`Collection component Serialization Serializion with Collection Variable }, "type": "default", }, - { - "attributes": { - "attribute_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - }, - "custom_prop": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentIndex", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "property_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "type": "default", - }, ], - "custom_prop": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentIndex", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "property_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "type": "default", + "type": "data-collection-item", }, ], "dataResolver": { @@ -398,20 +341,6 @@ exports[`Collection component Serialization Serializion with Collection Variable { "components": [ { - "attributes": { - "attribute_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - }, "components": [ { "attributes": { @@ -428,41 +357,78 @@ exports[`Collection component Serialization Serializion with Collection Variable "variableType": "currentItem", }, }, - "custom_prop": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentIndex", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "property_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "type": "default", - }, - { - "attributes": { - "attribute_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", + "components": [ + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "type": "default", }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", + { + "attributes": { + "attribute_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + }, + "custom_prop": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentIndex", + }, + "name": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "property_trait": { + "collectionId": "my_collection", + "path": "user", + "type": "data-variable", + "variableType": "currentItem", + }, + "type": "default", }, - }, + ], "custom_prop": { "collectionId": "my_collection", "path": "user", @@ -484,25 +450,7 @@ exports[`Collection component Serialization Serializion with Collection Variable "type": "default", }, ], - "custom_prop": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentIndex", - }, - "name": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "property_trait": { - "collectionId": "my_collection", - "path": "user", - "type": "data-variable", - "variableType": "currentItem", - }, - "type": "default", + "type": "data-collection-item", }, ], "dataResolver": { diff --git a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap index a399b59df..27dc4a319 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap +++ b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap @@ -17,7 +17,7 @@ exports[`Collection variable components Serialization Saving: Collection with co "type": "data-variable", }, ], - "type": "default", + "type": "data-collection-item", }, ], "dataResolver": { @@ -60,7 +60,7 @@ exports[`Collection variable components Serialization Saving: Collection with co "type": "data-variable", }, ], - "type": "default", + "type": "data-collection-item", }, ], "dataResolver": { @@ -93,7 +93,7 @@ exports[`Collection variable components Serialization Serializion to JSON: Colle "type": "data-variable", }, ], - "type": "default", + "type": "data-collection-item", }, ], "dataResolver": { @@ -136,7 +136,7 @@ exports[`Collection variable components Serialization Serializion to JSON: Colle "type": "data-variable", }, ], - "type": "default", + "type": "data-collection-item", }, ], "dataResolver": {