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 58 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
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
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;
};
Loading
Loading