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 { isArray } from 'underscore';
import Component from '../../../dom_components/model/Component'; import Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins'; import { toLowerCase } from '../../../utils/mixins';
import { ConditionDefinition } from '../conditional_variables/DataCondition'; import { ConditionDefinition } from '../conditional_variables/DataCondition';
import DataSource from '../DataSource'; import DataSource from '../DataSource';
import { DataVariableType } from '../DataVariable';
import { ObjectAny } from '../../../common'; import { ObjectAny } from '../../../common';
import EditorModel from '../../../editor/model/Editor'; import EditorModel from '../../../editor/model/Editor';
export const CollectionVariableType = 'collection-component'; 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 = type CollectionDataSource =
| any[] // Direct array | any[]
| { type: 'datasource-variable'; path: string } // Object representing a data source | { type: typeof DataVariableType; path: string }
| { type: 'parent-collection-variable'; path: string }; // Object representing an outer loop variable | CollectionVariable;
// Defines the collection's configuration, such as start and end indexes, and data source. type CollectionConfig = {
interface CollectionConfig { start_index?: number;
start_index?: number; // The starting index for the collection end_index?: number | ConditionDefinition;
end_index?: number | ConditionDefinition; // End index; can be absolute or relative (If omitted will loop over all items) dataSource: CollectionDataSource;
dataSource: CollectionDataSource; // The data source (array or object reference)
} }
// Provides access to collection state variables during iteration. type CollectionState = {
interface CollectionStateVariables { current_index: number;
current_index: number; // Current collection index start_index: number;
start_index: number; // Start index current_item: any;
current_item: any; // Current item in the iteration end_index: number;
end_index: number; // End index collection_name?: string;
collection_name?: string; // Optional name of the collection total_items: number;
total_items: number; // Total number of items in the collection remaining_items: number;
remaining_items: number; // Remaining items in the collection
} }
// Defines the complete structure for a collection, including configuration and state variables. type CollectionsStateMap = {
interface CollectionDefinition { [key: string]: CollectionState;
}
type CollectionDefinition = {
type: typeof CollectionVariableType; type: typeof CollectionVariableType;
collection_name?: string; // Optional collection name collection_name?: string;
config: CollectionConfig; // Loop configuration details config: CollectionConfig;
block: ComponentDefinition; // Component definition for each iteration block: ComponentDefinition;
} }
export const componentCollectionKey = 'collectionsItems';
export const collectionDefinitionKey = 'collectionDefinition';
export const collectionsStateKey = 'collectionsItems';
export const innerCollectionStateKey = 'innerCollectionState';
export default class CollectionComponent extends Component { export default class CollectionComponent extends Component {
constructor(props: CollectionDefinition & ComponentProperties, opt: ComponentOptions) { constructor(props: CollectionDefinition & ComponentProperties, opt: ComponentOptions) {
const { collection_name, block, config } = props.collectionDefinition; const em = opt.em;
const { const { collection_name, block, config } = props[collectionDefinitionKey];
start_index = 0, if (!block) {
end_index = Number.MAX_VALUE, throw new Error('The "block" property is required in the collection definition.');
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 components: ComponentDefinition[] = []; if (!config?.dataSource) {
const resolvedStartIndex = Math.max(0, start_index); throw new Error('The "config.dataSource" property is required in the collection definition.');
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
} }
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 item = items[index];
const innerMostCollectionItem = { const collectionState: CollectionState = {
collection_name, collection_name,
current_index: index, current_index: index,
current_item: item, current_item: item,
start_index: resolvedStartIndex, start_index: start_index,
end_index: resolvedEndIndex, end_index: end_index,
total_items: total_items, total_items: total_items,
remaining_items: total_items - (index + 1), remaining_items: total_items - (index + 1),
}; };
const allCollectionItem = { const collectionsStateMap: CollectionsStateMap = {
...props.collectionsItems, ...props[collectionDefinitionKey],
[innerMostCollectionItem.collection_name ? innerMostCollectionItem.collection_name : 'innerMostCollectionItem']: ...(collection_name && { [collection_name]: collectionState }),
innerMostCollectionItem, [innerCollectionStateKey]: collectionState,
innerMostCollectionItem };
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); components.push(cmpDefinition);
} }
@ -136,7 +106,7 @@ export default class CollectionComponent extends Component {
components: components, components: components,
dropbbable: false, dropbbable: false,
}; };
// @ts-expect-error // @ts-ignore
super(conditionalCmptDef, opt); super(conditionalCmptDef, opt);
} }
@ -145,111 +115,163 @@ export default class CollectionComponent extends Component {
} }
} }
function getResolvedComponent(component: Component, block: any, allCollectionItem: any, em: EditorModel) { function getDataSourceItems(dataSource: any, em: EditorModel) {
const instance = em.Components.addSymbol(component); let items: any[] = [];
const { overrideKeys } = resolveBlockValues(allCollectionItem, deepCloneObject(block)); 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)); Object.keys(overrideKeys).length && instance!.setSymbolOverride(Object.keys(overrideKeys));
instance!.set(overrideKeys); instance!.set(overrideKeys);
const children: any[] = []; const children: ComponentDefinition[] = [];
for (let index = 0; index < instance!.components().length; index++) { for (let index = 0; index < instance!.components().length; index++) {
const childComponent = component!.components().at(index); const childComponent = symbol!.components().at(index);
const childBlock = block['components'][index]; const childBlock = block['components']![index];
children.push(getResolvedComponent(childComponent, childBlock, allCollectionItem, em)); children.push(resolveComponent(childComponent, childBlock, collectionsStateMap, em));
} }
const componentJSON = instance?.toJSON(); const componentJSON = instance!.toJSON();
const cmpDefinition = { const componentDefinition: ComponentDefinition = {
...componentJSON, ...componentJSON,
components: children 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) { function resolveBlockValues(collectionsStateMap: CollectionsStateMap, block: ObjectAny) {
const { innerMostCollectionItem } = context;
const clonedBlock = deepCloneObject(block); const clonedBlock = deepCloneObject(block);
const overrideKeys: ObjectAny = {}; const resolvedCollectionValues: ObjectAny = {};
if (typeof clonedBlock === 'object') { if (typeof clonedBlock === 'object') {
const blockKeys = Object.keys(clonedBlock); const blockKeys = Object.keys(clonedBlock);
for (const key of blockKeys) { for (const key of blockKeys) {
let blockValue = clonedBlock[key]; let blockValue = clonedBlock[key];
if (key === 'collectionDefinition') continue; if (key === collectionDefinitionKey) continue;
let shouldBeOverridden = false; let hasCollectionVariable = false;
if (typeof blockValue === 'object') { if (typeof blockValue === 'object') {
const collectionItem = blockValue.collection_name const isCollectionVariable = blockValue.type === 'parent-collection-variable';
? context[blockValue.collection_name] if (isCollectionVariable) {
: innerMostCollectionItem; const {
if (blockValue.type === 'parent-collection-variable') { variable_type,
collection_name = 'innerMostCollectionItem',
path = ''
} = blockValue as CollectionVariable;
const collectionItem = collectionsStateMap[collection_name];
if (!collectionItem) { if (!collectionItem) {
throw new Error( throw new Error(
`Collection not found: ${blockValue.collection_name || 'default collection'}` `Collection not found: ${collection_name}`
); );
} }
if (!variable_type) {
if (blockValue.variable_type === 'current_item' && collectionItem.current_item.type === DataVariableType) { throw new Error(
const path = collectionItem.current_item.path ? `${collectionItem.current_item.path}.${blockValue.path}` : blockValue.path; `Missing collection variable type for collection: ${collection_name}`
clonedBlock[key] = { );
...collectionItem.current_item,
path
};
} else {
clonedBlock[key] = collectionItem[blockValue.variable_type];
} }
clonedBlock[key] = resolveCurrentItem(variable_type, collectionItem, path);
shouldBeOverridden = true; hasCollectionVariable = true;
} else if (Array.isArray(blockValue)) { } else if (Array.isArray(blockValue)) {
// Resolve each item in the array
clonedBlock[key] = blockValue.map((arrayItem: any) => { clonedBlock[key] = blockValue.map((arrayItem: any) => {
const { clonedBlock, overrideKeys: itemOverrideKeys } = resolveBlockValues(context, arrayItem) const { clonedBlock, resolvedCollectionValues: itemOverrideKeys } = resolveBlockValues(collectionsStateMap, arrayItem)
if (Object.keys(itemOverrideKeys).length > 0) { if (!isEmptyObject(itemOverrideKeys)) {
shouldBeOverridden = true; hasCollectionVariable = true;
} }
return typeof arrayItem === 'object' ? clonedBlock : arrayItem return typeof arrayItem === 'object' ? clonedBlock : arrayItem
}); });
} else { } else {
const { clonedBlock, overrideKeys: itemOverrideKeys } = resolveBlockValues(context, blockValue); const { clonedBlock, resolvedCollectionValues: itemOverrideKeys } = resolveBlockValues(collectionsStateMap, blockValue);
clonedBlock[key] = clonedBlock; clonedBlock[key] = clonedBlock;
if (Object.keys(itemOverrideKeys).length > 0) { if (!isEmptyObject(itemOverrideKeys)) {
shouldBeOverridden = true; hasCollectionVariable = true;
} }
} }
if (shouldBeOverridden && key !== 'components') { if (hasCollectionVariable && key !== 'components') {
overrideKeys[key] = clonedBlock[key] 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 { ConditionalVariableType, DataCondition } from '../../data_sources/model/conditional_variables/DataCondition';
import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils';
import { DynamicValueDefinition } from '../../data_sources/types'; import { DynamicValueDefinition } from '../../data_sources/types';
import { componentCollectionKey } from '../../data_sources/model/collection_component/CollectionComponent';
export interface IComponent extends ExtractMethods<Component> { } export interface IComponent extends ExtractMethods<Component> { }

Loading…
Cancel
Save