From dda4fb5dbe0c5d1e72ab3c6a206354c007953dda Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 1 Jul 2022 17:00:06 +0800 Subject: [PATCH] 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 40e04b040d5a80..0c75864359fa74 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 00000000000000..b980bec772771e --- /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 d24df56df241fe..ad442697cfbf91 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 00000000000000..752ca115e74435 --- /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 2e8c9e0138728a..8c7f9b07fccff2 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 91838b268b47d6..8d609328dcd6c3 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 00000000000000..208eebfa8fc0bf --- /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 213dca72e1a4a1..1c3185c912aa0a 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 ff43ee5644ad7c..7a38aa86e1756c 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 68a4c488fa875e..00000000000000 --- 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 00000000000000..7416c8f04d1e14 --- /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 5bf05fd16d3112..88587fbbf7e1c5 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 00000000000000..82bdf24e760daf --- /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 e5b60131d84ee2..d31acb81100481 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 d39b3e5e8a6bc5..ac47541174e955 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 ),