@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
334 lines (312 loc) • 8.96 kB
JavaScript
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import {
Modal,
Button,
ExternalLink,
__experimentalHStack as HStack,
withFilters,
} from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { addQueryArgs } from '@wordpress/url';
import { useEffect, createInterpolateElement } from '@wordpress/element';
import { addAction, removeAction } from '@wordpress/hooks';
import { useInstanceId } from '@wordpress/compose';
import { store as coreStore } from '@wordpress/core-data';
import { unlock } from '../../lock-unlock';
import { DOCUMENT_SIZE_LIMIT_EXCEEDED } from '../../utils/sync-error-messages';
/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
function CollaborationContext() {
const { isCollaborationSupported, syncConnectionStatus } = useSelect(
( select ) => {
const selectors = unlock( select( coreStore ) );
return {
isCollaborationSupported: selectors.isCollaborationSupported(),
syncConnectionStatus: selectors.getSyncConnectionStatus(),
};
},
[]
);
if ( isCollaborationSupported ) {
return null;
}
if ( DOCUMENT_SIZE_LIMIT_EXCEEDED === syncConnectionStatus?.error?.code ) {
return (
<p>
{ __(
'Because this post is too large for real-time collaboration, only one person can edit at a time.'
) }
</p>
);
}
return (
<p>
{ __(
'Because this post uses plugins that aren’t compatible with real-time collaboration, only one person can edit at a time.'
) }
</p>
);
}
function PostLockedModal() {
const instanceId = useInstanceId( PostLockedModal );
const hookName = 'core/editor/post-locked-modal-' + instanceId;
const { autosave, updatePostLock } = useDispatch( editorStore );
const {
isCollaborationEnabled,
isLocked,
isTakeover,
user,
postId,
postLockUtils,
activePostLock,
postType,
previewLink,
} = useSelect( ( select ) => {
const {
isCollaborationEnabledForCurrentPost,
isPostLocked,
isPostLockTakeover,
getPostLockUser,
getCurrentPostId,
getActivePostLock,
getEditedPostAttribute,
getEditedPostPreviewLink,
getEditorSettings,
} = select( editorStore );
const { getPostType } = select( coreStore );
return {
isCollaborationEnabled: isCollaborationEnabledForCurrentPost(),
isLocked: isPostLocked(),
isTakeover: isPostLockTakeover(),
user: getPostLockUser(),
postId: getCurrentPostId(),
postLockUtils: getEditorSettings().postLockUtils,
activePostLock: getActivePostLock(),
postType: getPostType( getEditedPostAttribute( 'type' ) ),
previewLink: getEditedPostPreviewLink(),
};
}, [] );
useEffect( () => {
/**
* Keep the lock refreshed.
*
* When the user does not send a heartbeat in a heartbeat-tick
* the user is no longer editing and another user can start editing.
*
* @param {Object} data Data to send in the heartbeat request.
*/
function sendPostLock( data ) {
if ( isLocked ) {
return;
}
data[ 'wp-refresh-post-lock' ] = {
lock: activePostLock,
post_id: postId,
};
}
/**
* Refresh post locks: update the lock string or show the dialog if somebody has taken over editing.
*
* @param {Object} data Data received in the heartbeat request
*/
function receivePostLock( data ) {
if ( ! data[ 'wp-refresh-post-lock' ] ) {
return;
}
const received = data[ 'wp-refresh-post-lock' ];
if ( received.lock_error ) {
// Auto save and display the takeover modal.
autosave();
updatePostLock( {
isLocked: true,
isTakeover: true,
user: {
name: received.lock_error.name,
avatar: received.lock_error.avatar_src_2x,
},
} );
} else if ( received.new_lock ) {
updatePostLock( {
isLocked: false,
activePostLock: received.new_lock,
} );
}
}
/**
* Unlock the post before the window is exited.
*/
function releasePostLock() {
if ( isLocked || ! activePostLock ) {
return;
}
const data = new window.FormData();
data.append( 'action', 'wp-remove-post-lock' );
data.append( '_wpnonce', postLockUtils.unlockNonce );
data.append( 'post_ID', postId );
data.append( 'active_post_lock', activePostLock );
if ( window.navigator.sendBeacon ) {
window.navigator.sendBeacon( postLockUtils.ajaxUrl, data );
} else {
const xhr = new window.XMLHttpRequest();
xhr.open( 'POST', postLockUtils.ajaxUrl, false );
xhr.send( data );
}
}
// Details on these events on the Heartbeat API docs
// https://developer.wordpress.org/plugins/javascript/heartbeat-api/
addAction( 'heartbeat.send', hookName, sendPostLock );
addAction( 'heartbeat.tick', hookName, receivePostLock );
window.addEventListener( 'beforeunload', releasePostLock );
return () => {
removeAction( 'heartbeat.send', hookName );
removeAction( 'heartbeat.tick', hookName );
window.removeEventListener( 'beforeunload', releasePostLock );
};
}, [] );
if ( ! isLocked ) {
return null;
}
// Avoid sending the modal if sync is supported, but retain functionality around locks etc.
if ( isCollaborationEnabled ) {
return null;
}
const userDisplayName = user.name;
const userAvatar = user.avatar;
const unlockUrl = addQueryArgs( 'post.php', {
'get-post-lock': '1',
lockKey: true,
post: postId,
action: 'edit',
_wpnonce: postLockUtils.nonce,
} );
const allPostsUrl = addQueryArgs( 'edit.php', {
post_type: postType?.slug,
} );
const allPostsLabel = __( 'Exit editor' );
return (
<Modal
title={
isTakeover
? __( 'Someone else has taken over this post' )
: __( 'This post is already being edited' )
}
focusOnMount
shouldCloseOnClickOutside={ false }
shouldCloseOnEsc={ false }
isDismissible={ false }
// Do not remove this class, as this class is used by third party plugins.
className="editor-post-locked-modal"
size="medium"
>
<HStack alignment="top" spacing={ 6 }>
{ !! userAvatar && (
<img
src={ userAvatar }
alt={ __( 'Avatar' ) }
className="editor-post-locked-modal__avatar"
width={ 64 }
height={ 64 }
/>
) }
<div>
{ !! isTakeover && (
<>
<p>
{ createInterpolateElement(
userDisplayName
? sprintf(
/* translators: %s: user's display name */
__(
'<strong>%s</strong> now has editing control of this post (<PreviewLink />). Don’t worry, your changes up to this moment have been saved.'
),
userDisplayName
)
: __(
'Another user now has editing control of this post (<PreviewLink />). Don’t worry, your changes up to this moment have been saved.'
),
{
strong: <strong />,
PreviewLink: (
<ExternalLink href={ previewLink }>
{ __( 'preview' ) }
</ExternalLink>
),
}
) }
</p>
<CollaborationContext />
</>
) }
{ ! isTakeover && (
<>
<p>
{ createInterpolateElement(
userDisplayName
? sprintf(
/* translators: %s: user's display name */
__(
'<strong>%s</strong> is currently working on this post (<PreviewLink />), which means you cannot make changes, unless you take over.'
),
userDisplayName
)
: __(
'Another user is currently working on this post (<PreviewLink />), which means you cannot make changes, unless you take over.'
),
{
strong: <strong />,
PreviewLink: (
<ExternalLink href={ previewLink }>
{ __( 'preview' ) }
</ExternalLink>
),
}
) }
</p>
<CollaborationContext />
<p>
{ __(
'If you take over, the other user will lose editing control to the post, but their changes will be saved.'
) }
</p>
</>
) }
<HStack
className="editor-post-locked-modal__buttons"
justify="flex-end"
>
{ ! isTakeover && (
<Button
__next40pxDefaultSize
variant="tertiary"
href={ unlockUrl }
>
{ __( 'Take over' ) }
</Button>
) }
<Button
__next40pxDefaultSize
variant="primary"
href={ allPostsUrl }
>
{ allPostsLabel }
</Button>
</HStack>
</div>
</HStack>
</Modal>
);
}
/**
* A modal component that is displayed when a post is locked for editing by another user.
* The modal provides information about the lock status and options to take over or exit the editor.
*
* @return {React.ReactNode} The rendered PostLockedModal component.
*/
export default globalThis.IS_GUTENBERG_PLUGIN
? withFilters( 'editor.PostLockedModal' )( PostLockedModal )
: PostLockedModal;