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 `<KoenigLexicalEditor>` to pass through the lexical state string as initial state and wired up the `onChange` prop
This commit is contained in:
Kevin Ansfield 2022-09-13 21:01:53 +01:00
parent 2d9dd4639d
commit a7c4991af5
9 changed files with 37 additions and 26 deletions

View File

@ -7,6 +7,10 @@ export default class Page extends ApplicationAdapter {
}
buildQuery(store, modelName, options) {
if (!options.formats) {
options.formats = 'mobiledoc,lexical';
}
return options;
}
}

View File

@ -25,6 +25,10 @@ export default class Post extends ApplicationAdapter {
}
buildQuery(store, modelName, options) {
if (!options.formats) {
options.formats = 'mobiledoc,lexical';
}
return options;
}
}

View File

@ -35,7 +35,10 @@
data-test-editor-title-input={{true}}
/>
<KoenigLexicalEditor />
<KoenigLexicalEditor
@lexical={{@body}}
@onChange={{@onBodyChange}}
/>
{{!-- <KoenigEditor
@mobiledoc={{@body}}

View File

@ -91,8 +91,8 @@ export default class KoenigLexicalEditor extends Component {
<div className={['koenig-react-editor', this.args.className].filter(Boolean).join(' ')}>
<ErrorHandler>
<Suspense fallback={<p className="koenig-react-editor-loading">Loading editor...</p>}>
<KoenigComposer>
<KoenigEditor />
<KoenigComposer initialEditorState={this.args.lexical}>
<KoenigEditor onChange={this.args.onChange} />
</KoenigComposer>
</Suspense>
</ErrorHandler>

View File

@ -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;
}
}

View File

@ -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

View File

@ -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}}

View File

@ -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))
) {

View File

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