diff --git a/ghost/admin/app/components/editor/modals/update-snippet.js b/ghost/admin/app/components/editor/modals/update-snippet.js index c5b15e4a04..d33d96fadf 100644 --- a/ghost/admin/app/components/editor/modals/update-snippet.js +++ b/ghost/admin/app/components/editor/modals/update-snippet.js @@ -15,6 +15,10 @@ export default class UpdateSnippetModal extends Component { } if (lexical) { snippet.lexical = lexical; + + if (!snippet.mobiledoc) { + snippet.mobiledoc = {}; + } } yield snippet.save(); diff --git a/ghost/admin/app/controllers/editor.js b/ghost/admin/app/controllers/editor.js index 6592aced41..a8483259fb 100644 --- a/ghost/admin/app/controllers/editor.js +++ b/ghost/admin/app/controllers/editor.js @@ -173,8 +173,8 @@ export default class EditorController extends Controller { get snippets() { return this._snippets .reject(snippet => snippet.get('isNew')) - .sort((a, b) => a.name.localeCompare(b.name)) - .filterBy('lexical', null); + .reject(snippet => JSON.stringify(snippet.mobiledoc) === '{}') + .sort((a, b) => a.name.localeCompare(b.name)); } @computed('session.user.{isAdmin,isEditor}') diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index afd897e244..0cc39e5e4b 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -22,6 +22,7 @@ import {isBlank} from '@ember/utils'; import {isArray as isEmberArray} from '@ember/array'; import {isHostLimitError, isServerUnreachableError, isVersionMismatchError} from 'ghost-admin/services/ajax'; import {isInvalidError} from 'ember-ajax/errors'; +import {mobiledocToLexical} from '@tryghost/kg-converters'; import {inject as service} from '@ember/service'; const DEFAULT_TITLE = '(Untitled)'; @@ -171,14 +172,15 @@ export default class LexicalEditorController extends Controller { return this.store.peekAll('snippet'); } - @computed('_snippets.@each.{name,isNew}') + @computed('_snippets.@each.{name,isNew,mobiledoc,lexical}') get snippets() { const snippets = this._snippets .reject(snippet => snippet.get('isNew')) .sort((a, b) => a.name.localeCompare(b.name)) .filter(item => item.lexical !== null); + return snippets.map((item) => { - item.value = item.lexical; + item.value = JSON.stringify(item.lexical); return item; }); @@ -360,9 +362,10 @@ export default class LexicalEditorController extends Controller { } @action - saveSnippet(snippet) { - const snippetData = {name: snippet.name, lexical: snippet.value, mobiledoc: '{}'}; - let snippetRecord = this.store.createRecord('snippet', snippetData); + saveNewSnippet(snippet) { + const snippetData = {name: snippet.name, lexical: JSON.parse(snippet.value), mobiledoc: {}}; + const snippetRecord = this.store.createRecord('snippet', snippetData); + return snippetRecord.save().then(() => { this.notifications.closeAlerts('snippet.save'); this.notifications.showNotification( @@ -388,9 +391,9 @@ export default class LexicalEditorController extends Controller { const existingSnippet = this.snippets.find(snippet => snippet.name.toLowerCase() === snippetNameLC); if (existingSnippet) { - await this.confirmUpdateSnippet(existingSnippet, {lexical: data.value, mobiledoc: '{}'}); + await this.confirmUpdateSnippet(existingSnippet, {lexical: data.value}); } else { - await this.saveSnippet(data); + await this.saveNewSnippet(data); } } @@ -771,10 +774,46 @@ export default class LexicalEditorController extends Controller { } } - // load supplementel data such as the members count in the background + // load supplemental data such as snippets and members count in the background @restartableTask *backgroundLoaderTask() { yield this.store.query('snippet', {limit: 'all'}); + this.syncMobiledocSnippets(); + } + + @action + syncMobiledocSnippets() { + const snippets = this.store.peekAll('snippet'); + + snippets.forEach((snippet) => { + if (!snippet.lexical || snippet.lexical.syncedAt && moment.utc(snippet.lexical.syncedAt).isBefore(snippet.updatedAtUTC)) { + const serializedLexical = mobiledocToLexical(JSON.stringify(snippet.mobiledoc)); + + // we get a full Lexical doc from the converter but Lexical only + // stores an array of nodes in it's copy/paste dataset that we use for snippets + const lexical = JSON.parse(serializedLexical); + let nodes = lexical.root.children; + + // for a single-paragraph text selection Lexical only stores the + // text children in the nodes array + if (nodes.length === 1 && nodes[0].type === 'paragraph') { + nodes = nodes[0].children; + } + + const lexicalData = { + namespace: 'KoenigEditor', + nodes + }; + + // set syncedAt so we can check if mobiledoc has been updated in next sync + lexicalData.syncedAt = moment.utc().toISOString(); + + snippet.lexical = lexicalData; + + // kick off a background save, we already have .lexical updated which is what we need + snippet.save(); + } + }); } /* Public methods --------------------------------------------------------*/ diff --git a/ghost/admin/app/serializers/snippet.js b/ghost/admin/app/serializers/snippet.js new file mode 100644 index 0000000000..5811b72a26 --- /dev/null +++ b/ghost/admin/app/serializers/snippet.js @@ -0,0 +1,8 @@ +import ApplicationSerializer from 'ghost-admin/serializers/application'; + +export default class PostSerializer extends ApplicationSerializer { + attrs = { + createdAtUTC: {key: 'created_at'}, + updatedAtUTC: {key: 'updated_at'} + }; +}