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 84 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
84 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
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import { DynamicValue } from '../types';
import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition';
import ComponentDataVariable from './ComponentDataVariable';
import { CollectionVariableType } from './collection_component/constants';
import CollectionVariable from './collection_component/CollectionVariable';

export interface DynamicVariableListenerManagerOptions {
em: EditorModel;
Expand Down Expand Up @@ -41,6 +43,13 @@
const type = dynamicVariable.get('type');
let dataListeners: DataVariableListener[] = [];
switch (type) {
case CollectionVariableType:
const collectionVariable = dynamicVariable as CollectionVariable;

Check failure on line 47 in packages/core/src/data_sources/model/DataVariableListenerManager.ts

View workflow job for this annotation

GitHub Actions / quality-checks

Unexpected lexical declaration in case block
if (collectionVariable.hasDynamicValue()) {
dataListeners = this.listenToDataVariable(collectionVariable.dataVariable!, em);
}

break;
case DataVariableType:
dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import DataVariable, { DataVariableType } from './../DataVariable';
import { isArray } from 'underscore';
import Component, { keySymbol, keySymbolOvrd, keySymbols } from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions, ComponentProperties } 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 { keyCollectionsStateMap } from '../../../dom_components/model/Component';
import { CollectionComponentDefinition, CollectionDefinition, CollectionState, CollectionsStateMap } from './types';
import { keyCollectionDefinition, keyInnerCollectionState, CollectionComponentType } from './constants';
import DynamicVariableListenerManager from '../DataVariableListenerManager';
import Components from '../../../dom_components/model/Components';

export default class CollectionComponent extends Component {
constructor(props: CollectionComponentDefinition, opt: ComponentOptions) {
const em = opt.em;
// @ts-ignore
const cmp: CollectionComponent = super(
// @ts-ignore
{
...props,
components: undefined,
droppable: false,
},
opt,
);

const collectionDefinition = props[keyCollectionDefinition];
if (!collectionDefinition) {
em.logError('missing collection definition');

return cmp;
}

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

const components: Component[] = getCollectionItems(em, collectionDefinition, parentCollectionStateMap, opt);

if (this.hasDynamicDataSource()) {
this.watchDataSource(em, collectionDefinition, parentCollectionStateMap, opt);
}
cmp.components(components);

return cmp;
}

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

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

toJSON(opts?: ObjectAny) {
const json = super.toJSON(opts) as CollectionComponentDefinition;

const firstChild = this.getBlockDefinition();
json[keyCollectionDefinition].block = 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,
collectionDefinition: CollectionDefinition,
parentCollectionStateMap: CollectionsStateMap,
opt: ComponentOptions,
) {
const path = this.get(keyCollectionDefinition).config.dataSource?.path;
const dataVariable = new DataVariable(
{
type: DataVariableType,
path,
},
{ em },
);
new DynamicVariableListenerManager({
em: em,
dataVariable,
updateValueFromDataVariable: () => {
const collectionItems = getCollectionItems(em, collectionDefinition, parentCollectionStateMap, opt);
this.components(collectionItems);
},
});
}
}

function getCollectionItems(
em: EditorModel,
collectionDefinition: CollectionDefinition,
parentCollectionStateMap: CollectionsStateMap,
opt: ComponentOptions,
) {
const { collection_name, block, config } = collectionDefinition;
if (!block) {
em.logError('The "block" property is required in the collection definition.');
return [];
}

if (!config?.dataSource) {
em.logError('The "config.dataSource" property is required in the collection definition.');
return [];
}

const components: Component[] = [];

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

const total_items = end_index - start_index + 1;
let blockSymbolMain: Component;
for (let index = start_index; index <= end_index; index++) {
const item = items[index];
const collectionState: CollectionState = {
collection_name,
current_index: index,
current_item: item,
start_index: start_index,
end_index: end_index,
total_items: total_items,
remaining_items: total_items - (index + 1),
};

const collectionsStateMap: CollectionsStateMap = {
...parentCollectionStateMap,
...(collection_name && { [collection_name]: collectionState }),
[keyInnerCollectionState]: collectionState,
};

if (index === start_index) {
// @ts-ignore
const type = em.Components.getType(block?.type || 'default');
const model = type.model;

blockSymbolMain = new model(
{
...block,
[keyCollectionsStateMap]: collectionsStateMap,
isCollectionItem: true,
draggable: false,
},
opt,
);
blockSymbolMain!.setSymbolOverride([keyCollectionsStateMap]);
}
blockSymbolMain!.set(keyCollectionsStateMap, collectionsStateMap);
const instance = blockSymbolMain!.clone({ symbol: true });

components.push(instance);
}

return components;
}

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')!;

Check failure on line 175 in packages/core/src/data_sources/model/collection_component/CollectionComponent.ts

View workflow job for this annotation

GitHub Actions / quality-checks

Unexpected lexical declaration in case block
items = listDataSourceVariables(id, em);
break;
case typeof dataSource === 'object' && dataSource.type === DataVariableType:
const isDataSourceId = dataSource.path.split('.').length === 1;

Check failure on line 179 in packages/core/src/data_sources/model/collection_component/CollectionComponent.ts

View workflow job for this annotation

GitHub Actions / quality-checks

Unexpected lexical declaration in case block
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,
}));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ComponentView from '../../../dom_components/view/ComponentView';
import CollectionComponent from './CollectionComponent';

export default class CollectionComponentView extends ComponentView<CollectionComponent> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { CollectionVariableDefinition } from '../../../../test/specs/dom_components/model/ComponentTypes';
import { Model } from '../../../common';
import EditorModel from '../../../editor/model/Editor';
import DataVariable, { DataVariableType } from '../DataVariable';
import { keyInnerCollectionState } from './constants';
import { CollectionState, CollectionsStateMap } from './types';

export default class CollectionVariable extends Model<CollectionVariableDefinition> {
em: EditorModel;
collectionsStateMap: CollectionsStateMap;
dataVariable?: DataVariable;

constructor(
attrs: CollectionVariableDefinition,
options: {
em: EditorModel;
collectionsStateMap: CollectionsStateMap;
},
) {
super(attrs, options);
this.em = options.em;
this.collectionsStateMap = options.collectionsStateMap;

this.updateDataVariable();
}

hasDynamicValue() {
return !!this.dataVariable;
}

getDataValue() {
const { resolvedValue } = this.updateDataVariable();

if (resolvedValue?.type === DataVariableType) {
return this.dataVariable!.getDataValue();
}
return resolvedValue;
}

private updateDataVariable() {
const resolvedValue = resolveCollectionVariable(
this.attributes as CollectionVariableDefinition,
this.collectionsStateMap,
this.em,
);

let dataVariable;
if (resolvedValue?.type === DataVariableType) {
dataVariable = new DataVariable(resolvedValue, { em: this.em });
this.dataVariable = dataVariable;
}

return { resolvedValue, dataVariable };
}

destroy() {
return this.dataVariable?.destroy?.() || super.destroy();
}
}

function resolveCollectionVariable(
collectionVariableDefinition: CollectionVariableDefinition,
collectionsStateMap: CollectionsStateMap,
em: EditorModel,
) {
const { collection_name = keyInnerCollectionState, variable_type, path } = collectionVariableDefinition;
const collectionItem = collectionsStateMap[collection_name];

if (!collectionItem) {
em.logError(`Collection not found: ${collection_name}`);
return '';
}

if (!variable_type) {
em.logError(`Missing collection variable type for collection: ${collection_name}`);
return '';
}

if (variable_type === 'current_item') {
return resolveCurrentItem(collectionItem, path, collection_name, em);
}

return collectionItem[variable_type];
}

function resolveCurrentItem(
collectionItem: CollectionState,
path: string | undefined,
collection_name: string,
em: EditorModel,
) {
const currentItem = collectionItem.current_item;

if (!currentItem) {
em.logError(`Current item is missing for collection: ${collection_name}`);
return '';
}

if (currentItem.type === DataVariableType) {
const resolvedPath = currentItem.path ? `${currentItem.path}.${path}` : path;
return {
...currentItem,
path: resolvedPath,
};
}

if (path && !currentItem[path]) {
em.logError(`Path not found in current item: ${path} for collection: ${collection_name}`);
return '';
}

return path ? currentItem[path] : currentItem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const CollectionComponentType = 'collection-component';
export const keyCollectionDefinition = 'collectionDefinition';
export const keyInnerCollectionState = 'innerCollectionState';
export const CollectionVariableType = 'parent-collection-variable';
47 changes: 47 additions & 0 deletions packages/core/src/data_sources/model/collection_component/types.ts
mohamedsalem401 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { CollectionComponentType, keyCollectionDefinition } from './constants';

import { ComponentDefinition } from '../../../dom_components/model/types';
import { CollectionVariableDefinition } from '../../../../test/specs/dom_components/model/ComponentTypes';
import { DataVariableDefinition } from '../DataVariable';

type CollectionDataSource = any[] | DataVariableDefinition | CollectionVariableDefinition;
type CollectionConfig = {
start_index?: number;
end_index?: number;
dataSource: CollectionDataSource;
};

export enum CollectionStateVariableType {
current_index = 'current_index',
start_index = 'start_index',
current_item = 'current_item',
end_index = 'end_index',
collection_name = 'collection_name',
total_items = 'total_items',
remaining_items = 'remaining_items',
}

export type CollectionState = {
[CollectionStateVariableType.current_index]: number;
[CollectionStateVariableType.start_index]: number;
[CollectionStateVariableType.current_item]: any;
[CollectionStateVariableType.end_index]: number;
[CollectionStateVariableType.collection_name]?: string;
[CollectionStateVariableType.total_items]: number;
[CollectionStateVariableType.remaining_items]: number;
};

export type CollectionsStateMap = {
[key: string]: CollectionState;
};

export type CollectionComponentDefinition = {
[keyCollectionDefinition]: CollectionDefinition;
} & ComponentDefinition;

export type CollectionDefinition = {
type: typeof CollectionComponentType;
collection_name?: string;
config: CollectionConfig;
block: ComponentDefinition;
};
mohamedsalem401 marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading