diff --git a/lib/init.php b/lib/init.php
index c6a7c66af502f..a834a946805b2 100644
--- a/lib/init.php
+++ b/lib/init.php
@@ -187,6 +187,33 @@ function register_site_icon_url( $response ) {
add_filter( 'rest_index', 'register_site_icon_url' );
+/**
+ * Exposes the site logo to the Gutenberg editor through the WordPress REST
+ * API. This is used for fetching this information when user has no rights
+ * to update settings.
+ *
+ * @since 10.9
+ *
+ * @param WP_REST_Response $response Response data served by the WordPress REST index endpoint.
+ * @return WP_REST_Response
+ */
+function register_site_logo_to_rest_index( $response ) {
+ $site_logo_id = get_theme_mod( 'custom_logo' );
+ $response->data['site_logo'] = $site_logo_id;
+ if ( $site_logo_id ) {
+ $response->add_link(
+ 'https://api.w.org/featuredmedia',
+ rest_url( 'wp/v2/media/' . $site_logo_id ),
+ array(
+ 'embeddable' => true,
+ )
+ );
+ }
+ return $response;
+}
+
+add_filter( 'rest_index', 'register_site_logo_to_rest_index' );
+
add_theme_support( 'widgets-block-editor' );
/**
diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
index fec66c1ef0913..178f42ec04b15 100644
--- a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
+++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
@@ -59,7 +59,7 @@ function useInsertionPoint( {
let _destinationRootClientId = rootClientId;
let _destinationIndex;
- if ( insertionIndex ) {
+ if ( insertionIndex !== undefined ) {
// Insert into a specific index.
_destinationIndex = insertionIndex;
} else if ( clientId ) {
diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js
index 62e13c9b6b0af..a034f1af2d7e7 100644
--- a/packages/block-editor/src/components/use-block-drop-zone/index.js
+++ b/packages/block-editor/src/components/use-block-drop-zone/index.js
@@ -43,11 +43,6 @@ export function getNearestBlockIndex( elements, position, orientation ) {
let candidateDistance;
elements.forEach( ( element, index ) => {
- // Ensure the element is a block. It should have the `wp-block` class.
- if ( ! element.classList.contains( 'wp-block' ) ) {
- return;
- }
-
const rect = element.getBoundingClientRect();
const [ distance, edge ] = getDistanceToNearestEdge(
position,
@@ -105,7 +100,10 @@ export default function useBlockDropZone( {
const onBlockDrop = useOnBlockDrop( targetRootClientId, targetBlockIndex );
const throttled = useThrottle(
useCallback( ( event, currentTarget ) => {
- const blockElements = Array.from( currentTarget.children );
+ const blockElements = Array.from( currentTarget.children ).filter(
+ // Ensure the element is a block. It should have the `wp-block` class.
+ ( element ) => element.classList.contains( 'wp-block' )
+ );
const targetIndex = getNearestBlockIndex(
blockElements,
{ x: event.clientX, y: event.clientY },
diff --git a/packages/block-editor/src/components/use-block-drop-zone/test/index.js b/packages/block-editor/src/components/use-block-drop-zone/test/index.js
index 42a9476be0cd4..d60ee4ba574b5 100644
--- a/packages/block-editor/src/components/use-block-drop-zone/test/index.js
+++ b/packages/block-editor/src/components/use-block-drop-zone/test/index.js
@@ -83,22 +83,6 @@ describe( 'getNearestBlockIndex', () => {
expect( result ).toBeUndefined();
} );
- it( 'returns `undefined` if the elements do not have the `wp-block` class', () => {
- const nonBlockElements = [
- { classList: createMockClassList( 'some-other-class' ) },
- ];
- const position = { x: 0, y: 0 };
- const orientation = 'horizontal';
-
- const result = getNearestBlockIndex(
- nonBlockElements,
- position,
- orientation
- );
-
- expect( result ).toBeUndefined();
- } );
-
describe( 'Vertical block lists', () => {
const orientation = 'vertical';
diff --git a/packages/block-library/src/categories/edit.js b/packages/block-library/src/categories/edit.js
index 12adf03e1ffce..1b49ceae36acf 100644
--- a/packages/block-library/src/categories/edit.js
+++ b/packages/block-library/src/categories/edit.js
@@ -28,7 +28,7 @@ export default function CategoriesEdit( {
const { categories, isRequesting } = useSelect( ( select ) => {
const { getEntityRecords } = select( coreStore );
const { isResolving } = select( 'core/data' );
- const query = { per_page: -1, hide_empty: true };
+ const query = { per_page: -1, hide_empty: true, context: 'view' };
return {
categories: getEntityRecords( 'taxonomy', 'category', query ),
isRequesting: isResolving( 'core', 'getEntityRecords', [
@@ -151,7 +151,7 @@ export default function CategoriesEdit( {
) }
- { ! isRequesting && categories.length === 0 && (
+ { ! isRequesting && categories?.length === 0 && (
{ __(
'Your site does not have any posts, so there is nothing to display here at the moment.'
@@ -159,7 +159,7 @@ export default function CategoriesEdit( {
) }
{ ! isRequesting &&
- categories.length > 0 &&
+ categories?.length > 0 &&
( displayAsDropdown
? renderCategoryDropdown()
: renderCategoryList() ) }
diff --git a/packages/block-library/src/post-terms/edit.js b/packages/block-library/src/post-terms/edit.js
index 3532fb747f759..aa75e2f55177e 100644
--- a/packages/block-library/src/post-terms/edit.js
+++ b/packages/block-library/src/post-terms/edit.js
@@ -2,7 +2,6 @@
* External dependencies
*/
import classnames from 'classnames';
-import { find } from 'lodash';
/**
* WordPress dependencies
@@ -21,7 +20,7 @@ import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
-import useTermLinks from './use-term-links';
+import usePostTerms from './use-post-terms';
export default function PostTermsEdit( {
attributes,
@@ -34,28 +33,18 @@ export default function PostTermsEdit( {
const selectedTerm = useSelect(
( select ) => {
if ( ! term ) return {};
- const taxonomies = select( coreStore ).getTaxonomies( {
- per_page: -1,
- } );
- return (
- find(
- taxonomies,
- ( taxonomy ) =>
- taxonomy.slug === term && taxonomy.visibility.show_ui
- ) || {}
- );
+ const { getTaxonomy } = select( coreStore );
+ const taxonomy = getTaxonomy( term );
+ return taxonomy?.visibility?.show_ui ? taxonomy : {};
},
[ term ]
);
-
- const { termLinks, isLoadingTermLinks } = useTermLinks( {
+ const { postTerms, hasPostTerms, isLoading } = usePostTerms( {
postId,
postType,
term: selectedTerm,
} );
-
const hasPost = postId && postType;
- const hasTermLinks = termLinks && termLinks.length > 0;
const blockProps = useBlockProps( {
className: classnames( {
[ `has-text-align-${ textAlign }` ]: textAlign,
@@ -89,19 +78,22 @@ export default function PostTermsEdit( {
/>
- { isLoadingTermLinks &&
}
-
- { hasTermLinks &&
- ! isLoadingTermLinks &&
- termLinks.reduce( ( prev, curr ) => [
- prev,
- ' | ',
- curr,
- ] ) }
-
- { ! isLoadingTermLinks &&
- ! hasTermLinks &&
- // eslint-disable-next-line camelcase
+ { isLoading &&
}
+ { ! isLoading &&
+ hasPostTerms &&
+ postTerms
+ .map( ( postTerm ) => (
+
event.preventDefault() }
+ >
+ { postTerm.name }
+
+ ) )
+ .reduce( ( prev, curr ) => [ prev, ' | ', curr ] ) }
+ { ! isLoading &&
+ ! hasPostTerms &&
( selectedTerm?.labels?.no_terms ||
__( 'Term items not found.' ) ) }
diff --git a/packages/block-library/src/post-terms/use-post-terms.js b/packages/block-library/src/post-terms/use-post-terms.js
new file mode 100644
index 0000000000000..facfffa21117f
--- /dev/null
+++ b/packages/block-library/src/post-terms/use-post-terms.js
@@ -0,0 +1,38 @@
+/**
+ * WordPress dependencies
+ */
+import { useEntityProp, store as coreStore } from '@wordpress/core-data';
+import { useSelect } from '@wordpress/data';
+
+export default function usePostTerms( { postId, postType, term } ) {
+ const { rest_base: restBase, slug } = term;
+ const [ termIds ] = useEntityProp( 'postType', postType, restBase, postId );
+ return useSelect(
+ ( select ) => {
+ if ( ! termIds ) {
+ // Waiting for post terms to be fetched.
+ return { isLoading: true };
+ }
+ if ( ! termIds.length ) {
+ return { isLoading: false };
+ }
+ const { getEntityRecords, isResolving } = select( coreStore );
+ const taxonomyArgs = [
+ 'taxonomy',
+ slug,
+ {
+ include: termIds,
+ context: 'view',
+ },
+ ];
+ const terms = getEntityRecords( ...taxonomyArgs );
+ const _isLoading = isResolving( 'getEntityRecords', taxonomyArgs );
+ return {
+ postTerms: terms,
+ isLoading: _isLoading,
+ hasPostTerms: !! terms?.length,
+ };
+ },
+ [ termIds ]
+ );
+}
diff --git a/packages/block-library/src/post-terms/use-term-links.js b/packages/block-library/src/post-terms/use-term-links.js
deleted file mode 100644
index 387e8cf92d3c4..0000000000000
--- a/packages/block-library/src/post-terms/use-term-links.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * External dependencies
- */
-import { map } from 'lodash';
-
-/**
- * WordPress dependencies
- */
-import { useEntityProp, store as coreStore } from '@wordpress/core-data';
-import { useSelect } from '@wordpress/data';
-
-export default function useTermLinks( { postId, postType, term } ) {
- const { rest_base: restBase, slug } = term;
-
- const [ termItems ] = useEntityProp(
- 'postType',
- postType,
- restBase,
- postId
- );
-
- const { termLinks, isLoadingTermLinks } = useSelect(
- ( select ) => {
- const { getEntityRecord } = select( coreStore );
-
- let loaded = true;
-
- const links = map( termItems, ( itemId ) => {
- const item = getEntityRecord( 'taxonomy', slug, itemId );
-
- if ( ! item ) {
- return ( loaded = false );
- }
-
- return (
- event.preventDefault() }
- >
- { item.name }
-
- );
- } );
-
- return {
- termLinks: links,
- isLoadingTermLinks: ! loaded,
- };
- },
- [ termItems ]
- );
-
- return { termLinks, isLoadingTermLinks };
-}
diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js
index 05e225513392d..aecda3ec75d2b 100644
--- a/packages/block-library/src/site-logo/edit.js
+++ b/packages/block-library/src/site-logo/edit.js
@@ -17,6 +17,7 @@ import {
ResizableBox,
Spinner,
ToggleControl,
+ Icon,
} from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import {
@@ -73,7 +74,7 @@ const SiteLogo = ( {
title: siteEntities.title,
...pick( getSettings(), [ 'imageSizes', 'maxWidth' ] ),
};
- } );
+ }, [] );
function onResizeStart() {
toggleSelection( false );
@@ -255,27 +256,38 @@ export default function LogoEdit( {
const [ logoUrl, setLogoUrl ] = useState();
const [ error, setError ] = useState();
const ref = useRef();
- const { mediaItemData, siteLogo, url } = useSelect( ( select ) => {
- const siteSettings = select( coreStore ).getEditedEntityRecord(
- 'root',
- 'site'
- );
- const mediaItem = siteSettings.site_logo
- ? select( coreStore ).getEntityRecord(
+
+ const { siteLogoId, canUserEdit, url, mediaItemData } = useSelect(
+ ( select ) => {
+ const { canUser, getEntityRecord, getEditedEntityRecord } = select(
+ coreStore
+ );
+ const siteSettings = getEditedEntityRecord( 'root', 'site' );
+ const siteData = getEntityRecord( 'root', '__unstableBase' );
+ const _siteLogo = siteSettings?.site_logo;
+ const _readOnlyLogo = siteData?.site_logo;
+ const _canUserEdit = canUser( 'update', 'settings' );
+ const _siteLogoId = _siteLogo || _readOnlyLogo;
+ const mediaItem =
+ _siteLogoId &&
+ select( coreStore ).getEntityRecord(
'root',
'media',
- siteSettings.site_logo
- )
- : null;
- return {
- mediaItemData: mediaItem && {
- url: mediaItem.source_url,
- alt: mediaItem.alt_text,
- },
- siteLogo: siteSettings.site_logo,
- url: siteSettings.url,
- };
- }, [] );
+ _siteLogoId,
+ { context: 'view' }
+ );
+ return {
+ siteLogoId: _siteLogoId,
+ canUserEdit: _canUserEdit,
+ url: siteData?.url,
+ mediaItemData: mediaItem && {
+ url: mediaItem.source_url,
+ alt: mediaItem.alt_text,
+ },
+ };
+ },
+ []
+ );
const { editEntityRecord } = useDispatch( coreStore );
const setLogo = ( newValue ) =>
@@ -290,7 +302,6 @@ export default function LogoEdit( {
setLogoUrl( mediaItemData.url );
}
}
-
const onSelectLogo = ( media ) => {
if ( ! media ) {
return;
@@ -311,7 +322,7 @@ export default function LogoEdit( {
setError( message[ 2 ] ? message[ 2 ] : null );
};
- const controls = logoUrl && (
+ const controls = canUserEdit && logoUrl && (
;
}
-
if ( !! logoUrl ) {
logoImage = (
);
}
-
- const mediaPlaceholder = (
- }
- labels={ {
- title: label,
- instructions: __(
- 'Upload an image, or pick one from your media library, to be your site logo'
- ),
- } }
- onSelect={ onSelectLogo }
- accept={ ACCEPT_MEDIA_STRING }
- allowedTypes={ ALLOWED_MEDIA_TYPES }
- mediaPreview={ logoImage }
- notices={
- error && (
-
- { error }
-
- )
- }
- onError={ onUploadError }
- />
- );
-
const classes = classnames( className, {
'is-default-size': ! width,
} );
-
const blockProps = useBlockProps( {
ref,
className: classes,
} );
-
return (
{ controls }
- { logoUrl && logoImage }
- { ! logoUrl && mediaPlaceholder }
+ { !! logoUrl && logoImage }
+ { ! logoUrl && ! canUserEdit && (
+
+
+
{ __( 'Site Logo' ) }
+
+ ) }
+ { ! logoUrl && canUserEdit && (
+
}
+ labels={ {
+ title: label,
+ instructions: __(
+ 'Upload an image, or pick one from your media library, to be your site logo'
+ ),
+ } }
+ onSelect={ onSelectLogo }
+ accept={ ACCEPT_MEDIA_STRING }
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
+ mediaPreview={ logoImage }
+ notices={
+ error && (
+
+ { error }
+
+ )
+ }
+ onError={ onUploadError }
+ />
+ ) }
);
}
diff --git a/packages/block-library/src/site-logo/editor.scss b/packages/block-library/src/site-logo/editor.scss
index 694cc98031283..de72e08f7cbff 100644
--- a/packages/block-library/src/site-logo/editor.scss
+++ b/packages/block-library/src/site-logo/editor.scss
@@ -80,3 +80,23 @@
}
}
}
+.editor-styles-wrapper {
+ .site-logo_placeholder {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ border-radius: $radius-block-ui;
+ background-color: $white;
+ box-shadow: inset 0 0 0 $border-width $gray-900;
+ padding: $grid-unit-15;
+ svg {
+ margin-right: $grid-unit-15;
+ }
+ p {
+ font-family: $default-font;
+ font-size: $default-font-size;
+ margin: 0;
+ line-height: initial;
+ }
+ }
+}
diff --git a/packages/core-data/src/queried-data/get-query-parts.js b/packages/core-data/src/queried-data/get-query-parts.js
index 9b54bd4c7cfa2..1cd9631495505 100644
--- a/packages/core-data/src/queried-data/get-query-parts.js
+++ b/packages/core-data/src/queried-data/get-query-parts.js
@@ -41,6 +41,7 @@ export function getQueryParts( query ) {
perPage: 10,
fields: null,
include: null,
+ context: 'default',
};
// Ensure stable key by sorting keys. Also more efficient for iterating.
@@ -65,6 +66,10 @@ export function getQueryParts( query ) {
);
break;
+ case 'context':
+ parts.context = value;
+ break;
+
default:
// While in theory, we could exclude "_fields" from the stableKey
// because two request with different fields have the same results
diff --git a/packages/core-data/src/queried-data/reducer.js b/packages/core-data/src/queried-data/reducer.js
index 9198b9f16fa12..12ad7d69fac5f 100644
--- a/packages/core-data/src/queried-data/reducer.js
+++ b/packages/core-data/src/queried-data/reducer.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { map, flowRight, omit, forEach, filter } from 'lodash';
+import { map, flowRight, omit, filter, mapValues } from 'lodash';
/**
* WordPress dependencies
@@ -20,6 +20,16 @@ import {
import { DEFAULT_ENTITY_KEY } from '../entities';
import getQueryParts from './get-query-parts';
+function getContextFromAction( action ) {
+ const { query } = action;
+ if ( ! query ) {
+ return 'default';
+ }
+
+ const queryParts = getQueryParts( query );
+ return queryParts.context;
+}
+
/**
* Returns a merged array of item IDs, given details of the received paginated
* items. The array is sparse-like with `undefined` entries where holes exist.
@@ -71,24 +81,30 @@ export function getMergedItemIds( itemIds, nextItemIds, page, perPage ) {
*
* @return {Object} Next state.
*/
-function items( state = {}, action ) {
+export function items( state = {}, action ) {
switch ( action.type ) {
- case 'RECEIVE_ITEMS':
+ case 'RECEIVE_ITEMS': {
+ const context = getContextFromAction( action );
const key = action.key || DEFAULT_ENTITY_KEY;
return {
...state,
- ...action.items.reduce( ( accumulator, value ) => {
- const itemId = value[ key ];
- accumulator[ itemId ] = conservativeMapItem(
- state[ itemId ],
- value
- );
- return accumulator;
- }, {} ),
+ [ context ]: {
+ ...state[ context ],
+ ...action.items.reduce( ( accumulator, value ) => {
+ const itemId = value[ key ];
+ accumulator[ itemId ] = conservativeMapItem(
+ state?.[ context ]?.[ itemId ],
+ value
+ );
+ return accumulator;
+ }, {} ),
+ },
};
+ }
case 'REMOVE_ITEMS':
- const newState = omit( state, action.itemIds );
- return newState;
+ return mapValues( state, ( contextState ) =>
+ omit( contextState, action.itemIds )
+ );
}
return state;
}
@@ -106,32 +122,45 @@ function items( state = {}, action ) {
* @return {Object} Next state.
*/
export function itemIsComplete( state = {}, action ) {
- const { type, query, key = DEFAULT_ENTITY_KEY } = action;
- if ( type !== 'RECEIVE_ITEMS' ) {
- return state;
+ switch ( action.type ) {
+ case 'RECEIVE_ITEMS': {
+ const context = getContextFromAction( action );
+ const { query, key = DEFAULT_ENTITY_KEY } = action;
+
+ // An item is considered complete if it is received without an associated
+ // fields query. Ideally, this would be implemented in such a way where the
+ // complete aggregate of all fields would satisfy completeness. Since the
+ // fields are not consistent across all entity types, this would require
+ // introspection on the REST schema for each entity to know which fields
+ // compose a complete item for that entity.
+ const queryParts = query ? getQueryParts( query ) : {};
+ const isCompleteQuery =
+ ! query || ! Array.isArray( queryParts.fields );
+
+ return {
+ ...state,
+ [ context ]: {
+ ...state[ context ],
+ ...action.items.reduce( ( result, item ) => {
+ const itemId = item[ key ];
+
+ // Defer to completeness if already assigned. Technically the
+ // data may be outdated if receiving items for a field subset.
+ result[ itemId ] =
+ state?.[ context ]?.[ itemId ] || isCompleteQuery;
+
+ return result;
+ }, {} ),
+ },
+ };
+ }
+ case 'REMOVE_ITEMS':
+ return mapValues( state, ( contextState ) =>
+ omit( contextState, action.itemIds )
+ );
}
- // An item is considered complete if it is received without an associated
- // fields query. Ideally, this would be implemented in such a way where the
- // complete aggregate of all fields would satisfy completeness. Since the
- // fields are not consistent across all entity types, this would require
- // introspection on the REST schema for each entity to know which fields
- // compose a complete item for that entity.
- const isCompleteQuery =
- ! query || ! Array.isArray( getQueryParts( query ).fields );
-
- return {
- ...state,
- ...action.items.reduce( ( result, item ) => {
- const itemId = item[ key ];
-
- // Defer to completeness if already assigned. Technically the
- // data may be outdated if receiving items for a field subset.
- result[ itemId ] = state[ itemId ] || isCompleteQuery;
-
- return result;
- }, {} ),
- };
+ return state;
}
/**
@@ -163,6 +192,8 @@ const receiveQueries = flowRight( [
return action;
} ),
+ onSubKey( 'context' ),
+
// Queries shape is shared, but keyed by query `stableKey` part. Original
// reducer tracks only a single query object.
onSubKey( 'stableKey' ),
@@ -194,17 +225,18 @@ const queries = ( state = {}, action ) => {
case 'RECEIVE_ITEMS':
return receiveQueries( state, action );
case 'REMOVE_ITEMS':
- const newState = { ...state };
const removedItems = action.itemIds.reduce( ( result, itemId ) => {
result[ itemId ] = true;
return result;
}, {} );
- forEach( newState, ( queryItems, key ) => {
- newState[ key ] = filter( queryItems, ( queryId ) => {
- return ! removedItems[ queryId ];
+
+ return mapValues( state, ( contextQueries ) => {
+ return mapValues( contextQueries, ( queryItems ) => {
+ return filter( queryItems, ( queryId ) => {
+ return ! removedItems[ queryId ];
+ } );
} );
} );
- return newState;
default:
return state;
}
diff --git a/packages/core-data/src/queried-data/selectors.js b/packages/core-data/src/queried-data/selectors.js
index 63468bc65491b..2e9ee2538160a 100644
--- a/packages/core-data/src/queried-data/selectors.js
+++ b/packages/core-data/src/queried-data/selectors.js
@@ -28,10 +28,16 @@ const queriedItemsCacheByState = new WeakMap();
* @return {?Array} Query items.
*/
function getQueriedItemsUncached( state, query ) {
- const { stableKey, page, perPage, include, fields } = getQueryParts(
- query
- );
+ const {
+ stableKey,
+ page,
+ perPage,
+ include,
+ fields,
+ context,
+ } = getQueryParts( query );
let itemIds;
+
if ( Array.isArray( include ) && ! stableKey ) {
// If the parsed query yields a set of IDs, but otherwise no filtering,
// it's safe to consider targeted item IDs as the include set. This
@@ -40,8 +46,8 @@ function getQueriedItemsUncached( state, query ) {
itemIds = include;
// TODO: Avoid storing the empty stable string in reducer, since it
// can be computed dynamically here always.
- } else if ( state.queries[ stableKey ] ) {
- itemIds = state.queries[ stableKey ];
+ } else if ( state.queries?.[ context ]?.[ stableKey ] ) {
+ itemIds = state.queries[ context ][ stableKey ];
}
if ( ! itemIds ) {
@@ -61,11 +67,11 @@ function getQueriedItemsUncached( state, query ) {
continue;
}
- if ( ! state.items.hasOwnProperty( itemId ) ) {
+ if ( ! state.items[ context ]?.hasOwnProperty( itemId ) ) {
return null;
}
- const item = state.items[ itemId ];
+ const item = state.items[ context ][ itemId ];
let filteredItem;
if ( Array.isArray( fields ) ) {
@@ -79,7 +85,7 @@ function getQueriedItemsUncached( state, query ) {
} else {
// If expecting a complete item, validate that completeness, or
// otherwise abort.
- if ( ! state.itemIsComplete[ itemId ] ) {
+ if ( ! state.itemIsComplete[ context ]?.[ itemId ] ) {
return null;
}
diff --git a/packages/core-data/src/queried-data/test/get-query-parts.js b/packages/core-data/src/queried-data/test/get-query-parts.js
index 156e18c995cc4..0de6036d696b3 100644
--- a/packages/core-data/src/queried-data/test/get-query-parts.js
+++ b/packages/core-data/src/queried-data/test/get-query-parts.js
@@ -8,6 +8,7 @@ describe( 'getQueryParts', () => {
const parts = getQueryParts( { page: 2, per_page: 2 } );
expect( parts ).toEqual( {
+ context: 'default',
page: 2,
perPage: 2,
stableKey: '',
@@ -20,6 +21,7 @@ describe( 'getQueryParts', () => {
const parts = getQueryParts( { include: [ 1 ] } );
expect( parts ).toEqual( {
+ context: 'default',
page: 1,
perPage: 10,
stableKey: '',
@@ -34,6 +36,7 @@ describe( 'getQueryParts', () => {
expect( first ).toEqual( second );
expect( first ).toEqual( {
+ context: 'default',
page: 1,
perPage: 10,
stableKey: '%3F=%26&b=2',
@@ -46,6 +49,7 @@ describe( 'getQueryParts', () => {
const parts = getQueryParts( { a: [ 1, 2 ] } );
expect( parts ).toEqual( {
+ context: 'default',
page: 1,
perPage: 10,
stableKey: 'a%5B0%5D=1&a%5B1%5D=2',
@@ -60,6 +64,7 @@ describe( 'getQueryParts', () => {
expect( first ).toEqual( second );
expect( first ).toEqual( {
+ context: 'default',
page: 1,
perPage: 10,
stableKey: 'b=2',
@@ -72,6 +77,7 @@ describe( 'getQueryParts', () => {
const parts = getQueryParts( { b: 2, page: 1, per_page: -1 } );
expect( parts ).toEqual( {
+ context: 'default',
page: 1,
perPage: -1,
stableKey: 'b=2',
@@ -84,6 +90,7 @@ describe( 'getQueryParts', () => {
const parts = getQueryParts( { _fields: [ 'id', 'title' ] } );
expect( parts ).toEqual( {
+ context: 'default',
page: 1,
perPage: 10,
stableKey: '_fields=id%2Ctitle',
@@ -91,4 +98,17 @@ describe( 'getQueryParts', () => {
include: null,
} );
} );
+
+ it( 'returns the context as a dedicated query part', () => {
+ const parts = getQueryParts( { context: 'view' } );
+
+ expect( parts ).toEqual( {
+ page: 1,
+ perPage: 10,
+ stableKey: '',
+ include: null,
+ fields: null,
+ context: 'view',
+ } );
+ } );
} );
diff --git a/packages/core-data/src/queried-data/test/reducer.js b/packages/core-data/src/queried-data/test/reducer.js
index 71f5a205e3995..63a4f4bd259ce 100644
--- a/packages/core-data/src/queried-data/test/reducer.js
+++ b/packages/core-data/src/queried-data/test/reducer.js
@@ -75,7 +75,7 @@ describe( 'itemIsComplete', () => {
} );
expect( state ).toEqual( {
- 1: true,
+ default: { 1: true },
} );
} );
@@ -85,12 +85,13 @@ describe( 'itemIsComplete', () => {
type: 'RECEIVE_ITEMS',
query: {
per_page: 5,
+ context: 'edit',
},
items: [ { id: 1, content: 'chicken', author: 'bob' } ],
} );
expect( state ).toEqual( {
- 1: true,
+ edit: { 1: true },
} );
} );
@@ -105,13 +106,13 @@ describe( 'itemIsComplete', () => {
} );
expect( state ).toEqual( {
- 1: false,
+ default: { 1: false },
} );
} );
it( 'should defer to existing completeness when receiving filtered query', () => {
const original = deepFreeze( {
- 1: true,
+ default: { 1: true },
} );
const state = itemIsComplete( original, {
type: 'RECEIVE_ITEMS',
@@ -122,7 +123,7 @@ describe( 'itemIsComplete', () => {
} );
expect( state ).toEqual( {
- 1: true,
+ default: { 1: true },
} );
} );
} );
@@ -133,16 +134,16 @@ describe( 'reducer', () => {
expect( state ).toEqual( {
items: {},
- itemIsComplete: {},
queries: {},
+ itemIsComplete: {},
} );
} );
it( 'receives a page of queried data', () => {
const original = deepFreeze( {
- items: {},
+ items: { default: {} },
queries: {},
- itemIsComplete: {},
+ itemIsComplete: { default: {} },
} );
const state = reducer( original, {
type: 'RECEIVE_ITEMS',
@@ -152,22 +153,22 @@ describe( 'reducer', () => {
expect( state ).toEqual( {
items: {
- 1: { id: 1, name: 'abc' },
+ default: { 1: { id: 1, name: 'abc' } },
},
itemIsComplete: {
- 1: true,
+ default: { 1: true },
},
queries: {
- 's=a': [ 1 ],
+ default: { 's=a': [ 1 ] },
},
} );
} );
it( 'receives an unqueried page of items', () => {
const original = deepFreeze( {
- items: {},
+ items: { default: {} },
queries: {},
- itemIsComplete: {},
+ itemIsComplete: { default: {} },
} );
const state = reducer( original, {
type: 'RECEIVE_ITEMS',
@@ -176,10 +177,10 @@ describe( 'reducer', () => {
expect( state ).toEqual( {
items: {
- 1: { id: 1, name: 'abc' },
+ default: { 1: { id: 1, name: 'abc' } },
},
itemIsComplete: {
- 1: true,
+ default: { 1: true },
},
queries: {},
} );
@@ -190,14 +191,18 @@ describe( 'reducer', () => {
const name = 'menu';
const original = deepFreeze( {
items: {
- 1: { id: 1, name: 'abc' },
- 2: { id: 2, name: 'def' },
- 3: { id: 3, name: 'ghi' },
- 4: { id: 4, name: 'klm' },
+ default: {
+ 1: { id: 1, name: 'abc' },
+ 2: { id: 2, name: 'def' },
+ 3: { id: 3, name: 'ghi' },
+ 4: { id: 4, name: 'klm' },
+ },
},
queries: {
- '': [ 1, 2, 3, 4 ],
- 's=a': [ 1, 3 ],
+ default: {
+ '': [ 1, 2, 3, 4 ],
+ 's=a': [ 1, 3 ],
+ },
},
} );
const state = reducer( original, removeItems( kind, name, 3 ) );
@@ -205,13 +210,17 @@ describe( 'reducer', () => {
expect( state ).toEqual( {
itemIsComplete: {},
items: {
- 1: { id: 1, name: 'abc' },
- 2: { id: 2, name: 'def' },
- 4: { id: 4, name: 'klm' },
+ default: {
+ 1: { id: 1, name: 'abc' },
+ 2: { id: 2, name: 'def' },
+ 4: { id: 4, name: 'klm' },
+ },
},
queries: {
- '': [ 1, 2, 4 ],
- 's=a': [ 1 ],
+ default: {
+ '': [ 1, 2, 4 ],
+ 's=a': [ 1 ],
+ },
},
} );
} );
diff --git a/packages/core-data/src/queried-data/test/selectors.js b/packages/core-data/src/queried-data/test/selectors.js
index 76dec520a7358..39b46a97a9395 100644
--- a/packages/core-data/src/queried-data/test/selectors.js
+++ b/packages/core-data/src/queried-data/test/selectors.js
@@ -19,15 +19,21 @@ describe( 'getQueriedItems', () => {
it( 'should return an array of items', () => {
const state = {
items: {
- 1: { id: 1 },
- 2: { id: 2 },
+ default: {
+ 1: { id: 1 },
+ 2: { id: 2 },
+ },
},
itemIsComplete: {
- 1: true,
- 2: true,
+ default: {
+ 1: true,
+ 2: true,
+ },
},
queries: {
- '': [ 1, 2 ],
+ default: {
+ '': [ 1, 2 ],
+ },
},
};
@@ -39,12 +45,16 @@ describe( 'getQueriedItems', () => {
it( 'should cache on query by state', () => {
const state = {
items: {
- 1: { id: 1 },
- 2: { id: 2 },
+ default: {
+ 1: { id: 1 },
+ 2: { id: 2 },
+ },
},
itemIsComplete: {
- 1: true,
- 2: true,
+ default: {
+ 1: true,
+ 2: true,
+ },
},
queries: [ 1, 2 ],
};
@@ -58,15 +68,21 @@ describe( 'getQueriedItems', () => {
it( 'should return items queried by include', () => {
const state = {
items: {
- 1: { id: 1 },
- 2: { id: 2 },
+ default: {
+ 1: { id: 1 },
+ 2: { id: 2 },
+ },
},
itemIsComplete: {
- 1: true,
- 2: true,
+ default: {
+ 1: true,
+ 2: true,
+ },
},
queries: {
- '': [ 1, 2 ],
+ default: {
+ '': [ 1, 2 ],
+ },
},
};
@@ -78,23 +94,29 @@ describe( 'getQueriedItems', () => {
it( 'should dynamically construct fields-filtered item from available data', () => {
const state = {
items: {
- 1: {
- id: 1,
- content: 'chicken',
- author: 'bob',
- },
- 2: {
- id: 2,
- content: 'ribs',
- author: 'sally',
+ default: {
+ 1: {
+ id: 1,
+ content: 'chicken',
+ author: 'bob',
+ },
+ 2: {
+ id: 2,
+ content: 'ribs',
+ author: 'sally',
+ },
},
},
itemIsComplete: {
- 1: true,
- 2: true,
+ default: {
+ 1: true,
+ 2: true,
+ },
},
queries: {
- '_fields=content': [ 1, 2 ],
+ default: {
+ '_fields=content': [ 1, 2 ],
+ },
},
};
@@ -109,31 +131,37 @@ describe( 'getQueriedItems', () => {
it( 'should dynamically construct fields-filtered item from available data with nested fields', () => {
const state = {
items: {
- 1: {
- id: 1,
- content: 'chicken',
- author: 'bob',
- meta: {
- template: 'single',
- _private: 'unused',
+ default: {
+ 1: {
+ id: 1,
+ content: 'chicken',
+ author: 'bob',
+ meta: {
+ template: 'single',
+ _private: 'unused',
+ },
},
- },
- 2: {
- id: 2,
- content: 'ribs',
- author: 'sally',
- meta: {
- template: 'single',
- _private: 'unused',
+ 2: {
+ id: 2,
+ content: 'ribs',
+ author: 'sally',
+ meta: {
+ template: 'single',
+ _private: 'unused',
+ },
},
},
},
itemIsComplete: {
- 1: true,
- 2: true,
+ default: {
+ 1: true,
+ 2: true,
+ },
},
queries: {
- '_fields=content%2Cmeta.template': [ 1, 2 ],
+ default: {
+ '_fields=content%2Cmeta.template': [ 1, 2 ],
+ },
},
};
@@ -150,21 +178,27 @@ describe( 'getQueriedItems', () => {
it( 'should return null if attempting to filter by yet-unknown fields', () => {
const state = {
items: {
- 1: {
- id: 1,
- author: 'bob',
- },
- 2: {
- id: 2,
- author: 'sally',
+ default: {
+ 1: {
+ id: 1,
+ author: 'bob',
+ },
+ 2: {
+ id: 2,
+ author: 'sally',
+ },
},
},
itemIsComplete: {
- 1: false,
- 2: false,
+ default: {
+ 1: false,
+ 2: false,
+ },
},
queries: {
- '': [ 1, 2 ],
+ default: {
+ '': [ 1, 2 ],
+ },
},
};
@@ -176,21 +210,27 @@ describe( 'getQueriedItems', () => {
it( 'should return null if querying non-filtered data for incomplete item', () => {
const state = {
items: {
- 1: {
- id: 1,
- author: 'bob',
- },
- 2: {
- id: 2,
- author: 'sally',
+ default: {
+ 1: {
+ id: 1,
+ author: 'bob',
+ },
+ 2: {
+ id: 2,
+ author: 'sally',
+ },
},
},
itemIsComplete: {
- 1: false,
- 2: false,
+ default: {
+ 1: false,
+ 2: false,
+ },
},
queries: {
- '': [ 1, 2 ],
+ default: {
+ '': [ 1, 2 ],
+ },
},
};
diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js
index 51c43a529d9ab..c60a9583c7ad9 100644
--- a/packages/core-data/src/reducer.js
+++ b/packages/core-data/src/reducer.js
@@ -197,6 +197,11 @@ function entity( entityConfig ) {
edits: ( state = {}, action ) => {
switch ( action.type ) {
case 'RECEIVE_ITEMS':
+ const context = action?.query?.context ?? 'default';
+ if ( context !== 'default' ) {
+ return state;
+ }
+
const nextState = { ...state };
for ( const record of action.items ) {
diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js
index 877f94e0a5ebc..8ed8d1474e864 100644
--- a/packages/core-data/src/resolvers.js
+++ b/packages/core-data/src/resolvers.js
@@ -193,8 +193,8 @@ export function* getEntityRecords( kind, name, query = {} ) {
}
const path = addQueryArgs( entity.baseURL, {
+ ...entity.baseURLParams,
...query,
- context: 'edit',
} );
let records = Object.values( yield apiFetch( { path } ) );
@@ -217,7 +217,7 @@ export function* getEntityRecords( kind, name, query = {} ) {
// When requesting all fields, the list of results can be used to
// resolve the `getEntityRecord` selector in addition to `getEntityRecords`.
// See https://github.com/WordPress/gutenberg/pull/26575
- if ( ! query?._fields ) {
+ if ( ! query?._fields && ! query.context ) {
const key = entity.key || DEFAULT_ENTITY_KEY;
const resolutionsArgs = records
.filter( ( record ) => record[ key ] )
diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js
index 4dd61d8526539..3208ea7988216 100644
--- a/packages/core-data/src/selectors.js
+++ b/packages/core-data/src/selectors.js
@@ -151,17 +151,18 @@ export function getEntityRecord( state, kind, name, key, query ) {
if ( ! queriedState ) {
return undefined;
}
+ const context = query?.context ?? 'default';
if ( query === undefined ) {
// If expecting a complete item, validate that completeness.
- if ( ! queriedState.itemIsComplete[ key ] ) {
+ if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) {
return undefined;
}
- return queriedState.items[ key ];
+ return queriedState.items[ context ][ key ];
}
- const item = queriedState.items[ key ];
+ const item = queriedState.items[ context ]?.[ key ];
if ( item && query._fields ) {
const filteredItem = {};
const fields = getNormalizedCommaSeparable( query._fields );
diff --git a/packages/core-data/src/test/reducer.js b/packages/core-data/src/test/reducer.js
index c8cd519e16bcd..e3665d619d668 100644
--- a/packages/core-data/src/test/reducer.js
+++ b/packages/core-data/src/test/reducer.js
@@ -67,12 +67,16 @@ describe( 'entities', () => {
expect( state.data.root.postType.queriedData ).toEqual( {
items: {
- b: { slug: 'b', title: 'beach' },
- s: { slug: 's', title: 'sun' },
+ default: {
+ b: { slug: 'b', title: 'beach' },
+ s: { slug: 's', title: 'sun' },
+ },
},
itemIsComplete: {
- b: true,
- s: true,
+ default: {
+ b: true,
+ s: true,
+ },
},
queries: {},
} );
@@ -85,10 +89,14 @@ describe( 'entities', () => {
postType: {
queriedData: {
items: {
- w: { slug: 'w', title: 'water' },
+ default: {
+ w: { slug: 'w', title: 'water' },
+ },
},
itemIsComplete: {
- w: true,
+ default: {
+ w: true,
+ },
},
queries: {},
},
@@ -105,12 +113,16 @@ describe( 'entities', () => {
expect( state.data.root.postType.queriedData ).toEqual( {
items: {
- w: { slug: 'w', title: 'water' },
- b: { slug: 'b', title: 'beach' },
+ default: {
+ w: { slug: 'w', title: 'water' },
+ b: { slug: 'b', title: 'beach' },
+ },
},
itemIsComplete: {
- w: true,
- b: true,
+ default: {
+ w: true,
+ b: true,
+ },
},
queries: {},
} );
diff --git a/packages/core-data/src/test/resolvers.js b/packages/core-data/src/test/resolvers.js
index e22958c387f83..0cb095dc5fb87 100644
--- a/packages/core-data/src/test/resolvers.js
+++ b/packages/core-data/src/test/resolvers.js
@@ -120,8 +120,18 @@ describe( 'getEntityRecords', () => {
page: { slug: 'page', id: 2 },
};
const ENTITIES = [
- { name: 'postType', kind: 'root', baseURL: '/wp/v2/types' },
- { name: 'postType', kind: 'root', baseURL: '/wp/v2/types' },
+ {
+ name: 'postType',
+ kind: 'root',
+ baseURL: '/wp/v2/types',
+ baseURLParams: { context: 'edit' },
+ },
+ {
+ name: 'postType',
+ kind: 'root',
+ baseURL: '/wp/v2/types',
+ baseURLParams: { context: 'edit' },
+ },
];
it( 'yields with requested post type', async () => {
diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js
index cce4293885f32..68878692351a0 100644
--- a/packages/core-data/src/test/selectors.js
+++ b/packages/core-data/src/test/selectors.js
@@ -74,10 +74,14 @@ describe.each( [
postType: {
queriedData: {
items: {
- post: { slug: 'post' },
+ default: {
+ post: { slug: 'post' },
+ },
},
itemIsComplete: {
- post: true,
+ default: {
+ post: true,
+ },
},
queries: {},
},
@@ -105,14 +109,18 @@ describe.each( [
post: {
queriedData: {
items: {
- 1: {
- id: 1,
- content: 'chicken',
- author: 'bob',
+ default: {
+ 1: {
+ id: 1,
+ content: 'chicken',
+ author: 'bob',
+ },
},
},
itemIsComplete: {
- 1: true,
+ default: {
+ 1: true,
+ },
},
queries: {},
},
@@ -168,15 +176,21 @@ describe( 'hasEntityRecords', () => {
postType: {
queriedData: {
items: {
- post: { slug: 'post' },
- page: { slug: 'page' },
+ default: {
+ post: { slug: 'post' },
+ page: { slug: 'page' },
+ },
},
itemIsComplete: {
- post: true,
- page: true,
+ default: {
+ post: true,
+ page: true,
+ },
},
queries: {
- '': [ 'post', 'page' ],
+ default: {
+ '': [ 'post', 'page' ],
+ },
},
},
},
@@ -227,15 +241,21 @@ describe( 'getEntityRecords', () => {
postType: {
queriedData: {
items: {
- post: { slug: 'post' },
- page: { slug: 'page' },
+ default: {
+ post: { slug: 'post' },
+ page: { slug: 'page' },
+ },
},
itemIsComplete: {
- post: true,
- page: true,
+ default: {
+ post: true,
+ page: true,
+ },
},
queries: {
- '': [ 'post', 'page' ],
+ default: {
+ '': [ 'post', 'page' ],
+ },
},
},
},
@@ -257,17 +277,23 @@ describe( 'getEntityRecords', () => {
post: {
queriedData: {
items: {
- 1: {
- id: 1,
- content: 'chicken',
- author: 'bob',
+ default: {
+ 1: {
+ id: 1,
+ content: 'chicken',
+ author: 'bob',
+ },
},
},
itemIsComplete: {
- 1: true,
+ default: {
+ 1: true,
+ },
},
queries: {
- '_fields=id%2Ccontent': [ 1 ],
+ default: {
+ '_fields=id%2Ccontent': [ 1 ],
+ },
},
},
},
@@ -335,16 +361,20 @@ describe( '__experimentalGetDirtyEntityRecords', () => {
someName: {
queriedData: {
items: {
- someKey: {
- someProperty: 'somePersistedValue',
- someRawProperty: {
- raw: 'somePersistedRawValue',
+ default: {
+ someKey: {
+ someProperty: 'somePersistedValue',
+ someRawProperty: {
+ raw: 'somePersistedRawValue',
+ },
+ id: 'someKey',
},
- id: 'someKey',
},
},
itemIsComplete: {
- someKey: true,
+ default: {
+ someKey: true,
+ },
},
},
edits: {
@@ -475,10 +505,17 @@ describe( 'canUserEditEntityRecord', () => {
postType: {
queriedData: {
items: {
- post: { slug: 'post', __unstable: 'posts' },
+ default: {
+ post: {
+ slug: 'post',
+ __unstable: 'posts',
+ },
+ },
},
itemIsComplete: {
- post: true,
+ default: {
+ post: true,
+ },
},
queries: {},
},
diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js
index a7581187b7116..259d376c190e1 100644
--- a/packages/edit-post/src/components/header/header-toolbar/index.js
+++ b/packages/edit-post/src/components/header/header-toolbar/index.js
@@ -23,7 +23,6 @@ import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
/**
* Internal dependencies
*/
-import TemplateTitle from '../template-title';
import { store as editPostStore } from '../../../store';
function HeaderToolbar() {
@@ -156,8 +155,6 @@ function HeaderToolbar() {
>
) }
-
-
);
}
diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js
index c24e41ec59b41..40e5fa91239e0 100644
--- a/packages/edit-post/src/components/header/index.js
+++ b/packages/edit-post/src/components/header/index.js
@@ -21,6 +21,7 @@ import PostPublishButtonOrToggle from './post-publish-button-or-toggle';
import { default as DevicePreview } from '../device-preview';
import MainDashboardButton from './main-dashboard-button';
import { store as editPostStore } from '../../store';
+import TemplateTitle from './template-title';
function Header( { setEntitiesSavedStatesCallback } ) {
const {
@@ -59,6 +60,7 @@ function Header( { setEntitiesSavedStatesCallback } ) {
+
{ ! isPublishSidebarOpened && (
diff --git a/packages/edit-post/src/components/header/template-title/index.js b/packages/edit-post/src/components/header/template-title/index.js
index a2cd8b0ca07f7..21431ee1e319c 100644
--- a/packages/edit-post/src/components/header/template-title/index.js
+++ b/packages/edit-post/src/components/header/template-title/index.js
@@ -3,7 +3,7 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
-import { Dropdown, ToolbarItem, Button } from '@wordpress/components';
+import { Dropdown, Button } from '@wordpress/components';
import { chevronDown } from '@wordpress/icons';
/**
@@ -47,59 +47,50 @@ function TemplateTitle() {
}
return (
-
- { ( toolbarItemHTMLProps ) => {
- return (
- (
- <>
- {
- clearSelectedBlock();
- setIsEditingTemplate( false );
- } }
- >
- { title }
-
-
- { templateTitle }
-
- >
+
+ {
+ clearSelectedBlock();
+ setIsEditingTemplate( false );
+ } }
+ >
+ { title }
+
+ (
+
+ { templateTitle }
+
+ ) }
+ renderContent={ () => (
+ <>
+ { template.has_theme_file ? (
+
+ ) : (
+
) }
- renderContent={ () => (
- <>
- { template.has_theme_file ? (
-
- ) : (
-
- ) }
-
- >
- ) }
- />
- );
- } }
-
+
+ >
+ ) }
+ />
+
);
}
diff --git a/packages/edit-widgets/src/blocks/widget-area/edit/index.js b/packages/edit-widgets/src/blocks/widget-area/edit/index.js
index 9f2388265b0b2..1957361586e5a 100644
--- a/packages/edit-widgets/src/blocks/widget-area/edit/index.js
+++ b/packages/edit-widgets/src/blocks/widget-area/edit/index.js
@@ -15,6 +15,7 @@ import {
*/
import WidgetAreaInnerBlocks from './inner-blocks';
import { store as editWidgetsStore } from '../../../store';
+import useIsDraggingWithin from './use-is-dragging-within';
/** @typedef {import('@wordpress/element').RefObject} RefObject */
@@ -68,16 +69,17 @@ export default function WidgetAreaEdit( {
// unmounted when the panel is collapsed. Unmounting legacy
// widgets may have unintended consequences (e.g. TinyMCE
// not being properly reinitialized)
-
-
-
-
-
-
+
+
+
+
) }
@@ -117,51 +119,3 @@ const useIsDragging = ( elementRef ) => {
return isDragging;
};
-
-/**
- * A React hook to determine if it's dragging within the target element.
- *
- * @param {RefObject} elementRef The target elementRef object.
- *
- * @return {boolean} Is dragging within the target element.
- */
-const useIsDraggingWithin = ( elementRef ) => {
- const [ isDraggingWithin, setIsDraggingWithin ] = useState( false );
-
- useEffect( () => {
- const { ownerDocument } = elementRef.current;
-
- function handleDragStart( event ) {
- // Check the first time when the dragging starts.
- handleDragEnter( event );
- }
-
- // Set to false whenever the user cancel the drag event by either releasing the mouse or press Escape.
- function handleDragEnd() {
- setIsDraggingWithin( false );
- }
-
- function handleDragEnter( event ) {
- // Check if the current target is inside the item element.
- if ( elementRef.current.contains( event.target ) ) {
- setIsDraggingWithin( true );
- } else {
- setIsDraggingWithin( false );
- }
- }
-
- // Bind these events to the document to catch all drag events.
- // Ideally, we can also use `event.relatedTarget`, but sadly that doesn't work in Safari.
- ownerDocument.addEventListener( 'dragstart', handleDragStart );
- ownerDocument.addEventListener( 'dragend', handleDragEnd );
- ownerDocument.addEventListener( 'dragenter', handleDragEnter );
-
- return () => {
- ownerDocument.removeEventListener( 'dragstart', handleDragStart );
- ownerDocument.removeEventListener( 'dragend', handleDragEnd );
- ownerDocument.removeEventListener( 'dragenter', handleDragEnter );
- };
- }, [] );
-
- return isDraggingWithin;
-};
diff --git a/packages/edit-widgets/src/blocks/widget-area/edit/inner-blocks.js b/packages/edit-widgets/src/blocks/widget-area/edit/inner-blocks.js
index 15ca45640a4c1..38d17428fac5f 100644
--- a/packages/edit-widgets/src/blocks/widget-area/edit/inner-blocks.js
+++ b/packages/edit-widgets/src/blocks/widget-area/edit/inner-blocks.js
@@ -1,21 +1,53 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
/**
* WordPress dependencies
*/
import { useEntityBlockEditor } from '@wordpress/core-data';
-import { InnerBlocks } from '@wordpress/block-editor';
+import {
+ InnerBlocks,
+ __experimentalUseInnerBlocksProps,
+} from '@wordpress/block-editor';
+import { useRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import useIsDraggingWithin from './use-is-dragging-within';
export default function WidgetAreaInnerBlocks() {
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
'root',
'postType'
);
+ const innerBlocksRef = useRef();
+ const isDraggingWithinInnerBlocks = useIsDraggingWithin( innerBlocksRef );
+ const shouldHighlightDropZone = isDraggingWithinInnerBlocks;
+ // Using the experimental hook so that we can control the className of the element.
+ const innerBlocksProps = __experimentalUseInnerBlocksProps(
+ { ref: innerBlocksRef },
+ {
+ value: blocks,
+ onInput,
+ onChange,
+ templateLock: false,
+ renderAppender: InnerBlocks.ButtonBlockAppender,
+ }
+ );
+
return (
-
+
);
}
diff --git a/packages/edit-widgets/src/blocks/widget-area/edit/use-is-dragging-within.js b/packages/edit-widgets/src/blocks/widget-area/edit/use-is-dragging-within.js
new file mode 100644
index 0000000000000..4588c7bda34a9
--- /dev/null
+++ b/packages/edit-widgets/src/blocks/widget-area/edit/use-is-dragging-within.js
@@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect } from '@wordpress/element';
+
+/** @typedef {import('@wordpress/element').RefObject} RefObject */
+
+/**
+ * A React hook to determine if it's dragging within the target element.
+ *
+ * @param {RefObject} elementRef The target elementRef object.
+ *
+ * @return {boolean} Is dragging within the target element.
+ */
+const useIsDraggingWithin = ( elementRef ) => {
+ const [ isDraggingWithin, setIsDraggingWithin ] = useState( false );
+
+ useEffect( () => {
+ const { ownerDocument } = elementRef.current;
+
+ function handleDragStart( event ) {
+ // Check the first time when the dragging starts.
+ handleDragEnter( event );
+ }
+
+ // Set to false whenever the user cancel the drag event by either releasing the mouse or press Escape.
+ function handleDragEnd() {
+ setIsDraggingWithin( false );
+ }
+
+ function handleDragEnter( event ) {
+ // Check if the current target is inside the item element.
+ if ( elementRef.current.contains( event.target ) ) {
+ setIsDraggingWithin( true );
+ } else {
+ setIsDraggingWithin( false );
+ }
+ }
+
+ // Bind these events to the document to catch all drag events.
+ // Ideally, we can also use `event.relatedTarget`, but sadly that doesn't work in Safari.
+ ownerDocument.addEventListener( 'dragstart', handleDragStart );
+ ownerDocument.addEventListener( 'dragend', handleDragEnd );
+ ownerDocument.addEventListener( 'dragenter', handleDragEnter );
+
+ return () => {
+ ownerDocument.removeEventListener( 'dragstart', handleDragStart );
+ ownerDocument.removeEventListener( 'dragend', handleDragEnd );
+ ownerDocument.removeEventListener( 'dragenter', handleDragEnter );
+ };
+ }, [] );
+
+ return isDraggingWithin;
+};
+
+export default useIsDraggingWithin;
diff --git a/packages/edit-widgets/src/blocks/widget-area/editor.scss b/packages/edit-widgets/src/blocks/widget-area/editor.scss
index e1c1fcbc69bc0..94c637fd08d6b 100644
--- a/packages/edit-widgets/src/blocks/widget-area/editor.scss
+++ b/packages/edit-widgets/src/blocks/widget-area/editor.scss
@@ -1,3 +1,5 @@
+$panel-title-height: 48px;
+
.wp-block[data-type="core/widget-area"] {
max-width: $widget-area-width;
margin-left: auto;
@@ -5,37 +7,66 @@
.components-panel__body > .components-panel__body-title {
font-family: $default-font;
- margin-bottom: 0;
+ margin: 0;
+ height: $panel-title-height;
+ // Create a stacking context and make sure it's higher is than the content.
+ // Since the inner blocks will be stretched to cover the whole panel,
+ // we still want the title to be interactive.
+ position: relative;
+ z-index: 1;
+ background: $white;
+ // z-index should be enough to create a new stacking context,
+ // unfortunately, Safari needs this to stop it from flickering while dragging.
+ // The reason behind that is still unknown, probably a bug in the browser.
+ transform: translateZ(0);
// Remove default hover background in panel title. See #25752.
&:hover {
- background: inherit;
+ background: $white;
}
}
- .components-panel__body .editor-styles-wrapper {
- margin: 0 (-$grid-unit-20) (-$grid-unit-20) (-$grid-unit-20);
- padding: $grid-unit-20;
- }
-
.block-list-appender.wp-block {
width: initial;
}
+
// Override theme custom widths for blocks.
.editor-styles-wrapper .wp-block.wp-block.wp-block.wp-block.wp-block {
max-width: 100%;
}
+
+ .components-panel__body.is-opened {
+ padding: 0;
+ }
}
-// Add some spacing above the inner blocks so that the block toolbar doesn't
-// overlap the panel header.
-.wp-block-widget-area > .components-panel__body > div > .editor-styles-wrapper > .block-editor-inner-blocks {
- padding-top: $grid-unit-30;
+.blocks-widgets-container .wp-block-widget-area__inner-blocks.editor-styles-wrapper {
+ margin: 0;
+ padding: 0;
- // Ensure the widget area block lists have a minimum height so that it doesn't
- // collapse to zero height when it has no blocks. When that happens the block
- // can't be used as a drop target.
> .block-editor-block-list__layout {
+ // Stretch the inner-blocks container to cover the entire panel,
+ // so that dragging onto anywhere in it works.
+ margin-top: -$panel-title-height;
+ // Add some spacing above the inner blocks so that the block toolbar doesn't
+ // overlap the panel header.
+ padding: ($panel-title-height + $grid-unit-30) $grid-unit-20 $grid-unit-20;
+
+ // Ensure the widget area block lists have a minimum height so that it doesn't
+ // collapse to zero height when it has no blocks. When that happens the block
+ // can't be used as a drop target.
min-height: $grid-unit-40;
}
}
+
+.wp-block-widget-area__highlight-drop-zone {
+ outline: var(--wp-admin-border-width-focus) solid var(--wp-admin-theme-color);
+}
+
+// Prevent "dragenter" event from firing when dragging onto the title component.
+body.is-dragging-components-draggable .wp-block[data-type="core/widget-area"] .components-panel__body > .components-panel__body-title {
+ &,
+ * {
+ pointer-events: none;
+ }
+}
diff --git a/packages/edit-widgets/src/store/actions.js b/packages/edit-widgets/src/store/actions.js
index 1ba1b03d8abf9..8b9c5df3676cd 100644
--- a/packages/edit-widgets/src/store/actions.js
+++ b/packages/edit-widgets/src/store/actions.js
@@ -108,10 +108,15 @@ export function* saveWidgetArea( widgetAreaId ) {
( { sidebar } ) => sidebar === widgetAreaId
);
- // Remove all duplicate reference widget instances
+ // Remove all duplicate reference widget instances for legacy widgets.
+ // Why? We filter out the widgets with duplicate IDs to prevent adding more than one instance of a widget
+ // implemented using a function. WordPress doesn't support having more than one instance of these, if you try to
+ // save multiple instances of these in different sidebars you will run into undefined behaviors.
const usedReferenceWidgets = [];
- const widgetsBlocks = post.blocks.filter( ( { attributes: { id } } ) => {
- if ( id ) {
+ const widgetsBlocks = post.blocks.filter( ( block ) => {
+ const { id } = block.attributes;
+
+ if ( block.name === 'core/legacy-widget' && id ) {
if ( usedReferenceWidgets.includes( id ) ) {
return false;
}
diff --git a/packages/widgets/src/blocks/legacy-widget/editor.scss b/packages/widgets/src/blocks/legacy-widget/editor.scss
index 8ad7a3faa5c33..72c15ff32a6b1 100644
--- a/packages/widgets/src/blocks/legacy-widget/editor.scss
+++ b/packages/widgets/src/blocks/legacy-widget/editor.scss
@@ -1,4 +1,5 @@
.wp-block-legacy-widget__edit-form {
+ display: flow-root;
background: $white;
border-radius: $radius-block-ui;
border: 1px solid $gray-900;
@@ -22,36 +23,32 @@
font-size: $default-font-size;
}
- label + .widefat {
- margin-top: $grid-unit-15;
- }
-
// Override theme style bleed.
label,
input,
a {
+ font-family: system-ui;
+ font-weight: normal;
color: $black;
}
+ input[type="text"],
select {
font-family: system-ui;
- -webkit-appearance: revert;
- color: revert;
- border: revert;
- border-radius: revert;
- background: revert;
- box-shadow: revert;
- text-shadow: revert;
- outline: revert;
- cursor: revert;
- transform: revert;
- font-size: revert;
- line-height: revert;
- padding: revert;
- margin: revert;
- min-height: revert;
- max-width: revert;
- vertical-align: revert;
- font-weight: revert;
+ background: transparent;
+ box-sizing: border-box;
+ border: 1px solid $gray-700;
+ border-radius: 3px;
+ box-shadow: none;
+ color: $black;
+ display: block;
+ margin: 0;
+ width: 100%;
+ font-size: $default-font-size;
+ font-weight: normal;
+ height: 30px;
+ line-height: 1;
+ min-height: 30px;
+ padding-left: $grid-unit-05;
}
}
@@ -130,6 +127,11 @@
}
}
+// Wide widgets opening in popovers in the Customizer should have a min-width
+.components-popover__content .wp-block-legacy-widget__edit-form {
+ min-width: 400px;
+}
+
.wp-block-legacy-widget {
.components-select-control__input {
padding: 0;