Repository navigation
Editor: Decouple local and remote autosave monitors #79801
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
Open
Mamaduka
wants to merge
8
commits into
trunk
Choose a base branch
from
try/autosave-monitor-follow-ups
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
aa5cf82
Autosave: Colocate files and add tests for local
Mamaduka 882e038
Refactor
Mamaduka 40cc6d5
Gate by PostTypeSupportCheck
Mamaduka 903825a
Simplify LocalAutosaveMonitor unit tests
Mamaduka de57539
Fold tests in AutosaveMonitor
Mamaduka 095fbc0
Add note regarding 'autosave-exists' notice
Mamaduka 28b7ac1
Remove isSavingPost gating and just check savable content
Mamaduka d91e31d
Add new e2e test
Mamaduka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # AutosaveMonitor | ||
|
|
||
| `AutosaveMonitor` monitors the changes made to the edited post and triggers an autosave when there is something new to save. The post is checked on an interval, which defaults to the editor's `autosaveInterval` setting and saves to the server via the editor store's `autosave` action. Both the interval and the save callback can be overridden with props. | ||
|
|
||
| It also creates the `autosave-exists` warning notice when the server already holds an autosave more recent than the loaded post. This notice therefore only appears where `AutosaveMonitor` is rendered (and where the post type supports `autosave`). An editor that receives `settings.autosave` but does not render `AutosaveMonitor` will not surface this warning. | ||
|
|
||
| ## Example | ||
|
|
||
| ```js | ||
| const MyLayout = () => ( | ||
| <main> | ||
| <AutosaveMonitor interval={ 30 } /> | ||
| <MyEditor /> | ||
| </main> | ||
| ); | ||
| ``` | ||
|
|
||
| # LocalAutosaveMonitor | ||
|
|
||
| `LocalAutosaveMonitor` ensures that a local copy of the current post is regularly saved in `sessionStorage`. It runs its own timer, sharing only the `useInterval` helper with `AutosaveMonitor`. Additionally, it will: | ||
|
|
||
| - attempt to clear the local copy if a copy is successfully saved on the server; | ||
| - warn the user upon loading a post that there is a local copy that can be loaded; | ||
| - suppress that restore notice when the server already holds a more recent autosave. | ||
|
|
||
| `LocalAutosaveMonitor` observes a saving interval defined specifically for local autosaves, in contrast with remote (server-side) autosaving. | ||
|
|
||
| The interval used for the local autosave can be modified by updating the editor settings | ||
| ```js | ||
| wp.data.dispatch( 'core/editor' ).updateEditorSettings( { | ||
| localAutosaveInterval: 45, | ||
| } ); | ||
| ``` | ||
|
|
||
| ## Example | ||
|
|
||
| ```js | ||
| const MyLayout = () => ( | ||
| <main> | ||
| <LocalAutosaveMonitor /> | ||
| <MyEditor /> | ||
| </main> | ||
| ); | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,24 @@ | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { useCallback, useEffect, useRef } from '@wordpress/element'; | ||
| import { useEffect, useRef } from '@wordpress/element'; | ||
| import { ifCondition, usePrevious } from '@wordpress/compose'; | ||
| import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; | ||
| import { store as coreStore } from '@wordpress/core-data'; | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { parse } from '@wordpress/blocks'; | ||
| import { store as noticesStore } from '@wordpress/notices'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import AutosaveMonitor from '../autosave-monitor'; | ||
| import { | ||
| localAutosaveGet, | ||
| localAutosaveClear, | ||
| } from '../../store/local-autosave'; | ||
| import { store as editorStore } from '../../store'; | ||
| import PostTypeSupportCheck from '../post-type-support-check'; | ||
| import useInterval from './use-interval'; | ||
|
|
||
| const requestIdleCallback = window.requestIdleCallback | ||
| ? window.requestIdleCallback | ||
|
|
@@ -63,21 +65,11 @@ function useAutosaveNotice() { | |
| ); | ||
|
|
||
| useEffect( () => { | ||
| let localAutosave = localAutosaveGet( postId, isEditedPostNew ); | ||
| if ( ! localAutosave ) { | ||
| const edits = localAutosaveGet( postId, isEditedPostNew ); | ||
| if ( ! edits ) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| localAutosave = JSON.parse( localAutosave ); | ||
| } catch { | ||
| // Not usable if it can't be parsed. | ||
| return; | ||
| } | ||
|
|
||
| const { post_title: title, content, excerpt } = localAutosave; | ||
| const edits = { title, content, excerpt }; | ||
|
|
||
| const { getEditedPostAttribute, getEditorSettings } = | ||
| registry.select( editorStore ); | ||
|
|
||
|
|
@@ -172,11 +164,18 @@ function useAutosavePurge() { | |
| }, [ isEditedPostNew, postId ] ); | ||
| } | ||
|
|
||
| function LocalAutosaveMonitor() { | ||
| function LocalAutosaveMonitorInner() { | ||
| const { autosave } = useDispatch( editorStore ); | ||
| const deferredAutosave = useCallback( () => { | ||
| requestIdleCallback( () => autosave( { local: true } ) ); | ||
| }, [] ); | ||
| const { | ||
| getCurrentPostId, | ||
| isEditedPostNew, | ||
| isEditedPostEmpty, | ||
| isEditedPostDirty, | ||
| isPostAutosavingLocked, | ||
| getEditedPostAttribute, | ||
| } = useSelect( editorStore ); | ||
| const { getReferenceByDistinctEdits } = useSelect( coreStore ); | ||
|
|
||
| useAutosaveNotice(); | ||
| useAutosavePurge(); | ||
|
|
||
|
|
@@ -186,11 +185,56 @@ function LocalAutosaveMonitor() { | |
| [] | ||
| ); | ||
|
|
||
| // Reference of the edits last considered for autosaving. Mutable state that | ||
| // must not trigger a re-render, hence a ref. | ||
| const lastEditsReferenceRef = useRef(); | ||
|
Mamaduka marked this conversation as resolved.
Resolved
|
||
|
|
||
| useInterval( () => { | ||
| // A sessionStorage backup only needs saveable content, checked inline | ||
| // because `isEditedPostSaveable()` short-circuits to false during any | ||
| // save, including a remote autosave, which must not block the backup. | ||
| const hasSaveableContent = | ||
| !! getEditedPostAttribute( 'title' ) || | ||
| !! getEditedPostAttribute( 'excerpt' ) || | ||
| ! isEditedPostEmpty(); | ||
| if ( ! hasSaveableContent || isPostAutosavingLocked() ) { | ||
| return; | ||
| } | ||
|
|
||
| const editsReference = getReferenceByDistinctEdits(); | ||
| const hasNewEdits = editsReference !== lastEditsReferenceRef.current; | ||
| if ( ! hasNewEdits || ! isEditedPostDirty() ) { | ||
| return; | ||
| } | ||
|
|
||
| // Skip the backup when the edits already match it. | ||
| const backup = localAutosaveGet( | ||
| getCurrentPostId(), | ||
| isEditedPostNew() | ||
| ); | ||
| const isBackupCurrent = | ||
| backup && | ||
| Object.keys( backup ).every( | ||
| ( key ) => backup[ key ] === getEditedPostAttribute( key ) | ||
| ); | ||
| if ( isBackupCurrent ) { | ||
| return; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will never update the And if yes, we might want to add test coverage for this scenario. |
||
| } | ||
|
|
||
| // Only consume the edits reference when we save the backup, so edits | ||
| // made in the meantime aren't skipped. | ||
| lastEditsReferenceRef.current = editsReference; | ||
| requestIdleCallback( () => autosave( { local: true } ) ); | ||
| }, localAutosaveInterval ); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function LocalAutosaveMonitor() { | ||
| return ( | ||
| <AutosaveMonitor | ||
| interval={ localAutosaveInterval } | ||
| autosave={ deferredAutosave } | ||
| /> | ||
| <PostTypeSupportCheck supportKeys="autosave"> | ||
| <LocalAutosaveMonitorInner /> | ||
| </PostTypeSupportCheck> | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -200,7 +244,7 @@ function LocalAutosaveMonitor() { | |
| * - `useAutosaveNotice` hook: Manages the creation of a notice prompting the user to restore a local autosave, if one exists. | ||
| * - `useAutosavePurge` hook: Ejects a local autosave after a successful save occurs. | ||
| * - `hasSessionStorageSupport` function: Checks if the current environment supports browser sessionStorage. | ||
| * - `LocalAutosaveMonitor` component: Uses the `AutosaveMonitor` component to perform autosaves at a specified interval. | ||
| * - `LocalAutosaveMonitor` component: Saves a sessionStorage backup of the post at the `localAutosaveInterval`. | ||
| * | ||
| * The module also checks for sessionStorage support and conditionally exports the `LocalAutosaveMonitor` component based on that. | ||
| * | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.