|
|
|
@ -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<T extends Record<string, any> | 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<string, any> = {}; |
|
|
|
|
|
|
|
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<T extends Record<string, any> | 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<string, any> = {}; |
|
|
|
|
|
|
|
for (const key in obj) { |
|
|
|
if (Object.prototype.hasOwnProperty.call(obj, key)) { |
|
|
|
clonedObj[key] = deepCloneObject(obj[key]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return clonedObj as T; |
|
|
|
} |
|
|
|
|