diff --git a/docs/reference-guides/data/data-core-blocks.md b/docs/reference-guides/data/data-core-blocks.md index a25a521931e25a..084c9c1d7a5fbc 100644 --- a/docs/reference-guides/data/data-core-blocks.md +++ b/docs/reference-guides/data/data-core-blocks.md @@ -504,54 +504,6 @@ _Returns_ - `string?`: Name of the block for handling the grouping of blocks. -### getHookedBlocks - -Returns the hooked blocks for a given anchor block. - -Given an anchor block name, returns an object whose keys are relative positions, and whose values are arrays of block names that are hooked to the anchor block at that relative position. - -_Usage_ - -```js -import { store as blocksStore } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; - -const ExampleComponent = () => { - const hookedBlockNames = useSelect( - ( select ) => - select( blocksStore ).getHookedBlocks( 'core/navigation' ), - [] - ); - - return ( - - ); -}; -``` - -_Parameters_ - -- _state_ `Object`: Data state. -- _blockName_ `string`: Anchor block type name. - -_Returns_ - -- `Object`: Lists of hooked block names for each relative position. - ### getUnregisteredFallbackBlockName Returns the name of the block for handling unregistered blocks. diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php index 3bfade3aa4aa75..1fc757accf6f6b 100644 --- a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php +++ b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php @@ -800,14 +800,14 @@ public function print_router_loading_and_screen_reader_markup() { echo <<
HTML; diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index e6d8d12769e701..7152794975f420 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -191,6 +191,8 @@ $z-layers: ( ".edit-site-page-content": 1, ".edit-site-patterns__header": 2, ".edit-site-patterns__grid-pagination": 2, + ".edit-site-patterns__dataviews-list-pagination": 2, + ".edit-site-templates__dataviews-list-pagination": 2, ".edit-site-layout__canvas-container": 2, ".edit-site-layout__sidebar": 1, ".edit-site-layout__canvas-container.is-resizing::after": 100, diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index 19ad39caca336a..bd4c8ea5fb371c 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -81,11 +81,16 @@ function InbetweenInsertionPointPopover( { isInserterShown: insertionPoint?.__unstableWithInserter, }; }, [] ); + const { getBlockEditingMode } = useSelect( blockEditorStore ); const disableMotion = useReducedMotion(); function onClick( event ) { - if ( event.target === ref.current && nextClientId ) { + if ( + event.target === ref.current && + nextClientId && + getBlockEditingMode( nextClientId ) !== 'disabled' + ) { selectBlock( nextClientId, -1 ); } } diff --git a/packages/block-editor/src/components/global-styles/advanced-panel.js b/packages/block-editor/src/components/global-styles/advanced-panel.js index af43552c0a3eba..1ad59451d1468a 100644 --- a/packages/block-editor/src/components/global-styles/advanced-panel.js +++ b/packages/block-editor/src/components/global-styles/advanced-panel.js @@ -3,12 +3,11 @@ */ import { TextareaControl, - Tooltip, + Notice, __experimentalVStack as VStack, } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Icon, info } from '@wordpress/icons'; /** * Internal dependencies @@ -58,6 +57,11 @@ export default function AdvancedPanel( { return ( + { cssError && ( + setCSSError( null ) }> + { cssError } + + ) } - { cssError && ( - -
- -
-
- ) }
); } diff --git a/packages/block-editor/src/components/global-styles/shadow-panel-components.js b/packages/block-editor/src/components/global-styles/shadow-panel-components.js index 6e4e3a15b184d8..8c9ba795bc17ba 100644 --- a/packages/block-editor/src/components/global-styles/shadow-panel-components.js +++ b/packages/block-editor/src/components/global-styles/shadow-panel-components.js @@ -19,13 +19,13 @@ import { shadow as shadowIcon, Icon, check } from '@wordpress/icons'; import classNames from 'classnames'; export function ShadowPopoverContainer( { shadow, onShadowChange, settings } ) { - const defaultShadows = settings?.shadow?.presets?.default; - const themeShadows = settings?.shadow?.presets?.theme; + const defaultShadows = settings?.shadow?.presets?.default || []; + const themeShadows = settings?.shadow?.presets?.theme || []; const defaultPresetsEnabled = settings?.shadow?.defaultPresets; const shadows = [ ...( defaultPresetsEnabled ? defaultShadows : [] ), - ...( themeShadows || [] ), + ...themeShadows, ]; return ( diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index 010c5faaefff44..d2ba88f9f31e00 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -47,13 +47,3 @@ /*rtl:ignore*/ direction: ltr; } - -.block-editor-global-styles-advanced-panel__custom-css-validation-wrapper { - position: absolute; - bottom: $grid-unit-20; - right: $grid-unit * 3; -} - -.block-editor-global-styles-advanced-panel__custom-css-validation-icon { - fill: $alert-red; -} diff --git a/packages/block-editor/src/components/inserter/media-tab/media-preview.js b/packages/block-editor/src/components/inserter/media-tab/media-preview.js index 9efed229f0adf2..d3b221910eae22 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -195,7 +195,12 @@ export function MediaPreview( { media, onClick, category } ) { createSuccessNotice, ] ); - const title = media.title?.rendered || media.title; + + const title = + typeof media.title === 'string' + ? media.title + : media.title?.rendered || __( 'no title' ); + let truncatedTitle; if ( title.length > MAXIMUM_TITLE_LENGTH ) { const omission = '...'; diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index f45cc618d4547e..bac2559ffbba9b 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -156,12 +156,19 @@ export function RichTextWrapper( for ( const [ attribute, args ] of Object.entries( blockBindings ) ) { - // If any of the attributes with source "rich-text" is part of the bindings, - // has a source with `lockAttributesEditing`, disable it. if ( - blockTypeAttributes?.[ attribute ]?.source === - 'rich-text' && - getBlockBindingsSource( args.source )?.lockAttributesEditing + blockTypeAttributes?.[ attribute ]?.source !== 'rich-text' + ) { + break; + } + + // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. + const blockBindingsSource = getBlockBindingsSource( + args.source + ); + if ( + ! blockBindingsSource || + blockBindingsSource.lockAttributesEditing ) { shouldDisableEditing = true; break; diff --git a/packages/block-editor/src/components/rich-text/use-paste-handler.js b/packages/block-editor/src/components/rich-text/use-paste-handler.js index 1302e2d0dce469..fc75caa43615f5 100644 --- a/packages/block-editor/src/components/rich-text/use-paste-handler.js +++ b/packages/block-editor/src/components/rich-text/use-paste-handler.js @@ -58,13 +58,35 @@ export function usePasteHandler( props ) { const isInternal = event.clipboardData.getData( 'rich-text' ) === 'true'; + function pasteInline( content ) { + const transformed = formatTypes.reduce( + ( accumulator, { __unstablePasteRule } ) => { + // Only allow one transform. + if ( __unstablePasteRule && accumulator === value ) { + accumulator = __unstablePasteRule( value, { + html, + plainText, + } ); + } + + return accumulator; + }, + value + ); + if ( transformed !== value ) { + onChange( transformed ); + } else { + const valueToInsert = create( { html: content } ); + addActiveFormats( valueToInsert, value.activeFormats ); + onChange( insert( value, valueToInsert ) ); + } + } + // If the data comes from a rich text instance, we can directly use it // without filtering the data. The filters are only meant for externally // pasted content and remove inline styles. if ( isInternal ) { - const pastedValue = create( { html } ); - addActiveFormats( pastedValue, value.activeFormats ); - onChange( insert( value, pastedValue ) ); + pasteInline( html ); return; } @@ -135,28 +157,7 @@ export function usePasteHandler( props ) { } ); if ( typeof content === 'string' ) { - const transformed = formatTypes.reduce( - ( accumlator, { __unstablePasteRule } ) => { - // Only allow one transform. - if ( __unstablePasteRule && accumlator === value ) { - accumlator = __unstablePasteRule( value, { - html, - plainText, - } ); - } - - return accumlator; - }, - value - ); - - if ( transformed !== value ) { - onChange( transformed ); - } else { - const valueToInsert = create( { html: content } ); - addActiveFormats( valueToInsert, value.activeFormats ); - onChange( insert( value, valueToInsert ) ); - } + pasteInline( content ); } else if ( content.length > 0 ) { if ( onReplace && isEmpty( value ) ) { onReplace( content, content.length - 1, -1 ); diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 80b6090eca2190..1564d9ea9a81ca 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -374,11 +374,14 @@ function backgroundSizeHelpText( value ) { } export const coordsToBackgroundPosition = ( value ) => { - if ( ! value || isNaN( value.x ) || isNaN( value.y ) ) { + if ( ! value || ( isNaN( value.x ) && isNaN( value.y ) ) ) { return undefined; } - return `${ value.x * 100 }% ${ value.y * 100 }%`; + const x = isNaN( value.x ) ? 0.5 : value.x; + const y = isNaN( value.y ) ? 0.5 : value.y; + + return `${ x * 100 }% ${ y * 100 }%`; }; export const backgroundPositionToCoords = ( value ) => { diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 0ba05e88a0f9b1..220aa5f4127270 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -26,6 +26,7 @@ import { usesContextKey } from './components/rich-text/format-edit'; import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; import { useFlashEditableBlocks } from './components/use-flash-editable-blocks'; +import { selectBlockPatternsKey } from './store/private-keys'; /** * Private @wordpress/block-editor APIs. @@ -56,4 +57,5 @@ lock( privateApis, { useReusableBlocksRenameHint, usesContextKey, useFlashEditableBlocks, + selectBlockPatternsKey, } ); diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 10e16a0779cd63..0bcc00cb5f6ae8 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -10,7 +10,6 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as privateActions from './private-actions'; import * as privateSelectors from './private-selectors'; -import * as resolvers from './resolvers'; import * as actions from './actions'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; @@ -23,7 +22,6 @@ import { unlock } from '../lock-unlock'; export const storeConfig = { reducer, selectors, - resolvers, actions, }; diff --git a/packages/block-editor/src/store/private-keys.js b/packages/block-editor/src/store/private-keys.js new file mode 100644 index 00000000000000..8bfa4bb68297f8 --- /dev/null +++ b/packages/block-editor/src/store/private-keys.js @@ -0,0 +1 @@ +export const selectBlockPatternsKey = Symbol( 'selectBlockPatternsKey' ); diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 4700e50f739f45..c885e43ba75208 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -22,6 +22,7 @@ import { checkAllowListRecursive, getAllPatternsDependants } from './utils'; import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; +import { selectBlockPatternsKey } from './private-keys'; export { getBlockSettings } from './get-block-settings'; @@ -250,10 +251,6 @@ export const getInserterMediaCategories = createSelector( ] ); -export function getFetchedPatterns( state ) { - return state.blockPatterns; -} - /** * Returns whether there is at least one allowed pattern for inner blocks children. * This is useful for deferring the parsing of all patterns until needed. @@ -285,7 +282,7 @@ export const hasAllowedPatterns = createRegistrySelector( ( select ) => } ); }, ( state, rootClientId ) => [ - getAllPatternsDependants( state ), + getAllPatternsDependants( select )( state ), state.settings.allowedBlockTypes, state.settings.templateLock, state.blockListSettings[ rootClientId ], @@ -325,12 +322,12 @@ export const getAllPatterns = createRegistrySelector( ( select ) => return [ ...userPatterns, ...__experimentalBlockPatterns, - ...unlock( select( STORE_NAME ) ).getFetchedPatterns(), + ...( state.settings[ selectBlockPatternsKey ]?.( select ) ?? [] ), ].filter( ( x, index, arr ) => index === arr.findIndex( ( y ) => x.name === y.name ) ); - }, getAllPatternsDependants ) + }, getAllPatternsDependants( select ) ) ); /** diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 0be421b757bce1..1b535e51950164 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2064,15 +2064,6 @@ function blockBindingsSources( state = {}, action ) { return state; } -function blockPatterns( state = [], action ) { - switch ( action.type ) { - case 'RECEIVE_BLOCK_PATTERNS': - return action.patterns; - } - - return state; -} - const combinedReducers = combineReducers( { blocks, isDragging, @@ -2105,7 +2096,6 @@ const combinedReducers = combineReducers( { openedBlockSettingsMenu, registeredInserterMediaCategories, blockBindingsSources, - blockPatterns, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-editor/src/store/resolvers.js b/packages/block-editor/src/store/resolvers.js deleted file mode 100644 index 40c51d241ac676..00000000000000 --- a/packages/block-editor/src/store/resolvers.js +++ /dev/null @@ -1,17 +0,0 @@ -export const getFetchedPatterns = - () => - async ( { dispatch, select } ) => { - const { __experimentalFetchBlockPatterns } = select.getSettings(); - if ( ! __experimentalFetchBlockPatterns ) { - return []; - } - const patterns = await __experimentalFetchBlockPatterns(); - dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } ); - }; - -getFetchedPatterns.shouldInvalidate = ( action ) => { - return ( - action.type === 'UPDATE_SETTINGS' && - !! action.settings.__experimentalFetchBlockPatterns - ); -}; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 373611cd3bd8e8..87d22e3e4727d7 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -10,7 +10,6 @@ import { getBlockType, getBlockTypes, getBlockVariations, - getHookedBlocks, hasBlockSupport, getPossibleBlockTransformations, parse, @@ -1937,16 +1936,9 @@ const buildBlockTypeItem = blockType.name, 'inserter' ); - - const ignoredHookedBlocks = [ - ...new Set( Object.values( getHookedBlocks( id ) ).flat() ), - ]; - return { ...blockItemBase, - initialAttributes: ignoredHookedBlocks.length - ? { metadata: { ignoredHookedBlocks } } - : {}, + initialAttributes: {}, description: blockType.description, category: blockType.category, keywords: blockType.keywords, @@ -2307,12 +2299,12 @@ export const __experimentalGetParsedPattern = createRegistrySelector( __unstableSkipMigrationLogs: true, } ), }; - }, getAllPatternsDependants ) + }, getAllPatternsDependants( select ) ) ); -const getAllowedPatternsDependants = ( state, rootClientId ) => { +const getAllowedPatternsDependants = ( select ) => ( state, rootClientId ) => { return [ - ...getAllPatternsDependants( state ), + ...getAllPatternsDependants( select )( state ), state.settings.allowedBlockTypes, state.settings.templateLock, state.blockListSettings[ rootClientId ], @@ -2353,7 +2345,7 @@ export const __experimentalGetAllowedPatterns = createRegistrySelector( ); return patternsAllowed; - }, getAllowedPatternsDependants ); + }, getAllowedPatternsDependants( select ) ); } ); @@ -2392,7 +2384,7 @@ export const getPatternsByBlockTypes = createRegistrySelector( ( select ) => return filteredPatterns; }, ( state, blockNames, rootClientId ) => - getAllowedPatternsDependants( state, rootClientId ) + getAllowedPatternsDependants( select )( state, rootClientId ) ) ); @@ -2466,7 +2458,7 @@ export const __experimentalGetPatternTransformItems = createRegistrySelector( ); }, ( state, blocks, rootClientId ) => - getAllowedPatternsDependants( state, rootClientId ) + getAllowedPatternsDependants( select )( state, rootClientId ) ) ); diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 6cde56da1b55a7..4d9d114946c1fd 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -1,3 +1,8 @@ +/** + * Internal dependencies + */ +import { selectBlockPatternsKey } from './private-keys'; + export const checkAllowList = ( list, item, defaultResult = null ) => { if ( typeof list === 'boolean' ) { return list; @@ -40,12 +45,12 @@ export const checkAllowListRecursive = ( blocks, allowedBlockTypes ) => { return true; }; -export const getAllPatternsDependants = ( state ) => { +export const getAllPatternsDependants = ( select ) => ( state ) => { return [ state.settings.__experimentalBlockPatterns, state.settings.__experimentalUserPatternCategories, state.settings.__experimentalReusableBlocks, - state.settings.__experimentalFetchBlockPatterns, + state.settings[ selectBlockPatternsKey ]?.( select ), state.blockPatterns, ]; }; diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index e01898ca00dec4..ff90cdd1bf64c0 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -238,15 +238,15 @@ function ButtonEdit( props ) { return {}; } - const { getBlockBindingsSource } = unlock( + const blockBindingsSource = unlock( select( blockEditorStore ) - ); + ).getBlockBindingsSource( metadata?.bindings?.url?.source ); return { lockUrlControls: !! metadata?.bindings?.url && - getBlockBindingsSource( metadata?.bindings?.url?.source ) - ?.lockAttributesEditing, + ( ! blockBindingsSource || + blockBindingsSource?.lockAttributesEditing ), }; }, [ isSelected ] diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 61d023e4e580a1..86970b588ff03d 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -341,15 +341,15 @@ export function ImageEdit( { return {}; } - const { getBlockBindingsSource } = unlock( + const blockBindingsSource = unlock( select( blockEditorStore ) - ); + ).getBlockBindingsSource( metadata?.bindings?.url?.source ); return { lockUrlControls: !! metadata?.bindings?.url && - getBlockBindingsSource( metadata?.bindings?.url?.source ) - ?.lockAttributesEditing, + ( ! blockBindingsSource || + blockBindingsSource?.lockAttributesEditing ), }; }, [ isSingleSelected ] diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 62184e6522adb9..a0d481681ee934 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -426,23 +426,32 @@ export default function Image( { } = metadata?.bindings || {}; const hasParentPattern = getBlockParentsByBlockName( clientId, 'core/block' ).length > 0; + const urlBindingSource = getBlockBindingsSource( + urlBinding?.source + ); + const altBindingSource = getBlockBindingsSource( + altBinding?.source + ); + const titleBindingSource = getBlockBindingsSource( + titleBinding?.source + ); return { lockUrlControls: !! urlBinding && - getBlockBindingsSource( urlBinding?.source ) - ?.lockAttributesEditing, + ( ! urlBindingSource || + urlBindingSource?.lockAttributesEditing ), lockHrefControls: // Disable editing the link of the URL if the image is inside a pattern instance. // This is a temporary solution until we support overriding the link on the frontend. hasParentPattern, lockAltControls: !! altBinding && - getBlockBindingsSource( altBinding?.source ) - ?.lockAttributesEditing, + ( ! altBindingSource || + altBindingSource?.lockAttributesEditing ), lockTitleControls: !! titleBinding && - getBlockBindingsSource( titleBinding?.source ) - ?.lockAttributesEditing, + ( ! titleBindingSource || + titleBindingSource?.lockAttributesEditing ), }; }, [ clientId, isSingleSelected, metadata?.bindings ] diff --git a/packages/blocks/README.md b/packages/blocks/README.md index eda3ac629bef87..8e6fdc9d900dbb 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -234,20 +234,6 @@ _Returns_ - `?string`: Block name. -### getHookedBlocks - -Returns the hooked blocks for a given anchor block. - -Given an anchor block name, returns an object whose keys are relative positions, and whose values are arrays of block names that are hooked to the anchor block at that relative position. - -_Parameters_ - -- _name_ `string`: Anchor block name. - -_Returns_ - -- `Object`: Lists of hooked block names for each relative position. - ### getPhrasingContentSchema Undocumented declaration. diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 8b0e389724f385..aa72979818c9c7 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -124,7 +124,6 @@ export { getBlockTypes, getBlockSupport, hasBlockSupport, - getHookedBlocks, getBlockVariations, isReusableBlock, isTemplatePart, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 71e59949d51d17..6633adf40050c5 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -550,21 +550,6 @@ export function hasBlockSupport( nameOrType, feature, defaultSupports ) { ); } -/** - * Returns the hooked blocks for a given anchor block. - * - * Given an anchor block name, returns an object whose keys are relative positions, - * and whose values are arrays of block names that are hooked to the anchor block - * at that relative position. - * - * @param {string} name Anchor block name. - * - * @return {Object} Lists of hooked block names for each relative position. - */ -export function getHookedBlocks( name ) { - return select( blocksStore ).getHookedBlocks( name ); -} - /** * Determines whether or not the given block is a reusable block. This is a * special block type that is used to point to a global block stored via the diff --git a/packages/blocks/src/api/templates.js b/packages/blocks/src/api/templates.js index 34e6954a9ff33f..bc76218892688a 100644 --- a/packages/blocks/src/api/templates.js +++ b/packages/blocks/src/api/templates.js @@ -8,7 +8,7 @@ import { renderToString } from '@wordpress/element'; */ import { convertLegacyBlockNameAndAttributes } from './parser/convert-legacy-block'; import { createBlock } from './factory'; -import { getBlockType, getHookedBlocks } from './registration'; +import { getBlockType } from './registration'; /** * Checks whether a list of blocks matches a template by comparing the block names. @@ -115,35 +115,6 @@ export function synchronizeBlocksWithTemplate( blocks = [], template ) { normalizedAttributes ); - const ignoredHookedBlocks = [ - ...new Set( - Object.values( getHookedBlocks( blockName ) ).flat() - ), - ]; - - if ( ignoredHookedBlocks.length ) { - const { metadata = {}, ...otherAttributes } = blockAttributes; - const { - ignoredHookedBlocks: ignoredHookedBlocksFromTemplate = [], - ...otherMetadata - } = metadata; - - const newIgnoredHookedBlocks = [ - ...new Set( [ - ...ignoredHookedBlocks, - ...ignoredHookedBlocksFromTemplate, - ] ), - ]; - - blockAttributes = { - metadata: { - ignoredHookedBlocks: newIgnoredHookedBlocks, - ...otherMetadata, - }, - ...otherAttributes, - }; - } - // If a Block is undefined at this point, use the core/missing block as // a placeholder for a better user experience. if ( undefined === getBlockType( blockName ) ) { diff --git a/packages/blocks/src/api/test/templates.js b/packages/blocks/src/api/test/templates.js index 8ee031aedbeefc..0a23505f0ac036 100644 --- a/packages/blocks/src/api/test/templates.js +++ b/packages/blocks/src/api/test/templates.js @@ -28,11 +28,7 @@ describe( 'templates', () => { beforeEach( () => { registerBlockType( 'core/test-block', { - attributes: { - metadata: { - type: 'object', - }, - }, + attributes: {}, save: noop, category: 'text', title: 'test block', @@ -136,80 +132,6 @@ describe( 'templates', () => { ] ); } ); - it( 'should set ignoredHookedBlocks metadata if a block has hooked blocks', () => { - registerBlockType( 'core/hooked-block', { - attributes: {}, - save: noop, - category: 'text', - title: 'hooked block', - blockHooks: { 'core/test-block': 'after' }, - } ); - - const template = [ - [ 'core/test-block' ], - [ 'core/test-block-2' ], - [ 'core/test-block-2' ], - ]; - const blockList = []; - - expect( - synchronizeBlocksWithTemplate( blockList, template ) - ).toMatchObject( [ - { - name: 'core/test-block', - attributes: { - metadata: { - ignoredHookedBlocks: [ 'core/hooked-block' ], - }, - }, - }, - { name: 'core/test-block-2' }, - { name: 'core/test-block-2' }, - ] ); - } ); - - it( 'retains previously set ignoredHookedBlocks metadata', () => { - registerBlockType( 'core/hooked-block', { - attributes: {}, - save: noop, - category: 'text', - title: 'hooked block', - blockHooks: { 'core/test-block': 'after' }, - } ); - - const template = [ - [ - 'core/test-block', - { - metadata: { - ignoredHookedBlocks: [ 'core/other-hooked-block' ], - }, - }, - ], - [ 'core/test-block-2' ], - [ 'core/test-block-2' ], - ]; - const blockList = []; - - expect( - synchronizeBlocksWithTemplate( blockList, template ) - ).toMatchObject( [ - { - name: 'core/test-block', - attributes: { - metadata: { - ignoredHookedBlocks: [ - 'core/hooked-block', - 'core/other-hooked-block', - ], - }, - }, - }, - { name: 'core/test-block-2' }, - { name: 'core/test-block-2' }, - ] ); - } ); - it( 'should create nested blocks', () => { const template = [ [ 'core/test-block', {}, [ [ 'core/test-block-2' ] ] ], diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index 9eda135d0d6999..b2b8ab8106f097 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -106,68 +106,6 @@ export function getBlockType( state, name ) { return state.blockTypes[ name ]; } -/** - * Returns the hooked blocks for a given anchor block. - * - * Given an anchor block name, returns an object whose keys are relative positions, - * and whose values are arrays of block names that are hooked to the anchor block - * at that relative position. - * - * @param {Object} state Data state. - * @param {string} blockName Anchor block type name. - * - * @example - * ```js - * import { store as blocksStore } from '@wordpress/blocks'; - * import { useSelect } from '@wordpress/data'; - * - * const ExampleComponent = () => { - * const hookedBlockNames = useSelect( ( select ) => - * select( blocksStore ).getHookedBlocks( 'core/navigation' ), - * [] - * ); - * - * return ( - * - * ); - * }; - * ``` - * - * @return {Object} Lists of hooked block names for each relative position. - */ -export const getHookedBlocks = createSelector( - ( state, blockName ) => { - const hookedBlockTypes = getBlockTypes( state ).filter( - ( { blockHooks } ) => blockHooks && blockName in blockHooks - ); - - let hookedBlocks = {}; - for ( const blockType of hookedBlockTypes ) { - const relativePosition = blockType.blockHooks[ blockName ]; - hookedBlocks = { - ...hookedBlocks, - [ relativePosition ]: [ - ...( hookedBlocks[ relativePosition ] ?? [] ), - blockType.name, - ], - }; - } - return hookedBlocks; - }, - ( state ) => [ state.blockTypes ] -); - /** * Returns block styles by block name. * diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js index 0dcdde3c07bf10..1fda11d72311a3 100644 --- a/packages/blocks/src/store/test/selectors.js +++ b/packages/blocks/src/store/test/selectors.js @@ -12,7 +12,6 @@ import { getBlockVariations, getDefaultBlockVariation, getGroupingBlockName, - getHookedBlocks, isMatchingSearchTerm, getCategories, getActiveBlockVariation, @@ -229,111 +228,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'getHookedBlocks', () => { - it( 'should return an empty object if state is empty', () => { - const state = { - blockTypes: {}, - }; - - expect( getHookedBlocks( state, 'anchor' ) ).toEqual( {} ); - } ); - - it( 'should return an empty object if the anchor block is not found', () => { - const state = { - blockTypes: { - anchor: { - name: 'anchor', - }, - hookedBlock: { - name: 'hookedBlock', - blockHooks: { - anchor: 'after', - }, - }, - }, - }; - - expect( getHookedBlocks( state, 'otherAnchor' ) ).toEqual( {} ); - } ); - - it( "should return the anchor block name even if the anchor block doesn't exist", () => { - const state = { - blockTypes: { - hookedBlock: { - name: 'hookedBlock', - blockHooks: { - anchor: 'after', - }, - }, - }, - }; - - expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { - after: [ 'hookedBlock' ], - } ); - } ); - - it( 'should return an array with the hooked block names', () => { - const state = { - blockTypes: { - anchor: { - name: 'anchor', - }, - hookedBlock1: { - name: 'hookedBlock1', - blockHooks: { - anchor: 'after', - }, - }, - hookedBlock2: { - name: 'hookedBlock2', - blockHooks: { - anchor: 'before', - }, - }, - }, - }; - - expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { - after: [ 'hookedBlock1' ], - before: [ 'hookedBlock2' ], - } ); - } ); - - it( 'should return an array with the hooked block names, even if multiple blocks are in the same relative position', () => { - const state = { - blockTypes: { - anchor: { - name: 'anchor', - }, - hookedBlock1: { - name: 'hookedBlock1', - blockHooks: { - anchor: 'after', - }, - }, - hookedBlock2: { - name: 'hookedBlock2', - blockHooks: { - anchor: 'before', - }, - }, - hookedBlock3: { - name: 'hookedBlock3', - blockHooks: { - anchor: 'after', - }, - }, - }, - }; - - expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { - after: [ 'hookedBlock1', 'hookedBlock3' ], - before: [ 'hookedBlock2' ], - } ); - } ); - } ); - describe( 'Testing block variations selectors', () => { const blockName = 'block/name'; const createBlockVariationsState = ( variations ) => { diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 94aa00e1c8de45..85dc4e3be7203c 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -1,7 +1,18 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; + +/** + * WordPress dependencies + */ +import { createRegistrySelector } from '@wordpress/data'; + /** * Internal dependencies */ import type { State } from './selectors'; +import { STORE_NAME } from './name'; type EntityRecordKey = string | number; @@ -28,3 +39,19 @@ export function getNavigationFallbackId( ): EntityRecordKey | undefined { return state.navigationFallbackId; } + +export const getBlockPatternsForPostType = createRegistrySelector( + ( select: any ) => + createSelector( + ( state, postType ) => + select( STORE_NAME ) + .getBlockPatterns() + .filter( + ( { postTypes } ) => + ! postTypes || + ( Array.isArray( postTypes ) && + postTypes.includes( postType ) ) + ), + () => [ select( STORE_NAME ).getBlockPatterns() ] + ) +); diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 14024163878d1d..44ab1822a60750 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -12,6 +12,7 @@ import { __experimentalVStack as VStack, Tooltip, } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; import { useAsyncList } from '@wordpress/compose'; import { useState } from '@wordpress/element'; @@ -128,6 +129,7 @@ export default function ViewGrid( { fields, view, actions, + isLoading, getItemId, deferredRendering, selection, @@ -148,29 +150,45 @@ export default function ViewGrid( { ); const shownData = useAsyncList( data, { step: 3 } ); const usedData = deferredRendering ? shownData : data; + const hasData = !! usedData?.length; return ( - - { usedData.map( ( item ) => { - return ( - - ); - } ) } - + <> + { hasData && ( + + { usedData.map( ( item ) => { + return ( + + ); + } ) } + + ) } + { ! hasData && ( +
+

{ isLoading ? __( 'Loading…' ) : __( 'No results' ) }

+
+ ) } + ); } diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php index 4abcee3de2c399..0b8e6e1012d1a4 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php @@ -18,7 +18,7 @@ ?>

diff --git a/packages/edit-post/src/components/editor-initialization/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/listener-hooks.js index 57f5f67721ba72..73872b4d7110e4 100644 --- a/packages/edit-post/src/components/editor-initialization/listener-hooks.js +++ b/packages/edit-post/src/components/editor-initialization/listener-hooks.js @@ -5,6 +5,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect, useRef } from '@wordpress/element'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as editorStore } from '@wordpress/editor'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -22,19 +23,25 @@ import { * @param {number} postId The current post id. */ export const useBlockSelectionListener = ( postId ) => { - const { hasBlockSelection, isEditorSidebarOpened } = useSelect( - ( select ) => ( { - hasBlockSelection: - !! select( blockEditorStore ).getBlockSelectionStart(), - isEditorSidebarOpened: select( STORE_NAME ).isEditorSidebarOpened(), - } ), - [ postId ] - ); + const { hasBlockSelection, isEditorSidebarOpened, isDistractionFree } = + useSelect( + ( select ) => { + const { get } = select( preferencesStore ); + return { + hasBlockSelection: + !! select( blockEditorStore ).getBlockSelectionStart(), + isEditorSidebarOpened: + select( STORE_NAME ).isEditorSidebarOpened(), + isDistractionFree: get( 'core', 'distractionFree' ), + }; + }, + [ postId ] + ); const { openGeneralSidebar } = useDispatch( STORE_NAME ); useEffect( () => { - if ( ! isEditorSidebarOpened ) { + if ( ! isEditorSidebarOpened || isDistractionFree ) { return; } if ( hasBlockSelection ) { diff --git a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js index c263d993982978..5087d303fafe1e 100644 --- a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js +++ b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js @@ -44,6 +44,12 @@ describe( 'listener hook tests', () => { isViewportMatch: jest.fn(), }, }, + 'core/preferences': { + ...storeConfig, + selectors: { + get: jest.fn(), + }, + }, [ STORE_NAME ]: { ...storeConfig, actions: { @@ -112,6 +118,7 @@ describe( 'listener hook tests', () => { 'getBlockSelectionStart', true ); + setMockReturnValue( 'core/preferences', 'get', false ); render( ); @@ -120,12 +127,14 @@ describe( 'listener hook tests', () => { ).toHaveBeenCalledWith( 'edit-post/block' ); } ); it( 'opens document sidebar if block is not selected', () => { + setMockReturnValue( STORE_NAME, 'isEditorSidebarOpened', true ); setMockReturnValue( STORE_NAME, 'isEditorSidebarOpened', true ); setMockReturnValue( 'core/block-editor', 'getBlockSelectionStart', false ); + setMockReturnValue( 'core/preferences', 'get', false ); render( ); @@ -133,6 +142,37 @@ describe( 'listener hook tests', () => { getSpyedAction( STORE_NAME, 'openGeneralSidebar' ) ).toHaveBeenCalledWith( 'edit-post/document' ); } ); + it( 'does not open block sidebar if block is selected and distraction free mode is on', () => { + setMockReturnValue( STORE_NAME, 'isEditorSidebarOpened', true ); + setMockReturnValue( + 'core/block-editor', + 'getBlockSelectionStart', + true + ); + setMockReturnValue( 'core/preferences', 'get', true ); + + render( ); + + expect( + getSpyedAction( STORE_NAME, 'openGeneralSidebar' ) + ).toHaveBeenCalledTimes( 0 ); + } ); + it( 'does not open document sidebar if block is not selected and distraction free is on', () => { + setMockReturnValue( STORE_NAME, 'isEditorSidebarOpened', true ); + setMockReturnValue( STORE_NAME, 'isEditorSidebarOpened', true ); + setMockReturnValue( + 'core/block-editor', + 'getBlockSelectionStart', + false + ); + setMockReturnValue( 'core/preferences', 'get', true ); + + render( ); + + expect( + getSpyedAction( STORE_NAME, 'openGeneralSidebar' ) + ).toHaveBeenCalledTimes( 0 ); + } ); } ); describe( 'useUpdatePostLinkListener', () => { diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 9dd7314357e043..eb2b696fa39706 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -385,7 +385,7 @@ function Layout( { initialPost } ) { - + { ! isDistractionFree && } ); } diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 08bc7c5aa7002c..527818018ad5c9 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -49,6 +49,7 @@ export function initializeEditor( settings, initialEdits ) { + const isMediumOrBigger = window.matchMedia( '(min-width: 782px)' ).matches; const target = document.getElementById( id ); const root = createRoot( target ); @@ -77,7 +78,9 @@ export function initializeEditor( // Check if the block list view should be open by default. // If `distractionFree` mode is enabled, the block list view should not be open. + // This behavior is disabled for small viewports. if ( + isMediumOrBigger && select( preferencesStore ).get( 'core', 'showListViewByDefault' ) && ! select( preferencesStore ).get( 'core', 'distractionFree' ) ) { diff --git a/packages/edit-site/src/components/add-new-pattern/index.js b/packages/edit-site/src/components/add-new-pattern/index.js index 014ac6165aaef2..0d25ff67bf4482 100644 --- a/packages/edit-site/src/components/add-new-pattern/index.js +++ b/packages/edit-site/src/components/add-new-pattern/index.js @@ -25,10 +25,11 @@ import { PATTERN_DEFAULT_CATEGORY, TEMPLATE_PART_POST_TYPE, } from '../../utils/constants'; -import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories'; const { useHistory, useLocation } = unlock( routerPrivateApis ); -const { CreatePatternModal } = unlock( editPatternsPrivateApis ); +const { CreatePatternModal, useAddPatternCategory } = unlock( + editPatternsPrivateApis +); export default function AddNewPattern() { const history = useHistory(); @@ -43,7 +44,6 @@ export default function AddNewPattern() { const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); const patternUploadInputRef = useRef(); - const { patternCategories } = usePatternCategories(); function handleCreatePattern( { pattern, categoryId } ) { setShowPatternModal( false ); @@ -97,6 +97,7 @@ export default function AddNewPattern() { title: __( 'Import pattern from JSON' ), } ); + const { categoryMap, findOrCreateTerm } = useAddPatternCategory(); return ( <> - category.name === params.categoryId - )?.id; + let currentCategoryId; + // When we're not handling template parts, we should + // add or create the proper pattern category. + if ( params.categoryType !== TEMPLATE_PART_POST_TYPE ) { + const currentCategory = categoryMap + .values() + .find( + ( term ) => term.name === params.categoryId + ); + if ( !! currentCategory ) { + currentCategoryId = + currentCategory.id || + ( await findOrCreateTerm( + currentCategory.label + ) ); + } + } const pattern = await createPatternFromFile( file, currentCategoryId @@ -146,8 +158,12 @@ export default function AddNewPattern() { ); // Navigate to the All patterns category for the newly created pattern - // if we're not on that page already. - if ( ! currentCategoryId ) { + // if we're not on that page already and if we're not in the `my-patterns` + // category. + if ( + ! currentCategoryId && + params.categoryId !== 'my-patterns' + ) { history.push( { path: `/patterns`, categoryType: PATTERN_TYPES.theme, diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 9c95755db28031..6563374858a37f 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -263,6 +263,7 @@ export default function Editor( { isLoading } ) { ( shouldShowListView && ) ) } sidebar={ + ! isDistractionFree && isEditMode && isRightSidebarOpen && ( <> diff --git a/packages/edit-site/src/components/global-styles/block-preview-panel.js b/packages/edit-site/src/components/global-styles/block-preview-panel.js index 24bd2e666eab3e..787d8c6d47e500 100644 --- a/packages/edit-site/src/components/global-styles/block-preview-panel.js +++ b/packages/edit-site/src/components/global-styles/block-preview-panel.js @@ -28,7 +28,7 @@ const BlockPreviewPanel = ( { name, variation = '' } ) => { }, [ name, blockExample, variation ] ); const viewportWidth = blockExample?.viewportWidth ?? null; - const previewHeight = '150px'; + const previewHeight = 150; if ( ! blockExample ) { return null; @@ -48,7 +48,7 @@ const BlockPreviewPanel = ( { name, variation = '' } ) => { { css: ` body{ - min-height:${ previewHeight }; + min-height:${ previewHeight }px; display:flex;align-items:center;justify-content:center; } `, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js index de0c1cfa16ec6e..a6962952661939 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js @@ -48,7 +48,7 @@ function CollectionFontDetails( { /> ) ) } - + ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 4a14ee245694b8..6236ea8fe3f246 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -1,11 +1,18 @@ /** * WordPress dependencies */ -import { useContext, useEffect, useState, useMemo } from '@wordpress/element'; +import { + useContext, + useEffect, + useState, + useMemo, + createInterpolateElement, +} from '@wordpress/element'; import { __experimentalSpacer as Spacer, __experimentalInputControl as InputControl, __experimentalText as Text, + __experimentalHStack as HStack, SelectControl, Spinner, Icon, @@ -14,7 +21,7 @@ import { Button, } from '@wordpress/components'; import { debounce } from '@wordpress/compose'; -import { __, _x } from '@wordpress/i18n'; +import { sprintf, __, _x } from '@wordpress/i18n'; import { search, closeSmall } from '@wordpress/icons'; /** @@ -22,7 +29,6 @@ import { search, closeSmall } from '@wordpress/icons'; */ import TabPanelLayout from './tab-panel-layout'; import { FontLibraryContext } from './context'; -import FontsGrid from './fonts-grid'; import FontCard from './font-card'; import filterFonts from './utils/filter-fonts'; import CollectionFontDetails from './collection-font-details'; @@ -48,6 +54,7 @@ function FontCollection( { slug } ) { const [ selectedFont, setSelectedFont ] = useState( null ); const [ fontsToInstall, setFontsToInstall ] = useState( [] ); + const [ page, setPage ] = useState( 1 ); const [ filters, setFilters ] = useState( {} ); const [ renderConfirmDialog, setRenderConfirmDialog ] = useState( requiresPermission && ! getGoogleFontsPermissionFromStorage() @@ -109,22 +116,34 @@ function FontCollection( { slug } ) { [ collectionFonts, filters ] ); + // NOTE: The height of the font library modal unavailable to use for rendering font family items is roughly 417px + // The height of each font family item is 61px. + const pageSize = Math.floor( ( window.innerHeight - 417 ) / 61 ); + const totalPages = Math.ceil( fonts.length / pageSize ); + const itemsStart = ( page - 1 ) * pageSize; + const itemsLimit = page * pageSize; + const items = fonts.slice( itemsStart, itemsLimit ); + const handleCategoryFilter = ( category ) => { setFilters( { ...filters, category } ); + setPage( 1 ); }; const handleUpdateSearchInput = ( value ) => { setFilters( { ...filters, search: value } ); + setPage( 1 ); }; const debouncedUpdateSearchInput = debounce( handleUpdateSearchInput, 300 ); const resetFilters = () => { setFilters( {} ); + setPage( 1 ); }; const resetSearch = () => { setFilters( { ...filters, search: '' } ); + setPage( 1 ); }; const handleUnselectFont = () => { @@ -186,6 +205,24 @@ function FontCollection( { slug } ) { resetFontsToInstall(); }; + let footerComponent = null; + if ( selectedFont ) { + footerComponent = ( + + ); + } else if ( ! renderConfirmDialog && totalPages > 1 ) { + footerComponent = ( + + ); + } + return ( - } + footer={ footerComponent } > { renderConfirmDialog && ( <> @@ -275,8 +307,8 @@ function FontCollection( { slug } ) { ) } { ! renderConfirmDialog && ! selectedFont && ( - - { fonts.map( ( font ) => ( +
+ { items.map( ( font ) => ( ) ) } - +
) }
); } -function Footer( { handleInstall, isDisabled } ) { +function PaginationFooter( { page, totalPages, setPage } ) { + return ( + + + + + { createInterpolateElement( + sprintf( + // translators: %s: Total number of pages. + _x( 'Page of %s', 'paging' ), + totalPages + ), + { + CurrenPageControl: ( + { + return { + label: i + 1, + value: i + 1, + }; + } + ) } + onChange={ ( newPage ) => + setPage( parseInt( newPage ) ) + } + size={ 'compact' } + __nextHasNoMarginBottom + /> + ), + } + ) } + + + + + ); +} + +function InstallFooter( { handleInstall, isDisabled } ) { const { isInstalling } = useContext( FontLibraryContext ); return ( diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/fonts-grid.js b/packages/edit-site/src/components/global-styles/font-library-modal/fonts-grid.js deleted file mode 100644 index 9700831a7adef1..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/fonts-grid.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalVStack as VStack, - __experimentalText as Text, - __experimentalSpacer as Spacer, -} from '@wordpress/components'; -import { useState, useEffect } from '@wordpress/element'; - -function FontsGrid( { title, children, pageSize = 32 } ) { - const [ lastItem, setLastItem ] = useState( null ); - const [ page, setPage ] = useState( 1 ); - const itemsLimit = page * pageSize; - const items = children.slice( 0, itemsLimit ); - - useEffect( () => { - if ( lastItem ) { - const observer = new window.IntersectionObserver( ( [ entry ] ) => { - if ( entry.isIntersecting ) { - setPage( ( prevPage ) => prevPage + 1 ); - } - } ); - - observer.observe( lastItem ); - - return () => observer.disconnect(); - } - }, [ lastItem ] ); - - return ( -
- - { title && ( - <> - - { title } - - - - ) } -
- { items.map( ( child, i ) => { - if ( i === itemsLimit - 1 ) { - return ( -
- { child } -
- ); - } - return
{ child }
; - } ) } -
-
-
- ); -} - -export default FontsGrid; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/google-fonts-confirm-dialog.js b/packages/edit-site/src/components/global-styles/font-library-modal/google-fonts-confirm-dialog.js index 67140fbe4d0d93..dde6dc87c858ac 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/google-fonts-confirm-dialog.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/google-fonts-confirm-dialog.js @@ -24,7 +24,7 @@ function GoogleFontsConfirmDialog() {
- Connect to Google Fonts + { __( 'Connect to Google Fonts' ) } { __( diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index 73ff17f25b9a66..99c99e44a43afc 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -7,6 +7,7 @@ import { privateApis as componentsPrivateApis, __experimentalHStack as HStack, __experimentalSpacer as Spacer, + __experimentalText as Text, Button, Spinner, FlexItem, @@ -17,7 +18,6 @@ import { */ import TabPanelLayout from './tab-panel-layout'; import { FontLibraryContext } from './context'; -import FontsGrid from './fonts-grid'; import LibraryFontDetails from './library-font-details'; import LibraryFontCard from './library-font-card'; import ConfirmDeleteDialog from './confirm-delete-dialog'; @@ -123,36 +123,38 @@ function InstalledFonts() { ) } { baseCustomFonts.length > 0 && ( <> - - { baseCustomFonts.map( ( font ) => ( - { - handleSelectFont( font ); - } } - /> - ) ) } - + { baseCustomFonts.map( ( font ) => ( + { + handleSelectFont( font ); + } } + /> + ) ) } ) } { baseThemeFonts.length > 0 && ( <> - - { baseThemeFonts.map( ( font ) => ( - { - handleSelectFont( font ); - } } - /> - ) ) } - + + { __( 'Theme Fonts' ) } + + + + { baseThemeFonts.map( ( font ) => ( + { + handleSelectFont( font ); + } } + /> + ) ) } ) } + ) } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss index beb1ba46714ff7..544e3ed63c9883 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss +++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss @@ -36,11 +36,8 @@ } } -.font-library-modal__fonts-grid { - .font-library-modal__fonts-grid__main { - display: flex; - flex-direction: column; - } +.font-library-modal__tabpanel-layout .components-base-control__field { + margin-bottom: 0; } .font-library-modal__font-card { diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 163d4fe6e938ff..65dfb71949a7f5 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -251,47 +251,64 @@ export default function Layout() { The NavigableRegion must always be rendered and not use `inert` otherwise `useNavigateRegions` will fail. */ } - - - { canvasMode === 'view' && ( - - - - ) } - - + { ( ! isMobileViewport || + ( isMobileViewport && ! areas.mobile ) ) && ( + + + { canvasMode === 'view' && ( + + + + ) } + + + ) } - { areas.content && canvasMode !== 'edit' && ( + { isMobileViewport && areas.mobile && (
- { areas.content } + { areas.mobile }
) } - { areas.preview && ( + { ! isMobileViewport && + areas.content && + canvasMode !== 'edit' && ( +
+ { areas.content } +
+ ) } + + { ! isMobileViewport && areas.preview && (
{ canvasResizer } { !! canvasSize.width && ( diff --git a/packages/edit-site/src/components/layout/router.js b/packages/edit-site/src/components/layout/router.js index aeb124bf76fe5f..02f7e7b25594ae 100644 --- a/packages/edit-site/src/components/layout/router.js +++ b/packages/edit-site/src/components/layout/router.js @@ -23,13 +23,21 @@ const { useLocation } = unlock( routerPrivateApis ); export default function useLayoutAreas() { const isSiteEditorLoading = useIsSiteEditorLoading(); const { params } = useLocation(); - const { postType, postId, path, layout, isCustom } = params ?? {}; + const { postType, postId, path, layout, isCustom, canvas } = params ?? {}; + + // Note: Since "sidebar" is not yet supported here, + // returning undefined from "mobile" means show the sidebar. + // Regular page if ( path === '/page' ) { return { areas: { content: undefined, preview: , + mobile: + canvas === 'edit' ? ( + + ) : undefined, }, widths: { content: undefined, @@ -63,6 +71,10 @@ export default function useLayoutAreas() { return { areas: { preview: , + mobile: + canvas === 'edit' ? ( + + ) : undefined, }, }; } @@ -79,6 +91,11 @@ export default function useLayoutAreas() { preview: isListLayout && ( ), + mobile: ( + + ), }, widths: { content: isListLayout ? 380 : undefined, @@ -98,6 +115,11 @@ export default function useLayoutAreas() { preview: isListLayout && ( ), + mobile: ( + + ), }, widths: { content: isListLayout ? 380 : undefined, @@ -110,12 +132,19 @@ export default function useLayoutAreas() { return { areas: { content: , + mobile: , }, }; } // Fallback shows the home page preview return { - areas: { preview: }, + areas: { + preview: , + mobile: + canvas === 'edit' ? ( + + ) : undefined, + }, }; } diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 9be0b001ed9e25..ad33768989a81b 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -83,6 +83,12 @@ flex-direction: column; } +.edit-site-layout__mobile { + position: relative; + width: 100%; + z-index: z-index(".edit-site-layout__canvas-container"); +} + .edit-site-layout__canvas-container { position: relative; flex-grow: 1; @@ -147,6 +153,7 @@ } // This shouldn't be necessary (we should have a way to say that a skeletton is relative +.edit-site-layout__mobile .interface-interface-skeleton, .edit-site-layout__canvas .interface-interface-skeleton, .edit-site-template-pages-preview .interface-interface-skeleton { position: relative !important; diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index e5bd44956b2629..dd32818edd614b 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -225,6 +225,12 @@ * TODO: when this becomes stable, consolidate styles with the above. */ .edit-site-page-patterns-dataviews { + margin-top: 60px; + + @include break-medium { + margin-top: 0; + } + .page-patterns-preview-field { display: flex; flex-direction: column; @@ -283,6 +289,10 @@ text-overflow: ellipsis; color: inherit; } + + .dataviews-pagination { + z-index: z-index(".edit-site-patterns__dataviews-list-pagination"); + } } .dataviews-action-modal__duplicate-pattern { diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js index a0b82247c85a6d..b099b70b36fe1a 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -111,6 +111,7 @@ const selectTemplatePartsAsPatterns = createSelector( const selectThemePatterns = createSelector( ( select ) => { const { getSettings } = unlock( select( editSiteStore ) ); + const { getIsResolving } = select( coreStore ); const settings = getSettings(); const blockPatterns = settings.__experimentalAdditionalBlockPatterns ?? @@ -136,19 +137,23 @@ const selectThemePatterns = createSelector( __unstableSkipMigrationLogs: true, } ), } ) ); - - return { patterns, isResolving: false }; + return { patterns, isResolving: getIsResolving( 'getBlockPatterns' ) }; }, ( select ) => [ select( coreStore ).getBlockPatterns(), + select( coreStore ).getIsResolving( 'getBlockPatterns' ), unlock( select( editSiteStore ) ).getSettings(), ] ); const selectPatterns = createSelector( ( select, categoryId, syncStatus, search = '' ) => { - const { patterns: themePatterns } = selectThemePatterns( select ); - const { patterns: userPatterns } = selectUserPatterns( select ); + const { + patterns: themePatterns, + isResolving: isResolvingThemePatterns, + } = selectThemePatterns( select ); + const { patterns: userPatterns, isResolving: isResolvingUserPatterns } = + selectUserPatterns( select ); let patterns = [ ...( themePatterns || [] ), @@ -176,7 +181,10 @@ const selectPatterns = createSelector( hasCategory: ( item ) => ! item.hasOwnProperty( 'categories' ), } ); } - return { patterns, isResolving: false }; + return { + patterns, + isResolving: isResolvingThemePatterns || isResolvingUserPatterns, + }; }, ( select ) => [ selectThemePatterns( select ), diff --git a/packages/edit-site/src/components/page-templates-template-parts/index.js b/packages/edit-site/src/components/page-templates-template-parts/index.js index 514ff148071955..9089d31cd66e0a 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/index.js +++ b/packages/edit-site/src/components/page-templates-template-parts/index.js @@ -429,6 +429,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { return ( { }; export default function SidebarNavigationScreenPages() { + const isMobileViewport = useViewportMatch( 'medium', '<' ); const { records: pages, isResolving: isLoadingPages } = useEntityRecords( 'postType', 'page', @@ -220,12 +222,14 @@ export default function SidebarNavigationScreenPages() { ) ) } - - { __( 'Manage all pages' ) } - + { ! isMobileViewport && ( + + { __( 'Manage all pages' ) } + + ) } } /> diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index 84a89957b475d5..56107edc63c863 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -13,6 +13,7 @@ import { } from '@wordpress/components'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { __ } from '@wordpress/i18n'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -52,6 +53,7 @@ function SidebarScreenWrapper( { className, ...props } ) { function SidebarScreens() { useSyncPathWithURL(); + const isMobileViewport = useViewportMatch( 'medium', '<' ); return ( <> @@ -82,9 +84,11 @@ function SidebarScreens() { - - - + { ! isMobileViewport && ( + + + + ) } diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index 930e89c6254102..fd23903a6a05e4 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -22,6 +22,8 @@ import { TEMPLATE_POST_TYPE } from '../utils/constants'; export const setCanvasMode = ( mode ) => ( { registry, dispatch } ) => { + const isMediumOrBigger = + window.matchMedia( '(min-width: 782px)' ).matches; registry.dispatch( blockEditorStore ).__unstableSetEditorMode( 'edit' ); dispatch( { type: 'SET_CANVAS_MODE', @@ -29,7 +31,9 @@ export const setCanvasMode = } ); // Check if the block list view should be open by default. // If `distractionFree` mode is enabled, the block list view should not be open. + // This behavior is disabled for small viewports. if ( + isMediumOrBigger && mode === 'edit' && registry .select( preferencesStore ) diff --git a/packages/editor/src/components/document-tools/index.js b/packages/editor/src/components/document-tools/index.js index 05907654fa9b84..26adcb1bb725a7 100644 --- a/packages/editor/src/components/document-tools/index.js +++ b/packages/editor/src/components/document-tools/index.js @@ -46,6 +46,7 @@ function DocumentTools( { const { setIsInserterOpened, setIsListViewOpened } = useDispatch( editorStore ); const { + isDistractionFree, isInserterOpened, isListViewOpen, listViewShortcut, @@ -69,6 +70,7 @@ function DocumentTools( { listViewToggleRef: getListViewToggleRef(), hasFixedToolbar: getSettings().hasFixedToolbar, showIconLabels: get( 'core', 'showIconLabels' ), + isDistractionFree: get( 'core', 'distractionFree' ), }; }, [] ); @@ -158,22 +160,26 @@ function DocumentTools( { variant={ showIconLabels ? 'tertiary' : undefined } size="compact" /> - + { ! isDistractionFree && ( + + ) } ) } { children } diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 164f925743522c..d8973850111d1b 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -7,12 +7,12 @@ import { store as coreStore, __experimentalFetchLinkSuggestions as fetchLinkSuggestions, __experimentalFetchUrlData as fetchUrlData, - fetchBlockPatterns, } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; import { useViewportMatch } from '@wordpress/compose'; import { store as blocksStore } from '@wordpress/blocks'; +import { privateApis } from '@wordpress/block-editor'; /** * Internal dependencies @@ -20,6 +20,7 @@ import { store as blocksStore } from '@wordpress/blocks'; import inserterMediaCategories from '../media-categories'; import { mediaUpload } from '../../utils'; import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; const EMPTY_BLOCKS_LIST = []; @@ -247,17 +248,10 @@ function useBlockEditorSettings( settings, postType, postId ) { keepCaretInsideBlock, mediaUpload: hasUploadPermissions ? mediaUpload : undefined, __experimentalBlockPatterns: blockPatterns, - __experimentalFetchBlockPatterns: async () => { - return ( await fetchBlockPatterns() ).filter( - ( { postTypes } ) => { - return ( - ! postTypes || - ( Array.isArray( postTypes ) && - postTypes.includes( postType ) ) - ); - } - ); - }, + [ unlock( privateApis ).selectBlockPatternsKey ]: ( select ) => + unlock( select( coreStore ) ).getBlockPatternsForPostType( + postType + ), __experimentalReusableBlocks: reusableBlocks, __experimentalBlockPatternCategories: blockPatternCategories, __experimentalUserPatternCategories: userPatternCategories, diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 137c14222ced34..9576e50309e237 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -10,21 +10,17 @@ import { ToggleControl, } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; -import { useState, useMemo } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; -import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ import { PATTERN_DEFAULT_CATEGORY, PATTERN_SYNC_TYPES } from '../constants'; - -/** - * Internal dependencies - */ import { store as patternsStore } from '../store'; -import CategorySelector, { CATEGORY_SLUG } from './category-selector'; +import CategorySelector from './category-selector'; +import { useAddPatternCategory } from '../private-hooks'; import { unlock } from '../lock-unlock'; export default function CreatePatternModal( { @@ -59,47 +55,9 @@ export function CreatePatternModalContents( { const [ isSaving, setIsSaving ] = useState( false ); const { createPattern } = unlock( useDispatch( patternsStore ) ); - const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); const { createErrorNotice } = useDispatch( noticesStore ); - const { corePatternCategories, userPatternCategories } = useSelect( - ( select ) => { - const { getUserPatternCategories, getBlockPatternCategories } = - select( coreStore ); - - return { - corePatternCategories: getBlockPatternCategories(), - userPatternCategories: getUserPatternCategories(), - }; - } - ); - - const categoryMap = useMemo( () => { - // Merge the user and core pattern categories and remove any duplicates. - const uniqueCategories = new Map(); - userPatternCategories.forEach( ( category ) => { - uniqueCategories.set( category.label.toLowerCase(), { - label: category.label, - name: category.name, - id: category.id, - } ); - } ); - - corePatternCategories.forEach( ( category ) => { - if ( - ! uniqueCategories.has( category.label.toLowerCase() ) && - // There are two core categories with `Post` label so explicitly remove the one with - // the `query` slug to avoid any confusion. - category.name !== 'query' - ) { - uniqueCategories.set( category.label.toLowerCase(), { - label: category.label, - name: category.name, - } ); - } - } ); - return uniqueCategories; - }, [ userPatternCategories, corePatternCategories ] ); + const { categoryMap, findOrCreateTerm } = useAddPatternCategory(); async function onCreate( patternTitle, sync ) { if ( ! title || isSaving ) { @@ -137,38 +95,6 @@ export function CreatePatternModalContents( { } } - /** - * @param {string} term - * @return {Promise} The pattern category id. - */ - async function findOrCreateTerm( term ) { - try { - const existingTerm = categoryMap.get( term.toLowerCase() ); - if ( existingTerm && existingTerm.id ) { - return existingTerm.id; - } - // If we have an existing core category we need to match the new user category to the - // correct slug rather than autogenerating it to prevent duplicates, eg. the core `Headers` - // category uses the singular `header` as the slug. - const termData = existingTerm - ? { name: existingTerm.label, slug: existingTerm.name } - : { name: term }; - const newTerm = await saveEntityRecord( - 'taxonomy', - CATEGORY_SLUG, - termData, - { throwOnError: true } - ); - invalidateResolution( 'getUserPatternCategories' ); - return newTerm.id; - } catch ( error ) { - if ( error.code !== 'term_exists' ) { - throw error; - } - - return error.data.term_id; - } - } return (
{ diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 099e4ae8ffed4c..a5fbddb62fd62c 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -15,6 +15,7 @@ import PatternsMenuItems from './components'; import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; import PartialSyncingControls from './components/partial-syncing-controls'; import ResetOverridesControl from './components/reset-overrides-control'; +import { useAddPatternCategory } from './private-hooks'; import { PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, @@ -35,6 +36,7 @@ lock( privateApis, { RenamePatternCategoryModal, PartialSyncingControls, ResetOverridesControl, + useAddPatternCategory, PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, PATTERN_USER_CATEGORY, diff --git a/packages/patterns/src/private-hooks.js b/packages/patterns/src/private-hooks.js new file mode 100644 index 00000000000000..7dee37222fbbda --- /dev/null +++ b/packages/patterns/src/private-hooks.js @@ -0,0 +1,91 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { CATEGORY_SLUG } from './components/category-selector'; + +/** + * Helper hook that creates a Map with the core and user patterns categories + * and removes any duplicates. It's used when we need to create new user + * categories when creating or importing patterns. + * This hook also provides a function to find or create a pattern category. + * + * @return {Object} The merged categories map and the callback function to find or create a category. + */ +export function useAddPatternCategory() { + const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); + const { corePatternCategories, userPatternCategories } = useSelect( + ( select ) => { + const { getUserPatternCategories, getBlockPatternCategories } = + select( coreStore ); + + return { + corePatternCategories: getBlockPatternCategories(), + userPatternCategories: getUserPatternCategories(), + }; + }, + [] + ); + const categoryMap = useMemo( () => { + // Merge the user and core pattern categories and remove any duplicates. + const uniqueCategories = new Map(); + userPatternCategories.forEach( ( category ) => { + uniqueCategories.set( category.label.toLowerCase(), { + label: category.label, + name: category.name, + id: category.id, + } ); + } ); + + corePatternCategories.forEach( ( category ) => { + if ( + ! uniqueCategories.has( category.label.toLowerCase() ) && + // There are two core categories with `Post` label so explicitly remove the one with + // the `query` slug to avoid any confusion. + category.name !== 'query' + ) { + uniqueCategories.set( category.label.toLowerCase(), { + label: category.label, + name: category.name, + } ); + } + } ); + return uniqueCategories; + }, [ userPatternCategories, corePatternCategories ] ); + + async function findOrCreateTerm( term ) { + try { + const existingTerm = categoryMap.get( term.toLowerCase() ); + if ( existingTerm?.id ) { + return existingTerm.id; + } + // If we have an existing core category we need to match the new user category to the + // correct slug rather than autogenerating it to prevent duplicates, eg. the core `Headers` + // category uses the singular `header` as the slug. + const termData = existingTerm + ? { name: existingTerm.label, slug: existingTerm.name } + : { name: term }; + const newTerm = await saveEntityRecord( + 'taxonomy', + CATEGORY_SLUG, + termData, + { throwOnError: true } + ); + invalidateResolution( 'getUserPatternCategories' ); + return newTerm.id; + } catch ( error ) { + if ( error.code !== 'term_exists' ) { + throw error; + } + return error.data.term_id; + } + } + + return { categoryMap, findOrCreateTerm }; +} diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 9392058ce54d6a..fc315e522b81ac 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -72,7 +72,7 @@ test.describe( 'Block bindings', () => { ); } ); - test( 'Should lock the appropriate controls', async ( { + test( 'Should lock the appropriate controls with a registered source', async ( { editor, page, } ) => { @@ -117,6 +117,52 @@ test.describe( 'Block bindings', () => { 'false' ); } ); + + test( 'Should lock the appropriate controls when source is not defined', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await paragraphBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Paragraph is not editable. + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); } ); test.describe( 'Heading', () => { @@ -143,7 +189,7 @@ test.describe( 'Block bindings', () => { await expect( headingBlock ).toHaveText( 'text_custom_field' ); } ); - test( 'Should lock the appropriate controls', async ( { + test( 'Should lock the appropriate controls with a registered source', async ( { editor, page, } ) => { @@ -188,6 +234,52 @@ test.describe( 'Block bindings', () => { 'false' ); } ); + + test( 'Should lock the appropriate controls when source is not defined', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const headingBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await headingBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Heading is not editable. + await expect( headingBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); } ); test.describe( 'Button', () => { @@ -221,7 +313,7 @@ test.describe( 'Block bindings', () => { await expect( buttonBlock ).toHaveText( 'text_custom_field' ); } ); - test( 'Should lock text controls when text is bound', async ( { + test( 'Should lock text controls when text is bound to a registered source', async ( { editor, page, } ) => { @@ -283,7 +375,69 @@ test.describe( 'Block bindings', () => { ).toBeVisible(); } ); - test( 'Should lock url controls when url is bound', async ( { + test( 'Should lock text controls when text is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Button is not editable. + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + + // Link controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Unlink' } ) + ).toBeVisible(); + } ); + + test( 'Should lock url controls when url is bound to a registered source', async ( { editor, page, } ) => { @@ -343,6 +497,66 @@ test.describe( 'Block bindings', () => { ).toBeHidden(); } ); + test( 'Should lock url controls when url is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + url: { + source: 'plugin/undefined-source', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + + // Format controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeVisible(); + + // Button is editable. + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + + // Link controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Link' } ) + ).toBeHidden(); + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Unlink' } ) + ).toBeHidden(); + } ); + test( 'Should lock url and text controls when both are bound', async ( { editor, page, @@ -429,7 +643,7 @@ test.describe( 'Block bindings', () => { ).toBeVisible(); } ); - test( 'Should NOT show the upload form when url is bound', async ( { + test( 'Should NOT show the upload form when url is bound to a registered source', async ( { editor, } ) => { await editor.insertBlock( { @@ -457,7 +671,35 @@ test.describe( 'Block bindings', () => { ).toBeHidden(); } ); - test( 'Should lock url controls when url is bound', async ( { + test( 'Should NOT show the upload form when url is bound to an undefined source', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'plugin/undefined-source', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + await expect( + imageBlock.getByRole( 'button', { name: 'Upload' } ) + ).toBeHidden(); + } ); + + test( 'Should lock url controls when url is bound to a registered source', async ( { editor, page, } ) => { @@ -526,7 +768,76 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); - test( 'Should disable alt textarea when alt is bound', async ( { + test( 'Should lock url controls when url is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'plugin/undefined-source', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Replace controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + ).toBeHidden(); + + // Image placeholder doesn't show the upload button. + await expect( + imageBlock.getByRole( 'button', { name: 'Upload' } ) + ).toBeHidden(); + + // Alt textarea is enabled and with the original value. + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + ).toBeEnabled(); + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'default alt value' ); + + // Title input is enabled and with the original value. + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + ).toBeEnabled(); + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'default title value' ); + } ); + + test( 'Should disable alt textarea when alt is bound to a registered source', async ( { editor, page, } ) => { @@ -589,7 +900,70 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); - test( 'Should disable title input when title is bound', async ( { + test( 'Should disable alt textarea when alt is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + alt: { + source: 'plguin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Replace controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + ).toBeVisible(); + + // Alt textarea is disabled and with the custom field value. + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + ).toHaveAttribute( 'readonly' ); + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'default alt value' ); + + // Title input is enabled and with the original value. + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + ).toBeEnabled(); + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'default title value' ); + } ); + + test( 'Should disable title input when title is bound to a registered source', async ( { editor, page, } ) => { @@ -652,6 +1026,69 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'text_custom_field' ); } ); + test( 'Should disable title input when title is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + title: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Replace controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + ).toBeVisible(); + + // Alt textarea is enabled and with the original value. + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + ).toBeEnabled(); + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'default alt value' ); + + // Title input is disabled and with the custom field value. + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + ).toHaveAttribute( 'readonly' ); + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'default title value' ); + } ); + test( 'Multiple bindings should lock the appropriate controls', async ( { editor, page, diff --git a/test/e2e/specs/editor/various/copy-cut-paste.spec.js b/test/e2e/specs/editor/various/copy-cut-paste.spec.js index 04113e013930b0..ec0cbab9ca809b 100644 --- a/test/e2e/specs/editor/various/copy-cut-paste.spec.js +++ b/test/e2e/specs/editor/various/copy-cut-paste.spec.js @@ -518,6 +518,30 @@ test.describe( 'Copy/cut/paste', () => { ] ); } ); + test( 'should link selection on internal paste', async ( { + pageUtils, + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'https://w.org' }, + } ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+x' ); + await page.keyboard.type( 'a' ); + await pageUtils.pressKeys( 'shift+ArrowLeft' ); + await pageUtils.pressKeys( 'primary+v' ); + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'a', + }, + }, + ] ); + } ); + test( 'should auto-link', async ( { pageUtils, editor } ) => { await editor.insertBlock( { name: 'core/paragraph',