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

Templates: add postTypes and isCustom fields & filters #62075

Closed
wants to merge 11 commits into from
3 changes: 3 additions & 0 deletions backport-changelog/6.6/6660.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/6660

* https://github.com/WordPress/gutenberg/pull/62075
40 changes: 40 additions & 0 deletions lib/compat/wordpress-6.6/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,43 @@ function gutenberg_register_global_styles_revisions_endpoints() {
}

add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' );

/**
* Registers a new post_type field for the Templates REST API route.
*/
function gutenberg_register_template_post_type_field() {
register_rest_field(
'wp_template',
'post_types',
array(
'get_callback' => 'gutenberg_rest_template_post_type_callback',
'schema' => array(
'description' => __( 'The post types the template is intended for.', 'gutenberg' ),
'type' => 'array',
'readonly' => true,
),
)
);
}
add_action( 'rest_api_init', 'gutenberg_register_template_post_type_field' );

/**
* Callback for the post_type field in the Templates REST API route.
*
* @param array $template The template object.
*
* @return array The post type the template is intended for.
*/
function gutenberg_rest_template_post_type_callback( $item ) {

$template_metadata = _get_block_template_file( 'wp_template', $item['slug'] );
if ( null === $template_metadata ) {
return null;
}
Comment on lines +120 to +122
Copy link
Contributor

Choose a reason for hiding this comment

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

Super minor, but this check isn't needed before isset.

Suggested change
if ( null === $template_metadata ) {
return null;
}


if ( isset( $template_metadata['postTypes'] ) ) {
return $template_metadata['postTypes'];
}

return null;
}
102 changes: 100 additions & 2 deletions packages/edit-site/src/components/page-templates/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { useAddedBy } from './hooks';
import {
TEMPLATE_POST_TYPE,
OPERATOR_IS_ANY,
OPERATOR_IS,
LAYOUT_GRID,
LAYOUT_TABLE,
LAYOUT_LIST,
Expand Down Expand Up @@ -80,7 +81,7 @@ const DEFAULT_VIEW = {
},
// All fields are visible by default, so it's
// better to keep track of the hidden ones.
hiddenFields: [ 'preview' ],
hiddenFields: [ 'preview', 'postTypes', 'isCustom' ],
layout: defaultConfigPerViewType[ LAYOUT_GRID ],
filters: [],
};
Expand Down Expand Up @@ -184,6 +185,22 @@ function Preview( { item, viewType } ) {
);
}

// This maps the template slug to the post types it should be available for.
// https://developer.wordpress.org/themes/basics/template-hierarchy/#visual-overview
// It only addresses primary and secondary templates, but not tertiary (aka variable) templates.
const TEMPLATE_TO_POST_TYPE = {
// 1. Primary templates.
index: [ 'post', 'page' ],
singular: [ 'post', 'page' ],
single: [ 'post' ],
page: [ 'page' ],
// 2. Secondary templates.
'single-post': [ 'post' ],
};

const CUSTOM_TEMPLATE = __( 'Custom' );
const NOT_CUSTOM_TEMPLATE = __( 'Not custom' );

export default function PageTemplates() {
const { params } = useLocation();
const { activeView = 'all', layout } = params;
Expand Down Expand Up @@ -229,6 +246,23 @@ export default function PageTemplates() {
per_page: -1,
}
);
const { records: types } = useEntityRecords( 'root', 'postType', {
per_page: -1,
context: 'edit',
} );

const registeredPostTypes = useMemo( () => {
const result =
types
?.filter( ( type ) => type.viewable && type.supports.editor ) // supports.editor is a proxy for supporting templates.
.map( ( { name, slug } ) => ( { name, slug } ) )
.reduce( ( acc, current ) => {
acc[ current.slug ] = current.name;
return acc;
}, {} ) || {};
return result;
}, [ types ] );

const history = useHistory();
const onSelectionChange = useCallback(
( items ) => {
Expand Down Expand Up @@ -256,6 +290,27 @@ export default function PageTemplates() {
} ) );
}, [ records ] );

const getPostTypesFromItem = ( item ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be a useCallback, no?

// This logic replicates querying the REST templates endpoint with a post_type parameter.
// https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/block-template-utils.php#L1077
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be a permalink and not just relative to trunk, otherwise it will go (and already has gone) stale.

//
// Additionaly, it also maps the the WordPress template hierarchy to known post types.
//
// This is how it works:
//
// 1. Return the list of post types defined by the item, if any.
// 2. If a template is custom, add it for any CPT.
// 3. Consider the template hierarchy and how it maps to post types. E.g.: single, page, etc.
// 4. If none of the above, default to no post types.

return (
item.post_types ||
( item.is_custom && Object.keys( registeredPostTypes ) ) ||
TEMPLATE_TO_POST_TYPE[ item.slug ] ||
[]
);
};

const fields = useMemo(
() => [
{
Expand Down Expand Up @@ -315,8 +370,51 @@ export default function PageTemplates() {
elements: authors,
width: '1%',
},
{
header: __( 'Post types' ),
id: 'postTypes',
getValue: ( { item } ) => getPostTypesFromItem( item ),
render: ( { item } ) => {
const postTypes = getPostTypesFromItem( item );
if ( ! postTypes || ! postTypes.length ) {
return __( 'n/a' );
}

if (
postTypes.length ===
Object.keys( registeredPostTypes ).length
) {
return __( 'Any' );
}

return postTypes
.map(
( postType ) =>
registeredPostTypes[ postType ] || postType
)
.join( ',' );
},
elements: Object.keys( registeredPostTypes ).map( ( key ) => ( {
value: key,
label: registeredPostTypes[ key ],
} ) ),
},
{
header: __( 'Type' ),
Copy link
Member Author

Choose a reason for hiding this comment

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

I've made the following decisions about how to present the isCustom field to the user:

  • Called it Type. Alternatives: Template type, Is custom?.
  • Displayed it as a badge field in the grid layout, as we do for other boolean-type fields (sync status in patterns).
  • In any layout, it'd only render "Custom" if the template is custom. If it is not, it'll render an empty value. Alternatively, it could say "Default".

This is a concept that requires a certain knowledge of WordPress templates, so I was unsure about how to simplify it further. Happy to hear thoughts.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah this is very tricky.

  • I'm not sure about displaying the type as a badge; it might not be obvious what this refers to without the label?
  • 'Type' suggests multiple options... It's odd that the column is either empty or 'custom'. Would it be feasible to add other types? E.g. Archive, Single, Utility? Combined with the post type filter this could potentially work quite well.
  • I'd probably hide this field by default.

Copy link
Member Author

@oandregal oandregal May 29, 2024

Choose a reason for hiding this comment

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

It's odd that the column is either empty or 'custom'. Would it be feasible to add other types? E.g. Archive, Single, Utility? Combined with the post type filter this could potentially work quite well.

To be honest, I'm struggling to see the value of this field/filter in isolation: having the postTypes one enables users to search for templates bound that those post types — which are considered custom already, so this Type is a redundant. Is there any other value to it I'm not seeing?

For adding more types: is there any stablished categorization of templates I can look at? That could be useful, though I wouldn't want to introduce a new concept to templates in this PR.

If this field doesn't provide much value right now, I'd rather start with postTypes only.

Copy link
Contributor

@jameskoster jameskoster May 29, 2024

Choose a reason for hiding this comment

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

I think the main motivator was to provide a way to replicate the 'Custom' section in 6.5's Templates sidebar:

Screenshot 2024-05-29 at 14 49 17

Without this filter it wouldn't be possible to view custom templates easily which could be seen as a regression? Possibly a bad idea, but could the filter be hidden in the UI but still function as part of a 'Custom' view? I think pattern categories work a similar way.


The template hierarchy resolves by page type, essentially; "Archive", "Singular", "Front Page", "Posts Page", "Search Results", "404".

Grouping by "Archive", "Singular", and "Utility" seems reasonable, but I don't think it's defined in the code. Agree it seems late to be adding this complexity.

Copy link
Member Author

Choose a reason for hiding this comment

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

The template hierarchy resolves by page type, essentially; "Archive", "Singular", "Front Page", "Posts Page", "Search Results", "404".

I feel this is the best solution, and I'm also wary to introduce it this close to the beta.

If we agree exposing those types would be best long-term, a plan for 6.6 can be:

  • naming the field "Type", so we can reuse it later
  • its values for now are "Custom" / "Not custom" — we can add more granularity in 6.7 ("Archive", etc.)

Thoughts?

Pushed this approach at 895214a. Made the field hidden by default and not a badge as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

How do you feel about implementing the filter with custom / not custom as values, but hiding it in the UI for 6.6?

We can then use it to add a 'Custom' view, including a description about what custom templates are. This would be similar (I think) to categories in the Patterns page.

Screenshot 2024-05-30 at 12 23 21

This would be dependent on updating the frame titles, but there's a PR for that ready to go.

id: 'isCustom',
getValue: ( { item } ) => !! item.is_custom,
render: ( { item } ) =>
!! item.is_custom ? CUSTOM_TEMPLATE : NOT_CUSTOM_TEMPLATE,
elements: [
{ value: true, label: CUSTOM_TEMPLATE },
{ value: false, label: NOT_CUSTOM_TEMPLATE },
],
filterBy: {
operators: [ OPERATOR_IS ],
},
},
],
[ authors, view.type ]
[ authors, view.type, registeredPostTypes, getPostTypesFromItem ]
);

const { data, paginationInfo } = useMemo( () => {
Expand Down
Loading