From a1ce18a159f26f8ec0e8ef96e6c8d63204df4e14 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 25 Dec 2024 11:23:04 +0200 Subject: [PATCH] Refactor collections --- .../CollectionComponent.ts | 342 ++++++++++-------- .../src/dom_components/model/Component.ts | 1 - 2 files changed, 182 insertions(+), 161 deletions(-) diff --git a/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts b/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts index c8bc21dc7..f3e232344 100644 --- a/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts +++ b/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts @@ -1,131 +1,101 @@ +import { DataVariableType } from './../DataVariable'; import { isArray } from 'underscore'; import Component from '../../../dom_components/model/Component'; import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; import { ConditionDefinition } from '../conditional_variables/DataCondition'; import DataSource from '../DataSource'; -import { DataVariableType } from '../DataVariable'; import { ObjectAny } from '../../../common'; import EditorModel from '../../../editor/model/Editor'; export const CollectionVariableType = 'collection-component'; -// Represents the type for defining a loop’s data source. +type CollectionVariable = { + type: 'parent-collection-variable'; + variable_type: keyof CollectionState; + collection_name?: string; + path?: string; +}; + type CollectionDataSource = - | any[] // Direct array - | { type: 'datasource-variable'; path: string } // Object representing a data source - | { type: 'parent-collection-variable'; path: string }; // Object representing an outer loop variable - -// Defines the collection's configuration, such as start and end indexes, and data source. -interface CollectionConfig { - start_index?: number; // The starting index for the collection - end_index?: number | ConditionDefinition; // End index; can be absolute or relative (If omitted will loop over all items) - dataSource: CollectionDataSource; // The data source (array or object reference) + | any[] + | { type: typeof DataVariableType; path: string } + | CollectionVariable; + +type CollectionConfig = { + start_index?: number; + end_index?: number | ConditionDefinition; + dataSource: CollectionDataSource; } -// Provides access to collection state variables during iteration. -interface CollectionStateVariables { - current_index: number; // Current collection index - start_index: number; // Start index - current_item: any; // Current item in the iteration - end_index: number; // End index - collection_name?: string; // Optional name of the collection - total_items: number; // Total number of items in the collection - remaining_items: number; // Remaining items in the collection +type CollectionState = { + current_index: number; + start_index: number; + current_item: any; + end_index: number; + collection_name?: string; + total_items: number; + remaining_items: number; } -// Defines the complete structure for a collection, including configuration and state variables. -interface CollectionDefinition { +type CollectionsStateMap = { + [key: string]: CollectionState; +} + +type CollectionDefinition = { type: typeof CollectionVariableType; - collection_name?: string; // Optional collection name - config: CollectionConfig; // Loop configuration details - block: ComponentDefinition; // Component definition for each iteration + collection_name?: string; + config: CollectionConfig; + block: ComponentDefinition; } -export const componentCollectionKey = 'collectionsItems'; + +export const collectionDefinitionKey = 'collectionDefinition'; +export const collectionsStateKey = 'collectionsItems'; +export const innerCollectionStateKey = 'innerCollectionState'; export default class CollectionComponent extends Component { constructor(props: CollectionDefinition & ComponentProperties, opt: ComponentOptions) { - const { collection_name, block, config } = props.collectionDefinition; - const { - start_index = 0, - end_index = Number.MAX_VALUE, - dataSource = [], - } = config; - let items: any[] = []; - switch (true) { - case isArray(dataSource): - items = dataSource; - break; - case typeof dataSource === 'object' && dataSource instanceof DataSource: - const id = dataSource.get('id')!; - const resolvedPath = opt.em.DataSources.getValue(id, []); - const keys = Object.keys(resolvedPath); - items = keys.map(key => ({ - type: DataVariableType, - path: id + '.' + key, - })); - break; - case typeof dataSource === 'object' && dataSource.type === DataVariableType: - const pathArr = dataSource.path.split('.'); - if (pathArr.length === 1) { - const resolvedPath = opt.em.DataSources.getValue(dataSource.path, []); - const keys = Object.keys(resolvedPath); - items = keys.map(key => ({ - type: DataVariableType, - path: id + '.' + key, - })); - } else { - items = opt.em.DataSources.getValue(dataSource.path, []); - } - break; - default: + const em = opt.em; + const { collection_name, block, config } = props[collectionDefinitionKey]; + if (!block) { + throw new Error('The "block" property is required in the collection definition.'); } - const components: ComponentDefinition[] = []; - const resolvedStartIndex = Math.max(0, start_index); - const resolvedEndIndex = Math.min(items.length - 1, end_index); - const item = items[resolvedStartIndex]; - let symbolMain; - const total_items = resolvedEndIndex - resolvedStartIndex + 1; - const innerMostCollectionItem = { - collection_name, - current_index: resolvedStartIndex, - current_item: item, - start_index: resolvedStartIndex, - end_index: resolvedEndIndex, - total_items: total_items, - remaining_items: total_items - (resolvedStartIndex + 1), - }; - - const allCollectionItem = { - ...props.collectionsItems, - [innerMostCollectionItem.collection_name ? innerMostCollectionItem.collection_name : 'innerMostCollectionItem']: - innerMostCollectionItem, - innerMostCollectionItem + if (!config?.dataSource) { + throw new Error('The "config.dataSource" property is required in the collection definition.'); } - const { clonedBlock, overrideKeys } = resolveBlockValues(allCollectionItem, block); - const type = opt.em.Components.getType(clonedBlock?.type || 'default'); - const model = type.model; - const component = new model(clonedBlock, opt); - for (let index = resolvedStartIndex; index <= resolvedEndIndex; index++) { + let items: any[] = getDataSourceItems(config.dataSource, em); + const components: ComponentDefinition[] = []; + const start_index = Math.max(0, config.start_index || 0); + const end_index = Math.min(items.length - 1, config.end_index || Number.MAX_VALUE); + + const total_items = end_index - start_index + 1; + let blockComponent: Component; + for (let index = start_index; index <= end_index; index++) { const item = items[index]; - const innerMostCollectionItem = { + const collectionState: CollectionState = { collection_name, current_index: index, current_item: item, - start_index: resolvedStartIndex, - end_index: resolvedEndIndex, + start_index: start_index, + end_index: end_index, total_items: total_items, remaining_items: total_items - (index + 1), }; - const allCollectionItem = { - ...props.collectionsItems, - [innerMostCollectionItem.collection_name ? innerMostCollectionItem.collection_name : 'innerMostCollectionItem']: - innerMostCollectionItem, - innerMostCollectionItem + const collectionsStateMap: CollectionsStateMap = { + ...props[collectionDefinitionKey], + ...(collection_name && { [collection_name]: collectionState }), + [innerCollectionStateKey]: collectionState, + }; + + if (index === start_index) { + const { clonedBlock } = resolveBlockValues(collectionsStateMap, block); + const type = em.Components.getType(clonedBlock?.type || 'default'); + const model = type.model; + blockComponent = new model(clonedBlock, opt); } - const cmpDefinition = getResolvedComponent(component, block, allCollectionItem, opt.em); + const cmpDefinition = resolveComponent(blockComponent!, block, collectionsStateMap, em); components.push(cmpDefinition); } @@ -136,7 +106,7 @@ export default class CollectionComponent extends Component { components: components, dropbbable: false, }; - // @ts-expect-error + // @ts-ignore super(conditionalCmptDef, opt); } @@ -145,111 +115,163 @@ export default class CollectionComponent extends Component { } } -function getResolvedComponent(component: Component, block: any, allCollectionItem: any, em: EditorModel) { - const instance = em.Components.addSymbol(component); - const { overrideKeys } = resolveBlockValues(allCollectionItem, deepCloneObject(block)); +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, + })); +} + +function resolveComponent(symbol: Component, block: ComponentDefinition, collectionsStateMap: CollectionsStateMap, em: EditorModel) { + const instance = em.Components.addSymbol(symbol); + const { resolvedCollectionValues: overrideKeys } = resolveBlockValues(collectionsStateMap, block); Object.keys(overrideKeys).length && instance!.setSymbolOverride(Object.keys(overrideKeys)); instance!.set(overrideKeys); - const children: any[] = []; + const children: ComponentDefinition[] = []; for (let index = 0; index < instance!.components().length; index++) { - const childComponent = component!.components().at(index); - const childBlock = block['components'][index]; - children.push(getResolvedComponent(childComponent, childBlock, allCollectionItem, em)); + const childComponent = symbol!.components().at(index); + const childBlock = block['components']![index]; + children.push(resolveComponent(childComponent, childBlock, collectionsStateMap, em)); } - const componentJSON = instance?.toJSON(); - const cmpDefinition = { + const componentJSON = instance!.toJSON(); + const componentDefinition: ComponentDefinition = { ...componentJSON, components: children }; - - return cmpDefinition; -} - -/** - * Deeply clones an object. - * @template T The type of the object to clone. - * @param {T} obj The object to clone. - * @returns {T} A deep clone of the object, or the original object if it's not an object or is null. Returns undefined if input is undefined. - */ -function deepCloneObject | null | undefined>(obj: T): T { - if (obj === null) return null as T; - if (obj === undefined) return undefined as T; - if (typeof obj !== 'object' || Array.isArray(obj)) { - return obj; // Return primitives directly - } - - const clonedObj: Record = {}; - - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - clonedObj[key] = deepCloneObject(obj[key]); - } - } - return clonedObj as T; + return componentDefinition; } -function resolveBlockValues(context: any, block: any) { - const { innerMostCollectionItem } = context; +function resolveBlockValues(collectionsStateMap: CollectionsStateMap, block: ObjectAny) { const clonedBlock = deepCloneObject(block); - const overrideKeys: ObjectAny = {}; + const resolvedCollectionValues: ObjectAny = {}; if (typeof clonedBlock === 'object') { const blockKeys = Object.keys(clonedBlock); for (const key of blockKeys) { let blockValue = clonedBlock[key]; - if (key === 'collectionDefinition') continue; - let shouldBeOverridden = false; + if (key === collectionDefinitionKey) continue; + let hasCollectionVariable = false; if (typeof blockValue === 'object') { - const collectionItem = blockValue.collection_name - ? context[blockValue.collection_name] - : innerMostCollectionItem; - if (blockValue.type === 'parent-collection-variable') { + const isCollectionVariable = blockValue.type === 'parent-collection-variable'; + if (isCollectionVariable) { + const { + variable_type, + collection_name = 'innerMostCollectionItem', + path = '' + } = blockValue as CollectionVariable; + const collectionItem = collectionsStateMap[collection_name]; if (!collectionItem) { throw new Error( - `Collection not found: ${blockValue.collection_name || 'default collection'}` + `Collection not found: ${collection_name}` ); } - - if (blockValue.variable_type === 'current_item' && collectionItem.current_item.type === DataVariableType) { - const path = collectionItem.current_item.path ? `${collectionItem.current_item.path}.${blockValue.path}` : blockValue.path; - clonedBlock[key] = { - ...collectionItem.current_item, - path - }; - } else { - clonedBlock[key] = collectionItem[blockValue.variable_type]; + if (!variable_type) { + throw new Error( + `Missing collection variable type for collection: ${collection_name}` + ); } + clonedBlock[key] = resolveCurrentItem(variable_type, collectionItem, path); - shouldBeOverridden = true; + hasCollectionVariable = true; } else if (Array.isArray(blockValue)) { - // Resolve each item in the array clonedBlock[key] = blockValue.map((arrayItem: any) => { - const { clonedBlock, overrideKeys: itemOverrideKeys } = resolveBlockValues(context, arrayItem) - if (Object.keys(itemOverrideKeys).length > 0) { - shouldBeOverridden = true; + const { clonedBlock, resolvedCollectionValues: itemOverrideKeys } = resolveBlockValues(collectionsStateMap, arrayItem) + if (!isEmptyObject(itemOverrideKeys)) { + hasCollectionVariable = true; } return typeof arrayItem === 'object' ? clonedBlock : arrayItem }); } else { - const { clonedBlock, overrideKeys: itemOverrideKeys } = resolveBlockValues(context, blockValue); + const { clonedBlock, resolvedCollectionValues: itemOverrideKeys } = resolveBlockValues(collectionsStateMap, blockValue); clonedBlock[key] = clonedBlock; - if (Object.keys(itemOverrideKeys).length > 0) { - shouldBeOverridden = true; + if (!isEmptyObject(itemOverrideKeys)) { + hasCollectionVariable = true; } } - if (shouldBeOverridden && key !== 'components') { - overrideKeys[key] = clonedBlock[key] + if (hasCollectionVariable && key !== 'components') { + resolvedCollectionValues[key] = clonedBlock[key] } } } } - return { clonedBlock, overrideKeys }; + return { clonedBlock, resolvedCollectionValues }; +} + +function resolveCurrentItem(variableType: CollectionVariable['variable_type'], collectionItem: CollectionState, path: string) { + const valueIsDataVariable = collectionItem.current_item?.type === DataVariableType; + if (variableType === 'current_item' && valueIsDataVariable) { + const resolvedPath = collectionItem.current_item.path + ? `${collectionItem.current_item.path}.${path}` + : path; + return { + ...collectionItem.current_item, + path: resolvedPath, + }; + } + return collectionItem[variableType]; +} + + +function isEmptyObject(itemOverrideKeys: ObjectAny) { + return Object.keys(itemOverrideKeys).length === 0; +} + +/** + * Deeply clones an object. + * @template T The type of the object to clone. + * @param {T} obj The object to clone. + * @returns {T} A deep clone of the object, or the original object if it's not an object or is null. Returns undefined if input is undefined. + */ +function deepCloneObject | null | undefined>(obj: T): T { + if (obj === null) return null as T; + if (obj === undefined) return undefined as T; + if (typeof obj !== 'object' || Array.isArray(obj)) { + return obj; // Return primitives directly + } + + const clonedObj: Record = {}; + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + clonedObj[key] = deepCloneObject(obj[key]); + } + } + + return clonedObj as T; } diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index bc61b682f..2ee4a57cd 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -55,7 +55,6 @@ import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import { ConditionalVariableType, DataCondition } from '../../data_sources/model/conditional_variables/DataCondition'; import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; import { DynamicValueDefinition } from '../../data_sources/types'; -import { componentCollectionKey } from '../../data_sources/model/collection_component/CollectionComponent'; export interface IComponent extends ExtractMethods { }