From a7c4991af58e9b1ca1ff99c3878c1b64c869974f Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Tue, 13 Sep 2022 21:01:53 +0100 Subject: [PATCH] Wired up lexical editor saving no issue - fixed API returning "Invalid mobiledoc structure" errors when `mobiledoc:null` is sent in the payload alongside `lexical: '{...}'` - updated Admin's `posts` and `pages` adapters to always add `?formats=mobiledoc,lexical` because the API doesn't return `lexical` by default - added `lexical` attribute to Admin's Post model - updated `lexical-editor` controller and related components to work with `lexical` always being a JSON string rather than a parsed object - updated `` to pass through the lexical state string as initial state and wired up the `onChange` prop --- ghost/admin/app/adapters/page.js | 4 +++ ghost/admin/app/adapters/post.js | 4 +++ .../components/gh-koenig-editor-lexical.hbs | 5 ++- .../app/components/koenig-lexical-editor.js | 4 +-- ghost/admin/app/controllers/lexical-editor.js | 35 +++++++++---------- ghost/admin/app/models/post.js | 3 +- ghost/admin/app/templates/lexical-editor.hbs | 2 +- ghost/core/core/server/models/post.js | 2 +- ghost/core/test/e2e-api/admin/posts.test.js | 4 ++- 9 files changed, 37 insertions(+), 26 deletions(-) diff --git a/ghost/admin/app/adapters/page.js b/ghost/admin/app/adapters/page.js index 4a73a826ef..5199196a8b 100644 --- a/ghost/admin/app/adapters/page.js +++ b/ghost/admin/app/adapters/page.js @@ -7,6 +7,10 @@ export default class Page extends ApplicationAdapter { } buildQuery(store, modelName, options) { + if (!options.formats) { + options.formats = 'mobiledoc,lexical'; + } + return options; } } diff --git a/ghost/admin/app/adapters/post.js b/ghost/admin/app/adapters/post.js index 9af059847b..edce3d0bd1 100644 --- a/ghost/admin/app/adapters/post.js +++ b/ghost/admin/app/adapters/post.js @@ -25,6 +25,10 @@ export default class Post extends ApplicationAdapter { } buildQuery(store, modelName, options) { + if (!options.formats) { + options.formats = 'mobiledoc,lexical'; + } + return options; } } diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs index af6f43b27e..3ed24c5326 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs @@ -35,7 +35,10 @@ data-test-editor-title-input={{true}} /> - + {{!-- Loading editor...

}> - - + +
diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 64eaadc764..a3e17b1c91 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -27,7 +27,7 @@ const TIMEDSAVE_TIMEOUT = 60000; // this array will hold properties we need to watch for this.hasDirtyAttributes let watchedProps = [ - 'post.scratch', + 'post.lexicalScratch', 'post.titleScratch', 'post.hasDirtyAttributes', 'post.tags.[]', @@ -190,8 +190,8 @@ export default class LexicalEditorController extends Controller { } @action - updateScratch(mobiledoc) { - this.set('post.scratch', mobiledoc); + updateScratch(lexical) { + this.set('post.lexicalScratch', JSON.stringify(lexical)); // save 3 seconds after last edit this._autosaveTask.perform(); @@ -544,10 +544,10 @@ export default class LexicalEditorController extends Controller { // Set the properties that are indirected - // Set mobiledoc equal to what's in the editor but create a copy so that + // Set lexical equal to what's in the editor but create a copy so that // nested objects/arrays don't keep references which can mean that both - // scratch and mobiledoc get updated simultaneously - this.set('post.mobiledoc', JSON.parse(JSON.stringify(this.post.scratch || null))); + // scratch and lexical get updated simultaneously + this.set('post.lexical', this.post.lexicalScratch || null); // Set a default title if (!this.get('post.titleScratch').trim()) { @@ -692,17 +692,17 @@ export default class LexicalEditorController extends Controller { post.updateTags(); this._previousTagNames = this._tagNames; - // update the scratch property if it's `null` and we get a blank mobiledoc + // update the scratch property if it's `null` and we get a blank lexical // back from the API - prevents "unsaved changes" modal on new+blank posts - if (!post.scratch) { - post.set('scratch', JSON.parse(JSON.stringify(post.get('mobiledoc')))); + if (!post.lexicalScratch) { + post.set('lexicalScratch', post.get('lexical')); } // if the two "scratch" properties (title and content) match the post, // then it's ok to set hasDirtyAttributes to false // TODO: why is this necessary? let titlesMatch = post.get('titleScratch') === post.get('title'); - let bodiesMatch = JSON.stringify(post.get('scratch')) === JSON.stringify(post.get('mobiledoc')); + let bodiesMatch = post.get('lexicalScratch') === post.get('lexical'); if (titlesMatch && bodiesMatch) { this.set('hasDirtyAttributes', false); @@ -789,7 +789,7 @@ export default class LexicalEditorController extends Controller { // edit of the post // TODO: can these be `boundOneWay` on the model as per the other attrs? post.set('titleScratch', post.get('title')); - post.set('scratch', post.get('mobiledoc')); + post.set('lexicalScratch', post.get('lexical')); this._previousTagNames = this._tagNames; @@ -980,15 +980,12 @@ export default class LexicalEditorController extends Controller { } // scratch isn't an attr so needs a manual dirty check - let mobiledoc = post.get('mobiledoc'); - let scratch = post.get('scratch'); + let lexical = post.get('lexical'); + let scratch = post.get('lexicalScratch'); // additional guard in case we are trying to compare null with undefined - if (scratch || mobiledoc) { - let mobiledocJSON = JSON.stringify(mobiledoc); - let scratchJSON = JSON.stringify(scratch); - - if (scratchJSON !== mobiledocJSON) { - this._leaveModalReason = {reason: 'mobiledoc is different', context: {current: mobiledocJSON, scratch: scratchJSON}}; + if (scratch || lexical) { + if (scratch !== lexical) { + this._leaveModalReason = {reason: 'lexical is different', context: {current: lexical, scratch}}; return true; } } diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index 144331b2d7..dc63648be1 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -94,7 +94,7 @@ export default Model.extend(Comparable, ValidationEngine, { metaDescription: attr('string'), metaTitle: attr('string'), mobiledoc: attr('json-string'), - lexical: attr('json-string'), + lexical: attr(), plaintext: attr('string'), publishedAtUTC: attr('moment-utc'), slug: attr('string'), @@ -122,6 +122,7 @@ export default Model.extend(Comparable, ValidationEngine, { primaryTag: reads('tags.firstObject'), scratch: null, + lexicalScratch: null, titleScratch: null, // HACK: used for validation so that date/time can be validated based on diff --git a/ghost/admin/app/templates/lexical-editor.hbs b/ghost/admin/app/templates/lexical-editor.hbs index baeb2ae7c6..faa20b6682 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -57,7 +57,7 @@ @titlePlaceholder={{concat (capitalize this.post.displayName) " title"}} @onTitleChange={{this.updateTitleScratch}} @onTitleBlur={{perform this.saveTitleTask}} - @body={{readonly this.post.scratch}} + @body={{readonly this.post.lexicalScratch}} @bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}} @onBodyChange={{this.updateScratch}} @headerOffset={{editor.headerHeight}} diff --git a/ghost/core/core/server/models/post.js b/ghost/core/core/server/models/post.js index 42eff5cdb4..628aefd260 100644 --- a/ghost/core/core/server/models/post.js +++ b/ghost/core/core/server/models/post.js @@ -610,7 +610,7 @@ Post = ghostBookshelf.Model.extend({ // CASE: ?force_rerender=true passed via Admin API // CASE: html is null, but mobiledoc exists (only important for migrations & importing) if ( - this.hasChanged('mobiledoc') + (this.hasChanged('mobiledoc') && !this.get('lexical')) || options.force_rerender || (!this.get('html') && (options.migrating || options.importing)) ) { diff --git a/ghost/core/test/e2e-api/admin/posts.test.js b/ghost/core/test/e2e-api/admin/posts.test.js index 300dbf52b1..c07100e891 100644 --- a/ghost/core/test/e2e-api/admin/posts.test.js +++ b/ghost/core/test/e2e-api/admin/posts.test.js @@ -67,7 +67,8 @@ describe('Posts API', function () { [0, [], 0, 'Testing post creation with mobiledoc'] ]] ] - }) + }), + lexical: null }; await agent @@ -86,6 +87,7 @@ describe('Posts API', function () { it('Can create a post with lexical', async function () { const post = { title: 'Lexical test', + mobiledoc: null, lexical: JSON.stringify({ editorState: { root: {