Skip to content

Commit

Permalink
Quick Edit: add Template field (#66591)
Browse files Browse the repository at this point in the history
Co-authored-by: louwie17 <[email protected]>
Co-authored-by: gigitux <[email protected]>
Co-authored-by: youknowriad <[email protected]>
  • Loading branch information
4 people authored Dec 3, 2024
1 parent fcee058 commit 5c76815
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 2 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/base-styles/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ $z-layers: (
".editor-action-modal": 1000001,
".editor-post-template__swap-template-modal": 1000001,
".edit-site-template-panel__replace-template-modal": 1000001,
".fields-controls__template-modal": 1000001,

// Note: The ConfirmDialog component's z-index is being set to 1000001 in packages/components/src/confirm-dialog/styles.ts
// because it uses emotion and not sass. We need it to render on top its parent popover.
Expand Down
36 changes: 35 additions & 1 deletion packages/edit-site/src/components/post-edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { privateApis as editorPrivateApis } from '@wordpress/editor';
*/
import Page from '../page';
import { unlock } from '../../lock-unlock';
import usePatternSettings from '../page-patterns/use-pattern-settings';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';

const { PostCardPanel, usePostFields } = unlock( editorPrivateApis );

Expand Down Expand Up @@ -85,6 +87,12 @@ function PostEditForm( { postType, postId } ) {
'slug',
'parent',
'comment_status',
{
label: __( 'Template' ),
labelPosition: 'side',
id: 'template',
layout: 'regular',
},
].filter(
( field ) =>
ids.length === 1 ||
Expand Down Expand Up @@ -123,14 +131,40 @@ function PostEditForm( { postType, postId } ) {
setMultiEdits( {} );
}, [ ids ] );

const { ExperimentalBlockEditorProvider } = unlock(
blockEditorPrivateApis
);
const settings = usePatternSettings();

/**
* The template field depends on the block editor settings.
* This is a workaround to ensure that the block editor settings are available.
* For more information, see: https://github.com/WordPress/gutenberg/issues/67521
*/
const fieldsWithDependency = useMemo( () => {
return fields.map( ( field ) => {
if ( field.id === 'template' ) {
return {
...field,
Edit: ( data ) => (
<ExperimentalBlockEditorProvider settings={ settings }>
<field.Edit { ...data } />
</ExperimentalBlockEditorProvider>
),
};
}
return field;
} );
}, [ fields, settings ] );

return (
<VStack spacing={ 4 }>
{ ids.length === 1 && (
<PostCardPanel postType={ postType } postId={ ids[ 0 ] } />
) }
<DataForm
data={ ids.length === 1 ? record : multiEdits }
fields={ fields }
fields={ fieldsWithDependency }
form={ form }
onChange={ onChange }
/>
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/dataviews/store/private-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
statusField,
authorField,
titleField,
templateField,
} from '@wordpress/fields';

export function registerEntityAction< Item >(
Expand Down Expand Up @@ -171,6 +172,7 @@ export const registerPostTypeSchema =
postTypeConfig.supports?.[ 'page-attributes' ] && parentField,
postTypeConfig.supports?.comments && commentStatusField,
passwordField,
templateField,
].filter( Boolean );

registry.batch( () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/fields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ Undocumented declaration.

Status field for BasePost.

### templateField

Undocumented declaration.

### titleField

Undocumented declaration.
Expand Down
1 change: 1 addition & 0 deletions packages/fields/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@babel/runtime": "7.25.7",
"@wordpress/api-fetch": "*",
"@wordpress/blob": "*",
"@wordpress/block-editor": "*",
"@wordpress/blocks": "*",
"@wordpress/components": "*",
"@wordpress/compose": "*",
Expand Down
4 changes: 3 additions & 1 deletion packages/fields/src/actions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export function isTemplateOrTemplatePart(
return p.type === 'wp_template' || p.type === 'wp_template_part';
}

export function getItemTitle( item: Post ): string {
export function getItemTitle( item: {
title: string | { rendered: string } | { raw: string };
} ) {
if ( typeof item.title === 'string' ) {
return decodeEntities( item.title );
}
Expand Down
1 change: 1 addition & 0 deletions packages/fields/src/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as slugField } from './slug';
export { default as titleField } from './title';
export { default as orderField } from './order';
export { default as featuredImageField } from './featured-image';
export { default as templateField } from './template';
export { default as parentField } from './parent';
export { default as passwordField } from './password';
export { default as statusField } from './status';
Expand Down
22 changes: 22 additions & 0 deletions packages/fields/src/fields/template/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* WordPress dependencies
*/
import type { Field } from '@wordpress/dataviews';

/**
* Internal dependencies
*/
import { __ } from '@wordpress/i18n';
import type { BasePost } from '../../types';
import { TemplateEdit } from './template-edit';

const templateField: Field< BasePost > = {
id: 'template',
type: 'text',
label: __( 'Template' ),
getValue: ( { item } ) => item.template,
Edit: TemplateEdit,
enableSorting: false,
};

export default templateField;
23 changes: 23 additions & 0 deletions packages/fields/src/fields/template/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.fields-controls__template-modal {
z-index: z-index(".fields-controls__template-modal");
}

.fields-controls__template-content .block-editor-block-patterns-list {
column-count: 2;
column-gap: $grid-unit-30;

// Small top padding required to avoid cutting off the visible outline when hovering items
padding-top: $border-width-focus-fallback;

@include break-medium() {
column-count: 3;
}

@include break-wide() {
column-count: 4;
}

.block-editor-block-patterns-list__list-item {
break-inside: avoid-column;
}
}
210 changes: 210 additions & 0 deletions packages/fields/src/fields/template/template-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/**
* WordPress dependencies
*/
import { useCallback, useMemo, useState } from '@wordpress/element';
// @ts-ignore
import { parse } from '@wordpress/blocks';
import type { WpTemplate } from '@wordpress/core-data';
import { store as coreStore } from '@wordpress/core-data';
import type { DataFormControlProps } from '@wordpress/dataviews';

/**
* Internal dependencies
*/
// @ts-expect-error block-editor is not typed correctly.
import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor';
import {
Button,
Dropdown,
MenuGroup,
MenuItem,
Modal,
} from '@wordpress/components';
import { useAsyncList } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';
import { getItemTitle } from '../../actions/utils';
import type { BasePost } from '../../types';
import { unlock } from '../../lock-unlock';

export const TemplateEdit = ( {
data,
field,
onChange,
}: DataFormControlProps< BasePost > ) => {
const { id } = field;
const postType = data.type;
const postId =
typeof data.id === 'number' ? data.id : parseInt( data.id, 10 );
const slug = data.slug;

const { availableTemplates, templates } = useSelect(
( select ) => {
const allTemplates =
select( coreStore ).getEntityRecords< WpTemplate >(
'postType',
'wp_template',
{
per_page: -1,
post_type: postType,
}
) ?? [];

const { getHomePage, getPostsPageId } = unlock(
select( coreStore )
);

const isPostsPage = getPostsPageId() === +postId;
const isFrontPage =
postType === 'page' && getHomePage()?.postId === +postId;

const allowSwitchingTemplate = ! isPostsPage && ! isFrontPage;

return {
templates: allTemplates,
availableTemplates: allowSwitchingTemplate
? allTemplates.filter(
( template ) =>
template.is_custom &&
template.slug !== data.template &&
!! template.content.raw // Skip empty templates.
)
: [],
};
},
[ data.template, postId, postType ]
);

const templatesAsPatterns = useMemo(
() =>
availableTemplates.map( ( template ) => ( {
name: template.slug,
blocks: parse( template.content.raw ),
title: decodeEntities( template.title.rendered ),
id: template.id,
} ) ),
[ availableTemplates ]
);

const shownTemplates = useAsyncList( templatesAsPatterns );

const value = field.getValue( { item: data } );

const currentTemplate = useSelect(
( select ) => {
const foundTemplate = templates?.find(
( template ) => template.slug === value
);

if ( foundTemplate ) {
return foundTemplate;
}

let slugToCheck;
// In `draft` status we might not have a slug available, so we use the `single`
// post type templates slug(ex page, single-post, single-product etc..).
// Pages do not need the `single` prefix in the slug to be prioritized
// through template hierarchy.
if ( slug ) {
slugToCheck =
postType === 'page'
? `${ postType }-${ slug }`
: `single-${ postType }-${ slug }`;
} else {
slugToCheck =
postType === 'page' ? 'page' : `single-${ postType }`;
}

if ( postType ) {
const templateId = select( coreStore ).getDefaultTemplateId( {
slug: slugToCheck,
} );

return select( coreStore ).getEntityRecord(
'postType',
'wp_template',
templateId
);
}
},
[ postType, slug, templates, value ]
);

const [ showModal, setShowModal ] = useState( false );

const onChangeControl = useCallback(
( newValue: string ) =>
onChange( {
[ id ]: newValue,
} ),
[ id, onChange ]
);

return (
<fieldset className="fields-controls__template">
<Dropdown
popoverProps={ { placement: 'bottom-start' } }
renderToggle={ ( { onToggle } ) => (
<Button
__next40pxDefaultSize
variant="tertiary"
size="compact"
onClick={ onToggle }
>
{ currentTemplate
? getItemTitle( currentTemplate )
: '' }
</Button>
) }
renderContent={ ( { onToggle } ) => (
<MenuGroup>
<MenuItem
onClick={ () => {
setShowModal( true );
onToggle();
} }
>
{ __( 'Swap template' ) }
</MenuItem>
{
// The default template in a post is indicated by an empty string
value !== '' && (
<MenuItem
onClick={ () => {
onChangeControl( '' );
onToggle();
} }
>
{ __( 'Use default template' ) }
</MenuItem>
)
}
</MenuGroup>
) }
/>
{ showModal && (
<Modal
title={ __( 'Choose a template' ) }
onRequestClose={ () => setShowModal( false ) }
overlayClassName="fields-controls__template-modal"
isFullScreen
>
<div className="fields-controls__template-content">
<BlockPatternsList
label={ __( 'Templates' ) }
blockPatterns={ templatesAsPatterns }
shownPatterns={ shownTemplates }
onClickPattern={ (
template: ( typeof templatesAsPatterns )[ 0 ]
) => {
onChangeControl( template.name );
setShowModal( false );
} }
/>
</div>
</Modal>
) }
</fieldset>
);
};
1 change: 1 addition & 0 deletions packages/fields/src/style.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import "./components/create-template-part-modal/style.scss";
@import "./fields/slug/style.scss";
@import "./fields/featured-image/style.scss";
@import "./fields/template/style.scss";
Loading

1 comment on commit 5c76815

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 5c76815.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/12138486395
📝 Reported issues:

Please sign in to comment.