Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add collection components #6359

Open
wants to merge 91 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
21a08cc
Add collection component
mohamedsalem401 Dec 17, 2024
d49cd39
Add resolving collection variables
mohamedsalem401 Dec 17, 2024
4aee355
Allow data-variable paths as a collection's datasource
mohamedsalem401 Dec 17, 2024
7294394
make collection items undraggable and undroppable
mohamedsalem401 Dec 17, 2024
17b84fb
Add many info to the currentItem
mohamedsalem401 Dec 18, 2024
7db4187
fix passing a datasource as collection's datasource
mohamedsalem401 Dec 18, 2024
09da446
Allow nested collections
mohamedsalem401 Dec 19, 2024
2f6f13c
Fix collection iteration values
mohamedsalem401 Dec 19, 2024
9d142f4
Fix getting current item
mohamedsalem401 Dec 19, 2024
01e81f4
Use values from the innermost collection instead of the outermost one.
mohamedsalem401 Dec 19, 2024
4a22be0
Throw an error when using the name of a non-existent collection.
mohamedsalem401 Dec 19, 2024
72539f1
refactor componentCollectionKey
mohamedsalem401 Dec 19, 2024
ddab929
remove circular dependancy
mohamedsalem401 Dec 19, 2024
d631ca7
Add start and end index for collections
mohamedsalem401 Dec 19, 2024
a36aa30
Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into colle…
mohamedsalem401 Dec 23, 2024
050a4cd
use symbols for collection components
mohamedsalem401 Dec 25, 2024
b142b14
refactor collection symbols
mohamedsalem401 Dec 25, 2024
a1ce18a
Refactor collections
mohamedsalem401 Dec 25, 2024
ed8ecbc
Refactor and format
mohamedsalem401 Dec 25, 2024
a1fea6e
Refactor collection keys
mohamedsalem401 Dec 25, 2024
8c734e2
Cleanup collectionStateMap
mohamedsalem401 Dec 25, 2024
c6f4ad4
Only use 1 symbol to be used for each item in the collections
mohamedsalem401 Dec 25, 2024
8fc9481
Fix path for static datasource
mohamedsalem401 Dec 30, 2024
16f15b8
Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into colle…
mohamedsalem401 Dec 30, 2024
9837116
Add collection variables
mohamedsalem401 Jan 3, 2025
721f7b0
Refactor dynamic component watcher
mohamedsalem401 Jan 3, 2025
c123c18
Refactor mehods for dynamic value watchers
mohamedsalem401 Jan 3, 2025
6aa7244
Bind watcher to component in the constructor
mohamedsalem401 Jan 3, 2025
369caed
Move ovveriding collection variables to component watcher
mohamedsalem401 Jan 3, 2025
54320d4
Add collection component stringfication
mohamedsalem401 Jan 3, 2025
fe4b09f
Refactor getting collection items
mohamedsalem401 Jan 3, 2025
c425c7e
Update collection items on datasource updates
mohamedsalem401 Jan 6, 2025
e50fe16
Console errors instead of raising errors for collection component
mohamedsalem401 Jan 6, 2025
bc3e65a
Refactor watch dynamic datasource
mohamedsalem401 Jan 6, 2025
e2d4bbe
Refactor CollectionStateVariableType
mohamedsalem401 Jan 6, 2025
33f3129
Fix zero end_index issue
mohamedsalem401 Jan 6, 2025
9c094dd
Collection tests
mohamedsalem401 Jan 6, 2025
d02852e
Don't Add collection symbols to the list of global symbols
mohamedsalem401 Jan 7, 2025
782549b
Refactor resolving collection items
mohamedsalem401 Jan 7, 2025
fcc8c45
Fix collection items traits
mohamedsalem401 Jan 7, 2025
4c6d1e6
Fix droppable for collection component
mohamedsalem401 Jan 7, 2025
6432a9c
Log error if no definition is passed to collection component
mohamedsalem401 Jan 7, 2025
7d967d2
Update tests for collection symbols
mohamedsalem401 Jan 7, 2025
5e472c9
Fix collection variables not listening correctly
mohamedsalem401 Jan 7, 2025
3f0588d
Refactor resolving collection variables
mohamedsalem401 Jan 7, 2025
aaa2481
Fix updating collection symbols overrides in runtime
mohamedsalem401 Jan 7, 2025
33da8bf
Refactor collectionsStateMap propagation
mohamedsalem401 Jan 8, 2025
29d983e
Fix collection items propagation
mohamedsalem401 Jan 8, 2025
5587e44
Refactor setting dynamic attributes
mohamedsalem401 Jan 8, 2025
d2078d2
Edit properties propagation logic
mohamedsalem401 Jan 8, 2025
fc01907
Fix Collection props and attributes propagation
mohamedsalem401 Jan 9, 2025
5bd74d5
Update collection attributes tests
mohamedsalem401 Jan 9, 2025
8c5800f
Update collection component serialization tests
mohamedsalem401 Jan 10, 2025
b7d2793
Fix falsy value being treated as undefined
mohamedsalem401 Jan 10, 2025
697c11a
Udpate tests for Diffirent Collection variable types
mohamedsalem401 Jan 10, 2025
407cc89
Add tests for saving and loading collection components
mohamedsalem401 Jan 10, 2025
7b0ca1f
Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into colle…
mohamedsalem401 Jan 10, 2025
de7a712
Make collection items undraggable
mohamedsalem401 Jan 10, 2025
0167fce
Change collection component definition options to camel case
mohamedsalem401 Jan 14, 2025
a9fec3d
Refactor propagation of collection map state
mohamedsalem401 Jan 14, 2025
6710b8f
Refactor collection type
mohamedsalem401 Jan 14, 2025
550499e
Delete null assertion
mohamedsalem401 Jan 14, 2025
d4fe2c3
Replace types with interfaces
mohamedsalem401 Jan 15, 2025
b9ebb75
Refactor keyIsCollectionItem
mohamedsalem401 Jan 15, 2025
e6f4a6e
Add missing opts in setId method
mohamedsalem401 Jan 15, 2025
535eba0
Remove console.log
mohamedsalem401 Jan 15, 2025
a6bb3e9
Replace `content` property for collection component testing
mohamedsalem401 Jan 15, 2025
5851eec
Fix collection component serialization tests
mohamedsalem401 Jan 15, 2025
68707db
Rename collection to DataCollection
mohamedsalem401 Jan 15, 2025
19007fa
Add collection variable component
mohamedsalem401 Jan 16, 2025
fedde43
Exclude "components" property from being dynamic
mohamedsalem401 Jan 16, 2025
0aeef2d
Improve DataCollectionVariable serialization
mohamedsalem401 Jan 16, 2025
ea01b76
Fix logic for updating data collection variable
mohamedsalem401 Jan 16, 2025
c3cd2a0
Fix tests
mohamedsalem401 Jan 16, 2025
2ad06d1
Change collection definition properties
mohamedsalem401 Jan 16, 2025
4297f12
Add more tests for Collection variable components
mohamedsalem401 Jan 16, 2025
cf774bb
Format
mohamedsalem401 Jan 16, 2025
0a15b69
Fix lint
mohamedsalem401 Jan 16, 2025
49b25ff
Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into colle…
mohamedsalem401 Jan 16, 2025
9847e0c
Refactor collectionStateMap propagation logic
mohamedsalem401 Jan 19, 2025
d293d59
Make collectionId a required field
mohamedsalem401 Jan 20, 2025
3d3e9e1
Tests for nested collection components
mohamedsalem401 Jan 20, 2025
d5c64ff
Cleanup
mohamedsalem401 Jan 20, 2025
3c6e72f
Cleanup
mohamedsalem401 Jan 20, 2025
374dbc4
Fix collection component symbols
mohamedsalem401 Jan 20, 2025
1f00f16
Add tests for adding and removing records
mohamedsalem401 Jan 21, 2025
39d6de8
Format
mohamedsalem401 Jan 21, 2025
979d035
Fix updating datasource
mohamedsalem401 Jan 21, 2025
e9b46e7
Fix nested symbols not working on collection component
mohamedsalem401 Jan 21, 2025
c3172a6
Fix syncing collection items for nested collections
mohamedsalem401 Jan 21, 2025
dc13e11
Refactor
mohamedsalem401 Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 './data_collection/constants';
import DataCollectionVariable from './data_collection/DataCollectionVariable';

export interface DynamicVariableListenerManagerOptions {
em: EditorModel;
Expand Down Expand Up @@ -41,6 +43,9 @@ export default class DynamicVariableListenerManager {
const type = dynamicVariable.get('type');
let dataListeners: DataVariableListener[] = [];
switch (type) {
case CollectionVariableType:
dataListeners = this.listenToDataCollectionVariable(dynamicVariable as DataCollectionVariable);
break;
case DataVariableType:
dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em);
break;
Expand Down Expand Up @@ -77,6 +82,10 @@ export default class DynamicVariableListenerManager {
return dataListeners;
}

private listenToDataCollectionVariable(dataVariable: DataCollectionVariable) {
return [{ obj: dataVariable, event: 'change:value' }];
}

private removeListeners() {
this.dataListeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange));
this.dataListeners = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
import DataVariable, { DataVariableType } from '../DataVariable';
import { isArray } from 'underscore';
import Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions } 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 {
ComponentDataCollectionDefinition,
DataCollectionConfig,
DataCollectionDefinition,
DataCollectionState,
DataCollectionStateMap,
} from './types';
import {
keyCollectionDefinition,
keyCollectionsStateMap,
CollectionComponentType,
keyIsCollectionItem,
} from './constants';
import DynamicVariableListenerManager from '../DataVariableListenerManager';

export default class ComponentDataCollection extends Component {
constructor(props: ComponentDataCollectionDefinition, opt: ComponentOptions) {
const collectionDef = props[keyCollectionDefinition];
if (opt.forCloning) {
// If we are cloning, leave setting the collection items to the main symbol collection
return super(props as any, opt) as unknown as ComponentDataCollection;
}

const em = opt.em;
const cmp: ComponentDataCollection = super(
{
...props,
components: undefined,
droppable: false,
} as any,
opt,
) as unknown as ComponentDataCollection;

if (!collectionDef) {
em.logError('missing collection definition');

return cmp;
}

const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as DataCollectionStateMap;

const components: Component[] = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt);
cmp.components(components);

if (this.hasDynamicDataSource()) {
this.watchDataSource(em, collectionDef, parentCollectionStateMap, opt);
}

return cmp;
}

static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === CollectionComponentType;
}

hasDynamicDataSource() {
const dataSource = this.get(keyCollectionDefinition).collectionConfig.dataSource;
return typeof dataSource === 'object' && dataSource.type === DataVariableType;
}

toJSON(opts?: ObjectAny) {
const json = super.toJSON.call(this, opts) as ComponentDataCollectionDefinition;

const firstChild = this.getBlockDefinition();
json[keyCollectionDefinition].componentDef = 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,
collectionDef: DataCollectionDefinition,
parentCollectionStateMap: DataCollectionStateMap,
opt: ComponentOptions,
) {
const path = this.get(keyCollectionDefinition).collectionConfig.dataSource?.path;
const dataVariable = new DataVariable(
{
type: DataVariableType,
path,
},
{ em },
);

new DynamicVariableListenerManager({
em: em,
dataVariable,
updateValueFromDataVariable: () => {
const collectionItems = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt);
this.components().reset(collectionItems);
},
});
}
}

function getCollectionItems(
em: EditorModel,
collectionDef: DataCollectionDefinition,
parentCollectionStateMap: DataCollectionStateMap,
opt: ComponentOptions,
) {
const { componentDef, collectionConfig } = collectionDef;
const result = validateCollectionConfig(collectionConfig, componentDef, em);
if (!result) {
return [];
}

const collectionId = collectionConfig.collectionId;

const components: Component[] = [];

let items: any[] = getDataSourceItems(collectionConfig.dataSource, em);
const startIndex = Math.max(0, collectionConfig.startIndex || 0);
const endIndex = Math.min(
items.length - 1,
collectionConfig.endIndex !== undefined ? collectionConfig.endIndex : Number.MAX_VALUE,
);

const totalItems = endIndex - startIndex + 1;
let blockSymbolMain: Component;
for (let index = startIndex; index <= endIndex; index++) {
const item = items[index];
const collectionState: DataCollectionState = {
collectionId,
currentIndex: index,
currentItem: item,
startIndex: startIndex,
endIndex: endIndex,
totalItems: totalItems,
remainingItems: totalItems - (index + 1),
};

if (parentCollectionStateMap[collectionId]) {
em.logError(
`The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`,
);
return [];
}

const collectionsStateMap: DataCollectionStateMap = {
...parentCollectionStateMap,
[collectionId]: collectionState,
};

if (index === startIndex) {
const componentType = (componentDef?.type as string) || 'default';
let type = em.Components.getType(componentType);
// Handle the case where the type is not found
if (!type) {
em.logWarning(`Component type "${componentType}" not found. Using default type.`);
const defaultType = em.Components.getType('default');
if (!defaultType) {
throw new Error('Default component type not found. Cannot proceed.');
}
type = defaultType;
}
const model = type.model;

blockSymbolMain = new model(
{
...componentDef,
draggable: false,
},
opt,
);
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(blockSymbolMain);
}
const instance = blockSymbolMain!.clone({ symbol: true });
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance);

components.push(instance);
}

return components;
}

function setCollectionStateMapAndPropagate(
collectionsStateMap: DataCollectionStateMap,
collectionId: string | undefined,
) {
return (model: Component) => {
// Set the collectionStateMap on the current model
setCollectionStateMap(collectionsStateMap)(model);

// Listener function for the 'add' event
const addListener = (component: Component) => {
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(component);
};

// Generate a unique listener key
const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`;

// Add the 'add' listener if not already in the listeners array
if (!model.collectionStateListeners.includes(listenerKey)) {
model.listenTo(model.components(), 'add', addListener);
model.collectionStateListeners.push(listenerKey);

// Add a 'remove' listener to clean up
const removeListener = () => {
model.stopListening(model.components(), 'add', addListener); // Remove the 'add' listener
model.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); // Remove the change listener
const index = model.collectionStateListeners.indexOf(listenerKey);
if (index > -1) {
model.collectionStateListeners.splice(index, 1); // Remove the listener key
}
};

model.listenTo(model.components(), 'remove', removeListener);
}

// Recursively apply to all child components
model
.components()
?.toArray()
.forEach((component: Component) => {
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(component);
});

// Listen for changes in the collectionStateMap and propagate to children
model.on(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange);
};
}

function handleCollectionStateMapChange(this: Component) {
const updatedCollectionsStateMap = this.get(keyCollectionsStateMap);
this.components()
?.toArray()
.forEach((component: Component) => {
setCollectionStateMap(updatedCollectionsStateMap)(component);
});
}

function logErrorIfMissing(property: any, propertyPath: string, em: EditorModel) {
if (!property) {
em.logError(`The "${propertyPath}" property is required in the collection definition.`);
return false;
}
return true;
}

function validateCollectionConfig(
collectionConfig: DataCollectionConfig,
componentDef: ComponentDefinition,
em: EditorModel,
) {
const validations = [
{ property: collectionConfig, propertyPath: 'collectionConfig' },
{ property: componentDef, propertyPath: 'componentDef' },
{ property: collectionConfig?.collectionId, propertyPath: 'collectionConfig.collectionId' },
{ property: collectionConfig?.dataSource, propertyPath: 'collectionConfig.dataSource' },
];

for (const { property, propertyPath } of validations) {
if (!logErrorIfMissing(property, propertyPath, em)) {
return [];
}
}

return true;
}

function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) {
return (cmp: Component) => {
cmp.set(keyIsCollectionItem, true);
const updatedCollectionStateMap = {
...cmp.get(keyCollectionsStateMap),
...collectionsStateMap,
};
cmp.set(keyCollectionsStateMap, updatedCollectionStateMap);
cmp.componentDVListener.updateCollectionStateMap(updatedCollectionStateMap);
};
}

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,
}));
}
Loading
Loading