diff --git a/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js b/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js index 3c5980eb8088af..838ae1bb0b37a7 100644 --- a/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js +++ b/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js @@ -25,11 +25,34 @@ import { useEntityRecords } from '@wordpress/core-data'; import { mapToIHasNameAndId } from './utils'; const EMPTY_ARRAY = []; -const BASE_QUERY = { - order: 'asc', - _fields: 'id,title,slug,link', - context: 'view', -}; + +function selectSuggestion( suggestion, onSelect, entityForSuggestions ) { + const { + labels, + slug, + config: { templateSlug, templatePrefix }, + } = entityForSuggestions; + 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 or taxonomy and %2$s is the name of the post or term, e.g. "Post: Hello, WordPress", "Category: shoes" + __( '%1$s: %2$s' ), + labels.singular_name, + suggestion.name + ); + let newTemplateSlug = `${ templateSlug || slug }-${ suggestion.slug }`; + if ( templatePrefix ) { + newTemplateSlug = templatePrefix + newTemplateSlug; + } + const newTemplate = { + 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: newTemplateSlug, + }; + onSelect( newTemplate ); +} function SuggestionListItem( { suggestion, @@ -46,23 +69,9 @@ function SuggestionListItem( { 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 }`, - } ); - } } + onClick={ () => + selectSuggestion( suggestion, onSelect, entityForSuggestions ) + } > @@ -76,48 +85,73 @@ function SuggestionListItem( { ); } -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, - }; +function useDebouncedInput() { + const [ input, setInput ] = useState( '' ); + const [ debounced, setter ] = useState( '' ); + const setDebounced = useDebounce( setter, 250 ); + useEffect( () => { + if ( debounced !== input ) { + setDebounced( input ); + } + }, [ debounced, input ] ); + return [ input, setInput, debounced ]; +} + +function useSearchSuggestions( entityForSuggestions, search ) { + const { config, postsToExclude } = entityForSuggestions; + const query = useMemo( + () => ( { + order: 'asc', + _fields: 'id,name,title,slug,link', + context: 'view', + search, + orderBy: config.getOrderBy( { search } ), + exclude: postsToExclude, + per_page: search ? 20 : 10, + } ), + [ search, config, postsToExclude ] + ); 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. + const [ suggestions, setSuggestions ] = useState( EMPTY_ARRAY ); useEffect( () => { if ( ! searchHasResolved ) return; - setSuggestions( entitiesInfo ); - }, [ entitiesInfo, searchHasResolved ] ); + let newSuggestions = EMPTY_ARRAY; + if ( searchResults?.length ) { + newSuggestions = searchResults; + if ( config.recordNamePath ) { + newSuggestions = mapToIHasNameAndId( + newSuggestions, + config.recordNamePath + ); + } + } + // Update suggestions only when the query has resolved, so as to keep + // the previous results in the UI. + setSuggestions( newSuggestions ); + }, [ searchResults, searchHasResolved ] ); + return suggestions; +} + +function SuggestionList( { entityForSuggestions, onSelect } ) { + const composite = useCompositeState( { orientation: 'vertical' } ); + const [ search, setSearch, debouncedSearch ] = useDebouncedInput(); + const suggestions = useSearchSuggestions( + entityForSuggestions, + debouncedSearch + ); + const { labels } = entityForSuggestions; return ( <> { !! suggestions?.length && ( ) } - { search && ! suggestions?.length && ( + { debouncedSearch && ! suggestions?.length && (

- { entityForSuggestions.labels.not_found } + { labels.not_found }

) } @@ -187,7 +221,7 @@ function AddCustomTemplateModal( { onClose, onSelect, entityForSuggestions } ) { { - // translators: The user is given the choice to set up a template for all items of a post type, or just a specific one. + // translators: The user is given the choice to set up a template for all items of a post type or taxonomy, or just a specific one. __( 'For all items' ) } @@ -203,7 +237,7 @@ function AddCustomTemplateModal( { onClose, onSelect, entityForSuggestions } ) { { - // translators: The user is given the choice to set up a template for all items of a post type, or just a specific one. + // translators: The user is given the choice to set up a template for all items of a post type or taxonomy, or just a specific one. __( 'For a specific item' ) } 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 index f8b2f9ec26d318..cd615228a915f9 100644 --- a/packages/edit-site/src/components/add-new-template/new-template.js +++ b/packages/edit-site/src/components/add-new-template/new-template.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { filter, includes } from 'lodash'; - /** * WordPress dependencies */ @@ -13,9 +8,8 @@ import { NavigableMenu, } from '@wordpress/components'; import { useState } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { store as editorStore } from '@wordpress/editor'; import { archive, blockMeta, @@ -31,14 +25,23 @@ import { search, tag, } from '@wordpress/icons'; -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ import AddCustomTemplateModal from './add-custom-template-modal'; -import { usePostTypes, usePostTypesEntitiesInfo } from './utils'; +import { + useExistingTemplates, + useDefaultTemplateTypes, + entitiesConfig, + usePostTypes, + useTaxonomies, + useTaxonomyCategory, + useTaxonomyTag, + useExtraTemplates, +} from './utils'; import { useHistory } from '../routes'; import { store as editSiteStore } from '../../store'; @@ -74,28 +77,15 @@ const TEMPLATE_ICONS = { }; export default function NewTemplate( { postType } ) { - const history = useHistory(); - const postTypes = usePostTypes(); const [ showCustomTemplateModal, setShowCustomTemplateModal ] = useState( false ); + const [ entityForSuggestions, setEntityForSuggestions ] = useState( {} ); - const { existingTemplates, defaultTemplateTypes } = useSelect( - ( select ) => ( { - existingTemplates: select( coreStore ).getEntityRecords( - 'postType', - 'wp_template', - { per_page: -1 } - ), - defaultTemplateTypes: - select( editorStore ).__experimentalGetDefaultTemplateTypes(), - } ), - [] - ); - const postTypesEntitiesInfo = usePostTypesEntitiesInfo( existingTemplates ); + + const history = useHistory(); const { saveEntityRecord } = useDispatch( coreStore ); const { createErrorNotice } = useDispatch( noticesStore ); const { setTemplate } = useDispatch( editSiteStore ); - async function createTemplate( template ) { try { const { title, description, slug } = template; @@ -108,7 +98,7 @@ export default function NewTemplate( { postType } ) { slug: slug.toString(), status: 'publish', title, - // This adds a post meta field in template that is part of `is_custom` value calculation. + // This adds a post meta field in template, that is part of `is_custom` value calculation. is_wp_suggestion: true, }, { throwOnError: true } @@ -135,78 +125,14 @@ export default function NewTemplate( { postType } ) { } ); } } - const existingTemplateSlugs = ( existingTemplates || [] ).map( - ( { slug } ) => slug - ); - const missingTemplates = filter( - defaultTemplateTypes, - ( template ) => - includes( DEFAULT_TEMPLATE_SLUGS, template.slug ) && - ! includes( existingTemplateSlugs, template.slug ) - ); - const extraTemplates = ( postTypes || [] ).reduce( - ( accumulator, _postType ) => { - const { slug, labels, icon } = _postType; - const hasGeneralTemplate = existingTemplateSlugs?.includes( - `single-${ slug }` - ); - const hasEntities = postTypesEntitiesInfo?.[ slug ]?.hasEntities; - const menuItem = { - slug: `single-${ slug }`, - title: sprintf( - // translators: %s: Name of the post type e.g: "Post". - __( 'Single item: %s' ), - labels.singular_name - ), - description: sprintf( - // translators: %s: Name of the post type e.g: "Post". - __( 'Displays a single item: %s.' ), - labels.singular_name - ), - // `icon` is the `menu_icon` property of a post type. We - // only handle `dashicons` for now, even if the `menu_icon` - // also supports urls and svg as values. - icon: icon?.startsWith( 'dashicons-' ) - ? icon.slice( 10 ) - : null, - }; - // We have a different template creation flow only if they have entities. - if ( hasEntities ) { - menuItem.onClick = ( template ) => { - setShowCustomTemplateModal( true ); - setEntityForSuggestions( { - type: 'postType', - slug, - labels, - hasGeneralTemplate, - template, - postsToExclude: - postTypesEntitiesInfo[ slug ].existingPosts, - } ); - }; - } - // We don't need to add the menu item if there are no - // entities and the general template exists. - if ( ! hasGeneralTemplate || hasEntities ) { - accumulator.push( menuItem ); - } - return accumulator; - }, - [] + const missingTemplates = useMissingTemplates( + setEntityForSuggestions, + setShowCustomTemplateModal ); - if ( ! missingTemplates.length && ! extraTemplates.length ) { + if ( ! missingTemplates.length ) { return null; } - // Update the sort order to match the DEFAULT_TEMPLATE_SLUGS order. - missingTemplates?.sort( ( template1, template2 ) => { - return ( - DEFAULT_TEMPLATE_SLUGS.indexOf( template1.slug ) - - DEFAULT_TEMPLATE_SLUGS.indexOf( template2.slug ) - ); - } ); - // Append all extra templates at the end of the list for now. - missingTemplates.push( ...extraTemplates ); return ( <> ); } + +function useMissingTemplates( + setEntityForSuggestions, + setShowCustomTemplateModal +) { + const postTypes = usePostTypes(); + const taxonomies = useTaxonomies(); + const categoryTaxonomy = useTaxonomyCategory(); + const tagTaxonomy = useTaxonomyTag(); + + const existingTemplates = useExistingTemplates(); + const defaultTemplateTypes = useDefaultTemplateTypes(); + + const existingTemplateSlugs = ( existingTemplates || [] ).map( + ( { slug } ) => slug + ); + + const missingDefaultTemplates = ( defaultTemplateTypes || [] ).filter( + ( template ) => + DEFAULT_TEMPLATE_SLUGS.includes( template.slug ) && + ! existingTemplateSlugs.includes( template.slug ) + ); + const onClickMenuItem = ( _entityForSuggestions ) => { + setShowCustomTemplateModal( true ); + setEntityForSuggestions( _entityForSuggestions ); + }; + // TODO: find better names for these variables. `useExtraTemplates` returns an array of items. + const categoryMenuItem = useExtraTemplates( + categoryTaxonomy, + entitiesConfig.category, + onClickMenuItem + ); + const tagMenuItem = useExtraTemplates( + tagTaxonomy, + entitiesConfig.tag, + onClickMenuItem + ); + // We need to replace existing default template types with + // the create specific template functionality. The original + // info (title, description, etc.) is preserved in the + // `useExtraTemplates` hook. + const enhancedMissingDefaultTemplateTypes = [ ...missingDefaultTemplates ]; + [ categoryMenuItem, tagMenuItem ].forEach( ( menuItem ) => { + if ( ! menuItem?.length ) { + return; + } + const matchIndex = enhancedMissingDefaultTemplateTypes.findIndex( + ( template ) => template.slug === menuItem[ 0 ].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.splice( + matchIndex, + 1, + menuItem[ 0 ] + ); + } else { + enhancedMissingDefaultTemplateTypes.push( menuItem[ 0 ] ); + } + } ); + // Update the sort order to match the DEFAULT_TEMPLATE_SLUGS order. + enhancedMissingDefaultTemplateTypes?.sort( ( template1, template2 ) => { + return ( + DEFAULT_TEMPLATE_SLUGS.indexOf( template1.slug ) - + DEFAULT_TEMPLATE_SLUGS.indexOf( template2.slug ) + ); + } ); + const extraPostTypeTemplates = useExtraTemplates( + postTypes, + entitiesConfig.postType, + onClickMenuItem + ); + const extraTaxonomyTemplates = useExtraTemplates( + taxonomies, + entitiesConfig.taxonomy, + onClickMenuItem + ); + const missingTemplates = [ + ...enhancedMissingDefaultTemplateTypes, + ...extraPostTypeTemplates, + ...extraTaxonomyTemplates, + ]; + return missingTemplates; +} diff --git a/packages/edit-site/src/components/add-new-template/utils.js b/packages/edit-site/src/components/add-new-template/utils.js index 0726d019d45e5d..725ff6352e125e 100644 --- a/packages/edit-site/src/components/add-new-template/utils.js +++ b/packages/edit-site/src/components/add-new-template/utils.js @@ -8,8 +8,126 @@ import { get } from 'lodash'; */ 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 { __, sprintf } from '@wordpress/i18n'; +import { blockMeta, post } from '@wordpress/icons'; + +/** + * @typedef IHasNameAndId + * @property {string|number} id The entity's id. + * @property {string} name The entity's name. + */ + +/** + * Helper util to map records to add a `name` prop from a + * provided path, in order to handle all entities in the same + * fashion(implementing`IHasNameAndId` interface). + * + * @param {Object[]} entities The array of entities. + * @param {string} path The path to map a `name` property from the entity. + * @return {IHasNameAndId[]} An array of enitities that now implement the `IHasNameAndId` interface. + */ +export const mapToIHasNameAndId = ( entities, path ) => { + return ( entities || [] ).map( ( entity ) => ( { + ...entity, + name: decodeEntities( get( entity, path ) ), + } ) ); +}; + +/** + * @typedef {Object} EntitiesInfo + * @property {boolean} hasEntities If an entity has available records(posts, terms, etc..). + * @property {number[]} existingEntitiesIds An array of the existing entities ids. + */ + +/** + * @typedef {Object} EntityConfig + * @property {string} entityName The entity's name. + * @property {Function} getOrderBy Getter for an entity's `orderBy` query parameter, given the object + * {search} as argument. + * @property {Function} getIcon Getter function for returning an entity's icon for the menu item. + * @property {Function} getTitle Getter function for returning an entity's title for the menu item. + * @property {Function} getDescription Getter function for returning an entity's description for the menu item. + * @property {string} recordNamePath The path to an entity's properties to use as a `name`. If not provided + * is assumed that `name` property exists. + * @property {string} templatePrefix The template prefix to create new templates and check against existing + * templates. For example custom post types need a `single-` prefix to all + * templates(`single-post-hello`), whereas `pages` don't (`page-hello`). + * @property {string} templateSlug If this property is provided, it is going to be used for the creation of + * new templates and the check against existing templates in the place + * of the actual entity's `slug`. An example is `Tag` templates where the + * the Tag's taxonomy slug is `post_tag`, but template hierarchy is based + * on `tag` alias. + */ + +const taxonomyBaseConfig = { + entityName: 'taxonomy', + getOrderBy: ( { search } ) => ( search ? 'name' : 'count' ), + getIcon: () => blockMeta, + getTitle: ( labels ) => + sprintf( + // translators: %s: Name of the taxonomy e.g: "Cagegory". + __( 'Single taxonomy: %s' ), + labels.singular_name + ), + getDescription: ( labels ) => + sprintf( + // translators: %s: Name of the taxonomy e.g: "Product Categories". + __( 'Displays a single taxonomy: %s.' ), + labels.singular_name + ), +}; +export const entitiesConfig = { + postType: { + entityName: 'postType', + templatePrefix: 'single-', + getOrderBy: ( { search } ) => ( search ? 'relevance' : 'modified' ), + recordNamePath: 'title.rendered', + // `icon` is the `menu_icon` property of a post type. We + // only handle `dashicons` for now, even if the `menu_icon` + // also supports urls and svg as values. + getIcon: ( _icon ) => + _icon?.startsWith( 'dashicons-' ) ? _icon.slice( 10 ) : post, + getTitle: ( labels ) => + sprintf( + // translators: %s: Name of the post type e.g: "Post". + __( 'Single item: %s' ), + labels.singular_name + ), + getDescription: ( labels ) => + sprintf( + // translators: %s: Name of the post type e.g: "Post". + __( 'Displays a single item: %s.' ), + labels.singular_name + ), + }, + taxonomy: { + ...taxonomyBaseConfig, + templatePrefix: 'taxonomy-', + }, + category: { ...taxonomyBaseConfig }, + tag: { ...taxonomyBaseConfig, templateSlug: 'tag' }, +}; + +export const useExistingTemplates = () => { + return useSelect( + ( select ) => + select( coreStore ).getEntityRecords( 'postType', 'wp_template', { + per_page: -1, + } ), + [] + ); +}; + +export const useDefaultTemplateTypes = () => { + return useSelect( + ( select ) => + select( editorStore ).__experimentalGetDefaultTemplateTypes(), + [] + ); +}; export const usePostTypes = () => { const postTypes = useSelect( @@ -25,32 +143,81 @@ export const usePostTypes = () => { }, [ postTypes ] ); }; +const usePublicTaxonomies = () => { + const taxonomies = useSelect( + ( select ) => select( coreStore ).getTaxonomies( { per_page: -1 } ), + [] + ); + return useMemo( () => { + return taxonomies?.filter( + ( { visibility } ) => visibility?.publicly_queryable + ); + }, [ taxonomies ] ); +}; + /** - * @typedef {Object} PostTypeEntitiesInfo - * @property {boolean} hasEntities If a postType has available entities. - * @property {number[]} existingPosts An array of the existing entities ids. + * `category` and `post_tag` are handled specifically in template + * hierarchy so we need to differentiate them and return the rest, + * e.g. `category-$slug` and `taxonomy-$taxonomy-$term`. */ +export const useTaxonomies = () => { + const taxonomies = usePublicTaxonomies(); + const specialTaxonomies = [ 'category', 'post_tag' ]; + return useMemo( + () => + taxonomies?.filter( + ( { slug } ) => ! specialTaxonomies.includes( slug ) + ), + [ taxonomies ] + ); +}; + +export const useTaxonomyCategory = () => { + const taxonomies = usePublicTaxonomies(); + return useMemo( + () => taxonomies?.filter( ( { slug } ) => slug === 'category' ), + [ taxonomies ] + ); +}; +export const useTaxonomyTag = () => { + const taxonomies = usePublicTaxonomies(); + return useMemo( + () => taxonomies?.filter( ( { slug } ) => slug === 'post_tag' ), + [ taxonomies ] + ); +}; /** - * Helper hook that returns information about a post type having - * posts that we can create a specific template for. + * Helper hook that returns information about an entity having + * records that we can create a specific template for. * - * First we need to find the existing posts with an associated template, - * to query afterwards for any remaing post, by excluding them. + * For example we can search for `terms` in `taxonomy` entity or + * `posts` in `postType` entity. * - * @param {string[]} existingTemplates The existing templates. - * @return {Record} An object with the postTypes as `keys` and PostTypeEntitiesInfo as values. + * First we need to find the existing records with an associated template, + * to query afterwards for any remaing record, by excluding them. + * + * @param {string[]} existingTemplates The existing templates. + * @param {Object[]} entities The array of entities we need to get extra information. + * @param {EntityConfig} entityConfig The entity config. + * @return {Record} An object with the `entities.slug` as `keys` and EntitiesInfo as values. */ -export const usePostTypesEntitiesInfo = ( existingTemplates ) => { - const postTypes = usePostTypes(); +const useEntitiesInfo = ( + existingTemplates, + entities, + { entityName, templatePrefix, templateSlug } +) => { const slugsToExcludePerEntity = useMemo( () => { - return postTypes?.reduce( ( accumulator, _postType ) => { + return entities?.reduce( ( accumulator, entity ) => { + let _prefix = `${ templateSlug || entity.slug }-`; + if ( templatePrefix ) { + _prefix = templatePrefix + _prefix; + } const slugsWithTemplates = ( existingTemplates || [] ).reduce( ( _accumulator, existingTemplate ) => { - const prefix = `single-${ _postType.slug }-`; - if ( existingTemplate.slug.startsWith( prefix ) ) { + if ( existingTemplate.slug.startsWith( _prefix ) ) { _accumulator.push( - existingTemplate.slug.substring( prefix.length ) + existingTemplate.slug.substring( _prefix.length ) ); } return _accumulator; @@ -58,68 +225,125 @@ export const usePostTypesEntitiesInfo = ( existingTemplates ) => { [] ); if ( slugsWithTemplates.length ) { - accumulator[ _postType.slug ] = slugsWithTemplates; + accumulator[ entity.slug ] = slugsWithTemplates; } return accumulator; }, {} ); - }, [ postTypes, existingTemplates ] ); - const postsToExcludePerEntity = useSelect( + }, [ entities, existingTemplates ] ); + const recordsToExcludePerEntity = useSelect( ( select ) => { if ( ! slugsToExcludePerEntity ) { return; } - const postsToExclude = Object.entries( - slugsToExcludePerEntity - ).reduce( ( accumulator, [ slug, slugsWithTemplates ] ) => { - const postsWithTemplates = select( coreStore ).getEntityRecords( - 'postType', - slug, - { + return Object.entries( slugsToExcludePerEntity ).reduce( + ( accumulator, [ slug, slugsWithTemplates ] ) => { + const postsWithTemplates = select( + coreStore + ).getEntityRecords( entityName, slug, { _fields: 'id', context: 'view', slug: slugsWithTemplates, + } ); + if ( postsWithTemplates?.length ) { + accumulator[ slug ] = postsWithTemplates; } - ); - if ( postsWithTemplates?.length ) { - accumulator[ slug ] = postsWithTemplates; - } - return accumulator; - }, {} ); - return postsToExclude; + return accumulator; + }, + {} + ); }, [ slugsToExcludePerEntity ] ); const entitiesInfo = useSelect( ( select ) => { - return postTypes?.reduce( ( accumulator, { slug } ) => { - const existingPosts = - postsToExcludePerEntity?.[ slug ]?.map( + return entities?.reduce( ( accumulator, { slug } ) => { + const existingEntitiesIds = + recordsToExcludePerEntity?.[ slug ]?.map( ( { id } ) => id ) || []; accumulator[ slug ] = { hasEntities: !! select( coreStore ).getEntityRecords( - 'postType', + entityName, slug, { per_page: 1, _fields: 'id', context: 'view', - exclude: existingPosts, + exclude: existingEntitiesIds, } )?.length, - existingPosts, + existingEntitiesIds, }; return accumulator; }, {} ); }, - [ postTypes, postsToExcludePerEntity ] + [ entities, recordsToExcludePerEntity ] ); return entitiesInfo; }; -export const mapToIHasNameAndId = ( entities, path ) => { - return ( entities || [] ).map( ( entity ) => ( { - ...entity, - name: decodeEntities( get( entity, path ) ), - } ) ); +export const useExtraTemplates = ( + entities, + entityConfig, + onClickMenuItem +) => { + const existingTemplates = useExistingTemplates(); + const defaultTemplateTypes = useDefaultTemplateTypes(); + const entitiesInfo = useEntitiesInfo( + existingTemplates, + entities, + entityConfig + ); + const existingTemplateSlugs = ( existingTemplates || [] ).map( + ( { slug } ) => slug + ); + const extraTemplates = ( entities || [] ).reduce( + ( accumulator, entity ) => { + const { slug, labels, icon } = entity; + const slugForGeneralTemplate = entityConfig.templateSlug || slug; + // We need to check if the general template is part of the + // defaultTemplateTypes. If it is, just use that info and + // augment it with the specific template functionality. + const defaultTemplateType = defaultTemplateTypes?.find( + ( { slug: _slug } ) => _slug === slugForGeneralTemplate + ); + const generalTemplateSlug = + defaultTemplateType?.slug || + `${ entityConfig.templatePrefix }${ slug }`; + const hasGeneralTemplate = + existingTemplateSlugs?.includes( generalTemplateSlug ); + const menuItem = defaultTemplateType + ? { ...defaultTemplateType } + : { + slug: generalTemplateSlug, + title: entityConfig.getTitle( labels ), + description: entityConfig.getDescription( labels ), + icon: entityConfig.getIcon?.( icon ), + }; + const hasEntities = entitiesInfo?.[ slug ]?.hasEntities; + // We have a different template creation flow only if they have entities. + if ( hasEntities ) { + menuItem.onClick = ( template ) => { + onClickMenuItem( { + type: entityConfig.entityName, + slug, + config: entityConfig, + labels, + hasGeneralTemplate, + template, + postsToExclude: + entitiesInfo[ slug ].existingEntitiesIds, + } ); + }; + } + // We don't need to add the menu item if there are no + // entities and the general template exists. + if ( ! hasGeneralTemplate || hasEntities ) { + accumulator.push( menuItem ); + } + return accumulator; + }, + [] + ); + return extraTemplates; };