diff --git a/lib/full-site-editing/edit-site-page.php b/lib/full-site-editing/edit-site-page.php index 3a0a8557ec5c7..3ee017395b6ee 100644 --- a/lib/full-site-editing/edit-site-page.php +++ b/lib/full-site-editing/edit-site-page.php @@ -78,8 +78,10 @@ function gutenberg_get_editor_styles() { /** * Initialize the Gutenberg Templates List Page. + * + * @param array $settings The editor settings. */ -function gutenberg_edit_site_list_init() { +function gutenberg_edit_site_list_init( $settings ) { wp_enqueue_script( 'wp-edit-site' ); wp_enqueue_style( 'wp-edit-site' ); wp_enqueue_media(); @@ -111,10 +113,11 @@ function gutenberg_edit_site_list_init() { 'wp-edit-site', sprintf( 'wp.domReady( function() { - wp.editSite.initializeList( "%s", "%s" ); + wp.editSite.initializeList( "%s", "%s", %s ); } );', 'edit-site-editor', - $template_type + $template_type, + wp_json_encode( $settings ) ) ); } @@ -142,8 +145,18 @@ static function( $classes ) { } ); + $custom_settings = array( + 'siteUrl' => site_url(), + 'postsPerPage' => get_option( 'posts_per_page' ), + 'styles' => gutenberg_get_editor_styles(), + 'defaultTemplateTypes' => gutenberg_get_indexed_default_template_types(), + 'defaultTemplatePartAreas' => get_allowed_block_template_part_areas(), + '__experimentalBlockPatterns' => WP_Block_Patterns_Registry::get_instance()->get_all_registered(), + '__experimentalBlockPatternCategories' => WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered(), + ); + if ( gutenberg_is_edit_site_list_page() ) { - return gutenberg_edit_site_list_init(); + return gutenberg_edit_site_list_init( $custom_settings ); } /** @@ -154,15 +167,6 @@ static function( $classes ) { */ $current_screen->is_block_editor( true ); - $custom_settings = array( - 'siteUrl' => site_url(), - 'postsPerPage' => get_option( 'posts_per_page' ), - 'styles' => gutenberg_get_editor_styles(), - 'defaultTemplateTypes' => gutenberg_get_indexed_default_template_types(), - 'defaultTemplatePartAreas' => get_allowed_block_template_part_areas(), - '__experimentalBlockPatterns' => WP_Block_Patterns_Registry::get_instance()->get_all_registered(), - '__experimentalBlockPatternCategories' => WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered(), - ); $site_editor_context = new WP_Block_Editor_Context(); $settings = gutenberg_get_block_editor_settings( $custom_settings, $site_editor_context ); $active_global_styles_id = WP_Theme_JSON_Resolver_Gutenberg::get_user_custom_post_type_id(); diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index d9651201a3873..f1c59559f31fc 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -139,7 +139,7 @@ $z-layers: ( // Should be above the popover (dropdown) ".reusable-blocks-menu-items__convert-modal": 1000001, - ".edit-site-template-part-converter__modal": 1000001, + ".edit-site-create-template-part-modal": 1000001, // ...Except for popovers immediately beneath wp-admin menu on large breakpoints ".components-popover.block-editor-inserter__popover": 99999, diff --git a/packages/e2e-tests/specs/experiments/template-part.test.js b/packages/e2e-tests/specs/experiments/template-part.test.js index 8f18da2b88006..f670d9026c144 100644 --- a/packages/e2e-tests/specs/experiments/template-part.test.js +++ b/packages/e2e-tests/specs/experiments/template-part.test.js @@ -19,7 +19,7 @@ import { import { siteEditor } from '../../experimental-features'; const templatePartNameInput = - '.edit-site-template-part-converter__modal .components-text-control__input'; + '.edit-site-create-template-part-modal .components-text-control__input'; describe( 'Template Part', () => { beforeAll( async () => { diff --git a/packages/edit-site/src/components/add-new-template/index.js b/packages/edit-site/src/components/add-new-template/index.js new file mode 100644 index 0000000000000..89abbc0aa4614 --- /dev/null +++ b/packages/edit-site/src/components/add-new-template/index.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import NewTemplate from './new-template'; +import NewTemplatePart from './new-template-part'; + +export default function AddNewTemplate( { templateType = 'wp_template' } ) { + const postType = useSelect( + ( select ) => select( coreStore ).getPostType( templateType ), + [ templateType ] + ); + + if ( ! postType ) { + return null; + } + + if ( templateType === 'wp_template' ) { + return ; + } else if ( templateType === 'wp_template_part' ) { + return ; + } + + return null; +} diff --git a/packages/edit-site/src/components/add-new-template/new-template-part.js b/packages/edit-site/src/components/add-new-template/new-template-part.js new file mode 100644 index 0000000000000..94b51a1a99c78 --- /dev/null +++ b/packages/edit-site/src/components/add-new-template/new-template-part.js @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import { kebabCase } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { Button } from '@wordpress/components'; +import { addQueryArgs } from '@wordpress/url'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import CreateTemplatePartModal from '../create-template-part-modal'; + +export default function NewTemplatePart( { postType } ) { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + + async function createTemplatePart( { title, area } ) { + if ( ! title ) { + return; + } + + const templatePart = await apiFetch( { + path: '/wp/v2/template-parts', + method: 'POST', + data: { + slug: kebabCase( title ), + title, + content: '', + area, + }, + } ); + + // Navigate to the created template part editor. + window.location.search = addQueryArgs( '', { + page: 'gutenberg-edit-site', + postId: templatePart.id, + postType: 'wp_template_part', + } ); + + // Wait for async navigation to happen before closing the modal. + await new Promise( () => {} ); + } + + return ( + <> + + { isModalOpen && ( + setIsModalOpen( false ) } + onCreate={ createTemplatePart } + /> + ) } + + ); +} diff --git a/packages/edit-site/src/components/add-new-template/new-template.js b/packages/edit-site/src/components/add-new-template/new-template.js new file mode 100644 index 0000000000000..55719e6ec71bb --- /dev/null +++ b/packages/edit-site/src/components/add-new-template/new-template.js @@ -0,0 +1,117 @@ +/** + * External dependencies + */ +import { filter, find, includes, map } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + DropdownMenu, + MenuGroup, + MenuItem, + NavigableMenu, +} from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as editorStore } from '@wordpress/editor'; +import { addQueryArgs } from '@wordpress/url'; +import apiFetch from '@wordpress/api-fetch'; + +const DEFAULT_TEMPLATE_SLUGS = [ + 'front-page', + 'single-post', + 'page', + 'archive', + 'search', + '404', + 'index', +]; + +export default function NewTemplate( { postType } ) { + const { templates, defaultTemplateTypes } = useSelect( + ( select ) => ( { + templates: select( coreStore ).getEntityRecords( + 'postType', + 'wp_template' + ), + defaultTemplateTypes: select( + editorStore + ).__experimentalGetDefaultTemplateTypes(), + } ), + [] + ); + + async function createTemplate( { slug } ) { + const { title, description } = find( defaultTemplateTypes, { slug } ); + + const template = await apiFetch( { + path: '/wp/v2/templates', + method: 'POST', + data: { + excerpt: description, + // Slugs need to be strings, so this is for template `404` + slug: slug.toString(), + status: 'publish', + title, + }, + } ); + + // Navigate to the created template editor. + window.location.search = addQueryArgs( '', { + page: 'gutenberg-edit-site', + postId: template.id, + postType: 'wp_template', + } ); + } + + const existingTemplateSlugs = map( templates, 'slug' ); + + const missingTemplates = filter( + defaultTemplateTypes, + ( template ) => + includes( DEFAULT_TEMPLATE_SLUGS, template.slug ) && + ! includes( existingTemplateSlugs, template.slug ) + ); + + if ( ! missingTemplates.length ) { + return null; + } + + return ( + + { () => ( + + + { map( + missingTemplates, + ( { title, description, slug } ) => ( + { + createTemplate( { slug } ); + // We will be navigated way so no need to close the dropdown. + } } + > + { title } + + ) + ) } + + + ) } + + ); +} diff --git a/packages/edit-site/src/components/add-new-template/style.scss b/packages/edit-site/src/components/add-new-template/style.scss new file mode 100644 index 0000000000000..78527882b330c --- /dev/null +++ b/packages/edit-site/src/components/add-new-template/style.scss @@ -0,0 +1,11 @@ +.edit-site-new-template-dropdown { + .components-dropdown-menu__toggle { + padding: 6px 12px; + } + + .edit-site-new-template-dropdown__popover { + @include break-small() { + min-width: 300px; + } + } +} diff --git a/packages/edit-site/src/components/create-template-part-modal/index.js b/packages/edit-site/src/components/create-template-part-modal/index.js new file mode 100644 index 0000000000000..bbdf7434a466b --- /dev/null +++ b/packages/edit-site/src/components/create-template-part-modal/index.js @@ -0,0 +1,132 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { + Icon, + BaseControl, + TextControl, + Flex, + FlexItem, + FlexBlock, + Button, + Modal, + __experimentalRadioGroup as RadioGroup, + __experimentalRadio as Radio, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { useInstanceId } from '@wordpress/compose'; +import { store as editorStore } from '@wordpress/editor'; +import { check } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { TEMPLATE_PART_AREA_GENERAL } from '../../store/constants'; + +export default function CreateTemplatePartModal( { closeModal, onCreate } ) { + const [ title, setTitle ] = useState( '' ); + const [ area, setArea ] = useState( TEMPLATE_PART_AREA_GENERAL ); + const [ isSubmitting, setIsSubmitting ] = useState( false ); + const instanceId = useInstanceId( CreateTemplatePartModal ); + + const templatePartAreas = useSelect( + ( select ) => + select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + [] + ); + + return ( + +
{ + event.preventDefault(); + if ( ! title ) { + return; + } + setIsSubmitting( true ); + await onCreate( { title, area } ); + setIsSubmitting( false ); + closeModal(); + } } + > + + + + { templatePartAreas.map( + ( { icon, label, area: value, description } ) => ( + + + + + + + { label } +
{ description }
+
+ + + { area === value && ( + + ) } + +
+
+ ) + ) } +
+
+ + + + + + + + + +
+ ); +} diff --git a/packages/edit-site/src/components/template-part-converter/style.scss b/packages/edit-site/src/components/create-template-part-modal/style.scss similarity index 64% rename from packages/edit-site/src/components/template-part-converter/style.scss rename to packages/edit-site/src/components/create-template-part-modal/style.scss index 9a3ab7aa41c74..71bba3db9974d 100644 --- a/packages/edit-site/src/components/template-part-converter/style.scss +++ b/packages/edit-site/src/components/create-template-part-modal/style.scss @@ -1,5 +1,5 @@ -.edit-site-template-part-converter__modal { - z-index: z-index(".edit-site-template-part-converter__modal"); +.edit-site-create-template-part-modal { + z-index: z-index(".edit-site-create-template-part-modal"); .components-modal__frame { @include break-small { @@ -9,21 +9,21 @@ } -.edit-site-template-part-converter__convert-modal-actions { +.edit-site-create-template-part-modal__modal-actions { padding-top: $grid-unit-15; } -.edit-site-template-part-converter__area-base-control .components-base-control__label { +.edit-site-create-template-part-modal__area-base-control .components-base-control__label { margin: $grid-unit-20 0 $grid-unit-10; cursor: auto; } -.edit-site-template-part-converter__area-radio-group { +.edit-site-create-template-part-modal__area-radio-group { width: 100%; border: $border-width solid $gray-700; border-radius: 2px; - .components-button.edit-site-template-part-converter__area-radio { + .components-button.edit-site-create-template-part-modal__area-radio { display: block; width: 100%; height: 100%; @@ -56,12 +56,12 @@ color: $gray-900; cursor: auto; - .edit-site-template-part-converter__option-label div { + .edit-site-create-template-part-modal__option-label div { color: $gray-600; } } - .edit-site-template-part-converter__option-label { + .edit-site-create-template-part-modal__option-label { padding-top: $grid-unit-05; white-space: normal; @@ -71,7 +71,7 @@ } } - .edit-site-template-part-converter__checkbox { + .edit-site-create-template-part-modal__checkbox { margin-left: auto; min-width: $grid-unit-30; } diff --git a/packages/edit-site/src/components/list/header.js b/packages/edit-site/src/components/list/header.js index e090dddfb3311..4f98ecf7843d5 100644 --- a/packages/edit-site/src/components/list/header.js +++ b/packages/edit-site/src/components/list/header.js @@ -3,10 +3,12 @@ */ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { - __experimentalHeading as Heading, - Button, -} from '@wordpress/components'; +import { __experimentalHeading as Heading } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import AddNewTemplate from '../add-new-template'; export default function Header( { templateType } ) { const postType = useSelect( @@ -24,8 +26,8 @@ export default function Header( { templateType } ) { { postType.labels?.name } -
- +
+
); diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index 972bda61d6eff..e044aaf052f7f 100644 --- a/packages/edit-site/src/components/list/style.scss +++ b/packages/edit-site/src/components/list/style.scss @@ -27,6 +27,11 @@ } } +.edit-site-list-header__right { + // Creating a stacking context so that it won't be covered by title. + position: relative; +} + .edit-site { .edit-site-list { .interface-interface-skeleton__editor { diff --git a/packages/edit-site/src/components/template-part-converter/convert-to-template-part.js b/packages/edit-site/src/components/template-part-converter/convert-to-template-part.js index 7274344816f1f..35f8428b60dac 100644 --- a/packages/edit-site/src/components/template-part-converter/convert-to-template-part.js +++ b/packages/edit-site/src/components/template-part-converter/convert-to-template-part.js @@ -6,61 +6,36 @@ import { kebabCase } from 'lodash'; /** * WordPress dependencies */ -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { BlockSettingsMenuControls, store as blockEditorStore, } from '@wordpress/block-editor'; -import { - MenuItem, - Icon, - BaseControl, - TextControl, - Flex, - FlexItem, - FlexBlock, - Button, - Modal, - __experimentalRadioGroup as RadioGroup, - __experimentalRadio as Radio, -} from '@wordpress/components'; -import { useInstanceId } from '@wordpress/compose'; +import { MenuItem } from '@wordpress/components'; import { createBlock, serialize } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; import { store as noticesStore } from '@wordpress/notices'; -import { store as editorStore } from '@wordpress/editor'; -import { check } from '@wordpress/icons'; /** * Internal dependencies */ -import { TEMPLATE_PART_AREA_GENERAL } from '../../store/constants'; +import CreateTemplatePartModal from '../create-template-part-modal'; export default function ConvertToTemplatePart( { clientIds, blocks } ) { - const instanceId = useInstanceId( ConvertToTemplatePart ); const [ isModalOpen, setIsModalOpen ] = useState( false ); - const [ title, setTitle ] = useState( '' ); const { replaceBlocks } = useDispatch( blockEditorStore ); const { saveEntityRecord } = useDispatch( coreStore ); const { createSuccessNotice } = useDispatch( noticesStore ); - const [ area, setArea ] = useState( TEMPLATE_PART_AREA_GENERAL ); - - const templatePartAreas = useSelect( - ( select ) => - select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), - [] - ); - const onConvert = async ( templatePartTitle ) => { - const defaultTitle = __( 'Untitled Template Part' ); + const onConvert = async ( { title, area } ) => { const templatePart = await saveEntityRecord( 'postType', 'wp_template_part', { - slug: kebabCase( templatePartTitle || defaultTitle ), - title: templatePartTitle || defaultTitle, + slug: kebabCase( title ), + title, content: serialize( blocks ), area, } @@ -78,122 +53,27 @@ export default function ConvertToTemplatePart( { clientIds, blocks } ) { }; return ( - - { ( { onClose } ) => ( - <> + <> + + { ( { onClose } ) => ( { setIsModalOpen( true ); + onClose(); } } > { __( 'Make template part' ) } - { isModalOpen && ( - { - setIsModalOpen( false ); - setTitle( '' ); - } } - overlayClassName="edit-site-template-part-converter__modal" - > -
{ - event.preventDefault(); - onConvert( title ); - setIsModalOpen( false ); - setTitle( '' ); - onClose(); - } } - > - - - - { templatePartAreas.map( - ( { - icon, - label, - area: value, - description, - } ) => ( - - - - - - - { label } -
- { description } -
-
- - - { area === - value && ( - - ) } - -
-
- ) - ) } -
-
- - - - - - - - - -
- ) } - + ) } +
+ { isModalOpen && ( + { + setIsModalOpen( false ); + } } + onCreate={ onConvert } + /> ) } -
+ ); } diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 83f918dc63a94..9b9a00a7a3e54 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -12,6 +12,7 @@ import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions, __experimentalFetchUrlData as fetchUrlData, } from '@wordpress/core-data'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -73,10 +74,16 @@ export function initializeEditor( id, settings ) { * * @param {string} id ID of the root element to render the screen in. * @param {string} templateType The type of the list. "wp_template" or "wp_template_part". + * @param {Object} settings Editor settings. */ -export function initializeList( id, templateType ) { +export function initializeList( id, templateType, settings ) { const target = document.getElementById( id ); + dispatch( editorStore ).updateEditorSettings( { + defaultTemplateTypes: settings.defaultTemplateTypes, + defaultTemplatePartAreas: settings.defaultTemplatePartAreas, + } ); + render( , target ); } diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 3b46c91ed2ead..4f9dc3f418489 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -116,7 +116,8 @@ export function* removeTemplate( template ) { 'deleteEntityRecord', 'postType', template.type, - template.id + template.id, + { force: true } ); } diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index 52532cbd612e1..5d45a0f0556ca 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -86,7 +86,12 @@ describe( 'actions', () => { const it = removeTemplate( template ); expect( it.next().value ).toEqual( { actionName: 'deleteEntityRecord', - args: [ 'postType', 'wp_template_part', 'tt1-blocks//general' ], + args: [ + 'postType', + 'wp_template_part', + 'tt1-blocks//general', + { force: true }, + ], storeKey: 'core', type: '@@data/DISPATCH', } ); diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 56dddd41499ee..6502a0ae1dd51 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -8,12 +8,13 @@ @import "./components/navigation-sidebar/navigation-toggle/style.scss"; @import "./components/navigation-sidebar/navigation-panel/style.scss"; @import "./components/list/style.scss"; +@import "./components/add-new-template/style.scss"; @import "./components/sidebar/style.scss"; @import "./components/sidebar/settings-header/style.scss"; @import "./components/sidebar/template-card/style.scss"; @import "./components/editor/style.scss"; @import "./components/template-details/style.scss"; -@import "./components/template-part-converter/style.scss"; +@import "./components/create-template-part-modal/style.scss"; @import "./components/secondary-sidebar/style.scss"; @import "./components/welcome-guide/style.scss";