Browse Source

feat: add DataSources project json storage

pull/6160/head
danstarns 1 year ago
parent
commit
ef90f51491
  1. 16
      docs/api/data_source_manager.md
  2. 55
      docs/modules/DataSources.md
  3. 58
      packages/core/src/data_sources/index.ts
  4. 6
      packages/core/src/data_sources/types.ts
  5. 1
      packages/core/src/editor/model/Editor.ts
  6. 32
      packages/core/test/common.ts
  7. 3
      packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap
  8. 125
      packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap
  9. 38
      packages/core/test/specs/data_sources/serialization.ts
  10. 146
      packages/core/test/specs/data_sources/storage.ts

16
docs/api/data_source_manager.md

@ -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

55
docs/modules/DataSources.md

@ -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
};
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:
@ -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
packages/core/src/data_sources/index.ts

@ -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';
@ -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 {}
@ -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 };
}
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,
};
}
}
}

6
packages/core/src/data_sources/types.ts

@ -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 {

1
packages/core/src/editor/model/Editor.ts

@ -73,6 +73,7 @@ const storableDeps: (new (em: EditorModel) => IModule & IStorableModule)[] = [
CssComposer,
PageManager,
ComponentManager,
DataSourceManager,
];
Extender({ $ });

32
packages/core/test/common.ts

@ -28,3 +28,35 @@ export function setupTestEditor() {
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;
}

3
packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap

@ -3,6 +3,7 @@
exports[`DataSource Serialization .getProjectData ComponentDataVariable 1`] = `
{
"assets": [],
"dataSources": {},
"pages": [
{
"frames": [
@ -53,6 +54,7 @@ exports[`DataSource Serialization .getProjectData ComponentDataVariable 1`] = `
exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
{
"assets": [],
"dataSources": {},
"pages": [
{
"frames": [
@ -113,6 +115,7 @@ exports[`DataSource Serialization .getProjectData StyleDataVariable 1`] = `
exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = `
{
"assets": [],
"dataSources": {},
"pages": [
{
"frames": [

125
packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap

@ -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": [],
}
`;

38
packages/core/test/specs/data_sources/serialization.ts

@ -5,45 +5,12 @@ import { DataVariableType } from '../../../src/data_sources/model/DataVariable';
import EditorModel from '../../../src/editor/model/Editor';
import { ProjectData } from '../../../src/storage_manager';
import { DataSourceProps } from '../../../src/data_sources/types';
import { setupTestEditor } from '../../common';
// Filter out the unique ids and selectors replaced with 'data-variable-id'
// Makes the snapshot more stable
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;
}
import { filterObjectForSnapshot, setupTestEditor } from '../../common';
describe('DataSource Serialization', () => {
let editor: Editor;
let em: EditorModel;
let dsm: DataSourceManager;
let fixtures: HTMLElement;
let cmpRoot: ComponentWrapper;
const componentDataSource: DataSourceProps = {
id: 'component-serialization',
@ -62,7 +29,7 @@ describe('DataSource Serialization', () => {
};
beforeEach(() => {
({ editor, em, dsm, cmpRoot, fixtures } = setupTestEditor());
({ editor, em, dsm, cmpRoot } = setupTestEditor());
dsm.add(componentDataSource);
dsm.add(styleDataSource);
@ -234,6 +201,7 @@ describe('DataSource Serialization', () => {
],
styles: [],
symbols: [],
dataSources: {},
};
editor.loadProjectData(componentProjectData);

146
packages/core/test/specs/data_sources/storage.ts

@ -0,0 +1,146 @@
import Editor from '../../../src/editor';
import DataSourceManager from '../../../src/data_sources';
import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper';
import { DataVariableType } from '../../../src/data_sources/model/DataVariable';
import EditorModel from '../../../src/editor/model/Editor';
import { DataSourceProps } from '../../../src/data_sources/types';
import { filterObjectForSnapshot, setupTestEditor } from '../../common';
import { ProjectData } from '../../../src/storage_manager';
describe('DataSource Storage', () => {
let editor: Editor;
let em: EditorModel;
let dsm: DataSourceManager;
let cmpRoot: ComponentWrapper;
const storedDataSource: DataSourceProps = {
id: 'component-storage',
records: [{ id: 'id1', content: 'Hello World' }],
shouldStoreInProject: true,
};
const nonStoredDataSource: DataSourceProps = {
id: 'component-non-storage',
records: [{ id: 'id1', content: 'Hello World' }],
shouldStoreInProject: false,
};
beforeEach(() => {
({ editor, em, dsm, cmpRoot } = setupTestEditor());
dsm.add(storedDataSource);
dsm.add(nonStoredDataSource);
});
afterEach(() => {
em.destroy();
});
describe('.getProjectData', () => {
test('ComponentDataVariable', () => {
const dataVariable = {
type: DataVariableType,
defaultValue: 'default',
path: `${storedDataSource.id}.id1.content`,
};
cmpRoot.append({
tagName: 'h1',
type: 'text',
components: [dataVariable],
})[0];
const projectData = editor.getProjectData();
const page = projectData.pages[0];
const frame = page.frames[0];
const component = frame.component.components[0];
expect(component.components[0]).toEqual(dataVariable);
const snapshot = filterObjectForSnapshot(projectData);
expect(snapshot).toMatchSnapshot(``);
const dataSources = projectData.dataSources;
expect(dataSources).toEqual({
[storedDataSource.id]: {
id: storedDataSource.id,
records: storedDataSource.records,
shouldStoreInProject: true,
},
});
});
});
describe('.loadProjectData', () => {
test('ComponentDataVariable', () => {
const componentProjectData: ProjectData = {
assets: [],
dataSources: {
[storedDataSource.id]: {
id: storedDataSource.id,
records: storedDataSource.records,
shouldStoreInProject: true,
},
},
pages: [
{
frames: [
{
component: {
components: [
{
components: [
{
defaultValue: 'default',
path: `${storedDataSource.id}.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: 'frame-id',
},
],
id: 'page-id',
type: 'main',
},
],
styles: [],
symbols: [],
};
editor.loadProjectData(componentProjectData);
const dataSource = dsm.get(storedDataSource.id);
const record = dataSource?.getRecord('id1');
expect(record?.get('content')).toBe('Hello World');
expect(editor.getHtml()).toEqual('<body><h1><div>Hello World</div></h1></body>');
record?.set('content', 'Hello World Updated');
expect(editor.getHtml()).toEqual('<body><h1><div>Hello World Updated</div></h1></body>');
const reloadedProjectData = editor.getProjectData();
const snapshot = filterObjectForSnapshot(reloadedProjectData);
expect(snapshot).toMatchSnapshot(``);
});
});
});
Loading…
Cancel
Save