diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 474207aa20460f..199c29cd67dd2e 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -359,7 +359,7 @@ _Parameters_ - _state_ `State`: State tree - _kind_ `string`: Entity kind. - _name_ `string`: Entity name. -- _key_ `EntityRecordKey`: Record's key +- _key_ `EntityRecordKey`: Optional record's key. If requesting a global record (e.g. site settings), the key can be omitted. If requesting a specific item, the key must always be included. - _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available "Retrieve a [Entity kind]". _Returns_ diff --git a/packages/core-data/README.md b/packages/core-data/README.md index eb6980cdd4eea1..9549e6742d8cd8 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -581,7 +581,7 @@ _Parameters_ - _state_ `State`: State tree - _kind_ `string`: Entity kind. - _name_ `string`: Entity name. -- _key_ `EntityRecordKey`: Record's key +- _key_ `EntityRecordKey`: Optional record's key. If requesting a global record (e.g. site settings), the key can be omitted. If requesting a specific item, the key must always be included. - _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available "Retrieve a [Entity kind]". _Returns_ diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 02fe152ed0abb6..77790512653065 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -151,7 +151,6 @@ export const getHomePage = createRegistrySelector( ( select ) => return { postType: 'wp_template', postId: frontPageTemplateId }; }, ( state ) => [ - // @ts-expect-error getEntityRecord( state, 'root', 'site' ), getDefaultTemplateId( state, { slug: 'front-page', diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 7ea8c2f7f26d53..7f4b0d38846468 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -310,7 +310,7 @@ export interface GetEntityRecord { state: State, kind: string, name: string, - key: EntityRecordKey, + key?: EntityRecordKey, query?: GetRecordsHttpQuery ): EntityRecord | undefined; @@ -321,7 +321,7 @@ export interface GetEntityRecord { >( kind: string, name: string, - key: EntityRecordKey, + key?: EntityRecordKey, query?: GetRecordsHttpQuery ) => EntityRecord | undefined; __unstableNormalizeArgs?: ( args: EntityRecordArgs ) => EntityRecordArgs; @@ -335,7 +335,7 @@ export interface GetEntityRecord { * @param state State tree * @param kind Entity kind. * @param name Entity name. - * @param key Record's key + * @param key Optional record's key. If requesting a global record (e.g. site settings), the key can be omitted. If requesting a specific item, the key must always be included. * @param query Optional query. If requesting specific * fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available "Retrieve a [Entity kind]". * @@ -350,7 +350,7 @@ export const getEntityRecord = createSelector( state: State, kind: string, name: string, - key: EntityRecordKey, + key?: EntityRecordKey, query?: GetRecordsHttpQuery ): EntityRecord | undefined => { const queriedState = diff --git a/packages/dataviews/src/components/dataviews-item-actions/index.tsx b/packages/dataviews/src/components/dataviews-item-actions/index.tsx index 787cef4420acc0..b5eaac11bcd8d0 100644 --- a/packages/dataviews/src/components/dataviews-item-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-item-actions/index.tsx @@ -114,7 +114,7 @@ export function ActionModal< Item >( { __experimentalHideHeader={ !! action.hideModalHeader } onRequestClose={ closeModal ?? ( () => {} ) } focusOnMount="firstContentElement" - size="small" + size="medium" overlayClassName={ `dataviews-action-modal dataviews-action-modal__${ kebabCase( action.id ) }` } diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 8dbe5b9dfcd5ad..1b6ff4fbe384b5 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -3,12 +3,14 @@ */ import { useDispatch, useSelect } from '@wordpress/data'; import { useMemo, useEffect } from '@wordpress/element'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import { useSetAsHomepageAction } from './set-as-homepage'; export function usePostActions( { postType, onActionPerformed, context } ) { const { defaultActions } = useSelect( @@ -21,19 +23,46 @@ export function usePostActions( { postType, onActionPerformed, context } ) { [ postType ] ); + const { canManageOptions, hasFrontPageTemplate } = useSelect( + ( select ) => { + const { getEntityRecords } = select( coreStore ); + const templates = getEntityRecords( 'postType', 'wp_template', { + per_page: -1, + } ); + + return { + canManageOptions: select( coreStore ).canUser( 'update', { + kind: 'root', + name: 'site', + } ), + hasFrontPageTemplate: !! templates?.find( + ( template ) => template?.slug === 'front-page' + ), + }; + } + ); + + const setAsHomepageAction = useSetAsHomepageAction(); + const shouldShowSetAsHomepageAction = + canManageOptions && ! hasFrontPageTemplate; + const { registerPostTypeSchema } = unlock( useDispatch( editorStore ) ); useEffect( () => { registerPostTypeSchema( postType ); }, [ registerPostTypeSchema, postType ] ); return useMemo( () => { + let actions = [ + ...defaultActions, + shouldShowSetAsHomepageAction ? setAsHomepageAction : [], + ]; // 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. // Actions should also provide the `context` they support, if it's specific, to // compare with the provided context to get all the actions. // Right now the only supported context is `list`. - const actions = defaultActions.filter( ( action ) => { + actions = actions.filter( ( action ) => { if ( ! action.context ) { return true; } @@ -88,5 +117,11 @@ export function usePostActions( { postType, onActionPerformed, context } ) { } return actions; - }, [ defaultActions, onActionPerformed, context ] ); + }, [ + context, + defaultActions, + onActionPerformed, + setAsHomepageAction, + shouldShowSetAsHomepageAction, + ] ); } diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index 9f39b1f3305aeb..ab11b5e318b5a6 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -123,7 +123,7 @@ function ActionWithModal( { action, item, ActionTrigger, onClose } ) { action.id ) }` } focusOnMount="firstContentElement" - size="small" + size="medium" > { + const [ item ] = items; + const pageTitle = getItemTitle( item ); + const { showOnFront, currentHomePage, isSaving } = useSelect( + ( select ) => { + const { getEntityRecord, isSavingEntityRecord } = + select( coreStore ); + const siteSettings = getEntityRecord( 'root', 'site' ); + const currentHomePageItem = getEntityRecord( + 'postType', + 'page', + siteSettings?.page_on_front + ); + return { + showOnFront: siteSettings?.show_on_front, + currentHomePage: currentHomePageItem, + isSaving: isSavingEntityRecord( 'root', 'site' ), + }; + } + ); + const currentHomePageTitle = currentHomePage + ? getItemTitle( currentHomePage ) + : ''; + + const { saveEditedEntityRecord, 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' ), { + type: 'snackbar', + } ); + } catch ( error ) { + const typedError = error; + const errorMessage = + typedError.message && typedError.code !== 'unknown_error' + ? typedError.message + : __( 'An error occurred while setting the homepage' ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } finally { + closeModal?.(); + } + } + + const modalWarning = + 'posts' === showOnFront + ? __( + 'This will replace the current homepage which is set to display latest posts.' + ) + : sprintf( + // translators: %s: title of the current home page. + __( 'This will replace the current homepage: "%s"' ), + currentHomePageTitle + ); + + const modalText = sprintf( + // translators: %1$s: title of the page to be set as the homepage, %2$s: homepage replacement warning message. + __( 'Set "%1$s" as the site homepage? %2$s' ), + pageTitle, + modalWarning + ); + + // translators: Button label to confirm setting the specified page as the homepage. + const modalButtonLabel = __( 'Set homepage' ); + + return ( +
+ + { modalText } + + + + + +
+ ); +}; + +export const useSetAsHomepageAction = () => { + 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-homepage', + label: __( 'Set as homepage' ), + 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: SetAsHomepageModal, + } ), + [ pageForPosts, pageOnFront ] + ); +}; diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 664c2dd417201c..4d27fc7dc4139d 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -1,5 +1,5 @@ type PostStatus = - | 'published' + | 'publish' | 'draft' | 'pending' | 'private' diff --git a/packages/fields/src/actions/utils.ts b/packages/fields/src/actions/utils.ts index 60d3d00e82766a..8f990fb1168fcc 100644 --- a/packages/fields/src/actions/utils.ts +++ b/packages/fields/src/actions/utils.ts @@ -30,7 +30,7 @@ export function isTemplateOrTemplatePart( return p.type === TEMPLATE_POST_TYPE || p.type === TEMPLATE_PART_POST_TYPE; } -export function getItemTitle( item: Post ) { +export function getItemTitle( item: Post ): string { if ( typeof item.title === 'string' ) { return decodeEntities( item.title ); } diff --git a/packages/fields/src/fields/title/title-view.tsx b/packages/fields/src/fields/title/title-view.tsx index c15ed96b89b73b..f6bf5fb1817d93 100644 --- a/packages/fields/src/fields/title/title-view.tsx +++ b/packages/fields/src/fields/title/title-view.tsx @@ -17,11 +17,10 @@ import { getItemTitle } from '../../actions/utils'; const TitleView = ( { item }: { item: BasePost } ) => { const { frontPageId, postsPageId } = useSelect( ( select ) => { const { getEntityRecord } = select( coreStore ); - const siteSettings: Settings | undefined = getEntityRecord( + const siteSettings = getEntityRecord( 'root', - 'site', - '' - ); + 'site' + ) as Partial< Settings >; return { frontPageId: siteSettings?.page_on_front, postsPageId: siteSettings?.page_for_posts, diff --git a/test/e2e/specs/site-editor/homepage-settings.spec.js b/test/e2e/specs/site-editor/homepage-settings.spec.js new file mode 100644 index 00000000000000..d53130af23ac8b --- /dev/null +++ b/test/e2e/specs/site-editor/homepage-settings.spec.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Homepage Settings via Editor', () => { + test.beforeAll( async ( { requestUtils } ) => { + await Promise.all( [ requestUtils.activateTheme( 'emptytheme' ) ] ); + await requestUtils.createPage( { + title: 'Homepage', + status: 'publish', + } ); + } ); + + test.beforeEach( async ( { admin, page } ) => { + await admin.visitSiteEditor(); + await page.getByRole( 'button', { name: 'Pages' } ).click(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.deleteAllPages(), + requestUtils.updateSiteSettings( { + show_on_front: 'posts', + page_on_front: 0, + page_for_posts: 0, + } ), + ] ); + } ); + + test( 'should show "Set as homepage" action on pages with `publish` status', async ( { + page, + } ) => { + const samplePage = page + .getByRole( 'gridcell' ) + .getByLabel( 'Homepage' ); + const samplePageRow = page + .getByRole( 'row' ) + .filter( { has: samplePage } ); + await samplePageRow.hover(); + await samplePageRow + .getByRole( 'button', { + name: 'Actions', + } ) + .click(); + await expect( + page.getByRole( 'menuitem', { name: 'Set as homepage' } ) + ).toBeVisible(); + } ); + + test( 'should not show "Set as homepage" action on current homepage', async ( { + page, + } ) => { + const samplePage = page + .getByRole( 'gridcell' ) + .getByLabel( 'Homepage' ); + const samplePageRow = page + .getByRole( 'row' ) + .filter( { has: samplePage } ); + await samplePageRow.click(); + await samplePageRow + .getByRole( 'button', { + name: 'Actions', + } ) + .click(); + await page.getByRole( 'menuitem', { name: 'Set as homepage' } ).click(); + await page.getByRole( 'button', { name: 'Set homepage' } ).click(); + await expect( + page.getByRole( 'menuitem', { name: 'Set as homepage' } ) + ).toBeHidden(); + } ); +} );