Skip to content

Commit

Permalink
Data Views: Add action for pages to set site homepage (#65426)
Browse files Browse the repository at this point in the history
* Adds basic action and modal to set page as homepage

* Adds permissions and settings checks to set as homepage action

* Adds proper description and handles unpublished pages

* Adds action to set homepage to show latest posts

* Doesn't show action if there's a front-page template

* Creates page for posts, when specified

* Refactors modal component

* Fixes issues from rebase

* Only show option on published pages

* Update snackbar wording

* Check item exists before running getItemTitle logic

* Make key optional on GetEntityRecord

* Remove some ts-ignore comments

* Add support for page_for_posts to Settings

* Remove some more ts-ignores

* Allow recordId to be optional

* Increase size of action modal

* Implement choose existing page option

* Fix number/string comparison

* Add initial e2e test

* Set post actions modal to medium

* Tweak ToggleGroupControl help text

* Fix initial test

* Remove extra useSiteSettings hook

* Allow setting draft pages to homepage

* Fix merge conflict

* Remove item check from getItemTitle

* Remove posts page options

* Don't show homepage option if selected page is the page for posts

* Reload actions list when site settings change

* Update tests

* Remove call to __experimentalGetTemplateForLink

* Update tests

* Remove item check in getItemTitle

* Use useSelect instead of select

* Remove PAGE_POST_TYPE constant

* Use saveEntityRecord instead of editEntityRecord

* Remove onSetLatestPostsHomepage option

* Remove select for site settings from isEligible

* Update post actions with site settings info

* Remove select for templates from isEligible

* Skip last test for now

* Restore whitespace

* Rename _select

* Remove sub-objects from additionalContext selectors

* Remove duplicate page_for_posts definition

* Fix page/post type error

* Remove additional groups within additionalContext

* Fix siteSettings in TitleView

* Move hasFrontPageTemplate check to private-actions

* Add JSDoc to setAsHomepage

* Refactor siteSettings in post-list

* Move homepage action to edit-site

* Revert unnecessary changes

* Move getItemTitle to edit-site utils

* Allow undefined on GetEntityRecord key

* Make it more clear that draft page will be published

* Update draft page wording

* Add set homepage action to post editor

* Attempt to fix build error

* Remove homepage action from edit-site

* Remove extra line

* Fix getting current homepage title

* Make key in getEntityRecord optional

* Use getHomePage selector

* Move canManageOptions and hasFrontPageTemplate to actions.js

* Make key optional in EntityRecordKey

* Remove undefined from getEntityRecord calls

* Update packages/editor/src/components/post-actions/actions.js

Co-authored-by: Dave Smith <[email protected]>

* Update getEntityRecord key docs

* Refactor fetching currentHomePage

* Disable modal buttons if saving

* Store isPageDraft in useRef

* Fix lint error

* Remove onActionPerformed

* Fix current homepage test

* Remove duplicate getItemTitle function

* Update logic for shouldShowSetAsHomepageAction

* Swap order of list of actions

* Add comment about manual saveEntityRecord call

* Remove unnecessary space

* Remove temporary modalButtonLabel variable

* Combine draft and publish status tests

* Only allow action on pages with draft or publish status

* Remove handling of draft pages

* Move closeModal into finally block

* Refactor and remove renderModalBody

---------

Co-authored-by: Sarah Norris <[email protected]>
Co-authored-by: Sarah Norris <[email protected]>
Co-authored-by: Dave Smith <[email protected]>
Co-authored-by: creativecoder <[email protected]>
Co-authored-by: mikachan <[email protected]>
Co-authored-by: ntsekouras <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: jsnajdr <[email protected]>
Co-authored-by: ellatrix <[email protected]>
Co-authored-by: oandregal <[email protected]>
Co-authored-by: getdave <[email protected]>
Co-authored-by: jameskoster <[email protected]>
Co-authored-by: richtabor <[email protected]>
Co-authored-by: ramonjd <[email protected]>
Co-authored-by: jasmussen <[email protected]>
Co-authored-by: mtias <[email protected]>
  • Loading branch information
17 people authored Nov 28, 2024
1 parent b54d1fe commit d64cdab
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 17 deletions.
2 changes: 1 addition & 1 deletion docs/reference-guides/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down
2 changes: 1 addition & 1 deletion packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down
1 change: 0 additions & 1 deletion packages/core-data/src/private-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export interface GetEntityRecord {
state: State,
kind: string,
name: string,
key: EntityRecordKey,
key?: EntityRecordKey,
query?: GetRecordsHttpQuery
): EntityRecord | undefined;

Expand All @@ -321,7 +321,7 @@ export interface GetEntityRecord {
>(
kind: string,
name: string,
key: EntityRecordKey,
key?: EntityRecordKey,
query?: GetRecordsHttpQuery
) => EntityRecord | undefined;
__unstableNormalizeArgs?: ( args: EntityRecordArgs ) => EntityRecordArgs;
Expand All @@ -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]".
*
Expand All @@ -350,7 +350,7 @@ export const getEntityRecord = createSelector(
state: State,
kind: string,
name: string,
key: EntityRecordKey,
key?: EntityRecordKey,
query?: GetRecordsHttpQuery
): EntityRecord | undefined => {
const queriedState =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
) }` }
Expand Down
39 changes: 37 additions & 2 deletions packages/editor/src/components/post-actions/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
}
Expand Down Expand Up @@ -88,5 +117,11 @@ export function usePostActions( { postType, onActionPerformed, context } ) {
}

return actions;
}, [ defaultActions, onActionPerformed, context ] );
}, [
context,
defaultActions,
onActionPerformed,
setAsHomepageAction,
shouldShowSetAsHomepageAction,
] );
}
2 changes: 1 addition & 1 deletion packages/editor/src/components/post-actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function ActionWithModal( { action, item, ActionTrigger, onClose } ) {
action.id
) }` }
focusOnMount="firstContentElement"
size="small"
size="medium"
>
<RenderModal
items={ [ item ] }
Expand Down
174 changes: 174 additions & 0 deletions packages/editor/src/components/post-actions/set-as-homepage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* 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 '../../dataviews/actions/utils';

const SetAsHomepageModal = ( { items, closeModal } ) => {
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 (
<form onSubmit={ onSetPageAsHomepage }>
<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 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 ]
);
};
2 changes: 1 addition & 1 deletion packages/editor/src/dataviews/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
type PostStatus =
| 'published'
| 'publish'
| 'draft'
| 'pending'
| 'private'
Expand Down
2 changes: 1 addition & 1 deletion packages/fields/src/actions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Expand Down
7 changes: 3 additions & 4 deletions packages/fields/src/fields/title/title-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit d64cdab

Please sign in to comment.