diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 73e4186fce9b6..628225f536e45 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -275,7 +275,7 @@ Add a link to a downloadable file. ([Source](https://github.com/WordPress/gutenb - **Name:** core/footnotes - **Category:** text -- **Supports:** ~~html~~, ~~inserter~~, ~~multiple~~, ~~reusable~~ +- **Supports:** ~~html~~, ~~multiple~~, ~~reusable~~ - **Attributes:** ## Classic diff --git a/lib/compat/wordpress-6.3/theme-previews.php b/lib/compat/wordpress-6.3/theme-previews.php index eab05c5824b1f..f663e7a9783d6 100644 --- a/lib/compat/wordpress-6.3/theme-previews.php +++ b/lib/compat/wordpress-6.3/theme-previews.php @@ -107,7 +107,7 @@ function block_theme_activate_nonce() { $nonce_handle = 'switch-theme_' . gutenberg_get_theme_preview_path(); ?> { - const doc = document.implementation.createHTMLDocument( '' ); - doc.body.innerHTML = html; - return Array.from( doc.body.children ); - }, [ html ] ); -} - -async function loadScript( head, { id, src } ) { - return new Promise( ( resolve, reject ) => { - const script = head.ownerDocument.createElement( 'script' ); - script.id = id; - if ( src ) { - script.src = src; - script.onload = () => resolve(); - script.onerror = () => reject(); - } else { - resolve(); - } - head.appendChild( script ); - } ); -} - function Iframe( { contentRef, children, @@ -112,21 +88,22 @@ function Iframe( { forwardedRef: ref, ...props } ) { - const assets = useSelect( + const { styles = '', scripts = '' } = useSelect( ( select ) => select( blockEditorStore ).getSettings().__unstableResolvedAssets, [] ); - const [ , forceRender ] = useReducer( () => ( {} ) ); const [ iframeDocument, setIframeDocument ] = useState(); const [ bodyClasses, setBodyClasses ] = useState( [] ); const compatStyles = useCompatibilityStyles(); - const scripts = useParsedAssets( assets?.scripts ); const clearerRef = useBlockSelectionClearer(); const [ before, writingFlowRef, after ] = useWritingFlow(); const [ contentResizeListener, { height: contentHeight } ] = useResizeObserver(); const setRef = useRefEffect( ( node ) => { + node._load = () => { + setIframeDocument( node.contentDocument ); + }; let iFrameDocument; // Prevent the default browser action for files dropped outside of dropzones. function preventFileDropDefault( event ) { @@ -138,7 +115,6 @@ function Iframe( { iFrameDocument = contentDocument; bubbleEvents( contentDocument ); - setIframeDocument( contentDocument ); clearerRef( documentElement ); // Ideally ALL classes that are added through get_body_class should @@ -154,7 +130,6 @@ function Iframe( { ); contentDocument.dir = ownerDocument.dir; - documentElement.removeChild( contentDocument.body ); for ( const compatStyle of compatStyles ) { if ( contentDocument.getElementById( compatStyle.id ) ) { @@ -199,35 +174,29 @@ function Iframe( { }; }, [] ); - const headRef = useRefEffect( ( element ) => { - scripts - .reduce( - ( promise, script ) => - promise.then( () => loadScript( element, script ) ), - Promise.resolve() - ) - .finally( () => { - // When script are loaded, re-render blocks to allow them - // to initialise. - forceRender(); - } ); - }, [] ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ contentRef, clearerRef, writingFlowRef, disabledRef, - headRef, ] ); // Correct doctype is required to enable rendering in standards // mode. Also preload the styles to avoid a flash of unstyled // content. - const html = - '' + - '' + - ( assets?.styles ?? '' ); + const html = ` + + + + + ${ styles } + ${ scripts } + + + + +`; const [ src, cleanup ] = useMemo( () => { const _src = URL.createObjectURL( diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js index dfd206a1ddb7e..9762582f86f14 100644 --- a/packages/block-editor/src/components/rich-text/content.js +++ b/packages/block-editor/src/components/rich-text/content.js @@ -2,11 +2,7 @@ * WordPress dependencies */ import { RawHTML } from '@wordpress/element'; -import { - children as childrenSource, - getSaveElement, - __unstableGetBlockProps as getBlockProps, -} from '@wordpress/blocks'; +import { children as childrenSource } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; /** @@ -42,44 +38,3 @@ export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { return content; }; - -Content.__unstableIsRichTextContent = {}; - -function findContent( blocks, richTextValues = [] ) { - if ( ! Array.isArray( blocks ) ) { - blocks = [ blocks ]; - } - - for ( const block of blocks ) { - if ( - block?.type?.__unstableIsRichTextContent === - Content.__unstableIsRichTextContent - ) { - richTextValues.push( block.props.value ); - continue; - } - - if ( block?.props?.children ) { - findContent( block.props.children, richTextValues ); - } - } - - return richTextValues; -} - -function _getSaveElement( { name, attributes, innerBlocks } ) { - return getSaveElement( - name, - attributes, - innerBlocks.map( _getSaveElement ) - ); -} - -export function getRichTextValues( blocks = [] ) { - getBlockProps.skipFilters = true; - const values = findContent( - ( Array.isArray( blocks ) ? blocks : [ blocks ] ).map( _getSaveElement ) - ); - getBlockProps.skipFilters = false; - return values; -} diff --git a/packages/block-editor/src/components/rich-text/get-rich-text-values.js b/packages/block-editor/src/components/rich-text/get-rich-text-values.js new file mode 100644 index 0000000000000..4ecee9b76530e --- /dev/null +++ b/packages/block-editor/src/components/rich-text/get-rich-text-values.js @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { RawHTML, StrictMode, Fragment } from '@wordpress/element'; +import { + getSaveElement, + __unstableGetBlockProps as getBlockProps, +} from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import InnerBlocks from '../inner-blocks'; +import { Content } from './content'; + +/* + * This function is similar to `@wordpress/element`'s `renderToString` function, + * except that it does not render the elements to a string, but instead collects + * the values of all rich text `Content` elements. + */ +function addValuesForElement( element, ...args ) { + if ( null === element || undefined === element || false === element ) { + return; + } + + if ( Array.isArray( element ) ) { + return addValuesForElements( element, ...args ); + } + + switch ( typeof element ) { + case 'string': + case 'number': + return; + } + + const { type, props } = element; + + switch ( type ) { + case StrictMode: + case Fragment: + return addValuesForElements( props.children, ...args ); + case RawHTML: + return; + case InnerBlocks.Content: + return addValuesForBlocks( ...args ); + case Content: + const [ values ] = args; + values.push( props.value ); + return; + } + + switch ( typeof type ) { + case 'string': + if ( typeof props.children !== 'undefined' ) { + return addValuesForElements( props.children, ...args ); + } + return; + case 'function': + if ( + type.prototype && + typeof type.prototype.render === 'function' + ) { + return addValuesForElement( + new type( props ).render(), + ...args + ); + } + + return addValuesForElement( type( props ), ...args ); + } +} + +function addValuesForElements( children, ...args ) { + children = Array.isArray( children ) ? children : [ children ]; + + for ( let i = 0; i < children.length; i++ ) { + addValuesForElement( children[ i ], ...args ); + } +} + +function addValuesForBlocks( values, blocks ) { + for ( let i = 0; i < blocks.length; i++ ) { + const { name, attributes, innerBlocks } = blocks[ i ]; + const saveElement = getSaveElement( name, attributes ); + addValuesForElement( saveElement, values, innerBlocks ); + } +} + +export function getRichTextValues( blocks = [] ) { + getBlockProps.skipFilters = true; + const values = []; + addValuesForBlocks( values, blocks ); + getBlockProps.skipFilters = false; + return values; +} diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index dd8d2d8ff411f..432312d0ce0aa 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -4,7 +4,7 @@ import * as globalStyles from './components/global-styles'; import { ExperimentalBlockEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; -import { getRichTextValues } from './components/rich-text/content'; +import { getRichTextValues } from './components/rich-text/get-rich-text-values'; import ResizableBoxPopover from './components/resizable-box-popover'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; import { PrivateListView } from './components/list-view'; diff --git a/packages/block-library/src/comment-template/index.php b/packages/block-library/src/comment-template/index.php index 3a553e802de0e..bb1cfa474e4c3 100644 --- a/packages/block-library/src/comment-template/index.php +++ b/packages/block-library/src/comment-template/index.php @@ -35,8 +35,11 @@ function block_core_comment_template_render_comments( $comments, $block ) { * We set commentId context through the `render_block_context` filter so * that dynamically inserted blocks (at `render_block` filter stage) * will also receive that context. + * + * Use an early priority to so that other 'render_block_context' filters + * have access to the values. */ - add_filter( 'render_block_context', $filter_block_context ); + add_filter( 'render_block_context', $filter_block_context, 1 ); /* * We construct a new WP_Block instance from the parsed block so that @@ -44,7 +47,7 @@ function block_core_comment_template_render_comments( $comments, $block ) { */ $block_content = ( new WP_Block( $block->parsed_block ) )->render( array( 'dynamic' => false ) ); - remove_filter( 'render_block_context', $filter_block_context ); + remove_filter( 'render_block_context', $filter_block_context, 1 ); $children = $comment->get_children(); diff --git a/packages/block-library/src/footnotes/block.json b/packages/block-library/src/footnotes/block.json index 0ab992009d123..e021e9c5225da 100644 --- a/packages/block-library/src/footnotes/block.json +++ b/packages/block-library/src/footnotes/block.json @@ -11,7 +11,6 @@ "supports": { "html": false, "multiple": false, - "inserter": false, "reusable": false }, "style": "wp-block-footnotes" diff --git a/packages/block-library/src/footnotes/edit.js b/packages/block-library/src/footnotes/edit.js index e90a7f82be94a..fdfe7a94039af 100644 --- a/packages/block-library/src/footnotes/edit.js +++ b/packages/block-library/src/footnotes/edit.js @@ -1,8 +1,11 @@ /** * WordPress dependencies */ -import { RichText, useBlockProps } from '@wordpress/block-editor'; +import { BlockIcon, RichText, useBlockProps } from '@wordpress/block-editor'; import { useEntityProp } from '@wordpress/core-data'; +import { __ } from '@wordpress/i18n'; +import { Placeholder } from '@wordpress/components'; +import { formatListNumbered as icon } from '@wordpress/icons'; export default function FootnotesEdit( { context: { postType, postId } } ) { const [ meta, updateMeta ] = useEntityProp( @@ -12,8 +15,24 @@ export default function FootnotesEdit( { context: { postType, postId } } ) { postId ); const footnotes = meta?.footnotes ? JSON.parse( meta.footnotes ) : []; + const blockProps = useBlockProps(); + + if ( ! footnotes.length ) { + return ( +
+ } + label={ __( 'Footnotes' ) } + instructions={ __( + 'Footnotes found in blocks within this document will be displayed here.' + ) } + /> +
+ ); + } + return ( -
    +
      { footnotes.map( ( { id, content } ) => (
    1. *`, }, value.end, value.end diff --git a/packages/block-library/src/footnotes/style.scss b/packages/block-library/src/footnotes/style.scss index 4debba0560f17..aa7ab8b6951dd 100644 --- a/packages/block-library/src/footnotes/style.scss +++ b/packages/block-library/src/footnotes/style.scss @@ -1,17 +1,20 @@ +// These styles are for backwards compatibility with the old footnotes anchors. +// Can be removed in the future. .editor-styles-wrapper, .entry-content { counter-reset: footnotes; } -[data-fn].fn { +a[data-fn].fn { vertical-align: super; font-size: smaller; counter-increment: footnotes; - display: inline-block; + display: inline-flex; + text-decoration: none; text-indent: -9999999px; } -[data-fn].fn::after { +a[data-fn].fn::after { content: "[" counter(footnotes) "]"; text-indent: 0; float: left; diff --git a/packages/block-library/src/image/deprecated.js b/packages/block-library/src/image/deprecated.js index 9b7a41cab188d..bdfdca6ee3c4d 100644 --- a/packages/block-library/src/image/deprecated.js +++ b/packages/block-library/src/image/deprecated.js @@ -6,355 +6,632 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { RichText, useBlockProps } from '@wordpress/block-editor'; +import { + RichText, + useBlockProps, + __experimentalGetElementClassName as getBorderClassesAndStyles, +} from '@wordpress/block-editor'; -const blockAttributes = { - align: { - type: 'string', - }, - url: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'src', - }, - alt: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'alt', - default: '', - }, - caption: { - type: 'string', - source: 'html', - selector: 'figcaption', - }, - title: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'title', - }, - href: { - type: 'string', - source: 'attribute', - selector: 'figure > a', - attribute: 'href', - }, - rel: { - type: 'string', - source: 'attribute', - selector: 'figure > a', - attribute: 'rel', - }, - linkClass: { - type: 'string', - source: 'attribute', - selector: 'figure > a', - attribute: 'class', - }, - id: { - type: 'number', +/** + * Deprecation for adding the `wp-image-${id}` class to the image block for + * responsive images. + * + * @see https://github.com/WordPress/gutenberg/pull/4898 + */ +const v1 = { + attributes: { + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + caption: { + type: 'array', + source: 'children', + selector: 'figcaption', + }, + href: { + type: 'string', + source: 'attribute', + selector: 'a', + attribute: 'href', + }, + id: { + type: 'number', + }, + align: { + type: 'string', + }, + width: { + type: 'number', + }, + height: { + type: 'number', + }, }, - width: { - type: 'number', + save( { attributes } ) { + const { url, alt, caption, align, href, width, height } = attributes; + const extraImageProps = width || height ? { width, height } : {}; + const image = {; + + let figureStyle = {}; + + if ( width ) { + figureStyle = { width }; + } else if ( align === 'left' || align === 'right' ) { + figureStyle = { maxWidth: '50%' }; + } + + return ( +
      + { href ? { image } : image } + { ! RichText.isEmpty( caption ) && ( + + ) } +
      + ); }, - height: { - type: 'number', +}; + +/** + * Deprecation for adding the `is-resized` class to the image block to fix + * captions on resized images. + * + * @see https://github.com/WordPress/gutenberg/pull/6496 + */ +const v2 = { + attributes: { + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + caption: { + type: 'array', + source: 'children', + selector: 'figcaption', + }, + href: { + type: 'string', + source: 'attribute', + selector: 'a', + attribute: 'href', + }, + id: { + type: 'number', + }, + align: { + type: 'string', + }, + width: { + type: 'number', + }, + height: { + type: 'number', + }, }, - sizeSlug: { - type: 'string', + save( { attributes } ) { + const { url, alt, caption, align, href, width, height, id } = + attributes; + + const image = ( + { + ); + + return ( +
      + { href ? { image } : image } + { ! RichText.isEmpty( caption ) && ( + + ) } +
      + ); }, - linkDestination: { - type: 'string', +}; + +/** + * Deprecation for image floats including a wrapping div. + * + * @see https://github.com/WordPress/gutenberg/pull/7721 + */ +const v3 = { + attributes: { + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + caption: { + type: 'array', + source: 'children', + selector: 'figcaption', + }, + href: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'href', + }, + id: { + type: 'number', + }, + align: { + type: 'string', + }, + width: { + type: 'number', + }, + height: { + type: 'number', + }, + linkDestination: { + type: 'string', + default: 'none', + }, }, - linkTarget: { - type: 'string', - source: 'attribute', - selector: 'figure > a', - attribute: 'target', + save( { attributes } ) { + const { url, alt, caption, align, href, width, height, id } = + attributes; + + const classes = classnames( { + [ `align${ align }` ]: align, + 'is-resized': width || height, + } ); + + const image = ( + { + ); + + return ( +
      + { href ? { image } : image } + { ! RichText.isEmpty( caption ) && ( + + ) } +
      + ); }, }; -const blockSupports = { - anchor: true, - color: { - __experimentalDuotone: 'img', - text: false, - background: false, - }, - __experimentalBorder: { - radius: true, - __experimentalDefaultControls: { - radius: true, +/** + * Deprecation for removing the outer div wrapper around aligned images. + * + * @see https://github.com/WordPress/gutenberg/pull/38657 + */ +const v4 = { + attributes: { + align: { + type: 'string', + }, + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + caption: { + type: 'string', + source: 'html', + selector: 'figcaption', + }, + title: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'title', + }, + href: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'href', + }, + rel: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'rel', + }, + linkClass: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'class', + }, + id: { + type: 'number', + }, + width: { + type: 'number', + }, + height: { + type: 'number', + }, + sizeSlug: { + type: 'string', + }, + linkDestination: { + type: 'string', + }, + linkTarget: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'target', }, }, -}; + supports: { + anchor: true, + }, + save( { attributes } ) { + const { + url, + alt, + caption, + align, + href, + rel, + linkClass, + width, + height, + id, + linkTarget, + sizeSlug, + title, + } = attributes; -const deprecated = [ - // The following deprecation moves existing border radius styles onto the - // inner img element where new border block support styles must be applied. - // It will also add a new `.has-custom-border` class for existing blocks - // with border radii set. This class is required to improve caption position - // and styling when an image within a gallery has a custom border or - // rounded corners. - // - // See: https://github.com/WordPress/gutenberg/pull/31366/ - { - attributes: blockAttributes, - supports: blockSupports, - save( { attributes } ) { - const { - url, - alt, - caption, - align, - href, - rel, - linkClass, - width, - height, - id, - linkTarget, - sizeSlug, - title, - } = attributes; - - const newRel = ! rel ? undefined : rel; - - const classes = classnames( { - [ `align${ align }` ]: align, - [ `size-${ sizeSlug }` ]: sizeSlug, - 'is-resized': width || height, - } ); - - const image = ( - { - ); + const newRel = ! rel ? undefined : rel; - const figure = ( - <> - { href ? ( - - { image } - - ) : ( - image - ) } - { ! RichText.isEmpty( caption ) && ( - - ) } - - ); + const classes = classnames( { + [ `align${ align }` ]: align, + [ `size-${ sizeSlug }` ]: sizeSlug, + 'is-resized': width || height, + } ); + + const image = ( + { + ); + + const figure = ( + <> + { href ? ( + + { image } + + ) : ( + image + ) } + { ! RichText.isEmpty( caption ) && ( + + ) } + + ); + if ( 'left' === align || 'right' === align || 'center' === align ) { return ( -
      - { figure } -
      +
      +
      { figure }
      +
      ); + } + + return ( +
      + { figure } +
      + ); + }, +}; + +/** + * Deprecation for moving existing border radius styles onto the inner img + * element where new border block support styles must be applied. + * It will also add a new `.has-custom-border` class for existing blocks + * with border radii set. This class is required to improve caption position + * and styling when an image within a gallery has a custom border or + * rounded corners. + * + * @see https://github.com/WordPress/gutenberg/pull/31366 + */ +const v5 = { + attributes: { + align: { + type: 'string', + }, + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + caption: { + type: 'string', + source: 'html', + selector: 'figcaption', + }, + title: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'title', + }, + href: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'href', + }, + rel: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'rel', + }, + linkClass: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'class', + }, + id: { + type: 'number', + }, + width: { + type: 'number', + }, + height: { + type: 'number', + }, + sizeSlug: { + type: 'string', + }, + linkDestination: { + type: 'string', + }, + linkTarget: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'target', }, }, - { - attributes: { - ...blockAttributes, - title: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'title', + supports: { + anchor: true, + color: { + __experimentalDuotone: 'img', + text: false, + background: false, + }, + __experimentalBorder: { + radius: true, + __experimentalDefaultControls: { + radius: true, }, - sizeSlug: { - type: 'string', + }, + __experimentalStyle: { + spacing: { + margin: '0 0 1em 0', }, }, - supports: blockSupports, - save( { attributes } ) { - const { - url, - alt, - caption, - align, - href, - rel, - linkClass, - width, - height, - id, - linkTarget, - sizeSlug, - title, - } = attributes; - - const newRel = ! rel ? undefined : rel; - - const classes = classnames( { - [ `align${ align }` ]: align, - [ `size-${ sizeSlug }` ]: sizeSlug, - 'is-resized': width || height, - } ); - - const image = ( - { - ); + }, + save( { attributes } ) { + const { + url, + alt, + caption, + align, + href, + rel, + linkClass, + width, + height, + id, + linkTarget, + sizeSlug, + title, + } = attributes; - const figure = ( - <> - { href ? ( - - { image } - - ) : ( - image - ) } - { ! RichText.isEmpty( caption ) && ( - - ) } - - ); + const newRel = ! rel ? undefined : rel; - if ( 'left' === align || 'right' === align || 'center' === align ) { - return ( -
      -
      { figure }
      -
      - ); - } + const classes = classnames( { + [ `align${ align }` ]: align, + [ `size-${ sizeSlug }` ]: sizeSlug, + 'is-resized': width || height, + } ); - return ( -
      - { figure } -
      - ); - }, - }, - { - attributes: blockAttributes, - save( { attributes } ) { - const { url, alt, caption, align, href, width, height, id } = - attributes; - - const classes = classnames( { - [ `align${ align }` ]: align, - 'is-resized': width || height, - } ); - - const image = ( - { - ); + const image = ( + { + ); - return ( -
      - { href ? { image } : image } - { ! RichText.isEmpty( caption ) && ( - - ) } -
      - ); - }, - }, - { - attributes: blockAttributes, - save( { attributes } ) { - const { url, alt, caption, align, href, width, height, id } = - attributes; - - const image = ( - { - ); + const figure = ( + <> + { href ? ( + + { image } + + ) : ( + image + ) } + { ! RichText.isEmpty( caption ) && ( + + ) } + + ); - return ( -
      - { href ? { image } : image } - { ! RichText.isEmpty( caption ) && ( - - ) } -
      - ); - }, + return ( +
      + { figure } +
      + ); }, - { - attributes: blockAttributes, - save( { attributes } ) { - const { url, alt, caption, align, href, width, height } = - attributes; - const extraImageProps = width || height ? { width, height } : {}; - const image = ( - { - ); +}; - let figureStyle = {}; +/** + * Deprecation for adding width and height as style rules on the inner img. + * It also updates the widht and height attributes to be strings instead of numbers. + * + * @see https://github.com/WordPress/gutenberg/pull/31366 + */ +const v6 = { + save( { attributes } ) { + const { + url, + alt, + caption, + align, + href, + rel, + linkClass, + width, + height, + aspectRatio, + scale, + id, + linkTarget, + sizeSlug, + title, + } = attributes; - if ( width ) { - figureStyle = { width }; - } else if ( align === 'left' || align === 'right' ) { - figureStyle = { maxWidth: '50%' }; - } + const newRel = ! rel ? undefined : rel; + const borderProps = getBorderClassesAndStyles( attributes ); - return ( -
      - { href ? { image } : image } - { ! RichText.isEmpty( caption ) && ( - - ) } -
      - ); - }, + const classes = classnames( { + [ `align${ align }` ]: align, + [ `size-${ sizeSlug }` ]: sizeSlug, + 'is-resized': width || height, + 'has-custom-border': + !! borderProps.className || + ( borderProps.style && + Object.keys( borderProps.style ).length > 0 ), + } ); + + const imageClasses = classnames( borderProps.className, { + [ `wp-image-${ id }` ]: !! id, + } ); + + const image = ( + { + ); + + const figure = ( + <> + { href ? ( + + { image } + + ) : ( + image + ) } + { ! RichText.isEmpty( caption ) && ( + + ) } + + ); + + return ( +
      + { figure } +
      + ); }, -]; +}; -export default deprecated; +export default [ v6, v5, v4, v3, v2, v1 ]; diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js index 95e8803dd6785..6fa8c6b2342f3 100644 --- a/packages/block-library/src/image/save.js +++ b/packages/block-library/src/image/save.js @@ -58,6 +58,8 @@ export default function save( { attributes } ) { ...borderProps.style, aspectRatio, objectFit: scale, + width, + height, } } width={ width } height={ height } diff --git a/packages/block-library/src/list/edit.js b/packages/block-library/src/list/edit.js index 24d5ead74c47d..7c8c15e05fe87 100644 --- a/packages/block-library/src/list/edit.js +++ b/packages/block-library/src/list/edit.js @@ -177,10 +177,12 @@ export default function Edit( { attributes, setAttributes, clientId, style } ) { { controls } { ordered && ( ) } diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index b91015313f403..96d6f16926a66 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -181,6 +181,10 @@ export default function NavigationLinkEdit( { const itemLabelPlaceholder = __( 'Add label…' ); const ref = useRef(); + // Change the label using inspector causes rich text to change focus on firefox. + // This is a workaround to keep the focus on the label field when label filed is focused we don't render the rich text. + const [ isLabelFieldFocused, setIsLabelFieldFocused ] = useState( false ); + const { innerBlocks, isAtMaxNesting, @@ -424,6 +428,8 @@ export default function NavigationLinkEdit( { } } label={ __( 'Label' ) } autoComplete="off" + onFocus={ () => setIsLabelFieldFocused( true ) } + onBlur={ () => setIsLabelFieldFocused( false ) } /> ) : ( <> - { ! isInvalid && ! isDraft && ( - <> - - setAttributes( { - label: labelValue, - } ) - } - onMerge={ mergeBlocks } - onReplace={ onReplace } - __unstableOnSplitAtEnd={ () => - insertBlocksAfter( - createBlock( - 'core/navigation-link' + { ! isInvalid && + ! isDraft && + ! isLabelFieldFocused && ( + <> + + setAttributes( { + label: labelValue, + } ) + } + onMerge={ mergeBlocks } + onReplace={ onReplace } + __unstableOnSplitAtEnd={ () => + insertBlocksAfter( + createBlock( + 'core/navigation-link' + ) ) - ) - } - aria-label={ __( - 'Navigation link text' - ) } - placeholder={ itemLabelPlaceholder } - withoutInteractiveFormatting - allowedFormats={ [ - 'core/bold', - 'core/italic', - 'core/image', - 'core/strikethrough', - ] } - onClick={ () => { - if ( ! url ) { - setIsLinkOpen( true ); } - } } - /> - { description && ( - - { description } - - ) } - - ) } - { ( isInvalid || isDraft ) && ( + aria-label={ __( + 'Navigation link text' + ) } + placeholder={ itemLabelPlaceholder } + withoutInteractiveFormatting + allowedFormats={ [ + 'core/bold', + 'core/italic', + 'core/image', + 'core/strikethrough', + ] } + onClick={ () => { + if ( ! url ) { + setIsLinkOpen( true ); + } + } } + /> + { description && ( + + { description } + + ) } + + ) } + { ( isInvalid || + isDraft || + isLabelFieldFocused ) && (
      diff --git a/packages/block-library/src/post-template/index.php b/packages/block-library/src/post-template/index.php index 3c023c80ed263..b1499d845f39a 100644 --- a/packages/block-library/src/post-template/index.php +++ b/packages/block-library/src/post-template/index.php @@ -97,11 +97,13 @@ function render_block_core_post_template( $attributes, $content, $block ) { $context['postId'] = $post_id; return $context; }; - add_filter( 'render_block_context', $filter_block_context ); + + // Use an early priority to so that other 'render_block_context' filters have access to the values. + add_filter( 'render_block_context', $filter_block_context, 1 ); // Render the inner blocks of the Post Template block with `dynamic` set to `false` to prevent calling // `render_callback` and ensure that no wrapper markup is included. $block_content = ( new WP_Block( $block_instance ) )->render( array( 'dynamic' => false ) ); - remove_filter( 'render_block_context', $filter_block_context ); + remove_filter( 'render_block_context', $filter_block_context, 1 ); // Wrap the render inner blocks in a `li` element with the appropriate post classes. $post_classes = implode( ' ', get_post_class( 'wp-block-post' ) ); diff --git a/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap index 5b5df918f2bee..65d87d5b0d7bd 100644 --- a/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap +++ b/packages/block-library/src/quote/test/__snapshots__/transforms.native.js.snap @@ -22,6 +22,16 @@ exports[`Quote block transforms to Group block 1`] = ` " `; +exports[`Quote block transforms to Paragraph block 1`] = ` +" +

      "This will make running your own blog a viable alternative again."

      + + + +

      Adrian Zumbrunnen

      +" +`; + exports[`Quote block transforms to Pullquote block 1`] = ` "

      "This will make running your own blog a viable alternative again."

      Adrian Zumbrunnen
      diff --git a/packages/block-library/src/quote/test/transforms.native.js b/packages/block-library/src/quote/test/transforms.native.js index 46c4eb2b6f972..25030e0a018d4 100644 --- a/packages/block-library/src/quote/test/transforms.native.js +++ b/packages/block-library/src/quote/test/transforms.native.js @@ -21,7 +21,11 @@ const initialHtml = ` `; const transformsWithInnerBlocks = [ 'Columns', 'Group' ]; -const blockTransforms = [ 'Pullquote', ...transformsWithInnerBlocks ]; +const blockTransforms = [ + 'Pullquote', + 'Paragraph', + ...transformsWithInnerBlocks, +]; setupCoreBlocks(); diff --git a/packages/block-library/src/quote/transforms.js b/packages/block-library/src/quote/transforms.js index d4cd77177bf03..4e153a6399029 100644 --- a/packages/block-library/src/quote/transforms.js +++ b/packages/block-library/src/quote/transforms.js @@ -109,6 +109,19 @@ const transforms = { } ); }, }, + { + type: 'block', + blocks: [ 'core/paragraph' ], + transform: ( { citation }, innerBlocks ) => + citation + ? [ + ...innerBlocks, + createBlock( 'core/paragraph', { + content: citation, + } ), + ] + : innerBlocks, + }, { type: 'block', blocks: [ 'core/group' ], diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index da048944f1498..6cc1e021841b4 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -191,9 +191,10 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { const updateFootnotes = useCallback( ( _blocks ) => { - if ( ! meta ) return; + const output = { blocks: _blocks }; + if ( ! meta ) return output; // If meta.footnotes is empty, it means the meta is not registered. - if ( meta.footnotes === undefined ) return {}; + if ( meta.footnotes === undefined ) return output; const { getRichTextValues } = unlock( blockEditorPrivateApis ); const _content = getRichTextValues( _blocks ).join( '' ) || ''; @@ -215,7 +216,8 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { : []; const currentOrder = footnotes.map( ( fn ) => fn.id ); - if ( currentOrder.join( '' ) === newOrder.join( '' ) ) return; + if ( currentOrder.join( '' ) === newOrder.join( '' ) ) + return output; const newFootnotes = newOrder.map( ( fnId ) => @@ -226,6 +228,71 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { } ); + function updateAttributes( attributes ) { + attributes = { ...attributes }; + + for ( const key in attributes ) { + const value = attributes[ key ]; + + if ( Array.isArray( value ) ) { + attributes[ key ] = value.map( updateAttributes ); + continue; + } + + if ( typeof value !== 'string' ) { + continue; + } + + if ( value.indexOf( 'data-fn' ) === -1 ) { + continue; + } + + // When we store rich text values, this would no longer + // require a regex. + const regex = + /(]+data-fn="([^"]+)"[^>]*>]*>)[\d*]*<\/a><\/sup>/g; + + attributes[ key ] = value.replace( + regex, + ( match, opening, fnId ) => { + const index = newOrder.indexOf( fnId ); + return `${ opening }${ index + 1 }`; + } + ); + + const compatRegex = + /]+data-fn="([^"]+)"[^>]*>\*<\/a>/g; + + attributes[ key ] = attributes[ key ].replace( + compatRegex, + ( match, fnId ) => { + const index = newOrder.indexOf( fnId ); + return `${ + index + 1 + }`; + } + ); + } + + return attributes; + } + + function updateBlocksAttributes( __blocks ) { + return __blocks.map( ( block ) => { + return { + ...block, + attributes: updateAttributes( block.attributes ), + innerBlocks: updateBlocksAttributes( + block.innerBlocks + ), + }; + } ); + } + + // We need to go through all block attributs deeply and update the + // footnote anchor numbering (textContent) to match the new order. + const newBlocks = updateBlocksAttributes( _blocks ); + oldFootnotes = { ...oldFootnotes, ...footnotes.reduce( ( acc, fn ) => { @@ -241,6 +308,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { ...meta, footnotes: JSON.stringify( newFootnotes ), }, + blocks: newBlocks, }; }, [ meta ] @@ -258,7 +326,6 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { // to make sure the edit makes the post dirty and creates // a new undo level. const edits = { - blocks: newBlocks, selection, content: ( { blocks: blocksForSerialization = [] } ) => __unstableSerializeAndClean( blocksForSerialization ), @@ -282,7 +349,7 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { ( newBlocks, options ) => { const { selection } = options; const footnotesChanges = updateFootnotes( newBlocks ); - const edits = { blocks: newBlocks, selection, ...footnotesChanges }; + const edits = { selection, ...footnotesChanges }; editEntityRecord( kind, name, id, edits, { isCached: true } ); }, diff --git a/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php b/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php index ad98354dd45dc..3f24a6e25cfcb 100644 --- a/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php +++ b/packages/e2e-tests/plugins/iframed-enqueue-block-assets.php @@ -17,5 +17,18 @@ static function() { filemtime( plugin_dir_path( __FILE__ ) . 'iframed-enqueue-block-assets/style.css' ) ); wp_add_inline_style( 'iframed-enqueue-block-assets', 'body{padding:20px!important}' ); + wp_enqueue_script( + 'iframed-enqueue-block-assets-script', + plugin_dir_url( __FILE__ ) . 'iframed-enqueue-block-assets/script.js', + array(), + filemtime( plugin_dir_path( __FILE__ ) . 'iframed-enqueue-block-assets/script.js' ) + ); + wp_localize_script( + 'iframed-enqueue-block-assets-script', + 'iframedEnqueueBlockAssetsL10n', + array( + 'test' => 'Iframed Enqueue Block Assets!', + ) + ); } ); diff --git a/packages/e2e-tests/plugins/iframed-enqueue-block-assets/script.js b/packages/e2e-tests/plugins/iframed-enqueue-block-assets/script.js new file mode 100644 index 0000000000000..f0eddd65c70eb --- /dev/null +++ b/packages/e2e-tests/plugins/iframed-enqueue-block-assets/script.js @@ -0,0 +1,3 @@ +window.addEventListener( 'load', () => { + document.body.dataset.iframedEnqueueBlockAssetsL10n = window.iframedEnqueueBlockAssetsL10n.test; +} ); diff --git a/packages/e2e-tests/specs/editor/plugins/iframed-equeue-block-assets.test.js b/packages/e2e-tests/specs/editor/plugins/iframed-equeue-block-assets.test.js index c1bd26fe1c761..c29af593abb12 100644 --- a/packages/e2e-tests/specs/editor/plugins/iframed-equeue-block-assets.test.js +++ b/packages/e2e-tests/specs/editor/plugins/iframed-equeue-block-assets.test.js @@ -32,6 +32,7 @@ describe( 'iframed inline styles', () => { } ); it( 'should load styles added through enqueue_block_assets', async () => { + await page.waitForSelector( 'iframe[name="editor-canvas"]' ); // Check stylesheet. expect( await getComputedStyle( canvas(), 'body', 'background-color' ) @@ -40,5 +41,11 @@ describe( 'iframed inline styles', () => { expect( await getComputedStyle( canvas(), 'body', 'padding' ) ).toBe( '20px' ); + + expect( + await canvas().evaluate( () => ( { ...document.body.dataset } ) ) + ).toEqual( { + iframedEnqueueBlockAssetsL10n: 'Iframed Enqueue Block Assets!', + } ); } ); } ); diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 0c18521b1215f..15bc017900daa 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -16,10 +16,7 @@ import { store as editorStore, } from '@wordpress/editor'; import { useSelect, useDispatch } from '@wordpress/data'; -import { - BlockBreadcrumb, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; +import { BlockBreadcrumb } from '@wordpress/block-editor'; import { Button, ScrollLock, Popover } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { PluginArea } from '@wordpress/plugins'; @@ -52,9 +49,6 @@ import WelcomeGuide from '../welcome-guide'; import ActionsPanel from './actions-panel'; import StartPageOptions from '../start-page-options'; import { store as editPostStore } from '../../store'; -import { unlock } from '../../lock-unlock'; - -const { BlockRemovalWarningModal } = unlock( blockEditorPrivateApis ); const interfaceLabels = { /* translators: accessibility text for the editor top bar landmark region. */ @@ -69,12 +63,6 @@ const interfaceLabels = { footer: __( 'Editor footer' ), }; -const blockRemovalRules = { - 'core/footnotes': __( - 'The Footnotes block displays all footnotes found in the content. Note that any footnotes in the content will persist after removing this block.' - ), -}; - function Layout( { styles } ) { const isMobileViewport = useViewportMatch( 'medium', '<' ); const isHugeViewport = useViewportMatch( 'huge', '>=' ); @@ -214,7 +202,6 @@ function Layout( { styles } ) { - { + const { canUser } = select( coreStore ); + const { getEditorSettings } = select( editorStore ); + + const isBlockTheme = getEditorSettings().__unstableIsBlockBasedTheme; + const defaultUrl = addQueryArgs( 'edit.php', { + post_type: 'wp_block', + } ); + const patternsUrl = addQueryArgs( 'site-editor.php', { + path: '/patterns', + } ); + + // The site editor and templates both check whether the user has + // edit_theme_options capabilities. We can leverage that here and not + // display the manage patterns link if the user can't access it. + return canUser( 'read', 'templates' ) && isBlockTheme + ? patternsUrl + : defaultUrl; + }, [] ); + + return ( + + { __( 'Manage Patterns' ) } + + ); +} + registerPlugin( 'edit-post', { render() { return ( @@ -22,14 +53,7 @@ registerPlugin( 'edit-post', { { ( { onClose } ) => ( <> - - { __( 'Manage Patterns' ) } - + diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index d33a5ae22c0fd..fac5deef18f71 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -41,13 +41,7 @@ } } -// In order to use mix-blend-mode, this element needs to have an explicitly set background-color -// We scope it to .wp-toolbar to be wp-admin only, to prevent bleed into other implementations -html.wp-toolbar { - background: $white; -} - -body.block-editor-page { +body.js.block-editor-page { @include wp-admin-reset( ".block-editor" ); } diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index 34c23cc699bc2..a8bbe75e0261b 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -88,7 +88,7 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) { enableResizing ? 'min-height:0!important;' : '' }}body{position:relative; ${ canvasMode === 'view' - ? 'cursor: pointer; height: 100vh' + ? 'cursor: pointer; min-height: 100vh;' : '' }}}` } diff --git a/packages/edit-site/src/components/create-pattern-modal/index.js b/packages/edit-site/src/components/create-pattern-modal/index.js index 46d734b86fdd1..753dccfb961dd 100644 --- a/packages/edit-site/src/components/create-pattern-modal/index.js +++ b/packages/edit-site/src/components/create-pattern-modal/index.js @@ -14,6 +14,7 @@ import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { useDispatch } from '@wordpress/data'; +import { serialize } from '@wordpress/blocks'; /** * Internal dependencies @@ -21,9 +22,11 @@ import { useDispatch } from '@wordpress/data'; import { SYNC_TYPES, USER_PATTERN_CATEGORY } from '../page-patterns/utils'; export default function CreatePatternModal( { + blocks = [], closeModal, onCreate, onError, + title, } ) { const [ name, setName ] = useState( '' ); const [ syncType, setSyncType ] = useState( SYNC_TYPES.unsynced ); @@ -52,7 +55,7 @@ export default function CreatePatternModal( { 'wp_block', { title: name || __( 'Untitled Pattern' ), - content: '', + content: blocks?.length ? serialize( blocks ) : '', status: 'publish', meta: syncType === SYNC_TYPES.unsynced @@ -76,7 +79,7 @@ export default function CreatePatternModal( { return ( diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss index 6281887b13738..d26bbdaf28ff6 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss @@ -40,6 +40,10 @@ overflow: hidden; grid-column: 2 / 3; + .block-editor-block-icon { + min-width: $grid-unit-30; + } + h1 { white-space: nowrap; overflow: hidden; diff --git a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js new file mode 100644 index 0000000000000..d2c14d15f341b --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js @@ -0,0 +1,196 @@ +/** + * WordPress dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { + TEMPLATE_PARTS, + PATTERNS, + SYNC_TYPES, + USER_PATTERNS, + USER_PATTERN_CATEGORY, +} from './utils'; +import { + useExistingTemplateParts, + getUniqueTemplatePartTitle, + getCleanTemplatePartSlug, +} from '../../utils/template-part-create'; +import { unlock } from '../../lock-unlock'; + +const { useHistory } = unlock( routerPrivateApis ); + +function getPatternMeta( item ) { + if ( item.type === PATTERNS ) { + return { wp_pattern_sync_status: SYNC_TYPES.unsynced }; + } + + const syncStatus = item.reusableBlock.wp_pattern_sync_status; + const isUnsynced = syncStatus === SYNC_TYPES.unsynced; + + return { + ...item.reusableBlock.meta, + wp_pattern_sync_status: isUnsynced ? syncStatus : undefined, + }; +} + +export default function DuplicateMenuItem( { + categoryId, + item, + label = __( 'Duplicate' ), + onClose, +} ) { + const { saveEntityRecord } = useDispatch( coreStore ); + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + + const history = useHistory(); + const existingTemplateParts = useExistingTemplateParts(); + + async function createTemplatePart() { + try { + const copiedTitle = sprintf( + /* translators: %s: Existing template part title */ + __( '%s (Copy)' ), + item.title + ); + const title = getUniqueTemplatePartTitle( + copiedTitle, + existingTemplateParts + ); + const slug = getCleanTemplatePartSlug( title ); + const { area, content } = item.templatePart; + + const result = await saveEntityRecord( + 'postType', + 'wp_template_part', + { slug, title, content, area }, + { throwOnError: true } + ); + + createSuccessNotice( + sprintf( + // translators: %s: The new template part's title e.g. 'Call to action (copy)'. + __( '"%s" created.' ), + title + ), + { + type: 'snackbar', + id: 'edit-site-patterns-success', + actions: [ + { + label: __( 'Edit' ), + onClick: () => + history.push( { + postType: TEMPLATE_PARTS, + postId: result?.id, + categoryType: TEMPLATE_PARTS, + categoryId, + } ), + }, + ], + } + ); + + onClose(); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( + 'An error occurred while creating the template part.' + ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + id: 'edit-site-patterns-error', + } ); + onClose(); + } + } + + async function createPattern() { + try { + const isThemePattern = item.type === PATTERNS; + const title = sprintf( + /* translators: %s: Existing pattern title */ + __( '%s (Copy)' ), + item.title + ); + + const result = await saveEntityRecord( + 'postType', + 'wp_block', + { + content: isThemePattern + ? item.content + : item.reusableBlock.content, + meta: getPatternMeta( item ), + status: 'publish', + title, + }, + { throwOnError: true } + ); + + const actionLabel = isThemePattern + ? __( 'View my patterns' ) + : __( 'Edit' ); + + const newLocation = isThemePattern + ? { + categoryType: USER_PATTERNS, + categoryId: USER_PATTERN_CATEGORY, + path: '/patterns', + } + : { + categoryType: USER_PATTERNS, + categoryId: USER_PATTERN_CATEGORY, + postType: USER_PATTERNS, + postId: result?.id, + }; + + createSuccessNotice( + sprintf( + // translators: %s: The new pattern's title e.g. 'Call to action (copy)'. + __( '"%s" added to my patterns.' ), + title + ), + { + type: 'snackbar', + id: 'edit-site-patterns-success', + actions: [ + { + label: actionLabel, + onClick: () => history.push( newLocation ), + }, + ], + } + ); + + onClose(); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while creating the pattern.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + id: 'edit-site-patterns-error', + } ); + onClose(); + } + } + + const createItem = + item.type === TEMPLATE_PARTS ? createTemplatePart : createPattern; + + return { label }; +} diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js index 7db14e1d37788..441529e1c0583 100644 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -8,87 +8,103 @@ import classnames from 'classnames'; */ import { BlockPreview } from '@wordpress/block-editor'; import { + Button, __experimentalConfirmDialog as ConfirmDialog, DropdownMenu, MenuGroup, MenuItem, __experimentalHeading as Heading, __experimentalHStack as HStack, - __unstableCompositeItem as CompositeItem, Tooltip, Flex, } from '@wordpress/components'; import { useDispatch } from '@wordpress/data'; -import { useState, useId } from '@wordpress/element'; +import { useState, useId, memo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { Icon, header, footer, - symbolFilled, + symbolFilled as uncategorized, + symbol, moreHorizontal, lockSmall, } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; -import { DELETE, BACKSPACE } from '@wordpress/keycodes'; /** * Internal dependencies */ -import { PATTERNS, USER_PATTERNS } from './utils'; +import RenameMenuItem from './rename-menu-item'; +import DuplicateMenuItem from './duplicate-menu-item'; +import { PATTERNS, TEMPLATE_PARTS, USER_PATTERNS, SYNC_TYPES } from './utils'; +import { store as editSiteStore } from '../../store'; import { useLink } from '../routes/link'; -const THEME_PATTERN_TOOLTIP = __( 'Theme patterns cannot be edited.' ); +const templatePartIcons = { header, footer, uncategorized }; -export default function GridItem( { categoryId, composite, icon, item } ) { +function GridItem( { categoryId, item, ...props } ) { const descriptionId = useId(); const [ isDeleteDialogOpen, setIsDeleteDialogOpen ] = useState( false ); + const { removeTemplate } = useDispatch( editSiteStore ); const { __experimentalDeleteReusableBlock } = useDispatch( reusableBlocksStore ); const { createErrorNotice, createSuccessNotice } = useDispatch( noticesStore ); + const isUserPattern = item.type === USER_PATTERNS; + const isNonUserPattern = item.type === PATTERNS; + const isTemplatePart = item.type === TEMPLATE_PARTS; + const { onClick } = useLink( { postType: item.type, - postId: item.type === USER_PATTERNS ? item.id : item.name, + postId: isUserPattern ? item.id : item.name, categoryId, categoryType: item.type, } ); - const onKeyDown = ( event ) => { - if ( DELETE === event.keyCode || BACKSPACE === event.keyCode ) { - setIsDeleteDialogOpen( true ); - } - }; - const isEmpty = ! item.blocks?.length; const patternClassNames = classnames( 'edit-site-patterns__pattern', { 'is-placeholder': isEmpty, } ); const previewClassNames = classnames( 'edit-site-patterns__preview', { - 'is-inactive': item.type === PATTERNS, + 'is-inactive': isNonUserPattern, } ); const deletePattern = async () => { try { await __experimentalDeleteReusableBlock( item.id ); - createSuccessNotice( __( 'Pattern successfully deleted.' ), { - type: 'snackbar', - } ); + createSuccessNotice( + sprintf( + // translators: %s: The pattern's title e.g. 'Call to action'. + __( '"%s" deleted.' ), + item.title + ), + { type: 'snackbar', id: 'edit-site-patterns-success' } + ); } catch ( error ) { const errorMessage = error.message && error.code !== 'unknown_error' ? error.message : __( 'An error occurred while deleting the pattern.' ); - createErrorNotice( errorMessage, { type: 'snackbar' } ); + createErrorNotice( errorMessage, { + type: 'snackbar', + id: 'edit-site-patterns-error', + } ); } }; + const deleteItem = () => + isTemplatePart ? removeTemplate( item ) : deletePattern(); - const isUserPattern = item.type === USER_PATTERNS; + // Only custom patterns or custom template parts can be renamed or deleted. + const isCustomPattern = + isUserPattern || ( isTemplatePart && item.isCustom ); + const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file; const ariaDescriptions = []; - if ( isUserPattern ) { + + if ( isCustomPattern ) { // User patterns don't have descriptions, but can be edited and deleted, so include some help text. ariaDescriptions.push( __( 'Press Enter to edit, or Delete to delete the pattern.' ) @@ -96,139 +112,173 @@ export default function GridItem( { categoryId, composite, icon, item } ) { } else if ( item.description ) { ariaDescriptions.push( item.description ); } - if ( item.type === PATTERNS ) { - ariaDescriptions.push( THEME_PATTERN_TOOLTIP ); - } - let itemIcon = icon; - if ( categoryId === 'header' ) { - itemIcon = header; - } else if ( categoryId === 'footer' ) { - itemIcon = footer; - } else if ( categoryId === 'uncategorized' ) { - itemIcon = symbolFilled; + if ( isNonUserPattern ) { + ariaDescriptions.push( __( 'Theme patterns cannot be edited.' ) ); } + const itemIcon = + templatePartIcons[ categoryId ] || + ( item.syncStatus === SYNC_TYPES.full ? symbol : undefined ); + + const confirmButtonText = hasThemeFile ? __( 'Clear' ) : __( 'Delete' ); + const confirmPrompt = hasThemeFile + ? __( 'Are you sure you want to clear these customizations?' ) + : sprintf( + // translators: %s: The pattern or template part's title e.g. 'Call to action'. + __( 'Are you sure you want to delete "%s"?' ), + item.title + ); + return ( - <> -
      - - `${ descriptionId }-${ index }` - ) - .join( ' ' ) - : undefined - } +
    2. + + { ariaDescriptions.map( ( ariaDescription, index ) => ( + + ) ) } + - { item.type === USER_PATTERNS && ( - - { () => ( - - - setIsDeleteDialogOpen( true ) - } - > - { __( 'Delete' ) } - - + + { isCustomPattern && ( + + setIsDeleteDialogOpen( true ) + } + > + { hasThemeFile + ? __( 'Clear customizations' ) + : __( 'Delete' ) } + ) } - + ) } - - + + + { isDeleteDialogOpen && ( setIsDeleteDialogOpen( false ) } > - { __( 'Are you sure you want to delete this pattern?' ) } + { confirmPrompt } ) } - +
    3. ); } + +export default memo( GridItem ); diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js index 3f6e5fd01f72f..1902b36982c14 100644 --- a/packages/edit-site/src/components/page-patterns/grid.js +++ b/packages/edit-site/src/components/page-patterns/grid.js @@ -1,39 +1,52 @@ /** * WordPress dependencies */ -import { - __unstableComposite as Composite, - __unstableUseCompositeState as useCompositeState, -} from '@wordpress/components'; +import { __experimentalText as Text } from '@wordpress/components'; +import { useRef } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import GridItem from './grid-item'; -export default function Grid( { categoryId, label, icon, items } ) { - const composite = useCompositeState( { orientation: 'vertical' } ); +const PAGE_SIZE = 100; + +export default function Grid( { categoryId, items, ...props } ) { + const gridRef = useRef(); if ( ! items?.length ) { return null; } + const list = items.slice( 0, PAGE_SIZE ); + const restLength = items.length - PAGE_SIZE; + return ( - - { items.map( ( item ) => ( - - ) ) } - + <> + + { restLength > 0 && ( + + { sprintf( + /* translators: %d: number of patterns */ + __( '+ %d more patterns discoverable by searching' ), + restLength + ) } + + ) } + ); } diff --git a/packages/edit-site/src/components/page-patterns/header.js b/packages/edit-site/src/components/page-patterns/header.js new file mode 100644 index 0000000000000..1237b85d6c978 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/header.js @@ -0,0 +1,69 @@ +/** + * WordPress dependencies + */ +import { + __experimentalVStack as VStack, + __experimentalHeading as Heading, + __experimentalText as Text, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { store as editorStore } from '@wordpress/editor'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories'; +import { + USER_PATTERN_CATEGORY, + USER_PATTERNS, + TEMPLATE_PARTS, + PATTERNS, +} from './utils'; + +export default function PatternsHeader( { + categoryId, + type, + titleId, + descriptionId, +} ) { + const { patternCategories } = usePatternCategories(); + const templatePartAreas = useSelect( + ( select ) => + select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + [] + ); + + let title, description; + if ( categoryId === USER_PATTERN_CATEGORY && type === USER_PATTERNS ) { + title = __( 'My Patterns' ); + description = ''; + } else if ( type === TEMPLATE_PARTS ) { + const templatePartArea = templatePartAreas.find( + ( area ) => area.area === categoryId + ); + title = templatePartArea?.label; + description = templatePartArea?.description; + } else if ( type === PATTERNS ) { + const patternCategory = patternCategories.find( + ( category ) => category.name === categoryId + ); + title = patternCategory?.label; + description = patternCategory?.description; + } + + if ( ! title ) return null; + + return ( + + + { title } + + { description ? ( + + { description } + + ) : null } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 961ed51f39e5d..d90fc74844244 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -32,7 +32,12 @@ export default function PagePatterns() { title={ __( 'Patterns content' ) } hideTitleFromUI > - + ); diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index 545ffdb044275..7bf2a9d506584 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -1,51 +1,87 @@ /** * WordPress dependencies */ - +import { useState, useDeferredValue, useId } from '@wordpress/element'; import { SearchControl, - __experimentalHeading as Heading, - __experimentalText as Text, __experimentalVStack as VStack, Flex, FlexBlock, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalHeading as Heading, + __experimentalText as Text, } from '@wordpress/components'; import { __, isRTL } from '@wordpress/i18n'; -import { symbol, chevronLeft, chevronRight } from '@wordpress/icons'; +import { chevronLeft, chevronRight } from '@wordpress/icons'; import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { useViewportMatch } from '@wordpress/compose'; +import { useViewportMatch, useAsyncList } from '@wordpress/compose'; /** * Internal dependencies */ +import PatternsHeader from './header'; import Grid from './grid'; import NoPatterns from './no-patterns'; import usePatterns from './use-patterns'; import SidebarButton from '../sidebar-button'; import useDebouncedInput from '../../utils/use-debounced-input'; import { unlock } from '../../lock-unlock'; +import { SYNC_TYPES, USER_PATTERN_CATEGORY } from './utils'; const { useLocation, useHistory } = unlock( routerPrivateApis ); +const SYNC_FILTERS = { + all: __( 'All' ), + [ SYNC_TYPES.full ]: __( 'Synced' ), + [ SYNC_TYPES.unsynced ]: __( 'Standard' ), +}; + +const SYNC_DESCRIPTIONS = { + all: '', + [ SYNC_TYPES.full ]: __( + 'Patterns that are kept in sync across the site.' + ), + [ SYNC_TYPES.unsynced ]: __( + 'Patterns that can be changed freely without affecting the site.' + ), +}; + export default function PatternsList( { categoryId, type } ) { const location = useLocation(); const history = useHistory(); const isMobileViewport = useViewportMatch( 'medium', '<' ); const [ filterValue, setFilterValue, delayedFilterValue ] = useDebouncedInput( '' ); + const deferredFilterValue = useDeferredValue( delayedFilterValue ); - const [ patterns, isResolving ] = usePatterns( - type, - categoryId, - delayedFilterValue - ); + const [ syncFilter, setSyncFilter ] = useState( 'all' ); + const deferredSyncedFilter = useDeferredValue( syncFilter ); + const { patterns, isResolving } = usePatterns( type, categoryId, { + search: deferredFilterValue, + syncStatus: + deferredSyncedFilter === 'all' ? undefined : deferredSyncedFilter, + } ); - const { syncedPatterns, unsyncedPatterns } = patterns; - const hasPatterns = !! syncedPatterns.length || !! unsyncedPatterns.length; + const id = useId(); + const titleId = `${ id }-title`; + const descriptionId = `${ id }-description`; + + const hasPatterns = patterns.length; + const title = SYNC_FILTERS[ syncFilter ]; + const description = SYNC_DESCRIPTIONS[ syncFilter ]; + const shownPatterns = useAsyncList( patterns ); return ( - + + + { isMobileViewport && ( ) } - + setFilterValue( value ) } @@ -71,42 +107,48 @@ export default function PatternsList( { categoryId, type } ) { __nextHasNoMarginBottom /> + { categoryId === USER_PATTERN_CATEGORY && ( + setSyncFilter( value ) } + __nextHasNoMarginBottom + > + { Object.entries( SYNC_FILTERS ).map( + ( [ key, label ] ) => ( + + ) + ) } + + ) } - { isResolving && __( 'Loading' ) } - { ! isResolving && !! syncedPatterns.length && ( - <> - - { __( 'Synced' ) } - - { __( - 'Patterns that are kept in sync across the site' - ) } + { syncFilter !== 'all' && ( + + + { title } + + { description ? ( + + { description } - - - + ) : null } + ) } - { ! isResolving && !! unsyncedPatterns.length && ( - <> - - { __( 'Standard' ) } - - { __( - 'Patterns that can be changed freely without affecting the site' - ) } - - - - + { hasPatterns && ( + ) } { ! isResolving && ! hasPatterns && } diff --git a/packages/edit-site/src/components/page-patterns/rename-menu-item.js b/packages/edit-site/src/components/page-patterns/rename-menu-item.js new file mode 100644 index 0000000000000..938023a62cefd --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/rename-menu-item.js @@ -0,0 +1,115 @@ +/** + * WordPress dependencies + */ +import { + Button, + MenuItem, + Modal, + TextControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { TEMPLATE_PARTS } from './utils'; + +export default function RenameMenuItem( { item, onClose } ) { + const [ title, setTitle ] = useState( () => item.title ); + const [ isModalOpen, setIsModalOpen ] = useState( false ); + + const { editEntityRecord, saveEditedEntityRecord } = + useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + if ( item.type === TEMPLATE_PARTS && ! item.isCustom ) { + return null; + } + + async function onRename( event ) { + event.preventDefault(); + + try { + await editEntityRecord( 'postType', item.type, item.id, { title } ); + + // Update state before saving rerenders the list. + setTitle( '' ); + setIsModalOpen( false ); + onClose(); + + // Persist edited entity. + await saveEditedEntityRecord( 'postType', item.type, item.id, { + throwOnError: true, + } ); + + createSuccessNotice( __( 'Entity renamed.' ), { + type: 'snackbar', + } ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while renaming the entity.' ); + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + } + + return ( + <> + { + setIsModalOpen( true ); + setTitle( item.title ); + } } + > + { __( 'Rename' ) } + + { isModalOpen && ( + { + setIsModalOpen( false ); + onClose(); + } } + overlayClassName="edit-site-list__rename_modal" + > +
      + + + + + + + + + +
      +
      + ) } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index fdf0aea3431f6..79731999f46ef 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -1,6 +1,13 @@ .edit-site-patterns { - background: rgba(0, 0, 0, 0.05); + background: rgba(0, 0, 0, 0.15); margin: $header-height 0 0; + .components-base-control { + width: 100%; + @include break-medium { + width: auto; + } + } + .components-text { color: $gray-600; } @@ -12,33 +19,81 @@ @include break-medium { margin: 0; } + + .edit-site-patterns__search-block { + min-width: fit-content; + flex-grow: 1; + } + + // The increased specificity here is to overcome component styles + // without relying on internal component class names. + .edit-site-patterns__search { + input[type="search"] { + height: $button-size-next-default-40px; + background: $gray-800; + color: $gray-200; + + &:focus { + background: $gray-800; + } + } + + svg { + fill: $gray-600; + } + } + + .edit-site-patterns__sync-status-filter { + background: $gray-800; + border: none; + height: $button-size-next-default-40px; + min-width: max-content; + width: 100%; + max-width: 100%; + + @include break-medium { + width: 300px; + } + } + .edit-site-patterns__sync-status-filter-option:active { + background: $gray-700; + color: $gray-100; + } } -.edit-site-patterns__grid { - column-gap: $grid-unit-30; - @include break-large() { - column-count: 2; +.edit-site-patterns__section-header { + .screen-reader-shortcut:focus { + top: 0; } +} +.edit-site-patterns__grid { + display: grid; + grid-template-columns: 1fr; + gap: $grid-unit-40; // Small top padding required to avoid cutting off the visible outline // when hovering items. padding-top: $border-width-focus-fallback; margin-bottom: $grid-unit-40; - + @include break-large { + grid-template-columns: 1fr 1fr; + } .edit-site-patterns__pattern { break-inside: avoid-column; display: flex; flex-direction: column; - margin-bottom: $grid-unit-60; - .edit-site-patterns__preview { - border-radius: $radius-block-ui; + box-shadow: none; + border: none; + padding: 0; + background-color: unset; + box-sizing: border-box; + border-radius: 4px; cursor: pointer; overflow: hidden; &:focus { - box-shadow: inset 0 0 0 2px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - + box-shadow: inset 0 0 0 0 $white, 0 0 0 2px var(--wp-admin-theme-color); // Windows High Contrast mode will show this outline, but not the box-shadow. outline: 2px solid transparent; } @@ -46,6 +101,10 @@ &.is-inactive { cursor: default; } + &.is-inactive:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $gray-800; + opacity: 0.8; + } } .edit-site-patterns__footer, @@ -68,31 +127,28 @@ } .edit-site-patterns__preview { - flex: 1; + flex: 0 1 auto; margin-bottom: $grid-unit-20; } } -// The increased specificity here is to overcome component styles -// without relying on internal component class names. -.edit-site-patterns__search { - &#{&} input[type="search"] { - background: $gray-800; +.edit-site-patterns__load-more { + align-self: center; +} + +.edit-site-patterns__pattern-title { + color: $gray-200; + + .is-link { + text-decoration: none; color: $gray-200; + &:hover, &:focus { - background: $gray-800; + color: $white; } } - svg { - fill: $gray-600; - } -} - -.edit-site-patterns__pattern-title { - color: $gray-600; - .edit-site-patterns__pattern-icon { border-radius: $grid-unit-05; background: var(--wp-block-synced-color); @@ -101,6 +157,10 @@ .edit-site-patterns__pattern-lock-icon { display: inline-flex; + + svg { + fill: currentcolor; + } } } 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 a394aabf572c4..ea2b8ac976fea 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -4,7 +4,7 @@ import { parse } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { useMemo } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -15,7 +15,6 @@ import { SYNC_TYPES, TEMPLATE_PARTS, USER_PATTERNS, - USER_PATTERN_CATEGORY, filterOutDuplicatesByName, } from './utils'; import { unlock } from '../../lock-unlock'; @@ -31,9 +30,11 @@ const templatePartToPattern = ( templatePart ) => ( { blocks: parse( templatePart.content.raw ), categories: [ templatePart.area ], description: templatePart.description || '', + isCustom: templatePart.source === 'custom', keywords: templatePart.keywords || [], + id: createTemplatePartId( templatePart.theme, templatePart.slug ), name: createTemplatePartId( templatePart.theme, templatePart.slug ), - title: templatePart.title.rendered, + title: decodeEntities( templatePart.title.rendered ), type: templatePart.type, templatePart, } ); @@ -41,106 +42,64 @@ const templatePartToPattern = ( templatePart ) => ( { const templatePartHasCategory = ( item, category ) => item.templatePart.area === category; -const useTemplatePartsAsPatterns = ( - categoryId, - postType = TEMPLATE_PARTS, - filterValue = '' +const selectTemplatePartsAsPatterns = ( + select, + { categoryId, search = '' } = {} ) => { - const { templateParts, isResolving } = useSelect( - ( select ) => { - if ( postType !== TEMPLATE_PARTS ) { - return { - templateParts: EMPTY_PATTERN_LIST, - isResolving: false, - }; - } - - const { getEntityRecords, isResolving: _isResolving } = - select( coreStore ); - const query = { per_page: -1 }; - const rawTemplateParts = getEntityRecords( - 'postType', - postType, - query - ); - const partsAsPatterns = rawTemplateParts?.map( ( templatePart ) => - templatePartToPattern( templatePart ) - ); - - return { - templateParts: partsAsPatterns, - isResolving: _isResolving( 'getEntityRecords', [ - 'postType', - 'wp_template_part', - query, - ] ), - }; - }, - [ postType ] + const { getEntityRecords, getIsResolving } = select( coreStore ); + const query = { per_page: -1 }; + const rawTemplateParts = + getEntityRecords( 'postType', TEMPLATE_PARTS, query ) ?? + EMPTY_PATTERN_LIST; + const templateParts = rawTemplateParts.map( ( templatePart ) => + templatePartToPattern( templatePart ) ); - const filteredTemplateParts = useMemo( () => { - if ( ! templateParts ) { - return EMPTY_PATTERN_LIST; - } + const isResolving = getIsResolving( 'getEntityRecords', [ + 'postType', + 'wp_template_part', + query, + ] ); - return searchItems( templateParts, filterValue, { - categoryId, - hasCategory: templatePartHasCategory, - } ); - }, [ templateParts, filterValue, categoryId ] ); + const patterns = searchItems( templateParts, search, { + categoryId, + hasCategory: templatePartHasCategory, + } ); - return { templateParts: filteredTemplateParts, isResolving }; + return { patterns, isResolving }; }; -const useThemePatterns = ( - categoryId, - postType = PATTERNS, - filterValue = '' -) => { - const blockPatterns = useSelect( ( select ) => { - const { getSettings } = unlock( select( editSiteStore ) ); - const settings = getSettings(); - return ( - settings.__experimentalAdditionalBlockPatterns ?? - settings.__experimentalBlockPatterns - ); +const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { + const { getSettings } = unlock( select( editSiteStore ) ); + const settings = getSettings(); + const blockPatterns = + settings.__experimentalAdditionalBlockPatterns ?? + settings.__experimentalBlockPatterns; + + const restBlockPatterns = select( coreStore ).getBlockPatterns(); + + let patterns = [ + ...( blockPatterns || [] ), + ...( restBlockPatterns || [] ), + ] + .filter( + ( pattern ) => ! CORE_PATTERN_SOURCES.includes( pattern.source ) + ) + .filter( filterOutDuplicatesByName ) + .map( ( pattern ) => ( { + ...pattern, + keywords: pattern.keywords || [], + type: 'pattern', + blocks: parse( pattern.content ), + } ) ); + + patterns = searchItems( patterns, search, { + categoryId, + hasCategory: ( item, currentCategory ) => + item.categories?.includes( currentCategory ), } ); - const restBlockPatterns = useSelect( ( select ) => - select( coreStore ).getBlockPatterns() - ); - - const patterns = useMemo( - () => - [ ...( blockPatterns || [] ), ...( restBlockPatterns || [] ) ] - .filter( - ( pattern ) => - ! CORE_PATTERN_SOURCES.includes( pattern.source ) - ) - .filter( filterOutDuplicatesByName ) - .map( ( pattern ) => ( { - ...pattern, - keywords: pattern.keywords || [], - type: 'pattern', - blocks: parse( pattern.content ), - } ) ), - [ blockPatterns, restBlockPatterns ] - ); - - const filteredPatterns = useMemo( () => { - if ( postType !== PATTERNS ) { - return EMPTY_PATTERN_LIST; - } - - return searchItems( patterns, filterValue, { - categoryId, - hasCategory: ( item, currentCategory ) => - item.categories?.includes( currentCategory ), - } ); - }, [ patterns, filterValue, categoryId, postType ] ); - - return filteredPatterns; + return { patterns, isResolving: false }; }; const reusableBlockToPattern = ( reusableBlock ) => ( { @@ -154,88 +113,58 @@ const reusableBlockToPattern = ( reusableBlock ) => ( { reusableBlock, } ); -const useUserPatterns = ( - categoryId, - categoryType = PATTERNS, - filterValue = '' -) => { - const postType = categoryType === PATTERNS ? USER_PATTERNS : categoryType; - const unfilteredPatterns = useSelect( - ( select ) => { - if ( - postType !== USER_PATTERNS || - categoryId !== USER_PATTERN_CATEGORY - ) { - return EMPTY_PATTERN_LIST; - } +const selectUserPatterns = ( select, { search = '', syncStatus } = {} ) => { + const { getEntityRecords, getIsResolving } = select( coreStore ); - const { getEntityRecords } = select( coreStore ); - const records = getEntityRecords( 'postType', postType, { - per_page: -1, - } ); + const query = { per_page: -1 }; + const records = getEntityRecords( 'postType', USER_PATTERNS, query ); - if ( ! records ) { - return EMPTY_PATTERN_LIST; - } + let patterns = records + ? records.map( ( record ) => reusableBlockToPattern( record ) ) + : EMPTY_PATTERN_LIST; + const isResolving = getIsResolving( 'getEntityRecords', [ + 'postType', + USER_PATTERNS, + query, + ] ); - return records.map( ( record ) => - reusableBlockToPattern( record ) - ); - }, - [ postType, categoryId ] - ); + if ( syncStatus ) { + patterns = patterns.filter( + ( pattern ) => pattern.syncStatus === syncStatus + ); + } - const filteredPatterns = useMemo( () => { - if ( ! unfilteredPatterns.length ) { - return EMPTY_PATTERN_LIST; - } - - return searchItems( unfilteredPatterns, filterValue, { - // We exit user pattern retrieval early if we aren't in the - // catch-all category for user created patterns, so it has - // to be in the category. - hasCategory: () => true, - } ); - }, [ unfilteredPatterns, filterValue ] ); - - const patterns = { syncedPatterns: [], unsyncedPatterns: [] }; - - filteredPatterns.forEach( ( pattern ) => { - if ( pattern.syncStatus === SYNC_TYPES.full ) { - patterns.syncedPatterns.push( pattern ); - } else { - patterns.unsyncedPatterns.push( pattern ); - } + patterns = searchItems( patterns, search, { + // We exit user pattern retrieval early if we aren't in the + // catch-all category for user created patterns, so it has + // to be in the category. + hasCategory: () => true, } ); - return patterns; + return { patterns, isResolving }; }; -export const usePatterns = ( categoryType, categoryId, filterValue ) => { - const blockPatterns = useThemePatterns( - categoryId, - categoryType, - filterValue - ); - - const { syncedPatterns = [], unsyncedPatterns = [] } = useUserPatterns( - categoryId, - categoryType, - filterValue - ); - - const { templateParts, isResolving } = useTemplatePartsAsPatterns( - categoryId, - categoryType, - filterValue +export const usePatterns = ( + categoryType, + categoryId, + { search = '', syncStatus } +) => { + return useSelect( + ( select ) => { + if ( categoryType === TEMPLATE_PARTS ) { + return selectTemplatePartsAsPatterns( select, { + categoryId, + search, + } ); + } else if ( categoryType === PATTERNS ) { + return selectThemePatterns( select, { categoryId, search } ); + } else if ( categoryType === USER_PATTERNS ) { + return selectUserPatterns( select, { search, syncStatus } ); + } + return { patterns: EMPTY_PATTERN_LIST, isResolving: false }; + }, + [ categoryId, categoryType, search, syncStatus ] ); - - const patterns = { - syncedPatterns: [ ...templateParts, ...syncedPatterns ], - unsyncedPatterns: [ ...blockPatterns, ...unsyncedPatterns ], - }; - - return [ patterns, isResolving ]; }; export default usePatterns; diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js index bcfc540b1f841..f864d48de3383 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/status-label.js @@ -9,42 +9,10 @@ import classnames from 'classnames'; import { __, sprintf } from '@wordpress/i18n'; import { dateI18n, getDate, humanTimeDiff } from '@wordpress/date'; import { createInterpolateElement } from '@wordpress/element'; -import { Path, SVG } from '@wordpress/primitives'; - -const publishedIcon = ( - - - -); - -const draftIcon = ( - - - -); - -const pendingIcon = ( - - - -); export default function StatusLabel( { status, date, short } ) { const relateToNow = humanTimeDiff( date ); let statusLabel = status; - let statusIcon = pendingIcon; switch ( status ) { case 'publish': statusLabel = date @@ -57,7 +25,6 @@ export default function StatusLabel( { status, date, short } ) { { time: