Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add starter patterns to site editor new page #55117

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/block-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,10 @@ _Related_

- <https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/skip-to-selected-block/README.md>

### StarterPatternsModal

Undocumented declaration.

Comment on lines +766 to +769
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should maybe make this one private.

### store

Store definition for the block editor namespace.
Expand Down
1 change: 1 addition & 0 deletions packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export {
useMouseMoveTypingReset as __unstableUseMouseMoveTypingReset,
} from './observe-typing';
export { default as SkipToSelectedBlock } from './skip-to-selected-block';
export { default as StarterPatternsModal } from './starter-patterns-modal';
export {
default as Typewriter,
useTypewriter as __unstableUseTypewriter,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* WordPress dependencies
*/
import { Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { useAsyncList } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import { __experimentalBlockPatternsList as BlockPatternsList } from '../';

function useStarterPatterns( postType, rootClientId ) {
// A pattern is a start pattern if it includes 'core/post-content' in its blockTypes,
// and it has no postTypes declared and the current post type is page or if
// the current post type is part of the postTypes declared.
const blockPatternsWithPostContentBlockType = useSelect(
( select ) =>
select( blockEditorStore ).getPatternsByBlockTypes(
'core/post-content',
rootClientId
),
[ rootClientId ]
);

return useMemo( () => {
// filter patterns without postTypes declared if the current postType is page
// or patterns that declare the current postType in its post type array.
return blockPatternsWithPostContentBlockType.filter( ( pattern ) => {
return (
( postType === 'page' && ! pattern.postTypes ) ||
( Array.isArray( pattern.postTypes ) &&
pattern.postTypes.includes( postType ) )
);
} );
}, [ postType, blockPatternsWithPostContentBlockType ] );
}

export default function StarterPatternsModal( {
onChoosePattern,
onRequestClose,
postType,
rootClientId,
} ) {
const starterPatterns = useStarterPatterns( postType, rootClientId );
const shownStarterPatterns = useAsyncList( starterPatterns );

if ( starterPatterns.length === 0 ) {
return null;
}

return (
<Modal
title={ __( 'Choose a pattern' ) }
isFullScreen
onRequestClose={ onRequestClose }
>
<div className="block-editor-starter-patterns-modal__content">
<BlockPatternsList
blockPatterns={ starterPatterns }
shownPatterns={ shownStarterPatterns }
onClickPattern={ onChoosePattern }
/>
</div>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 2 column masonry layout.
.block-editor-starter-patterns-modal__content .block-editor-block-patterns-list {
column-count: 2;
column-gap: $grid-unit-30;

@include break-medium() {
column-count: 3;
}

@include break-wide() {
column-count: 4;
}

.block-editor-block-patterns-list__list-item {
break-inside: avoid-column;
margin-bottom: $grid-unit-30;

.block-editor-block-preview__container {
min-height: 100px;
}

.block-editor-block-preview__content {
width: 100%;
}
}
}
1 change: 1 addition & 0 deletions packages/block-editor/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
@import "./components/responsive-block-control/style.scss";
@import "./components/rich-text/style.scss";
@import "./components/skip-to-selected-block/style.scss";
@import "./components/starter-patterns-modal/style.scss";
@import "./components/text-decoration-control/style.scss";
@import "./components/text-transform-control/style.scss";
@import "./components/tool-selector/style.scss";
Expand Down
108 changes: 22 additions & 86 deletions packages/edit-post/src/components/start-page-options/index.js
Original file line number Diff line number Diff line change
@@ -1,108 +1,44 @@
/**
* WordPress dependencies
*/
import { Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useState, useMemo } from '@wordpress/element';
import {
store as blockEditorStore,
__experimentalBlockPatternsList as BlockPatternsList,
} from '@wordpress/block-editor';
import { useSelect, useDispatch } from '@wordpress/data';
import { useAsyncList } from '@wordpress/compose';
import { store as editorStore } from '@wordpress/editor';
import { useState } from '@wordpress/element';
import { StarterPatternsModal } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { store as editPostStore } from '../../store';

function useStartPatterns() {
// A pattern is a start pattern if it includes 'core/post-content' in its blockTypes,
// and it has no postTypes declared and the current post type is page or if
// the current post type is part of the postTypes declared.
const { blockPatternsWithPostContentBlockType, postType } = useSelect(
( select ) => {
const { getPatternsByBlockTypes } = select( blockEditorStore );
const { getCurrentPostType } = select( editorStore );
return {
blockPatternsWithPostContentBlockType:
getPatternsByBlockTypes( 'core/post-content' ),
postType: getCurrentPostType(),
};
},
[]
);

return useMemo( () => {
// filter patterns without postTypes declared if the current postType is page
// or patterns that declare the current postType in its post type array.
return blockPatternsWithPostContentBlockType.filter( ( pattern ) => {
return (
( postType === 'page' && ! pattern.postTypes ) ||
( Array.isArray( pattern.postTypes ) &&
pattern.postTypes.includes( postType ) )
);
} );
}, [ postType, blockPatternsWithPostContentBlockType ] );
}

function PatternSelection( { blockPatterns, onChoosePattern } ) {
const shownBlockPatterns = useAsyncList( blockPatterns );
const { resetEditorBlocks } = useDispatch( editorStore );
return (
<BlockPatternsList
blockPatterns={ blockPatterns }
shownPatterns={ shownBlockPatterns }
onClickPattern={ ( _pattern, blocks ) => {
resetEditorBlocks( blocks );
onChoosePattern();
} }
/>
);
}

function StartPageOptionsModal( { onClose } ) {
const startPatterns = useStartPatterns();
const hasStartPattern = startPatterns.length > 0;

if ( ! hasStartPattern ) {
return null;
}

return (
<Modal
className="edit-post-start-page-options__modal"
title={ __( 'Choose a pattern' ) }
isFullScreen
onRequestClose={ onClose }
>
<div className="edit-post-start-page-options__modal-content">
<PatternSelection
blockPatterns={ startPatterns }
onChoosePattern={ onClose }
/>
</div>
</Modal>
);
}

export default function StartPageOptions() {
const [ isClosed, setIsClosed ] = useState( false );
const shouldEnableModal = useSelect( ( select ) => {
const { isCleanNewPost } = select( editorStore );
const { resetEditorBlocks } = useDispatch( editorStore );
const { shouldEnableModal, postType } = useSelect( ( select ) => {
const { isCleanNewPost, getCurrentPostType } = select( editorStore );
const { isEditingTemplate, isFeatureActive } = select( editPostStore );

return (
! isEditingTemplate() &&
! isFeatureActive( 'welcomeGuide' ) &&
isCleanNewPost()
);
return {
shouldEnableModal:
! isEditingTemplate() &&
! isFeatureActive( 'welcomeGuide' ) &&
isCleanNewPost(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's possible to replicate isCleanNewPost in the site editor because it checks for pages in the auto-draft status (never been saved). The site editor creates new pages as draft. The main drawback is that if the page has no content when saved and you reload, the modal shows again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modal also showed again for me if the post did have content.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a play around with this and the only way I could get it to not load the modal when refreshing a draft page with content was by adding something like:

			const pageRecord = getEntityRecord(
				'postType',
				'page',
				context?.postId
			);
			const hasContent = pageRecord?.content?.raw !== '';

			if ( hasContent || ! isEditingPage || isWelcomeGuideOpen ) {
				return { shouldOpenModal: false };
			}

I don't think it is a big issue for the modal to load again on a page refresh of a still empty page, but it might be worth stopping it for pages with content. But maybe I am missing something if you are only seeing it on empty page reloads?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main drawback is that if the page has no content when saved and you reload, the modal shows again.

We are protected against this, because a completely empty page cannot be saved. It needs to have a non-empty title, excerpt or content. See the isEditedPostSaveable selector.

postType: getCurrentPostType(),
};
}, [] );

if ( ! shouldEnableModal || isClosed ) {
return null;
}

return <StartPageOptionsModal onClose={ () => setIsClosed( true ) } />;
return (
<StarterPatternsModal
postType={ postType }
onChoosePattern={ ( pattern, blocks ) => {
resetEditorBlocks( blocks );
setIsClosed( true );
} }
onRequestClose={ () => setIsClosed( true ) }
/>
);
}
2 changes: 2 additions & 0 deletions packages/edit-site/src/components/editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import InserterSidebar from '../secondary-sidebar/inserter-sidebar';
import ListViewSidebar from '../secondary-sidebar/list-view-sidebar';
import WelcomeGuide from '../welcome-guide';
import StartTemplateOptions from '../start-template-options';
import StartPageOptions from '../start-page-options';
import { store as editSiteStore } from '../../store';
import { GlobalStylesRenderer } from '../global-styles-renderer';
import useTitle from '../routes/use-title';
Expand Down Expand Up @@ -194,6 +195,7 @@ export default function Editor( { listViewToggleElement, isLoading } ) {
<BlockContextProvider value={ blockContext }>
<SidebarComplementaryAreaFills />
{ isEditMode && <StartTemplateOptions /> }
{ isEditMode && <StartPageOptions /> }
<InterfaceSkeleton
isDistractionFree={ true }
enableRegionNavigation={ false }
Expand Down
79 changes: 79 additions & 0 deletions packages/edit-site/src/components/start-page-options/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* WordPress dependencies
*/
import {
store as blockEditorStore,
StarterPatternsModal,
} from '@wordpress/block-editor';
import { useDispatch, useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { store as preferencesStore } from '@wordpress/preferences';

/**
* Internal dependencies
*/
import { store as editSiteStore } from '../../store';

export default function StartPageOptions() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why duplicate this here. I thought that personally we should just move the whole component to the "editor" package (without extracting things to block-editor) and render the component directly within EditorProvider. (And yes, we need to relax the change to only check for "content" and not "content+title")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@youknowriad Back when I worked on this there were a bunch of inconsistencies between the editors (I don't think there was even a consistent to get the current page id), so I extracted all the common stuff to a new StarterPatternsModal component. Those inconsistencies might not exist any more today with all the unification efforts, so using one component could be a possibility.

I won't have any bandwidth to work on this till post WordPress 6.5, so feel free to take over.

const { replaceInnerBlocks } = useDispatch( blockEditorStore );
const [ isClosed, setIsClosed ] = useState( false );
const { shouldOpenModal, postType, rootClientId } = useSelect(
( select ) => {
const { hasPageContentFocus, getEditedPostContext } =
select( editSiteStore );
const context = getEditedPostContext();
const isEditingPage =
context?.postType === 'page' &&
context?.postId &&
hasPageContentFocus();
const isWelcomeGuideOpen = select( preferencesStore ).get(
'core/edit-site',
'welcomeGuide'
);
if ( ! isEditingPage || isWelcomeGuideOpen ) {
return { shouldOpenModal: false };
}

const { __experimentalGetGlobalBlocksByName, getBlock } =
select( blockEditorStore );
const [ contentBlockClientId ] =
__experimentalGetGlobalBlocksByName( 'core/post-content' );
if ( ! contentBlockClientId ) {
return { shouldOpenModal: false };
}

const contentBlock = getBlock( contentBlockClientId );
if ( contentBlock?.innerBlocks?.length ) {
return { shouldOpenModal: false };
}

return {
shouldOpenModal: true,
postType: context.postType,
rootClientId: contentBlockClientId,
};
},
[]
);

if ( isClosed || ! shouldOpenModal ) {
return null;
}

return (
<StarterPatternsModal
postType={ postType }
rootClientId={ rootClientId }
onChoosePattern={ ( pattern, blocks ) => {
setIsClosed( true );
const selectInsertedBlocks = true;
replaceInnerBlocks(
rootClientId,
blocks,
selectInsertedBlocks
);
} }
onRequestClose={ () => setIsClosed( true ) }
/>
);
}
Loading