From 23ee02ca7853c07492c4db4c5aa5b7c425afcd1c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 1 Jul 2022 17:00:06 +0800 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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/5] 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");