-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Editor: Sync blocks state to edited post content #9403
Conversation
const isIgnored = includes( options.ignoreTypes, action.type ); | ||
const isIgnored = ( | ||
includes( options.ignoreTypes, action.type ) || | ||
options.isIgnored( action ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why two options to ignore actions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely "sugar". In fact, in retrospect (or an earlier alternate branch) I'd even expressed isIgnored
as a composition of ignoreTypes
using _.overSome
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neglected to finish my thought: I've personally found we are heavy-handed in using ignoreTypes
for things which don't obviously seem warranting ignore. This also serves the purpose of allowing an action to be extended with additional metadata to trigger the ignore (which is what we're doing here with considering "programmatic changes" as something to ignore).
// While editing text, value is maintained in state. Prefer this value, | ||
// deferring to the incoming prop only if not editing (value `null`). | ||
let { value } = this.state; | ||
if ( value === null ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this better achieved with getDerivedStateFromProps
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I guess that would be a matter of returning value
as state using props value when... the state
is set to null
? I can give it a try.
edit-post/index.js
Outdated
@@ -66,8 +65,12 @@ export function initializeEditor( id, postType, postId, settings, overridePost ) | |||
'core/editor.publish', | |||
] ); | |||
|
|||
if ( initialEdits ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we move this to the Editor
component's did mount instead to avoid relying on dispatch
as a global?
My thinking also is that ultimately we should get rid of initializeEditor
function entirely, It should be just an Editor
component exposed in edit-post
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thinking also is that ultimately we should get rid of initializeEditor function entirely, It should be just an Editor component exposed in edit-post
That's an interesting thought! It also solves another issue where it's hard to extract things like template synchronization out of EditorProvider because it relies on the post entity having been resolved; something we can't do from initializeEditor
, but we could do from the (edit-post
) Editor component.
return { | ||
onChange( content ) { | ||
editPost( { content } ); | ||
editPost( { content }, { skipContentParse: true } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes me think, this should be a local state and only called when onPersist
is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps.
Good: It might help us avoid the need for this skipContentParse
flag altogether.
Bad: We rely on editPost
to trigger the change detection which would alert the user to their changes being unsaved. As noted in the original comment, this isn't entirely perfect as implemented anyways.
// such that it is considered synced at the time of the optimistic update, | ||
// and divergences in the persisted value are accounted for in the post | ||
// reset which follows (i.e. mark as dirty if saved content differs). | ||
dispatch( editPost( { content } ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not certain I follow why we need this?
Is it an optimistic update, if it's the case should we add the optimist: { id: POST_UPDATE_TRANSACTION_ID }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably not a good sign that my initial thought is I didn't quite remember why I added it. I'll see if I can improve the comment.
I think the issue was that without it, the content was being considered as an unsaved edit because we compare what we receive via updatePost
against edits
, keeping those which differ.
let previousContent; | ||
|
||
return ( next ) => ( action ) => { | ||
const result = next( action ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this to be a middleware? You probably guessed that I don't like middlewares too much :P because I think they obfuscate code even more than effects :). Also, it's one other pattern that may be replaced by something else.
If I understand properly it's a middleware for two reasons (correct me if I'm wrong):
- Because it's not really triggered by a single action but it's more triggered by a change in the state
- Because it needs to perform side effects (async stuff)
Did you consider:
- Making it a higher-order reducer. (I guess the argument against this is the upcoming async parser, instead of a required sync parser in reducers)
- Making it an effect? How many actions need to trigger this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this be a combination of an effect (adding the parsed content to the required actions) and a higher-order reducer using the parsed content?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, my original intention was that this didn't fall neatly into our other patterns (effects, generators) because it doesn't care which action triggered the change, it only cares about the change in the content state values. In retrospect, I'd probably agree that this is the exact sort of unpredictable behavior we might want to discourage.
An original thought I'd had was to support some kind of action creator to set content using our new routines generators (enabling both our current sync parse and a future async parse). Roughly:
function* setContent( content ) {
const blocks = yield { type: 'PARSE', content };
yield resetBlocks( blocks );
}
In thinking more on it, it seemed a bit redundant considering we have other actions which operate on any edit to a post property; having a separate one for "content" seemed out of place.
Should those action creators do the parse when the payload includes content
? What are those action creators? editPost
, updatePost
, resetPost
... ?
Worth thinking on more...
Thanks for the review @youknowriad . I'm still liking the overarching goal here, but finding some of the compromises to have introduced new confusions / shortcomings. I want to spend more time thinking on how best to move forward here (your input very welcome). In the meantime, we'll need a more immediate solution for #9433. I'll see if there's an option immediately available to us, otherwise we should fall back to a simple revert of #9288 . |
packages/editor/src/store/actions.js
Outdated
@@ -22,6 +23,12 @@ import { | |||
* @return {Object} Action object. | |||
*/ | |||
export function setupEditor( post, autosaveStatus ) { | |||
deprecated( 'setupEditor', { | |||
version: '2.9', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3.9 🤦♂️
Granted I don't like the middleware approach, but I think personally, I like the direction this PR is going. |
b1c2fa0
to
26a32ff
Compare
Rebased to resolve conflicts. Couple updates:
Current plan is to fork out a couple separate pull requests:
In parallel, I'll continue to think on when and how the content -> blocks sync should occur (substitute for current proposed middleware). |
In considering how this enables #7970, I think it helps to consider how an asynchronous parse highlights the need for intentionality around parse times, particularly in disruptiveness to user flow. If we imagine that the editor enters into a read-only state while undergoing the parse (so as to avoid destructiveness / syncing concerns relating to changes made during the parse), it becomes evident that there are only few occasions under which we expect a parse to occur; namely, when we change entirely the context of the post being edited. In terms of actions, this would be the With that in mind, it becomes clearer that the parse doesn't need to act as a middleware responding to any change in content, but rather as part of the routine of resetting the post. My recommendation here therefore would be to call One issue we'll encounter here is that currently, we call to |
26a32ff
to
9ba4034
Compare
The second was addressed and merged in #9513. The latest rebase here reflects this. I am working on the first as I speak, and will follow-up with a pull request reference. |
Template validation refactor in #9916 |
Extracted |
Ignored initial state action should default to non-dirty
Non-apparent assumption that past should be created even when action type is ignored
Previously the Code Editor would sync its textarea value with state, hence the need to prefer state content edits when retrieving content. The Code Editor component since been revised to maintain its own value state internally. Passing content from blocks state and persisting on subsequent blur is sufficient to drop this logic.
9ba4034
to
25787b1
Compare
With #9916 and #9921 having been merged, I've rebased here to resolve conflicts. To be clear, this is not quite ready yet, as I need to implement the revised approach as described in #9403 (comment) . |
One of the original objectives here was in trying to eliminate a setup sequence for the editor. As I've since discovered in #11267, this may in-fact be inevitable in order to support both post canonical date and initial edits, while also ensuring that at most a single parse of content occurs when content "changes". #11267 includes a number of the revisions which had been proposed here, including an explicit treatment of I would have liked to be able to at least consolidate We may, however, be able to consolidate some of the redundant handling between With all this in mind, I think this pull request no longer serves much value in its current form. |
Blocked by: #9287(merged)Fixes regression introduced in #9288 (#9288 (comment))(Fixed separately by #9448)Related: #7970
This pull request seeks to refactor how blocks state is kept in sync with the edited post content, in an effort to simplify / improve consistency of behaviors around editor content initialization.
Status: This pull request is rather large and includes a few changes that could potentially be forked out into their own, blocking pull requests (see commits as standalone indicators). It is otherwise expected to be fully functional and passing all tests. New end-to-end tests are planned to verify associated code editor impact, but there are presently issues with the local Docker installation, making end-to-end test authoring impossible for the moment.
Goal: The editor module needn't be so conscious of distinct start and end points. It should render itself as derived by its state at any given moment in time. This should help enable efforts like that of #7970, where the editor may need to present itself without relying on a synchronous initialization setup. It is made challenging by various factors such as: enacting a template, assigning initial edits, alerting the user to the presence of an existing autosave which can be restored. To me, these are considerations which should be moved into the scope of
edit-post
. This has its own set of challenges, and is proposed to be addressed separately to the work here.Changes:
setupEditor
,setupEditorState
, andcheckTemplateValidity
actions from the editor moduleRESET_BLOCKS
overridePost
as representing an initial set of edits (for demo content, auto-draft edits)withHistory
higher-order reducerPostTextEditor
Implementation notes:
edits
, where ifcontent
was assigned asedits
(as in by the Code Editor textarea), it would be preferred. This was to accommodate the fact that parsing content is a non-performant task to perform on each key press, thus the parse was deferred. However, this introduced a dual source of truth for content (sometimes as a serialization of blocks state, sometimes as theedits.content
property).Future tasks:
EditorProvider
into theedit-post
module, namely template synchronization and the autosave warning.Testing instructions:
Verify there are no regressions in the initialization of the editor, notably:
Verify that HTML edits made in the Code Editor are respected: