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

Add the ability to create a new page in the Site Editor #50565

Merged
merged 23 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
105 changes: 105 additions & 0 deletions packages/edit-site/src/components/add-new-page/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* External dependencies
*/
import { kebabCase } from 'lodash';

/**
* WordPress dependencies
*/
import {
Button,
Modal,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
TextControl,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';

export default function AddNewPageModal( { onSave, onClose } ) {
const [ isCreatingPage, setIsCreatingPage ] = useState( false );
const [ title, setTitle ] = useState( '' );

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

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

if ( isCreatingPage ) {
return;
}
setIsCreatingPage( true );
try {
const newPage = await saveEntityRecord(
'postType',
'page',
{
status: 'draft',
title,
slug: kebabCase( title || __( 'No title' ) ),
},
{ throwOnError: true }
);

onSave( newPage );

Copy link
Contributor

Choose a reason for hiding this comment

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

After the page is created, where should focus get placed? Right now it's being dropped at the hidden Back button in the closed sidebar. I agree with @carolinan that the current mode of being moved to the site editor template for this page is an odd experience. I'd be fine with it if the sidebar stayed open, in which case, handling the focus here isn't an issue since it's being placed in a sensible spot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the current mode of being moved to the site editor template for this page is an odd experience

This will make more sense once the content editing PR is merged. There is also some follow up work around improving the add page experience, including selecting starter patterns.

createSuccessNotice(
sprintf(
// translators: %s: Title of the created template e.g: "Category".
__( '"%s" successfully created.' ),
newPage.title?.rendered || title
),
{
type: 'snackbar',
}
);
} catch ( error ) {
const errorMessage =
error.message && error.code !== 'unknown_error'
? error.message
: __( 'An error occurred while creating the page.' );

createErrorNotice( errorMessage, {
type: 'snackbar',
} );
} finally {
setIsCreatingPage( false );
}
}

return (
<Modal title={ __( 'Draft a new page' ) } onRequestClose={ onClose }>
Copy link
Member

Choose a reason for hiding this comment

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

Not a blocker, but I noticed that when I throttled my connection I could close the modal and click around, only to be redirected to the new page, which seemed confusing.

A bit of an edge case though, unless you live in an area with bad internet speeds like me 😄

2023-06-06.12.01.53.mp4

Maybe in another iteration we could look at blocking the modal close until the save callback is fired?

	const closeModal = () => {
		if ( isCreatingPage ) {
			return;
		}
		onClose();
	};

<form onSubmit={ createPage }>
<VStack spacing={ 3 }>
<TextControl
SaxonF marked this conversation as resolved.
Show resolved Hide resolved
/* eslint-disable jsx-a11y/no-autofocus */
autoFocus
/* eslint-enable jsx-a11y/no-autofocus */
label={ __( 'Page title' ) }
Copy link
Member

Choose a reason for hiding this comment

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

Give page a title and hit create

Not a blocker, just asking if we're not enforcing a page title for creation, maybe add 'Page title (optional)'?

onChange={ setTitle }
placeholder={ __( 'No title' ) }
value={ title }
SaxonF marked this conversation as resolved.
Show resolved Hide resolved
/>
<HStack spacing={ 2 } justify="end">
<Button variant="tertiary" onClick={ onClose }>
{ __( 'Cancel' ) }
</Button>
<Button
variant="primary"
type="submit"
isBusy={ isCreatingPage }
aria-disabled={ isCreatingPage }
>
{ __( 'Create draft' ) }
</Button>
</HStack>
</VStack>
</form>
</Modal>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
__experimentalTruncate as Truncate,
__experimentalVStack as VStack,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { useEntityRecords, store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { layout, page, home, loop } from '@wordpress/icons';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { layout, page, home, loop, plus } from '@wordpress/icons';
import { useSelect } from '@wordpress/data';

/**
Expand All @@ -19,6 +21,11 @@ import { useSelect } from '@wordpress/data';
import SidebarNavigationScreen from '../sidebar-navigation-screen';
import { useLink } from '../routes/link';
import SidebarNavigationItem from '../sidebar-navigation-item';
import SidebarButton from '../sidebar-button';
import AddNewPageModal from '../add-new-page';
import { unlock } from '../../private-apis';

const { useHistory } = unlock( routerPrivateApis );

const PageItem = ( { postType = 'page', postId, ...props } ) => {
const linkInfo = useLink( {
Expand Down Expand Up @@ -85,98 +92,127 @@ export default function SidebarNavigationScreenPages() {
reorderedPages.splice( 1, 0, ...blogPage );
}

const [ showAddPage, setShowAddPage ] = useState( false );

const history = useHistory();

const handleNewPage = ( { type, id } ) => {
// Navigate to the created template editor.
history.push( {
SaxonF marked this conversation as resolved.
Show resolved Hide resolved
postId: id,
postType: type,
canvas: 'edit',
} );
setShowAddPage( false );
};

return (
<SidebarNavigationScreen
title={ __( 'Pages' ) }
description={ __( 'Browse and edit pages on your site.' ) }
content={
<>
{ ( isLoadingPages || isLoadingTemplates ) && (
<ItemGroup>
<Item>{ __( 'Loading pages' ) }</Item>
</ItemGroup>
) }
{ ! ( isLoadingPages || isLoadingTemplates ) && (
<ItemGroup>
{ ! pagesAndTemplates?.length && (
<Item>{ __( 'No page found' ) }</Item>
) }
{ isHomePageBlog && homeTemplate && (
<PageItem
postType="wp_template"
postId={ homeTemplate.id }
key={ homeTemplate.id }
icon={ home }
withChevron
>
<Truncate numberOfLines={ 1 }>
{ decodeEntities(
homeTemplate.title?.rendered ||
__( '(no title)' )
) }
</Truncate>
</PageItem>
) }
{ reorderedPages?.map( ( item ) => {
let itemIcon;
switch ( item.id ) {
case frontPage:
itemIcon = home;
break;
case postsPage:
itemIcon = loop;
break;
default:
itemIcon = page;
}
return (
<PageItem
postId={ item.id }
key={ item.id }
icon={ itemIcon }
withChevron
>
<Truncate numberOfLines={ 1 }>
{ decodeEntities(
item?.title?.rendered ||
__( '(no title)' )
) }
</Truncate>
</PageItem>
);
} ) }
<VStack className="edit-site-sidebar-navigation-screen__sticky-section">
{ dynamicPageTemplates?.map( ( item ) => (
<>
{ showAddPage && (
<AddNewPageModal
onSave={ handleNewPage }
onClose={ () => setShowAddPage( false ) }
/>
) }
<SidebarNavigationScreen
title={ __( 'Pages' ) }
description={ __( 'Browse and edit pages on your site.' ) }
actions={
<SidebarButton
icon={ plus }
label={ __( 'Draft a new page' ) }
onClick={ () => setShowAddPage( true ) }
/>
}
content={
<>
{ ( isLoadingPages || isLoadingTemplates ) && (
<ItemGroup>
<Item>{ __( 'Loading pages' ) }</Item>
</ItemGroup>
) }
{ ! ( isLoadingPages || isLoadingTemplates ) && (
<ItemGroup>
{ ! pagesAndTemplates?.length && (
<Item>{ __( 'No page found' ) }</Item>
) }
{ isHomePageBlog && homeTemplate && (
<PageItem
postType="wp_template"
postId={ item.id }
key={ item.id }
icon={ layout }
postId={ homeTemplate.id }
key={ homeTemplate.id }
icon={ home }
withChevron
>
<Truncate numberOfLines={ 1 }>
{ decodeEntities(
item.title?.rendered ||
homeTemplate.title?.rendered ||
__( '(no title)' )
) }
</Truncate>
</PageItem>
) ) }
<SidebarNavigationItem
className="edit-site-sidebar-navigation-screen-pages__see-all"
href="edit.php?post_type=page"
onClick={ () => {
document.location =
'edit.php?post_type=page';
} }
>
{ __( 'Manage all pages' ) }
</SidebarNavigationItem>
</VStack>
</ItemGroup>
) }
</>
}
/>
) }
{ reorderedPages?.map( ( item ) => {
let itemIcon;
switch ( item.id ) {
case frontPage:
itemIcon = home;
break;
case postsPage:
itemIcon = loop;
break;
default:
itemIcon = page;
}
return (
<PageItem
postId={ item.id }
key={ item.id }
icon={ itemIcon }
withChevron
>
<Truncate numberOfLines={ 1 }>
{ decodeEntities(
item?.title?.rendered ||
__( '(no title)' )
) }
</Truncate>
</PageItem>
);
} ) }
<VStack className="edit-site-sidebar-navigation-screen__sticky-section">
{ dynamicPageTemplates?.map( ( item ) => (
<PageItem
postType="wp_template"
postId={ item.id }
key={ item.id }
icon={ layout }
withChevron
>
<Truncate numberOfLines={ 1 }>
{ decodeEntities(
item.title?.rendered ||
__( '(no title)' )
) }
</Truncate>
</PageItem>
) ) }
<SidebarNavigationItem
className="edit-site-sidebar-navigation-screen-pages__see-all"
href="edit.php?post_type=page"
onClick={ () => {
document.location =
SaxonF marked this conversation as resolved.
Show resolved Hide resolved
'edit.php?post_type=page';
} }
>
{ __( 'Manage all pages' ) }
</SidebarNavigationItem>
</VStack>
</ItemGroup>
) }
</>
}
/>
</>
);
}
30 changes: 30 additions & 0 deletions test/e2e/specs/site-editor/pages.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* WordPress dependencies
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

test.describe( 'Pages', () => {
test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'emptytheme' );
} );
test( 'Create a new page', async ( { admin, page } ) => {
const pageName = 'demo';
await admin.visitSiteEditor();
await page.getByRole( 'button', { name: 'Pages' } ).click();
await page.getByRole( 'button', { name: 'Draft a new page' } ).click();
// Fill the page title and submit.
const newPageDialog = page.locator(
'role=dialog[name="Draft a new page"i]'
);
const pageTitleInput = newPageDialog.locator(
'role=textbox[name="Page title"i]'
);
await pageTitleInput.fill( pageName );
await page.keyboard.press( 'Enter' );
await expect(
page.locator(
`role=button[name="Dismiss this notice"i] >> text="${ pageName }" successfully created.`
)
).toBeVisible();
} );
} );