From 046e10c5286923eb36776ccf76f960fad39f9180 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 25 Feb 2022 16:32:38 +0100 Subject: [PATCH 01/35] Refactor storage in modules --- src/asset_manager/index.js | 43 ++--------- src/common/module.js | 34 ++++++++- src/css_composer/index.js | 69 +++--------------- src/dom_components/config/config.js | 11 +-- src/dom_components/index.js | 103 +++++---------------------- src/editor/model/Editor.js | 5 +- src/pages/index.js | 43 +++-------- src/storage_manager/config/config.js | 41 +++++++++++ src/storage_manager/index.js | 47 ++++++------ 9 files changed, 144 insertions(+), 252 deletions(-) diff --git a/src/asset_manager/index.js b/src/asset_manager/index.js index bd0c939ef..756402ffe 100644 --- a/src/asset_manager/index.js +++ b/src/asset_manager/index.js @@ -280,47 +280,12 @@ export default () => { return this.__remove(asset, opts); }, - /** - * Store assets data to the selected storage - * @param {Boolean} noStore If true, won't store - * @returns {Object} Data to store - * @example - * var assets = assetManager.store(); - */ - store(noStore) { - const obj = {}; - const assets = JSON.stringify(this.getAll().toJSON()); - obj[this.storageKey] = assets; - if (!noStore && c.stm) c.stm.store(obj); - return obj; + store() { + return this.getProjectData(); }, - /** - * Load data from the passed object. - * The fetched data will be added to the collection. - * @param {Object} data Object of data to load - * @returns {Object} Loaded assets - * @example - * var assets = assetManager.load({ - * assets: [...] - * }) - * - */ - load(data = {}) { - const name = this.storageKey; - let assets = data[name] || []; - - if (typeof assets == 'string') { - try { - assets = JSON.parse(data[name]); - } catch (err) {} - } - - if (assets && assets.length) { - this.getAll().reset(assets); - } - - return assets; + load(data) { + return this.loadProjectData(data); }, /** diff --git a/src/common/module.js b/src/common/module.js index 8794a0542..8d1b1851b 100644 --- a/src/common/module.js +++ b/src/common/module.js @@ -6,6 +6,36 @@ export default { return this.__getConfig(name); }, + getProjectData(data) { + const obj = {}; + const key = this.storageKey; + if (key) { + obj[key] = data || this.getAll(); + } + return obj; + }, + + loadProjectData(data = {}, { all, def = [], onResult } = {}) { + const key = this.storageKey; + let result = data[key] || def; + + if (typeof result == 'string') { + try { + result = JSON.parse(result); + } catch (err) { + this.__logWarn('Data parsing failed', { input: result }); + } + } + + if (onResult) { + onResult(result); + } else if (result && result.length) { + (all || this.getAll()).reset(result); + } + + return result; + }, + __getConfig(name) { const res = this.config || {}; return name ? res[name] : res; @@ -82,8 +112,8 @@ export default { __onAllEvent() {}, - __logWarn(str) { - this.em.logWarning(`[${this.name}]: ${str}`); + __logWarn(str, opts) { + this.em.logWarning(`[${this.name}]: ${str}`, opts); }, _createId(len = 16) { diff --git a/src/css_composer/index.js b/src/css_composer/index.js index 939c7ae2e..eb375c12e 100644 --- a/src/css_composer/index.js +++ b/src/css_composer/index.js @@ -35,6 +35,7 @@ import CssRules from './model/CssRules'; import CssRulesView from './view/CssRulesView'; import Selectors from 'selector_manager/model/Selectors'; import Selector from 'selector_manager/model/Selector'; +import Module from 'common/module'; export default () => { let em; @@ -42,6 +43,8 @@ export default () => { var rules, rulesView; return { + ...Module, + Selectors, /** @@ -51,23 +54,12 @@ export default () => { */ name: 'CssComposer', + storageKey: 'styles', + getConfig() { return c; }, - /** - * Mandatory for the storage manager - * @type {String} - * @private - */ - storageKey() { - var keys = []; - var smc = (c.stm && c.stm.getConfig()) || {}; - if (smc.storeCss) keys.push('css'); - if (smc.storeStyles) keys.push('styles'); - return keys; - }, - /** * Initializes module. Automatically called with a new instance of the editor * @param {Object} config Configurations @@ -108,55 +100,12 @@ export default () => { um && um.add(this.getAll()); }, - /** - * Load data from the passed object, if the object is empty will try to fetch them - * autonomously from the storage manager. - * The fetched data will be added to the collection - * @param {Object} data Object of data to load - * @return {Object} Loaded rules - * @private - */ - load(data) { - var d = data || ''; - - if (!d && c.stm) { - d = c.em.getCacheLoad(); - } - - var obj = d.styles || ''; - - if (d.styles) { - try { - obj = JSON.parse(d.styles); - } catch (err) {} - } else if (d.css) { - obj = c.em.get('Parser').parseCss(d.css); - } - - if (isArray(obj)) { - obj.length && rules.reset(obj); - } else if (obj) { - rules.reset(obj); - } - - return obj; + store() { + return this.getProjectData(); }, - /** - * Store data to the selected storage - * @param {Boolean} noStore If true, won't store - * @return {Object} Data to store - * @private - */ - store(noStore) { - if (!c.stm) return; - const obj = {}; - const keys = this.storageKey(); - const hasPages = em && em.get('hasPages'); - if (keys.indexOf('css') >= 0 && !hasPages) obj.css = c.em.getCss(); - if (keys.indexOf('styles') >= 0) obj.styles = JSON.stringify(rules); - if (!noStore) c.stm.store(obj); - return obj; + load(data) { + return this.loadProjectData(data); }, /** diff --git a/src/dom_components/config/config.js b/src/dom_components/config/config.js index cc895ee7c..b882cb42b 100644 --- a/src/dom_components/config/config.js +++ b/src/dom_components/config/config.js @@ -7,13 +7,6 @@ export default { // If the component is draggable you can drag the component itself (not only from the toolbar) draggableComponents: 1, - // Generally, if you don't edit the wrapper in the editor, like - // custom attributes, you don't need the wrapper stored in your JSON - // structure, but in case you need it you can use this option. - // If you have `config.avoidInlineStyle` disabled the wrapper will be stored - // as we need to store inlined style. - storeWrapper: 0, - /** * You can setup a custom component definition processor before adding it into the editor. * It might be useful to transform custom objects (es. some framework specific JSX) to GrapesJS component one. @@ -53,6 +46,6 @@ export default { 'param', 'source', 'track', - 'wbr' - ] + 'wbr', + ], }; diff --git a/src/dom_components/index.js b/src/dom_components/index.js index d508b20dc..c4a66558a 100644 --- a/src/dom_components/index.js +++ b/src/dom_components/index.js @@ -96,6 +96,7 @@ import ComponentTextView from './view/ComponentTextView'; import ComponentWrapper from './model/ComponentWrapper'; import ComponentFrame from './model/ComponentFrame'; import ComponentFrameView from './view/ComponentFrameView'; +import Module from 'common/module'; export default () => { var c = {}; @@ -207,6 +208,8 @@ export default () => { ]; return { + ...Module, + Component, Components, @@ -224,6 +227,8 @@ export default () => { */ name: 'DomComponents', + storageKey: 'components', + /** * Returns config * @return {Object} Config object @@ -233,19 +238,6 @@ export default () => { return c; }, - /** - * Mandatory for the storage manager - * @type {String} - * @private - */ - storageKey() { - var keys = []; - var smc = (c.stm && c.stm.getConfig()) || {}; - if (smc.storeHtml) keys.push('html'); - if (smc.storeComponents) keys.push('components'); - return keys; - }, - /** * Initialize module. Called on a new instance of the editor with configurations passed * inside 'domComponents' field @@ -295,82 +287,21 @@ export default () => { c.components && this.setComponents(c.components, { silent: 1 }); }, - /** - * Load components from the passed object, if the object is empty will try to fetch them - * autonomously from the selected storage - * The fetched data will be added to the collection - * @param {Object} data Object of data to load - * @return {Object} Loaded data - */ - load(data = '') { - const { em } = this; - let result = ''; - - if (!data && c.stm) { - data = c.em.getCacheLoad(); - } - - const { components, html } = data; - - if (components) { - if (isObject(components) || isArray(components)) { - result = components; - } else { - try { - result = JSON.parse(components); - } catch (err) { - em && em.logError(err); + load(data) { + return this.loadProjectData(data, { + onResult: result => { + if (isArray(result)) { + result.length && this.getComponents().reset(result); + } else { + this.getWrapper().set(result); } - } - } else if (html) { - result = html; - } - - const isObj = result && result.constructor === Object; - - if ((result && result.length) || isObj) { - this.clear(); - - // If the result is an object I consider it the wrapper - if (isObj) { - this.getWrapper().set(result); - } else { - this.getComponents().add(result); - } - } - - return result; + }, + }); }, - /** - * Store components on the selected storage - * @param {Boolean} noStore If true, won't store - * @return {Object} Data to store - */ - store(noStore) { - if (!c.stm || this.em.get('hasPages')) { - return {}; - } - - var obj = {}; - var keys = this.storageKey(); - - if (keys.indexOf('html') >= 0) { - obj.html = c.em.getHtml(); - } - - if (keys.indexOf('components') >= 0) { - // const storeWrap = (em && !em.getConfig('avoidInlineStyle')) || c.storeWrapper; - const storeWrap = c.storeWrapper; - const toStore = storeWrap ? this.getWrapper() : this.getComponents(); - obj.components = JSON.stringify(toStore); - } - - if (!noStore) { - c.stm.store(obj); - } - - return obj; + store() { + if (this.em.get('hasPages')) return {}; + return this.getProjectData(this.getWrapper()); }, /** diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index ca3029d09..4f74307cb 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -611,10 +611,11 @@ export default class EditorModel extends Model { * @return {Object} Stored data * @private */ - store(clb) { + store(opts) { const sm = this.get('StorageManager'); if (!sm) return; + const isCallback = isFunction(opts); const store = this.storeData(); sm.store(store, res => { clb && clb(res, store); @@ -634,7 +635,7 @@ export default class EditorModel extends Model { this.get('storables').forEach(m => { result = { ...result, ...m.store(1) }; }); - return result; + return JSON.parse(JSON.stringify(result)); } /** diff --git a/src/pages/index.js b/src/pages/index.js index 65fb5e0d2..28489348b 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -46,7 +46,7 @@ import { isString, bindAll, unique, flatten } from 'underscore'; import { createId } from 'utils/mixins'; -import { Model } from 'backbone'; +import { Model, Module } from 'common'; import Pages from './model/Pages'; import Page from './model/Page'; @@ -64,6 +64,8 @@ const typeMain = 'main'; export default () => { return { + ...Module, + name: 'PageManager', storageKey: 'pages', @@ -80,7 +82,7 @@ export default () => { add: evPageAdd, addBefore: evPageAddBefore, remove: evPageRemove, - removeBefore: evPageRemoveBefore + removeBefore: evPageRemoveBefore, }, /** @@ -119,9 +121,7 @@ export default () => { const { pages } = this; const opt = { silent: true }; pages.add(this.config.pages || [], opt); - const mainPage = !pages.length - ? this.add({ type: typeMain }, opt) - : this.getMain(); + const mainPage = !pages.length ? this.add({ type: typeMain }, opt) : this.getMain(); this.select(mainPage, opt); }, @@ -228,13 +228,7 @@ export default () => { */ getAllWrappers() { const pages = this.getAll(); - return unique( - flatten( - pages.map(page => - page.getAllFrames().map(frame => frame.getComponent()) - ) - ) - ); + return unique(flatten(pages.map(page => page.getAllFrames().map(frame => frame.getComponent())))); }, getAllMap() { @@ -280,28 +274,13 @@ export default () => { ['selected', 'config', 'em', 'pages', 'model'].map(i => (this[i] = 0)); }, - store(noStore) { + store() { if (!this.em.get('hasPages')) return {}; - const obj = {}; - const cnf = this.config; - obj[this.storageKey] = JSON.stringify(this.getAll()); - if (!noStore && cnf.stm) cnf.stm.store(obj); - return obj; + return this.getProjectData(); }, - load(data = {}) { - const key = this.storageKey; - let res = data[key] || []; - - if (typeof res == 'string') { - try { - res = JSON.parse(data[key]); - } catch (err) {} - } - - res && res.length && this.pages.reset(res); - - return res; + load(data) { + return this.loadProjectData(data, { all: this.pages }); }, _createId() { @@ -315,6 +294,6 @@ export default () => { } while (pagesMap[id]); return id; - } + }, }; }; diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index 8d2c43bba..59027500f 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -31,6 +31,47 @@ export default { // If enabled, checks if browser supports Local Storage checkLocal: true, + // Default storage options + options: { + local: { + key: 'gjs-project', + }, + remote: { + // Custom parameters to pass with the remote request, eg. csrf token + params: {}, + // Custom headers + headers: {}, + // Endpoint where to save all stuff + urlStore: '', + // Endpoint where to fetch data + urlLoad: '', + //Callback before request + beforeSend(jqXHR, settings) {}, + //Callback after request + onComplete(jqXHR, status) {}, + // set contentType paramater of $.ajax + // true: application/json; charset=utf-8' + // false: 'x-www-form-urlencoded' + contentTypeJson: true, + // Pass custom options to fetch API (remote storage) + // You can pass a simple object: { someOption: 'someValue' } + // or a function which returns and object to add: + // currentOpts => { + // return currentOpts.method === 'post' ? { method: 'patch' } : {}; + // } + fetchOptions: '', + credentials: 'include', + + /** + * (TODO) This will enable the store of the project also on the local storage. + * The local data are cleared on every sucessful remote save. In case the remote storage + * fails (eg. network issue), on project reload, a dialog with the possibility to recovery + * previous data will be shown. + */ + recovery: true, + }, + }, + // ONLY FOR REMOTE STORAGE // Custom parameters to pass with the remote storage request, eg. csrf token params: {}, diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index fd39e8d7a..8853daf4c 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -213,35 +213,32 @@ export default () => { }, /** - * Store key-value resources in the current storage - * @param {Object} data Data in key-value format, eg. {item1: value1, item2: value2} - * @param {Function} clb Callback function + * Store data in the current storage. + * @param {Object} data Data in key-value format, eg. `{ item1: value1, item2: value2 }` + * @param {Function} resolve Resolve callback function. The result is passed as an argument. + * @param {Function} reject Reject callback function. The error is passed as an argument. * @return {Object|null} * @example * storageManager.store({item1: value1, item2: value2}); * */ - store(data, clb) { - const st = this.get(this.getCurrent()); + store(data, resolve, reject, options = {}) { + const st = this.getCurrentStorage(); const toStore = {}; + const opts = { ...this.getCurrentOptons(), ...options }; this.onStart('store', data); - for (let key in data) { - toStore[c.id + key] = data[key]; - } + const onResult = res => { + this.onAfter('store', res); + resolve?.(res); + this.onEnd('store', res); + }; + + const onError = err => { + reject?.(err); + this.onError('store', err); + }; - return st - ? st.store( - toStore, - res => { - this.onAfter('store', res); - clb && clb(res); - this.onEnd('store', res); - }, - err => { - this.onError('store', err); - } - ) - : null; + return st ? st.store(toStore, onResult, onError, opts) : null; }, /** @@ -257,7 +254,7 @@ export default () => { * }); * */ load(keys, clb) { - const st = this.get(this.getCurrent()); + const st = this.getCurrentStorage(); const keysF = []; let result = {}; @@ -323,6 +320,12 @@ export default () => { return this.get(this.getCurrent()); }, + getCurrentOptons() { + const config = this.getConfig(); + const current = this.getCurrent(); + return config.options[current] || {}; + }, + /** * On start callback * @private From f447ccef517599400a7f820469bddc5433c08176 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 25 Feb 2022 17:41:09 +0100 Subject: [PATCH 02/35] Refactor LocalStorage --- src/editor/model/Editor.js | 12 ++--- src/storage_manager/config/config.js | 5 ++- src/storage_manager/index.js | 53 ++++++++++------------- src/storage_manager/model/LocalStorage.js | 47 +++++++++++--------- 4 files changed, 58 insertions(+), 59 deletions(-) diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 4f74307cb..2a565fb58 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -616,14 +616,14 @@ export default class EditorModel extends Model { if (!sm) return; const isCallback = isFunction(opts); - const store = this.storeData(); - sm.store(store, res => { - clb && clb(res, store); + const toStore = this.storeData(); + sm.store(toStore, res => { + isCallback && opts(res, toStore); this.set('changesCount', 0); - this.trigger('storage:store', store); + this.trigger('storage:store', toStore); }); - return store; + return toStore; } storeData() { @@ -683,7 +683,7 @@ export default class EditorModel extends Model { keys.forEach(k => load.push(k)); }); - sm.load(load, res => { + sm.load(res => { this.cacheLoad = res; clb && clb(res); setTimeout(() => this.trigger('storage:load', res)); diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index 59027500f..7eb8326a1 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -34,7 +34,10 @@ export default { // Default storage options options: { local: { - key: 'gjs-project', + key: 'gjsProject', + + // If enabled, checks if browser supports LocalStorage + checkLocal: true, }, remote: { // Custom parameters to pass with the remote request, eg. csrf token diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index 8853daf4c..aa75bdb7a 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -221,24 +221,24 @@ export default () => { * @example * storageManager.store({item1: value1, item2: value2}); * */ - store(data, resolve, reject, options = {}) { + async store(data, resolve, reject, options = {}) { + const ev = 'store'; const st = this.getCurrentStorage(); - const toStore = {}; const opts = { ...this.getCurrentOptons(), ...options }; - this.onStart('store', data); + this.onStart(ev, data); const onResult = res => { - this.onAfter('store', res); + this.onAfter(ev, res); resolve?.(res); - this.onEnd('store', res); + this.onEnd(ev, res); }; const onError = err => { reject?.(err); - this.onError('store', err); + this.onError(ev, err); }; - return st ? st.store(toStore, onResult, onError, opts) : null; + return st ? st.store(data, onResult, onError, opts) : null; }, /** @@ -253,35 +253,26 @@ export default () => { * // res -> {item1: value1} * }); * */ - load(keys, clb) { + async load(resolve, reject, options = {}) { + const ev = 'load'; const st = this.getCurrentStorage(); - const keysF = []; + const opts = { ...this.getCurrentOptons(), ...options }; let result = {}; + this.onStart(ev); - if (typeof keys === 'string') keys = [keys]; - this.onStart('load', keys); + const onResult = res => { + result = this.__clearKeys(res); + this.onAfter(ev, result); + resolve?.(result); + this.onEnd(ev, result); + }; - for (var i = 0, len = keys.length; i < len; i++) { - keysF.push(c.id + keys[i]); - } + const onError = err => { + reject?.(err); + this.onError(ev, err); + }; - if (st) { - st.load( - keysF, - res => { - result = this.__clearKeys(res); - this.onAfter('load', result); - clb && clb(result); - this.onEnd('load', result); - }, - err => { - clb && clb(result); - this.onError('load', err); - } - ); - } else { - clb && clb(result); - } + return st ? st.load(onResult, onError, opts) : resolve(result); }, /** diff --git a/src/storage_manager/model/LocalStorage.js b/src/storage_manager/model/LocalStorage.js index 3bec5a11b..51f8ffbf9 100644 --- a/src/storage_manager/model/LocalStorage.js +++ b/src/storage_manager/model/LocalStorage.js @@ -1,37 +1,46 @@ import { Model } from 'backbone'; import { hasWin } from 'utils/mixins'; +const noLocalStorage = 'localStorage not available'; + export default Model.extend({ defaults: { - checkLocal: true + checkLocal: true, }, /** * @private */ - store(data, clb = () => {}) { - if (this.hasLocal()) { - for (let key in data) localStorage.setItem(key, data[key]); + async store(data, resolve, reject, opts) { + try { + if (this.hasLocal()) { + localStorage.setItem(opts.key, JSON.stringify(data)); + return resolve(); + } else { + reject(noLocalStorage); + } + } catch (error) { + reject(error); } - - clb && clb(); }, /** * @private */ - load(keys, clb = () => {}) { - const result = {}; + async load(resolve, reject, opts) { + let result = {}; - if (this.hasLocal()) { - for (let i = 0, len = keys.length; i < len; i++) { - const value = localStorage.getItem(keys[i]); - if (value) result[keys[i]] = value; + try { + if (this.hasLocal()) { + result = JSON.parse(localStorage.getItem(opts.key) || '{}'); + resolve(result); + } else { + reject(noLocalStorage); } + } catch (error) { + reject(error); } - clb && clb(result); - return result; }, @@ -41,8 +50,7 @@ export default Model.extend({ remove(keys) { if (!this.hasLocal()) return; - for (let i = 0, len = keys.length; i < len; i++) - localStorage.removeItem(keys[i]); + for (let i = 0, len = keys.length; i < len; i++) localStorage.removeItem(keys[i]); }, /** @@ -50,13 +58,10 @@ export default Model.extend({ * @private * */ hasLocal() { - const win = hasWin(); - - if (this.get('checkLocal') && (!win || !localStorage)) { - win && console.warn("Your browser doesn't support localStorage"); + if (this.get('checkLocal') && (!hasWin() || !localStorage)) { return false; } return true; - } + }, }); From 7f3166fcbec12f97f933031cc68f249bbda756a8 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 25 Feb 2022 17:42:39 +0100 Subject: [PATCH 03/35] Up jsdoc --- src/storage_manager/index.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index aa75bdb7a..884dcca77 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -217,7 +217,6 @@ export default () => { * @param {Object} data Data in key-value format, eg. `{ item1: value1, item2: value2 }` * @param {Function} resolve Resolve callback function. The result is passed as an argument. * @param {Function} reject Reject callback function. The error is passed as an argument. - * @return {Object|null} * @example * storageManager.store({item1: value1, item2: value2}); * */ @@ -243,15 +242,12 @@ export default () => { /** * Load resource from the current storage by keys - * @param {string|Array} keys Keys to load - * @param {Function} clb Callback function + * @param {Function} resolve Resolve callback function. The result is passed as an argument. + * @param {Function} reject Reject callback function. The error is passed as an argument. * @example - * storageManager.load(['item1', 'item2'], res => { + * storageManager.load(res => { * // res -> {item1: value1, item2: value2} * }); - * storageManager.load('item1', res => { - * // res -> {item1: value1} - * }); * */ async load(resolve, reject, options = {}) { const ev = 'load'; From 88024d8b490054e83b2a1160bf8567128e3947e5 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 2 Mar 2022 15:42:06 +0100 Subject: [PATCH 04/35] Refactor editor store/load methods --- src/dom_components/index.js | 7 +- src/editor/index.js | 42 +++++---- src/editor/model/Editor.js | 100 ++++++++-------------- src/storage_manager/index.js | 21 +++-- src/storage_manager/model/LocalStorage.js | 2 +- 5 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/dom_components/index.js b/src/dom_components/index.js index c4a66558a..9fda0b1c5 100644 --- a/src/dom_components/index.js +++ b/src/dom_components/index.js @@ -290,10 +290,13 @@ export default () => { load(data) { return this.loadProjectData(data, { onResult: result => { + const wrapper = this.getWrapper(); if (isArray(result)) { - result.length && this.getComponents().reset(result); + result.length && wrapper.components(result); } else { - this.getWrapper().set(result); + const { components = [], ...rest } = result; + wrapper.set(rest); + wrapper.components(components); } }, }); diff --git a/src/editor/index.js b/src/editor/index.js index 8653d843b..fbf4184f9 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -430,22 +430,28 @@ export default (config = {}, opts = {}) => { }, /** - * Store data to the current storage - * @param {Function} clb Callback function - * @return {Object} Stored data + * Store data to the current storage. + * @param {Function} [resolve] Resolve callback function. + * @param {Function} [reject] Reject callback function. The error is passed as an argument. + * @param {Object} [options] Storage options. + * @example + * editor.store( + * () => console.log('Project stored'), + * (err) => console.error('Store error', err), + * ); */ - store(clb) { - return em.store(clb); + store(resolve, reject, options) { + return em.store(resolve, reject, options); }, /** - * Get the JSON project data, which could be stored and loaded back with `editor.loadProject(json)` + * Get the JSON project data, which could be stored and loaded back with `editor.loadProjectData(json)` * @returns {Object} * @example - * console.log(editor.getProject()); + * console.log(editor.getProjectData()); * // { pages: [...], styles: [...], ... } */ - getProject() { + getProjectData() { return em.storeData(); }, @@ -454,21 +460,27 @@ export default (config = {}, opts = {}) => { }, /** - * Load data from the current storage - * @param {Function} clb Callback function - * @return {Object} Stored data + * Load data from the current storage. + * @param {Function} [resolve] Resolve callback function. + * @param {Function} [reject] Reject callback function. The error is passed as an argument. + * @param {Object} [options] Storage options. + * @example + * editor.load( + * () => console.log('Project loaded'), + * (err) => console.error('Load error', err), + * ); */ - load(clb) { - return em.load(clb); + load(resolve, reject, options) { + return em.load(resolve, reject, options); }, /** * Load data from the JSON project * @param {Object} data Project to load * @example - * editor.loadProject({ pages: [...], styles: [...], ... }) + * editor.loadProjectData({ pages: [...], styles: [...], ... }) */ - loadProject(data) { + loadProjectData(data) { return em.loadData(data); }, diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 2a565fb58..7c760fe99 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -133,10 +133,9 @@ export default class EditorModel extends Model { /** * Should be called once all modules and plugins are loaded - * @param {Function} clb * @private */ - loadOnStart(clb = null) { + loadOnStart() { const sm = this.get('StorageManager'); // In `onLoad`, the module will try to load the data from its configurations. @@ -147,14 +146,16 @@ export default class EditorModel extends Model { const modules = this.get('modules'); modules.forEach(mdl => mdl.postLoad && mdl.postLoad(this)); this.set('readyLoad', 1); - clb && clb(); }; - if (sm && sm.canAutoload()) { - this.load(postLoad); - } else { - setTimeout(postLoad); - } + // Defer for storage load events. + setTimeout(() => { + if (sm && sm.canAutoload()) { + this.load(postLoad, error => this.logError(error)); + } else { + postLoad(); + } + }); // Create shallow editor. // Here we can create components/styles without altering/triggering the main EditorModel @@ -607,23 +608,24 @@ export default class EditorModel extends Model { /** * Store data to the current storage - * @param {Function} clb Callback function - * @return {Object} Stored data + * @param {Function} [resolve] Resolve callback function. The result is passed as an argument. + * @param {Function} [reject] Reject callback function. The error is passed as an argument. + * @param {Object} [options] Storage options. * @private */ - store(opts) { + store(resolve, reject, options) { const sm = this.get('StorageManager'); if (!sm) return; - const isCallback = isFunction(opts); - const toStore = this.storeData(); - sm.store(toStore, res => { - isCallback && opts(res, toStore); - this.set('changesCount', 0); - this.trigger('storage:store', toStore); - }); - - return toStore; + return sm.store( + this.storeData(), + res => { + resolve?.(res); + this.set('changesCount', 0); + }, + reject, + options + ); } storeData() { @@ -640,54 +642,28 @@ export default class EditorModel extends Model { /** * Load data from the current storage - * @param {Function} clb Callback function + * @param {Function} [resolve] Resolve callback function. The result is passed as an argument. + * @param {Function} [reject] Reject callback function. The error is passed as an argument. + * @param {Object} [options] Storage options. * @private */ - load(clb = null) { - this.getCacheLoad(1, res => { - this.loadData(res); - clb && clb(res); - }); - } - - loadData(data = {}) { + load(resolve, reject, options) { const sm = this.get('StorageManager'); - const result = sm.__clearKeys(data); - - this.get('storables').forEach(module => { - module.load(result); - module.postLoad && module.postLoad(this); - }); + if (!sm) return; - return result; + return sm.load( + res => { + this.loadData(res); + resolve?.(res); + }, + reject, + options + ); } - /** - * Returns cached load - * @param {Boolean} force Force to reload - * @param {Function} clb Callback function - * @return {Object} - * @private - */ - getCacheLoad(force, clb) { - if (this.cacheLoad && !force) return this.cacheLoad; - const sm = this.get('StorageManager'); - const load = []; - - if (!sm) return {}; - - this.get('storables').forEach(m => { - let key = m.storageKey; - key = isFunction(key) ? key() : key; - const keys = isArray(key) ? key : [key]; - keys.forEach(k => load.push(k)); - }); - - sm.load(res => { - this.cacheLoad = res; - clb && clb(res); - setTimeout(() => this.trigger('storage:load', res)); - }); + loadData(data = {}) { + this.get('storables').forEach(module => module.load(data)); + return data; } /** diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index 884dcca77..401fef12e 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -215,8 +215,9 @@ export default () => { /** * Store data in the current storage. * @param {Object} data Data in key-value format, eg. `{ item1: value1, item2: value2 }` - * @param {Function} resolve Resolve callback function. The result is passed as an argument. - * @param {Function} reject Reject callback function. The error is passed as an argument. + * @param {Function} [resolve] Resolve callback function. + * @param {Function} [reject] Reject callback function. The error is passed as an argument. + * @param {Object} [options] Storage options. * @example * storageManager.store({item1: value1, item2: value2}); * */ @@ -226,6 +227,8 @@ export default () => { const opts = { ...this.getCurrentOptons(), ...options }; this.onStart(ev, data); + if (!st) return null; + const onResult = res => { this.onAfter(ev, res); resolve?.(res); @@ -237,13 +240,14 @@ export default () => { this.onError(ev, err); }; - return st ? st.store(data, onResult, onError, opts) : null; + return await st.store(data, onResult, onError, opts); }, /** * Load resource from the current storage by keys - * @param {Function} resolve Resolve callback function. The result is passed as an argument. - * @param {Function} reject Reject callback function. The error is passed as an argument. + * @param {Function} [resolve] Resolve callback function. The result is passed as an argument. + * @param {Function} [reject] Reject callback function. The error is passed as an argument. + * @param {Object} [options] Storage options. * @example * storageManager.load(res => { * // res -> {item1: value1, item2: value2} @@ -256,6 +260,8 @@ export default () => { let result = {}; this.onStart(ev); + if (!st) return resolve(result); + const onResult = res => { result = this.__clearKeys(res); this.onAfter(ev, result); @@ -268,7 +274,7 @@ export default () => { this.onError(ev, err); }; - return st ? st.load(onResult, onError, opts) : resolve(result); + return await st.load(onResult, onError, opts); }, /** @@ -331,7 +337,8 @@ export default () => { onAfter(ctx, data) { if (em) { em.trigger(eventAfter); - ctx && em.trigger(`${eventAfter}:${ctx}`, data); + em.trigger(`${eventAfter}:${ctx}`, data); + em.trigger(`storage:${ctx}`, data); } }, diff --git a/src/storage_manager/model/LocalStorage.js b/src/storage_manager/model/LocalStorage.js index 51f8ffbf9..e629617b9 100644 --- a/src/storage_manager/model/LocalStorage.js +++ b/src/storage_manager/model/LocalStorage.js @@ -15,7 +15,7 @@ export default Model.extend({ try { if (this.hasLocal()) { localStorage.setItem(opts.key, JSON.stringify(data)); - return resolve(); + return resolve(data); } else { reject(noLocalStorage); } From 0884befc805f464c110a5726c03eaf4d51abb5b6 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 2 Mar 2022 15:47:15 +0100 Subject: [PATCH 05/35] Update LocalStorage --- src/storage_manager/model/LocalStorage.js | 43 +++++++---------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/src/storage_manager/model/LocalStorage.js b/src/storage_manager/model/LocalStorage.js index e629617b9..2e6bcf0cc 100644 --- a/src/storage_manager/model/LocalStorage.js +++ b/src/storage_manager/model/LocalStorage.js @@ -1,37 +1,29 @@ -import { Model } from 'backbone'; +import { Model } from 'common'; import { hasWin } from 'utils/mixins'; const noLocalStorage = 'localStorage not available'; -export default Model.extend({ - defaults: { - checkLocal: true, - }, - - /** - * @private - */ +export default class LocalStorage extends Model { async store(data, resolve, reject, opts) { try { - if (this.hasLocal()) { + if (this.hasLocal(opts)) { localStorage.setItem(opts.key, JSON.stringify(data)); - return resolve(data); + resolve(data); } else { reject(noLocalStorage); } } catch (error) { reject(error); } - }, - /** - * @private - */ + return data; + } + async load(resolve, reject, opts) { let result = {}; try { - if (this.hasLocal()) { + if (this.hasLocal(opts)) { result = JSON.parse(localStorage.getItem(opts.key) || '{}'); resolve(result); } else { @@ -42,26 +34,17 @@ export default Model.extend({ } return result; - }, - - /** - * @private - */ - remove(keys) { - if (!this.hasLocal()) return; - - for (let i = 0, len = keys.length; i < len; i++) localStorage.removeItem(keys[i]); - }, + } /** * Check storage environment * @private * */ - hasLocal() { - if (this.get('checkLocal') && (!hasWin() || !localStorage)) { + hasLocal(opts = {}) { + if (opts.checkLocal && (!hasWin() || !localStorage)) { return false; } return true; - }, -}); + } +} From 0796ea73d4182e3dea7427df8c847c6e254237e4 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 9 Mar 2022 16:18:35 +0100 Subject: [PATCH 06/35] Update storageManager configs --- src/canvas/model/Frame.js | 24 ++-------- src/storage_manager/config/config.js | 71 ++++++---------------------- 2 files changed, 20 insertions(+), 75 deletions(-) diff --git a/src/canvas/model/Frame.js b/src/canvas/model/Frame.js index 65895c19a..e0f27779c 100644 --- a/src/canvas/model/Frame.js +++ b/src/canvas/model/Frame.js @@ -26,7 +26,7 @@ export default class Frame extends Model { component: '', styles: '', _undo: true, - _undoexc: ['changesCount'] + _undoexc: ['changesCount'], }; } @@ -125,12 +125,7 @@ export default class Frame extends Model { getHeadByAttr(attr, value, tag) { const head = this.getHead(); - return head.filter( - item => - item.attributes && - item.attributes[attr] == value && - (!tag || tag === item.tag) - )[0]; + return head.filter(item => item.attributes && item.attributes[attr] == value && (!tag || tag === item.tag))[0]; } removeHeadByAttr(attr, value, tag) { @@ -151,8 +146,8 @@ export default class Frame extends Model { tag, attributes: { href, - rel: 'stylesheet' - } + rel: 'stylesheet', + }, }); } @@ -165,7 +160,7 @@ export default class Frame extends Model { !this.getHeadByAttr('src', src, tag) && this.addHeadItem({ tag, - attributes: { src } + attributes: { src }, }); } @@ -184,17 +179,8 @@ export default class Frame extends Model { toJSON(opts = {}) { const obj = Model.prototype.toJSON.call(this, opts); - const { em } = this; - const sm = em && em.get('StorageManager'); - const smc = sm && sm.getConfig(); const defaults = result(this, 'defaults'); - if (smc && !opts.fromUndo) { - const opts = { component: this.getComponent() }; - if (smc.storeHtml) obj.html = em.getHtml(opts); - if (smc.storeCss) obj.css = em.getCss(opts); - } - if (opts.fromUndo) delete obj.component; delete obj.styles; delete obj.changesCount; diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index 7eb8326a1..75ad7ea0b 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -1,36 +1,21 @@ export default { // Prefix identifier that will be used inside storing and loading + // @deprecated id: 'gjs-', + // Default storage type. Available: local | remote + type: 'local', + // Enable/Disable autosaving autosave: true, - // Indicates if load data inside editor after init + // Enable/Disable autoload of data on editor init autoload: true, - // Indicates which storage to use. Available: local | remote - type: 'local', - // If autosave enabled, indicates how many steps (general changes to structure) // need to be done before save. Useful with remoteStorage to reduce remote calls stepsBeforeSave: 1, - //Enable/Disable components model (JSON format) - storeComponents: true, - - //Enable/Disable styles model (JSON format) - storeStyles: true, - - //Enable/Disable saving HTML template - storeHtml: true, - - //Enable/Disable saving CSS template - storeCss: true, - - // ONLY FOR LOCAL STORAGE - // If enabled, checks if browser supports Local Storage - checkLocal: true, - // Default storage options options: { local: { @@ -42,20 +27,27 @@ export default { remote: { // Custom parameters to pass with the remote request, eg. csrf token params: {}, + // Custom headers headers: {}, - // Endpoint where to save all stuff + + // Endpoint URL where to store data project urlStore: '', - // Endpoint where to fetch data + + // Endpoint URL where to load data project urlLoad: '', + //Callback before request beforeSend(jqXHR, settings) {}, + //Callback after request onComplete(jqXHR, status) {}, + // set contentType paramater of $.ajax // true: application/json; charset=utf-8' // false: 'x-www-form-urlencoded' contentTypeJson: true, + // Pass custom options to fetch API (remote storage) // You can pass a simple object: { someOption: 'someValue' } // or a function which returns and object to add: @@ -63,6 +55,7 @@ export default { // return currentOpts.method === 'post' ? { method: 'patch' } : {}; // } fetchOptions: '', + credentials: 'include', /** @@ -74,38 +67,4 @@ export default { recovery: true, }, }, - - // ONLY FOR REMOTE STORAGE - // Custom parameters to pass with the remote storage request, eg. csrf token - params: {}, - - // Custom headers for the remote storage request - headers: {}, - - // Endpoint where to save all stuff - urlStore: '', - - // Endpoint where to fetch data - urlLoad: '', - - //Callback before request - beforeSend(jqXHR, settings) {}, - - //Callback after request - onComplete(jqXHR, status) {}, - - // set contentType paramater of $.ajax - // true: application/json; charset=utf-8' - // false: 'x-www-form-urlencoded' - contentTypeJson: true, - - credentials: 'include', - - // Pass custom options to fetch API (remote storage) - // You can pass a simple object: { someOption: 'someValue' } - // or a function which returns and object to add: - // currentOpts => { - // return currentOpts.method === 'post' ? { method: 'patch' } : {}; - // } - fetchOptions: '', }; From 448d8e689b63ef0f4a470e88ec85869f8acf2b5c Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 10 Mar 2022 15:12:14 +0100 Subject: [PATCH 07/35] Make init always via pages --- src/common/module.js | 2 +- src/dom_components/index.js | 15 +-------------- src/editor/model/Editor.js | 29 +++++++++++++++++------------ src/pages/index.js | 1 - 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/common/module.js b/src/common/module.js index 8d1b1851b..518774448 100644 --- a/src/common/module.js +++ b/src/common/module.js @@ -28,7 +28,7 @@ export default { } if (onResult) { - onResult(result); + result && onResult(result); } else if (result && result.length) { (all || this.getAll()).reset(result); } diff --git a/src/dom_components/index.js b/src/dom_components/index.js index 9fda0b1c5..7674e8dd2 100644 --- a/src/dom_components/index.js +++ b/src/dom_components/index.js @@ -272,21 +272,9 @@ export default () => { em.listenTo(selected, 'remove', (sel, c, opts) => this.selectRemove(selected.getComponent(sel), opts)); } - if (em.get('hasPages')) { - c.components = ''; - } - return this; }, - /** - * On load callback - * @private - */ - onLoad() { - c.components && this.setComponents(c.components, { silent: 1 }); - }, - load(data) { return this.loadProjectData(data, { onResult: result => { @@ -303,8 +291,7 @@ export default () => { }, store() { - if (this.em.get('hasPages')) return {}; - return this.getProjectData(this.getWrapper()); + return {}; }, /** diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 0ac2e6c98..b11e23736 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -1,4 +1,4 @@ -import { isUndefined, isFunction, isArray, contains, toArray, keys, bindAll } from 'underscore'; +import { isUndefined, isArray, contains, toArray, keys, bindAll } from 'underscore'; import Backbone from 'backbone'; import $ from 'utils/cash-dom'; import Extender from 'utils/extender'; @@ -66,21 +66,23 @@ export default class EditorModel extends Model { }; } - initialize(c = {}) { - this.config = c; - this.set('Config', c); + initialize(conf = {}) { + this.config = conf; + const { config } = this; + this.set('Config', config); this.set('modules', []); this.set('toLoad', []); this.set('storables', []); this.set('selected', new Selected()); - this.set('dmode', c.dragMode); - this.set('hasPages', !!c.pageManager); - const el = c.el; - const log = c.log; + this.set('dmode', config.dragMode); + const { el, log } = config; const toLog = log === true ? keys(logs) : isArray(log) ? log : []; bindAll(this, 'initBaseColorPicker'); - if (el && c.fromElement) this.config.components = el.innerHTML; + if (el && config.fromElement) { + config.components = el.innerHTML; + } + this.attrsOrig = el ? toArray(el.attributes).reduce((res, next) => { res[next.nodeName] = next.nodeValue; @@ -88,6 +90,11 @@ export default class EditorModel extends Model { }, {}) : ''; + // Move components to pages + if (config.components && !config.pageManager) { + config.pageManager = { pages: [{ component: config.components }] }; + } + // Load modules deps.forEach(name => this.loadModule(name)); this.on('change:componentHovered', this.componentHovered, this); @@ -202,15 +209,13 @@ export default class EditorModel extends Model { const name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1); const cfgParent = !isUndefined(config[name]) ? config[name] : config[Mod.name]; const cfg = cfgParent === true ? {} : cfgParent || {}; - const sm = this.get('StorageManager'); cfg.pStylePrefix = config.pStylePrefix || ''; if (!isUndefined(cfgParent) && !cfgParent) { cfg._disable = 1; } - if (Mod.storageKey && Mod.store && Mod.load && sm) { - cfg.stm = sm; + if (Mod.storageKey && Mod.store && Mod.load) { // DomComponents should be load before CSS Composer const mth = name == 'domComponents' ? 'unshift' : 'push'; this.get('storables')[mth](Mod); diff --git a/src/pages/index.js b/src/pages/index.js index 28489348b..b7fc32ccc 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -275,7 +275,6 @@ export default () => { }, store() { - if (!this.em.get('hasPages')) return {}; return this.getProjectData(); }, From b80e1eca26ef661e20aa1fd5674e583c5c0e485e Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 10 Mar 2022 17:02:38 +0100 Subject: [PATCH 08/35] Refactor store and load methods --- src/editor/index.js | 46 +++++++--------- src/editor/model/Editor.js | 67 ++++++++--------------- src/storage_manager/index.js | 49 ++++++++--------- src/storage_manager/model/LocalStorage.js | 37 +++---------- 4 files changed, 72 insertions(+), 127 deletions(-) diff --git a/src/editor/index.js b/src/editor/index.js index fbf4184f9..4b9c10a79 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -431,17 +431,24 @@ export default (config = {}, opts = {}) => { /** * Store data to the current storage. - * @param {Function} [resolve] Resolve callback function. - * @param {Function} [reject] Reject callback function. The error is passed as an argument. * @param {Object} [options] Storage options. + * @returns {Object} Stored data. * @example - * editor.store( - * () => console.log('Project stored'), - * (err) => console.error('Store error', err), - * ); + * const storedData = await editor.store(); */ - store(resolve, reject, options) { - return em.store(resolve, reject, options); + async store(options) { + return await em.store(options); + }, + + /** + * Load data from the current storage. + * @param {Object} [options] Storage options. + * @returns {Object} Loaded data. + * @example + * const data = await editor.load(); + */ + async load(options) { + return await em.load(options); }, /** @@ -455,25 +462,6 @@ export default (config = {}, opts = {}) => { return em.storeData(); }, - storeData() { - return em.storeData(); - }, - - /** - * Load data from the current storage. - * @param {Function} [resolve] Resolve callback function. - * @param {Function} [reject] Reject callback function. The error is passed as an argument. - * @param {Object} [options] Storage options. - * @example - * editor.load( - * () => console.log('Project loaded'), - * (err) => console.error('Load error', err), - * ); - */ - load(resolve, reject, options) { - return em.load(resolve, reject, options); - }, - /** * Load data from the JSON project * @param {Object} data Project to load @@ -484,6 +472,10 @@ export default (config = {}, opts = {}) => { return em.loadData(data); }, + storeData() { + return em.storeData(); + }, + loadData(data) { return em.loadData(data); }, diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index b11e23736..b50a191bb 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -143,8 +143,6 @@ export default class EditorModel extends Model { * @private */ loadOnStart() { - const sm = this.get('StorageManager'); - // In `onLoad`, the module will try to load the data from its configurations. this.get('toLoad').forEach(mdl => mdl.onLoad()); @@ -156,12 +154,15 @@ export default class EditorModel extends Model { }; // Defer for storage load events. - setTimeout(() => { - if (sm && sm.canAutoload()) { - this.load(postLoad, error => this.logError(error)); - } else { - postLoad(); + setTimeout(async () => { + if (this.get('StorageManager').canAutoload()) { + try { + await this.load(); + } catch (error) { + this.logError(error); + } } + postLoad(); }); // Create shallow editor. @@ -612,25 +613,24 @@ export default class EditorModel extends Model { } /** - * Store data to the current storage - * @param {Function} [resolve] Resolve callback function. The result is passed as an argument. - * @param {Function} [reject] Reject callback function. The error is passed as an argument. - * @param {Object} [options] Storage options. + * Store data to the current storage. * @private */ - store(resolve, reject, options) { - const sm = this.get('StorageManager'); - if (!sm) return; + async store(options) { + const data = this.storeData(); + await this.get('StorageManager').store(data, options); + this.set('changesCount', 0); + return data; + } - return sm.store( - this.storeData(), - res => { - resolve?.(res); - this.set('changesCount', 0); - }, - reject, - options - ); + /** + * Load data from the current storage. + * @private + */ + async load(options) { + const result = await this.get('StorageManager').load(options); + this.loadData(result); + return result; } storeData() { @@ -645,27 +645,6 @@ export default class EditorModel extends Model { return JSON.parse(JSON.stringify(result)); } - /** - * Load data from the current storage - * @param {Function} [resolve] Resolve callback function. The result is passed as an argument. - * @param {Function} [reject] Reject callback function. The error is passed as an argument. - * @param {Object} [options] Storage options. - * @private - */ - load(resolve, reject, options) { - const sm = this.get('StorageManager'); - if (!sm) return; - - return sm.load( - res => { - this.loadData(res); - resolve?.(res); - }, - reject, - options - ); - } - loadData(data = {}) { this.get('storables').forEach(module => module.load(data)); return data; diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index 401fef12e..e4ee2898a 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -215,32 +215,29 @@ export default () => { /** * Store data in the current storage. * @param {Object} data Data in key-value format, eg. `{ item1: value1, item2: value2 }` - * @param {Function} [resolve] Resolve callback function. - * @param {Function} [reject] Reject callback function. The error is passed as an argument. * @param {Object} [options] Storage options. + * @returns {Object} Stored data. * @example - * storageManager.store({item1: value1, item2: value2}); + * await storageManager.store({item1: value1, item2: value2}); * */ - async store(data, resolve, reject, options = {}) { + async store(data, options = {}) { const ev = 'store'; const st = this.getCurrentStorage(); const opts = { ...this.getCurrentOptons(), ...options }; this.onStart(ev, data); - if (!st) return null; - - const onResult = res => { - this.onAfter(ev, res); - resolve?.(res); - this.onEnd(ev, res); - }; + if (!st) return data; - const onError = err => { - reject?.(err); - this.onError(ev, err); - }; + try { + await st.store(data, opts); + this.onAfter(ev, data); + this.onEnd(ev, data); + } catch (error) { + this.onError(ev, error); + throw error; + } - return await st.store(data, onResult, onError, opts); + return data; }, /** @@ -253,28 +250,26 @@ export default () => { * // res -> {item1: value1, item2: value2} * }); * */ - async load(resolve, reject, options = {}) { + async load(options = {}) { const ev = 'load'; const st = this.getCurrentStorage(); const opts = { ...this.getCurrentOptons(), ...options }; let result = {}; this.onStart(ev); - if (!st) return resolve(result); + if (!st) return result; - const onResult = res => { + try { + const res = await st.load(opts); result = this.__clearKeys(res); this.onAfter(ev, result); - resolve?.(result); this.onEnd(ev, result); - }; - - const onError = err => { - reject?.(err); - this.onError(ev, err); - }; + } catch (error) { + this.onError(ev, error); + throw error; + } - return await st.load(onResult, onError, opts); + return result; }, /** diff --git a/src/storage_manager/model/LocalStorage.js b/src/storage_manager/model/LocalStorage.js index 2e6bcf0cc..a8f57e20f 100644 --- a/src/storage_manager/model/LocalStorage.js +++ b/src/storage_manager/model/LocalStorage.js @@ -1,47 +1,26 @@ import { Model } from 'common'; import { hasWin } from 'utils/mixins'; -const noLocalStorage = 'localStorage not available'; - export default class LocalStorage extends Model { - async store(data, resolve, reject, opts) { - try { - if (this.hasLocal(opts)) { - localStorage.setItem(opts.key, JSON.stringify(data)); - resolve(data); - } else { - reject(noLocalStorage); - } - } catch (error) { - reject(error); + async store(data, opts = {}) { + if (this.hasLocal(opts, true)) { + localStorage.setItem(opts.key, JSON.stringify(data)); } - - return data; } - async load(resolve, reject, opts) { + async load(opts = {}) { let result = {}; - try { - if (this.hasLocal(opts)) { - result = JSON.parse(localStorage.getItem(opts.key) || '{}'); - resolve(result); - } else { - reject(noLocalStorage); - } - } catch (error) { - reject(error); + if (this.hasLocal(opts, true)) { + result = JSON.parse(localStorage.getItem(opts.key) || '{}'); } return result; } - /** - * Check storage environment - * @private - * */ - hasLocal(opts = {}) { + hasLocal(opts = {}, thr) { if (opts.checkLocal && (!hasWin() || !localStorage)) { + if (thr) throw new Error('localStorage not available'); return false; } From 8c8e1f0ffddc26c4eaebd3b92046895906a56a8d Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 11 Mar 2022 14:51:08 +0100 Subject: [PATCH 09/35] Update project load --- src/common/module.js | 16 +++++++++++++--- src/dom_components/index.js | 1 + src/dom_components/model/Component.js | 6 +++++- src/editor/model/Editor.js | 6 +++--- src/pages/index.js | 3 ++- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/common/module.js b/src/common/module.js index 518774448..a45b4006b 100644 --- a/src/common/module.js +++ b/src/common/module.js @@ -15,8 +15,10 @@ export default { return obj; }, - loadProjectData(data = {}, { all, def = [], onResult } = {}) { + loadProjectData(data = {}, { all, def = [], onResult, reset } = {}) { const key = this.storageKey; + const opts = { action: 'load' }; + const coll = all || this.getAll(); let result = data[key] || def; if (typeof result == 'string') { @@ -27,15 +29,23 @@ export default { } } + reset && result && coll.reset(null, opts); + if (onResult) { - result && onResult(result); + result && onResult(result, opts); } else if (result && result.length) { - (all || this.getAll()).reset(result); + coll.reset(result, opts); } return result; }, + clear(opts = {}) { + const { all } = this; + all && all.reset(null, opts); + return this; + }, + __getConfig(name) { const res = this.config || {}; return name ? res[name] : res; diff --git a/src/dom_components/index.js b/src/dom_components/index.js index 7674e8dd2..49ea8df0b 100644 --- a/src/dom_components/index.js +++ b/src/dom_components/index.js @@ -277,6 +277,7 @@ export default () => { load(data) { return this.loadProjectData(data, { + def: null, onResult: result => { const wrapper = this.getWrapper(); if (isArray(result)) { diff --git a/src/dom_components/model/Component.js b/src/dom_components/model/Component.js index eb6853247..316f81648 100644 --- a/src/dom_components/model/Component.js +++ b/src/dom_components/model/Component.js @@ -1677,7 +1677,11 @@ export default class Component extends Model.extend(Styleable) { const coll = this.collection; const remove = () => { coll && coll.remove(this, { ...opts, action: 'remove-component' }); - opts.root && this.components(''); + // Component without parent + if (!coll) { + this.components('', opts); + this.components().removeChildren(this, null, opts); + } }; const rmOpts = { ...opts }; [this, em].map(i => i.trigger('component:remove:before', this, remove, rmOpts)); diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index b50a191bb..a2e0cd462 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -217,8 +217,8 @@ export default class EditorModel extends Model { } if (Mod.storageKey && Mod.store && Mod.load) { - // DomComponents should be load before CSS Composer - const mth = name == 'domComponents' ? 'unshift' : 'push'; + // Components should be loaded before CSS due to reset + const mth = ['domComponents', 'pageManager'].indexOf(name) >= 0 ? 'unshift' : 'push'; this.get('storables')[mth](Mod); } @@ -629,7 +629,7 @@ export default class EditorModel extends Model { */ async load(options) { const result = await this.get('StorageManager').load(options); - this.loadData(result); + this.skip(() => this.loadData(result)); return result; } diff --git a/src/pages/index.js b/src/pages/index.js index b7fc32ccc..f69b867cb 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -98,6 +98,7 @@ export default () => { this.em = em; const pages = new Pages([], cnf); this.pages = pages; + this.all = pages; const model = new Model({ _undo: true }); this.model = model; pages.on('add', (p, c, o) => em.trigger(evPageAdd, p, o)); @@ -279,7 +280,7 @@ export default () => { }, load(data) { - return this.loadProjectData(data, { all: this.pages }); + return this.loadProjectData(data, { all: this.pages, reset: true, def: null }); }, _createId() { From 076a1915fd83ccd6f778159ba658ea674fd94838 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 11 Mar 2022 15:55:48 +0100 Subject: [PATCH 10/35] Refactor RemoteStorage --- src/storage_manager/config/config.js | 9 -- src/storage_manager/model/RemoteStorage.js | 167 ++++++--------------- 2 files changed, 44 insertions(+), 132 deletions(-) diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index 75ad7ea0b..5b9de7271 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -25,9 +25,6 @@ export default { checkLocal: true, }, remote: { - // Custom parameters to pass with the remote request, eg. csrf token - params: {}, - // Custom headers headers: {}, @@ -37,12 +34,6 @@ export default { // Endpoint URL where to load data project urlLoad: '', - //Callback before request - beforeSend(jqXHR, settings) {}, - - //Callback after request - onComplete(jqXHR, status) {}, - // set contentType paramater of $.ajax // true: application/json; charset=utf-8' // false: 'x-www-form-urlencoded' diff --git a/src/storage_manager/model/RemoteStorage.js b/src/storage_manager/model/RemoteStorage.js index 0555628b0..423a4817e 100644 --- a/src/storage_manager/model/RemoteStorage.js +++ b/src/storage_manager/model/RemoteStorage.js @@ -1,146 +1,67 @@ -import Backbone from 'backbone'; import fetch from 'utils/fetch'; -import { isUndefined, isFunction } from 'underscore'; +import { Model } from 'common'; +import { isUndefined, isFunction, isString } from 'underscore'; -export default Backbone.Model.extend({ - fetch, - - defaults: { - urlStore: '', - urlLoad: '', - params: {}, - beforeSend() {}, - onComplete() {}, - contentTypeJson: false, - credentials: 'include', - fetchOptions: '' - }, - - /** - * Triggered before the request is started - * @private - */ - onStart() { - const em = this.get('em'); - const before = this.get('beforeSend'); - before && before(); - }, - - /** - * Triggered on request error - * @param {Object} err Error - * @param {Function} [clbErr] Error callback - * @private - */ - onError(err, clbErr) { - if (clbErr) { - clbErr(err); - } else { - const em = this.get('em'); - console.error(err); - em && em.trigger('storage:error', err); - } - }, - - /** - * Triggered on request response - * @param {string} text Response text - * @private - */ - onResponse(text, clb) { - const em = this.get('em'); - const complete = this.get('onComplete'); - const typeJson = this.get('contentTypeJson'); - const parsable = text && typeof text === 'string'; - const res = typeJson && parsable ? JSON.parse(text) : text; - complete && complete(res); - clb && clb(res); - em && em.trigger('storage:response', res); - }, - - store(data, clb, clbErr) { - const body = {}; - - for (let key in data) { - body[key] = data[key]; - } +export default class RemoteStorage extends Model { + async store(data, opts = {}) { + await this.request(opts.urlStore, this.__props(opts, data), opts); + } - this.request(this.get('urlStore'), { body }, clb, clbErr); - }, + async load(opts) { + return await this.request(opts.urlLoad, this.__props(opts), opts); + } - load(keys, clb, clbErr) { - this.request(this.get('urlLoad'), { method: 'get' }, clb, clbErr); - }, + request(url, props = {}, opts = {}) { + return fetch(url, props) + .then(res => { + const result = res.text(); + const isOk = ((res.status / 200) | 0) === 1; + return isOk ? result : result.then(Promise.reject); + }) + .then(text => { + const parsable = text && isString(text); + return opts.contentTypeJson && parsable ? JSON.parse(text) : text; + }); + } - /** - * Execute remote request - * @param {string} url Url - * @param {Object} [opts={}] Options - * @param {Function} [clb=null] Callback - * @param {Function} [clbErr=null] Error callback - * @private - */ - request(url, opts = {}, clb = null, clbErr = null) { - const typeJson = this.get('contentTypeJson'); - const headers = this.get('headers') || {}; - const params = this.get('params'); + __props(opts = {}, data) { + const typeJson = opts.contentTypeJson; + const headers = opts.headers || {}; + const fetchOpts = opts.fetchOptions || {}; const reqHead = 'X-Requested-With'; const typeHead = 'Content-Type'; - const bodyObj = opts.body || {}; - let fetchOptions; let body; - for (let param in params) { - bodyObj[param] = params[param]; - } - if (isUndefined(headers[reqHead])) { headers[reqHead] = 'XMLHttpRequest'; } - // With `fetch`, have to send FormData without any 'Content-Type' - // https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post - if (isUndefined(headers[typeHead]) && typeJson) { headers[typeHead] = 'application/json; charset=utf-8'; } - if (typeJson) { - body = JSON.stringify(bodyObj); - } else { - body = new FormData(); + if (data) { + if (typeJson) { + body = JSON.stringify(data); + } else { + body = new FormData(); - for (let bodyKey in bodyObj) { - body.append(bodyKey, bodyObj[bodyKey]); + for (let key in data) { + body.append(key, data[key]); + } } } - fetchOptions = { - method: opts.method || 'post', - credentials: this.get('credentials'), - headers - }; - // Body should only be included on POST method - if (fetchOptions.method === 'post') { - fetchOptions.body = body; - } - - const fetchOpts = this.get('fetchOptions') || {}; - const addOpts = isFunction(fetchOpts) - ? fetchOpts(fetchOptions) - : fetchOptions; + const result = { + method: body ? 'post' : 'get', + credentials: opts.credentials, + headers, + body, + }; - this.onStart(); - this.fetch(url, { - ...fetchOptions, - ...(addOpts || {}) - }) - .then(res => - ((res.status / 200) | 0) == 1 - ? res.text() - : res.text().then(text => Promise.reject(text)) - ) - .then(text => this.onResponse(text, clb)) - .catch(err => this.onError(err, clbErr)); + return { + ...result, + ...(isFunction(fetchOpts) ? fetchOpts(result) : fetchOpts), + }; } -}); +} From 7540c1ed00b66f18374b5baa0d4b09cbf16e4164 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 11 Mar 2022 17:01:03 +0100 Subject: [PATCH 11/35] up conig --- src/storage_manager/config/config.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index 5b9de7271..425523473 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -12,6 +12,15 @@ export default { // Enable/Disable autoload of data on editor init autoload: true, + /** + * (TODO) In case the remote storage is selected, and this options is enabled, the project + * will be also stored on the local one. + * The local data are cleared on every sucessful remote save. In case the remote storage + * fails (eg. network issue), on project reload, a dialog with the possibility to recovery + * previous data will be shown. + */ + recovery: false, + // If autosave enabled, indicates how many steps (general changes to structure) // need to be done before save. Useful with remoteStorage to reduce remote calls stepsBeforeSave: 1, @@ -48,14 +57,6 @@ export default { fetchOptions: '', credentials: 'include', - - /** - * (TODO) This will enable the store of the project also on the local storage. - * The local data are cleared on every sucessful remote save. In case the remote storage - * fails (eg. network issue), on project reload, a dialog with the possibility to recovery - * previous data will be shown. - */ - recovery: true, }, }, }; From 40efcb70d69237ae30b64fe700488ab5aedf2ffb Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 11 Mar 2022 17:15:29 +0100 Subject: [PATCH 12/35] Catch error for store --- src/editor/model/Editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index a2e0cd462..f4f04e3a1 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -193,7 +193,7 @@ export default class EditorModel extends Model { } if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) { - this.store(); + this.store().catch(err => this.logError(err)); } } From 08e207c650c6d9ebf5edc5a0360d71e77bf488c9 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 11 Mar 2022 17:19:01 +0100 Subject: [PATCH 13/35] Update load jsdoc --- src/storage_manager/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index e4ee2898a..d9e8b052c 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -242,13 +242,10 @@ export default () => { /** * Load resource from the current storage by keys - * @param {Function} [resolve] Resolve callback function. The result is passed as an argument. - * @param {Function} [reject] Reject callback function. The error is passed as an argument. * @param {Object} [options] Storage options. + * @returns {Object} Loaded data. * @example - * storageManager.load(res => { - * // res -> {item1: value1, item2: value2} - * }); + * const data = await storageManager.load(); * */ async load(options = {}) { const ev = 'load'; From 00247015b7d0147affc3407d3edd7a4d486c7943 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 11 Mar 2022 17:21:15 +0100 Subject: [PATCH 14/35] Update jsdoc --- src/storage_manager/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index d9e8b052c..7065feaf1 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -214,11 +214,12 @@ export default () => { /** * Store data in the current storage. - * @param {Object} data Data in key-value format, eg. `{ item1: value1, item2: value2 }` + * @param {Object} data Project data. * @param {Object} [options] Storage options. * @returns {Object} Stored data. * @example - * await storageManager.store({item1: value1, item2: value2}); + * const data = editor.getProjectData(); + * await storageManager.store(data); * */ async store(data, options = {}) { const ev = 'store'; @@ -246,6 +247,7 @@ export default () => { * @returns {Object} Loaded data. * @example * const data = await storageManager.load(); + * editor.loadProjectData(data); * */ async load(options = {}) { const ev = 'load'; From 835a1b4e73cf3556005091536b18c1d71c2abc3d Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 11 Mar 2022 17:36:53 +0100 Subject: [PATCH 15/35] Refactor load/store in storageManager --- src/storage_manager/index.js | 40 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index 7065feaf1..3c371a1e1 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -222,23 +222,10 @@ export default () => { * await storageManager.store(data); * */ async store(data, options = {}) { - const ev = 'store'; const st = this.getCurrentStorage(); const opts = { ...this.getCurrentOptons(), ...options }; - this.onStart(ev, data); - - if (!st) return data; - - try { - await st.store(data, opts); - this.onAfter(ev, data); - this.onEnd(ev, data); - } catch (error) { - this.onError(ev, error); - throw error; - } - return data; + return await this.__exec(st, opts, data); }, /** @@ -250,17 +237,30 @@ export default () => { * editor.loadProjectData(data); * */ async load(options = {}) { - const ev = 'load'; const st = this.getCurrentStorage(); const opts = { ...this.getCurrentOptons(), ...options }; - let result = {}; - this.onStart(ev); - if (!st) return result; + return await this.__exec(st, opts); + }, + + async __exec(storage, opts, data) { + const ev = data ? 'store' : 'load'; + let result; + + this.onStart(ev, data); + + if (!storage) { + return data || {}; + } try { - const res = await st.load(opts); - result = this.__clearKeys(res); + if (data) { + await storage.store(data, opts); + result = data; + } else { + result = await storage.load(opts); + result = this.__clearKeys(result); + } this.onAfter(ev, result); this.onEnd(ev, result); } catch (error) { From fd53793986552234660587cb44aed3e94e86198e Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 14 Mar 2022 12:41:19 +0100 Subject: [PATCH 16/35] Update jsdoc --- src/editor/index.js | 1 + src/editor/model/Editor.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/editor/index.js b/src/editor/index.js index 4b9c10a79..d6e5e3c8f 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -431,6 +431,7 @@ export default (config = {}, opts = {}) => { /** * Store data to the current storage. + * This will reset the counter of changes (`editor.getDirtyCount()`). * @param {Object} [options] Storage options. * @returns {Object} Stored data. * @example diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index f4f04e3a1..4efc0ddd3 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -629,7 +629,7 @@ export default class EditorModel extends Model { */ async load(options) { const result = await this.get('StorageManager').load(options); - this.skip(() => this.loadData(result)); + this.loadData(result); return result; } From 11a69b68d698b8c57a98ad301d771a4a1c87d6db Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 14 Mar 2022 17:14:19 +0100 Subject: [PATCH 17/35] Add onStore and onLoad options to remove --- src/storage_manager/config/config.js | 10 ++++++++ src/storage_manager/index.js | 35 +++++++--------------------- src/utils/mixins.js | 21 +++++++++++++++++ 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index 425523473..ec105ca70 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -57,6 +57,16 @@ export default { fetchOptions: '', credentials: 'include', + + /** + * Edit project data before sending them to the storage. + */ + onStore: data => data, + + /** + * Edit project data before loading them from the storage. + */ + onLoad: result => result, }, }, }; diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index 3c371a1e1..d0b41fbb6 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -53,6 +53,7 @@ import defaults from './config/config'; import LocalStorage from './model/LocalStorage'; import RemoteStorage from './model/RemoteStorage'; +import { deepMerge } from 'utils/mixins'; const eventStart = 'storage:start'; const eventAfter = 'storage:after'; @@ -66,33 +67,10 @@ export default () => { var defaultStorages = {}; return { - /** - * Name of the module - * @type {String} - * @private - */ name: 'StorageManager', - /** - * Initialize module. Automatically called with a new instance of the editor - * @param {Object} config Configurations - * @param {string} [config.id='gjs-'] The prefix for the fields, useful to differentiate storing/loading - * with multiple editors on the same page. For example, in local storage, the item of HTML will be saved like 'gjs-html' - * @param {Boolean} [config.autosave=true] Indicates if autosave mode is enabled, works in conjunction with stepsBeforeSave - * @param {number} [config.stepsBeforeSave=1] If autosave enabled, indicates how many steps/changes are necessary - * before autosave is triggered - * @param {string} [config.type='local'] Default storage type. Available: 'local' | 'remote' | ''(do not store) - * @private - * @example - * ... - * { - * autosave: false, - * type: 'remote', - * } - * ... - */ init(config = {}) { - c = { ...defaults, ...config }; + c = deepMerge(defaults, config); em = c.em; if (c._disable) c.type = 0; defaultStorages.remote = new RemoteStorage(c); @@ -239,8 +217,9 @@ export default () => { async load(options = {}) { const st = this.getCurrentStorage(); const opts = { ...this.getCurrentOptons(), ...options }; - - return await this.__exec(st, opts); + const result = await this.__exec(st, opts); + console.log('onLOAD', { opts, result }); + return result; }, async __exec(storage, opts, data) { @@ -255,11 +234,13 @@ export default () => { try { if (data) { - await storage.store(data, opts); + const toStore = (opts.onStore && (await opts.onStore(data))) || data; + await storage.store(toStore, opts); result = data; } else { result = await storage.load(opts); result = this.__clearKeys(result); + result = (opts.onLoad && (await opts.onLoad(result))) || result; } this.onAfter(ev, result); this.onEnd(ev, result); diff --git a/src/utils/mixins.js b/src/utils/mixins.js index f1dd4b354..eea59a0a1 100644 --- a/src/utils/mixins.js +++ b/src/utils/mixins.js @@ -180,6 +180,27 @@ export const escape = (str = '') => { .replace(/`/g, '`'); }; +export const deepMerge = (...args) => { + const target = { ...args[0] }; + + for (let i = 1; i < args.length; i++) { + const source = { ...args[i] }; + + for (let key in source) { + const targValue = target[key]; + const srcValue = source[key]; + + if (isObject(targValue) && isObject(srcValue)) { + target[key] = deepMerge(targValue, srcValue); + } else { + target[key] = srcValue; + } + } + } + + return target; +}; + /** * Ensure to fetch the model from the input argument * @param {HTMLElement|Component} el Component or HTML element From 93b8ed94a2d7a40c22ceb1cd8db50f0371222732 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 15 Mar 2022 15:56:40 +0100 Subject: [PATCH 18/35] Update collection load --- src/common/module.js | 4 ++-- src/css_composer/model/CssRule.js | 3 +++ src/storage_manager/config/config.js | 2 +- src/storage_manager/index.js | 2 +- src/storage_manager/model/RemoteStorage.js | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/common/module.js b/src/common/module.js index a45b4006b..4db7cefd0 100644 --- a/src/common/module.js +++ b/src/common/module.js @@ -1,5 +1,5 @@ import { isString, isElement } from 'underscore'; -import { createId } from 'utils/mixins'; +import { createId, isDef } from 'utils/mixins'; export default { getConfig(name) { @@ -33,7 +33,7 @@ export default { if (onResult) { result && onResult(result, opts); - } else if (result && result.length) { + } else if (result && isDef(result.length)) { coll.reset(result, opts); } diff --git a/src/css_composer/model/CssRule.js b/src/css_composer/model/CssRule.js index 0ec25000b..cc5153e48 100644 --- a/src/css_composer/model/CssRule.js +++ b/src/css_composer/model/CssRule.js @@ -234,6 +234,9 @@ export default class CssRule extends Model.extend(Styleable) { } }); + // Delete the property used for partial updates + delete obj.style.__p; + if (isEmpty(obj.selectors)) delete obj.selectors; if (isEmpty(obj.style)) delete obj.style; } diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index ec105ca70..6c01ac52f 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -52,7 +52,7 @@ export default { // You can pass a simple object: { someOption: 'someValue' } // or a function which returns and object to add: // currentOpts => { - // return currentOpts.method === 'post' ? { method: 'patch' } : {}; + // return currentOpts.method === 'POST' ? { method: 'PATCH' } : {}; // } fetchOptions: '', diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index d0b41fbb6..6bb69ba26 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -218,7 +218,7 @@ export default () => { const st = this.getCurrentStorage(); const opts = { ...this.getCurrentOptons(), ...options }; const result = await this.__exec(st, opts); - console.log('onLOAD', { opts, result }); + return result; }, diff --git a/src/storage_manager/model/RemoteStorage.js b/src/storage_manager/model/RemoteStorage.js index 423a4817e..4540d9688 100644 --- a/src/storage_manager/model/RemoteStorage.js +++ b/src/storage_manager/model/RemoteStorage.js @@ -53,7 +53,7 @@ export default class RemoteStorage extends Model { } const result = { - method: body ? 'post' : 'get', + method: body ? 'POST' : 'GET', credentials: opts.credentials, headers, body, From 833a663ebea86d53f0343e7c5a9e4cf197586947 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 16 Mar 2022 13:41:50 +0100 Subject: [PATCH 19/35] Add recvoery to storage --- src/editor/index.js | 9 ---- src/editor/model/Editor.js | 4 ++ src/editor/view/EditorView.js | 20 +++++---- src/i18n/locale/en.js | 3 ++ src/storage_manager/config/config.js | 17 ++++--- src/storage_manager/index.js | 67 +++++++++++++++++++++++++--- 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/editor/index.js b/src/editor/index.js index d6e5e3c8f..57b6ebf2d 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -133,15 +133,6 @@ export default (config = {}, opts = {}) => { } }); - // Do post render stuff after the iframe is loaded otherwise it'll - // be empty during tests - em.once('change:ready', () => { - this.UndoManager.clear(); - em.get('modules').forEach(module => { - module.postRender && module.postRender(editorView); - }); - }); - return this; }, diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 4efc0ddd3..935e95484 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -754,6 +754,10 @@ export default class EditorModel extends Model { return this.get('changesCount'); } + clearDirtyCount() { + this.set('changesCount', 0); + } + getZoomDecimal() { return this.get('Canvas').getZoomDecimal(); } diff --git a/src/editor/view/EditorView.js b/src/editor/view/EditorView.js index a9c1af266..98f8e877c 100644 --- a/src/editor/view/EditorView.js +++ b/src/editor/view/EditorView.js @@ -7,22 +7,23 @@ const $ = Backbone.$; export default class EditorView extends View { initialize() { const { model } = this; + const { Panels, UndoManager } = model.attributes; model.view = this; - this.conf = model.config; - this.pn = model.get('Panels'); - this.cv = model.get('Canvas'); model.once('change:ready', () => { - this.pn.active(); - this.pn.disableButtons(); + Panels.active(); + Panels.disableButtons(); + UndoManager.clear(); setTimeout(() => { model.trigger('load', model.get('Editor')); - model.set('changesCount', 0); + model.clearDirtyCount(); }); }); } render() { - const { $el, conf, model } = this; + const { $el, model } = this; + const { Panels, Canvas, modules } = model.attributes; + const conf = model.getConfig(); const pfx = conf.stylePrefix; const contEl = $(conf.el || `body ${conf.container}`); appendStyles(conf.cssIcons, { unique: 1, prepand: 1 }); @@ -31,8 +32,8 @@ export default class EditorView extends View { if (conf.width) contEl.css('width', conf.width); if (conf.height) contEl.css('height', conf.height); - $el.append(this.cv.render()); - $el.append(this.pn.render()); + $el.append(Canvas.render()); + $el.append(Panels.render()); // Load shallow editor const shallow = model.get('shallow'); @@ -42,6 +43,7 @@ export default class EditorView extends View { $el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`); contEl.addClass(`${pfx}editor-cont`).empty().append($el); + modules.forEach(md => md.postRender && md.postRender(this)); return this; } diff --git a/src/i18n/locale/en.js b/src/i18n/locale/en.js index 2d6659016..f2594f298 100644 --- a/src/i18n/locale/en.js +++ b/src/i18n/locale/en.js @@ -160,4 +160,7 @@ export default { }, }, }, + storageManager: { + recover: 'Do you want to recover unsaved changes?', + }, }; diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index 6c01ac52f..e070b5f50 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -13,11 +13,18 @@ export default { autoload: true, /** - * (TODO) In case the remote storage is selected, and this options is enabled, the project - * will be also stored on the local one. - * The local data are cleared on every sucessful remote save. In case the remote storage - * fails (eg. network issue), on project reload, a dialog with the possibility to recovery - * previous data will be shown. + * In case the `remote` storage is selected, and this options is enabled, the project + * will be stored on the `local` storage in case the remote one fails. + * The local data are cleared on every sucessful remote save. When the remote storage + * fails (eg. network issue) and the editor is reloaded, a dialog with the possibility to + * recovery previous data will be shown. + * @example + * // Enable recovery with default confirm dialog + * recovery: true, + * // Enable recovery with a custom dialog + * recovery: (accept, cancel, editor) => { + * confirm('Recover data?') ? accept() : cancel(); + * }, */ recovery: false, diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index 6bb69ba26..be7e95faa 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -54,12 +54,16 @@ import defaults from './config/config'; import LocalStorage from './model/LocalStorage'; import RemoteStorage from './model/RemoteStorage'; import { deepMerge } from 'utils/mixins'; +import { isEmpty, isFunction } from 'underscore'; const eventStart = 'storage:start'; const eventAfter = 'storage:after'; const eventEnd = 'storage:end'; const eventError = 'storage:error'; +const STORAGE_LOCAL = 'local'; +const STORAGE_REMOTE = 'remote'; + export default () => { var c = {}; let em; @@ -73,8 +77,8 @@ export default () => { c = deepMerge(defaults, config); em = c.em; if (c._disable) c.type = 0; - defaultStorages.remote = new RemoteStorage(c); - defaultStorages.local = new LocalStorage(c); + defaultStorages[STORAGE_REMOTE] = new RemoteStorage(c); + defaultStorages[STORAGE_LOCAL] = new LocalStorage(c); c.currentStorage = c.type; this.loadDefaultProviders().setCurrent(c.type); return this; @@ -202,8 +206,21 @@ export default () => { async store(data, options = {}) { const st = this.getCurrentStorage(); const opts = { ...this.getCurrentOptons(), ...options }; + const recovery = this.getRecoveryStorage(); + const recoveryOpts = this.getCurrentOptons(STORAGE_LOCAL); + + try { + await this.__exec(st, opts, data); + recovery && (await this.__exec(recovery, recoveryOpts, {})); + } catch (error) { + if (recovery) { + await this.__exec(recovery, recoveryOpts, data); + } else { + throw error; + } + } - return await this.__exec(st, opts, data); + return data; }, /** @@ -217,9 +234,45 @@ export default () => { async load(options = {}) { const st = this.getCurrentStorage(); const opts = { ...this.getCurrentOptons(), ...options }; - const result = await this.__exec(st, opts); + const recoveryStorage = this.getRecoveryStorage(); + let result; - return result; + if (recoveryStorage) { + const recoveryData = await this.__exec(recoveryStorage, this.getCurrentOptons(STORAGE_LOCAL)); + if (!isEmpty(recoveryData)) { + try { + await this.__askRecovery(); + result = recoveryData; + } catch (error) {} + } + } + + if (!result) { + result = await this.__exec(st, opts); + } + + return result || {}; + }, + + __askRecovery() { + const recovery = this.getRecovery(); + + return new Promise((res, rej) => { + if (isFunction(recovery)) { + recovery(res, rej, em?.getEditor()); + } else { + confirm(em?.t('storageManager.recover')) ? res() : rej(); + } + }); + }, + + getRecovery() { + return this.getConfig().recovery; + }, + + getRecoveryStorage() { + const recovery = this.getRecovery(); + return recovery && this.getCurrent() === STORAGE_REMOTE && this.get(STORAGE_LOCAL); }, async __exec(storage, opts, data) { @@ -288,9 +341,9 @@ export default () => { return this.get(this.getCurrent()); }, - getCurrentOptons() { + getCurrentOptons(type) { const config = this.getConfig(); - const current = this.getCurrent(); + const current = type || this.getCurrent(); return config.options[current] || {}; }, From 7fd7faf796265e88085355eef443c81dc1eb5b27 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 16 Mar 2022 15:57:51 +0100 Subject: [PATCH 20/35] Refactor StorageManager --- src/common/module.js | 7 +- src/storage_manager/index.js | 172 +++++++++++++++-------------------- 2 files changed, 76 insertions(+), 103 deletions(-) diff --git a/src/common/module.js b/src/common/module.js index 4db7cefd0..4a4847568 100644 --- a/src/common/module.js +++ b/src/common/module.js @@ -1,5 +1,5 @@ import { isString, isElement } from 'underscore'; -import { createId, isDef } from 'utils/mixins'; +import { createId, deepMerge, isDef } from 'utils/mixins'; export default { getConfig(name) { @@ -63,10 +63,7 @@ export default { }, __initConfig(def = {}, conf = {}) { - this.config = { - ...def, - ...conf, - }; + this.config = deepMerge(def, conf); this.em = this.config.em; this.cls = []; }, diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index be7e95faa..b5f404895 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -53,6 +53,7 @@ import defaults from './config/config'; import LocalStorage from './model/LocalStorage'; import RemoteStorage from './model/RemoteStorage'; +import Module from 'common/module'; import { deepMerge } from 'utils/mixins'; import { isEmpty, isFunction } from 'underscore'; @@ -65,135 +66,127 @@ const STORAGE_LOCAL = 'local'; const STORAGE_REMOTE = 'remote'; export default () => { - var c = {}; - let em; - var storages = {}; - var defaultStorages = {}; - return { - name: 'StorageManager', + ...Module, - init(config = {}) { - c = deepMerge(defaults, config); - em = c.em; - if (c._disable) c.type = 0; - defaultStorages[STORAGE_REMOTE] = new RemoteStorage(c); - defaultStorages[STORAGE_LOCAL] = new LocalStorage(c); - c.currentStorage = c.type; - this.loadDefaultProviders().setCurrent(c.type); - return this; - }, + name: 'StorageManager', /** * Get configuration object + * @name getConfig + * @function * @return {Object} - * */ - getConfig() { - return c; + */ + + /** + * Initialize module. Automatically called with a new instance of the editor + * @param {Object} config Configurations + * @private + */ + init(config = {}) { + this.__initConfig(defaults, config); + const c = this.getConfig(); + if (c._disable) c.type = 0; + this.storages = {}; + this.add(STORAGE_LOCAL, new LocalStorage(c)); + this.add(STORAGE_REMOTE, new RemoteStorage(c)); + this.setCurrent(c.type); + return this; }, /** - * Checks if autosave is enabled - * @return {Boolean} + * Check if autosave is enabled. + * @returns {Boolean} * */ isAutosave() { - return !!c.autosave; + return !!this.getConfig().autosave; }, /** - * Set autosave value - * @param {Boolean} v - * @return {this} + * Set autosave value. + * @param {Boolean} value * */ - setAutosave(v) { - c.autosave = !!v; + setAutosave(value) { + this.getConfig().autosave = !!value; return this; }, /** - * Returns number of steps required before trigger autosave - * @return {number} + * Returns number of steps required before trigger autosave. + * @returns {Number} * */ getStepsBeforeSave() { - return c.stepsBeforeSave; + return this.getConfig().stepsBeforeSave; }, /** - * Set steps required before trigger autosave - * @param {number} v - * @return {this} + * Set steps required before trigger autosave. + * @param {Number} value * */ - setStepsBeforeSave(v) { - c.stepsBeforeSave = v; + setStepsBeforeSave(value) { + this.getConfig().stepsBeforeSave = value; return this; }, /** - * Add new storage - * @param {string} id Storage ID - * @param {Object} storage Storage wrapper - * @param {Function} storage.load Load method + * Add new storage. + * @param {String} type Storage type + * @param {Object} storage Storage definition + * @param {Function} storage.load Load method * @param {Function} storage.store Store method - * @return {this} * @example * storageManager.add('local2', { - * load: function(keys, clb, clbErr) { - * var res = {}; - * for (var i = 0, len = keys.length; i < len; i++){ - * var v = localStorage.getItem(keys[i]); - * if(v) res[keys[i]] = v; - * } - * clb(res); // might be called inside some async method - * // In case of errors... - * // clbErr('Went something wrong'); + * async load(storageOptions) { + * // ... + * }, + * async store(data, storageOptions) { + * // ... * }, - * store: function(data, clb, clbErr) { - * for(var key in data) - * localStorage.setItem(key, data[key]); - * clb(); // might be called inside some async method - * } * }); * */ - add(id, storage) { - storages[id] = storage; + add(type, storage) { + this.storages[type] = storage; return this; }, /** - * Returns storage by id - * @param {string} id Storage ID - * @return {Object|null} + * Return storage by type. + * @param {String} type Storage type + * @returns {Object|null} * */ - get(id) { - return storages[id] || null; + get(type) { + return this.storages[type] || null; }, /** - * Returns all storages - * @return {Array} + * Get all storages. + * @returns {Object} * */ getStorages() { - return storages; + return this.storages; }, /** - * Returns current storage type - * @return {string} + * Get current storage type. + * @returns {String} * */ getCurrent() { - return c.currentStorage; + return this.getConfig().currentStorage; }, /** - * Set current storage type - * @param {string} id Storage ID - * @return {this} + * Set current storage type. + * @param {String} type Storage type * */ - setCurrent(id) { - c.currentStorage = id; + setCurrent(type) { + this.getConfig().currentStorage = type; return this; }, + getCurrentStorage() { + return this.get(this.getCurrent()); + }, + /** * Store data in the current storage. * @param {Object} data Project data. @@ -255,6 +248,7 @@ export default () => { }, __askRecovery() { + const { em } = this; const recovery = this.getRecovery(); return new Promise((res, rej) => { @@ -305,15 +299,10 @@ export default () => { return result; }, - /** - * Restore key names - * @param {Object} data - * @returns {Object} - * @private - */ __clearKeys(data = {}) { + const config = this.getConfig(); + const reg = new RegExp(`^${config.id}`); const result = {}; - const reg = new RegExp('^' + c.id + ''); for (let itemKey in data) { const itemKeyR = itemKey.replace(reg, ''); @@ -323,24 +312,6 @@ export default () => { return result; }, - /** - * Load default storages - * @return {this} - * @private - * */ - loadDefaultProviders() { - for (var id in defaultStorages) this.add(id, defaultStorages[id]); - return this; - }, - - /** - * Get current storage - * @return {Storage} - * */ - getCurrentStorage() { - return this.get(this.getCurrent()); - }, - getCurrentOptons(type) { const config = this.getConfig(); const current = type || this.getCurrent(); @@ -352,6 +323,7 @@ export default () => { * @private */ onStart(ctx, data) { + const { em } = this; if (em) { em.trigger(eventStart); ctx && em.trigger(`${eventStart}:${ctx}`, data); @@ -363,6 +335,7 @@ export default () => { * @private */ onAfter(ctx, data) { + const { em } = this; if (em) { em.trigger(eventAfter); em.trigger(`${eventAfter}:${ctx}`, data); @@ -375,6 +348,7 @@ export default () => { * @private */ onEnd(ctx, data) { + const { em } = this; if (em) { em.trigger(eventEnd); ctx && em.trigger(`${eventEnd}:${ctx}`, data); @@ -386,6 +360,7 @@ export default () => { * @private */ onError(ctx, data) { + const { em } = this; if (em) { em.trigger(eventError, data); ctx && em.trigger(`${eventError}:${ctx}`, data); @@ -404,7 +379,8 @@ export default () => { }, destroy() { - [c, em, storages, defaultStorages].forEach(i => (i = {})); + this.__destroy(); + this.storages = {}; }, }; }; From bdd0b40f0f85ea21b17e256af148272ac4fbcc16 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 16 Mar 2022 15:58:12 +0100 Subject: [PATCH 21/35] Up --- src/storage_manager/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index b5f404895..e0e65da9f 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -54,7 +54,6 @@ import defaults from './config/config'; import LocalStorage from './model/LocalStorage'; import RemoteStorage from './model/RemoteStorage'; import Module from 'common/module'; -import { deepMerge } from 'utils/mixins'; import { isEmpty, isFunction } from 'underscore'; const eventStart = 'storage:start'; From 181da62b8ca847b02b593d40d668162a5931a906 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 17 Mar 2022 15:13:11 +0100 Subject: [PATCH 22/35] Update storageManager doc --- docs/modules/Storage.md | 125 ++++++++++++++++++++++----- package.json | 3 +- src/storage_manager/config/config.js | 7 +- yarn.lock | 2 +- 4 files changed, 111 insertions(+), 26 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index 0fc5fe0fa..e7fc0ee44 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -4,64 +4,144 @@ title: Storage Manager # Storage Manager -The aim of this guide is to show how to setup correctly your storage configuration for common usages of the editor and explain also some additional advanced settings +The Storage Manager is a built-in module that allows the persistence of your project data. The aim of this guide is to show how to setup correctly your storage configuration for common usages of the editor and explain also some additional advanced settings. ::: warning -This guide requires GrapesJS v0.14.15 or higher +This guide requires GrapesJS v0.19.* or higher ::: [[toc]] -## Basic configuration +## Configuration + +To change the default configurations you have to pass the `storageManager` property with the main configuration object. -The storage manager is a built-in module implemented inside GrapesJS which allows the persistence of your data. By default, GrapesJS saves the data locally by using the built-in `LocalStorage` which just leverages [localStorage API]. -You can initialize the editor with different storage configurations via `storageManager` option: ```js const editor = grapesjs.init({ ... // Default configurations storageManager: { - id: 'gjs-', // Prefix identifier that will be used on parameters - type: 'local', // Type of the storage - autosave: true, // Store data automatically - autoload: true, // Autoload stored data on init - stepsBeforeSave: 1, // If autosave enabled, indicates how many changes are necessary before store method is triggered + type: 'local', // Storage type. Available: local | remote + autosave: true, // Store data automatically + autoload: true, // Autoload stored data on init + stepsBeforeSave: 1, // If autosave is enabled, indicates how many changes are necessary before the store method is triggered + // ... + // Default storage options + options: { + local: {/* ... */}, + remote: {/* ... */}, + } }, }); ``` -The `id` option is used to prevent collisions (quite common with localStorage) in case of multiple editors on the same page, therefore you will see parameters passed like `{ 'gjs-components': '...', 'gjs-styles': '...', }` -If you need to disable the storage manager you can pass any empty `type`: +In case you don't need any persistence, you can disable the module in this way: ```js -... -storageManager: { type: null }, +const editor = grapesjs.init({ + ... + storageManager: false, +}); ``` -For all other available options check directly the [configuration source file](https://github.com/artf/grapesjs/blob/dev/src/storage_manager/config/config.js). +Check the full list of available options here: [Storage Manager Config](https://github.com/artf/grapesjs/blob/master/src/storage_manager/config/config.js) + + + + + +## Project data + +The project data is a JSON object containing all the necessary information (styles, pages, etc.) about your project in the editor. You can get the current state of the data in this way: + +```js +const projectData = editor.getProjectData(); +``` + +That object is used in the storage manager methods in order to store and load your project data (locally or remotely in your DB/file). + +::: danger +You should only rely on the JSON project data in order to load your project properly in the editor. + +The editor is able to parse and use HTML/CSS code, you can use it as part of your project initialization but never rely on it as a persitance layer in the load of projects as many information could be stripped off. +::: + + + + +## Setup local storage + +By default, GrapesJS saves the data locally by using the built-in `local` storage which leverages [localStorage API]. + +The only option you might probably care for the local storage is the `key` used to store the data. If the user loads different projects in your application, you might probably need to differentiate the local storage by the ID of the project (the ID here is intended to be part of your application domain). + +```js +// Get your project ID (eg. taken from the route) +const projectId = getProjectId(); + +const editor = grapesjs.init({ + ... + storageManager: { + type: 'local', + options: { + local: { key: `gjsProject-${projectId}` } + } + }, +}); +``` ## Setup remote storage -Switching up the remote storage is very simple, it's just a matter of specifying your endpoints for storing and loading, which generally might be also the same (if you rely on HTTP methods). +Most commonly the data of the project might be saved remotely on your server (DB, file, etc.) therefore you need to setup your server-side API calls in order to store/load project data. + +For the sake of simplicity we can setup a fake REST API server by relying on [json-server]. + +```sh +mkdir my-server +cd my-server +npm init +npm i json-server +echo '{"projects": [ {"id": 1, "data": {"assets": [], "styles": [], "pages": [{"component": "
Initial content
"}]} } ]}' > db.json +npx json-server --watch db.json +``` + +This will start up a local server with one single project available on `http://localhost:3000/projects/1`. The data will be updated on the `db.json` file. + +Here below an example of how you would configure a `remote` storage in GrapesJS. ```js +const projectID = 1; +const projectEndpoint = `http://localhost:3000/projects/${projectID}`; + const editor = grapesjs.init({ ... storageManager: { type: 'remote', stepsBeforeSave: 3, - urlStore: 'http://endpoint/store-template/some-id-123', - urlLoad: 'http://endpoint/load-template/some-id-123', - // For custom parameters/headers on requests - params: { _some_token: '....' }, - headers: { Authorization: 'Basic ...' }, + options: { + remote: { + urlLoad: projectEndpoint, + urlStore: projectEndpoint, + // The `remote` storage uses the POST method when stores data but + // the json-server API requires PATCH. + fetchOptions: opts => (opts.method === 'POST' ? { method: 'PATCH' } : {}), + // As the API stores projects in this format `{id: 1, data: projectData }`, + // we have to properly update the body before the store and extract the + // project data from the response result. + onStore: data => ({ id: projectID, data }), + onLoad: result => result.data, + } + } } }); ``` -As you can see we've left some default option unchanged, increased changes necessary for autosave triggering and passed remote endpoints. + +::: danger +Be sure to configure properly [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) on your server API. The [json-server] is not intended to be used in production and therefore enables all of them automatically for the sake of simplicity. +::: @@ -298,3 +378,4 @@ editor.on('storage:end:load', (resultObject) => { [grapesjs-firestore]: [localStorage API]: [IndexedDB]: +[json-server]: diff --git a/package.json b/package.json index 06b3280d3..3299b9ead 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.18.3", + "version": "0.19.0", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", @@ -29,6 +29,7 @@ "documentation": "^13.2.5", "eslint": "^7.32.0", "grapesjs-cli": "^3.0.0", + "html-entities": "^1.4.0", "husky": "^2.7.0", "jest": "^24.9.0", "lint-staged": "^8.2.1", diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index e070b5f50..65fb46b51 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -66,12 +66,15 @@ export default { credentials: 'include', /** - * Edit project data before sending them to the storage. + * The remote storage sends the project data as a body of the request. + * You can use this method to update the body before the store call in order to align + * with your API requirements. */ onStore: data => data, /** - * Edit project data before loading them from the storage. + * The remote storage loads the project data directly from the request response. + * You can use this method to properly extract the project data from the response. */ onLoad: result => result, }, diff --git a/yarn.lock b/yarn.lock index c7ddc2351..aabc2749b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6352,7 +6352,7 @@ html-encoding-sniffer@^1.0.2: dependencies: whatwg-encoding "^1.0.1" -html-entities@^1.3.1: +html-entities@^1.3.1, html-entities@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" From 4d5492267402a9901d5cf844357eb1db4379ad0d Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 17 Mar 2022 15:27:02 +0100 Subject: [PATCH 23/35] Add onLoad/onStore --- src/storage_manager/config/config.js | 11 +++++++++++ src/storage_manager/index.js | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/storage_manager/config/config.js b/src/storage_manager/config/config.js index 65fb46b51..38eb52448 100644 --- a/src/storage_manager/config/config.js +++ b/src/storage_manager/config/config.js @@ -32,6 +32,17 @@ export default { // need to be done before save. Useful with remoteStorage to reduce remote calls stepsBeforeSave: 1, + /** + * Callback triggered before the store call (can be asynchronous). + * This can be used to enrich the project data to store. + */ + onStore: data => data, + + /** + * Callback triggered after the load call (can be asynchronous). + */ + onLoad: data => data, + // Default storage options options: { local: { diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index e0e65da9f..4e4105b0c 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -270,6 +270,7 @@ export default () => { async __exec(storage, opts, data) { const ev = data ? 'store' : 'load'; + const { onStore, onLoad } = this.getConfig(); let result; this.onStart(ev, data); @@ -280,13 +281,15 @@ export default () => { try { if (data) { - const toStore = (opts.onStore && (await opts.onStore(data))) || data; + let toStore = (onStore && (await onStore(data))) || data; + toStore = (opts.onStore && (await opts.onStore(toStore))) || toStore; await storage.store(toStore, opts); result = data; } else { result = await storage.load(opts); result = this.__clearKeys(result); result = (opts.onLoad && (await opts.onLoad(result))) || result; + result = (onLoad && (await onLoad(result))) || result; } this.onAfter(ev, result); this.onEnd(ev, result); From 4c96ceaff6df5ca2aa6d8ada91b822ec0de69e75 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 18 Mar 2022 13:52:05 +0100 Subject: [PATCH 24/35] Add editor.clearDirtyCount and Storage strategy doc --- docs/modules/Storage.md | 68 +++++++++++++++++++++----------------- src/editor/index.js | 7 ++++ src/editor/model/Editor.js | 2 +- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index e7fc0ee44..649644c06 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -4,7 +4,7 @@ title: Storage Manager # Storage Manager -The Storage Manager is a built-in module that allows the persistence of your project data. The aim of this guide is to show how to setup correctly your storage configuration for common usages of the editor and explain also some additional advanced settings. +The Storage Manager is a built-in module that allows the persistence of your project data. ::: warning This guide requires GrapesJS v0.19.* or higher @@ -49,15 +49,21 @@ Check the full list of available options here: [Storage Manager Config](https:// -## Project data +## Project Data -The project data is a JSON object containing all the necessary information (styles, pages, etc.) about your project in the editor. You can get the current state of the data in this way: +The project data is a JSON object containing all the necessary information (styles, pages, etc.) about your project in the editor and is the one used in the storage manager methods in order to store and load your project (locally or remotely in your DB/file). + +::: tip +You can get the current state of the data and load it manually in this way: ```js +// Get current project data const projectData = editor.getProjectData(); +// ... +// Load project data +editor.loadProjectData(projectData); ``` - -That object is used in the storage manager methods in order to store and load your project data (locally or remotely in your DB/file). +::: ::: danger You should only rely on the JSON project data in order to load your project properly in the editor. @@ -70,6 +76,24 @@ The editor is able to parse and use HTML/CSS code, you can use it as part of you +## Storage strategy + +Project data are automatically stored every time the amount of changes (`editor.getDirtyCount()`) reaches the number of steps before save (`editor.Storage.getStepsBeforeSave()`). On any successful store of the data, the counter of changes is reset (`editor.clearDirtyCount()`). + +::: tip +When necessary, you can always trigger store/load manually. + +```js +// Store data +const storedProjectData = await editor.store(); + +// Load data +const loadedProjectData = await editor.load(); +``` +::: + + + ## Setup local storage By default, GrapesJS saves the data locally by using the built-in `local` storage which leverages [localStorage API]. @@ -143,6 +167,15 @@ const editor = grapesjs.init({ Be sure to configure properly [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) on your server API. The [json-server] is not intended to be used in production and therefore enables all of them automatically for the sake of simplicity. ::: +
+ +### Server setup + +Server configuration might differ case to case so usually, it's up to you to know how to configure it properly. +The default remote storage follows a simple REST API approach with project data exchanged as a JSON (`Content-Type: application/json`). +* On **load** (`GET` method), the JSON project data are expected to be returned directly in the response. As from example above, you can use `options.remote.onLoad` to extract the project data if the response contains other metadata. +* On **store** (`POST` method), the editor doesn't expect any particular result but only a valid response from the server (status code `200`). + @@ -200,34 +233,9 @@ const editor = grapesjs.init({ editor.store(res => console.log('Store callback')); ``` -If you need to check changes which yet need to be stored you can use `editor.getDirtyCount()`. At any, successful, store of the editor, it resets the count. - - -## Setup the server - -Server configuration might differ for any use case so generally, it's something up to you on how to make it work, but usually, the flow is pretty straightforward. Create two endpoints, one for storing (eg. `mydomain.com/store-page/123`) and the other one for loading (eg. `mydomain.com/load-page/123`), you can also create just one and distinguish them via HTTP methods (eg. `mydomain.com/page/123`, via GET you load the template, with POST you store it). -When you **store**, the editor doesn't expect any particular result but only a valid response from the server (status code 200). -When you **load** the template, return a JSON object with the data you have (don't forget to include the `id` prefix if it's used) -```js -{ - // `gjs-` is the id prefix - 'gjs-components': [{ tagName: 'div', ... }, {...}, ...], - 'gjs-styles': [{...}, {...}, ...], -} -``` -Be sure to have a correct `Content-Type` response header, eg. in PHP you would do something like this: -```php -header('Content-Type: application/json'); -echo json_encode([ - 'gjs-components': [...], - 'gjs-styles': [...], -]); -``` - - ## Storage API diff --git a/src/editor/index.js b/src/editor/index.js index 57b6ebf2d..e86d896ba 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -490,6 +490,13 @@ export default (config = {}, opts = {}) => { return em.getDirtyCount(); }, + /** + * Reset the counter of changes. + */ + clearDirtyCount() { + return em.clearDirtyCount(); + }, + /** * Update editor dimension offsets * diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 935e95484..529aa05af 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -755,7 +755,7 @@ export default class EditorModel extends Model { } clearDirtyCount() { - this.set('changesCount', 0); + return this.set('changesCount', 0); } getZoomDecimal() { From 21faf1a70772840117ef83990f7771196fadbc82 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 18 Mar 2022 13:55:48 +0100 Subject: [PATCH 25/35] Up changesCount --- src/editor/model/Editor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 529aa05af..68bb376ce 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -184,7 +184,7 @@ export default class EditorModel extends Model { */ updateChanges() { const stm = this.get('StorageManager'); - const changes = this.get('changesCount'); + const changes = this.getDirtyCount(); updateItr && clearTimeout(updateItr); updateItr = setTimeout(() => this.trigger('update')); @@ -266,7 +266,7 @@ export default class EditorModel extends Model { timedInterval && clearTimeout(timedInterval); timedInterval = setTimeout(() => { - const curr = this.get('changesCount') || 0; + const curr = this.getDirtyCount() || 0; const { unset, ...opts } = opt; this.set('changesCount', curr + 1, opts); }, 0); @@ -619,7 +619,7 @@ export default class EditorModel extends Model { async store(options) { const data = this.storeData(); await this.get('StorageManager').store(data, options); - this.set('changesCount', 0); + this.clearDirtyCount(); return data; } From 4294ca30c335dd9311b474aae29ec7c14e671a2d Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 18 Mar 2022 14:30:08 +0100 Subject: [PATCH 26/35] Update loadProjectData --- docs/modules/Storage.md | 89 +++++++++++-------------------------- src/common/module.js | 4 +- src/dom_components/index.js | 1 - src/pages/index.js | 2 +- 4 files changed, 28 insertions(+), 68 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index 649644c06..9fd33a0ca 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -49,7 +49,7 @@ Check the full list of available options here: [Storage Manager Config](https:// -## Project Data +## Project data The project data is a JSON object containing all the necessary information (styles, pages, etc.) about your project in the editor and is the one used in the storage manager methods in order to store and load your project (locally or remotely in your DB/file). @@ -180,7 +180,7 @@ The default remote storage follows a simple REST API approach with project data -## Store and load templates + @@ -240,58 +219,40 @@ editor.store(res => console.log('Store callback')); ## Storage API -The Storage module has also its own [set of API](https://github.com/artf/grapesjs/wiki/API-Storage-Manager) that allows you to extend and add new functionalities. +The Storage Manager module has also its own [set of APIs](https://github.com/artf/grapesjs/wiki/API-Storage-Manager) that allows you to extend and add new functionalities. ### Define new storage -One of the most useful methods of API is the possibility to add new storages. You might think, we have the `local` and `remote` storages, what else do we need, right? Well, let's take as an example the `local` one. As you already know, it relies on [localStorage API] which is really cool and easy to use but one of his specs might be a big limit, by default it has a limited amount of MB to use per site (something around 5MB-10MB, depends on the browser implementation). As an alternative, we can make use of [IndexedDB] which is also quite [well supported](https://caniuse.com/#search=indexedDB) and allows more space usage (each browser implements its own rules, for a better understanding on how browser storage limits work, check [here](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria)). +One of the most useful methods of API is the possibility to add new storages. +You might think, we have the `local` and `remote` storages, what else do we need, right? Well, let's take as an example the `local` one. As you already know, it relies on [localStorage API] which is really cool and easy to use but one of his specs might be a big limit, by default it has a limited amount of MB to use per site (something around 5MB-10MB, depends on the browser implementation). As an alternative, we can make use of [IndexedDB] which is also quite [well supported](https://caniuse.com/#search=indexedDB) and allows more space usage (each browser implements its own rules, for a better understanding on how browser storage limits work, check [here](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria)). [IndexedDB configuration](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB) might be too much verbose for this guide so we decided to create the [grapesjs-indexeddb] plugin, so you can check its source and see how it's implemented. For this guide we are going to see something much simpler but with the same flow, it'll be just a simple javascript object which stores key-value data, not persistent at all but the concept is the same. ```js -const editor = grapesjs.init({ - ... - storageManager: { type: 'simple-storage' }, -}); - -// Here our `simple-storage` implementation -const SimpleStorage = {}; - -editor.StorageManager.add('simple-storage', { - /** - * Load the data - * @param {Array} keys Array containing values to load, eg, ['gjs-components', 'gjs-styles', ...] - * @param {Function} clb Callback function to call when the load is ended - * @param {Function} clbErr Callback function to call in case of errors - */ - load(keys, clb, clbErr) { - const result = {}; +const addMemoryStorage = (editor) => { + const MemoryStorage = {}; - keys.forEach(key => { - const value = SimpleStorage[key]; - if (value) { - result[key] = value; - } - }); + editor.Storage.add('memory', { + async load(options = {}) { + return MemoryStorage[options.key]; + }, - // Might be called inside some async method - clb(result); - }, + async store(data, options = {}) { + MemoryStorage[options.key] = data; + } + }); +}; - /** - * Store the data - * @param {Object} data Data object to store - * @param {Function} clb Callback function to call when the load is ended - * @param {Function} clbErr Callback function to call in case of errors - */ - store(data, clb, clbErr) { - for (let key in data) { - SimpleStorage[key] = data[key]; +const editor = grapesjs.init({ + ... + plugins: [addMemoryStorage], + storageManager: { + type: 'memory', + options: { + memory: { key: 'myKey' } } - // Might be called inside some async method - clb(); - } + }, }); ``` diff --git a/src/common/module.js b/src/common/module.js index 4a4847568..1594b8de9 100644 --- a/src/common/module.js +++ b/src/common/module.js @@ -15,11 +15,11 @@ export default { return obj; }, - loadProjectData(data = {}, { all, def = [], onResult, reset } = {}) { + loadProjectData(data = {}, { all, onResult, reset } = {}) { const key = this.storageKey; const opts = { action: 'load' }; const coll = all || this.getAll(); - let result = data[key] || def; + let result = data[key]; if (typeof result == 'string') { try { diff --git a/src/dom_components/index.js b/src/dom_components/index.js index 49ea8df0b..7674e8dd2 100644 --- a/src/dom_components/index.js +++ b/src/dom_components/index.js @@ -277,7 +277,6 @@ export default () => { load(data) { return this.loadProjectData(data, { - def: null, onResult: result => { const wrapper = this.getWrapper(); if (isArray(result)) { diff --git a/src/pages/index.js b/src/pages/index.js index f69b867cb..829348f46 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -280,7 +280,7 @@ export default () => { }, load(data) { - return this.loadProjectData(data, { all: this.pages, reset: true, def: null }); + return this.loadProjectData(data, { all: this.pages, reset: true }); }, _createId() { From a726e51e4f3bf06fc4be70de2cd312a2e9cfe9eb Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 18 Mar 2022 16:02:49 +0100 Subject: [PATCH 27/35] Add getStorageOptions --- docs/modules/Storage.md | 76 +++++++++++++++++++----------------- src/storage_manager/index.js | 10 +++++ 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index 9fd33a0ca..b67ea695d 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -121,7 +121,7 @@ const editor = grapesjs.init({ Most commonly the data of the project might be saved remotely on your server (DB, file, etc.) therefore you need to setup your server-side API calls in order to store/load project data. -For the sake of simplicity we can setup a fake REST API server by relying on [json-server]. +For a sake of simplicity we can setup a fake REST API server by relying on [json-server]. ```sh mkdir my-server @@ -164,7 +164,7 @@ const editor = grapesjs.init({ ``` ::: danger -Be sure to configure properly [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) on your server API. The [json-server] is not intended to be used in production and therefore enables all of them automatically for the sake of simplicity. +Be sure to configure properly [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) on your server API. The [json-server] is not intended to be used in production and therefore enables all of them automatically. :::
@@ -225,32 +225,30 @@ The Storage Manager module has also its own [set of APIs](https://github.com/art ### Define new storage -One of the most useful methods of API is the possibility to add new storages. -You might think, we have the `local` and `remote` storages, what else do we need, right? Well, let's take as an example the `local` one. As you already know, it relies on [localStorage API] which is really cool and easy to use but one of his specs might be a big limit, by default it has a limited amount of MB to use per site (something around 5MB-10MB, depends on the browser implementation). As an alternative, we can make use of [IndexedDB] which is also quite [well supported](https://caniuse.com/#search=indexedDB) and allows more space usage (each browser implements its own rules, for a better understanding on how browser storage limits work, check [here](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria)). -[IndexedDB configuration](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB) might be too much verbose for this guide so we decided to create the [grapesjs-indexeddb] plugin, so you can check its source and see how it's implemented. For this guide we are going to see something much simpler but with the same flow, it'll be just a simple javascript object which stores key-value data, not persistent at all but the concept is the same. +Defining a new storage is a matter of passing of two asyncronous methods to the `editor.Storage.add` API. For a sake of simplicity, the example below illustrates the API usage for defining the `session` storage by using [sessionStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). ```js -const addMemoryStorage = (editor) => { - const MemoryStorage = {}; - - editor.Storage.add('memory', { +const sessionStoragePlugin = (editor) => { + // As sessionStorage is not an asynchronous API, + // the `async` keyword could be skipped + editor.Storage.add('session', { async load(options = {}) { - return MemoryStorage[options.key]; + return JSON.parse(sessionStorage.getItem(options.key)); }, async store(data, options = {}) { - MemoryStorage[options.key] = data; + sessionStorage.setItem(options.key, JSON.stringify(data)); } }); }; const editor = grapesjs.init({ ... - plugins: [addMemoryStorage], + plugins: [sessionStoragePlugin], storageManager: { - type: 'memory', + type: 'session', options: { - memory: { key: 'myKey' } + session: { key: 'myKey' } } }, }); @@ -260,37 +258,45 @@ const editor = grapesjs.init({ ### Extend storage -Among other needs, you might need to use existing storages to create more complex uses. For example, let's say we would like to mix the local and remote storages inside another one. This is how it would look like: -```js -const sm = editor.StorageManager; +Among other needs, you might need to use existing storages to combine them in a more complex use case. +For example, let's say we would like to mix the local and remote storages inside another one. This is how it would look like: -sm.add('local-remote', { - store(data, clb, clbErr) { - const remote = sm.get('remote'); - const local = sm.get('local'); - // ... - remote.store(data, clb, err => { - // eg. some error on remote side, store it locally - local.store(data, clb, clbError); - }); +```js +const { Storage } = editor; + +Storage.add('remote-local', { + async store(data) { + const remoteStorage = Storage.get('remote'); + + try { + await remoteStorage.store(data, Storage.getStorageOptions('remote')); + } catch (err) { + // On remote error, store data locally + const localStorage = Storage.get('local'); + await localStorage.store(data, Storage.getStorageOptions('local')); + } }, - load(keys, clb, clbErr) { + async load() { // ... }, }); ``` -If you need to completely replace the storage, just use the same id in `add` method +### Replace storage + +You can also replace already defined storages with other implementations by passing the same storage type in the `Storage.add` method. You can switch, for example, the default `local`, which relies on [localStorage API], with something more scalable like [IndexedDB API]. + +It might also be possible that you're already using some HTTP client library (eg. [axios](https://github.com/axios/axios)) which handles for you all the necessary HTTP headers in your application (CSRF token, session data, etc.), so you can simply replace the default `remote` storage wiht your implemenation of choice without caring about the default configurations. + ```js -editor.StorageManager.add('local', { - // New logic for the local storage - load() { - // ... +editor.Storage.add('remote', { + async load() { + return await axios.get(`projects/${projectId}`); }, - store() { - // ... + async store(data) { + return await axios.patch(`projects/${projectId}`, { data }); }, }); ``` @@ -346,5 +352,5 @@ editor.on('storage:end:load', (resultObject) => { [grapesjs-indexeddb]: [grapesjs-firestore]: [localStorage API]: -[IndexedDB]: +[IndexedDB API]: [json-server]: diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index 4e4105b0c..6813d20db 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -42,6 +42,7 @@ * * [getCurrent](#getcurrent) * * [getCurrentStorage](#getcurrentstorage) * * [setCurrent](#setcurrent) + * * [getStorageOptions](#getstorageoptions) * * [add](#add) * * [get](#get) * * [store](#store) @@ -186,6 +187,15 @@ export default () => { return this.get(this.getCurrent()); }, + /** + * Get storage options by type. + * @param {String} type Storage type + * @returns {Object} + * */ + getStorageOptions(type) { + return this.getCurrentOptons(type); + }, + /** * Store data in the current storage. * @param {Object} data Project data. From d3bd384104fea21001c15c1cd83d4c119a790821 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 18 Mar 2022 16:08:14 +0100 Subject: [PATCH 28/35] Update old Storage links --- docs/modules/Storage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index b67ea695d..1cb8ba47e 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -219,7 +219,7 @@ const editor = grapesjs.init({ ## Storage API -The Storage Manager module has also its own [set of APIs](https://github.com/artf/grapesjs/wiki/API-Storage-Manager) that allows you to extend and add new functionalities. +The Storage Manager module has also its own [set of APIs](/api/storage_manager.html) that allows you to extend and add new functionalities. @@ -316,7 +316,7 @@ Here you can find some of the plugins extending the Storage Manager ## Events -Another way to extend storage capabilities is to make use of GrapesJS's event hooks, you can check [here](https://github.com/artf/grapesjs/wiki/API-Editor#storages) the list of all available events for the Storage module. Let's see some of the cases where you might want to use them: +Another way to extend storage capabilities is to make use of GrapesJS's event hooks, you can check [here](/api/storage_manager.html#available-events) the list of all available events for the Storage module. Let's see some of the cases where you might want to use them: * Loading animation on storage requests ```js From 90955fa5d01db11b86e9ebc4506ff866b5c60580 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 18 Mar 2022 16:15:35 +0100 Subject: [PATCH 29/35] Update Storage events doc --- docs/modules/Storage.md | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index 1cb8ba47e..0e295a5d0 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -316,35 +316,7 @@ Here you can find some of the plugins extending the Storage Manager ## Events -Another way to extend storage capabilities is to make use of GrapesJS's event hooks, you can check [here](/api/storage_manager.html#available-events) the list of all available events for the Storage module. Let's see some of the cases where you might want to use them: - -* Loading animation on storage requests -```js -editor.on('storage:start', startLoading); -editor.on('storage:end', endLoading); -``` -* Error handling -```js -editor.on('storage:error', (err) => { - alert(`Error: ${err}`); -}); -``` -* Extend parameters to store -```js -editor.on('storage:start:store', (objectToStore) => { - if (needToAddExtraParam) { - objectToStore.customHtml = `
...${editor.getHtml()}...
`; - } -}); -``` -* Do stuff post load -```js -editor.on('storage:end:load', (resultObject) => { - if (resultObject.hasSomeKey) { - // do stuff - } -}); -``` +For a complete list of available events, you can check it [here](/api/storage_manager.html#available-events). From cd825006d21f9c4e54c936191d73b4090211183c Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 21 Mar 2022 16:33:58 +0100 Subject: [PATCH 30/35] Added `projectData` option --- docs/modules/Storage.md | 38 +++++++++++++++++++++++++++++++++++++ src/editor/config/config.js | 20 ++++++++++++++++--- src/editor/model/Editor.js | 6 +++++- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index 0e295a5d0..40652c8e2 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -313,6 +313,44 @@ Here you can find some of the plugins extending the Storage Manager +## Common use cases + +### Init without first load + +In case you're using a `remote` storage, you might probably want to skip the initial remote call by loading the project instantly. In that case, you can specify the `projectData` on initialization. + +```js +// Get the data before initializing the editor (eg. printed on server-side). +const projectData = {...}; +// ... +grapesjs.init({ + // ... + // If projectData is not defined we might want to load some initial data for the project. + projectData: projectData || { + pages: [ + { + component: ` +
Initial content
+ + ` + } + ] + }, + storageManager: { + type: 'remote', + // ... + }, +}) +``` +In case `projectData` is defined, the initial storage load will be automatically skipped. + + +### HTML code in project data + +### Inline data + + + ## Events diff --git a/src/editor/config/config.js b/src/editor/config/config.js index 538b77bee..926df187c 100644 --- a/src/editor/config/config.js +++ b/src/editor/config/config.js @@ -2,15 +2,29 @@ export default { // Style prefix stylePrefix: 'gjs-', - // HTML string or object of components + /** + * HTML string or object of components + * @deprecated Rely on `projectData` option + */ components: '', - // CSS string or object of rules + /** + * CSS string or object of rules + * @deprecated Rely on `projectData` option + */ style: '', - // If true, will fetch HTML and CSS from selected container + /** + * If true, will fetch HTML and CSS from selected container + * @deprecated + */ fromElement: 0, + /** + * Initial project data + */ + projectData: null, + // Show an alert before unload the page with unsaved changes noticeOnUnload: true, diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 68bb376ce..35275aa18 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -155,7 +155,11 @@ export default class EditorModel extends Model { // Defer for storage load events. setTimeout(async () => { - if (this.get('StorageManager').canAutoload()) { + const projectData = this.getConfig('projectData'); + + if (projectData) { + this.loadData(projectData); + } else if (this.get('StorageManager').canAutoload()) { try { await this.load(); } catch (error) { From 6780389cecc5ce18c88b6e7c06322a9333766cc3 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 22 Mar 2022 12:18:32 +0100 Subject: [PATCH 31/35] Pass editor to onStore/onLoad --- docs/modules/Storage.md | 34 ++++++++++++++++++++++++++++++++-- src/storage_manager/index.js | 10 ++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index 40652c8e2..c6db2e197 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -345,9 +345,39 @@ grapesjs.init({ In case `projectData` is defined, the initial storage load will be automatically skipped. -### HTML code in project data +### HTML code with project data -### Inline data +The project data doesn't contain HTML/CSS of your pages as its main purpose is to collect only the strictly necessary information. +In case you have a strict requirement to execute also other logic connected to the store of your project data (eg. deploy HTML/CSS result to the stage environment) you can enrich your remote calls by using the `onStore` option in your remote storage. + +```js +const getPagesHtml = async (editor) => { + const pages = editor.Pages.getAll(); + return await Promise.all(pages.map(async (page) => { + return { + id: page.getId(), + html: await editor.getCode('html', { component: page.getMainComponent() }), + }; + })); +}; + +grapesjs.init({ + // ... + storageManager: { + type: 'remote', + // Enrich the store call + options: { + remote: { + onStore: (data, editor) => { + return { id: projectID, data }; + }, + } + }, + }, +}) +``` + +### Inline project data diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index 6813d20db..2a402b5ab 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -290,16 +290,18 @@ export default () => { } try { + const editor = this.em?.getEditor(); + if (data) { - let toStore = (onStore && (await onStore(data))) || data; - toStore = (opts.onStore && (await opts.onStore(toStore))) || toStore; + let toStore = (onStore && (await onStore(data, editor))) || data; + toStore = (opts.onStore && (await opts.onStore(toStore, editor))) || toStore; await storage.store(toStore, opts); result = data; } else { result = await storage.load(opts); result = this.__clearKeys(result); - result = (opts.onLoad && (await opts.onLoad(result))) || result; - result = (onLoad && (await onLoad(result))) || result; + result = (opts.onLoad && (await opts.onLoad(result, editor))) || result; + result = (onLoad && (await onLoad(result, editor))) || result; } this.onAfter(ev, result); this.onEnd(ev, result); From 416a8cf398d882360bc20d91fe00a5af27333fa1 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 22 Mar 2022 15:10:57 +0100 Subject: [PATCH 32/35] Update doc with Inline project data --- docs/modules/Storage.md | 86 +++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/docs/modules/Storage.md b/docs/modules/Storage.md index c6db2e197..40d948707 100644 --- a/docs/modules/Storage.md +++ b/docs/modules/Storage.md @@ -303,21 +303,22 @@ editor.Storage.add('remote', { -### Examples + + ## Common use cases -### Init without first load +### Skip initial load -In case you're using a `remote` storage, you might probably want to skip the initial remote call by loading the project instantly. In that case, you can specify the `projectData` on initialization. +In case you're using the `remote` storage, you might probably want to skip the initial remote call by loading the project instantly. In that case, you can specify the `projectData` on initialization. ```js // Get the data before initializing the editor (eg. printed on server-side). @@ -348,28 +349,25 @@ In case `projectData` is defined, the initial storage load will be automatically ### HTML code with project data The project data doesn't contain HTML/CSS of your pages as its main purpose is to collect only the strictly necessary information. -In case you have a strict requirement to execute also other logic connected to the store of your project data (eg. deploy HTML/CSS result to the stage environment) you can enrich your remote calls by using the `onStore` option in your remote storage. +In case you have a strict requirement to execute also other logic connected to the store of your project data (eg. deploy HTML/CSS result to the stage environment) you can enrich your remote calls by using the `onStore` option in the remote configuration. ```js -const getPagesHtml = async (editor) => { - const pages = editor.Pages.getAll(); - return await Promise.all(pages.map(async (page) => { - return { - id: page.getId(), - html: await editor.getCode('html', { component: page.getMainComponent() }), - }; - })); -}; - grapesjs.init({ // ... storageManager: { type: 'remote', - // Enrich the store call options: { remote: { + // Enrich the store call onStore: (data, editor) => { - return { id: projectID, data }; + const pagesHtml = editor.Pages.getAll().map(page => { + const component = page.getMainComponent(); + return { + html: editor.getHtml({ component }), + css: editor.getCss({ component }) + } + }); + return { id: projectID, data, pagesHtml }; }, } }, @@ -379,6 +377,60 @@ grapesjs.init({ ### Inline project data +In might be a case where the editor is not connected to any storage but simply read/write the data in inputs placed in a form. For such a case you can create an inline storage. + +```html +
+ + +
+ +
+ + +``` + +In the example above we're relying on two hidden inputs, one for containing the project data and the another one for the HTML/CSS. + + From 1cdb9d15f552c4e5c1597b04e23f30c04290b649 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 23 Mar 2022 14:23:15 +0100 Subject: [PATCH 33/35] Remove Model dependency in storages --- index.d.ts | 71 +++++++++++----------- src/storage_manager/model/LocalStorage.js | 3 +- src/storage_manager/model/RemoteStorage.js | 3 +- 3 files changed, 37 insertions(+), 40 deletions(-) diff --git a/index.d.ts b/index.d.ts index 45a714640..22f2748b8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3568,40 +3568,28 @@ declare namespace Backbone { * Add new storage * @example * storageManager.add('local2', { - * load: function(keys, clb, clbErr) { - * var res = {}; - * for (var i = 0, len = keys.length; i < len; i++){ - * var v = localStorage.getItem(keys[i]); - * if(v) res[keys[i]] = v; - * } - * clb(res); // might be called inside some async method - * // In case of errors... - * // clbErr('Went something wrong'); + * async load(storageOptions) { + * // ... + * }, + * async store(data, storageOptions) { + * // ... * }, - * store: function(data, clb, clbErr) { - * for(var key in data) - * localStorage.setItem(key, data[key]); - * clb(); // might be called inside some async method - * } * }); * @param id - Storage ID * @param storage - Storage wrapper * @param storage.load - Load method * @param storage.store - Store method */ - add(id: string, storage: { - load: (...params: any[]) => any; - store: (...params: any[]) => any; - }): this; + add(id: string, storage: IStorage): this; /** * Returns storage by id * @param id - Storage ID */ - get(id: string): any | null; + get(id: string): IStorage | null; /** * Returns all storages */ - getStorages(): any[]; + getStorages(): Record; /** * Returns current storage type */ @@ -3612,30 +3600,41 @@ declare namespace Backbone { */ setCurrent(id: string): this; /** - * Store key-value resources in the current storage + * Store data in the current storage. + * @param data Project data. + * @param options Storage options. + * @returns Stored data. * @example - * storageManager.store({item1: value1, item2: value2}); - * @param data - Data in key-value format, eg. {item1: value1, item2: value2} - * @param clb - Callback function + * const data = editor.getProjectData(); + * await storageManager.store(data); */ - store(data: any, clb: (...params: any[]) => any): any | null; + store(data: ProjectData, options: StorageOptions): Promise; /** * Load resource from the current storage by keys + * @param options Storage options. + * @returns Loaded data. * @example - * storageManager.load(['item1', 'item2'], res => { - * // res -> {item1: value1, item2: value2} - * }); - * storageManager.load('item1', res => { - * // res -> {item1: value1} - * }); - * @param keys - Keys to load - * @param clb - Callback function - */ - load(keys: string | string[], clb: (...params: any[]) => any): void; + * const data = await storageManager.load(); + * editor.loadProjectData(data); + * */ + load(options: StorageOptions): Promise; /** * Get current storage */ - getCurrentStorage(): Storage; + getCurrentStorage(): IStorage; + } + + interface ProjectData { + + } + + interface StorageOptions { + + } + + interface IStorage { + load: (options: StorageOptions) => Promise; + store: (data: ProjectData, options: StorageOptions) => Promise; } /** diff --git a/src/storage_manager/model/LocalStorage.js b/src/storage_manager/model/LocalStorage.js index a8f57e20f..dd70a6235 100644 --- a/src/storage_manager/model/LocalStorage.js +++ b/src/storage_manager/model/LocalStorage.js @@ -1,7 +1,6 @@ -import { Model } from 'common'; import { hasWin } from 'utils/mixins'; -export default class LocalStorage extends Model { +export default class LocalStorage { async store(data, opts = {}) { if (this.hasLocal(opts, true)) { localStorage.setItem(opts.key, JSON.stringify(data)); diff --git a/src/storage_manager/model/RemoteStorage.js b/src/storage_manager/model/RemoteStorage.js index 4540d9688..d4c6b4f0a 100644 --- a/src/storage_manager/model/RemoteStorage.js +++ b/src/storage_manager/model/RemoteStorage.js @@ -1,8 +1,7 @@ import fetch from 'utils/fetch'; -import { Model } from 'common'; import { isUndefined, isFunction, isString } from 'underscore'; -export default class RemoteStorage extends Model { +export default class RemoteStorage { async store(data, opts = {}) { await this.request(opts.urlStore, this.__props(opts, data), opts); } From a3b23e6a80a3a67e3cef0b74f720109a0961b423 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 28 Mar 2022 16:07:38 +0200 Subject: [PATCH 34/35] Adjust tests --- test/setup.js | 3 +- test/specs/asset_manager/index.js | 50 ++------------------------ test/specs/css_composer/index.js | 58 ++++++++++-------------------- test/specs/dom_components/index.js | 29 --------------- test/specs/grapesjs/index.js | 7 ++-- 5 files changed, 26 insertions(+), 121 deletions(-) diff --git a/test/setup.js b/test/setup.js index ad21f2765..760e78209 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,3 +1,4 @@ +import 'regenerator-runtime/runtime'; import 'whatwg-fetch'; import _ from 'underscore'; import sinon from 'sinon'; @@ -11,7 +12,7 @@ const localStorage = { }, removeItem(key, value) { delete this[key]; - } + }, }; global._ = _; diff --git a/test/specs/asset_manager/index.js b/test/specs/asset_manager/index.js index d4a0aa8f0..94b598da4 100644 --- a/test/specs/asset_manager/index.js +++ b/test/specs/asset_manager/index.js @@ -1,21 +1,9 @@ -import StorageManager from 'storage_manager'; import AssetManager from 'asset_manager'; describe('Asset Manager', () => { describe('Main', () => { - var obj; - var imgObj; - - var storage; - var storageId = 'testStorage'; - var storageMock = { - store(data) { - storage = data; - }, - load(keys) { - return storage; - } - }; + let obj; + let imgObj; beforeEach(() => { document.body.innerHTML = '
'; @@ -23,7 +11,7 @@ describe('Asset Manager', () => { type: 'image', src: 'path/to/image', width: 101, - height: 102 + height: 102, }; obj = new AssetManager(); obj.init(); @@ -81,37 +69,5 @@ describe('Asset Manager', () => { obj.add(imgObj); expect(obj.render()).toBeTruthy(); }); - - describe('With storage', () => { - var storageManager; - - beforeEach(() => { - document.body.innerHTML = '
'; - storageManager = new StorageManager().init({ - autoload: 0, - type: storageId - }); - obj = new AssetManager().init({ - stm: storageManager - }); - storageManager.add(storageId, storageMock); - document.body.querySelector('#asset-c').appendChild(obj.render()); - }); - - afterEach(() => { - storageManager = null; - }); - - test('Store and load data', () => { - obj.add(imgObj); - obj.store(); - obj.remove(imgObj.src); - obj.load({ assets: storage['gjs-assets'] }); - var asset = obj.get(imgObj.src); - expect(asset.get('width')).toEqual(imgObj.width); - expect(asset.get('height')).toEqual(imgObj.height); - expect(asset.get('type')).toEqual('image'); - }); - }); }); }); diff --git a/test/specs/css_composer/index.js b/test/specs/css_composer/index.js index 20dc78af5..bc38698ee 100644 --- a/test/specs/css_composer/index.js +++ b/test/specs/css_composer/index.js @@ -14,14 +14,14 @@ describe('Css Composer', () => { }, getCacheLoad() { return storagMock.load(); - } + }, }; var setSmConfig = () => { config.stm = storagMock; config.stm.getConfig = () => ({ storeCss: 1, - storeStyles: 1 + storeStyles: 1, }); }; var setEm = () => { @@ -49,19 +49,13 @@ describe('Css Composer', () => { }); test('storageKey returns array', () => { - expect(obj.storageKey() instanceof Array).toEqual(true); - }); - - test('storageKey returns correct composition', () => { - setSmConfig(); - expect(obj.storageKey()).toEqual(['css', 'styles']); + expect(obj.storageKey).toEqual('styles'); }); test('Store data', () => { setSmConfig(); setEm(); - var expected = { css: 'testCss', styles: '[]' }; - expect(obj.store(1)).toEqual(expected); + expect(JSON.parse(JSON.stringify(obj.store()))).toEqual({ styles: [] }); }); test('Rules are empty', () => { @@ -87,14 +81,7 @@ describe('Css Composer', () => { var sel = new obj.Selectors([{ name: 'test1' }]); var rule = obj.add(sel.models); expect(obj.getAll().length).toEqual(1); - expect( - obj - .getAll() - .at(0) - .get('selectors') - .at(0) - .get('name') - ).toEqual('test1'); + expect(obj.getAll().at(0).get('selectors').at(0).get('name')).toEqual('test1'); }); test('Returns correct rule with the same selector', () => { @@ -175,12 +162,8 @@ describe('Css Composer', () => { const rule = obj.getIdRule(name); expect(rule.selectorsToString()).toEqual(`#${name}`); expect(rule.styleToString()).toEqual(`color:red;`); - expect(rule.styleToString({ important: 1 })).toEqual( - `color:red !important;` - ); - expect(rule.styleToString({ important: ['color'] })).toEqual( - `color:red !important;` - ); + expect(rule.styleToString({ important: 1 })).toEqual(`color:red !important;`); + expect(rule.styleToString({ important: ['color'] })).toEqual(`color:red !important;`); }); test('Create a rule with id selector and state by using setIdRule()', () => { @@ -273,26 +256,24 @@ describe('Css Composer', () => { style: { color: 'blue' }, opts: { atRuleType: 'media', - atRuleParams: 'screen and (min-width: 480px)' - } + atRuleParams: 'screen and (min-width: 480px)', + }, }, { selector: '.class1:hover', style: { color: 'red' }, opts: { atRuleType: 'media', - atRuleParams: 'screen and (min-width: 480px)' - } - } + atRuleParams: 'screen and (min-width: 480px)', + }, + }, ]; toTest.forEach(test => { const { selector, style, opts } = test; const result = obj.setRule(selector, style, opts); expect(obj.getAll().length).toEqual(1); const rule = obj.getRule(selector, opts); - expect(rule.getAtRule()).toEqual( - `@${opts.atRuleType} ${opts.atRuleParams}` - ); + expect(rule.getAtRule()).toEqual(`@${opts.atRuleType} ${opts.atRuleParams}`); expect(rule.selectorsToString()).toEqual(selector); expect(rule.getStyle()).toEqual(style); }); @@ -308,15 +289,14 @@ describe('Css Composer', () => { { selector: '.class4, .class1 .class2', style: { color: 'blue' }, - opt: { atRuleType: 'media', atRuleParams: '(min-width: 480px)' } - } + opt: { atRuleType: 'media', atRuleParams: '(min-width: 480px)' }, + }, ]; toTest.forEach(test => { const { selector, style, opt = {} } = test; obj.setRule(selector, style, opt); const rule = obj.getRule(selector, opt); - const atRule = `${opt.atRuleType || ''} ${opt.atRuleParams || - ''}`.trim(); + const atRule = `${opt.atRuleType || ''} ${opt.atRuleParams || ''}`.trim(); expect(rule.getAtRule()).toEqual(atRule ? `@${atRule}` : ''); expect(rule.selectorsToString()).toEqual(selector); expect(rule.getStyle()).toEqual(style); @@ -356,7 +336,7 @@ describe('Css Composer', () => { `.test-rule{color:red;}`, `.test-rule:hover{color:blue;}`, `@media (max-width: 992px){.test-rule{color:darkred;}}`, - `@media (max-width: 992px){.test-rule:hover{color:darkblue;}}` + `@media (max-width: 992px){.test-rule:hover{color:darkblue;}}`, ]; const cssString = cssRules.join(''); obj.addCollection(cssString); @@ -388,9 +368,7 @@ describe('Css Composer', () => { expect(obj.get('.test-rule2', 'hover')).toBe(rule2); expect(rule3.get('mediaText')).toBe('(max-width: 992px)'); expect(obj.get('.test-rule3', null, '(max-width: 992px)')).toBe(rule3); - expect(obj.get('.test-rule4', 'hover', '(max-width: 992px)')).toBe( - rule4 - ); + expect(obj.get('.test-rule4', 'hover', '(max-width: 992px)')).toBe(rule4); }); }); }); diff --git a/test/specs/dom_components/index.js b/test/specs/dom_components/index.js index 6249acddc..39d9eab95 100644 --- a/test/specs/dom_components/index.js +++ b/test/specs/dom_components/index.js @@ -65,35 +65,6 @@ describe('DOM Components', () => { expect(DomComponents).toBeTruthy(); }); - test('storageKey returns array', () => { - expect(obj.storageKey() instanceof Array).toEqual(true); - }); - - test('storageKey returns correct composition', () => { - config.stm = { - getConfig() { - return { - storeHtml: 1, - storeComponents: 1, - }; - }, - }; - expect(obj.storageKey()).toEqual(['html', 'components']); - }); - - test('Store data', () => { - setSmConfig(); - (em.getHtml = () => { - return 'testHtml'; - }), - (obj = em.get('DomComponents').init(config)); - var expected = { - html: 'testHtml', - components: JSON.stringify(obj.getWrapper()), - }; - expect(obj.store(1)).toEqual(expected); - }); - test.skip('Store and load data', () => { setSmConfig(); setEm(); diff --git a/test/specs/grapesjs/index.js b/test/specs/grapesjs/index.js index 70a1da93d..8b3496f0c 100644 --- a/test/specs/grapesjs/index.js +++ b/test/specs/grapesjs/index.js @@ -12,12 +12,11 @@ describe('GrapesJS', () => { var storage; var storageId = 'testStorage'; var storageMock = { - store(data, clb) { + store(data) { storage = data; - clb(); }, - load(keys, clb) { - return clb(storage); + load() { + return storage; }, }; From 3cc3ada6e3c2507ca03be91f0931c424c9a86e79 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 29 Mar 2022 14:45:23 +0200 Subject: [PATCH 35/35] Update storage tests --- test/specs/storage_manager/index.js | 7 +- test/specs/storage_manager/model/Models.js | 164 +++++++-------------- 2 files changed, 57 insertions(+), 114 deletions(-) diff --git a/test/specs/storage_manager/index.js b/test/specs/storage_manager/index.js index f657f25eb..7672e1e36 100644 --- a/test/specs/storage_manager/index.js +++ b/test/specs/storage_manager/index.js @@ -48,12 +48,13 @@ describe('Storage Manager', () => { expect(obj.getCurrent()).toEqual('remote'); }); - test('Store do not execute if empty', () => { - expect(obj.store({ item: 'test' })).toBeUndefined(); + test('Store is executed', async () => { + const spy = jest.spyOn(obj, '__exec'); + await obj.store({ item: 'test' }); + expect(spy).toBeCalledTimes(1); }); test('Load default storages ', () => { - obj.loadDefaultProviders(); expect(obj.get('local')).toBeTruthy(); expect(obj.get('remote')).toBeTruthy(); expect(obj.get('test')).toBeFalsy(); diff --git a/test/specs/storage_manager/model/Models.js b/test/specs/storage_manager/model/Models.js index 6a99805a5..81b89a18c 100644 --- a/test/specs/storage_manager/model/Models.js +++ b/test/specs/storage_manager/model/Models.js @@ -2,11 +2,10 @@ import LocalStorage from 'storage_manager/model/LocalStorage'; import RemoteStorage from 'storage_manager/model/RemoteStorage'; describe('LocalStorage', () => { - var obj; - var itemName = 'testItem'; - var data = { + let obj; + let data = { item1: 'value1', - item2: 'value2' + item2: 'value2', }; beforeEach(() => { @@ -17,141 +16,84 @@ describe('LocalStorage', () => { obj = null; }); - test('Store and load items', () => { - obj.store(data); - var result = obj.load(['item1', 'item2']); + test('Store and load items', async () => { + await obj.store(data); + const result = await obj.load(); expect(result).toEqual(data); }); - - test('Store, update and load items', () => { - obj.store(data); - obj.store({ item3: 'value3' }); - obj.store({ item2: 'value22' }); - var result = obj.load(['item1', 'item2', 'item3']); - expect(result).toEqual({ - item1: 'value1', - item2: 'value22', - item3: 'value3' - }); - }); - - test('Remove items', () => { - var items = ['item1', 'item2', 'item3']; - obj.store(data); - obj.remove(items); - expect(obj.load(items)).toEqual({}); - }); }); describe('RemoteStorage', () => { - var obj; - var itemName = 'testItem'; - var endpointStore = 'testStoreEndpoint'; - var endpointLoad = 'testLoadEndpoint'; - var params = { test: 'testValue' }; - var storageOptions; - var data; - var mockResponse = (body = {}) => { + let obj; + let data; + let defaultOpts = { + urlStore: '/store', + urlLoad: '/load', + credentials: true, + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + }; + let mockResponse = (body = {}) => { return new window.Response(JSON.stringify(body), { status: 200, - headers: { 'Content-type': 'application/json' } + headers: { 'Content-type': 'application/json' }, }); }; beforeEach(() => { data = { item1: 'value1', - item2: 'value2' - }; - storageOptions = { - urlStore: endpointStore, - urlLoad: endpointLoad, - params + item2: 'value2', }; - obj = new RemoteStorage(storageOptions); - sinon - .stub(obj, 'fetch') - .returns(Promise.resolve(mockResponse({ data: 1 }))); + obj = new RemoteStorage(); + obj.request = jest.fn(() => Promise.resolve(mockResponse({ data: 1 }))); }); afterEach(() => { - obj.fetch.restore(); + obj.request.mockRestore(); obj = null; }); - test('Store data', () => { - obj.store(data); - const callResult = obj.fetch; - expect(callResult.called).toEqual(true); - expect(callResult.firstCall.args[0]).toEqual(endpointStore); - }); - - test('Load data', () => { - obj.load(['item1', 'item2']); - const callResult = obj.fetch; - expect(callResult.called).toEqual(true); - expect(callResult.firstCall.args[0]).toEqual(endpointLoad); - }); - - test("Load data with credentials option as 'include' by default", () => { - obj.load(['item1', 'item2']); - const callResult = obj.fetch; - expect(callResult.called).toEqual(true); - expect(callResult.firstCall.args[1]).toMatchObject({ - credentials: 'include' + test('Store data', async () => { + await obj.store(data, defaultOpts); + const { calls } = obj.request.mock; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe(defaultOpts.urlStore); + // expect(obj.request).toBeCalledWith(opts.urlStore, defaultOpts, opts); + const { body, ...args } = calls[0][1]; + expect(args).toEqual({ + method: 'POST', + headers: defaultOpts.headers, + credentials: defaultOpts.credentials, }); }); - test("Store data with credentials option as 'include' by default", () => { - obj.store(data); - const callResult = obj.fetch; - expect(callResult.called).toEqual(true); - expect(callResult.firstCall.args[1]).toMatchObject({ - credentials: 'include' + test('Load data', async () => { + await obj.load(defaultOpts); + const { calls } = obj.request.mock; + expect(obj.request).toBeCalledTimes(1); + expect(calls[0][0]).toBe(defaultOpts.urlLoad); + expect(calls[0][1]).toEqual({ + method: 'GET', + body: undefined, + headers: defaultOpts.headers, + credentials: defaultOpts.credentials, }); }); - test('Store data with credentials option as false ', () => { - obj = new RemoteStorage({ ...storageOptions, credentials: false }); - sinon - .stub(obj, 'fetch') - .returns(Promise.resolve(mockResponse({ data: 1 }))); - - obj.store(data); - const callResult = obj.fetch; - expect(callResult.called).toEqual(true); - expect(callResult.firstCall.args[1]).toMatchObject({ - credentials: false - }); - }); - - test('Load data with credentials option as false', () => { - obj = new RemoteStorage({ ...storageOptions, credentials: false }); - sinon - .stub(obj, 'fetch') - .returns(Promise.resolve(mockResponse({ data: 1 }))); - obj.load(['item1', 'item2']); - const callResult = obj.fetch; - expect(callResult.called).toEqual(true); - expect(callResult.firstCall.args[1]).toMatchObject({ - credentials: false + test('Load data with custom fetch options', async () => { + const customOpts = { customOpt: 'customValue' }; + await obj.load({ + ...defaultOpts, + fetchOptions: () => customOpts, }); - }); - test('Load data with custom fetch options as function', () => { - const customOpts = { customOpt: 'customValue' }; - obj = new RemoteStorage({ - ...storageOptions, - fetchOptions: () => { - return customOpts; - } + expect(obj.request).toBeCalledTimes(1); + expect(obj.request.mock.calls[0][1]).toEqual({ + method: 'GET', + body: undefined, + headers: defaultOpts.headers, + credentials: defaultOpts.credentials, + ...customOpts, }); - sinon - .stub(obj, 'fetch') - .returns(Promise.resolve(mockResponse({ data: 1 }))); - obj.load(['item1', 'item2']); - const callResult = obj.fetch; - expect(callResult.called).toEqual(true); - expect(callResult.firstCall.args[1]).toMatchObject(customOpts); }); });