Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 3 additions & 1 deletion docs/reference-guides/data/data-core-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,9 @@ _Returns_

### getEditedPostSlug

Returns the slug for the post being edited, preferring a manually edited value if one exists, then the server-generated slug from the post entity, then a JS approximation of the current post title, and finally the post ID.
Returns the slug for the post being edited: the edited or saved slug if one is set, otherwise the slug derived from the current title, which is what WordPress uses when the post is published. Returns an empty string for a post with neither.

The server-generated slug isn't used: it comes from the saved title, so it lags behind title edits, and for a new post it comes from the `Auto Draft` placeholder.

_Parameters_

Expand Down
39 changes: 19 additions & 20 deletions packages/core-fields/src/post_type_supports/slug/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from '@wordpress/components';
import { copySmall } from '@wordpress/icons';
import { useCopyToClipboard, useInstanceId } from '@wordpress/compose';
import { useDispatch } from '@wordpress/data';
import { useCallback, useState } from '@wordpress/element';
import { useCallback } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { InputControl, InputLayout, Link, Stack } from '@wordpress/ui';
import { safeDecodeURIComponent } from '@wordpress/url';
Expand All @@ -17,8 +17,7 @@ const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/;
/*
* The slug control, laid out with `Stack` and built with `InputControl`, `InputLayout`, and `Link` from
* `@wordpress/ui`, as `use-recommended-components` asks, and styled with a
* CSS module. It keeps the first slug in state rather than in a ref read
* during render.
* CSS module.
*/
const SlugEdit = ( {
field,
Expand All @@ -43,13 +42,12 @@ const SlugEdit = ( {
const isEditable =
PERMALINK_POSTNAME_REGEX.test( permalinkTemplate ) &&
hasActionLink( data, 'wp:action-publish' );
// The slug the post had first, shown and restored while the slug is
// emptied.
const [ originalSlug ] = useState( slug );
const slugToDisplay = slug || originalSlug;
const permalink = isEditable
? `${ permalinkPrefix }${ slugToDisplay }${ permalinkSuffix }`
: safeDecodeURIComponent( data.link || '' );
// A post without a slug or a title has no pretty permalink yet, so use its
// plain one (`?p=ID`).
const permalink =
isEditable && slug
? `${ permalinkPrefix }${ slug }${ permalinkSuffix }`
: safeDecodeURIComponent( data.link || '' );

const onChangeControl = useCallback(
( newValue?: string ) =>
Expand Down Expand Up @@ -109,11 +107,6 @@ const SlugEdit = ( {
onValueChange={ ( newValue ) => {
onChangeControl( newValue );
} }
onBlur={ () => {
if ( slug === '' ) {
onChangeControl( originalSlug );
}
} }
aria-describedby={ postUrlSlugDescriptionId }
/>
<div className={ styles.help }>
Expand All @@ -123,11 +116,17 @@ const SlugEdit = ( {
href={ permalink }
openInNewTab
>
<span>{ permalinkPrefix }</span>
<span className={ styles[ 'help-slug' ] }>
{ slugToDisplay }
</span>
<span>{ permalinkSuffix }</span>
{ slug ? (
<>
<span>{ permalinkPrefix }</span>
<span className={ styles[ 'help-slug' ] }>
{ slug }
</span>
<span>{ permalinkSuffix }</span>
</>
) : (
permalink
) }
</Link>
</div>
</Stack>
Expand Down
15 changes: 8 additions & 7 deletions packages/core-fields/src/post_type_supports/slug/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { getItemTitle } from '../../shared/title/get-item-title';
import type { PostWithSlug } from './types';

/**
* The slug of a post, or the one WordPress would generate for it.
* The slug of a post: the slug if one is set, otherwise the slug derived from
* the current title, which is what WordPress uses when the post is published.
* Empty for a post with neither.
*
* The server-generated slug isn't used: it comes from the saved title, so it
* lags behind title edits, and for a new post it comes from the `Auto Draft`
* placeholder.
*
* @param item The post.
* @return The slug.
Expand All @@ -13,10 +19,5 @@ export const getSlug = ( item: PostWithSlug ): string => {
return '';
}

return (
item.slug ||
item.generated_slug ||
cleanForSlug( getItemTitle( item ) ) ||
( item.id?.toString() ?? '' )
);
return item.slug || cleanForSlug( getItemTitle( item, '' ) );
};
13 changes: 4 additions & 9 deletions packages/core-fields/src/post_type_supports/slug/view.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import type { PostWithSlug } from './types';
import { getSlug } from './utils';

/*
* The slug of a post. It keeps the first slug in state rather than in a ref read during render.
* The slug of a post.
*/
const SlugView = ( { item }: { item: PostWithSlug } ) => {
const slug = getSlug( item );
// The slug the post had first, shown while the slug is emptied.
const [ originalSlug ] = useState( slug );

const slugToDisplay = slug || originalSlug;

return `${ slugToDisplay }`;
// A draft without a slug or a title has no slug yet.
return getSlug( item ) || __( 'None' );
};

export default SlugView;
34 changes: 34 additions & 0 deletions packages/core-fields/src/test/slug-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { getSlug } from '../post_type_supports/slug/utils';

describe( 'getSlug', () => {
it( 'returns the slug if one exists', () => {
expect(
getSlug( {
id: 123,
slug: 'custom-slug',
title: 'Edited Title',
} )
).toBe( 'custom-slug' );
} );

it( 'derives the slug from the current title, not the server-generated slug', () => {
expect(
getSlug( {
id: 123,
title: 'Hello Slug',
generated_slug: 'auto-draft',
} )
).toBe( 'hello-slug' );
} );

it( 'returns an empty string for a post without a slug or a title', () => {
expect(
getSlug( {
id: 123,
title: '',
generated_slug: 'auto-draft',
} )
).toBe( '' );
} );
} );
2 changes: 2 additions & 0 deletions packages/editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
- Visual History: Render the inline removed-text diff (`<del>`) at the theme's body color over a red-tinted background instead of recoloring it with a translucent color-matrix filter, so it keeps the theme's WCAG AA contrast on any background ([#82258](https://github.com/WordPress/gutenberg/pull/82258)).
- Notes: Stop announcing the "Add note" block menu item as opening a dialog ([#84818](https://github.com/WordPress/gutenberg/pull/84818)).
- `EditorProvider`: Keep a preview canvas from restoring a block selection written to the entity by another editor, such as the site editor's navigation sidebar ([#84832](https://github.com/WordPress/gutenberg/pull/84832)).
- Post slug: Show the slug of the current title instead of the server-generated slug, which could be `auto-draft` or the slug of an outdated title. A post without a slug or a title shows `None` and its plain `?p=ID` permalink: `getEditedPostSlug` now returns an empty string instead of the post ID, `getPermalinkParts` uses it as the post name, and `getPermalink` returns the post's `link` ([#84893](https://github.com/WordPress/gutenberg/pull/84893)).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

getEditedPostSlug now returns an empty string instead of the post ID

Should this be under "Breaking changes"?

"Bug fix" is okay if it was never really meant to work that way.

Optional, just asking.

- `PostURL`: Only clean and save the slug when the field loses focus if its value changed, so focusing the field no longer strips characters such as emoji that WordPress kept in the slug ([#84893](https://github.com/WordPress/gutenberg/pull/84893)).

### Internal

Expand Down
47 changes: 31 additions & 16 deletions packages/editor/src/components/post-url/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useSelect, useDispatch } from '@wordpress/data';
import { safeDecodeURIComponent, cleanForSlug } from '@wordpress/url';
import { useState, createInterpolateElement } from '@wordpress/element';
import { useRef, useState, createInterpolateElement } from '@wordpress/element';
import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import {
Expand Down Expand Up @@ -34,7 +34,6 @@ export default function PostURL( { onClose } ) {
const {
isEditable,
postSlug,
hasSlug,
postLink,
permalinkPrefix,
permalinkSuffix,
Expand All @@ -52,7 +51,6 @@ export default function PostURL( { onClose } ) {
postSlug: safeDecodeURIComponent(
select( editorStore ).getEditedPostSlug()
),
hasSlug: !! select( editorStore ).getEditedPostAttribute( 'slug' ),
Comment thread
youknowriad marked this conversation as resolved.
Resolved
viewPostLabel: postType?.labels?.view_item,
postLink: post.link,
permalinkPrefix: permalinkParts?.prefix,
Expand All @@ -65,6 +63,7 @@ export default function PostURL( { onClose } ) {
const { editPost } = useDispatch( editorStore );
const { createNotice } = useDispatch( noticesStore );
const [ forceEmptyField, setForceEmptyField ] = useState( false );
const slugOnFocusRef = useRef();
const copyButtonRef = useCopyToClipboard( permalink, () => {
createNotice( 'info', __( 'Copied Permalink to clipboard.' ), {
isDismissible: true,
Expand Down Expand Up @@ -142,11 +141,20 @@ export default function PostURL( { onClose } ) {
setForceEmptyField( false );
}
} }
onFocus={ ( event ) => {
slugOnFocusRef.current = event.target.value;
} }
onBlur={ ( event ) => {
// Without a slug the field shows a fallback, which for
// a new post is the `auto-draft` placeholder. Writing
// it would pin that value as the post slug.
if ( hasSlug ) {
// Only clean and save a value the user changed. An
// untouched slug came from the server, and cleaning
// it would strip characters `sanitize_title` kept,
// such as emoji. Without a slug the field shows the
// slug derived from the title, and saving it would
// stop the slug from following the title.
if (
event.target.value !==
slugOnFocusRef.current
) {
editPost( {
slug: cleanForSlug(
event.target.value
Expand All @@ -168,15 +176,22 @@ export default function PostURL( { onClose } ) {
href={ postLink }
target="_blank"
>
<span className="editor-post-url__link-prefix">
{ permalinkPrefix }
</span>
<span className="editor-post-url__link-slug">
{ postSlug }
</span>
<span className="editor-post-url__link-suffix">
{ permalinkSuffix }
</span>
{ postSlug ? (
<>
<span className="editor-post-url__link-prefix">
{ permalinkPrefix }
</span>
<span className="editor-post-url__link-slug">
{ postSlug }
</span>
<span className="editor-post-url__link-suffix">
{ permalinkSuffix }
</span>
</>
) : (
// Without a slug the post only has its plain permalink.
permalink
) }
</ExternalLink>
</p>
</>
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/components/post-url/panel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ function PostURLToggle( { isOpen, onClick } ) {
slug: select( editorStore ).getEditedPostSlug(),
};
}, [] );
const decodedSlug = safeDecodeURIComponent( slug );
// A draft without a slug or a title has no slug yet.
const decodedSlug = safeDecodeURIComponent( slug ) || __( 'None' );
return (
<Button
size="compact"
Expand Down
24 changes: 15 additions & 9 deletions packages/editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -980,16 +980,26 @@ export function getPermalink( state ) {
const { prefix, postName, suffix } = permalinkParts;

if ( isPermalinkEditable( state ) ) {
// A post without a slug or a title has no pretty permalink yet, so
// use its plain one (`?p=ID`).
if ( ! postName ) {
return getCurrentPostAttribute( state, 'link' ) ?? null;
}
return prefix + postName + suffix;
}

return prefix;
}

/**
* Returns the slug for the post being edited, preferring a manually edited
* value if one exists, then the server-generated slug from the post entity,
* then a JS approximation of the current post title, and finally the post ID.
* Returns the slug for the post being edited: the edited or saved slug if one
* is set, otherwise the slug derived from the current title, which is what
* WordPress uses when the post is published. Returns an empty string for a
* post with neither.
*
* The server-generated slug isn't used: it comes from the saved title, so it
* lags behind title edits, and for a new post it comes from the `Auto Draft`
* placeholder.
*
* @param {Object} state Editor state.
*
Expand All @@ -998,9 +1008,7 @@ export function getPermalink( state ) {
export function getEditedPostSlug( state ) {
return (
getEditedPostAttribute( state, 'slug' ) ||
getEditedPostAttribute( state, 'generated_slug' ) ||
cleanForSlug( getEditedPostAttribute( state, 'title' ) ) ||
getCurrentPostId( state )
cleanForSlug( getEditedPostAttribute( state, 'title' ) )
);
}

Expand All @@ -1022,9 +1030,7 @@ export function getPermalinkParts( state ) {
return null;
}

const postName =
getEditedPostAttribute( state, 'slug' ) ||
getEditedPostAttribute( state, 'generated_slug' );
const postName = getEditedPostSlug( state );

const [ prefix, suffix ] = permalinkTemplate.split(
PERMALINK_POSTNAME_REGEX
Expand Down
Loading
Loading