Browse Source

Refactor collections

pull/6359/head
mohamedsalem401 1 year ago
parent
commit
a1ce18a159
  1. 342
      packages/core/src/data_sources/model/collection_component/CollectionComponent.ts
  2. 1
      packages/core/src/dom_components/model/Component.ts

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

1
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<Component> { }

Loading…
Cancel
Save