Skip to content

Commit

Permalink
[Site Editor]: Expand the template types that can be added - single c…
Browse files Browse the repository at this point in the history
…ustom post type and specific posts templates (#41189)

* WIP: [Site Editor]: Expand the template types that can be added

* Fix typo

* Remove icons in general vs specific selection modal

* Update post lookup styling to match linkcontrol

* Fix modal width, set suggestion list height

* fix linting

* remove `archive` additions and check for available posts by excluding the existing ones before adding the menu item

* add safeguard for `existingTemplates`

* add link in suggestions list

* clean up comments

* update formatting after trunk change

* use gutenberg_get_block_template for updates in templates

* add `icon` in Post Types REST API and handle only dashicons in the templates list for now

* use `rest_prepare_post_type` filter

* address feedback  part 1

* update jsdoc

* remove mixin

* try different labels

* Apply suggestions from code review

Co-authored-by: Miguel Fonseca <miguelcsf@gmail.com>

* address feedback

Co-authored-by: James Koster <james@jameskoster.co.uk>
Co-authored-by: Miguel Fonseca <miguelcsf@gmail.com>
  • Loading branch information
3 people authored Jun 21, 2022
1 parent 6250a1f commit 41c7450
Show file tree
Hide file tree
Showing 7 changed files with 680 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,75 @@ public function create_item( $request ) {
);
}

/**
* Updates a single template.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$template = gutenberg_get_block_template( $request['id'], $this->post_type );
if ( ! $template ) {
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.', 'gutenberg' ), array( 'status' => 404 ) );
}

$post_before = get_post( $template->wp_id );

if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
wp_delete_post( $template->wp_id, true );
$request->set_param( 'context', 'edit' );

$template = gutenberg_get_block_template( $request['id'], $this->post_type );
$response = $this->prepare_item_for_response( $template, $request );

return rest_ensure_response( $response );
}

$changes = $this->prepare_item_for_database( $request );

if ( is_wp_error( $changes ) ) {
return $changes;
}

if ( 'custom' === $template->source ) {
$update = true;
$result = wp_update_post( wp_slash( (array) $changes ), false );
} else {
$update = false;
$post_before = null;
$result = wp_insert_post( wp_slash( (array) $changes ), false );
}

if ( is_wp_error( $result ) ) {
if ( 'db_update_error' === $result->get_error_code() ) {
$result->add_data( array( 'status' => 500 ) );
} else {
$result->add_data( array( 'status' => 400 ) );
}
return $result;
}

$template = gutenberg_get_block_template( $request['id'], $this->post_type );
$fields_update = $this->update_additional_fields_for_object( $template, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}

$request->set_param( 'context', 'edit' );

$post = get_post( $template->wp_id );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );

wp_after_insert_post( $post, $update, $post_before );

$response = $this->prepare_item_for_response( $template, $request );

return rest_ensure_response( $response );
}

/**
* Prepares a single template for create or update.
*
Expand Down
15 changes: 15 additions & 0 deletions lib/compat/wordpress-6.1/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,18 @@ function gutenberg_update_templates_template_parts_rest_controller( $args, $post
return $args;
}
add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 );


/**
* Add the post type's `icon`(menu_icon) in the response.
* When we backport this change we will need to add the
* `icon` to WP_REST_Post_Types_Controller schema.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post_Type $post_type The original post type object.
*/
function gutenberg_update_post_types_rest_response( $response, $post_type ) {
$response->data['icon'] = $post_type->menu_icon;
return $response;
}
add_filter( 'rest_prepare_post_type', 'gutenberg_update_post_types_rest_response', 10, 2 );
1 change: 0 additions & 1 deletion packages/base-styles/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@
}
}


/**
* Allows users to opt-out of animations via OS-level preferences.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/**
* WordPress dependencies
*/
import { useState, useMemo, useEffect } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import {
Button,
Flex,
FlexItem,
Modal,
SearchControl,
TextHighlight,
__experimentalText as Text,
__experimentalHeading as Heading,
__unstableComposite as Composite,
__unstableUseCompositeState as useCompositeState,
__unstableCompositeItem as CompositeItem,
} from '@wordpress/components';
import { useDebounce } from '@wordpress/compose';
import { useEntityRecords } from '@wordpress/core-data';

/**
* Internal dependencies
*/
import { mapToIHasNameAndId } from './utils';

const EMPTY_ARRAY = [];
const BASE_QUERY = {
order: 'asc',
_fields: 'id,title,slug,link',
context: 'view',
};

function SuggestionListItem( {
suggestion,
search,
onSelect,
entityForSuggestions,
composite,
} ) {
const baseCssClass =
'edit-site-custom-template-modal__suggestions_list__list-item';
return (
<CompositeItem
role="option"
as={ Button }
{ ...composite }
className={ baseCssClass }
onClick={ () => {
const title = sprintf(
// translators: Represents the title of a user's custom template in the Site Editor, where %1$s is the singular name of a post type and %2$s is the name of the post, e.g. "Post: Hello, WordPress"
__( '%1$s: %2$s' ),
entityForSuggestions.labels.singular_name,
suggestion.name
);
onSelect( {
title,
description: sprintf(
// translators: Represents the description of a user's custom template in the Site Editor, e.g. "Template for Post: Hello, WordPress"
__( 'Template for %1$s' ),
title
),
slug: `single-${ entityForSuggestions.slug }-${ suggestion.slug }`,
} );
} }
>
<span className={ `${ baseCssClass }__title` }>
<TextHighlight text={ suggestion.name } highlight={ search } />
</span>
{ suggestion.link && (
<span className={ `${ baseCssClass }__info` }>
{ suggestion.link }
</span>
) }
</CompositeItem>
);
}

function SuggestionList( { entityForSuggestions, onSelect } ) {
const composite = useCompositeState( { orientation: 'vertical' } );
const [ suggestions, setSuggestions ] = useState( EMPTY_ARRAY );
// We need to track two values, the search input's value(searchInputValue)
// and the one we want to debounce(search) and make REST API requests.
const [ searchInputValue, setSearchInputValue ] = useState( '' );
const [ search, setSearch ] = useState( '' );
const debouncedSearch = useDebounce( setSearch, 250 );
const query = {
...BASE_QUERY,
search,
orderby: search ? 'relevance' : 'modified',
exclude: entityForSuggestions.postsToExclude,
per_page: search ? 20 : 10,
};
const { records: searchResults, hasResolved: searchHasResolved } =
useEntityRecords(
entityForSuggestions.type,
entityForSuggestions.slug,
query
);
useEffect( () => {
if ( search !== searchInputValue ) {
debouncedSearch( searchInputValue );
}
}, [ search, searchInputValue ] );
const entitiesInfo = useMemo( () => {
if ( ! searchResults?.length ) return EMPTY_ARRAY;
return mapToIHasNameAndId( searchResults, 'title.rendered' );
}, [ searchResults ] );
// Update suggestions only when the query has resolved.
useEffect( () => {
if ( ! searchHasResolved ) return;
setSuggestions( entitiesInfo );
}, [ entitiesInfo, searchHasResolved ] );
return (
<>
<SearchControl
onChange={ setSearchInputValue }
value={ searchInputValue }
label={ entityForSuggestions.labels.search_items }
placeholder={ entityForSuggestions.labels.search_items }
/>
{ !! suggestions?.length && (
<Composite
{ ...composite }
role="listbox"
className="edit-site-custom-template-modal__suggestions_list"
>
{ suggestions.map( ( suggestion ) => (
<SuggestionListItem
key={ suggestion.slug }
suggestion={ suggestion }
search={ search }
onSelect={ onSelect }
entityForSuggestions={ entityForSuggestions }
composite={ composite }
/>
) ) }
</Composite>
) }
{ search && ! suggestions?.length && (
<p className="edit-site-custom-template-modal__no-results">
{ entityForSuggestions.labels.not_found }
</p>
) }
</>
);
}

function AddCustomTemplateModal( { onClose, onSelect, entityForSuggestions } ) {
const [ showSearchEntities, setShowSearchEntities ] = useState(
entityForSuggestions.hasGeneralTemplate
);
const baseCssClass = 'edit-site-custom-template-modal';
return (
<Modal
title={ sprintf(
// translators: %s: Name of the post type e.g: "Post".
__( 'Add template: %s' ),
entityForSuggestions.labels.singular_name
) }
className={ baseCssClass }
closeLabel={ __( 'Close' ) }
onRequestClose={ onClose }
>
{ ! showSearchEntities && (
<>
<p>
{ __(
'Select whether to create a single template for all items or a specific one.'
) }
</p>
<Flex
className={ `${ baseCssClass }__contents` }
gap="4"
align="initial"
>
<FlexItem
isBlock
onClick={ () => {
const { slug, title, description } =
entityForSuggestions.template;
onSelect( { slug, title, description } );
} }
>
<Heading level={ 5 }>
{ entityForSuggestions.labels.all_items }
</Heading>
<Text as="span">
{
// translators: The user is given the choice to set up a template for all items of a post type, or just a specific one.
__( 'For all items' )
}
</Text>
</FlexItem>
<FlexItem
isBlock
onClick={ () => {
setShowSearchEntities( true );
} }
>
<Heading level={ 5 }>
{ entityForSuggestions.labels.singular_name }
</Heading>
<Text as="span">
{
// translators: The user is given the choice to set up a template for all items of a post type, or just a specific one.
__( 'For a specific item' )
}
</Text>
</FlexItem>
</Flex>
</>
) }
{ showSearchEntities && (
<>
<p>
{ __(
'This template will be used only for the specific item chosen.'
) }
</p>
<SuggestionList
entityForSuggestions={ entityForSuggestions }
onSelect={ onSelect }
/>
</>
) }
</Modal>
);
}

export default AddCustomTemplateModal;
Loading

0 comments on commit 41c7450

Please sign in to comment.