/**
* External dependencies
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import {
Button,
Modal,
__experimentalGrid as Grid,
__experimentalText as Text,
__experimentalVStack as VStack,
Flex,
Icon,
} from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
import { useState } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import {
archive,
blockMeta,
calendar,
category,
commentAuthorAvatar,
edit,
home,
layout,
list,
media,
notFound,
page,
plus,
pin,
verse,
search,
tag,
} from '@wordpress/icons';
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { privateApis as routerPrivateApis } from '@wordpress/router';
/**
* Internal dependencies
*/
import AddCustomTemplateModalContent from './add-custom-template-modal-content';
import {
useExistingTemplates,
useDefaultTemplateTypes,
useTaxonomiesMenuItems,
usePostTypeMenuItems,
useAuthorMenuItem,
usePostTypeArchiveMenuItems,
} from './utils';
import AddCustomGenericTemplateModalContent from './add-custom-generic-template-modal-content';
import TemplateActionsLoadingScreen from './template-actions-loading-screen';
import { store as editSiteStore } from '../../store';
import { unlock } from '../../lock-unlock';
const { useHistory } = unlock( routerPrivateApis );
const DEFAULT_TEMPLATE_SLUGS = [
'front-page',
'home',
'single',
'page',
'index',
'archive',
'author',
'category',
'date',
'tag',
'search',
'404',
];
const TEMPLATE_ICONS = {
'front-page': home,
home: verse,
single: pin,
page,
archive,
search,
404: notFound,
index: list,
category,
author: commentAuthorAvatar,
taxonomy: blockMeta,
date: calendar,
tag,
attachment: media,
};
function TemplateListItem( {
title,
direction,
className,
description,
icon,
onClick,
children,
} ) {
return (
);
}
const modalContentMap = {
templatesList: 1,
customTemplate: 2,
customGenericTemplate: 3,
};
export default function NewTemplate( {
postType,
toggleProps,
showIcon = true,
} ) {
const [ showModal, setShowModal ] = useState( false );
const [ modalContent, setModalContent ] = useState(
modalContentMap.templatesList
);
const [ entityForSuggestions, setEntityForSuggestions ] = useState( {} );
const [ isCreatingTemplate, setIsCreatingTemplate ] = useState( false );
const history = useHistory();
const { saveEntityRecord } = useDispatch( coreStore );
const { createErrorNotice, createSuccessNotice } =
useDispatch( noticesStore );
const { setTemplate } = unlock( useDispatch( editSiteStore ) );
const { homeUrl } = useSelect( ( select ) => {
const {
getUnstableBase, // Site index.
} = select( coreStore );
return {
homeUrl: getUnstableBase()?.home,
};
}, [] );
const TEMPLATE_SHORT_DESCRIPTIONS = {
'front-page': homeUrl,
date: sprintf(
// translators: %s: The homepage url.
__( 'E.g. %s' ),
homeUrl + '/' + new Date().getFullYear()
),
};
async function createTemplate( template, isWPSuggestion = true ) {
if ( isCreatingTemplate ) {
return;
}
setIsCreatingTemplate( true );
try {
const { title, description, slug } = template;
const newTemplate = await saveEntityRecord(
'postType',
'wp_template',
{
description,
// Slugs need to be strings, so this is for template `404`
slug: slug.toString(),
status: 'publish',
title,
// This adds a post meta field in template that is part of `is_custom` value calculation.
is_wp_suggestion: isWPSuggestion,
},
{ throwOnError: true }
);
// Set template before navigating away to avoid initial stale value.
setTemplate( newTemplate.id, newTemplate.slug );
// Navigate to the created template editor.
history.push( {
postId: newTemplate.id,
postType: newTemplate.type,
canvas: 'edit',
} );
createSuccessNotice(
sprintf(
// translators: %s: Title of the created template e.g: "Category".
__( '"%s" successfully created.' ),
decodeEntities( newTemplate.title?.rendered || title )
),
{
type: 'snackbar',
}
);
} catch ( error ) {
const errorMessage =
error.message && error.code !== 'unknown_error'
? error.message
: __( 'An error occurred while creating the template.' );
createErrorNotice( errorMessage, {
type: 'snackbar',
} );
} finally {
setIsCreatingTemplate( false );
}
}
const onModalClose = () => {
setShowModal( false );
setModalContent( modalContentMap.templatesList );
};
const missingTemplates = useMissingTemplates( setEntityForSuggestions, () =>
setModalContent( modalContentMap.customTemplate )
);
if ( ! missingTemplates.length ) {
return null;
}
const { as: Toggle = Button, ...restToggleProps } = toggleProps ?? {};
let modalTitle = __( 'Add template' );
if ( modalContent === modalContentMap.customTemplate ) {
modalTitle = sprintf(
// translators: %s: Name of the post type e.g: "Post".
__( 'Add template: %s' ),
entityForSuggestions.labels.singular_name
);
} else if ( modalContent === modalContentMap.customGenericTemplate ) {
modalTitle = __( 'Create custom template' );
}
return (
<>
{ isCreatingTemplate && }
setShowModal( true ) }
icon={ showIcon ? plus : null }
label={ postType.labels.add_new_item }
>
{ showIcon ? null : postType.labels.add_new_item }
{ showModal && (
{ modalContent === modalContentMap.templatesList && (
{ __(
'Select what the new template should apply to:'
) }
{ missingTemplates.map( ( template ) => {
const { title, slug, onClick } = template;
return (
onClick
? onClick( template )
: createTemplate( template )
}
/>
);
} ) }
setModalContent(
modalContentMap.customGenericTemplate
)
}
>
{ __(
'A custom template can be manually applied to any post or page.'
) }
) }
{ modalContent === modalContentMap.customTemplate && (
) }
{ modalContent ===
modalContentMap.customGenericTemplate && (
) }
) }
>
);
}
function useMissingTemplates( setEntityForSuggestions, onClick ) {
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 ) => {
onClick?.();
setEntityForSuggestions( _entityForSuggestions );
};
// We need to replace existing default template types with
// the create specific template functionality. The original
// info (title, description, etc.) is preserved in the
// used hooks.
const enhancedMissingDefaultTemplateTypes = [ ...missingDefaultTemplates ];
const { defaultTaxonomiesMenuItems, taxonomiesMenuItems } =
useTaxonomiesMenuItems( onClickMenuItem );
const { defaultPostTypesMenuItems, postTypesMenuItems } =
usePostTypeMenuItems( onClickMenuItem );
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 (
DEFAULT_TEMPLATE_SLUGS.indexOf( template1.slug ) -
DEFAULT_TEMPLATE_SLUGS.indexOf( template2.slug )
);
} );
const missingTemplates = [
...enhancedMissingDefaultTemplateTypes,
...usePostTypeArchiveMenuItems(),
...postTypesMenuItems,
...taxonomiesMenuItems,
];
return missingTemplates;
}