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 and michalczaplinski committed Dec 5, 2024
1 parent 6d40fb4 commit a5bfc41
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

0 comments on commit a5bfc41

Please sign in to comment.