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, }; }; @@ -1880,6 +1881,7 @@ const buildBlockTypeItem = variations: inserterVariations, example: blockType.example, utility: 1, // Deprecated. + inserterItem: blockType.inserterItem, }; }; @@ -1997,6 +1999,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 ); @@ -2005,6 +2008,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 213dca72e1a4a..1c3185c912aa0 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, @@ -16,24 +16,26 @@ import { __experimentalUseNoRecursiveRenders as useNoRecursiveRenders, __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 ),