diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index ccd119aa90..a43ef0b631 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -6,6 +6,8 @@ import DataVariable, { DataVariableType } from './DataVariable'; import { DynamicValue } from '../types'; import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition'; import ComponentDataVariable from './ComponentDataVariable'; +import { CollectionVariableType } from './collection_component/constants'; +import CollectionVariable from './collection_component/CollectionVariable'; export interface DynamicVariableListenerManagerOptions { em: EditorModel; @@ -41,6 +43,13 @@ export default class DynamicVariableListenerManager { const type = dynamicVariable.get('type'); let dataListeners: DataVariableListener[] = []; switch (type) { + case CollectionVariableType: + const collectionVariable = dynamicVariable as CollectionVariable; + if (collectionVariable.hasDynamicValue()) { + dataListeners = this.listenToDataVariable(collectionVariable.dataVariable!, em); + } + + break; case DataVariableType: dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em); break; diff --git a/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts b/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts new file mode 100644 index 0000000000..cb62fea0d6 --- /dev/null +++ b/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts @@ -0,0 +1,201 @@ +import DataVariable, { DataVariableType } from './../DataVariable'; +import { isArray } from 'underscore'; +import Component, { keySymbol, keySymbolOvrd, keySymbols } from '../../../dom_components/model/Component'; +import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; +import { toLowerCase } from '../../../utils/mixins'; +import DataSource from '../DataSource'; +import { ObjectAny } from '../../../common'; +import EditorModel from '../../../editor/model/Editor'; +import { keyCollectionsStateMap } from '../../../dom_components/model/Component'; +import { CollectionComponentDefinition, CollectionDefinition, CollectionState, CollectionsStateMap } from './types'; +import { keyCollectionDefinition, keyInnerCollectionState, CollectionComponentType } from './constants'; +import DynamicVariableListenerManager from '../DataVariableListenerManager'; +import Components from '../../../dom_components/model/Components'; + +export default class CollectionComponent extends Component { + constructor(props: CollectionComponentDefinition, opt: ComponentOptions) { + const em = opt.em; + // @ts-ignore + const cmp: CollectionComponent = super( + // @ts-ignore + { + ...props, + components: undefined, + droppable: false, + }, + opt, + ); + + const collectionDefinition = props[keyCollectionDefinition]; + if (!collectionDefinition) { + em.logError('missing collection definition'); + + return cmp; + } + + const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as CollectionsStateMap; + + const components: Component[] = getCollectionItems(em, collectionDefinition, parentCollectionStateMap, opt); + + if (this.hasDynamicDataSource()) { + this.watchDataSource(em, collectionDefinition, parentCollectionStateMap, opt); + } + cmp.components(components); + + return cmp; + } + + static isComponent(el: HTMLElement) { + return toLowerCase(el.tagName) === CollectionComponentType; + } + + hasDynamicDataSource() { + const dataSource = this.get(keyCollectionDefinition).config.dataSource; + return typeof dataSource === 'object' && dataSource.type === DataVariableType; + } + + toJSON(opts?: ObjectAny) { + const json = super.toJSON(opts) as CollectionComponentDefinition; + + const firstChild = this.getBlockDefinition(); + json[keyCollectionDefinition].block = firstChild; + + delete json.components; + delete json.droppable; + return json; + } + + private getBlockDefinition() { + const firstChild = this.components().at(0)?.toJSON() || {}; + delete firstChild.draggable; + + return firstChild; + } + + private watchDataSource( + em: EditorModel, + collectionDefinition: CollectionDefinition, + parentCollectionStateMap: CollectionsStateMap, + opt: ComponentOptions, + ) { + const path = this.get(keyCollectionDefinition).config.dataSource?.path; + const dataVariable = new DataVariable( + { + type: DataVariableType, + path, + }, + { em }, + ); + new DynamicVariableListenerManager({ + em: em, + dataVariable, + updateValueFromDataVariable: () => { + const collectionItems = getCollectionItems(em, collectionDefinition, parentCollectionStateMap, opt); + this.components(collectionItems); + }, + }); + } +} + +function getCollectionItems( + em: EditorModel, + collectionDefinition: CollectionDefinition, + parentCollectionStateMap: CollectionsStateMap, + opt: ComponentOptions, +) { + const { collection_name, block, config } = collectionDefinition; + if (!block) { + em.logError('The "block" property is required in the collection definition.'); + return []; + } + + if (!config?.dataSource) { + em.logError('The "config.dataSource" property is required in the collection definition.'); + return []; + } + + const components: Component[] = []; + + let items: any[] = getDataSourceItems(config.dataSource, em); + const start_index = Math.max(0, config.start_index || 0); + const end_index = Math.min(items.length - 1, config.end_index !== undefined ? config.end_index : Number.MAX_VALUE); + + const total_items = end_index - start_index + 1; + let blockSymbolMain: Component; + for (let index = start_index; index <= end_index; index++) { + const item = items[index]; + const collectionState: CollectionState = { + collection_name, + current_index: index, + current_item: item, + start_index: start_index, + end_index: end_index, + total_items: total_items, + remaining_items: total_items - (index + 1), + }; + + const collectionsStateMap: CollectionsStateMap = { + ...parentCollectionStateMap, + ...(collection_name && { [collection_name]: collectionState }), + [keyInnerCollectionState]: collectionState, + }; + + if (index === start_index) { + // @ts-ignore + const type = em.Components.getType(block?.type || 'default'); + const model = type.model; + + blockSymbolMain = new model( + { + ...block, + [keyCollectionsStateMap]: collectionsStateMap, + isCollectionItem: true, + draggable: false, + }, + opt, + ); + blockSymbolMain!.setSymbolOverride([keyCollectionsStateMap]); + } + blockSymbolMain!.set(keyCollectionsStateMap, collectionsStateMap); + const instance = blockSymbolMain!.clone({ symbol: true }); + + components.push(instance); + } + + return components; +} + +function getDataSourceItems(dataSource: any, em: EditorModel) { + let items: any[] = []; + switch (true) { + case isArray(dataSource): + items = dataSource; + break; + case typeof dataSource === 'object' && dataSource instanceof DataSource: + const id = dataSource.get('id')!; + items = listDataSourceVariables(id, em); + break; + case typeof dataSource === 'object' && dataSource.type === DataVariableType: + const isDataSourceId = dataSource.path.split('.').length === 1; + if (isDataSourceId) { + const id = dataSource.path; + items = listDataSourceVariables(id, em); + } else { + // Path points to a record in the data source + items = em.DataSources.getValue(dataSource.path, []); + } + break; + default: + } + return items; +} + +function listDataSourceVariables(dataSource_id: string, em: EditorModel) { + const records = em.DataSources.getValue(dataSource_id, []); + const keys = Object.keys(records); + + return keys.map((key) => ({ + type: DataVariableType, + path: dataSource_id + '.' + key, + })); +} diff --git a/packages/core/src/data_sources/model/collection_component/CollectionComponentView.ts b/packages/core/src/data_sources/model/collection_component/CollectionComponentView.ts new file mode 100644 index 0000000000..19a8fdce69 --- /dev/null +++ b/packages/core/src/data_sources/model/collection_component/CollectionComponentView.ts @@ -0,0 +1,4 @@ +import ComponentView from '../../../dom_components/view/ComponentView'; +import CollectionComponent from './CollectionComponent'; + +export default class CollectionComponentView extends ComponentView {} diff --git a/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts b/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts new file mode 100644 index 0000000000..ccb15a183f --- /dev/null +++ b/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts @@ -0,0 +1,113 @@ +import { CollectionVariableDefinition } from '../../../../test/specs/dom_components/model/ComponentTypes'; +import { Model } from '../../../common'; +import EditorModel from '../../../editor/model/Editor'; +import DataVariable, { DataVariableType } from '../DataVariable'; +import { keyInnerCollectionState } from './constants'; +import { CollectionState, CollectionsStateMap } from './types'; + +export default class CollectionVariable extends Model { + em: EditorModel; + collectionsStateMap: CollectionsStateMap; + dataVariable?: DataVariable; + + constructor( + attrs: CollectionVariableDefinition, + options: { + em: EditorModel; + collectionsStateMap: CollectionsStateMap; + }, + ) { + super(attrs, options); + this.em = options.em; + this.collectionsStateMap = options.collectionsStateMap; + + this.updateDataVariable(); + } + + hasDynamicValue() { + return !!this.dataVariable; + } + + getDataValue() { + const { resolvedValue } = this.updateDataVariable(); + + if (resolvedValue?.type === DataVariableType) { + return this.dataVariable!.getDataValue(); + } + return resolvedValue; + } + + private updateDataVariable() { + const resolvedValue = resolveCollectionVariable( + this.attributes as CollectionVariableDefinition, + this.collectionsStateMap, + this.em, + ); + + let dataVariable; + if (resolvedValue?.type === DataVariableType) { + dataVariable = new DataVariable(resolvedValue, { em: this.em }); + this.dataVariable = dataVariable; + } + + return { resolvedValue, dataVariable }; + } + + destroy() { + return this.dataVariable?.destroy?.() || super.destroy(); + } +} + +function resolveCollectionVariable( + collectionVariableDefinition: CollectionVariableDefinition, + collectionsStateMap: CollectionsStateMap, + em: EditorModel, +) { + const { collection_name = keyInnerCollectionState, variable_type, path } = collectionVariableDefinition; + const collectionItem = collectionsStateMap[collection_name]; + + if (!collectionItem) { + em.logError(`Collection not found: ${collection_name}`); + return ''; + } + + if (!variable_type) { + em.logError(`Missing collection variable type for collection: ${collection_name}`); + return ''; + } + + if (variable_type === 'current_item') { + return resolveCurrentItem(collectionItem, path, collection_name, em); + } + + return collectionItem[variable_type]; +} + +function resolveCurrentItem( + collectionItem: CollectionState, + path: string | undefined, + collection_name: string, + em: EditorModel, +) { + const currentItem = collectionItem.current_item; + + if (!currentItem) { + em.logError(`Current item is missing for collection: ${collection_name}`); + return ''; + } + + if (currentItem.type === DataVariableType) { + const resolvedPath = currentItem.path ? `${currentItem.path}.${path}` : path; + return { + ...currentItem, + path: resolvedPath, + }; + } + + if (path && !currentItem[path]) { + em.logError(`Path not found in current item: ${path} for collection: ${collection_name}`); + return ''; + } + + return path ? currentItem[path] : currentItem; +} diff --git a/packages/core/src/data_sources/model/collection_component/constants.ts b/packages/core/src/data_sources/model/collection_component/constants.ts new file mode 100644 index 0000000000..adf3b232c3 --- /dev/null +++ b/packages/core/src/data_sources/model/collection_component/constants.ts @@ -0,0 +1,4 @@ +export const CollectionComponentType = 'collection-component'; +export const keyCollectionDefinition = 'collectionDefinition'; +export const keyInnerCollectionState = 'innerCollectionState'; +export const CollectionVariableType = 'parent-collection-variable'; diff --git a/packages/core/src/data_sources/model/collection_component/types.ts b/packages/core/src/data_sources/model/collection_component/types.ts new file mode 100644 index 0000000000..e7cc146769 --- /dev/null +++ b/packages/core/src/data_sources/model/collection_component/types.ts @@ -0,0 +1,47 @@ +import { CollectionComponentType, keyCollectionDefinition } from './constants'; + +import { ComponentDefinition } from '../../../dom_components/model/types'; +import { CollectionVariableDefinition } from '../../../../test/specs/dom_components/model/ComponentTypes'; +import { DataVariableDefinition } from '../DataVariable'; + +type CollectionDataSource = any[] | DataVariableDefinition | CollectionVariableDefinition; +type CollectionConfig = { + start_index?: number; + end_index?: number; + dataSource: CollectionDataSource; +}; + +export enum CollectionStateVariableType { + current_index = 'current_index', + start_index = 'start_index', + current_item = 'current_item', + end_index = 'end_index', + collection_name = 'collection_name', + total_items = 'total_items', + remaining_items = 'remaining_items', +} + +export type CollectionState = { + [CollectionStateVariableType.current_index]: number; + [CollectionStateVariableType.start_index]: number; + [CollectionStateVariableType.current_item]: any; + [CollectionStateVariableType.end_index]: number; + [CollectionStateVariableType.collection_name]?: string; + [CollectionStateVariableType.total_items]: number; + [CollectionStateVariableType.remaining_items]: number; +}; + +export type CollectionsStateMap = { + [key: string]: CollectionState; +}; + +export type CollectionComponentDefinition = { + [keyCollectionDefinition]: CollectionDefinition; +} & ComponentDefinition; + +export type CollectionDefinition = { + type: typeof CollectionComponentType; + collection_name?: string; + config: CollectionConfig; + block: ComponentDefinition; +}; diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index 85451ca7fb..71322c1609 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -1,10 +1,17 @@ import EditorModel from '../../editor/model/Editor'; import { DynamicValue, DynamicValueDefinition } from '../types'; +import { CollectionsStateMap } from './collection_component/types'; +import CollectionVariable from './collection_component/CollectionVariable'; +import { CollectionVariableDefinition } from '../../../test/specs/dom_components/model/ComponentTypes'; +import { CollectionVariableType } from './collection_component/constants'; import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition'; import DataVariable, { DataVariableType } from './DataVariable'; export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition { - return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type); + return ( + typeof value === 'object' && + [DataVariableType, ConditionalVariableType, CollectionVariableType].includes(value?.type) + ); } export function isDynamicValue(value: any): value is DynamicValue { @@ -23,7 +30,14 @@ export function evaluateVariable(variable: any, em: EditorModel) { return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; } -export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue { +export function getDynamicValueInstance( + valueDefinition: DynamicValueDefinition, + options: { + em: EditorModel; + collectionsStateMap?: CollectionsStateMap; + }, +): DynamicValue { + const { em } = options; const dynamicType = valueDefinition.type; let dynamicVariable: DynamicValue; @@ -36,6 +50,11 @@ export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em }); break; } + case CollectionVariableType: { + // @ts-ignore + dynamicVariable = new CollectionVariable(valueDefinition, options); + break; + } default: throw new Error(`Unsupported dynamic type: ${dynamicType}`); } @@ -43,8 +62,14 @@ export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, return dynamicVariable; } -export function evaluateDynamicValueDefinition(valueDefinition: DynamicValueDefinition, em: EditorModel) { - const dynamicVariable = getDynamicValueInstance(valueDefinition, em); +export function evaluateDynamicValueDefinition( + valueDefinition: DynamicValueDefinition, + options: { + em: EditorModel; + collectionsStateMap?: CollectionsStateMap; + }, +) { + const dynamicVariable = getDynamicValueInstance(valueDefinition, options); return { variable: dynamicVariable, value: dynamicVariable.getDataValue() }; } diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index 95e86123ec..f40bf729a6 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -1,12 +1,17 @@ import { ObjectAny } from '../common'; +import CollectionVariable from './model/collection_component/CollectionVariable'; +import { CollectionVariableDefinition } from '../../test/specs/dom_components/model/ComponentTypes'; import ComponentDataVariable from './model/ComponentDataVariable'; import DataRecord from './model/DataRecord'; import DataRecords from './model/DataRecords'; import DataVariable, { DataVariableDefinition } from './model/DataVariable'; import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition'; -export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition; -export type DynamicValueDefinition = DataVariableDefinition | ConditionalVariableDefinition; +export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition | CollectionVariable; +export type DynamicValueDefinition = + | DataVariableDefinition + | ConditionalVariableDefinition + | CollectionVariableDefinition; export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 1111e1a371..8c1235d61f 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -128,6 +128,8 @@ import { DataVariableType } from '../data_sources/model/DataVariable'; import { ConditionalVariableType } from '../data_sources/model/conditional_variables/DataCondition'; import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ConditionalComponent'; import ConditionalComponentView from '../data_sources/view/ComponentDynamicView'; +import CollectionComponent from '../data_sources/model/collection_component/CollectionComponent'; +import CollectionComponentView from '../data_sources/model/collection_component/CollectionComponentView'; export type ComponentEvent = | 'component:create' @@ -193,6 +195,11 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ + { + id: 'collection-component', + model: CollectionComponent, + view: CollectionComponentView, + }, { id: ConditionalVariableType, model: ComponentConditionalVariable, diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index dd135c727b..4920f8ffb1 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -1,3 +1,4 @@ +import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; import { isUndefined, isFunction, @@ -52,14 +53,9 @@ import { updateSymbolProps, } from './SymbolUtils'; import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher'; -import { DynamicValueWatcher } from './DynamicValueWatcher'; -import { DynamicValueDefinition } from '../../data_sources/types'; +import { DynamicWatchersOptions } from './DynamicValueWatcher'; export interface IComponent extends ExtractMethods {} -export interface DynamicWatchersOptions { - skipWatcherUpdates?: boolean; - fromDataSource?: boolean; -} export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {} export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {} @@ -75,6 +71,8 @@ export const keySymbol = '__symbol'; export const keySymbolOvrd = '__symbol_ovrd'; export const keyUpdate = ComponentsEvents.update; export const keyUpdateInside = ComponentsEvents.updateInside; +export const keyCollectionsStateMap = '__collections_state_map'; +export const keyIsCollectionItem = '__is_collection_item'; /** * The Component object represents a single node of our template structure, so when you update its properties the changes are @@ -265,9 +263,17 @@ export default class Component extends StyleableModel { componentDVListener: ComponentDynamicValueWatcher; constructor(props: ComponentProperties = {}, opt: ComponentOptions) { - super(props, opt); - this.componentDVListener = new ComponentDynamicValueWatcher(this, opt.em); - this.componentDVListener.addProps(props); + const componentDVListener = new ComponentDynamicValueWatcher(undefined, { + em: opt.em, + collectionsStateMap: props[keyCollectionsStateMap], + }); + super(props, { + ...opt, + // @ts-ignore + componentDVListener, + }); + componentDVListener.bindComponent(this); + this.componentDVListener = componentDVListener; bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps'); const em = opt.em; @@ -292,12 +298,15 @@ export default class Component extends StyleableModel { } opt.em = em; - this.opt = opt; + this.opt = { + ...opt, + collectionsStateMap: props[keyCollectionsStateMap], + isCollectionItem: !!props['isCollectionItem'], + }; this.em = em!; this.config = opt.config || {}; - this.setAttributes({ + this.addAttributes({ ...(result(this, 'defaults').attributes || {}), - ...(this.get('attributes') || {}), }); this.ccid = Component.createId(this, opt); this.preInit(); @@ -310,6 +319,7 @@ export default class Component extends StyleableModel { this.listenTo(this, 'change:tagName', this.tagUpdated); this.listenTo(this, 'change:attributes', this.attrUpdated); this.listenTo(this, 'change:attributes:id', this._idUpdated); + this.listenTo(this, `change:${keyCollectionsStateMap}`, this._collectionsStateUpdated); this.on('change:toolbar', this.__emitUpdateTlb); this.on('change', this.__onChange); this.on(keyUpdateInside, this.__propToParent); @@ -337,6 +347,16 @@ export default class Component extends StyleableModel { } } + getCollectionStateMap(): CollectionsStateMap { + const collectionStateMapProp = this.get(keyCollectionsStateMap); + if (collectionStateMapProp) { + return collectionStateMapProp; + } + + const parent = this.parent() || this.opt.parent; + return parent?.getCollectionStateMap() || {}; + } + set( keyOrAttributes: A | Partial, valueOrOptions?: ComponentProperties[A] | ComponentSetOptions, @@ -356,15 +376,10 @@ export default class Component extends StyleableModel { } // @ts-ignore - const em = this.em || options.em; - const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, em); - - const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; - if (!shouldSkipWatcherUpdates) { - this.componentDVListener?.addProps(attributes); - } + this.componentDVListener = this.componentDVListener || options.componentDVListener; + const evaluatedProps = this.componentDVListener.addProps(attributes, options); - return super.set(evaluatedAttributes, options); + return super.set(evaluatedProps, options); } __postAdd(opts: { recursive?: boolean } = {}) { @@ -503,8 +518,13 @@ export default class Component extends StyleableModel { * @example * component.setSymbolOverride(['children', 'classes']); */ - setSymbolOverride(value?: boolean | string | string[]) { - this.set(keySymbolOvrd, (isString(value) ? [value] : value) ?? 0); + setSymbolOverride(value: boolean | string | string[], options: DynamicWatchersOptions = {}) { + this.set( + { + [keySymbolOvrd]: (isString(value) ? [value] : value) ?? 0, + }, + options, + ); } /** @@ -685,14 +705,7 @@ export default class Component extends StyleableModel { * component.setAttributes({ id: 'test', 'data-key': 'value' }); */ setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) { - // @ts-ignore - const em = this.em || opts.em; - const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attrs, em); - const shouldSkipWatcherUpdates = opts.skipWatcherUpdates || opts.fromDataSource; - if (!shouldSkipWatcherUpdates) { - this.componentDVListener.setAttributes(attrs); - } - this.set('attributes', { ...evaluatedAttributes }, opts); + this.set('attributes', { ...attrs }, opts); return this; } @@ -965,7 +978,7 @@ export default class Component extends StyleableModel { const value = trait.getInitValue(); if (trait.changeProp) { - this.set(name, value); + isUndefined(this.get(name)) && this.set(name, value); } else { if (name && value) attrs[name] = value; } @@ -1318,7 +1331,8 @@ export default class Component extends StyleableModel { clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this { const em = this.em; const attr = { - ...this.componentDVListener.getPropsDefsOrValues(this.attributes), + ...this.attributes, + ...this.componentDVListener.getDynamicPropsDefs(), }; const opts = { ...this.opt }; const id = this.getId(); @@ -1591,6 +1605,17 @@ export default class Component extends StyleableModel { delete obj.open; // used in Layers delete obj._undoexc; delete obj.delegate; + if (this.get('isCollectionItem')) { + delete obj[keySymbol]; + delete obj[keySymbolOvrd]; + delete obj[keySymbols]; + delete obj[keyCollectionsStateMap]; + delete obj['isCollectionItem']; + delete obj.attributes.id; + obj['components'] = this.components() + .toArray() + .map((cmp) => cmp.toJSON()); + } if (!opts.fromUndo) { const symbol = obj[keySymbol]; @@ -1657,9 +1682,7 @@ export default class Component extends StyleableModel { * @return {this} */ setId(id: string, opts?: SetOptions & { idUpdate?: boolean }) { - const attrs = { ...this.get('attributes') }; - attrs.id = id; - this.set('attributes', attrs, opts); + this.addAttributes({ id }); return this; } @@ -1966,6 +1989,13 @@ export default class Component extends StyleableModel { selector && selector.set({ name: id, label: id }); } + _collectionsStateUpdated(m: any, v: CollectionsStateMap, opts = {}) { + this.componentDVListener.updateCollectionStateMap(v); + this.components().forEach((child) => { + child.set(keyCollectionsStateMap, v); + }); + } + static typeExtends = new Set(); static getDefaults() { diff --git a/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts b/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts index 9114178336..c9d04821e8 100644 --- a/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts +++ b/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts @@ -1,46 +1,88 @@ import { ObjectAny } from '../../common'; +import { CollectionVariableType } from '../../data_sources/model/collection_component/constants'; +import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; import EditorModel from '../../editor/model/Editor'; -import Component from './Component'; +import Component, { keyCollectionsStateMap } from './Component'; +import { DynamicWatchersOptions } from './DynamicValueWatcher'; import { DynamicValueWatcher } from './DynamicValueWatcher'; +import { getSymbolsToUpdate } from './SymbolUtils'; export class ComponentDynamicValueWatcher { private propertyWatcher: DynamicValueWatcher; private attributeWatcher: DynamicValueWatcher; constructor( - private component: Component, - em: EditorModel, + private component: Component | undefined, + options: { + em: EditorModel; + collectionsStateMap: CollectionsStateMap; + }, ) { - this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), em); - this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), em); + this.propertyWatcher = new DynamicValueWatcher(component, this.createPropertyUpdater(), options); + this.attributeWatcher = new DynamicValueWatcher(component, this.createAttributeUpdater(), options); } private createPropertyUpdater() { - return (key: string, value: any) => { - this.component.set(key, value, { fromDataSource: true, avoidStore: true }); + return (component: Component | undefined, key: string, value: any) => { + if (!component) return; + component.set(key, value, { fromDataSource: true, avoidStore: true }); }; } private createAttributeUpdater() { - return (key: string, value: any) => { - this.component.addAttributes({ [key]: value }, { fromDataSource: true, avoidStore: true }); + return (component: Component | undefined, key: string, value: any) => { + if (!component) return; + component.addAttributes({ [key]: value }, { fromDataSource: true, avoidStore: true }); }; } - addProps(props: ObjectAny) { - this.propertyWatcher.addDynamicValues(props); + bindComponent(component: Component) { + this.component = component; + this.propertyWatcher.bindComponent(component); + this.attributeWatcher.bindComponent(component); + this.updateSymbolOverride(); } - addAttributes(attributes: ObjectAny) { - this.attributeWatcher.addDynamicValues(attributes); + updateCollectionStateMap(collectionsStateMap: CollectionsStateMap) { + this.propertyWatcher.updateCollectionStateMap(collectionsStateMap); + this.attributeWatcher.updateCollectionStateMap(collectionsStateMap); } - setAttributes(attributes: ObjectAny) { - this.attributeWatcher.setDynamicValues(attributes); + addProps(props: ObjectAny, options: DynamicWatchersOptions = {}) { + const evaluatedProps = this.propertyWatcher.addDynamicValues(props, options); + if (props.attributes) { + const evaluatedAttributes = this.attributeWatcher.setDynamicValues(props.attributes, options); + evaluatedProps['attributes'] = evaluatedAttributes; + } + + const skipOverridUpdates = options.skipWatcherUpdates || options.fromDataSource; + if (!skipOverridUpdates) { + this.updateSymbolOverride(); + } + + return evaluatedProps; } removeAttributes(attributes: string[]) { this.attributeWatcher.removeListeners(attributes); + this.updateSymbolOverride(); + } + + private updateSymbolOverride() { + if (!this.component || !this.component.get('isCollectionItem')) return; + + const keys = this.propertyWatcher.getDynamicValuesOfType(CollectionVariableType); + const attributesKeys = this.attributeWatcher.getDynamicValuesOfType(CollectionVariableType); + + const combinedKeys = [keyCollectionsStateMap, ...keys]; + const haveOverridenAttributes = Object.keys(attributesKeys).length; + if (haveOverridenAttributes) combinedKeys.push('attributes'); + + const toUp = getSymbolsToUpdate(this.component); + toUp.forEach((child) => { + child.setSymbolOverride(combinedKeys, { fromDataSource: true }); + }); + this.component.setSymbolOverride(combinedKeys, { fromDataSource: true }); } getDynamicPropsDefs() { @@ -51,14 +93,14 @@ export class ComponentDynamicValueWatcher { return this.attributeWatcher.getAllSerializableValues(); } - getAttributesDefsOrValues(attributes: ObjectAny) { - return this.attributeWatcher.getSerializableValues(attributes); - } - getPropsDefsOrValues(props: ObjectAny) { return this.propertyWatcher.getSerializableValues(props); } + getAttributesDefsOrValues(attributes: ObjectAny) { + return this.attributeWatcher.getSerializableValues(attributes); + } + destroy() { this.propertyWatcher.removeListeners(); this.attributeWatcher.removeListeners(); diff --git a/packages/core/src/dom_components/model/Components.ts b/packages/core/src/dom_components/model/Components.ts index 892b1d6c27..36568a8a4c 100644 --- a/packages/core/src/dom_components/model/Components.ts +++ b/packages/core/src/dom_components/model/Components.ts @@ -1,5 +1,5 @@ import { isEmpty, isArray, isString, isFunction, each, includes, extend, flatten, keys } from 'underscore'; -import Component from './Component'; +import Component, { keyCollectionsStateMap } from './Component'; import { AddOptions, Collection } from '../../common'; import { DomComponentsConfig } from '../config/config'; import EditorModel from '../../editor/model/Editor'; @@ -17,6 +17,7 @@ import ComponentText from './ComponentText'; import ComponentWrapper from './ComponentWrapper'; import { ComponentsEvents, ParseStringOptions } from '../types'; import { isSymbolInstance, isSymbolRoot, updateSymbolComps } from './SymbolUtils'; +import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => { if (!cmp) return []; @@ -87,6 +88,8 @@ export interface ComponentsOptions { em: EditorModel; config?: DomComponentsConfig; domc?: ComponentManager; + collectionsStateMap?: CollectionsStateMap; + isCollectionItem?: boolean; } interface AddComponentOptions extends AddOptions { @@ -328,7 +331,20 @@ Component> { */ processDef(mdl: Component | ComponentDefinition | ComponentDefinitionDefined) { // Avoid processing Models - if (mdl.cid && mdl.ccid) return mdl; + if (mdl.cid && mdl.ccid) { + const componentCollectionsStateMap = mdl.get(keyCollectionsStateMap); + const parentCollectionsStateMap = this.opt.collectionsStateMap; + mdl.set(keyCollectionsStateMap, { + ...componentCollectionsStateMap, + ...parentCollectionsStateMap, + }); + + if (!mdl.get('isCollectionItem') && this.opt.isCollectionItem) { + mdl.set('isCollectionItem', this.opt.isCollectionItem); + } + + return mdl; + } const { em, config = {} } = this; const { processor } = config; let model = mdl; @@ -376,7 +392,15 @@ Component> { extend(model, res.props); } - return model; + return { + ...(this.opt.isCollectionItem && { + isCollectionItem: this.opt.isCollectionItem, + [keyCollectionsStateMap]: { + ...this.opt.collectionsStateMap, + }, + }), + ...model, + }; } onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) { diff --git a/packages/core/src/dom_components/model/DynamicValueWatcher.ts b/packages/core/src/dom_components/model/DynamicValueWatcher.ts index 88af44dad2..27229b7c96 100644 --- a/packages/core/src/dom_components/model/DynamicValueWatcher.ts +++ b/packages/core/src/dom_components/model/DynamicValueWatcher.ts @@ -1,78 +1,111 @@ +import { DynamicValueDefinition } from './../../data_sources/types'; +import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; import { ObjectAny } from '../../common'; import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; import { evaluateDynamicValueDefinition, isDynamicValueDefinition } from '../../data_sources/model/utils'; -import { DynamicValue } from '../../data_sources/types'; import EditorModel from '../../editor/model/Editor'; +import Component from './Component'; +import { CollectionVariableType } from '../../data_sources/model/collection_component/constants'; + +export interface DynamicWatchersOptions { + skipWatcherUpdates?: boolean; + fromDataSource?: boolean; +} export class DynamicValueWatcher { - dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {}; + private dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {}; constructor( - private updateFn: (key: string, value: any) => void, - private em: EditorModel, + private component: Component | undefined, + private updateFn: (component: Component | undefined, key: string, value: any) => void, + private options: { + em: EditorModel; + collectionsStateMap?: CollectionsStateMap; + }, ) {} - static getStaticValues(values: ObjectAny | undefined, em: EditorModel): ObjectAny { - if (!values) return {}; - const evaluatedValues: ObjectAny = { ...values }; - const propsKeys = Object.keys(values); + bindComponent(component: Component) { + this.component = component; + } - for (const key of propsKeys) { - const valueDefinition = values[key]; - if (!isDynamicValueDefinition(valueDefinition)) continue; + updateCollectionStateMap(collectionsStateMap: CollectionsStateMap) { + this.options = { + ...this.options, + collectionsStateMap, + }; + + const collectionVariablesKeys = this.getDynamicValuesOfType(CollectionVariableType); + const collectionVariablesObject = collectionVariablesKeys.reduce( + (acc: { [key: string]: DynamicValueDefinition | null }, key) => { + acc[key] = null; + return acc; + }, + {}, + ); + const newVariables = this.getSerializableValues(collectionVariablesObject); + const evaluatedValues = this.addDynamicValues(newVariables); + + Object.keys(evaluatedValues).forEach((key) => { + this.updateFn(this.component, key, evaluatedValues[key]); + }); + } - const { value } = evaluateDynamicValueDefinition(valueDefinition, em); - evaluatedValues[key] = value; + setDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) { + const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; + if (!shouldSkipWatcherUpdates) { + this.removeListeners(); } - return evaluatedValues; + return this.addDynamicValues(values, options); } - static areStaticValues(values: ObjectAny | undefined) { - if (!values) return true; - return Object.keys(values).every((key) => { - return !isDynamicValueDefinition(values[key]); - }); - } + addDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) { + if (!values) return {}; + const evaluatedValues = this.evaluateValues(values); - setDynamicValues(values: ObjectAny | undefined) { - this.removeListeners(); - return this.addDynamicValues(values); + const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; + if (!shouldSkipWatcherUpdates) { + this.updateListeners(values); + } + + return evaluatedValues; } - addDynamicValues(values: ObjectAny | undefined) { - if (!values) return {}; + private updateListeners(values: { [key: string]: any }) { + const em = this.options.em; this.removeListeners(Object.keys(values)); - const dynamicProps = this.getDynamicValues(values); - const propsKeys = Object.keys(dynamicProps); + const propsKeys = Object.keys(values); for (let index = 0; index < propsKeys.length; index++) { const key = propsKeys[index]; + if (!isDynamicValueDefinition(values[key])) { + continue; + } + + const { variable } = evaluateDynamicValueDefinition(values[key], this.options); this.dynamicVariableListeners[key] = new DynamicVariableListenerManager({ - em: this.em, - dataVariable: dynamicProps[key], + em: em, + dataVariable: variable, updateValueFromDataVariable: (value: any) => { - this.updateFn.bind(this)(key, value); + this.updateFn.bind(this)(this.component, key, value); }, }); } - - return dynamicProps; } - private getDynamicValues(values: ObjectAny) { - const dynamicValues: { - [key: string]: DynamicValue; - } = {}; + private evaluateValues(values: ObjectAny) { + const evaluatedValues: { + [key: string]: any; + } = { ...values }; const propsKeys = Object.keys(values); for (let index = 0; index < propsKeys.length; index++) { const key = propsKeys[index]; if (!isDynamicValueDefinition(values[key])) { continue; } - const { variable } = evaluateDynamicValueDefinition(values[key], this.em); - dynamicValues[key] = variable; + const { value } = evaluateDynamicValueDefinition(values[key], this.options); + evaluatedValues[key] = value; } - return dynamicValues; + return evaluatedValues; } /** @@ -88,6 +121,8 @@ export class DynamicValueWatcher { delete this.dynamicVariableListeners[key]; } }); + + return propsKeys; } getSerializableValues(values: ObjectAny | undefined) { @@ -114,4 +149,13 @@ export class DynamicValueWatcher { return serializableValues; } + + getDynamicValuesOfType(type: DynamicValueDefinition['type']) { + const keys = Object.keys(this.dynamicVariableListeners).filter((key: string) => { + // @ts-ignore + return this.dynamicVariableListeners[key].dynamicVariable.get('type') === type; + }); + + return keys; + } } diff --git a/packages/core/src/dom_components/model/SymbolUtils.ts b/packages/core/src/dom_components/model/SymbolUtils.ts index 76ae4b1397..60708c56c7 100644 --- a/packages/core/src/dom_components/model/SymbolUtils.ts +++ b/packages/core/src/dom_components/model/SymbolUtils.ts @@ -3,6 +3,7 @@ import Component, { keySymbol, keySymbolOvrd, keySymbols } from './Component'; import { SymbolToUpOptions } from './types'; import { isEmptyObj } from '../../utils/mixins'; import Components from './Components'; +import { CollectionVariableType } from '../../data_sources/model/collection_component/constants'; export const isSymbolMain = (cmp: Component) => isArray(cmp.get(keySymbols)); @@ -129,38 +130,58 @@ export const logSymbol = (symb: Component, type: string, toUp: Component[], opts symb.em.log(type, { model: symb, toUp, context: 'symbols', opts }); }; -export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}) => { - const changed = symbol.changedAttributes() || {}; - const attrs = changed.attributes || {}; - delete changed.status; - delete changed.open; - delete changed[keySymbols]; - delete changed[keySymbol]; - delete changed[keySymbolOvrd]; - delete changed.attributes; +export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}): void => { + const changed = symbol.componentDVListener.getPropsDefsOrValues({ ...symbol.changedAttributes() } || {}); + const attrs = symbol.componentDVListener.getAttributesDefsOrValues({ ...changed.attributes } || {}); + + cleanChangedProperties(changed, attrs); + + if (!isEmptyObj(changed)) { + const toUpdate = getSymbolsToUpdate(symbol, opts); + + // Filter properties to propagate + filterPropertiesForPropagation(changed, symbol); + + logSymbol(symbol, 'props', toUpdate, { opts, changed }); + + // Update child symbols + toUpdate.forEach((child) => { + const propsToUpdate = { ...changed }; + filterPropertiesForPropagation(propsToUpdate, child); + child.set(propsToUpdate, { fromInstance: symbol, ...opts }); + }); + } +}; + +const cleanChangedProperties = (changed: Record, attrs: Record): void => { + const keysToDelete = ['status', 'open', keySymbols, keySymbol, keySymbolOvrd, 'attributes']; + keysToDelete.forEach((key) => delete changed[key]); delete attrs.id; if (!isEmptyObj(attrs)) { changed.attributes = attrs; } +}; - if (!isEmptyObj(changed)) { - const toUp = getSymbolsToUpdate(symbol, opts); - // Avoid propagating overrides to other symbols - keys(changed).map((prop) => { - if (isSymbolOverride(symbol, prop)) delete changed[prop]; - }); +const filterPropertiesForPropagation = (props: Record, component: Component): void => { + keys(props).forEach((prop) => { + if (!shouldPropagateProperty(props, prop, component)) { + delete props[prop]; + } + }); +}; - logSymbol(symbol, 'props', toUp, { opts, changed }); - toUp.forEach((child) => { - const propsChanged = { ...changed }; - // Avoid updating those with override - keys(propsChanged).map((prop) => { - if (isSymbolOverride(child, prop)) delete propsChanged[prop]; - }); - child.set(propsChanged, { fromInstance: symbol, ...opts }); - }); - } +const shouldPropagateProperty = (props: Record, prop: string, component: Component): boolean => { + const isCollectionVariableDefinition = (() => { + if (prop === 'attributes') { + const attributes = props['attributes']; + return Object.values(attributes).some((attr: any) => attr?.type === CollectionVariableType); + } + + return props[prop]?.type === CollectionVariableType; + })(); + + return !isSymbolOverride(component, prop) || isCollectionVariableDefinition; }; export const updateSymbolCls = (symbol: Component, opts: any = {}) => { diff --git a/packages/core/src/dom_components/model/types.ts b/packages/core/src/dom_components/model/types.ts index 79b452b6bc..25f5ddc158 100644 --- a/packages/core/src/dom_components/model/types.ts +++ b/packages/core/src/dom_components/model/types.ts @@ -1,3 +1,4 @@ +import { DynamicWatchersOptions } from './DynamicValueWatcher'; import Frame from '../../canvas/model/Frame'; import { AddOptions, Nullable, OptionAsDocument } from '../../common'; import EditorModel from '../../editor/model/Editor'; @@ -11,7 +12,7 @@ import Component from './Component'; import Components from './Components'; import { ToolbarButtonProps } from './ToolbarButton'; import { ParseNodeOptions } from '../../parser/config/config'; -import { DynamicValueDefinition } from '../../data_sources/types'; +import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; export type DragMode = 'translate' | 'absolute' | ''; @@ -253,7 +254,7 @@ export interface ComponentProperties { [key: string]: any; } -export interface SymbolToUpOptions { +export interface SymbolToUpOptions extends DynamicWatchersOptions { changed?: string; fromInstance?: boolean; noPropagate?: boolean; @@ -321,4 +322,7 @@ export interface ComponentOptions { frame?: Frame; temporary?: boolean; avoidChildren?: boolean; + collectionsStateMap?: CollectionsStateMap; + isCollectionItem?: boolean; + parent?: Component; } diff --git a/packages/core/src/utils/sorter/CanvasComponentNode.ts b/packages/core/src/utils/sorter/CanvasComponentNode.ts index 2495e75f24..9563386e86 100644 --- a/packages/core/src/utils/sorter/CanvasComponentNode.ts +++ b/packages/core/src/utils/sorter/CanvasComponentNode.ts @@ -6,6 +6,17 @@ export default class CanvasComponentNode extends BaseComponentNode { minUndroppableDimension: 1, // In px maxUndroppableDimension: 15, // In px }; + /** + * Check if a source node can be moved to a specified index within this component. + * @param {BaseComponentNode} source - The source node to move. + * @param {number} index - The display index to move the source to. + * @returns {boolean} - True if the move is allowed, false otherwise. + */ + canMove(source: BaseComponentNode, index: number): boolean { + console.log('🚀 ~ CanvasComponentNode ~ canMove ~ this.model:', this.model); + console.log('🚀 ~ CanvasComponentNode ~ canMove ~ source.model:', source.model); + return this.model.em.Components.canMove(this.model, source.model, this.getRealIndex(index)).result; + } /** * Get the associated view of this component. * @returns The view associated with the component, or undefined if none. diff --git a/packages/core/test/specs/data_sources/model/collection_component/CollectionComponent.ts b/packages/core/test/specs/data_sources/model/collection_component/CollectionComponent.ts new file mode 100644 index 0000000000..81f657ca7b --- /dev/null +++ b/packages/core/test/specs/data_sources/model/collection_component/CollectionComponent.ts @@ -0,0 +1,863 @@ +import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { + CollectionComponentType, + CollectionVariableType, +} from '../../../../../src/data_sources/model/collection_component/constants'; +import { CollectionStateVariableType } from '../../../../../src/data_sources/model/collection_component/types'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; +import { getSymbolMain } from '../../../../../src/dom_components/model/SymbolUtils'; +import { ProjectData } from '../../../../../src/storage_manager'; + +describe('Collection component', () => { + let em: EditorModel; + let editor: Editor; + let dsm: DataSourceManager; + let dataSource: DataSource; + let wrapper: Component; + let firstRecord: DataRecord; + let secondRecord: DataRecord; + + beforeEach(() => { + ({ em, editor, dsm } = setupTestEditor()); + wrapper = em.getWrapper()!; + dataSource = dsm.add({ + id: 'my_data_source_id', + records: [ + { id: 'user1', user: 'user1', age: '12' }, + { id: 'user2', user: 'user2', age: '14' }, + { id: 'user3', user: 'user3', age: '16' }, + ], + }); + + firstRecord = dataSource.getRecord('user1')!; + secondRecord = dataSource.getRecord('user2')!; + }); + + afterEach(() => { + em.destroy(); + }); + + test('Collection component should be undroppable', () => { + const cmp = wrapper.components({ + type: CollectionComponentType, + collectionDefinition: { + block: { + type: 'default', + }, + config: { + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + expect(cmp.get('droppable')).toBe(false); + }); + + test('Collection items should be undraggable', () => { + const cmp = wrapper.components({ + type: CollectionComponentType, + collectionDefinition: { + block: { + type: 'default', + }, + config: { + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + cmp.components().forEach((child) => { + expect(child.get('draggable')).toBe(false); + }); + }); + + test('Collection items should be symbols', () => { + const cmp = wrapper.components({ + type: CollectionComponentType, + collectionDefinition: { + block: { + type: 'default', + components: [ + { + type: 'default', + }, + ], + }, + config: { + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + expect(cmp.components()).toHaveLength(3); + cmp.components().forEach((child) => expect(child.get('type')).toBe('default')); + const children = cmp.components(); + const firstChild = children.at(0); + + children.slice(1).forEach((component) => { + expect(getSymbolMain(component)).toBe(firstChild); + }); + }); + + describe('Collection variables', () => { + describe('Properties', () => { + let cmp: Component; + let firstChild!: Component; + let firstGrandchild!: Component; + let secondChild!: Component; + let secondGrandchild!: Component; + let thirdChild!: Component; + + beforeEach(() => { + cmp = wrapper.components({ + type: CollectionComponentType, + collectionDefinition: { + block: { + type: 'default', + components: [ + { + type: 'default', + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + ], + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + custom_property: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + config: { + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + firstChild = cmp.components().at(0); + firstGrandchild = firstChild.components().at(0); + secondChild = cmp.components().at(1); + secondGrandchild = secondChild.components().at(0); + thirdChild = cmp.components().at(2); + }); + + test('Evaluating to static value', () => { + expect(firstChild.get('content')).toBe('user1'); + expect(firstChild.get('custom_property')).toBe('user1'); + expect(firstGrandchild.get('content')).toBe('user1'); + + expect(secondChild.get('content')).toBe('user2'); + expect(secondChild.get('custom_property')).toBe('user2'); + expect(secondGrandchild.get('content')).toBe('user2'); + }); + + test('Watching Records', async () => { + firstRecord.set('user', 'new_user1_value'); + expect(firstChild.get('content')).toBe('new_user1_value'); + expect(firstChild.get('custom_property')).toBe('new_user1_value'); + expect(firstGrandchild.get('content')).toBe('new_user1_value'); + + expect(secondChild.get('content')).toBe('user2'); + expect(secondChild.get('custom_property')).toBe('user2'); + expect(secondGrandchild.get('content')).toBe('user2'); + }); + + test('Updating the value to a static value', async () => { + firstChild.set('content', 'new_content_value'); + expect(firstChild.get('content')).toBe('new_content_value'); + expect(secondChild.get('content')).toBe('new_content_value'); + + firstRecord.set('user', 'wrong_value'); + expect(firstChild.get('content')).toBe('new_content_value'); + expect(secondChild.get('content')).toBe('new_content_value'); + + firstGrandchild.set('content', 'new_content_value'); + expect(firstGrandchild.get('content')).toBe('new_content_value'); + expect(secondGrandchild.get('content')).toBe('new_content_value'); + + firstRecord.set('user', 'wrong_value'); + expect(firstGrandchild.get('content')).toBe('new_content_value'); + expect(secondGrandchild.get('content')).toBe('new_content_value'); + }); + + test('Updating the value to a diffirent collection variable', async () => { + firstChild.set('content', { + // @ts-ignore + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'age', + }); + expect(firstChild.get('content')).toBe('12'); + expect(secondChild.get('content')).toBe('14'); + + firstRecord.set('age', 'new_value_12'); + secondRecord.set('age', 'new_value_14'); + + firstRecord.set('user', 'wrong_value'); + secondRecord.set('user', 'wrong_value'); + + expect(firstChild.get('content')).toBe('new_value_12'); + expect(secondChild.get('content')).toBe('new_value_14'); + + firstGrandchild.set('content', { + // @ts-ignore + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'age', + }); + expect(firstGrandchild.get('content')).toBe('new_value_12'); + expect(secondGrandchild.get('content')).toBe('new_value_14'); + + firstRecord.set('age', 'most_new_value_12'); + secondRecord.set('age', 'most_new_value_14'); + + expect(firstGrandchild.get('content')).toBe('most_new_value_12'); + expect(secondGrandchild.get('content')).toBe('most_new_value_14'); + }); + + test('Updating the value to a diffirent dynamic variable', async () => { + firstChild.set('content', { + // @ts-ignore + type: DataVariableType, + path: 'my_data_source_id.user2.user', + }); + expect(firstChild.get('content')).toBe('user2'); + expect(secondChild.get('content')).toBe('user2'); + expect(thirdChild.get('content')).toBe('user2'); + + secondRecord.set('user', 'new_value'); + expect(firstChild.get('content')).toBe('new_value'); + expect(secondChild.get('content')).toBe('new_value'); + expect(thirdChild.get('content')).toBe('new_value'); + + firstGrandchild.set('content', { + // @ts-ignore + type: DataVariableType, + path: 'my_data_source_id.user2.user', + }); + expect(firstGrandchild.get('content')).toBe('new_value'); + expect(secondGrandchild.get('content')).toBe('new_value'); + + secondRecord.set('user', 'most_new_value'); + + expect(firstGrandchild.get('content')).toBe('most_new_value'); + expect(secondGrandchild.get('content')).toBe('most_new_value'); + }); + }); + + describe('Attributes', () => { + let cmp: Component; + let firstChild!: Component; + let firstGrandchild!: Component; + let secondChild!: Component; + let secondGrandchild!: Component; + let thirdChild!: Component; + + beforeEach(() => { + cmp = wrapper.components({ + type: CollectionComponentType, + collectionDefinition: { + block: { + type: 'default', + components: [ + { + type: 'default', + attributes: { + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + }, + ], + attributes: { + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + }, + config: { + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + firstChild = cmp.components().at(0); + firstGrandchild = firstChild.components().at(0); + secondChild = cmp.components().at(1); + secondGrandchild = secondChild.components().at(0); + thirdChild = cmp.components().at(2); + }); + + test('Evaluating to static value', () => { + expect(firstChild.getAttributes()['content']).toBe('user1'); + expect(firstGrandchild.getAttributes()['content']).toBe('user1'); + + expect(secondChild.getAttributes()['content']).toBe('user2'); + expect(secondGrandchild.getAttributes()['content']).toBe('user2'); + }); + + test('Watching Records', async () => { + firstRecord.set('user', 'new_user1_value'); + expect(firstChild.getAttributes()['content']).toBe('new_user1_value'); + expect(firstGrandchild.getAttributes()['content']).toBe('new_user1_value'); + + expect(secondChild.getAttributes()['content']).toBe('user2'); + expect(secondGrandchild.getAttributes()['content']).toBe('user2'); + }); + + test('Updating the value to a static value', async () => { + firstChild.setAttributes({ content: 'new_content_value' }); + expect(firstChild.getAttributes()['content']).toBe('new_content_value'); + expect(secondChild.getAttributes()['content']).toBe('new_content_value'); + + firstRecord.set('user', 'wrong_value'); + expect(firstChild.getAttributes()['content']).toBe('new_content_value'); + expect(secondChild.getAttributes()['content']).toBe('new_content_value'); + + firstGrandchild.setAttributes({ content: 'new_content_value' }); + expect(firstGrandchild.getAttributes()['content']).toBe('new_content_value'); + expect(secondGrandchild.getAttributes()['content']).toBe('new_content_value'); + + firstRecord.set('user', 'wrong_value'); + expect(firstGrandchild.getAttributes()['content']).toBe('new_content_value'); + expect(secondGrandchild.getAttributes()['content']).toBe('new_content_value'); + }); + + test('Updating the value to a diffirent collection variable', async () => { + firstChild.setAttributes({ + content: { + // @ts-ignore + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'age', + }, + }); + expect(firstChild.getAttributes()['content']).toBe('12'); + expect(secondChild.getAttributes()['content']).toBe('14'); + + firstRecord.set('age', 'new_value_12'); + secondRecord.set('age', 'new_value_14'); + + firstRecord.set('user', 'wrong_value'); + secondRecord.set('user', 'wrong_value'); + + expect(firstChild.getAttributes()['content']).toBe('new_value_12'); + expect(secondChild.getAttributes()['content']).toBe('new_value_14'); + + firstGrandchild.setAttributes({ + content: { + // @ts-ignore + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'age', + }, + }); + expect(firstGrandchild.getAttributes()['content']).toBe('new_value_12'); + expect(secondGrandchild.getAttributes()['content']).toBe('new_value_14'); + + firstRecord.set('age', 'most_new_value_12'); + secondRecord.set('age', 'most_new_value_14'); + + expect(firstGrandchild.getAttributes()['content']).toBe('most_new_value_12'); + expect(secondGrandchild.getAttributes()['content']).toBe('most_new_value_14'); + }); + + test('Updating the value to a diffirent dynamic variable', async () => { + firstChild.setAttributes({ + content: { + // @ts-ignore + type: DataVariableType, + path: 'my_data_source_id.user2.user', + }, + }); + expect(firstChild.getAttributes()['content']).toBe('user2'); + expect(secondChild.getAttributes()['content']).toBe('user2'); + expect(thirdChild.getAttributes()['content']).toBe('user2'); + + secondRecord.set('user', 'new_value'); + expect(firstChild.getAttributes()['content']).toBe('new_value'); + expect(secondChild.getAttributes()['content']).toBe('new_value'); + expect(thirdChild.getAttributes()['content']).toBe('new_value'); + + firstGrandchild.setAttributes({ + content: { + // @ts-ignore + type: DataVariableType, + path: 'my_data_source_id.user2.user', + }, + }); + expect(firstGrandchild.getAttributes()['content']).toBe('new_value'); + expect(secondGrandchild.getAttributes()['content']).toBe('new_value'); + + secondRecord.set('user', 'most_new_value'); + + expect(firstGrandchild.getAttributes()['content']).toBe('most_new_value'); + expect(secondGrandchild.getAttributes()['content']).toBe('most_new_value'); + }); + }); + + test('Traits', () => { + const cmp = wrapper.components({ + type: CollectionComponentType, + collectionDefinition: { + block: { + type: 'default', + traits: [ + { + name: 'attribute_trait', + value: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + { + name: 'property_trait', + changeProp: true, + value: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + ], + }, + config: { + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + expect(cmp.components()).toHaveLength(3); + const firstChild = cmp.components().at(0); + const secondChild = cmp.components().at(1); + + expect(firstChild.getAttributes()['attribute_trait']).toBe('user1'); + expect(firstChild.get('property_trait')).toBe('user1'); + + expect(secondChild.getAttributes()['attribute_trait']).toBe('user2'); + expect(secondChild.get('property_trait')).toBe('user2'); + + firstRecord.set('user', 'new_user1_value'); + expect(firstChild.getAttributes()['attribute_trait']).toBe('new_user1_value'); + expect(firstChild.get('property_trait')).toBe('new_user1_value'); + + expect(secondChild.getAttributes()['attribute_trait']).toBe('user2'); + expect(secondChild.get('property_trait')).toBe('user2'); + }); + }); + + describe('Serialization', () => { + let cmp: Component; + + beforeEach(() => { + const cmpDefinition = { + type: 'default', + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + custom_prop: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_index, + path: 'user', + }, + attributes: { + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + traits: [ + { + name: 'attribute_trait', + value: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + { + name: 'property_trait', + changeProp: true, + value: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + ], + }; + + const collectionComponentDefinition = { + type: CollectionComponentType, + collectionDefinition: { + collection_name: 'my_collection', + block: { + ...cmpDefinition, + components: [cmpDefinition, cmpDefinition], + }, + config: { + start_index: 0, + end_index: 1, + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + }; + + cmp = wrapper.components(collectionComponentDefinition)[0]; + }); + + test('Serializion with Collection Variables to JSON', () => { + expect(filterObjectForSnapshot(cmp.toJSON())).toMatchSnapshot(`Collection with no grandchildren`); + + const firstChild = cmp.components().at(0); + const newChildDefinition = { + type: 'default', + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_index, + path: 'user', + }, + }; + firstChild.components().at(0).components(newChildDefinition); + expect(filterObjectForSnapshot(cmp.toJSON())).toMatchSnapshot(`Collection with grandchildren`); + }); + + test('Saving', () => { + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const component = frame.component.components[0]; + + expect(filterObjectForSnapshot(component)).toMatchSnapshot(`Collection with no grandchildren`); + + const firstChild = cmp.components().at(0); + const newChildDefinition = { + type: 'default', + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_index, + path: 'user', + }, + }; + firstChild.components().at(0).components(newChildDefinition); + expect(filterObjectForSnapshot(cmp.toJSON())).toMatchSnapshot(`Collection with grandchildren`); + }); + + test('Loading', () => { + const componentProjectData: ProjectData = { + assets: [], + pages: [ + { + frames: [ + { + component: { + components: [ + { + collectionDefinition: { + block: { + attributes: { + attribute_trait: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + content: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + }, + components: [ + { + attributes: { + attribute_trait: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + content: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + }, + content: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + custom_prop: { + path: 'user', + type: CollectionVariableType, + variable_type: 'current_index', + }, + property_trait: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + type: 'default', + }, + { + attributes: { + attribute_trait: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + content: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + }, + content: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + custom_prop: { + path: 'user', + type: CollectionVariableType, + variable_type: 'current_index', + }, + property_trait: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + type: 'default', + }, + ], + content: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + custom_prop: { + path: 'user', + type: CollectionVariableType, + variable_type: 'current_index', + }, + property_trait: { + path: 'user', + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + }, + type: 'default', + }, + collection_name: 'my_collection', + config: { + dataSource: { + path: 'my_data_source_id', + type: DataVariableType, + }, + end_index: 1, + start_index: 0, + }, + }, + type: 'collection-component', + }, + ], + docEl: { + tagName: 'html', + }, + head: { + type: 'head', + }, + stylable: [ + 'background', + 'background-color', + 'background-image', + 'background-repeat', + 'background-attachment', + 'background-position', + 'background-size', + ], + type: 'wrapper', + }, + id: 'frameid', + }, + ], + id: 'pageid', + type: 'main', + }, + ], + styles: [], + symbols: [], + dataSources: [dataSource], + }; + editor.loadProjectData(componentProjectData); + + const components = editor.getComponents(); + const component = components.models[0]; + const firstChild = component.components().at(0); + const firstGrandchild = firstChild.components().at(0); + const secondChild = component.components().at(1); + const secondGrandchild = secondChild.components().at(0); + + expect(firstChild.get('content')).toBe('user1'); + expect(firstChild.getAttributes()['content']).toBe('user1'); + expect(firstGrandchild.get('content')).toBe('user1'); + expect(firstGrandchild.getAttributes()['content']).toBe('user1'); + + expect(secondChild.get('content')).toBe('user2'); + expect(secondChild.getAttributes()['content']).toBe('user2'); + expect(secondGrandchild.get('content')).toBe('user2'); + expect(secondGrandchild.getAttributes()['content']).toBe('user2'); + + firstRecord.set('user', 'new_user1_value'); + expect(firstChild.get('content')).toBe('new_user1_value'); + expect(firstChild.getAttributes()['content']).toBe('new_user1_value'); + expect(firstGrandchild.get('content')).toBe('new_user1_value'); + expect(firstGrandchild.getAttributes()['content']).toBe('new_user1_value'); + + expect(secondChild.get('content')).toBe('user2'); + expect(secondChild.getAttributes()['content']).toBe('user2'); + expect(secondGrandchild.get('content')).toBe('user2'); + expect(secondGrandchild.getAttributes()['content']).toBe('user2'); + }); + }); + + describe('Configuration options', () => { + test('Collection with start and end indexes', () => { + const cmp = wrapper.components({ + type: CollectionComponentType, + collectionDefinition: { + block: { + type: 'default', + content: { + type: CollectionVariableType, + variable_type: CollectionStateVariableType.current_item, + path: 'user', + }, + }, + config: { + start_index: 1, + end_index: 2, + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + expect(cmp.components()).toHaveLength(2); + const firstChild = cmp.components().at(0); + const secondChild = cmp.components().at(1); + + expect(firstChild.get('content')).toBe('user2'); + expect(secondChild.get('content')).toBe('user3'); + }); + }); + + describe('Diffirent Collection variable types', () => { + const stateVariableTests = [ + { variableType: CollectionStateVariableType.current_index, expectedValues: [0, 1, 2] }, + { variableType: CollectionStateVariableType.start_index, expectedValues: [0, 0, 0] }, + { variableType: CollectionStateVariableType.end_index, expectedValues: [2, 2, 2] }, + { + variableType: CollectionStateVariableType.collection_name, + expectedValues: ['my_collection', 'my_collection', 'my_collection'], + }, + { variableType: CollectionStateVariableType.total_items, expectedValues: [3, 3, 3] }, + { variableType: CollectionStateVariableType.remaining_items, expectedValues: [2, 1, 0] }, + ]; + + stateVariableTests.forEach(({ variableType, expectedValues }) => { + test(`Variable type: ${variableType}`, () => { + const cmp = wrapper.components({ + type: CollectionComponentType, + collectionDefinition: { + collection_name: 'my_collection', + block: { + type: 'default', + content: { + type: CollectionVariableType, + variable_type: variableType, + }, + attributes: { + custom_attribute: { + type: CollectionVariableType, + variable_type: variableType, + }, + }, + traits: [ + { + name: 'attribute_trait', + value: { + type: CollectionVariableType, + variable_type: variableType, + }, + }, + { + name: 'property_trait', + changeProp: true, + value: { + type: CollectionVariableType, + variable_type: variableType, + }, + }, + ], + }, + config: { + dataSource: { + type: DataVariableType, + path: 'my_data_source_id', + }, + }, + }, + })[0]; + + const children = cmp.components(); + expect(children).toHaveLength(3); + + children.each((child, index) => { + expect(child.get('content')).toBe(expectedValues[index]); + expect(child.get('property_trait')).toBe(expectedValues[index]); + expect(child.getAttributes()['custom_attribute']).toBe(expectedValues[index]); + expect(child.getAttributes()['attribute_trait']).toBe(expectedValues[index]); + }); + }); + }); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/collection_component/__snapshots__/CollectionComponent.ts.snap b/packages/core/test/specs/data_sources/model/collection_component/__snapshots__/CollectionComponent.ts.snap new file mode 100644 index 0000000000..46f76cdf52 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/collection_component/__snapshots__/CollectionComponent.ts.snap @@ -0,0 +1,457 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Collection component Serialization Saving: Collection with grandchildren 1`] = ` +{ + "collectionDefinition": { + "block": { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "components": [ + { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "components": [ + { + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "type": "default", + }, + ], + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + ], + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + "collection_name": "my_collection", + "config": { + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "end_index": 1, + "start_index": 0, + }, + }, + "type": "collection-component", +} +`; + +exports[`Collection component Serialization Saving: Collection with no grandchildren 1`] = ` +{ + "collectionDefinition": { + "block": { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "components": [ + { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + ], + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + "collection_name": "my_collection", + "config": { + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "end_index": 1, + "start_index": 0, + }, + }, + "type": "collection-component", +} +`; + +exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with grandchildren 1`] = ` +{ + "collectionDefinition": { + "block": { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "components": [ + { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "components": [ + { + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "type": "default", + }, + ], + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + ], + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + "collection_name": "my_collection", + "config": { + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "end_index": 1, + "start_index": 0, + }, + }, + "type": "collection-component", +} +`; + +exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with no grandchildren 1`] = ` +{ + "collectionDefinition": { + "block": { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "components": [ + { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + { + "attributes": { + "attribute_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + }, + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + ], + "content": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "custom_prop": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_index", + }, + "property_trait": { + "path": "user", + "type": "parent-collection-variable", + "variable_type": "current_item", + }, + "type": "default", + }, + "collection_name": "my_collection", + "config": { + "dataSource": { + "path": "my_data_source_id", + "type": "data-variable", + }, + "end_index": 1, + "start_index": 0, + }, + }, + "type": "collection-component", +} +`; diff --git a/packages/core/test/specs/dom_components/model/ComponentTypes.ts b/packages/core/test/specs/dom_components/model/ComponentTypes.ts index 63db8a0347..2cc912b97c 100644 --- a/packages/core/test/specs/dom_components/model/ComponentTypes.ts +++ b/packages/core/test/specs/dom_components/model/ComponentTypes.ts @@ -1,3 +1,5 @@ +import { CollectionVariableType } from '../../../../src/data_sources/model/collection_component/constants'; +import { CollectionStateVariableType } from '../../../../src/data_sources/model/collection_component/types'; import Editor from '../../../../src/editor'; describe('Component Types', () => { @@ -96,3 +98,10 @@ describe('Component Types', () => { expect(cmp.components().at(0).is('svg-in')).toBe(true); }); }); + +export type CollectionVariableDefinition = { + type: typeof CollectionVariableType; + variable_type: CollectionStateVariableType; + collection_name?: string; + path?: string; +};