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

Switch post-author component to use /wp/v2/users/?who=authors #6515

Merged
merged 7 commits into from
May 2, 2018
Merged
14 changes: 14 additions & 0 deletions core-data/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ export function receiveTerms( taxonomy, terms ) {
};
}

/**
* Returns an action object used in signalling that authors have been received.
*
* @param {Array|Object} authors Authors received.
*
* @return {Object} Action object.
*/
export function receiveAuthors( authors ) {
return {
type: 'RECEIVE_AUTHORS',
authors: castArray( authors ),
};
}

/**
* Returns an action object used in signalling that media have been received.
*
Expand Down
21 changes: 21 additions & 0 deletions core-data/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ export function terms( state = {}, action ) {
return state;
}

/**
* Reducer managing authors state. Keyed by id.
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
*
* @return {Object} Updated state.
*/
export function authors( state = [], action ) {
switch ( action.type ) {
case 'RECEIVE_AUTHORS':
return [
...state,
...action.authors,
];
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this would create duplicate authors if we trigger the request twice (it's not happening at the moment though :)). Maybe we can store them by Id :).

Also, why do we have an authors reducer instead of a users reducer. I believe we should have a users reducer and use the getAuthors selector to filter the data according to the user's object. This way we won't be duplicating data when we introduce the users fetching.

This looks more complex at first sight, but it's the way to go. Just imagine how you'd update the authors list if you create a new author from the JS side for instance? If it's all handled in a generic way in a users reducer, the selector will update its changes without any specific behavior to trigger the request again.

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe this would create duplicate authors if we trigger the request twice (it's not happening at the moment though :)). Maybe we can store them by Id :).

Fixed in 97a7a35

Also, why do we have an authors reducer instead of a users reducer. I believe we should have a users reducer and use the getAuthors selector to filter the data according to the user's object.

Because of #6361. With this approach, we keep the definition of "which user is an author" server-side.

Copy link
Contributor

Choose a reason for hiding this comment

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

Because of #6361. With this approach, we keep the definition of "which user is an author" server-side.

Well! Can I suggest that it's not a good compromise going forward. This means that our data module would consider authors as a separate entity than users. While this may be ok shortterm, I don't think it's good because we have to think of the core-data module as a generic module to handle client data in any WP Admin context. It means another page could create users, which could also be "authors" but since this knowledge is only available server-side, the UI won't update once those users are created.

How can we move this knowledge client-side? what property do we need in the user's object to say that it's an author or not?

Copy link
Member Author

Choose a reason for hiding this comment

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

While this may be ok shortterm, I don't think it's good because we have to think of the core-data module as a generic module to handle client data in any WP Admin context.

Would you like me to move it to editor data then?

How can we move this knowledge client-side? what property do we need in the user's object to say that it's an author or not?

We'd need to run a second query client-side for every user collection request. First, a WP_User_Query( [ 'who' => 'authors' ] ) to get all authors, and the second WP_User_Query for the full query. When preparing the results for return, we'd inspect whether the user is a part of the first query and include an author attribute correspondingly.

My preference is to keep this current implementation.

Copy link
Member Author

Choose a reason for hiding this comment

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

In fact, this current implementation can be progressively enhanced to a better implementation in the future, should one arise.

Copy link
Contributor

Choose a reason for hiding this comment

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

So we discussed this with @aduth and I believe there's a compromise to find here if we can't move the "author" knowledge to the client.

we can track the ?who=authors response as a set of IDs pointing to the canonical users state.

Also this relates to #6395 helper since ?who=authors can be considered just one query on the user model.

I'm fine using a compromise for now and generalizing this as a query in #6395 or similar.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you clarify what changes you'd like me to make to this PR? Or, open a PR against this PR with your suggested changes?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, give me some minutes

Copy link
Contributor

@youknowriad youknowriad May 1, 2018

Choose a reason for hiding this comment

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

Here #6522 It's not tested etc... just to get the idea.

}

return state;
}

/**
* Reducer managing media state. Keyed by id.
*
Expand Down Expand Up @@ -104,6 +124,7 @@ export function themeSupports( state = {}, action ) {

export default combineReducers( {
terms,
authors,
media,
postTypes,
themeSupports,
Expand Down
9 changes: 9 additions & 0 deletions core-data/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import apiRequest from '@wordpress/api-request';
import {
setRequested,
receiveTerms,
receiveAuthors,
receiveMedia,
receivePostTypes,
receiveThemeSupportsFromIndex,
Expand All @@ -24,6 +25,14 @@ export async function* getCategories() {
yield receiveTerms( 'categories', categories );
}

/**
* Requests authors from the REST API.
*/
export async function* getAuthors() {
const authors = await apiRequest( { path: '/wp/v2/users/?who=authors' } );
yield receiveAuthors( authors );
}

/**
* Requests a media element from the REST API.
*
Expand Down
11 changes: 11 additions & 0 deletions core-data/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ export function getMedia( state, id ) {
return state.media[ id ];
}

/**
* Returns all available authors.
*
* @param {Object} state Data state.
*
* @return {Array} Authors list.
*/
export function getAuthors( state ) {
return state.authors;
}

/**
* Returns the Post Type object by slug.
*
Expand Down
10 changes: 5 additions & 5 deletions editor/components/post-author/check.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { filter, get } from 'lodash';
import { get } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -15,8 +15,7 @@ import { withSelect } from '@wordpress/data';
*/
import PostTypeSupportCheck from '../post-type-support-check';

export function PostAuthorCheck( { user, users, children } ) {
const authors = filter( users.data, ( { capabilities } ) => get( capabilities, [ 'level_1' ], false ) );
export function PostAuthorCheck( { user, authors, children } ) {
const userCanPublishPosts = get( user.data, [ 'post_type_capabilities', 'publish_posts' ], false );

if ( ! userCanPublishPosts || authors.length < 2 ) {
Expand All @@ -30,13 +29,14 @@ export default compose( [
withSelect( ( select ) => {
return {
postType: select( 'core/editor' ).getCurrentPostType(),
authors: select( 'core' ).getAuthors(),
};
} ),
withAPIData( ( props ) => {
const { postType } = props;
const { postType, authors } = props;
Copy link
Contributor

Choose a reason for hiding this comment

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

Passing the props down is useless, it's done automatically in withAPIData

Copy link
Member Author

Choose a reason for hiding this comment

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


return {
users: '/wp/v2/users?context=edit&per_page=100',
authors,
user: `/wp/v2/users/me?post_type=${ postType }&context=edit`,
};
} ),
Expand Down
28 changes: 3 additions & 25 deletions editor/components/post-author/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
/**
* External dependencies
*/
import { get, filter } from 'lodash';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { withAPIData, withInstanceId } from '@wordpress/components';
import { withInstanceId } from '@wordpress/components';
import { Component, compose } from '@wordpress/element';
import { withSelect, withDispatch } from '@wordpress/data';

Expand All @@ -29,21 +24,8 @@ export class PostAuthor extends Component {
onUpdateAuthor( Number( value ) );
}

getAuthors() {
// While User Levels are officially deprecated, the behavior of the
// existing users dropdown on `who=authors` tests `user_level != 0`
//
// See: https://github.com/WordPress/WordPress/blob/a193916/wp-includes/class-wp-user-query.php#L322-L327
// See: https://codex.wordpress.org/Roles_and_Capabilities#User_Levels
const { users } = this.props;
return filter( users.data, ( user ) => {
return get( user, [ 'capabilities', 'level_1' ], false );
} );
}

render() {
const { postAuthor, instanceId } = this.props;
const authors = this.getAuthors();
const { postAuthor, instanceId, authors } = this.props;
const selectId = 'post-author-selector-' + instanceId;

// Disable reason: A select with an onchange throws a warning
Expand Down Expand Up @@ -72,17 +54,13 @@ export default compose( [
withSelect( ( select ) => {
return {
postAuthor: select( 'core/editor' ).getEditedPostAttribute( 'author' ),
authors: select( 'core' ).getAuthors(),
};
} ),
withDispatch( ( dispatch ) => ( {
onUpdateAuthor( author ) {
dispatch( 'core/editor' ).editPost( { author } );
},
} ) ),
withAPIData( () => {
return {
users: '/wp/v2/users?context=edit&per_page=100',
};
} ),
withInstanceId,
] )( PostAuthor );
28 changes: 3 additions & 25 deletions editor/components/post-author/test/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,36 +43,14 @@ describe( 'PostAuthorCheck', () => {
},
};

it( 'should not render anything if the user doesn\'t have the right capabilities', () => {
let wrapper = shallow( <PostAuthorCheck users={ users } user={ {} }>authors</PostAuthorCheck> );
expect( wrapper.type() ).toBe( null );
wrapper = shallow(
<PostAuthorCheck users={ users } user={
{ data: { post_type_capabilities: { publish_posts: false } } }
}>
authors
</PostAuthorCheck>
);
expect( wrapper.type() ).toBe( null );
} );

it( 'should not render anything if users unknown', () => {
const wrapper = shallow( <PostAuthorCheck users={ {} } user={ user }>authors</PostAuthorCheck> );
const wrapper = shallow( <PostAuthorCheck authors={ [] } user={ user }>authors</PostAuthorCheck> );
expect( wrapper.type() ).toBe( null );
} );

it( 'should not render anything if single user', () => {
const wrapper = shallow(
<PostAuthorCheck users={ { data: users.data.slice( 0, 1 ) } } user={ user }>
authors
</PostAuthorCheck>
);
expect( wrapper.type() ).toBe( null );
} );

it( 'should not render anything if single filtered user', () => {
const wrapper = shallow(
<PostAuthorCheck users={ { data: users.data.slice( 0, 2 ) } } user={ user }>
<PostAuthorCheck authors={ users.data.slice( 0, 1 ) } user={ user }>
authors
</PostAuthorCheck>
);
Expand All @@ -81,7 +59,7 @@ describe( 'PostAuthorCheck', () => {

it( 'should render control', () => {
const wrapper = shallow(
<PostAuthorCheck users={ users } user={ user }>
<PostAuthorCheck authors={ users } user={ user }>
authors
</PostAuthorCheck>
);
Expand Down
62 changes: 21 additions & 41 deletions editor/components/post-author/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,29 @@ import { shallow } from 'enzyme';
import { PostAuthor } from '../';

describe( 'PostAuthor', () => {
const users = {
data: [
{
id: 1,
name: 'admin',
capabilities: {
level_1: true,
},
const authors = [
{
id: 1,
name: 'admin',
capabilities: {
level_1: true,
},
{
id: 2,
name: 'subscriber',
capabilities: {
level_0: true,
},
},
{
id: 2,
name: 'subscriber',
capabilities: {
level_0: true,
},
{
id: 3,
name: 'andrew',
capabilities: {
level_1: true,
},
},
{
id: 3,
name: 'andrew',
capabilities: {
level_1: true,
},
],
};
},
];

const user = {
data: {
Expand All @@ -43,30 +41,12 @@ describe( 'PostAuthor', () => {
},
};

describe( '#getAuthors()', () => {
it( 'returns empty array on unknown users', () => {
const wrapper = shallow( <PostAuthor users={ {} } user={ user } /> );

const authors = wrapper.instance().getAuthors();

expect( authors ).toEqual( [] );
} );

it( 'filters users to authors', () => {
const wrapper = shallow( <PostAuthor users={ users } user={ user } /> );

const authors = wrapper.instance().getAuthors();

expect( authors.map( ( author ) => author.id ).sort() ).toEqual( [ 1, 3 ] );
} );
} );

describe( '#render()', () => {
it( 'should update author', () => {
const onUpdateAuthor = jest.fn();
const wrapper = shallow(
<PostAuthor
users={ users }
authors={ authors }
user={ user }
onUpdateAuthor={ onUpdateAuthor } />
);
Expand Down
66 changes: 66 additions & 0 deletions lib/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,69 @@ function gutenberg_ensure_wp_json_has_theme_supports( $response ) {
return $response;
}
add_filter( 'rest_index', 'gutenberg_ensure_wp_json_has_theme_supports' );

/**
* Handle any necessary checks early.
*
* @param WP_HTTP_Response $response Result to send to the client. Usually a WP_REST_Response.
* @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server).
* @param WP_REST_Request $request Request used to generate the response.
*/
function gutenberg_handle_early_callback_checks( $response, $handler, $request ) {
if ( '/wp/v2/users' === $request->get_route() ) {
if ( ! empty( $request['who'] ) && 'authors' === $request['who'] ) {
$can_view = false;
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
foreach ( $types as $type ) {
if ( current_user_can( $type->cap->edit_posts ) ) {
$can_view = true;
}
}
if ( ! $can_view ) {
return new WP_Error( 'rest_forbidden_who', __( 'Sorry, you are not allowed to query users by this parameter.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
}
}
}
return $response;
}
add_filter( 'rest_request_before_callbacks', 'gutenberg_handle_early_callback_checks', 10, 3 );

/**
* Include additional query parameters on the user query endpoint.
*
* @see https://core.trac.wordpress.org/ticket/42202
*
* @param array $query_params JSON Schema-formatted collection parameters.
* @return array
*/
function gutenberg_filter_user_collection_parameters( $query_params ) {
$query_params['who'] = array(
'description' => __( 'Limit result set to users who are considered authors.', 'gutenberg' ),
'type' => 'string',
'enum' => array(
'authors',
),
);
return $query_params;
}
add_filter( 'rest_user_collection_params', 'gutenberg_filter_user_collection_parameters' );

/**
* Filter user collection query parameters to include specific behavior.
*
* @see https://core.trac.wordpress.org/ticket/42202
*
* @param array $prepared_args Array of arguments for WP_User_Query.
* @param WP_REST_Request $request The current request.
* @return array
*/
function gutenberg_filter_user_query_arguments( $prepared_args, $request ) {
if ( ! empty( $request['who'] ) && 'authors' === $request['who'] ) {
$prepared_args['who'] = 'authors';
if ( isset( $prepared_args['has_published_posts'] ) ) {
unset( $prepared_args['has_published_posts'] );
}
}
return $prepared_args;
}
add_filter( 'rest_user_query', 'gutenberg_filter_user_query_arguments', 10, 2 );
Loading