Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/edit-post/src/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ function Layout( {
currentPost: { postId: currentPostId, postType: currentPostType },
onNavigateToEntityRecord,
onNavigateToPreviousEntityRecord,
previousSelectedBlockPath,
} = useNavigateToEntityRecord(
initialPostId,
initialPostType,
Expand Down Expand Up @@ -627,6 +628,7 @@ function Layout( {
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={ ! isWelcomeGuideVisible }
onActionPerformed={ onActionPerformed }
initialSelection={ previousSelectedBlockPath }
extraSidebarPanels={
showMetaBoxes && <MetaBoxes location="side" />
}
Expand Down
48 changes: 40 additions & 8 deletions packages/edit-post/src/hooks/use-navigate-to-entity-record.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
*/
import { useCallback, useReducer } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { store as editorStore, privateApis } from '@wordpress/editor';

/**
* Internal dependencies
*/
import { unlock } from '../lock-unlock';

const { useGenerateBlockPath } = unlock( privateApis );

/**
* A hook that records the 'entity' history in the post editor as a user
Expand All @@ -25,13 +32,24 @@ export default function useNavigateToEntityRecord(
initialPostType,
defaultRenderingMode
) {
const generateBlockPath = useGenerateBlockPath();
const [ postHistory, dispatch ] = useReducer(
( historyState, { type, post, previousRenderingMode } ) => {
(
historyState,
{ type, post, previousRenderingMode, selectedBlockPath }
) => {
if ( type === 'push' ) {
return [ ...historyState, { post, previousRenderingMode } ];
// Update the current item with the selected block path before pushing new item
const updatedHistory = [ ...historyState ];
const currentIndex = updatedHistory.length - 1;
updatedHistory[ currentIndex ] = {
...updatedHistory[ currentIndex ],
selectedBlockPath,
};
return [ ...updatedHistory, { post, previousRenderingMode } ];
}
if ( type === 'pop' ) {
// Try to leave one item in the history.
// Remove the current item from history
if ( historyState.length > 1 ) {
return historyState.slice( 0, -1 );
}
Expand All @@ -44,28 +62,40 @@ export default function useNavigateToEntityRecord(
},
]
);

const { post, previousRenderingMode } =
const { post, previousRenderingMode, selectedBlockPath } =
postHistory[ postHistory.length - 1 ];

const { getRenderingMode } = useSelect( editorStore );
const { setRenderingMode } = useDispatch( editorStore );

const onNavigateToEntityRecord = useCallback(
( params ) => {
// Generate block path from clientId if provided
const blockPath = params.selectedBlockClientId
? generateBlockPath( params.selectedBlockClientId )
: null;

dispatch( {
type: 'push',
post: { postId: params.postId, postType: params.postType },
// Save the current rendering mode so we can restore it when navigating back.
previousRenderingMode: getRenderingMode(),
selectedBlockPath: blockPath,
} );
setRenderingMode( defaultRenderingMode );
},
[ getRenderingMode, setRenderingMode, defaultRenderingMode ]
[
getRenderingMode,
setRenderingMode,
defaultRenderingMode,
generateBlockPath,
]
);

const onNavigateToPreviousEntityRecord = useCallback( () => {
dispatch( { type: 'pop' } );
dispatch( {
type: 'pop',
} );
if ( previousRenderingMode ) {
setRenderingMode( previousRenderingMode );
}
Expand All @@ -78,5 +108,7 @@ export default function useNavigateToEntityRecord(
postHistory.length > 1
? onNavigateToPreviousEntityRecord
: undefined,
// Return the selected block path from the current history item
previousSelectedBlockPath: selectedBlockPath,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,74 @@
*/
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { useCallback } from '@wordpress/element';
import { addQueryArgs } from '@wordpress/url';
import { privateApis as editorPrivateApis } from '@wordpress/editor';

/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';

const { useHistory } = unlock( routerPrivateApis );
const { useHistory, useLocation } = unlock( routerPrivateApis );
const { useGenerateBlockPath } = unlock( editorPrivateApis );

/**
* Hook to handle navigation to entity records and retrieve initial block selection.
*
* @return {Array} A tuple containing:
* - onNavigateToEntityRecord: Function to navigate to an entity record
* - initialBlockSelection: The block path or clientId to restore selection, or null if none stored
*/
export default function useNavigateToEntityRecord() {
const history = useHistory();
const { query, path } = useLocation();
const generateBlockPath = useGenerateBlockPath();

// Get the selected block from URL parameters and decode the block path
let initialBlockSelection = null;
if ( query.selectedBlock ) {
try {
initialBlockSelection = JSON.parse(
decodeURIComponent( query.selectedBlock )
);
} catch ( e ) {
// Invalid JSON, ignore
initialBlockSelection = null;
}
}

const onNavigateToEntityRecord = useCallback(
( params ) => {
history.navigate(
`/${ params.postType }/${ params.postId }?canvas=edit&focusMode=true`
// First, update the current URL to include the selected block path for when we navigate back
if ( params.selectedBlockClientId ) {
const blockPath = generateBlockPath(
params.selectedBlockClientId
);
if ( blockPath ) {
// Encode the block path as JSON in the URL
const currentUrl = addQueryArgs( path, {
...query,
selectedBlock: encodeURIComponent(
JSON.stringify( blockPath )
),
} );
history.navigate( currentUrl, { replace: true } );
}
}

// Then navigate to the new entity record
const url = addQueryArgs(
`/${ params.postType }/${ params.postId }`,
{
canvas: 'edit',
focusMode: true,
}
);

history.navigate( url );
},
[ history ]
[ history, path, query, generateBlockPath ]
);

return onNavigateToEntityRecord;
return [ onNavigateToEntityRecord, initialBlockSelection ];
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ function useNavigateToPreviousEntityRecord() {
export function useSpecificEditorSettings() {
const { query } = useLocation();
const { canvas = 'view' } = query;
const onNavigateToEntityRecord = useNavigateToEntityRecord();
const [ onNavigateToEntityRecord, initialBlockSelection ] =
useNavigateToEntityRecord();

const { settings, currentPostIsTrashed } = useSelect( ( select ) => {
const { getSettings } = select( editSiteStore );
const { getCurrentPostAttribute } = select( editorStore );
Expand Down Expand Up @@ -73,13 +75,15 @@ export function useSpecificEditorSettings() {
onNavigateToEntityRecord,
onNavigateToPreviousEntityRecord,
isPreviewMode: canvas === 'view',
initialBlockSelection,
};
}, [
settings,
canvas,
currentPostIsTrashed,
onNavigateToEntityRecord,
onNavigateToPreviousEntityRecord,
initialBlockSelection,
] );

return defaultEditorSettings;
Expand Down
4 changes: 3 additions & 1 deletion packages/edit-site/src/components/editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export default function EditSiteEditor( { isHomeRoute = false } ) {
);

const settings = useSpecificEditorSettings();
const { initialBlockSelection, ...editorSettings } = settings;
const { resetZoomLevel } = unlock( useDispatch( blockEditorStore ) );
const { createSuccessNotice } = useDispatch( noticesStore );
const history = useHistory();
Expand Down Expand Up @@ -222,7 +223,8 @@ export default function EditSiteEditor( { isHomeRoute = false } ) {
postType={ postWithTemplate ? context.postType : postType }
postId={ postWithTemplate ? context.postId : postId }
templateId={ postWithTemplate ? postId : undefined }
settings={ settings }
settings={ editorSettings }
initialSelection={ initialBlockSelection }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one thing that I'm uncertain about is that we pass "postId" to the Editor component. This means the "blocks" property of the "post" might or might not be present (the post might not have parsed the blocks yet).

I guess we're making an assumption that the the blocks are already parsed and clientIds stable. IT seems ok in the context of entity navigation but wanted to highlight this.

className="edit-site-editor__editor-interface"
customSaveButton={
_isPreviewingTheme && <SaveButton size="compact" />
Expand Down
32 changes: 31 additions & 1 deletion packages/editor/src/components/editor/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { Notice } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useEffect } from '@wordpress/element';
import { store as blockEditorStore } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import { TEMPLATE_POST_TYPE } from '../../store/constants';
import { useRestoreBlockFromPath } from '../../utils/block-selection-path';
import EditorInterface from '../editor-interface';
import { ExperimentalEditorProvider } from '../provider';
import Sidebar from '../sidebar';
Expand All @@ -25,6 +28,7 @@ function Editor( {
settings,
children,
initialEdits,
initialSelection,

// This could be part of the settings.
onActionPerformed,
Expand Down Expand Up @@ -94,6 +98,32 @@ function Editor( {
[ postType, postId, templateId ]
);

const { selectBlock } = useDispatch( blockEditorStore );
const restoreBlockFromPath = useRestoreBlockFromPath();

// Restore initial block selection if provided (e.g., from navigation)
useEffect( () => {
if ( ! initialSelection || ! hasLoadedPost || ! post ) {
return;
}

// Use setTimeout to ensure blocks are fully rendered before selecting
const timeoutId = setTimeout( () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing the timeout is needed because this code needs to run after the resetBlocks actions is called.

We should consider whether we should add a initialSelection to BlockEditorProvider (and pass down the prop) which would allow us to call this at the right moment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, maybe checking if the block is still the tree? Pretty edge casey I guess. I'm just thinking if block is deleted or changed (collaborative editing?) since navigation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if block is deleted or changed (collaborative editing?) since navigation

If that happens nothing gets selected, which should be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider whether we should add a initialSelection to BlockEditorProvider (and pass down the prop) which would allow us to call this at the right moment.

I'm not sure I understand. What would be the right place to select the block if not the Editor component?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the right place to select the block if not the Editor component?

The component responsible for actually inserting the blocks into the canvas aka BlockEditorProvider

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean passing the previously selected block as the selection prop to useBlockSync? I can't get that to work because useBlockSync expects a selection object with clientIds, and at the point we'd have to calculate those clientIds in the provider, the block order still corresponds to the previous screen, so the clientId can't be found. I added some console.logs and with or without the timeout, the code in the Editor component still runs after any code in the Provider.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly an alternative to setTimeout here and adding things to the event loop stack — and without going into a rabbit hole of architectural refactors — is to wait for the actual data, e.g., check the store for blocks?

If the store isn't ready, it returns null.

Once blocks are available, blocks becomes an array and triggers the useEffect.

Would that be more idiomatic to React?

	// Check if blocks are available in the store
	const blocks = useSelect(
		( select ) => {
			if ( ! hasLoadedPost || ! post ) {
				return null;
			}
			// Try to get blocks - returns empty array if not ready
			try {
				return select( blockEditorStore ).getBlocks();
			} catch {
				return null;
			}
		},
		[ hasLoadedPost, post ]
	);

	// Restore initial block selection when blocks are available
	useEffect( () => {
		if ( ! initialSelection || ! hasLoadedPost || ! post || ! blocks ) {
			return;
		}

		// Blocks are now available, try to restore selection
		const clientId = restoreBlockFromPath( initialSelection );
		if ( clientId ) {
			selectBlock( clientId );
		}
	}, [
		initialSelection,
		hasLoadedPost,
		post,
		blocks,
		selectBlock,
		restoreBlockFromPath,
	] );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's cheaper to use a timeout than query the store for blocks 😅

Maybe the need for the timeout points to underlying issues. hasLoadedPost should give us the correct information, and it isn't doing so. But I'm not convinced that should block a PR like this one. If we keep trying to go back to first principles in every piece of work we'll be awfully slow at getting anything done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's cheaper to use a timeout than query the store for blocks

Ah, yeah, fair call. 👍🏻

const clientId = restoreBlockFromPath( initialSelection );
if ( clientId ) {
selectBlock( clientId );
}
}, 0 );

return () => clearTimeout( timeoutId );
}, [
initialSelection,
hasLoadedPost,
post,
selectBlock,
restoreBlockFromPath,
] );

return (
<>
{ hasLoadedPost && ! post && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,27 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
[ saveEntityRecord, userCanCreatePages ]
);

const { getSelectedBlockClientId } = useSelect( blockEditorStore );

/**
* Wraps onNavigateToEntityRecord to automatically include the currently selected block.
* This ensures that navigation can restore the selection when returning to the previous entity.
*/
const wrappedOnNavigateToEntityRecord = useCallback(
( params ) => {
if ( ! settings.onNavigateToEntityRecord ) {
return;
}
const selectedBlockClientId = getSelectedBlockClientId();

return settings.onNavigateToEntityRecord( {
...params,
selectedBlockClientId,
} );
},
[ settings, getSelectedBlockClientId ]
);

const allowedBlockTypes = useMemo( () => {
// Omit hidden block types if exists and non-empty.
if ( hiddenBlockTypes && hiddenBlockTypes.length > 0 ) {
Expand All @@ -282,9 +303,12 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
return useMemo( () => {
const blockEditorSettings = {
...Object.fromEntries(
Object.entries( settings ).filter( ( [ key ] ) =>
BLOCK_EDITOR_SETTINGS.includes( key )
)
Object.entries( settings )
.filter( ( [ key ] ) =>
BLOCK_EDITOR_SETTINGS.includes( key )
)
// Exclude onNavigateToEntityRecord since we're wrapping it
.filter( ( [ key ] ) => key !== 'onNavigateToEntityRecord' )
),
[ globalStylesDataKey ]: globalStylesData,
[ globalStylesLinksDataKey ]: globalStylesLinksData,
Expand All @@ -294,6 +318,9 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
hasFixedToolbar,
isDistractionFree,
keepCaretInsideBlock,
onNavigateToEntityRecord: settings.onNavigateToEntityRecord
? wrappedOnNavigateToEntityRecord
: undefined,
[ getMediaSelectKey ]: ( select, attachmentId ) => {
return select( coreStore ).getEntityRecord(
'postType',
Expand Down Expand Up @@ -377,6 +404,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) {
globalStylesLinksData,
renderingMode,
editMediaEntity,
wrappedOnNavigateToEntityRecord,
] );
}

Expand Down
7 changes: 7 additions & 0 deletions packages/editor/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import GlobalStylesUIWrapper from './components/global-styles';
import { StyleBookPreview } from './components/style-book';
import { useGlobalStyles, useStyle } from './components/global-styles/hooks';
import { GlobalStylesActionMenu } from './components/global-styles/menu';
import {
useGenerateBlockPath,
useRestoreBlockFromPath,
} from './utils/block-selection-path';

const { store: interfaceStore, ...remainingInterfaceApis } = interfaceApis;

Expand Down Expand Up @@ -56,6 +60,9 @@ lock( privateApis, {
StyleBookPreview,
useGlobalStyles,
useStyle,
// Block selection
useGenerateBlockPath,
useRestoreBlockFromPath,
// This is a temporary private API while we're updating the site editor to use EditorProvider.
interfaceStore,
...remainingInterfaceApis,
Expand Down
Loading
Loading