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

feat: add DataSources to project json storage #6160

Merged
merged 11 commits into from
Sep 24, 2024
16 changes: 16 additions & 0 deletions docs/api/data_source_manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,22 @@ const [dataSource, dataRecord, propPath] = dsm.fromPath('my_data_source_id.recor
Returns **[DataSource?, DataRecord?, [String][7]?]** An array containing the data source,
data record, and optional property path.

## store

Store data sources to a JSON object.

Returns **[Object][6]** Stored data sources.

## load

Load data sources from a JSON object.

### Parameters

* `data` **[Object][6]** The data object containing data sources.

Returns **[Object][6]** Loaded data sources.

[1]: #add

[2]: #get
Expand Down
55 changes: 54 additions & 1 deletion docs/modules/DataSources.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,59 @@ const testDataSource = {

In this example, the `onRecordSetValue` transformer ensures that the `content` property is always an uppercase string.

## Storing DataSources in Project JSON

GrapesJS allows you to control whether a DataSource should be stored statically in the project JSON. This is useful for managing persistent data across project saves and loads.

### Using the `shouldStoreInProject` Key

When creating a DataSource, you can use the `shouldStoreInProject` key to specify whether it should be included in the project JSON.

**Example: Creating a DataSource with `shouldStoreInProject`**

```ts
const persistentDataSource = {
id: 'persistent-datasource',
records: [
{ id: 'id1', content: 'This data will be saved' },
{ id: 'id2', color: 'blue' },
],
shouldStoreInProject: true,
};

editor.DataSources.add(persistentDataSource);

const temporaryDataSource = {
id: 'temporary-datasource',
records: [
{ id: 'id1', content: 'This data will not be saved' },
],
shouldStoreInProject: false, // This is the default if not specified
danstarns marked this conversation as resolved.
Show resolved Hide resolved
};

editor.DataSources.add(temporaryDataSource);
```

In this example, `persistentDataSource` will be included in the project JSON when the project is saved, while `temporaryDataSource` will not.

### Benefits of Using `shouldStoreInProject`

1. **Persistent Configuration**: Store configuration data that should persist across project saves and loads.
2. **Default Data**: Include default data that should always be available in the project.
3. **Selective Storage**: Choose which DataSources to include in the project JSON, optimizing storage and load times.

### Accessing Stored DataSources

When a project is loaded, GrapesJS will automatically restore the DataSources that were saved with `shouldStoreInProject: true`. You can then access and use these DataSources as usual.

```ts
// After loading a project
const loadedDataSource = editor.DataSources.get('persistent-datasource');
console.log(loadedDataSource.getRecord('id1').get('content')); // Outputs: "This data will be saved"
```

Remember that DataSources with `shouldStoreInProject: false` (or those without the key specified) will not be available after a project is loaded unless you add them programmatically.

## Benefits of Using DataSources

DataSources are integrated with GrapesJS's runtime and BackboneJS models, enabling dynamic updates and synchronization between your data and UI components. This allows you to:
Expand Down Expand Up @@ -207,4 +260,4 @@ In this example, a counter is dynamically updated and displayed in the UI, demon
1. Injecting configuration
2. Managing global themes
3. Mocking & testing
4. Third-party integrations
4. Third-party integrations
58 changes: 57 additions & 1 deletion packages/core/src/data_sources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* @param {EditorModel} em - Editor model.
*/

import { readSync } from 'fs';
import { ItemManagerModule, ModuleConfig } from '../abstract/Module';
import { AddOptions, ObjectAny, RemoveOptions } from '../common';
import EditorModel from '../editor/model/Editor';
Expand All @@ -46,7 +47,7 @@ import { DataSourcesEvents, DataSourceProps } from './types';
import { Events } from 'backbone';

export default class DataSourceManager extends ItemManagerModule<ModuleConfig, DataSources> {
storageKey = '';
storageKey = 'dataSources';
events = DataSourcesEvents;
destroy(): void {}

Expand Down Expand Up @@ -148,4 +149,59 @@ export default class DataSourceManager extends ItemManagerModule<ModuleConfig, D

return result;
}

/**
* Store data sources to a JSON object.
* @returns {Object} Stored data sources.
*/
store() {
const data: ObjectAny = {};
this.all.forEach((dataSource) => {
const shouldStoreInProject = dataSource.get('shouldStoreInProject');
if (shouldStoreInProject) {
data[dataSource.id] = {
id: dataSource.id,
name: dataSource.get('name' as any),
records: dataSource.records.toJSON(),
shouldStoreInProject,
};
}
});

return { [this.storageKey]: data };
danstarns marked this conversation as resolved.
Show resolved Hide resolved
}

clear(): this {
// Clearing data sources are a no-op as to preserve data sources.
// This is because data sources are optionally stored in the project data.
// and could be defined prior to loading the project data.
return this;
}

/**
* Load data sources from a JSON object.
* @param {Object} data The data object containing data sources.
* @returns {Object} Loaded data sources.
*/
load(data: any) {
const storedDataSources: Record<string, DataSourceProps> = data[this.storageKey] || {};
const memoryDataSources = this.em.DataSources.getAllMap();

if (!Object.keys(storedDataSources).length) {
return {
...memoryDataSources,
};
} else {
this.clear();

Object.values(storedDataSources).forEach((ds) => {
this.add(ds, { silent: true });
});

return {
...storedDataSources,
...memoryDataSources,
};
}
}
danstarns marked this conversation as resolved.
Show resolved Hide resolved
}
6 changes: 5 additions & 1 deletion packages/core/src/data_sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ export interface DataSourceProps {
/**
* DataSource validation and transformation factories.
*/

transformers?: DataSourceTransformers;

/**
* If true will store the data source in the GrapesJS project.json file.
*/
shouldStoreInProject?: boolean;
}

export interface DataSourceTransformers {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/editor/model/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const storableDeps: (new (em: EditorModel) => IModule & IStorableModule)[] = [
CssComposer,
PageManager,
ComponentManager,
DataSourceManager,
];

Extender({ $ });
Expand Down
32 changes: 32 additions & 0 deletions packages/core/test/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,35 @@ export function waitEditorEvent(em: Editor | EditorModel, event: string) {
export function flattenHTML(html: string) {
return html.replace(/>\s+|\s+</g, (m) => m.trim());
}

// Filter out the unique ids and selectors replaced with 'data-variable-id'
// Makes the snapshot more stable
export function filterObjectForSnapshot(obj: any, parentKey: string = ''): any {
const result: any = {};

for (const key in obj) {
if (key === 'id') {
result[key] = 'data-variable-id';
continue;
}

if (key === 'selectors') {
result[key] = obj[key].map(() => 'data-variable-id');
continue;
}

if (typeof obj[key] === 'object' && obj[key] !== null) {
if (Array.isArray(obj[key])) {
result[key] = obj[key].map((item: any) =>
typeof item === 'object' ? filterObjectForSnapshot(item, key) : item,
);
} else {
result[key] = filterObjectForSnapshot(obj[key], key);
}
} else {
result[key] = obj[key];
}
}

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
exports[`DataSource Serialization .getProjectData ComponentDataVariable 1`] = `
{
"assets": [],
"dataSources": {},
"pages": [
{
"frames": [
Expand Down Expand Up @@ -53,6 +54,7 @@ exports[`DataSource Serialization .getProjectData ComponentDataVariable 1`] = `
exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
{
"assets": [],
"dataSources": {},
"pages": [
{
"frames": [
Expand Down Expand Up @@ -113,6 +115,7 @@ exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = `
{
"assets": [],
"dataSources": {},
"pages": [
{
"frames": [
Expand Down
125 changes: 125 additions & 0 deletions packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DataSource Storage .getProjectData ComponentDataVariable 1`] = `
{
"assets": [],
"dataSources": {
"component-storage": {
"id": "data-variable-id",
"records": [
{
"content": "Hello World",
"id": "data-variable-id",
},
],
"shouldStoreInProject": true,
},
},
"pages": [
{
"frames": [
{
"component": {
"components": [
{
"components": [
{
"defaultValue": "default",
"path": "component-storage.id1.content",
"type": "data-variable",
},
],
"tagName": "h1",
"type": "text",
},
],
"docEl": {
"tagName": "html",
},
"head": {
"type": "head",
},
"stylable": [
"background",
"background-color",
"background-image",
"background-repeat",
"background-attachment",
"background-position",
"background-size",
],
"type": "wrapper",
},
"id": "data-variable-id",
},
],
"id": "data-variable-id",
"type": "main",
},
],
"styles": [],
"symbols": [],
}
`;

exports[`DataSource Storage .loadProjectData ComponentDataVariable 1`] = `
{
"assets": [],
"dataSources": {
"component-storage": {
"id": "data-variable-id",
"records": [
{
"content": "Hello World Updated",
"id": "data-variable-id",
},
],
"shouldStoreInProject": true,
},
},
"pages": [
{
"frames": [
{
"component": {
"components": [
{
"components": [
{
"defaultValue": "default",
"path": "component-storage.id1.content",
"type": "data-variable",
},
],
"tagName": "h1",
"type": "text",
},
],
"docEl": {
"tagName": "html",
},
"head": {
"type": "head",
},
"stylable": [
"background",
"background-color",
"background-image",
"background-repeat",
"background-attachment",
"background-position",
"background-size",
],
"type": "wrapper",
},
"id": "data-variable-id",
},
],
"id": "data-variable-id",
"type": "main",
},
],
"styles": [],
"symbols": [],
}
`;
Loading