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 QueryMedia component connected to a Redux store. #10392

Merged
merged 18 commits into from
Feb 1, 2017

Conversation

ellatrix
Copy link
Contributor

@ellatrix ellatrix commented Jan 3, 2017

N.b.: This includes #10386. Merged!

  • Add QueryMedia component.
  • Add Redux state using MediaQueryManager.
  • Merge existing (simple) media store.

Split from #10352.
This is a task from #5046.

@ellatrix ellatrix added Components [Feature] Media The media screen in Calypso, general media management, or integration with third party media. State [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. [Type] Task labels Jan 3, 2017
@ellatrix
Copy link
Contributor Author

ellatrix commented Jan 3, 2017

I would need some feedback on "Merge existing (simple) media store", because I'm not sure if this should be kept or not. I see that the posts store has an items store as well, even though there is a query store. The items store was added in #10016. Cc @aduth.

@aduth
Copy link
Contributor

aduth commented Jan 4, 2017

In the case of posts, since we'd opted to enable keying and lookup by global ID, items exists as simply a mapping of global IDs to [ siteId, postId ] tuples. Since a media item does not have a global ID, we should be able to get away with only an items state subtree, though in order to perform rich query management like in posts, we may need to implement it as an instance of MediaQueryManager. See posts reducer as an example, though it's complex enough that I wonder if we can abstract any of it.

(Caveat to my comments being this is made in passing, I've not yet taken a close look at the changes here)

* @return {XMLHttpRequest} XMLHttpRequest
*/
export function requestMedia( { dispatch }, { siteId, query } ) {
dispatch( requestingMedia( siteId, query ) );
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have on thoughts on whether the isRequestingMedia check currently in the query component would make more sense in the API middleware handler?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that sounds like a good idea.

export function requestMedia( { dispatch }, { siteId, query } ) {
dispatch( requestingMedia( siteId, query ) );

return wpcom.site( siteId ).mediaList( query, function( error, data ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

At the very least I might suggest the more concise ES6 arrow function syntax for the anonymous function, though further I wonder if we should treat the return value as a promise. This would be difficult to test as-is, for example, since nock stubbing doesn't complete predictably in the current or next tick.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, planning to do this. :)

@@ -36,6 +40,52 @@ export const items = createReducer( {}, {
}
} );

export const queries = ( () => {
function applyToManager( state, siteId, method, createDefault, ...args ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I should have glanced at the changes before writing my initial comment 😆 But in this case I think we don't need both items and queries. A QueryManager instance allows us to retrieve a single item, all items, or items by queries, which should satisfy all of our selector requirements.

} );
} )();

export const queryRequests = keyedReducer( 'siteId', createReducer( [], {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's really not obvious, and I encountered it myself the hard way, but composing a keyedReducer createReducer will persist its state even though one would expect createReducer to only persist if a schema is provided.

Related: #10016 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, thanks for the tip. I was trying to figure out why this happened. :)

const queryRequests = state.media.queryRequests[ siteId ];
const stringifiedQuery = MediaQueryManager.QueryKey.stringify( query );

if ( ! queryRequests ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we've not yet used stringifiedQuery at this point where an early return could occur, and QueryKey.stringify is not exactly trivial, it might be better to declare after the condition.

return false;
}

return queryRequests.indexOf( stringifiedQuery ) !== -1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Personal preference, but I find _.includes to be a more human-readable equivalent, which also happens to automatically account for a falsey queryRequests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, according to the documentation, it does not accept a boolean collection argument.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, according to the documentation, it does not accept a boolean collection argument.

Sorry, I should have been more clear. Since the issue is we're trying to avoid calling indexOf when queryRequests is potentially null, I meant to point out that _.includes will automatically account for this and return false.

FWIW, though not documented, I did test and confirm that it behaves as expected if passed a boolean:

n_ > _.includes( false, 'foo' );
false

}

QueryMedia.propTypes = {
siteId: PropTypes.number,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should siteId be required? What happens if we pass a falsey value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I was thinking about this when adjusting the documentation, but then left it the same as PostsQuery. Should probably be updated there too.

* @return {Object} Action object
*/
export function receiveMedia( siteId, media ) {
export function receiveMedia( siteId, media, found, query ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Failing test on this action creator, likely not expecting the additional properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know, wanted to do this while merging the the reducers and tests. Probably the worst idea to let tests fail after a commit. 😶

return {
type: MEDIA_RECEIVE,
siteId,
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a bit arbitrary and conventions vary, but I tend to find it more "pleasing" to read when the shorthand properties are grouped together separate from properties with values defined (either top or bottom of object). What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was torn between that and having the same order as the arguments (and other action creators).

Copy link
Member

Choose a reason for hiding this comment

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

my arbitrary preference is for alphabetical arrangement 😛

@ellatrix ellatrix force-pushed the add/query-media-component branch from 082c2b3 to 7dff526 Compare January 4, 2017 10:14

log( 'Request media for site %d using query %o', siteId, query );

return wpcom.site( siteId ).mediaList( query, function( error, data ) {
Copy link
Contributor Author

@ellatrix ellatrix Jan 4, 2017

Choose a reason for hiding this comment

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

To do: return a promise and dispatch failures.


<table>
<tr><th>Type</th><td>Number</td></tr>
<tr><th>Required</th><td>No</td></tr>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To do: this is required now.

@ellatrix
Copy link
Contributor Author

ellatrix commented Jan 6, 2017

I see that there is a schema in state/posts, so it should probably be added here too. It looks like it would belong better with the query manager though, as that's creating the data structure.

@ellatrix ellatrix requested review from aduth and dmsnell January 6, 2017 09:04
@ellatrix ellatrix mentioned this pull request Jan 6, 2017
4 tasks
@ellatrix ellatrix force-pushed the add/query-media-component branch from acc3217 to baab6a1 Compare January 6, 2017 13:51
@ellatrix ellatrix requested a review from gwwar January 6, 2017 15:47
Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

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

Thanks for using the newer API middleware @iseulde! This is all looking very nice. Please let me know if I can be a help to you on any of my feedback.

QueryMedia.defaultProps = {
request: () => {}
};

Copy link
Member

Choose a reason for hiding this comment

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

as a note on style, we have been putting these inside the React classes as static properties

class extends Component {
	static propTypes = {
		blarg: 'woohoo',
	};

	static defaultProps = {
		translate: noop,
	};
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, will changed it. I wasn't sure what's preferred, and I only saw this style so far in the codebase.

return bindActionCreators( {
request: requestMedia
}, dispatch );
}
Copy link
Member

Choose a reason for hiding this comment

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

fun fact! if we have a simple list of functions to bind to action names then we can simply provide an object whose keys are the action name and whose values are the actual functions to call

const mapDispatchToProps = ( {
	request: requestMedia,
} );

export default connect( null, mapDispatchToProps )( QueryMedia );

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the tip. I also put mapDispatchToProps at the top of the file as it makes it easier to understand where the props of the component are coming from. I can move this back to the bottom if that's preferred.

*
* @param {Object} store Redux store
* @param {Object} action Action object
* @return {Promise} Promise
Copy link
Member

Choose a reason for hiding this comment

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

While I appreciate the comment Promise is somewhat redundant 😉 Maybe something a bit more descriptive could help here, or we could just leave it off.

/**
 * @return {Promise} wpcom.js request response
 */

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Leaving it off will give an eslint warning, which is why I added it here. Similarly "Action object" for {Object} action is somewhat redundant too. :)

Copy link
Member

Choose a reason for hiding this comment

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

this is one thing that bothers me about linters - they tend to push us towards caring about the wrong things. the linter says, "nothing is there" so we put something there, but the intention of the linter was to motivate us to provide a helpful description.

my personal habits tend to be a little more aggressive against linters in this regard, where I might ignore its noise or try and fix the linting strategy.

in this case, even adding a single word to it, like "Action object" helps a lot, even more so potentially something like, "Redux action"

dispatch( receiveMedia( siteId, media, found, query ) );
} ).catch( () => {
dispatch( failMediaRequest( siteId, query ) );
} );
Copy link
Member

Choose a reason for hiding this comment

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

Although it's a personal style request and I don't think we have this in our guidelines, would you be interested in breaking this into multiple lines? at a minimum so that .then and .catch start on their own lines? (You can feel free to say "no" to this)

return wpcom
	.site( siteId )
	.mediaList( query )
	.then( ( { media, found } ) => dispatch( receiveMedia( siteId, media, found, query ) ) )
	.catch( () => dispatch( failMediaRequest( siteId, query ) ) );

MEDIA_RECEIVE,
MEDIA_REQUEST,
MEDIA_REQUEST_FAILURE,
MEDIA_REQUESTING } from 'state/action-types';
Copy link
Member

Choose a reason for hiding this comment

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

how about a trailing comma and a newline before the }?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I personally like trailing command as well. For the same reasons I like separate var statements instead of comma separating them.

var something
var somethingElse

But I'll follow whatever is preferred.

Copy link
Contributor Author

@ellatrix ellatrix Jan 7, 2017

Choose a reason for hiding this comment

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

Similarly I would prefer this:

<MediaLibrary
    prop={ prop }
    prop={ prop }
/>

Even if it looks ugly. :) But haven't seen that so far.

return {
type: MEDIA_RECEIVE,
siteId,
Copy link
Member

Choose a reason for hiding this comment

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

my arbitrary preference is for alphabetical arrangement 😛

return applyToManager( state, siteId, 'removeItems', true, mediaIds );
}
} );
} )();
Copy link
Member

Choose a reason for hiding this comment

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

This IIFE is a bit curious to me here. Can you clarify what it's being used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Again, this is from state/posts.

Copy link
Contributor

Choose a reason for hiding this comment

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

This IIFE is a bit curious to me here. Can you clarify what it's being used for?

A questionable technique of scoping and colocating related pieces.

...state,
[ siteId ]: nextManager
};
}
Copy link
Member

Choose a reason for hiding this comment

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

@aduth would you agree that this might be a good place to look into using the keyedReducer()?

export const keyedReducer = ( keyName, reducer ) => {

I'm a bit distracted by some of the code, but @iseulde the idea here is that we can identify shape of the state for an item in a collection and then compose that item reducer with this utility to keep things well-separated and keep the shape of the underlying data transparent (vs. hidden behind initialState = {})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I copied applyToManager from state/posts :) Not sure what's best here. Looks like it could be abstracted. See #10392 (comment).

Copy link
Member

Choose a reason for hiding this comment

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

no worries @iseulde - I'm not as familiar with the code history.

please ignore this then. no need to conflate behavioral changes with code moves.

return null;
}

return queries.getItems( query );
Copy link
Member

Choose a reason for hiding this comment

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

What is this query object that we are storing? It appears like it contains a function, but if indeed we are storing functions inside of the Redux state tree we should be very skeptical about it. The state tree is best and ideal when only primitive data and compositions thereof are inside of it. Data is translatable, but functions aren't.

Copy link
Contributor Author

@ellatrix ellatrix Jan 6, 2017

Choose a reason for hiding this comment

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

Isn't this what the query managers are for?

Copy link
Member

Choose a reason for hiding this comment

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

Isn't this what the query managers are for?

What is "this"

What is "are for?"

It may be that the move into Redux demands some changes to the way we think about these things. In fact, if the query manager is working on its own data, a trivial change might be writing a fully-independent getItems()-type function which accepts the actual object being operated on as an input argument.

Keeping functions out of the state is pretty high up on the list of priorities for state design.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is "this"
What is "are for?"

Sorry, let me put it differently: Are the query managers not designed to manage Redux state? Sorry if they aren't, I saw it being used for posts and adopted it here. I'll think about how to do it differently then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there any broader discussion (ticket) for removing the use of query managers?

Copy link
Contributor

Choose a reason for hiding this comment

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

When it comes down to it though, when we store Immutable objects in state, we're storing an instance of its class, so I don't see much of a difference between something like state.values() (of an instance of Immutable.Map) vs. state.getItems() (of an instance of MediaQueryManager). You do raise a good point that Immutable objects can be converted Immutable <-> JS <-> String whereas QueryManager instances can only be converted QueryManager <-> String, though in practice we don't (and probably shouldn't) make use of the JavaScript form of an Immutable instance, except perhaps when initializing from an object.

Would it be much of a difficult task to turn stateObject.getItems( query ) into getItems( stateObject, query )?

I'm sure it could be done, but maintaining a valid state object shape and passing the required arguments for each argument is not nearly as easy to use as it is in its class form:

const nextManager = receive( {
	items: {},
	queries: {}
}, {
	itemKey: 'ID'
}, posts, query );

// vs.

const nextManager = new PostQueryManager().receive( posts, query );

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, in case it lends anything to the argument, operations on the QueryManager class are immutable, i.e. one can rely upon strict equality between two instances to determine whether the contents are in-fact equal.

https://github.com/Automattic/wp-calypso/blob/31ff257/client/lib/query-manager/test/index.js#L199-L283

Copy link
Member

Choose a reason for hiding this comment

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

okay @aduth. I sense that there is far more behind the QueryManager than I understand, though I don't think that understanding would change my opinion on whether or not it should be here in state. it's not actually state. it's something that operates on state and isn't directly linked to representing the data to the user.

that being said, I can let this slide in my mind as an exception since it's all existing code. it's an exception to an exception though because we're adding it fresh into state right now instead of refactoring existing stuff in state.

it's not altogether different than when using Immutable.js and that's the source of my love/hate relationship with it. it's not actually JavaScript data and it's awkward no matter how you slice it when interoperating with other normal JavaScript objects. they have chosen performance over native representation and I get that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will think about this a bit over the weekend and see if I can come up with an alternative. I guess the query managers have too much logic that should go to reducers and selectors.

Copy link
Contributor

@gwwar gwwar Jan 11, 2017

Choose a reason for hiding this comment

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

Are we blocked here by whether or not we should use a query manager for media items?

For more history I believe the original PR was #5135

See also #5289

Copy link
Contributor

@youknowriad youknowriad left a comment

Choose a reason for hiding this comment

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

Curious about the status of this PR. Is it blocked? How can we move this forward?

I'm asking because I've found myself needing a media redux subtree in my current WIP and I didn't want to duplicate the efforts.

}
}

export default connect( null, mapDispatchToProps )( QueryMedia );
Copy link
Contributor

Choose a reason for hiding this comment

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

could we provide a way to query medias by Id, the same way we do in QueryPosts by providing a postId prop ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Granted this could be done in a separate PR.

* @return {Promise} Promise
*/
export function requestMedia( { dispatch, getState }, { siteId, query } ) {
if ( ! isRequestingMedia( getState(), siteId, query ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Personal preference, but I would inverse the condition if ( isRequesting ) { return; }

@youknowriad youknowriad force-pushed the add/query-media-component branch from efa3d6b to e27ade2 Compare January 30, 2017 08:46
Copy link
Contributor

@youknowriad youknowriad left a comment

Choose a reason for hiding this comment

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

I rebase this, and it should be ok to merge now 👍

@youknowriad youknowriad added [Status] Ready to Merge and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Jan 30, 2017
@youknowriad youknowriad force-pushed the add/query-media-component branch from d370dd1 to ac59215 Compare January 31, 2017 14:05
* @param {Object} query Query object
* @return {Object} Action object
*/
export function requestingMedia( siteId, query ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are there separate action creators requestMedia and requestingMedia? I don't see any meaningful difference in how they're used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually there is a difference: requestMedia is responsible of triggering the middleware handler. While requestingMedia tells that we are effectively starting the Network Request. Because the middleware could decide to avoid triggering a request if we're already requesting the same request.

I guess we'll see more of those while moving to the data middleware

@aduth
Copy link
Contributor

aduth commented Jan 31, 2017

Aside from note above, this appears to look and work well to me. 👍

@youknowriad
Copy link
Contributor

I'm merging this and I'll follow up with a QueryMedia by ID PR

@youknowriad youknowriad merged commit db6188c into master Feb 1, 2017
@youknowriad youknowriad deleted the add/query-media-component branch February 1, 2017 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Components [Feature] Media The media screen in Calypso, general media management, or integration with third party media. OSS Citizen State [Type] Task
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants