From 23ee02ca7853c07492c4db4c5aa5b7c425afcd1c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 1 Jul 2022 17:00:06 +0800 Subject: [PATCH 1/7] Try a new `inserterItem` block API Show workflows in inserter Add selector for getting inserter workflow items Stub out workflow items in inserter Avoid computing draggable blocks for non-draggable items Spread items to flatten them rather than creating a nested array Fix error thrown for non-draggable inserter block Show workflow component when selecting workflow Fix order of items returned from hook Pass root client id to workflows Remove unused property Make header and footer the active workflows Use proper area label Insert the template part block Use same classname as other modal Show existing template parts in modal Add non-area template part workflow Avoid showing empty sections Change inserter panel title Remove existing template parts from modal Iteration 2: Try an `insert` block API Add `insert` property to inserter items selector result Show `insert` component for inserter items that have one Refactor callbacks Use pattern name for created template part Iteration 3: Launch modal from inserted block Reorganise components and utils Revert "Iteration 3: Launch modal from inserted block" This reverts commit 3d04accc51fd04e169fd4567f76cb4a5b85bfa92. Refactor template part selection modal to be more generic More refactoring Fix close button Fix non-pluralized text Fix naming nitpick Refactor to use `inserterItem` API --- .../src/components/block-types-list/index.js | 2 + packages/block-editor/src/components/index.js | 1 + .../inserter-draggable-blocks/index.js | 7 +- .../src/components/inserter-list-item/base.js | 134 ++++++++++++++++ .../components/inserter-list-item/index.js | 136 +---------------- .../inserter-list-item/with-modal.js | 39 +++++ .../components/inserter/block-types-tab.js | 4 + packages/block-editor/src/store/selectors.js | 4 + .../use-template-part-area-label.js | 2 +- .../components/template-part-selection.js | 107 +++++++++++++ .../{edit => components}/title-modal.js | 0 .../src/template-part/edit/index.js | 136 +++++++++++------ .../src/template-part/edit/placeholder.js | 8 +- .../src/template-part/edit/selection-modal.js | 143 ------------------ .../src/template-part/inserter-item/index.js | 80 ++++++++++ .../utils/create-template-part-id.js | 2 +- .../utils/create-template-part-post-data.js | 33 ++++ .../template-part/{edit => }/utils/hooks.js | 21 ++- .../template-part/{edit => }/utils/search.js | 0 .../src/template-part/variations.js | 10 ++ 20 files changed, 535 insertions(+), 334 deletions(-) create mode 100644 packages/block-editor/src/components/inserter-list-item/base.js create mode 100644 packages/block-editor/src/components/inserter-list-item/with-modal.js create mode 100644 packages/block-library/src/template-part/components/template-part-selection.js rename packages/block-library/src/template-part/{edit => components}/title-modal.js (100%) delete mode 100644 packages/block-library/src/template-part/edit/selection-modal.js create mode 100644 packages/block-library/src/template-part/inserter-item/index.js rename packages/block-library/src/template-part/{edit => }/utils/create-template-part-id.js (81%) create mode 100644 packages/block-library/src/template-part/utils/create-template-part-post-data.js rename packages/block-library/src/template-part/{edit => }/utils/hooks.js (87%) rename packages/block-library/src/template-part/{edit => }/utils/search.js (100%) diff --git a/packages/block-editor/src/components/block-types-list/index.js b/packages/block-editor/src/components/block-types-list/index.js index 40e04b040d5a8..0c75864359fa7 100644 --- a/packages/block-editor/src/components/block-types-list/index.js +++ b/packages/block-editor/src/components/block-types-list/index.js @@ -19,6 +19,7 @@ function chunk( array, size ) { function BlockTypesList( { items = [], + rootClientId, onSelect, onHover = () => {}, children, @@ -35,6 +36,7 @@ function BlockTypesList( { { row.map( ( item, j ) => ( { __experimentalTransferDataType="wp-blocks" transferData={ transferData } __experimentalDragComponent={ - + !! isEnabled && ( + + ) } > { ( { onDraggableStart, onDraggableEnd } ) => { diff --git a/packages/block-editor/src/components/inserter-list-item/base.js b/packages/block-editor/src/components/inserter-list-item/base.js new file mode 100644 index 0000000000000..b980bec772771 --- /dev/null +++ b/packages/block-editor/src/components/inserter-list-item/base.js @@ -0,0 +1,134 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __experimentalTruncate as Truncate } from '@wordpress/components'; +import { useMemo, useRef, memo } from '@wordpress/element'; +import { + createBlock, + createBlocksFromInnerBlocksTemplate, +} from '@wordpress/blocks'; +import { ENTER, isAppleOS } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import BlockIcon from '../block-icon'; +import { InserterListboxItem } from '../inserter-listbox'; +import InserterDraggableBlocks from '../inserter-draggable-blocks'; + +function InserterListItem( { + className, + isFirst, + item, + onSelect, + onHover, + isDraggable, + ...props +} ) { + const isDragging = useRef( false ); + const itemIconStyle = item.icon + ? { + backgroundColor: item.icon.background, + color: item.icon.foreground, + } + : {}; + const blocks = useMemo( () => { + return [ + createBlock( + item.name, + item.initialAttributes, + createBlocksFromInnerBlocksTemplate( item.innerBlocks ) + ), + ]; + }, [ item.name, item.initialAttributes, item.initialAttributes ] ); + + return ( + + { ( { draggable, onDragStart, onDragEnd } ) => ( +
{ + isDragging.current = true; + if ( onDragStart ) { + onHover( null ); + onDragStart( event ); + } + } } + onDragEnd={ ( event ) => { + isDragging.current = false; + if ( onDragEnd ) { + onDragEnd( event ); + } + } } + > + { + event.preventDefault(); + onSelect( + item, + isAppleOS() ? event.metaKey : event.ctrlKey + ); + onHover( null ); + } } + onKeyDown={ ( event ) => { + const { keyCode } = event; + if ( keyCode === ENTER ) { + event.preventDefault(); + onSelect( + item, + isAppleOS() ? event.metaKey : event.ctrlKey + ); + onHover( null ); + } + } } + onFocus={ () => { + if ( isDragging.current ) { + return; + } + onHover( item ); + } } + onMouseEnter={ () => { + if ( isDragging.current ) { + return; + } + onHover( item ); + } } + onMouseLeave={ () => onHover( null ) } + onBlur={ () => onHover( null ) } + { ...props } + > + + + + + + { item.title } + + + +
+ ) } +
+ ); +} + +export default memo( InserterListItem ); diff --git a/packages/block-editor/src/components/inserter-list-item/index.js b/packages/block-editor/src/components/inserter-list-item/index.js index d24df56df241f..ad442697cfbf9 100644 --- a/packages/block-editor/src/components/inserter-list-item/index.js +++ b/packages/block-editor/src/components/inserter-list-item/index.js @@ -1,134 +1,14 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { useMemo, useRef, memo } from '@wordpress/element'; -import { - createBlock, - createBlocksFromInnerBlocksTemplate, -} from '@wordpress/blocks'; -import { __experimentalTruncate as Truncate } from '@wordpress/components'; -import { ENTER, isAppleOS } from '@wordpress/keycodes'; - /** * Internal dependencies */ -import BlockIcon from '../block-icon'; -import { InserterListboxItem } from '../inserter-listbox'; -import InserterDraggableBlocks from '../inserter-draggable-blocks'; +import InserterListItemBase from './base'; -function InserterListItem( { - className, - isFirst, - item, - onSelect, - onHover, - isDraggable, - ...props -} ) { - const isDragging = useRef( false ); - const itemIconStyle = item.icon - ? { - backgroundColor: item.icon.background, - color: item.icon.foreground, - } - : {}; - const blocks = useMemo( () => { - return [ - createBlock( - item.name, - item.initialAttributes, - createBlocksFromInnerBlocksTemplate( item.innerBlocks ) - ), - ]; - }, [ item.name, item.initialAttributes, item.initialAttributes ] ); +export default function InserterListItem( props ) { + const { inserterItem: InserterItem } = props.item; - return ( - - { ( { draggable, onDragStart, onDragEnd } ) => ( -
{ - isDragging.current = true; - if ( onDragStart ) { - onHover( null ); - onDragStart( event ); - } - } } - onDragEnd={ ( event ) => { - isDragging.current = false; - if ( onDragEnd ) { - onDragEnd( event ); - } - } } - > - { - event.preventDefault(); - onSelect( - item, - isAppleOS() ? event.metaKey : event.ctrlKey - ); - onHover( null ); - } } - onKeyDown={ ( event ) => { - const { keyCode } = event; - if ( keyCode === ENTER ) { - event.preventDefault(); - onSelect( - item, - isAppleOS() ? event.metaKey : event.ctrlKey - ); - onHover( null ); - } - } } - onFocus={ () => { - if ( isDragging.current ) { - return; - } - onHover( item ); - } } - onMouseEnter={ () => { - if ( isDragging.current ) { - return; - } - onHover( item ); - } } - onMouseLeave={ () => onHover( null ) } - onBlur={ () => onHover( null ) } - { ...props } - > - - - - - - { item.title } - - - -
- ) } -
- ); -} + if ( InserterItem ) { + return ; + } -export default memo( InserterListItem ); + return ; +} diff --git a/packages/block-editor/src/components/inserter-list-item/with-modal.js b/packages/block-editor/src/components/inserter-list-item/with-modal.js new file mode 100644 index 0000000000000..752ca115e7443 --- /dev/null +++ b/packages/block-editor/src/components/inserter-list-item/with-modal.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { Modal } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import InserterListItem from './base'; + +export default function InserterListItemWithModal( { + modalProps, + children, + ...props +} ) { + const [ isModalVisible, setIsModalVisible ] = useState( false ); + + return ( + <> + setIsModalVisible( true ) } + aria-haspopup="dialog" + aria-expanded={ isModalVisible } + /> + { isModalVisible && ( + { + setIsModalVisible( false ); + } } + > + { children } + + ) } + + ); +} diff --git a/packages/block-editor/src/components/inserter/block-types-tab.js b/packages/block-editor/src/components/inserter/block-types-tab.js index 2e8c9e0138728..8c7f9b07fccff 100644 --- a/packages/block-editor/src/components/inserter/block-types-tab.js +++ b/packages/block-editor/src/components/inserter/block-types-tab.js @@ -104,6 +104,7 @@ export function BlockTypesTab( { { showMostUsedBlocks && !! suggestedItems.length && ( ( variation ) => { innerBlocks: variation.innerBlocks, keywords: variation.keywords || item.keywords, frecency: calculateFrecency( time, count ), + inserterItem: variation.inserterItem, }; }; @@ -1879,6 +1880,7 @@ const buildBlockTypeItem = variations: inserterVariations, example: blockType.example, utility: 1, // Deprecated. + inserterItem: blockType.inserterItem, }; }; @@ -1996,6 +1998,7 @@ export const getInserterItems = createSelector( const items = blockTypeInserterItems.reduce( ( accumulator, item ) => { const { variations = [] } = item; + // Exclude any block type item that is to be replaced by a default variation. if ( ! variations.some( ( { isDefault } ) => isDefault ) ) { accumulator.push( item ); @@ -2004,6 +2007,7 @@ export const getInserterItems = createSelector( const variationMapper = getItemFromVariation( state, item ); accumulator.push( ...variations.map( variationMapper ) ); } + return accumulator; }, [] ); diff --git a/packages/block-library/src/navigation/use-template-part-area-label.js b/packages/block-library/src/navigation/use-template-part-area-label.js index 91838b268b47d..8d609328dcd6c 100644 --- a/packages/block-library/src/navigation/use-template-part-area-label.js +++ b/packages/block-library/src/navigation/use-template-part-area-label.js @@ -10,7 +10,7 @@ import { useSelect } from '@wordpress/data'; */ // TODO: this util should perhaps be refactored somewhere like core-data. -import { createTemplatePartId } from '../template-part/edit/utils/create-template-part-id'; +import createTemplatePartId from '../template-part/utils/create-template-part-id'; export default function useTemplatePartAreaLabel( clientId ) { return useSelect( diff --git a/packages/block-library/src/template-part/components/template-part-selection.js b/packages/block-library/src/template-part/components/template-part-selection.js new file mode 100644 index 0000000000000..208eebfa8fc0b --- /dev/null +++ b/packages/block-library/src/template-part/components/template-part-selection.js @@ -0,0 +1,107 @@ +/** + * WordPress dependencies + */ +import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor'; +import { parse } from '@wordpress/blocks'; +import { useAsyncList } from '@wordpress/compose'; +import { + __experimentalHStack as HStack, + SearchControl, +} from '@wordpress/components'; +import { useMemo, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import createTemplatePartId from '../utils/create-template-part-id'; +import { + useAlternativeBlockPatterns, + useAlternativeTemplateParts, +} from '../utils/hooks'; +import { searchPatterns } from '../utils/search'; + +/** + * Convert template part (wp_template_part posts) to a pattern format accepted + * by the `BlockPatternsList` component. + * + * @param {Array} templateParts An array of wp_template_part posts. + * + * @return {Array} Template parts as patterns. + */ +const convertTemplatePartsToPatterns = ( templateParts ) => + templateParts?.map( ( templatePart ) => ( { + name: createTemplatePartId( templatePart.theme, templatePart.slug ), + title: templatePart.title.rendered, + blocks: parse( templatePart.content.raw ), + templatePart, + } ) ); + +export default function TemplatePartSelectionModalContent( { + area, + rootClientId, + templatePartId, + onTemplatePartSelect, + onPatternSelect, +} ) { + const [ searchValue, setSearchValue ] = useState( '' ); + const { templateParts } = useAlternativeTemplateParts( + area, + templatePartId + ); + const filteredTemplatePartPatterns = useMemo( () => { + const partsAsPatterns = convertTemplatePartsToPatterns( templateParts ); + return searchPatterns( partsAsPatterns, searchValue ); + }, [ templateParts, searchValue ] ); + const shownTemplatePartPatterns = useAsyncList( + filteredTemplatePartPatterns + ); + + const patterns = useAlternativeBlockPatterns( area, rootClientId ); + const filteredPatterns = useMemo( + () => searchPatterns( patterns, searchValue ), + [ patterns, searchValue ] + ); + const shownPatterns = useAsyncList( filteredPatterns ); + + const hasTemplateParts = !! filteredTemplatePartPatterns.length; + const hasBlockPatterns = !! filteredPatterns.length; + + return ( + <> +
+ +
+ { !! templateParts?.length && ( +
+

{ __( 'Existing template parts' ) }

+ +
+ ) } + { !! patterns?.length && ( + <> +

{ __( 'Patterns' ) }

+ + + ) } + { ! hasTemplateParts && ! hasBlockPatterns && ( + +

{ __( 'No results found.' ) }

+
+ ) } + + ); +} diff --git a/packages/block-library/src/template-part/edit/title-modal.js b/packages/block-library/src/template-part/components/title-modal.js similarity index 100% rename from packages/block-library/src/template-part/edit/title-modal.js rename to packages/block-library/src/template-part/components/title-modal.js diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js index f18e9f776d5e0..6bc660de2a8b0 100644 --- a/packages/block-library/src/template-part/edit/index.js +++ b/packages/block-library/src/template-part/edit/index.js @@ -6,7 +6,7 @@ import { isEmpty } from 'lodash'; /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { BlockSettingsMenuControls, BlockTitle, @@ -17,24 +17,26 @@ import { __experimentalUseHasRecursion as useHasRecursion, __experimentalUseBlockOverlayActive as useBlockOverlayActive, } from '@wordpress/block-editor'; -import { Spinner, Modal, MenuItem } from '@wordpress/components'; +import { Modal, Spinner, MenuItem } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; import { useState, createInterpolateElement } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ import TemplatePartPlaceholder from './placeholder'; -import TemplatePartSelectionModal from './selection-modal'; +import TemplatePartSelection from '../components/template-part-selection'; import { TemplatePartAdvancedControls } from './advanced-controls'; import TemplatePartInnerBlocks from './inner-blocks'; -import { createTemplatePartId } from './utils/create-template-part-id'; +import createTemplatePartId from '../utils/create-template-part-id'; +import createTemplatePartPostData from '../utils/create-template-part-post-data'; import { useAlternativeBlockPatterns, useAlternativeTemplateParts, useTemplatePartArea, -} from './utils/hooks'; +} from '../utils/hooks'; export default function TemplatePartEdit( { attributes, @@ -51,42 +53,49 @@ export default function TemplatePartEdit( { // Set the postId block attribute if it did not exist, // but wait until the inner blocks have loaded to allow // new edits to trigger this. - const { isResolved, innerBlocks, isMissing, area } = useSelect( - ( select ) => { - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const { getBlocks } = select( blockEditorStore ); + const { rootClientId, isResolved, innerBlocks, isMissing, area } = + useSelect( + ( select ) => { + const { getEditedEntityRecord, hasFinishedResolution } = + select( coreStore ); + const { getBlocks, getBlockRootClientId } = + select( blockEditorStore ); - const getEntityArgs = [ - 'postType', - 'wp_template_part', - templatePartId, - ]; - const entityRecord = templatePartId - ? getEditedEntityRecord( ...getEntityArgs ) - : null; - const _area = entityRecord?.area || attributes.area; - const hasResolvedEntity = templatePartId - ? hasFinishedResolution( - 'getEditedEntityRecord', - getEntityArgs - ) - : false; + const getEntityArgs = [ + 'postType', + 'wp_template_part', + templatePartId, + ]; + const entityRecord = templatePartId + ? getEditedEntityRecord( ...getEntityArgs ) + : null; + const _area = entityRecord?.area || attributes.area; + const hasResolvedEntity = templatePartId + ? hasFinishedResolution( + 'getEditedEntityRecord', + getEntityArgs + ) + : false; - return { - innerBlocks: getBlocks( clientId ), - isResolved: hasResolvedEntity, - isMissing: hasResolvedEntity && isEmpty( entityRecord ), - area: _area, - }; - }, - [ templatePartId, clientId ] - ); + return { + rootClientId: getBlockRootClientId( clientId ), + innerBlocks: getBlocks( clientId ), + isResolved: hasResolvedEntity, + isMissing: hasResolvedEntity && isEmpty( entityRecord ), + area: _area, + }; + }, + [ templatePartId, clientId ] + ); + + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice } = useDispatch( noticesStore ); + const { replaceInnerBlocks } = useDispatch( blockEditorStore ); const { templateParts } = useAlternativeTemplateParts( area, templatePartId ); - const blockPatterns = useAlternativeBlockPatterns( area, clientId ); + const blockPatterns = useAlternativeBlockPatterns( area, rootClientId ); const hasReplacements = !! templateParts.length || !! blockPatterns.length; const areaObject = useTemplatePartArea( area ); const hasBlockOverlay = useBlockOverlayActive( clientId ); @@ -155,7 +164,7 @@ export default function TemplatePartEdit( { setIsTemplatePartSelectionOpen( true ) @@ -206,21 +215,60 @@ export default function TemplatePartEdit( { title={ sprintf( // Translators: %s as template part area title ("Header", "Footer", etc.). __( 'Choose a %s' ), - areaObject.label.toLowerCase() + areaObject?.label.toLowerCase() ?? __( 'template part' ) ) } closeLabel={ __( 'Cancel' ) } onRequestClose={ () => setIsTemplatePartSelectionOpen( false ) } > - - setIsTemplatePartSelectionOpen( false ) - } + templatePartId={ templatePartId } + rootClientId={ rootClientId } + onTemplatePartSelect={ ( pattern ) => { + const { templatePart } = pattern; + setAttributes( { + slug: templatePart.slug, + theme: templatePart.theme, + area: undefined, + } ); + createSuccessNotice( + sprintf( + /* translators: %s: template part title. */ + __( 'Template Part "%s" inserted.' ), + templatePart.title?.rendered || + templatePart.slug + ), + { + type: 'snackbar', + } + ); + setIsTemplatePartSelectionOpen( false ); + } } + onPatternSelect={ async ( pattern, blocks ) => { + const hasSelectedTemplatePart = !! templatePartId; + if ( hasSelectedTemplatePart ) { + replaceInnerBlocks( clientId, blocks ); + } else { + const postData = createTemplatePartPostData( + area, + blocks, + pattern.title + ); + const templatePart = await saveEntityRecord( + 'postType', + 'wp_template_part', + postData + ); + setAttributes( { + slug: templatePart.slug, + theme: templatePart.theme, + area: undefined, + } ); + } + setIsTemplatePartSelectionOpen( false ); + } } /> ) } diff --git a/packages/block-library/src/template-part/edit/placeholder.js b/packages/block-library/src/template-part/edit/placeholder.js index ff43ee5644ad7..7a38aa86e1756 100644 --- a/packages/block-library/src/template-part/edit/placeholder.js +++ b/packages/block-library/src/template-part/edit/placeholder.js @@ -13,12 +13,12 @@ import { useAlternativeTemplateParts, useCreateTemplatePartFromBlocks, useTemplatePartArea, -} from './utils/hooks'; -import TitleModal from './title-modal'; +} from '../utils/hooks'; +import TitleModal from '../components/title-modal'; export default function TemplatePartPlaceholder( { area, - clientId, + rootClientId, templatePartId, onOpenSelectionModal, setAttributes, @@ -27,7 +27,7 @@ export default function TemplatePartPlaceholder( { area, templatePartId ); - const blockPatterns = useAlternativeBlockPatterns( area, clientId ); + const blockPatterns = useAlternativeBlockPatterns( area, rootClientId ); const [ showTitleModal, setShowTitleModal ] = useState( false ); const areaObject = useTemplatePartArea( area ); const createFromBlocks = useCreateTemplatePartFromBlocks( diff --git a/packages/block-library/src/template-part/edit/selection-modal.js b/packages/block-library/src/template-part/edit/selection-modal.js deleted file mode 100644 index 68a4c488fa875..0000000000000 --- a/packages/block-library/src/template-part/edit/selection-modal.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * WordPress dependencies - */ -import { useCallback, useMemo, useState } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { useDispatch } from '@wordpress/data'; -import { parse } from '@wordpress/blocks'; -import { useAsyncList } from '@wordpress/compose'; -import { - __experimentalBlockPatternsList as BlockPatternsList, - store as blockEditorStore, -} from '@wordpress/block-editor'; -import { - SearchControl, - __experimentalHStack as HStack, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { - useAlternativeBlockPatterns, - useAlternativeTemplateParts, - useCreateTemplatePartFromBlocks, -} from './utils/hooks'; -import { createTemplatePartId } from './utils/create-template-part-id'; -import { searchPatterns } from './utils/search'; - -export default function TemplatePartSelectionModal( { - setAttributes, - onClose, - templatePartId = null, - area, - clientId, -} ) { - const [ searchValue, setSearchValue ] = useState( '' ); - - // When the templatePartId is undefined, - // it means the user is creating a new one from the placeholder. - const isReplacingTemplatePartContent = !! templatePartId; - const { templateParts } = useAlternativeTemplateParts( - area, - templatePartId - ); - // We can map template parts to block patters to reuse the BlockPatternsList UI - const filteredTemplateParts = useMemo( () => { - const partsAsPatterns = templateParts.map( ( templatePart ) => ( { - name: createTemplatePartId( templatePart.theme, templatePart.slug ), - title: templatePart.title.rendered, - blocks: parse( templatePart.content.raw ), - templatePart, - } ) ); - - return searchPatterns( partsAsPatterns, searchValue ); - }, [ templateParts, searchValue ] ); - const shownTemplateParts = useAsyncList( filteredTemplateParts ); - const blockPatterns = useAlternativeBlockPatterns( area, clientId ); - const filteredBlockPatterns = useMemo( () => { - return searchPatterns( blockPatterns, searchValue ); - }, [ blockPatterns, searchValue ] ); - const shownBlockPatterns = useAsyncList( filteredBlockPatterns ); - - const { createSuccessNotice } = useDispatch( noticesStore ); - const { replaceInnerBlocks } = useDispatch( blockEditorStore ); - - const onTemplatePartSelect = useCallback( ( templatePart ) => { - setAttributes( { - slug: templatePart.slug, - theme: templatePart.theme, - area: undefined, - } ); - createSuccessNotice( - sprintf( - /* translators: %s: template part title. */ - __( 'Template Part "%s" inserted.' ), - templatePart.title?.rendered || templatePart.slug - ), - { - type: 'snackbar', - } - ); - onClose(); - }, [] ); - - const createFromBlocks = useCreateTemplatePartFromBlocks( - area, - setAttributes - ); - - const hasTemplateParts = !! filteredTemplateParts.length; - const hasBlockPatterns = !! filteredBlockPatterns.length; - - return ( -
-
- -
- { hasTemplateParts && ( -
-

{ __( 'Existing template parts' ) }

- { - onTemplatePartSelect( pattern.templatePart ); - } } - /> -
- ) } - - { hasBlockPatterns && ( -
-

{ __( 'Patterns' ) }

- { - if ( isReplacingTemplatePartContent ) { - replaceInnerBlocks( clientId, blocks ); - } else { - createFromBlocks( blocks, pattern.title ); - } - - onClose(); - } } - /> -
- ) } - - { ! hasTemplateParts && ! hasBlockPatterns && ( - -

{ __( 'No results found.' ) }

-
- ) } -
- ); -} diff --git a/packages/block-library/src/template-part/inserter-item/index.js b/packages/block-library/src/template-part/inserter-item/index.js new file mode 100644 index 0000000000000..7416c8f04d1e1 --- /dev/null +++ b/packages/block-library/src/template-part/inserter-item/index.js @@ -0,0 +1,80 @@ +/** + * WordPress dependencies + */ +import { __experimentalInserterListItemWithModal as InserterListItemWithModal } from '@wordpress/block-editor'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useTemplatePartArea } from '../utils/hooks'; +import TemplatePartSelection from '../components/template-part-selection'; +import createTemplatePartPostData from '../utils/create-template-part-post-data'; + +export default function TemplatePartInserterItem( props ) { + const { rootClientId, item, onSelect } = props; + const { saveEntityRecord } = useDispatch( coreStore ); + + const area = item?.initialAttributes?.area; + const areaType = useTemplatePartArea( area ); + const templatePartAreaLabel = + areaType?.label.toLowerCase() ?? __( 'template part' ); + + return ( + + { + const templatePart = pattern.templatePart; + const inserterItem = { + name: 'core/template-part', + initialAttributes: { + slug: templatePart.slug, + theme: templatePart.theme, + }, + }; + const focusBlock = true; + onSelect( inserterItem, focusBlock ); + } } + onPatternSelect={ async ( pattern, blocks ) => { + const templatePartPostData = + await createTemplatePartPostData( + area, + blocks, + pattern.title + ); + + const templatePart = await saveEntityRecord( + 'postType', + 'wp_template_part', + templatePartPostData + ); + + const inserterItem = { + name: 'core/template-part', + initialAttributes: { + slug: templatePart.slug, + theme: templatePart.theme, + }, + }; + const focusBlock = true; + onSelect( inserterItem, focusBlock ); + } } + /> + + ); +} diff --git a/packages/block-library/src/template-part/edit/utils/create-template-part-id.js b/packages/block-library/src/template-part/utils/create-template-part-id.js similarity index 81% rename from packages/block-library/src/template-part/edit/utils/create-template-part-id.js rename to packages/block-library/src/template-part/utils/create-template-part-id.js index 5bf05fd16d311..88587fbbf7e1c 100644 --- a/packages/block-library/src/template-part/edit/utils/create-template-part-id.js +++ b/packages/block-library/src/template-part/utils/create-template-part-id.js @@ -5,6 +5,6 @@ * @param {string} slug the template part's slug * @return {string|null} the template part's Id. */ -export function createTemplatePartId( theme, slug ) { +export default function createTemplatePartId( theme, slug ) { return theme && slug ? theme + '//' + slug : null; } diff --git a/packages/block-library/src/template-part/utils/create-template-part-post-data.js b/packages/block-library/src/template-part/utils/create-template-part-post-data.js new file mode 100644 index 0000000000000..82bdf24e760da --- /dev/null +++ b/packages/block-library/src/template-part/utils/create-template-part-post-data.js @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { kebabCase } from 'lodash'; + +/** + * WordPress dependencies + */ +import { serialize } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; + +export default function createTemplatePartPostData( + area, + blocks = [], + title = __( 'Untitled Template Part' ) +) { + // Currently template parts only allow latin chars. + // Fallback slug will receive suffix by default. + const cleanSlug = kebabCase( title ).replace( /[^\w-]+/g, '' ); + + // If we have `area` set from block attributes, means an exposed + // block variation was inserted. So add this prop to the template + // part entity on creation. Afterwards remove `area` value from + // block attributes. + return { + title, + slug: cleanSlug, + content: serialize( blocks ), + // `area` is filterable on the server and defaults to `UNCATEGORIZED` + // if provided value is not allowed. + area, + }; +} diff --git a/packages/block-library/src/template-part/edit/utils/hooks.js b/packages/block-library/src/template-part/utils/hooks.js similarity index 87% rename from packages/block-library/src/template-part/edit/utils/hooks.js rename to packages/block-library/src/template-part/utils/hooks.js index e5b60131d84ee..d31acb8110048 100644 --- a/packages/block-library/src/template-part/edit/utils/hooks.js +++ b/packages/block-library/src/template-part/utils/hooks.js @@ -16,13 +16,13 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { createTemplatePartId } from './create-template-part-id'; +import createTemplatePartId from './create-template-part-id'; /** * Retrieves the available template parts for the given area. * - * @param {string} area Template part area. - * @param {string} excludedId Template part ID to exclude. + * @param {string} area Template part area. + * @param {string?} excludedId Template part ID to exclude. * * @return {{ templateParts: Array, isResolving: boolean }} array of template parts. */ @@ -72,28 +72,25 @@ export function useAlternativeTemplateParts( area, excludedId ) { /** * Retrieves the available block patterns for the given area. * - * @param {string} area Template part area. - * @param {string} clientId Block Client ID. (The container of the block can impact allowed blocks). + * @param {string} area Template part area. + * @param {string} rootClientId Root client id * * @return {Array} array of block patterns. */ -export function useAlternativeBlockPatterns( area, clientId ) { +export function useAlternativeBlockPatterns( area, rootClientId ) { return useSelect( ( select ) => { const blockNameWithArea = area ? `core/template-part/${ area }` : 'core/template-part'; - const { - getBlockRootClientId, - __experimentalGetPatternsByBlockTypes, - } = select( blockEditorStore ); - const rootClientId = getBlockRootClientId( clientId ); + const { __experimentalGetPatternsByBlockTypes } = + select( blockEditorStore ); return __experimentalGetPatternsByBlockTypes( blockNameWithArea, rootClientId ); }, - [ area, clientId ] + [ area, rootClientId ] ); } diff --git a/packages/block-library/src/template-part/edit/utils/search.js b/packages/block-library/src/template-part/utils/search.js similarity index 100% rename from packages/block-library/src/template-part/edit/utils/search.js rename to packages/block-library/src/template-part/utils/search.js diff --git a/packages/block-library/src/template-part/variations.js b/packages/block-library/src/template-part/variations.js index d39b3e5e8a6bc..ac47541174e95 100644 --- a/packages/block-library/src/template-part/variations.js +++ b/packages/block-library/src/template-part/variations.js @@ -10,6 +10,11 @@ import { symbolFilled as symbolFilledIcon, } from '@wordpress/icons'; +/** + * Internal dependencies + */ +import InserterItem from './inserter-item'; + function getTemplatePartIcon( iconName ) { if ( 'header' === iconName ) { return headerIcon; @@ -44,8 +49,13 @@ export function enhanceTemplatePartVariations( settings, name ) { }; const variations = settings.variations.map( ( variation ) => { + const inserterItem = + variation.name === 'header' || variation.name === 'footer' + ? InserterItem + : undefined; return { ...variation, + inserterItem, ...( ! variation.isActive && { isActive } ), ...( typeof variation.icon === 'string' && { icon: getTemplatePartIcon( variation.icon ), From 38a541b0f63fd9e40be8e8117ac15c5f9d0b31c0 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 1 Aug 2022 16:12:24 +0800 Subject: [PATCH 2/7] Remove empty lines --- packages/block-editor/src/store/selectors.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 87131d3d36e9a..829514c9908f7 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1998,7 +1998,6 @@ export const getInserterItems = createSelector( const items = blockTypeInserterItems.reduce( ( accumulator, item ) => { const { variations = [] } = item; - // Exclude any block type item that is to be replaced by a default variation. if ( ! variations.some( ( { isDefault } ) => isDefault ) ) { accumulator.push( item ); @@ -2007,7 +2006,6 @@ export const getInserterItems = createSelector( const variationMapper = getItemFromVariation( state, item ); accumulator.push( ...variations.map( variationMapper ) ); } - return accumulator; }, [] ); From 9991ceb59acd8e9c7260e4f4e56f07d812a7c19c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 1 Aug 2022 16:56:43 +0800 Subject: [PATCH 3/7] Add an absolutely positioned footer. --- packages/base-styles/_z-index.scss | 4 +- .../components/template-part-selection.js | 4 +- .../src/template-part/edit/index.js | 2 +- .../src/template-part/editor.scss | 23 +++++++-- .../src/template-part/inserter-item/index.js | 47 ++++++++++++++++--- packages/components/src/modal/index.js | 13 +++-- 6 files changed, 73 insertions(+), 20 deletions(-) diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index f5b6331e6fa58..86d43ae321f67 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -18,7 +18,7 @@ $z-layers: ( ".block-editor-inserter__tabs .components-tab-panel__tab-content": 0, // lower scrolling content ".block-editor-inserter__tabs .components-tab-panel__tabs": 1, // higher sticky element ".block-editor-inserter__search": 1, // higher sticky element - ".block-library-template-part__selection-search": 1, // higher sticky element + ".block-library-template-part-selection__search": 1, // higher sticky element // These next two share a stacking context ".interface-complementary-area .components-panel" : 0, // lower scrolling content @@ -140,7 +140,7 @@ $z-layers: ( ".reusable-blocks-menu-items__convert-modal": 1000001, ".edit-site-create-template-part-modal": 1000001, ".block-editor-block-lock-modal": 1000001, - ".block-editor-template-part__selection-modal": 1000001, + ".block-library-template-part-selection-modal": 1000001, // Note: The ConfirmDialog component's z-index is being set to 1000001 in packages/components/src/confirm-dialog/styles.ts // because it uses emotion and not sass. We need it to render on top its parent popover. diff --git a/packages/block-library/src/template-part/components/template-part-selection.js b/packages/block-library/src/template-part/components/template-part-selection.js index 208eebfa8fc0b..92c6338e23153 100644 --- a/packages/block-library/src/template-part/components/template-part-selection.js +++ b/packages/block-library/src/template-part/components/template-part-selection.js @@ -37,7 +37,7 @@ const convertTemplatePartsToPatterns = ( templateParts ) => templatePart, } ) ); -export default function TemplatePartSelectionModalContent( { +export default function TemplatePartSelection( { area, rootClientId, templatePartId, @@ -69,7 +69,7 @@ export default function TemplatePartSelectionModalContent( { return ( <> -
+
{ - const templatePartPostData = - await createTemplatePartPostData( - area, - blocks, - pattern.title - ); + const templatePartPostData = createTemplatePartPostData( + area, + blocks, + pattern.title + ); const templatePart = await saveEntityRecord( 'postType', @@ -75,6 +77,37 @@ export default function TemplatePartInserterItem( props ) { onSelect( inserterItem, focusBlock ); } } /> + + + ); } diff --git a/packages/components/src/modal/index.js b/packages/components/src/modal/index.js index 5230f6b4c4852..def68464625ae 100644 --- a/packages/components/src/modal/index.js +++ b/packages/components/src/modal/index.js @@ -58,6 +58,7 @@ function Modal( props, forwardedRef ) { children, style, overlayClassName, + contentClassName, className, contentLabel, onKeyDown, @@ -157,10 +158,14 @@ function Modal( props, forwardedRef ) { onKeyDown={ onKeyDown } >
From f3b0d211a209d8f330136ed28d14d76103187718 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 4 Aug 2022 16:37:29 +0800 Subject: [PATCH 4/7] Show a spinner when some saving is required --- packages/base-styles/_z-index.scss | 1 + .../src/template-part/editor.scss | 31 +++++++++++++++++++ .../src/template-part/inserter-item/index.js | 24 +++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 86d43ae321f67..ed3b693a2f26e 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -19,6 +19,7 @@ $z-layers: ( ".block-editor-inserter__tabs .components-tab-panel__tabs": 1, // higher sticky element ".block-editor-inserter__search": 1, // higher sticky element ".block-library-template-part-selection__search": 1, // higher sticky element + ".block-library-template-part-selection__overlay": 1, // higher sticky element // These next two share a stacking context ".interface-complementary-area .components-panel" : 0, // lower scrolling content diff --git a/packages/block-library/src/template-part/editor.scss b/packages/block-library/src/template-part/editor.scss index 75cfaab6cb3da..e4a3ea4ea2e60 100644 --- a/packages/block-library/src/template-part/editor.scss +++ b/packages/block-library/src/template-part/editor.scss @@ -39,3 +39,34 @@ .block-library-template-part-selection-modal__content { margin-bottom: $grid-unit-80 + $grid-unit-15; } + +.block-library-template-part-selection__saving-overlay { + position: absolute; + top: 0; + left: 0; + height: 0; + width: 0; + overflow: hidden; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; + background: rgba($gray-700, 0.3); + opacity: 0; + transition: opacity 120ms linear; + @include reduce-motion("transition"); + + &.is-saving { + width: 100%; + height: 100%; + opacity: 1; + pointer-events: all; + z-index: z-index(".block-library-template-part-selection__overlay"); + } +} + +// Needs specificity. +.block-library-template-part-selection__spinner.block-library-template-part-selection__spinner { + width: $grid-unit-40; + height: $grid-unit-40; +} diff --git a/packages/block-library/src/template-part/inserter-item/index.js b/packages/block-library/src/template-part/inserter-item/index.js index 6e417f2bcd56e..bde3375f3b1f4 100644 --- a/packages/block-library/src/template-part/inserter-item/index.js +++ b/packages/block-library/src/template-part/inserter-item/index.js @@ -1,10 +1,20 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { __experimentalInserterListItemWithModal as InserterListItemWithModal } from '@wordpress/block-editor'; -import { Button, __experimentalHStack as HStack } from '@wordpress/components'; +import { + Button, + __experimentalHStack as HStack, + Spinner, +} from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -15,6 +25,7 @@ import TemplatePartSelection from '../components/template-part-selection'; import createTemplatePartPostData from '../utils/create-template-part-post-data'; export default function TemplatePartInserterItem( props ) { + const [ isSaving, setIsSaving ] = useState( false ); const { rootClientId, item, onSelect } = props; const { saveEntityRecord } = useDispatch( coreStore ); @@ -54,6 +65,7 @@ export default function TemplatePartInserterItem( props ) { onSelect( inserterItem, focusBlock ); } } onPatternSelect={ async ( pattern, blocks ) => { + setIsSaving( true ); const templatePartPostData = createTemplatePartPostData( area, blocks, @@ -73,6 +85,7 @@ export default function TemplatePartInserterItem( props ) { theme: templatePart.theme, }, }; + const focusBlock = true; onSelect( inserterItem, focusBlock ); } } @@ -84,6 +97,7 @@ export default function TemplatePartInserterItem( props ) { +
+ +
); } From 604e01081f24c02e945e199e77cad5023e25f864 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 11 Aug 2022 17:13:21 +0800 Subject: [PATCH 5/7] Make saving overlay more noticeable --- packages/base-styles/_z-index.scss | 2 +- packages/block-library/src/template-part/editor.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index ed3b693a2f26e..3f0a30995e263 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -19,7 +19,7 @@ $z-layers: ( ".block-editor-inserter__tabs .components-tab-panel__tabs": 1, // higher sticky element ".block-editor-inserter__search": 1, // higher sticky element ".block-library-template-part-selection__search": 1, // higher sticky element - ".block-library-template-part-selection__overlay": 1, // higher sticky element + ".block-library-template-part-selection__overlay": 10, // higher than modal header. // These next two share a stacking context ".interface-complementary-area .components-panel" : 0, // lower scrolling content diff --git a/packages/block-library/src/template-part/editor.scss b/packages/block-library/src/template-part/editor.scss index e4a3ea4ea2e60..d47e06253f515 100644 --- a/packages/block-library/src/template-part/editor.scss +++ b/packages/block-library/src/template-part/editor.scss @@ -51,7 +51,7 @@ display: flex; align-items: center; justify-content: center; - background: rgba($gray-700, 0.3); + background: rgba($gray-300, 0.75); opacity: 0; transition: opacity 120ms linear; @include reduce-motion("transition"); From d3d76f2e99002e5f224ce0a9aabc3836f43e9700 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 22 Aug 2022 15:14:52 +0800 Subject: [PATCH 6/7] Add TemplatePartInserterItem component to edit-site package along with related utils --- packages/edit-site/package.json | 4 +- .../template-part-inserter-item/index.js | 134 ++++++++++ .../template-part-selection.js | 107 ++++++++ .../template-part-inserter-item/utils.js | 235 ++++++++++++++++++ 4 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 packages/edit-site/src/components/template-part-inserter-item/index.js create mode 100644 packages/edit-site/src/components/template-part-inserter-item/template-part-selection.js create mode 100644 packages/edit-site/src/components/template-part-inserter-item/utils.js diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 4d79fd8bb75cc..9b82a0f442b0d 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -54,12 +54,14 @@ "@wordpress/style-engine": "file:../style-engine", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", + "change-case": "^4.1.2", "classnames": "^2.3.1", "downloadjs": "^1.4.7", "history": "^5.1.0", "lodash": "^4.17.21", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.0" + "rememo": "^4.0.0", + "remove-accents": "^0.4.2" }, "peerDependencies": { "react": "^17.0.0", diff --git a/packages/edit-site/src/components/template-part-inserter-item/index.js b/packages/edit-site/src/components/template-part-inserter-item/index.js new file mode 100644 index 0000000000000..9d6bb4996017f --- /dev/null +++ b/packages/edit-site/src/components/template-part-inserter-item/index.js @@ -0,0 +1,134 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __experimentalInserterListItemWithModal as InserterListItemWithModal } from '@wordpress/block-editor'; +import { + Button, + __experimentalHStack as HStack, + Spinner, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { createTemplatePartPostData, useTemplatePartArea } from './utils'; +import TemplatePartSelection from './template-part-selection'; + +export default function TemplatePartInserterItem( props ) { + const [ isSaving, setIsSaving ] = useState( false ); + const { rootClientId, item, onSelect } = props; + const { saveEntityRecord } = useDispatch( coreStore ); + + const area = item?.initialAttributes?.area; + const areaType = useTemplatePartArea( area ); + const templatePartAreaLabel = + areaType?.label.toLowerCase() ?? __( 'template part' ); + + return ( + + { + const templatePart = pattern.templatePart; + const inserterItem = { + name: 'core/template-part', + initialAttributes: { + slug: templatePart.slug, + theme: templatePart.theme, + }, + }; + const focusBlock = true; + onSelect( inserterItem, focusBlock ); + } } + onPatternSelect={ async ( pattern, blocks ) => { + setIsSaving( true ); + const templatePartPostData = createTemplatePartPostData( + area, + blocks, + pattern.title + ); + + const templatePart = await saveEntityRecord( + 'postType', + 'wp_template_part', + templatePartPostData + ); + + const inserterItem = { + name: 'core/template-part', + initialAttributes: { + slug: templatePart.slug, + theme: templatePart.theme, + }, + }; + + const focusBlock = true; + onSelect( inserterItem, focusBlock ); + } } + /> + + + +
+ +
+
+ ); +} diff --git a/packages/edit-site/src/components/template-part-inserter-item/template-part-selection.js b/packages/edit-site/src/components/template-part-inserter-item/template-part-selection.js new file mode 100644 index 0000000000000..b09996c181552 --- /dev/null +++ b/packages/edit-site/src/components/template-part-inserter-item/template-part-selection.js @@ -0,0 +1,107 @@ +/** + * WordPress dependencies + */ +import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor'; +import { parse } from '@wordpress/blocks'; +import { useAsyncList } from '@wordpress/compose'; +import { + __experimentalHStack as HStack, + SearchControl, +} from '@wordpress/components'; +import { useMemo, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { + createTemplatePartId, + searchPatterns, + useAlternativeBlockPatterns, + useAlternativeTemplateParts, +} from './utils'; + +/** + * Convert template part (wp_template_part posts) to a pattern format accepted + * by the `BlockPatternsList` component. + * + * @param {Array} templateParts An array of wp_template_part posts. + * + * @return {Array} Template parts as patterns. + */ +const convertTemplatePartsToPatterns = ( templateParts ) => + templateParts?.map( ( templatePart ) => ( { + name: createTemplatePartId( templatePart.theme, templatePart.slug ), + title: templatePart.title.rendered, + blocks: parse( templatePart.content.raw ), + templatePart, + } ) ); + +export default function TemplatePartSelection( { + area, + rootClientId, + templatePartId, + onTemplatePartSelect, + onPatternSelect, +} ) { + const [ searchValue, setSearchValue ] = useState( '' ); + const { templateParts } = useAlternativeTemplateParts( + area, + templatePartId + ); + const filteredTemplatePartPatterns = useMemo( () => { + const partsAsPatterns = convertTemplatePartsToPatterns( templateParts ); + return searchPatterns( partsAsPatterns, searchValue ); + }, [ templateParts, searchValue ] ); + const shownTemplatePartPatterns = useAsyncList( + filteredTemplatePartPatterns + ); + + const patterns = useAlternativeBlockPatterns( area, rootClientId ); + const filteredPatterns = useMemo( + () => searchPatterns( patterns, searchValue ), + [ patterns, searchValue ] + ); + const shownPatterns = useAsyncList( filteredPatterns ); + + const hasTemplateParts = !! filteredTemplatePartPatterns.length; + const hasBlockPatterns = !! filteredPatterns.length; + + return ( + <> +
+ +
+ { !! templateParts?.length && ( +
+

{ __( 'Existing template parts' ) }

+ +
+ ) } + { !! patterns?.length && ( + <> +

{ __( 'Patterns' ) }

+ + + ) } + { ! hasTemplateParts && ! hasBlockPatterns && ( + +

{ __( 'No results found.' ) }

+
+ ) } + + ); +} diff --git a/packages/edit-site/src/components/template-part-inserter-item/utils.js b/packages/edit-site/src/components/template-part-inserter-item/utils.js new file mode 100644 index 0000000000000..f8e86ec43af64 --- /dev/null +++ b/packages/edit-site/src/components/template-part-inserter-item/utils.js @@ -0,0 +1,235 @@ +// import createTemplatePartPostData from '../utils/create-template-part-post-data'; + +/** + * External dependencies + */ +import removeAccents from 'remove-accents'; +import { paramCase } from 'change-case'; + +/** + * WordPress dependencies + */ +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { serialize } from '@wordpress/blocks'; +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Generates a template part Id based on slug and theme inputs. + * + * @param {string} theme the template part's theme. + * @param {string} slug the template part's slug + * @return {string|null} the template part's Id. + */ +export function createTemplatePartId( theme, slug ) { + return theme && slug ? theme + '//' + slug : null; +} + +export function createTemplatePartPostData( + area, + blocks = [], + title = __( 'Untitled Template Part' ) +) { + // Currently template parts only allow latin chars. + // Fallback slug will receive suffix by default. + const cleanSlug = paramCase( title ).replace( /[^\w-]+/g, '' ); + + // If we have `area` set from block attributes, means an exposed + // block variation was inserted. So add this prop to the template + // part entity on creation. Afterwards remove `area` value from + // block attributes. + return { + title, + slug: cleanSlug, + content: serialize( blocks ), + // `area` is filterable on the server and defaults to `UNCATEGORIZED` + // if provided value is not allowed. + area, + }; +} + +/** + * Retrieves the available block patterns for the given area. + * + * @param {string} area Template part area. + * @param {string} rootClientId Root client id + * + * @return {Array} array of block patterns. + */ +export function useAlternativeBlockPatterns( area, rootClientId ) { + return useSelect( + ( select ) => { + const blockNameWithArea = area + ? `core/template-part/${ area }` + : 'core/template-part'; + const { __experimentalGetPatternsByBlockTypes } = + select( blockEditorStore ); + return __experimentalGetPatternsByBlockTypes( + blockNameWithArea, + rootClientId + ); + }, + [ area, rootClientId ] + ); +} + +/** + * Retrieves the available template parts for the given area. + * + * @param {string} area Template part area. + * @param {string?} excludedId Template part ID to exclude. + * + * @return {{ templateParts: Array, isResolving: boolean }} array of template parts. + */ +export function useAlternativeTemplateParts( area, excludedId ) { + const { templateParts, isResolving } = useSelect( ( select ) => { + const { getEntityRecords, isResolving: _isResolving } = + select( coreStore ); + const query = { per_page: -1 }; + return { + templateParts: getEntityRecords( + 'postType', + 'wp_template_part', + query + ), + isLoading: _isResolving( 'getEntityRecords', [ + 'postType', + 'wp_template_part', + query, + ] ), + }; + }, [] ); + + const filteredTemplateParts = useMemo( () => { + if ( ! templateParts ) { + return []; + } + return ( + templateParts.filter( + ( templatePart ) => + createTemplatePartId( + templatePart.theme, + templatePart.slug + ) !== excludedId && + ( ! area || + 'uncategorized' === area || + templatePart.area === area ) + ) || [] + ); + }, [ templateParts, area ] ); + + return { + templateParts: filteredTemplateParts, + isResolving, + }; +} + +/** + * Retrieves the template part area object. + * + * @param {string} area Template part area identifier. + * + * @return {{icon: Object, label: string, tagName: string}} Template Part area. + */ +export function useTemplatePartArea( area ) { + return useSelect( + ( select ) => { + // FIXME: @wordpress/block-library should not depend on @wordpress/editor. + // Blocks can be loaded into a *non-post* block editor. + /* eslint-disable @wordpress/data-no-store-string-literals */ + const definedAreas = + select( + 'core/editor' + ).__experimentalGetDefaultTemplatePartAreas(); + /* eslint-enable @wordpress/data-no-store-string-literals */ + + const selectedArea = definedAreas?.find( + ( { area: candidateArea } ) => candidateArea === area + ); + const defaultArea = definedAreas?.find( + ( { area: candidateArea } ) => candidateArea === 'uncategorized' + ); + + return { + icon: selectedArea?.icon || defaultArea?.icon, + label: selectedArea?.label || __( 'Template Part' ), + tagName: selectedArea?.area_tag ?? 'div', + }; + }, + [ area ] + ); +} + +/** + * Sanitizes the search input string. + * + * @param {string} input The search input to normalize. + * + * @return {string} The normalized search input. + */ +function normalizeSearchInput( input = '' ) { + // Disregard diacritics. + input = removeAccents( input ); + + // Trim & Lowercase. + input = input.trim().toLowerCase(); + + return input; +} + +/** + * Get the search rank for a given pattern and a specific search term. + * + * @param {Object} pattern Pattern to rank + * @param {string} searchValue Search term + * @return {number} A pattern search rank + */ +function getPatternSearchRank( pattern, searchValue ) { + const normalizedSearchValue = normalizeSearchInput( searchValue ); + const normalizedTitle = normalizeSearchInput( pattern.title ); + + let rank = 0; + + if ( normalizedSearchValue === normalizedTitle ) { + rank += 30; + } else if ( normalizedTitle.startsWith( normalizedSearchValue ) ) { + rank += 20; + } else { + const searchTerms = normalizedSearchValue.split( ' ' ); + const hasMatchedTerms = searchTerms.every( ( searchTerm ) => + normalizedTitle.includes( searchTerm ) + ); + + // Prefer pattern with every search word in the title. + if ( hasMatchedTerms ) { + rank += 10; + } + } + + return rank; +} + +/** + * Filters an pattern list given a search term. + * + * @param {Array} patterns Item list + * @param {string} searchValue Search input. + * + * @return {Array} Filtered pattern list. + */ +export function searchPatterns( patterns = [], searchValue = '' ) { + if ( ! searchValue ) { + return patterns; + } + + const rankedPatterns = patterns + .map( ( pattern ) => { + return [ pattern, getPatternSearchRank( pattern, searchValue ) ]; + } ) + .filter( ( [ , rank ] ) => rank > 0 ); + + rankedPatterns.sort( ( [ , rank1 ], [ , rank2 ] ) => rank2 - rank1 ); + return rankedPatterns.map( ( [ pattern ] ) => pattern ); +} From 32d6f379acf8a2f220006b5ef5b6ee1585aad11f Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 22 Aug 2022 15:56:13 +0800 Subject: [PATCH 7/7] Use a customInserterItems prop instead of a new block API --- package-lock.json | 6 +- .../components/inserter-list-item/index.js | 8 +- .../inserter/custom-inserter-items-context.js | 10 ++ .../src/components/inserter/library.js | 31 ++-- packages/block-editor/src/store/selectors.js | 2 - .../src/template-part/inserter-item/index.js | 135 ------------------ .../src/template-part/variations.js | 26 +--- .../secondary-sidebar/inserter-sidebar.js | 7 + .../index.js | 2 +- .../template-part-selection.js | 0 .../utils.js | 0 11 files changed, 53 insertions(+), 174 deletions(-) create mode 100644 packages/block-editor/src/components/inserter/custom-inserter-items-context.js delete mode 100644 packages/block-library/src/template-part/inserter-item/index.js rename packages/edit-site/src/components/{template-part-inserter-item => template-part-custom-inserter-item}/index.js (98%) rename packages/edit-site/src/components/{template-part-inserter-item => template-part-custom-inserter-item}/template-part-selection.js (100%) rename packages/edit-site/src/components/{template-part-inserter-item => template-part-custom-inserter-item}/utils.js (100%) diff --git a/package-lock.json b/package-lock.json index 03c40adf1615b..b9eaf24641968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17097,12 +17097,14 @@ "@wordpress/style-engine": "file:packages/style-engine", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", + "change-case": "^4.1.2", "classnames": "^2.3.1", "downloadjs": "^1.4.7", "history": "^5.1.0", "lodash": "^4.17.21", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.0" + "rememo": "^4.0.0", + "remove-accents": "^0.4.2" } }, "@wordpress/edit-widgets": { @@ -38494,7 +38496,7 @@ "is-window": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz", - "integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==", + "integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0=", "dev": true }, "is-windows": { diff --git a/packages/block-editor/src/components/inserter-list-item/index.js b/packages/block-editor/src/components/inserter-list-item/index.js index ad442697cfbf9..67ef377a421a9 100644 --- a/packages/block-editor/src/components/inserter-list-item/index.js +++ b/packages/block-editor/src/components/inserter-list-item/index.js @@ -2,12 +2,14 @@ * Internal dependencies */ import InserterListItemBase from './base'; +import { useCustomInserterItems } from '../inserter/custom-inserter-items-context'; export default function InserterListItem( props ) { - const { inserterItem: InserterItem } = props.item; + const customInserterItems = useCustomInserterItems(); + const CustomInserterItem = customInserterItems[ props.item.id ]; - if ( InserterItem ) { - return ; + if ( CustomInserterItem ) { + return ; } return ; diff --git a/packages/block-editor/src/components/inserter/custom-inserter-items-context.js b/packages/block-editor/src/components/inserter/custom-inserter-items-context.js new file mode 100644 index 0000000000000..8001d25514067 --- /dev/null +++ b/packages/block-editor/src/components/inserter/custom-inserter-items-context.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +const CustomInserterItemsContext = createContext( [] ); + +export const CustomInserterItemsProvider = CustomInserterItemsContext.Provider; +export const useCustomInserterItems = () => + useContext( CustomInserterItemsContext ); diff --git a/packages/block-editor/src/components/inserter/library.js b/packages/block-editor/src/components/inserter/library.js index aba22e3e1f3fb..6f24e4b19d778 100644 --- a/packages/block-editor/src/components/inserter/library.js +++ b/packages/block-editor/src/components/inserter/library.js @@ -8,9 +8,11 @@ import { forwardRef } from '@wordpress/element'; * Internal dependencies */ import InserterMenu from './menu'; +import { CustomInserterItemsProvider } from './custom-inserter-items-context'; import { store as blockEditorStore } from '../../store'; const noop = () => {}; +const EMPTY_ARRAY = []; function InserterLibrary( { @@ -19,6 +21,7 @@ function InserterLibrary( isAppender, showInserterHelpPanel, showMostUsedBlocks = false, + __experimentalCustomInserterItems = EMPTY_ARRAY, __experimentalInsertionIndex, __experimentalFilterValue, onSelect = noop, @@ -38,18 +41,22 @@ function InserterLibrary( ); return ( - + + + ); } diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 829514c9908f7..030977812dcd4 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1796,7 +1796,6 @@ const getItemFromVariation = ( state, item ) => ( variation ) => { innerBlocks: variation.innerBlocks, keywords: variation.keywords || item.keywords, frecency: calculateFrecency( time, count ), - inserterItem: variation.inserterItem, }; }; @@ -1880,7 +1879,6 @@ const buildBlockTypeItem = variations: inserterVariations, example: blockType.example, utility: 1, // Deprecated. - inserterItem: blockType.inserterItem, }; }; diff --git a/packages/block-library/src/template-part/inserter-item/index.js b/packages/block-library/src/template-part/inserter-item/index.js deleted file mode 100644 index bde3375f3b1f4..0000000000000 --- a/packages/block-library/src/template-part/inserter-item/index.js +++ /dev/null @@ -1,135 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { __experimentalInserterListItemWithModal as InserterListItemWithModal } from '@wordpress/block-editor'; -import { - Button, - __experimentalHStack as HStack, - Spinner, -} from '@wordpress/components'; -import { store as coreStore } from '@wordpress/core-data'; -import { useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { useTemplatePartArea } from '../utils/hooks'; -import TemplatePartSelection from '../components/template-part-selection'; -import createTemplatePartPostData from '../utils/create-template-part-post-data'; - -export default function TemplatePartInserterItem( props ) { - const [ isSaving, setIsSaving ] = useState( false ); - const { rootClientId, item, onSelect } = props; - const { saveEntityRecord } = useDispatch( coreStore ); - - const area = item?.initialAttributes?.area; - const areaType = useTemplatePartArea( area ); - const templatePartAreaLabel = - areaType?.label.toLowerCase() ?? __( 'template part' ); - - return ( - - { - const templatePart = pattern.templatePart; - const inserterItem = { - name: 'core/template-part', - initialAttributes: { - slug: templatePart.slug, - theme: templatePart.theme, - }, - }; - const focusBlock = true; - onSelect( inserterItem, focusBlock ); - } } - onPatternSelect={ async ( pattern, blocks ) => { - setIsSaving( true ); - const templatePartPostData = createTemplatePartPostData( - area, - blocks, - pattern.title - ); - - const templatePart = await saveEntityRecord( - 'postType', - 'wp_template_part', - templatePartPostData - ); - - const inserterItem = { - name: 'core/template-part', - initialAttributes: { - slug: templatePart.slug, - theme: templatePart.theme, - }, - }; - - const focusBlock = true; - onSelect( inserterItem, focusBlock ); - } } - /> - - - -
- -
-
- ); -} diff --git a/packages/block-library/src/template-part/variations.js b/packages/block-library/src/template-part/variations.js index ac47541174e95..58b7d70c52b3c 100644 --- a/packages/block-library/src/template-part/variations.js +++ b/packages/block-library/src/template-part/variations.js @@ -10,11 +10,6 @@ import { symbolFilled as symbolFilledIcon, } from '@wordpress/icons'; -/** - * Internal dependencies - */ -import InserterItem from './inserter-item'; - function getTemplatePartIcon( iconName ) { if ( 'header' === iconName ) { return headerIcon; @@ -48,20 +43,13 @@ export function enhanceTemplatePartVariations( settings, name ) { return entity?.area === variationAttributes.area; }; - const variations = settings.variations.map( ( variation ) => { - const inserterItem = - variation.name === 'header' || variation.name === 'footer' - ? InserterItem - : undefined; - return { - ...variation, - inserterItem, - ...( ! variation.isActive && { isActive } ), - ...( typeof variation.icon === 'string' && { - icon: getTemplatePartIcon( variation.icon ), - } ), - }; - } ); + const variations = settings.variations.map( ( variation ) => ( { + ...variation, + ...( ! variation.isActive && { isActive } ), + ...( typeof variation.icon === 'string' && { + icon: getTemplatePartIcon( variation.icon ), + } ), + } ) ); return { ...settings, diff --git a/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js index 5567bcc382119..e36d6ae5f382e 100644 --- a/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js @@ -16,6 +16,7 @@ import { useEffect, useRef } from '@wordpress/element'; * Internal dependencies */ import { store as editSiteStore } from '../../store'; +import TemplatePartCustomInserterItem from '../template-part-custom-inserter-item'; export default function InserterSidebar() { const { setIsInserterOpened } = useDispatch( editSiteStore ); @@ -57,6 +58,12 @@ export default function InserterSidebar() { __experimentalInsertionIndex={ insertionPoint.insertionIndex } + __experimentalCustomInserterItems={ { + 'core/template-part/header': + TemplatePartCustomInserterItem, + 'core/template-part/footer': + TemplatePartCustomInserterItem, + } } __experimentalFilterValue={ insertionPoint.filterValue } ref={ libraryRef } /> diff --git a/packages/edit-site/src/components/template-part-inserter-item/index.js b/packages/edit-site/src/components/template-part-custom-inserter-item/index.js similarity index 98% rename from packages/edit-site/src/components/template-part-inserter-item/index.js rename to packages/edit-site/src/components/template-part-custom-inserter-item/index.js index 9d6bb4996017f..1ca7c3cc49c6a 100644 --- a/packages/edit-site/src/components/template-part-inserter-item/index.js +++ b/packages/edit-site/src/components/template-part-custom-inserter-item/index.js @@ -23,7 +23,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { createTemplatePartPostData, useTemplatePartArea } from './utils'; import TemplatePartSelection from './template-part-selection'; -export default function TemplatePartInserterItem( props ) { +export default function TemplatePartCustomInserterItem( props ) { const [ isSaving, setIsSaving ] = useState( false ); const { rootClientId, item, onSelect } = props; const { saveEntityRecord } = useDispatch( coreStore ); diff --git a/packages/edit-site/src/components/template-part-inserter-item/template-part-selection.js b/packages/edit-site/src/components/template-part-custom-inserter-item/template-part-selection.js similarity index 100% rename from packages/edit-site/src/components/template-part-inserter-item/template-part-selection.js rename to packages/edit-site/src/components/template-part-custom-inserter-item/template-part-selection.js diff --git a/packages/edit-site/src/components/template-part-inserter-item/utils.js b/packages/edit-site/src/components/template-part-custom-inserter-item/utils.js similarity index 100% rename from packages/edit-site/src/components/template-part-inserter-item/utils.js rename to packages/edit-site/src/components/template-part-custom-inserter-item/utils.js