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

Pages: Add "Set as posts page" action #67650

Merged
merged 18 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions packages/editor/src/components/post-actions/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { store as coreStore } from '@wordpress/core-data';
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';
import { useSetAsHomepageAction } from './set-as-homepage';
import { useSetAsPostsPageAction } from './set-as-posts-page';

export function usePostActions( { postType, onActionPerformed, context } ) {
const { defaultActions } = useSelect(
Expand Down Expand Up @@ -43,7 +44,8 @@ export function usePostActions( { postType, onActionPerformed, context } ) {
);

const setAsHomepageAction = useSetAsHomepageAction();
const shouldShowSetAsHomepageAction =
const setAsPostsPageAction = useSetAsPostsPageAction();
const shouldShowHomepageActions =
canManageOptions && ! hasFrontPageTemplate;

const { registerPostTypeSchema } = unlock( useDispatch( editorStore ) );
Expand All @@ -53,10 +55,15 @@ export function usePostActions( { postType, onActionPerformed, context } ) {

return useMemo( () => {
let actions = [ ...defaultActions ];
if ( shouldShowSetAsHomepageAction ) {
actions.push( setAsHomepageAction );
if ( shouldShowHomepageActions ) {
actions.push( setAsHomepageAction, setAsPostsPageAction );
}

// Ensure "Move to trash" is always the last action.
actions = actions.sort( ( a, b ) =>
b.id === 'move-to-trash' ? -1 : 0
);

// Filter actions based on provided context. If not provided
// all actions are returned. We'll have a single entry for getting the actions
// and the consumer should provide the context to filter the actions, if needed.
Expand Down Expand Up @@ -123,6 +130,7 @@ export function usePostActions( { postType, onActionPerformed, context } ) {
defaultActions,
onActionPerformed,
setAsHomepageAction,
shouldShowSetAsHomepageAction,
setAsPostsPageAction,
shouldShowHomepageActions,
] );
}
38 changes: 9 additions & 29 deletions packages/editor/src/components/post-actions/set-as-homepage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,11 @@ import {
import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';
import { decodeEntities } from '@wordpress/html-entities';

const getItemTitle = ( item ) => {
if ( typeof item.title === 'string' ) {
return decodeEntities( item.title );
}
if ( item.title && 'rendered' in item.title ) {
return decodeEntities( item.title.rendered );
}
if ( item.title && 'raw' in item.title ) {
return decodeEntities( item.title.raw );
}
return '';
};
/**
* Internal dependencies
*/
import { getItemTitle } from '../../utils/get-item-title';

const SetAsHomepageModal = ( { items, closeModal } ) => {
const [ item ] = items;
Expand All @@ -48,38 +39,27 @@ const SetAsHomepageModal = ( { items, closeModal } ) => {
}
);

const { saveEditedEntityRecord, saveEntityRecord } =
useDispatch( coreStore );
const { saveEntityRecord } = useDispatch( coreStore );
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );

async function onSetPageAsHomepage( event ) {
event.preventDefault();

try {
// Save new home page settings.
await saveEditedEntityRecord( 'root', 'site', undefined, {
page_on_front: item.id,
show_on_front: 'page',
} );

// This second call to a save function is a workaround for a bug in
// `saveEditedEntityRecord`. This forces the root site settings to be updated.
// See https://github.com/WordPress/gutenberg/issues/67161.
await saveEntityRecord( 'root', 'site', {
page_on_front: item.id,
show_on_front: 'page',
} );

createSuccessNotice( __( 'Homepage updated' ), {
createSuccessNotice( __( 'Homepage updated.' ), {
type: 'snackbar',
} );
} catch ( error ) {
const typedError = error;
const errorMessage =
typedError.message && typedError.code !== 'unknown_error'
? typedError.message
: __( 'An error occurred while setting the homepage' );
error.message && error.code !== 'unknown_error'
? error.message
: __( 'An error occurred while setting the homepage.' );
createErrorNotice( errorMessage, { type: 'snackbar' } );
} finally {
closeModal?.();
Expand Down
158 changes: 158 additions & 0 deletions packages/editor/src/components/post-actions/set-as-posts-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
import {
Button,
__experimentalText as Text,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import { getItemTitle } from '../../utils/get-item-title';

const SetAsPostsPageModal = ( { items, closeModal } ) => {
const [ item ] = items;
const pageTitle = getItemTitle( item );
const { currentPostsPage, isPageForPostsSet, isSaving } = useSelect(
( select ) => {
const { getEntityRecord, isSavingEntityRecord } =
select( coreStore );
const siteSettings = getEntityRecord( 'root', 'site' );
const currentPostsPageItem = getEntityRecord(
'postType',
'page',
siteSettings?.page_for_posts
);
return {
currentPostsPage: currentPostsPageItem,
isPageForPostsSet: siteSettings?.page_for_posts !== 0,
isSaving: isSavingEntityRecord( 'root', 'site' ),
};
}
);

const { saveEntityRecord } = useDispatch( coreStore );
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );

async function onSetPageAsPostsPage( event ) {
event.preventDefault();

try {
await saveEntityRecord( 'root', 'site', {
page_for_posts: item.id,
show_on_front: 'page',
} );

createSuccessNotice( __( 'Posts page updated.' ), {
type: 'snackbar',
} );
} catch ( error ) {
const errorMessage =
error.message && error.code !== 'unknown_error'
? error.message
: __( 'An error occurred while setting the posts page.' );
createErrorNotice( errorMessage, { type: 'snackbar' } );
} finally {
closeModal?.();
}
}

const modalWarning =
isPageForPostsSet && currentPostsPage
? sprintf(
// translators: %s: title of the current posts page.
__( 'This will replace the current posts page: "%s"' ),
getItemTitle( currentPostsPage )
)
: __( 'This page will show the latest posts.' );

const modalText = sprintf(
// translators: %1$s: title of the page to be set as the posts page, %2$s: posts page replacement warning message.
__( 'Set "%1$s" as the posts page? %2$s' ),
pageTitle,
modalWarning
);

// translators: Button label to confirm setting the specified page as the posts page.
const modalButtonLabel = __( 'Set posts page' );

return (
<form onSubmit={ onSetPageAsPostsPage }>
<VStack spacing="5">
<Text>{ modalText }</Text>
<HStack justify="right">
<Button
__next40pxDefaultSize
variant="tertiary"
onClick={ () => {
closeModal?.();
} }
disabled={ isSaving }
accessibleWhenDisabled
>
{ __( 'Cancel' ) }
</Button>
<Button
__next40pxDefaultSize
variant="primary"
type="submit"
disabled={ isSaving }
accessibleWhenDisabled
>
{ modalButtonLabel }
</Button>
</HStack>
</VStack>
</form>
);
};

export const useSetAsPostsPageAction = () => {
const { pageOnFront, pageForPosts } = useSelect( ( select ) => {
const { getEntityRecord } = select( coreStore );
const siteSettings = getEntityRecord( 'root', 'site' );
return {
pageOnFront: siteSettings?.page_on_front,
pageForPosts: siteSettings?.page_for_posts,
};
} );

return useMemo(
() => ( {
id: 'set-as-posts-page',
label: __( 'Set as posts page' ),
isEligible( post ) {
if ( post.status !== 'publish' ) {
return false;
}

if ( post.type !== 'page' ) {
return false;
}

// Don't show the action if the page is already set as the homepage.
if ( pageOnFront === post.id ) {
return false;
}

// Don't show the action if the page is already set as the page for posts.
if ( pageForPosts === post.id ) {
return false;
}

return true;
},
RenderModal: SetAsPostsPageModal,
} ),
[ pageForPosts, pageOnFront ]
);
};
25 changes: 25 additions & 0 deletions packages/editor/src/utils/get-item-title.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* WordPress dependencies
*/
import { decodeEntities } from '@wordpress/html-entities';

/**
* Helper function to get the title of a post item.
* This is duplicated from the `@wordpress/fields` package.
* `packages/fields/src/actions/utils.ts`
*
* @param {Object} item The post item.
* @return {string} The title of the item, or an empty string if the title is not found.
*/
export function getItemTitle( item ) {
if ( typeof item.title === 'string' ) {
return decodeEntities( item.title );
}
if ( item.title && 'rendered' in item.title ) {
return decodeEntities( item.title.rendered );
}
if ( item.title && 'raw' in item.title ) {
return decodeEntities( item.title.raw );
}
return '';
}
56 changes: 47 additions & 9 deletions test/e2e/specs/site-editor/homepage-settings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ test.describe( 'Homepage Settings via Editor', () => {
title: 'Homepage',
status: 'publish',
} );
await requestUtils.createPage( {
title: 'Sample page',
status: 'publish',
} );
await requestUtils.createPage( {
title: 'Draft page',
status: 'draft',
} );
} );

test.beforeEach( async ( { admin, page } ) => {
Expand All @@ -28,27 +36,30 @@ test.describe( 'Homepage Settings via Editor', () => {
] );
} );

test( 'should show "Set as homepage" action on pages with `publish` status', async ( {
test( 'should not show "Set as homepage" and "Set as posts page" action on pages with `draft` status', async ( {
page,
} ) => {
const samplePage = page
const draftPage = page
.getByRole( 'gridcell' )
.getByLabel( 'Homepage' );
const samplePageRow = page
.getByLabel( 'Draft page' );
const draftPageRow = page
.getByRole( 'row' )
.filter( { has: samplePage } );
await samplePageRow.hover();
await samplePageRow
.filter( { has: draftPage } );
await draftPageRow.hover();
await draftPageRow
.getByRole( 'button', {
name: 'Actions',
} )
.click();
await expect(
page.getByRole( 'menuitem', { name: 'Set as homepage' } )
).toBeVisible();
).toBeHidden();
await expect(
page.getByRole( 'menuitem', { name: 'Set as posts page' } )
).toBeHidden();
} );

test( 'should not show "Set as homepage" action on current homepage', async ( {
test( 'should show correct homepage actions based on current homepage or posts page', async ( {
page,
} ) => {
const samplePage = page
Expand All @@ -68,5 +79,32 @@ test.describe( 'Homepage Settings via Editor', () => {
await expect(
page.getByRole( 'menuitem', { name: 'Set as homepage' } )
).toBeHidden();
await expect(
page.getByRole( 'menuitem', { name: 'Set as posts page' } )
).toBeHidden();

const samplePageTwo = page
.getByRole( 'gridcell' )
.getByLabel( 'Sample page' );
const samplePageTwoRow = page
.getByRole( 'row' )
.filter( { has: samplePageTwo } );
// eslint-disable-next-line playwright/no-force-option
await samplePageTwoRow.click( { force: true } );
await samplePageTwoRow
.getByRole( 'button', {
name: 'Actions',
} )
.click();
await page
.getByRole( 'menuitem', { name: 'Set as posts page' } )
.click();
await page.getByRole( 'button', { name: 'Set posts page' } ).click();
await expect(
page.getByRole( 'menuitem', { name: 'Set as homepage' } )
).toBeHidden();
await expect(
page.getByRole( 'menuitem', { name: 'Set as posts page' } )
).toBeHidden();
} );
} );
Loading