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: Author nicename template creation ability #42165

Merged
merged 9 commits into from
Aug 2, 2022
43 changes: 24 additions & 19 deletions packages/edit-site/src/components/add-new-template/new-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
useDefaultTemplateTypes,
useTaxonomiesMenuItems,
usePostTypeMenuItems,
useAuthorMenuItem,
} from './utils';
import AddCustomGenericTemplateModal from './add-custom-generic-template-modal';
import { useHistory } from '../routes';
Expand Down Expand Up @@ -243,26 +244,30 @@ function useMissingTemplates(
useTaxonomiesMenuItems( onClickMenuItem );
const { defaultPostTypesMenuItems, postTypesMenuItems } =
usePostTypeMenuItems( onClickMenuItem );
[ ...defaultTaxonomiesMenuItems, ...defaultPostTypesMenuItems ].forEach(
( menuItem ) => {
if ( ! menuItem ) {
return;
}
const matchIndex = enhancedMissingDefaultTemplateTypes.findIndex(
( template ) => template.slug === menuItem.slug
);
// Some default template types might have been filtered above from
// `missingDefaultTemplates` because they only check for the general
// template. So here we either replace or append the item, augmented
// with the check if it has available specific item to create a
// template for.
if ( matchIndex > -1 ) {
enhancedMissingDefaultTemplateTypes[ matchIndex ] = menuItem;
} else {
enhancedMissingDefaultTemplateTypes.push( menuItem );
}

const authorMenuItem = useAuthorMenuItem( onClickMenuItem );
[
...defaultTaxonomiesMenuItems,
...defaultPostTypesMenuItems,
authorMenuItem,
].forEach( ( menuItem ) => {
if ( ! menuItem ) {
return;
}
);
const matchIndex = enhancedMissingDefaultTemplateTypes.findIndex(
( template ) => template.slug === menuItem.slug
);
// Some default template types might have been filtered above from
// `missingDefaultTemplates` because they only check for the general
// template. So here we either replace or append the item, augmented
// with the check if it has available specific item to create a
// template for.
if ( matchIndex > -1 ) {
enhancedMissingDefaultTemplateTypes[ matchIndex ] = menuItem;
} else {
enhancedMissingDefaultTemplateTypes.push( menuItem );
}
} );
// Update the sort order to match the DEFAULT_TEMPLATE_SLUGS order.
enhancedMissingDefaultTemplateTypes?.sort( ( template1, template2 ) => {
return (
Expand Down
134 changes: 126 additions & 8 deletions packages/edit-site/src/components/add-new-template/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as editorStore } from '@wordpress/editor';
import { decodeEntities } from '@wordpress/html-entities';
import { useMemo } from '@wordpress/element';
import { useMemo, useCallback } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { blockMeta, post } from '@wordpress/icons';

Expand Down Expand Up @@ -408,6 +408,111 @@ export const useTaxonomiesMenuItems = ( onClickMenuItem ) => {
return taxonomiesMenuItems;
};

function useAuthorNeedsUniqueIndentifier() {
const authors = useSelect(
( select ) =>
select( coreStore ).getUsers( { who: 'authors', per_page: -1 } ),
[]
);
const authorsCountByName = useMemo( () => {
return ( authors || [] ).reduce( ( authorsCount, { name } ) => {
authorsCount[ name ] = ( authorsCount[ name ] || 0 ) + 1;
return authorsCount;
}, {} );
}, [ authors ] );
return useCallback(
( name ) => {
return authorsCountByName[ name ] > 1;
},
[ authorsCountByName ]
);
}

const USE_AUTHOR_MENU_ITEM_TEMPLATE_PREFIX = { user: 'author' };
const USE_AUTHOR_MENU_ITEM_QUERY_PARAMETERS = { user: { who: 'authors' } };
export function useAuthorMenuItem( onClickMenuItem ) {
const existingTemplates = useExistingTemplates();
const defaultTemplateTypes = useDefaultTemplateTypes();
const authorInfo = useEntitiesInfo(
'root',
USE_AUTHOR_MENU_ITEM_TEMPLATE_PREFIX,
USE_AUTHOR_MENU_ITEM_QUERY_PARAMETERS
);
const authorNeedsUniqueId = useAuthorNeedsUniqueIndentifier();
let authorMenuItem = defaultTemplateTypes?.find(
( { slug } ) => slug === 'author'
);
if ( ! authorMenuItem ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should add the item here even if the author template doesn't exist in the default templates, like we do for the other entities.

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 applied the feedback to be consistent with other entities, but I think as follow-up, we should not add a template even if it does not exist on default templates.
We are breaking backward compatibility. Currently a plugin can disable a template by removing it using the filter default_template_types in:

/**
 * Returns a filtered list of default template types, containing their
 * localized titles and descriptions.
 *
 * @since 5.9.0
 *
 * @return array The default template types.
 */
function get_default_block_template_types() {
	$default_template_types = array(
		'index'          => array(
			'title'       => _x( 'Index', 'Template name' ),
			'description' => __( 'Displays posts.' ),
		),
		'home'           => array(
			'title'       => _x( 'Home', 'Template name' ),
			'description' => __( 'Displays posts on the homepage, or on the Posts page if a static homepage is set.' ),
		),
		'front-page'     => array(
			'title'       => _x( 'Front Page', 'Template name' ),
			'description' => __( 'Displays the homepage.' ),
		),
		'singular'       => array(
			'title'       => _x( 'Singular', 'Template name' ),
			'description' => __( 'Displays a single post or page.' ),
		),
		'single'         => array(
			'title'       => _x( 'Single Post', 'Template name' ),
			'description' => __( 'Displays a single post.' ),
		),
		'page'           => array(
			'title'       => _x( 'Page', 'Template name' ),
			'description' => __( 'Displays a single page.' ),
		),
		'archive'        => array(
			'title'       => _x( 'Archive', 'Template name' ),
			'description' => __( 'Displays post categories, tags, and other archives.' ),
		),
		'author'         => array(
			'title'       => _x( 'Author', 'Template name' ),
			'description' => __( 'Displays latest posts written by a single author.' ),
		),
		'category'       => array(
			'title'       => _x( 'Category', 'Template name' ),
			'description' => __( 'Displays latest posts in single post category.' ),
		),
		'taxonomy'       => array(
			'title'       => _x( 'Taxonomy', 'Template name' ),
			'description' => __( 'Displays latest posts from a single post taxonomy.' ),
		),
		'date'           => array(
			'title'       => _x( 'Date', 'Template name' ),
			'description' => __( 'Displays posts from a specific date.' ),
		),
		'tag'            => array(
			'title'       => _x( 'Tag', 'Template name' ),
			'description' => __( 'Displays latest posts with a single post tag.' ),
		),
		'attachment'     => array(
			'title'       => __( 'Media' ),
			'description' => __( 'Displays individual media items or attachments.' ),
		),
		'search'         => array(
			'title'       => _x( 'Search', 'Template name' ),
			'description' => __( 'Displays search results.' ),
		),
		'privacy-policy' => array(
			'title'       => __( 'Privacy Policy' ),
			'description' => __( 'Displays the privacy policy page.' ),
		),
		'404'            => array(
			'title'       => _x( '404', 'Template name' ),
			'description' => __( 'Displays when no content is found.' ),
		),
	);

	/**
	 * Filters the list of template types.
	 *
	 * @since 5.9.0
	 *
	 * @param array $default_template_types An array of template types, formatted as [ slug => [ title, description ] ].
	 */
	return apply_filters( 'default_template_types', $default_template_types );
}

If we ignore the default code that currently removes a template will stop working because we add the template anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that's a tricky one we have to think better. When the filter was introduced though we didn't have this kind of functionality for adding more templates. We might need more feedback about what would be expected.

Another thing is that while this filter exists, users cannot actually add extra items, as they are filtered in js. The most obvious example would be the home template that already exists in this list but is filtered out in js..

authorMenuItem = {
description: __(
'Displays latest posts written by a single author.'
),
slug: 'author',
title: 'Author',
};
}
const hasGeneralTemplate = !! existingTemplates?.find(
( { slug } ) => slug === 'author'
);
if ( authorInfo.user?.hasEntities ) {
authorMenuItem = { ...authorMenuItem };
authorMenuItem.onClick = ( template ) => {
onClickMenuItem( {
type: 'root',
slug: 'user',
config: {
queryArgs: ( { search } ) => {
return {
_fields: 'id,name,slug,link',
orderBy: search ? 'name' : 'registered_date',
exclude: authorInfo.user.existingEntitiesIds,
who: 'authors',
};
},
getSpecificTemplate: ( suggestion ) => {
const needsUniqueId = authorNeedsUniqueId(
suggestion.name
);
const title = needsUniqueId
? sprintf(
// translators: %1$s: Represents the name of an author e.g: "Jorge", %2$s: Represents the slug of an author e.g: "author-jorge-slug".
__( 'Author: %1$s (%2$s)' ),
suggestion.name,
suggestion.slug
)
: sprintf(
// translators: %s: Represents the name of an author e.g: "Jorge".
__( 'Author: %s' ),
suggestion.name
);
const description = sprintf(
// translators: %s: Represents the name of an author e.g: "Jorge".
__( 'Template for Author: %s' ),
suggestion.name
);
return {
title,
description,
slug: `author-${ suggestion.slug }`,
};
},
},
labels: {
singular_name: __( 'Author' ),
search_items: __( 'Search Authors' ),
not_found: __( 'No authors found.' ),
all_items: __( 'All Authors' ),
},
hasGeneralTemplate,
template,
} );
};
}
if ( ! hasGeneralTemplate || authorInfo.user?.hasEntities ) {
return authorMenuItem;
}
}

/**
* Helper hook that filters all the existing templates by the given
* object with the entity's slug as key and the template prefix as value.
Expand Down Expand Up @@ -456,11 +561,16 @@ const useExistingTemplateSlugs = ( templatePrefixes ) => {
* Helper hook that finds the existing records with an associated template,
* as they need to be excluded from the template suggestions.
*
* @param {string} entityName The entity's name.
* @param {Record<string,string>} templatePrefixes An object with the entity's slug as key and the template prefix as value.
* @param {string} entityName The entity's name.
* @param {Record<string,string>} templatePrefixes An object with the entity's slug as key and the template prefix as value.
* @param {Record<string,Object>} additionalQueryParameters An object with the entity's slug as key and additional query parameters as value.
* @return {Record<string,EntitiesInfo>} An object with the entity's slug as key and the existing records as value.
*/
const useTemplatesToExclude = ( entityName, templatePrefixes ) => {
const useTemplatesToExclude = (
entityName,
templatePrefixes,
additionalQueryParameters = {}
) => {
const slugsToExcludePerEntity =
useExistingTemplateSlugs( templatePrefixes );
const recordsToExcludePerEntity = useSelect(
Expand All @@ -473,6 +583,7 @@ const useTemplatesToExclude = ( entityName, templatePrefixes ) => {
_fields: 'id',
context: 'view',
slug: slugsWithTemplates,
...additionalQueryParameters[ slug ],
} );
if ( entitiesWithTemplates?.length ) {
accumulator[ slug ] = entitiesWithTemplates;
Expand All @@ -497,14 +608,20 @@ const useTemplatesToExclude = ( entityName, templatePrefixes ) => {
* First we need to find the existing records with an associated template,
* to query afterwards for any remaining record, by excluding them.
*
* @param {string} entityName The entity's name.
* @param {Record<string,string>} templatePrefixes An object with the entity's slug as key and the template prefix as value.
* @param {string} entityName The entity's name.
* @param {Record<string,string>} templatePrefixes An object with the entity's slug as key and the template prefix as value.
* @param {Record<string,Object>} additionalQueryParameters An object with the entity's slug as key and additional query parameters as value.
* @return {Record<string,EntitiesInfo>} An object with the entity's slug as key and the EntitiesInfo as value.
*/
const useEntitiesInfo = ( entityName, templatePrefixes ) => {
const useEntitiesInfo = (
entityName,
templatePrefixes,
additionalQueryParameters = {}
) => {
const recordsToExcludePerEntity = useTemplatesToExclude(
entityName,
templatePrefixes
templatePrefixes,
additionalQueryParameters
);
const entitiesInfo = useSelect(
( select ) => {
Expand All @@ -523,6 +640,7 @@ const useEntitiesInfo = ( entityName, templatePrefixes ) => {
_fields: 'id',
context: 'view',
exclude: existingEntitiesIds,
...additionalQueryParameters[ slug ],
}
)?.length,
existingEntitiesIds,
Expand Down