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: {