From 83bb01b94be030eefad65de075d3d4096cc54ba6 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 28 Oct 2025 12:58:40 +0400 Subject: [PATCH] Data source schema & providers (#6633) * Setup initial schema types * Setup initial schema methods * Add DataSource tests * Up test * Add test getValue * Cleanup * Up test * Add setValue method * Up docs * Check nested arrays * Add a test for nested setValue * Improve setValue for nested values * Improve setValue for nested values * Setup relation tests * Add getResolvedRecords * Resolve one to many relations in DataSource records * Add DataSourceSchema * Up type * Start data source providers * Start test provider * Add tests for loadProvider * Cleanup * Skip records if DataSource provider is set * Load providers on project load * Type keymap events * Move modal events * Move layer events * Move RTE events * Move selector events * Move StyleManager events * Move editor events * Start DataSource callbacks * Add data source callbacks * Update docs * Up device_manager jsdoc * Format --- docs/api.mjs | 58 ++-- docs/api/block_manager.md | 2 + docs/api/canvas.md | 8 + docs/api/component.md | 7 +- docs/api/datasource.md | 44 +++ docs/api/datasources.md | 43 ++- docs/api/device_manager.md | 33 +- docs/api/editor.md | 12 +- docs/api/keymaps.md | 27 +- docs/api/layer_manager.md | 20 +- docs/api/modal_dialog.md | 19 +- docs/api/parser.md | 12 +- docs/api/rich_text_editor.md | 25 +- docs/api/selector_manager.md | 54 +++- docs/api/style_manager.md | 78 +++-- packages/core/src/block_manager/types.ts | 7 + .../core/src/data_sources/config/config.ts | 13 + packages/core/src/data_sources/index.ts | 72 ++++- .../core/src/data_sources/model/DataSource.ts | 142 ++++++++- packages/core/src/data_sources/types.ts | 137 +++++++- packages/core/src/data_sources/utils.ts | 2 + packages/core/src/device_manager/index.ts | 7 +- .../dom_components/view/ComponentTextView.ts | 4 +- packages/core/src/editor/index.ts | 73 ++--- packages/core/src/editor/model/Editor.ts | 11 +- packages/core/src/editor/types.ts | 47 +++ packages/core/src/keymaps/index.ts | 33 +- packages/core/src/keymaps/types.ts | 28 ++ packages/core/src/modal_dialog/index.ts | 25 +- packages/core/src/modal_dialog/types.ts | 27 ++ packages/core/src/navigator/index.ts | 42 +-- packages/core/src/navigator/types.ts | 39 +++ packages/core/src/rich_text_editor/index.ts | 48 +-- packages/core/src/rich_text_editor/types.ts | 39 +++ packages/core/src/selector_manager/index.ts | 64 +--- packages/core/src/selector_manager/types.ts | 57 ++++ packages/core/src/style_manager/index.ts | 106 ++----- packages/core/src/style_manager/types.ts | 88 ++++++ packages/core/src/utils/mixins.ts | 34 +- .../core/test/specs/data_sources/index.ts | 107 ++++++- .../specs/data_sources/model/DataSource.ts | 295 ++++++++++++++++++ 41 files changed, 1586 insertions(+), 403 deletions(-) create mode 100644 packages/core/src/data_sources/config/config.ts create mode 100644 packages/core/src/keymaps/types.ts create mode 100644 packages/core/src/modal_dialog/types.ts create mode 100644 packages/core/src/navigator/types.ts create mode 100644 packages/core/src/rich_text_editor/types.ts create mode 100644 packages/core/src/selector_manager/types.ts create mode 100644 packages/core/src/style_manager/types.ts create mode 100644 packages/core/test/specs/data_sources/model/DataSource.ts diff --git a/docs/api.mjs b/docs/api.mjs index a16dde8d6..6ad7b70e2 100644 --- a/docs/api.mjs +++ b/docs/api.mjs @@ -97,33 +97,43 @@ async function generateDocs() { throw `File not found '${filePath}'`; } - return build([filePath], { shallow: true }) - .then((cm) => formats.md(cm /*{ markdownToc: true }*/)) - .then(async (output) => { - let addLogs = []; - let result = output - .replace(/\*\*\\\[/g, '**[') - .replace(/\*\*\(\\\[/g, '**([') - .replace(/<\\\[/g, '<[') - .replace(/<\(\\\[/g, '<([') - .replace(/\| \\\[/g, '| [') - .replace(/\\n```js/g, '```js') - .replace(/docsjs\./g, '') - .replace('**Extends ModuleModel**', '') - .replace('**Extends Model**', ''); + try { + return build([filePath], { shallow: true }) + .then((cm) => formats.md(cm /*{ markdownToc: true }*/)) + .then(async (output) => { + let addLogs = []; + let result = output + .replace(/\*\*\\\[/g, '**[') + .replace(/\*\*\(\\\[/g, '**([') + .replace(/<\\\[/g, '<[') + .replace(/<\(\\\[/g, '<([') + .replace(/\| \\\[/g, '| [') + .replace(/\\n```js/g, '```js') + .replace(/docsjs\./g, '') + .replace('**Extends ModuleModel**', '') + .replace('**Extends Model**', ''); - // Search for module event documentation - if (result.indexOf(REPLACE_EVENTS) >= 0) { - const eventsMd = await getEventsMdFromTypes(filePath); - if (eventsMd && result.indexOf(REPLACE_EVENTS) >= 0) { - addLogs.push('replaced events'); + // Search for module event documentation + if (result.indexOf(REPLACE_EVENTS) >= 0) { + try { + const eventsMd = await getEventsMdFromTypes(filePath); + if (eventsMd && result.indexOf(REPLACE_EVENTS) >= 0) { + addLogs.push('replaced events'); + } + result = eventsMd ? result.replace(REPLACE_EVENTS, `## Available Events\n${eventsMd}`) : result; + } catch (err) { + console.error(`Failed getting events: ${file[0]}`); + throw err; + } } - result = eventsMd ? result.replace(REPLACE_EVENTS, `## Available Events\n${eventsMd}`) : result; - } - writeFileSync(`${docRoot}/api/${file[1]}`, result); - log('Created', file[1], addLogs.length ? `(${addLogs.join(', ')})` : ''); - }); + writeFileSync(`${docRoot}/api/${file[1]}`, result); + log('Created', file[1], addLogs.length ? `(${addLogs.join(', ')})` : ''); + }); + } catch (err) { + console.error(`Build failed: ${file[0]}`); + throw err; + } }), ); diff --git a/docs/api/block_manager.md b/docs/api/block_manager.md index 6bffe711e..9b149da71 100644 --- a/docs/api/block_manager.md +++ b/docs/api/block_manager.md @@ -84,6 +84,8 @@ editor.on('block:custom', ({ container, blocks, ... }) => { ... }); editor.on('block', ({ event, model, ... }) => { ... }); ``` +* BlocksEventCallback + [Block]: block.html [Component]: component.html diff --git a/docs/api/canvas.md b/docs/api/canvas.md index 415ec65c9..c82ef3a82 100644 --- a/docs/api/canvas.md +++ b/docs/api/canvas.md @@ -122,6 +122,14 @@ editor.on('canvas:frame:load:body', ({ window }) => { }); ``` +* `canvas:frame:unload` Frame is unloading from the canvas. + +```javascript +editor.on('canvas:frame:unload', ({ frame }) => { + console.log('Unloading frame', frame); +}); +``` + [Component]: component.html [Frame]: frame.html diff --git a/docs/api/component.md b/docs/api/component.md index 9dab1b1cc..5d8c281f3 100644 --- a/docs/api/component.md +++ b/docs/api/component.md @@ -137,7 +137,7 @@ By setting override to specific properties, changes of those properties will be ### Parameters * `value` **([Boolean][3] | [String][1] | [Array][5]<[String][1]>)** -* `options` **DynamicWatchersOptions** (optional, default `{}`) +* `options` **DataWatchersOptions** (optional, default `{}`) ### Examples @@ -335,8 +335,7 @@ Get the style of the component ### Parameters -* `options` **any** (optional, default `{}`) -* `optsAdd` **any** (optional, default `{}`) +* `opts` **GetComponentStyleOpts?** Returns **[Object][2]** @@ -363,7 +362,7 @@ Return all component's attributes ### Parameters -* `opts` **{noClass: [boolean][3]?, noStyle: [boolean][3]?}** (optional, default `{}`) +* `opts` **{noClass: [boolean][3]?, noStyle: [boolean][3]?, skipResolve: [boolean][3]?}** (optional, default `{}`) Returns **[Object][2]** diff --git a/docs/api/datasource.md b/docs/api/datasource.md index 920623407..23d094601 100644 --- a/docs/api/datasource.md +++ b/docs/api/datasource.md @@ -31,6 +31,44 @@ dataSource.addRecord({ id: 'id3', name: 'value3' }); * `props` **DataSourceProps** Properties to initialize the data source. * `opts` **DataSourceOptions** Options to initialize the data source. +### hasProvider + +Indicates if the data source has a provider for records. + +### getResolvedRecords + +Retrieves all records from the data source with resolved relations based on the schema. + +### upSchema + +Update the schema. + +#### Parameters + +* `schema` **Partial\** +* `opts` **SetOptions?** + +#### Examples + +```javascript +dataSource.upSchema({ name: { type: 'string' } }); +``` + +### getSchemaField + +Get schema field definition. + +#### Parameters + +* `fieldKey` **any** + +#### Examples + +```javascript +const fieldSchema = dataSource.getSchemaField('name'); +fieldSchema.type; // 'string' +``` + ## defaults Returns the default properties for the data source. @@ -55,6 +93,12 @@ Retrieves the collection of records associated with this data source. Returns **DataRecords\** The collection of data records. +## records + +Retrieves the collection of records associated with this data source. + +Returns **DataRecords\** The collection of data records. + ## em Retrieves the editor model associated with this data source. diff --git a/docs/api/datasources.md b/docs/api/datasources.md index cfe8080c6..108caa2e3 100644 --- a/docs/api/datasources.md +++ b/docs/api/datasources.md @@ -44,12 +44,26 @@ editor.on('data:path', ({ dataSource, dataRecord, path }) => { editor.on('data:pathSource:SOURCE_ID', ({ dataSource, dataRecord, path }) => { ... }); ``` +* `data:provider:load` Data source provider load. + +```javascript +editor.on('data:provider:load', ({ dataSource, result }) => { ... }); +``` + +* `data:provider:loadAll` Load of all data source providers (eg. on project load). + +```javascript +editor.on('data:provider:loadAll', () => { ... }); +``` + * `data` Catch-all event for all the events mentioned above. ```javascript editor.on('data', ({ event, model, ... }) => { ... }); ``` +* DataSourcesEventCallback + ## Methods * [add][1] - Add a new data source. @@ -101,15 +115,32 @@ Returns **[DataSource]** Data source. ## getValue -Get value from data sources by key +Get value from data sources by path. ### Parameters -* `key` **[String][7]** Path to value. -* `defValue` **any** +* `path` **[String][7]** Path to value. +* `defValue` **any** Default value if the path is not found. Returns **any** const value = dsm.getValue('ds\_id.record\_id.propName', 'defaultValue'); +## setValue + +Set value in data sources by path. + +### Parameters + +* `path` **[String][7]** Path to value in format 'dataSourceId.recordId.propName' +* `value` **any** Value to set + +### Examples + +```javascript +dsm.setValue('ds_id.record_id.propName', 'new value'); +``` + +Returns **[Boolean][8]** Returns true if the value was set successfully + ## remove Remove data source. @@ -152,7 +183,7 @@ data record, and optional property path. Store data sources to a JSON object. -Returns **[Array][8]** Stored data sources. +Returns **[Array][9]** Stored data sources. ## load @@ -178,4 +209,6 @@ Returns **[Object][6]** Loaded data sources. [7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array +[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean + +[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array diff --git a/docs/api/device_manager.md b/docs/api/device_manager.md index c1e283f86..4a6f6e914 100644 --- a/docs/api/device_manager.md +++ b/docs/api/device_manager.md @@ -19,12 +19,35 @@ const deviceManager = editor.Devices; ``` ## Available Events +* `device:add` New device added to the collection. The `Device` is passed as an argument. -* `device:add` - Added new device. The [Device] is passed as an argument to the callback -* `device:remove` - Device removed. The [Device] is passed as an argument to the callback -* `device:select` - New device selected. The newly selected [Device] and the previous one, are passed as arguments to the callback -* `device:update` - Device updated. The updated [Device] and the object containing changes are passed as arguments to the callback -* `device` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback +```javascript +editor.on('device:add', (device) => { ... }); +``` + +* `device:remove` Device removed from the collection. The `Device` is passed as an argument. + +```javascript +editor.on('device:remove', (device) => { ... }); +``` + +* `device:select` A new device is selected. The `Device` is passed as an argument. + +```javascript +editor.on('device:select', (device) => { ... }); +``` + +* `device:update` Device updated. The `Device` and the object containing changes are passed as arguments. + +```javascript +editor.on('device:update', (device) => { ... }); +``` + +* `device` Catch-all event for all the events mentioned above. + +```javascript +editor.on('device', ({ event, model, ... }) => { ... }); +``` ## Methods diff --git a/docs/api/editor.md b/docs/api/editor.md index 64a1c8c0d..35b30e20b 100644 --- a/docs/api/editor.md +++ b/docs/api/editor.md @@ -42,6 +42,15 @@ editor.on('load', () => { ... }); editor.on('project:load', ({ project, initial }) => { ... }); ``` +* `project:loaded` Similar to `project:load`, but triggers only if the project is loaded successfully. + +```javascript +editor.on('project:loaded', ({ project, initial }) => { ... }); + +// Loading an empty project, won't trigger this event. +editor.loadProjectData({}); +``` + * `project:get` Event triggered on request of the project data. This can be used to extend the project with custom data. ```javascript @@ -516,6 +525,7 @@ Load data from the JSON project ### Parameters * `data` **[Object][16]** Project to load +* `options` **[Object][16]?** Custom options that could be passed to the project load events. (optional, default `{}`) ### Examples @@ -722,7 +732,7 @@ Trigger event ### Parameters * `event` **[string][18]** Event to trigger -* `args` **...[Array][19]\** +* `args` **...any** Returns **this** diff --git a/docs/api/keymaps.md b/docs/api/keymaps.md index 643feeca9..c1b55b9ec 100644 --- a/docs/api/keymaps.md +++ b/docs/api/keymaps.md @@ -19,23 +19,30 @@ const editor = grapesjs.init({ }) ``` -Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance. +Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance. ```js -// Listen to events -editor.on('keymap:add', () => { ... }); - -// Use the API const keymaps = editor.Keymaps; -keymaps.add(...); ``` ## Available Events +* `keymap:add` New keymap added. The new keymap object is passed as an argument to the callback. + +```javascript +editor.on('keymap:add', (keymap) => { ... }); +``` + +* `keymap:remove` Keymap removed. The removed keymap object is passed as an argument to the callback. -* `keymap:add` - New keymap added. The new keyamp object is passed as an argument -* `keymap:remove` - Keymap removed. The removed keyamp object is passed as an argument -* `keymap:emit` - Some keymap emitted, in arguments you get keymapId, shortcutUsed, Event -* `keymap:emit:{keymapId}` - `keymapId` emitted, in arguments you get keymapId, shortcutUsed, Event +```javascript +editor.on('keymap:remove', (keymap) => { ... }); +``` + +* `keymap:emit` Some keymap emitted. The keymapId, shortcutUsed, and Event are passed as arguments to the callback. + +```javascript +editor.on('keymap:emit', (keymapId, shortcutUsed, event) => { ... }); +``` ## Methods diff --git a/docs/api/layer_manager.md b/docs/api/layer_manager.md index 2b879ce07..8ae97605e 100644 --- a/docs/api/layer_manager.md +++ b/docs/api/layer_manager.md @@ -13,16 +13,30 @@ const editor = grapesjs.init({ }) ``` -Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance +Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance. ```js const layers = editor.Layers; ``` ## Available Events +* `layer:root` Root layer changed. The new root component is passed as an argument to the callback. -* `layer:root` - Root layer changed. The new root component is passed as an argument to the callback. -* `layer:component` - Component layer is updated. The updated component is passed as an argument to the callback. +```javascript +editor.on('layer:root', (component) => { ... }); +``` + +* `layer:component` Component layer is updated. The updated component is passed as an argument to the callback. + +```javascript +editor.on('layer:component', (component, opts) => { ... }); +``` + +* `layer:custom` Custom layer event. Object with container and root is passed as an argument to the callback. + +```javascript +editor.on('layer:custom', ({ container, root }) => { ... }); +``` ## Methods diff --git a/docs/api/modal_dialog.md b/docs/api/modal_dialog.md index d82086795..052c6a4ce 100644 --- a/docs/api/modal_dialog.md +++ b/docs/api/modal_dialog.md @@ -19,10 +19,23 @@ const modal = editor.Modal; ``` ## Available Events +* `modal:open` Modal is opened -* `modal:open` - Modal is opened -* `modal:close` - Modal is closed -* `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback. +```javascript +editor.on('modal:open', () => { ... }); +``` + +* `modal:close` Modal is closed + +```javascript +editor.on('modal:close', () => { ... }); +``` + +* `modal` Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback. + +```javascript +editor.on('modal', ({ open, title, content, ... }) => { ... }); +``` ## Methods diff --git a/docs/api/parser.md b/docs/api/parser.md index df9d92993..b1fdea016 100644 --- a/docs/api/parser.md +++ b/docs/api/parser.md @@ -81,6 +81,12 @@ Parse HTML string and return the object containing the Component Definition * `options.htmlType` **[String][6]?** [HTML mime type][7] to parse * `options.allowScripts` **[Boolean][8]** Allow `