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

Post fields: move author from edit-site to fields package #66939

Merged
merged 3 commits into from
Nov 14, 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
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 4 additions & 61 deletions packages/edit-site/src/components/post-fields/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
/**
* External dependencies
*/
import clsx from 'clsx';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
featuredImageField,
slugField,
Expand All @@ -16,50 +10,10 @@ import {
commentStatusField,
titleField,
dateField,
authorField,
} from '@wordpress/fields';
import { useMemo, useState } from '@wordpress/element';
import { commentAuthorAvatar as authorIcon } from '@wordpress/icons';
import { __experimentalHStack as HStack, Icon } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useEntityRecords, store as coreStore } from '@wordpress/core-data';

function PostAuthorField( { item } ) {
const { text, imageUrl } = useSelect(
( select ) => {
const { getUser } = select( coreStore );
const user = getUser( item.author );
return {
imageUrl: user?.avatar_urls?.[ 48 ],
text: user?.name,
};
},
[ item ]
);
const [ isImageLoaded, setIsImageLoaded ] = useState( false );
return (
<HStack alignment="left" spacing={ 0 }>
{ !! imageUrl && (
<div
className={ clsx( 'page-templates-author-field__avatar', {
'is-loaded': isImageLoaded,
} ) }
>
<img
onLoad={ () => setIsImageLoaded( true ) }
alt={ __( 'Author avatar' ) }
src={ imageUrl }
/>
</div>
) }
{ ! imageUrl && (
<div className="page-templates-author-field__icon">
<Icon icon={ authorIcon } />
</div>
) }
<span className="page-templates-author-field__name">{ text }</span>
</HStack>
);
}
import { useMemo } from '@wordpress/element';
import { useEntityRecords } from '@wordpress/core-data';

function usePostFields() {
const { records: authors, isResolving: isLoadingAuthors } =
Expand All @@ -70,23 +24,12 @@ function usePostFields() {
featuredImageField,
titleField,
{
label: __( 'Author' ),
id: 'author',
type: 'integer',
...authorField,
elements:
authors?.map( ( { id, name } ) => ( {
Copy link
Member Author

Choose a reason for hiding this comment

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

The usePostFields hook returns an isLoading that is the isLoadingAuthors. That's why I've left the elements definition in this file.

I think this approach is ok because the final goal is to expose usePostFields to consumers, so they won't have to hook the data the author field anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think ultimately we need to think about that. Fields can have dynamic list of elements, how to represent it. I think the filter UI should be able to handle dynamic elements ...

Copy link
Member Author

@oandregal oandregal Nov 13, 2024

Choose a reason for hiding this comment

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

That's a good thought, and I don't have a good answer for that right now. A related example is the render function of this same component: it uses the core store to get the author & image name for the given item. In that sense, the data is still "loading".

Do you think we should address this differently in this PR or is good enough to land?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think it can be explored separately.

value: id,
label: name,
} ) ) || [],
render: PostAuthorField,
sort: ( a, b, direction ) => {
const nameA = a._embedded?.author?.[ 0 ]?.name || '';
const nameB = b._embedded?.author?.[ 0 ]?.name || '';

return direction === 'asc'
? nameA.localeCompare( nameB )
: nameB.localeCompare( nameA );
},
},
statusField,
dateField,
Expand Down
4 changes: 4 additions & 0 deletions packages/fields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ npm install @wordpress/fields --save

<!-- START TOKEN(Autogenerated API docs) -->

### authorField

Author field for BasePost.

### commentStatusField

Comment status field for BasePost.
Expand Down
1 change: 1 addition & 0 deletions packages/fields/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@wordpress/warning": "*",
"change-case": "4.1.2",
"client-zip": "^2.4.5",
"clsx": "2.1.1",
"remove-accents": "^0.5.0"
},
"peerDependencies": {
Expand Down
63 changes: 63 additions & 0 deletions packages/fields/src/fields/author/author-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* External dependencies
*/
import clsx from 'clsx';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { commentAuthorAvatar as authorIcon } from '@wordpress/icons';
import { __experimentalHStack as HStack, Icon } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import type { User } from '@wordpress/core-data';

/**
* Internal dependencies
*/
import type { BasePostWithEmbeddedAuthor } from '../../types';

function AuthorView( { item }: { item: BasePostWithEmbeddedAuthor } ) {
const { text, imageUrl } = useSelect(
( select ) => {
const { getEntityRecord } = select( coreStore );
let user: User | undefined;
if ( !! item.author ) {
user = getEntityRecord( 'root', 'user', item.author );
}
Comment on lines +25 to +29
Copy link
Member Author

Choose a reason for hiding this comment

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

There's an opportunity for improvement in this field. This code is triggering a request for user data (avatar + username), and the Pages page may potentially trigger as many requests as pages are visible if they have different authors.

But we already have that data via multiple sources:

  • The usePostFields hook requests all the users.
  • The request to the pages endpoint embeds the author data in the item. However, this is done is a way that is not there from the beginning, but rather the pages data come in and then later it comes the _embedded piece of it. This prevents us from using the _embedded data directly and only trigger the request if it becomes stale (the user updated the item's author via QuickEdit).

It's all a bit too convoluted, and there's no obvious solution, so I'd suggest looking at this separately from this PR.

Thoughts about potential ways forward:

  1. Given the item has elements that are the "valid elements", we could use that as a source for data retrieval. It means augmenting the elements with miscelaneous data plus making them accessible to the render function. By combining this with optimizing the data request so it is only received by DataViews when the embedded data is added to the entity would allow us to eliminate all author REST requests.
  2. Finding a way to augment item data. This is related to the conversation at Data Views: Add action for pages to set site homepage #65426 (comment)

return {
imageUrl: user?.avatar_urls?.[ 48 ],
text: user?.name,
};
Comment on lines +25 to +33
Copy link
Member Author

@oandregal oandregal Nov 12, 2024

Choose a reason for hiding this comment

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

I had to rewrite the nice-looking dynamic getUser selector into using getEntityRecord one. TypeScript was complaining because it wasn't able to find the getUser one. It was like this before:

const { getUser } = select( coreStore );
const user = getUser( item.author );
return {
  imageUrl: user?.avatar_urls?.[ 48 ],
  text: user?.name,
};

cc @youknowriad @sirreal can we use the dynamic selectors in TypeScript? I'm fine using the general selector, but I'm curious if there's a way to use the dynamic one that I didn't find.

Copy link
Member

Choose a reason for hiding this comment

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

I'd love to help but honestly I'm not up to speed on the data packages and TypeScript. I believe @adamziel was working on TypeScript support in the past but I'm not sure what the current status is.

This PR may be good to look at: #43643

Copy link
Member

Choose a reason for hiding this comment

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

In #66997 @manzoorwanijk is doing some work to improve TypeScript support.

},
[ item ]
);
const [ isImageLoaded, setIsImageLoaded ] = useState( false );
return (
<HStack alignment="left" spacing={ 0 }>
{ !! imageUrl && (
<div
className={ clsx( 'page-templates-author-field__avatar', {
'is-loaded': isImageLoaded,
} ) }
>
<img
onLoad={ () => setIsImageLoaded( true ) }
alt={ __( 'Author avatar' ) }
src={ imageUrl }
/>
</div>
) }
{ ! imageUrl && (
<div className="page-templates-author-field__icon">
<Icon icon={ authorIcon } />
</div>
) }
<span className="page-templates-author-field__name">{ text }</span>
</HStack>
);
}

export default AuthorView;
32 changes: 32 additions & 0 deletions packages/fields/src/fields/author/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* WordPress dependencies
*/
import type { Field } from '@wordpress/dataviews';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import type { BasePostWithEmbeddedAuthor } from '../../types';
import AuthorView from './author-view';

const authorField: Field< BasePostWithEmbeddedAuthor > = {
label: __( 'Author' ),
id: 'author',
type: 'integer',
elements: [],
render: AuthorView,
sort: ( a, b, direction ) => {
const nameA = a._embedded?.author?.[ 0 ]?.name || '';
const nameB = b._embedded?.author?.[ 0 ]?.name || '';

return direction === 'asc'
? nameA.localeCompare( nameB )
: nameB.localeCompare( nameA );
},
};

/**
* Author field for BasePost.
*/
export default authorField;
1 change: 1 addition & 0 deletions packages/fields/src/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as passwordField } from './password';
export { default as statusField } from './status';
export { default as commentStatusField } from './comment-status';
export { default as dateField } from './date';
export { default as authorField } from './author';
14 changes: 14 additions & 0 deletions packages/fields/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ interface Links {
[ key: string ]: { href: string }[] | undefined;
}

interface Author {
name: string;
avatar_urls: Record< string, string >;
}

interface EmbeddedAuthor {
author: Author[];
}

export interface BasePost extends CommonPost {
comment_status?: 'open' | 'closed';
excerpt?: string | { raw: string; rendered: string };
Expand All @@ -39,6 +48,11 @@ export interface BasePost extends CommonPost {
permalink_template?: string;
date?: string;
modified?: string;
author?: number;
}

export interface BasePostWithEmbeddedAuthor extends BasePost {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be BasePostWithEmbeddedAuthor or BasePostWithEmbeds. Basically what I'm asking is what are we doing at the request level is it something like embed=true or embed[]=author

Copy link
Member Author

Choose a reason for hiding this comment

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

It's _embed=author, that's why I thought it'd be best to make it specific to that. Happy to rename it to BasePostWithEmbeds.

_embedded: EmbeddedAuthor;
}

export interface Template extends CommonPost {
Expand Down
Loading