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