Browse Source

Dynamic values improvements (#6495)

* Avoid throwing errors if no condition is passed to a DataCondition

* Refactor Condition class

* Remove expecting errors for conditions tests

* Extend and refactor BaseOperator

* Rename and refactor GenericOperator

* Rename and refactor logicalOperator to BooleanOperator

* Refactor StringOperator

* Refactor NumberOperator

* Refactor and fix DataResolverListener

* Update tests for condition Operators

* Rename Condition class to ConditionEvaluator

* Add missing types file

* Update and refactor DataCondition

* Update utils

* Refactor StyleableModel class

* Update ComponentDataCondition

* Refactor ComponentResolverWatcher

* Fix conditional styles

* Rename LogicalGroupStatement to LogicalGroupEvaluator

* Fix tests for DataCondition

* Add setter methods for component data variable

* Add setters and getter to ComponentDataCondition

* Add getters to ComponentDataVariable

* Rename test file

* Make dynamic components undroppable

* Fix collection types

* Update collections ( add setters and getter, first item editable, sync styles )

* Update data collection tests

* Format tests

* Fix some ComponentData collection bugs

* Refactor setStartIndex and setEndIndex

* Fix getComponentDef test

* Fix bug with end index = 0

* fix test for setDataSource

* fix test for HTML updates

* Format

* Add tests for the new option in getDataValue for conditions ( skipDynamicValueResolution )

* rename Operation to DataConditionOperation

* Run __onStyleChange after style changes as before

* Format

* Up BooleanOperator

* Avoid raising errors for wrong dynamic value types

* Refactor DataCondition

* Format

* Refactor how component data variable works internally

* Symbols fix

* Add docs

* Refactor componentDatacondition

* Add types for Datacondition output ( result ) types

* Refactor DataCondition

* Refactor ComponentDataCollection

* Refactor styleableModel

* Add missing files and utils

* Up tests for ComponentDataCondition

* Add new tests for component data condition

* Fix quality check

* Move ComponentDataVariable path and default value to dataResolver property

* Update component data condition structure

* Refactor data condition

* Fix ifTrue and ifFalse position in ComponentDataCondition

* Fix typescript

* Change setIfTrueContent to be setIfTrueComponents

* Fix memory leak for binding to this.postRender

* Cleanup ComponentDataConditionView

* Up tests

* Save ComponentDataVariable on dataResolverChange

* Remove DataCollection componentDef

* Add setCollectionId method

* Replace DataCollectionVariable with DataVariable

* cleanup

* Fix bug

* PR review

* Add new API endpoints

* Add getCollectionsStateMap to ComponentDataVariable

* Fix tests

* up DataSources

* Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into dynamic-values-improvements

* Push branch with refactored datasources

* Fix conditions nested inside collections

* Refactor how collectionsStateMap works

* Add DataVariable static methods

* Fix dataconditions nested inside collections

* format

* Remove collectionsStateMap on component drag

* Fix remainingItems state

* Make the first component item as the main symbol

* make layrable as false

* Update how datacollection components sync styles

---------

Co-authored-by: Artur Arseniev <artur.catch@hotmail.it>
pull/6498/head
mohamed yahia 11 months ago
committed by GitHub
parent
commit
0570b57cd4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 78
      packages/core/src/data_sources/model/ComponentDataVariable.ts
  2. 93
      packages/core/src/data_sources/model/ComponentWithDataResolver.ts
  3. 110
      packages/core/src/data_sources/model/DataVariable.ts
  4. 52
      packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts
  5. 6
      packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts
  6. 28
      packages/core/src/data_sources/model/conditional_variables/DataCondition.ts
  7. 106
      packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts
  8. 12
      packages/core/src/data_sources/model/conditional_variables/LogicalGroupEvaluator.ts
  9. 10
      packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts
  10. 16
      packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts
  11. 12
      packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts
  12. 13
      packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts
  13. 16
      packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts
  14. 6
      packages/core/src/data_sources/model/conditional_variables/operators/types.ts
  15. 7
      packages/core/src/data_sources/model/conditional_variables/types.ts
  16. 274
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  17. 2
      packages/core/src/data_sources/model/data_collection/constants.ts
  18. 6
      packages/core/src/data_sources/types.ts
  19. 54
      packages/core/src/data_sources/utils.ts
  20. 94
      packages/core/src/dom_components/model/Component.ts
  21. 17
      packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts
  22. 43
      packages/core/src/dom_components/model/ComponentResolverWatcher.ts
  23. 5
      packages/core/src/domain_abstract/model/StyleableModel.ts
  24. 11
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts
  25. 7
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts
  26. 484
      packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap
  27. 8
      packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap

78
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<DataVariableProps> {
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) {

93
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<T extends DataResolverProps> extends ComponentProperties {
type: T['type'];
dataResolver: T;
}
export abstract class ComponentWithDataResolver<T extends DataResolverProps> extends Component {
dataResolver: ResolverFromProps<T>;
constructor(props: ComponentWithDataResolverProps<T>, opt: ComponentOptions) {
super(props, opt);
this.dataResolver = this.initializeDataResolver(props, opt);
this.listenToPropsChange();
}
private initializeDataResolver(
props: ComponentWithDataResolverProps<T>,
opt: ComponentOptions,
): ResolverFromProps<T> {
const resolverProps = props.dataResolver ?? {
type: props.type,
};
const resolver = this.createResolverInstance(resolverProps, {
...opt,
collectionsStateMap: this.collectionsStateMap,
});
return resolver as ResolverFromProps<T>;
}
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,
};
}
}

110
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<DataVariableProps> {
private em: EditorModel;
private collectionsStateMap: DataCollectionStateMap;
@ -27,7 +32,7 @@ export default class DataVariable extends Model<DataVariableProps> {
};
}
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<DataVariableProps> {
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<DataVariableProps> {
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<DataVariableProps> {
const filteredJson = Object.fromEntries(
Object.entries(json).filter(([key, value]) => value !== defaults[key as keyof DataVariableProps]),
) as Partial<DataVariableProps>;
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<DataVariableProps> {
}
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 '';
}

52
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<DataConditionProps> {
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;
}

6
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<DataConditionOperation>,
private operator: SimpleOperator<DataConditionSimpleOperation>,
private rightValue: any,
) {}

28
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<DataConditionProps> {
private em: EditorModel;
private collectionsStateMap: DataCollectionStateMap = {};
@ -53,13 +58,14 @@ export class DataCondition extends Model<DataConditionProps> {
};
}
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<DataConditionProps> {
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<DataConditionProps> {
}
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<DataConditionProps> {
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<DataConditionProps> {
updateCollectionsStateMap(collectionsStateMap: DataCollectionStateMap) {
this.collectionsStateMap = collectionsStateMap;
this._conditionEvaluator.updateCollectionStateMap(collectionsStateMap);
this.listenToDataVariables();
this.emitConditionEvaluationChange();
}
private listenToPropsChange() {

106
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<DataConditionEvaluatorProps> {
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<DataConditionSimpleOperation> | 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<DataConditionOperation> | undefined {
private resolveOperator(
left: any,
operator: string | undefined,
): SimpleOperator<DataConditionSimpleOperation> | undefined {
const em = this.em;
if (this.isOperatorInEnum(operator, AnyTypeOperation)) {
@ -66,17 +100,9 @@ export class DataConditionEvaluator extends Model<DataConditionEvaluatorProps> {
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<DataConditionEvaluatorProps> {
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') {

12
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);
}
}

10
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<AnyTypeOperation> {
export class AnyTypeOperator extends SimpleOperator<AnyTypeOperation> {
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<AnyTypeOperation> {
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;
}
}

16
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<OperationType extends DataConditionOperation> {
export abstract class SimpleOperator<OperationType extends DataConditionSimpleOperation> {
protected em: EditorModel;
protected operation: OperationType;
protected operationString: OperationType;
protected abstract operationsEnum: Record<string, OperationType>;
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);
}
}

12
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<BooleanOperation> {
export class BooleanOperator extends SimpleOperator<BooleanOperation> {
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<BooleanOperation> {
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;
}
}

13
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<NumberOperation> {
export class NumberOperator extends SimpleOperator<NumberOperation> {
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<NumberOperation> {
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;
}
}

16
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<StringOperation> {
export class StringOperator extends SimpleOperator<StringOperation> {
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<StringOperation> {
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;
}
}

6
packages/core/src/data_sources/model/conditional_variables/operators/types.ts

@ -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;

7
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;

274
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<DataCollectionProps>): 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<DataCollectionProps>): 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[] = [];

2
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';

6
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 DataResolverProps> = T extends DataVariableProps
? DataVariable
: T extends DataConditionProps
? DataCondition
: never;
export interface DataRecordProps extends ObjectAny {
/**

54
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<typeof resolverProps> | 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 };
}

94
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<Component> {}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {}
@ -258,14 +260,12 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @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<ComponentProperties> {
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<ComponentProperties> {
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<ComponentProperties> {
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<ComponentProperties> {
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<ComponentProperties> {
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<ComponentProperties> {
*/
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<ComponentProperties> {
}
destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR {
this.stopListening(this.components(), 'add remove reset', this.syncOnComponentChange);
this.dataResolverWatchers.destroy();
this.__onDestroy();
return super.destroy(options);
}

17
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();
}

43
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<string, DataResolverListener> = {};
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));

5
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<string, string | string[] | DataVariableProps | DataConditionProps>;
@ -115,7 +114,7 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
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<T extends ObjectHash = any> extends Model<T>
if (isDataResolverProps(styleValue)) {
resultStyle[key] = getDataResolverInstanceValue(styleValue, {
em: this.em!,
collectionsStateMap: this.get(keyCollectionsStateMap) ?? {},
collectionsStateMap: {},
});
}

11
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`);
});

7
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',

484
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": {

8
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": {

Loading…
Cancel
Save