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 revisions into redux #12733

Merged
merged 17 commits into from
Apr 24, 2017
Merged

Post revisions into redux #12733

merged 17 commits into from
Apr 24, 2017

Conversation

bperson
Copy link
Contributor

@bperson bperson commented Apr 3, 2017

First PR for my trial project described on the Team IO P2 blog: a UI for browsing a post's revisions.

While I continue to discuss the UI / UX for a post's history in the editor, this is a first step towards getting the revisions data for a post loaded in Redux. A placeholder page/route has been created and is available at: /post/revision/<siteId>/<postId>

  • QueryPostRevisions component
  • POST_REVISIONS_RECEIVE action and reducer handling
  • POST_REVISIONS_REQUEST_(SUCCESS|FAILURE) action and reducer handling
  • Proper state / display normalization of revisions coming from the REST 2.0 API
  • Tests actions / reducers
  • Add README.md for new components + JSDoc

Further down the line / might not be worth including in this specific PR:

  • Do not load all revisions for a post? (requires API update): https://core.trac.wordpress.org/ticket/40510
  • Support for revisions with various (siteId / postId) tuples in the same POST_REVISIONS_RECEIVE action, postponed / not needed for now

@matticbot matticbot added OSS Citizen [Size] XL Probably needs to be broken down into multiple smaller issues labels Apr 3, 2017
<QueryPostRevisions
siteId={ this.props.siteId }
postId={ this.props.postId } />
<h1>{ this.props.translate( 'Post History' ) }</h1>
Copy link

Choose a reason for hiding this comment

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

Hi! I've found a possible matching string that has already been translated 63 times:
translate( 'History' ) ES Score: 9

Help me improve these suggestions: react with 👎 if the suggestion doesn't make any sense, or with 👍 if it's a particularly good one (even if not implemented).

Copy link
Contributor

Choose a reason for hiding this comment

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

This string will need to include the post type as a translation placeholder.

const siteId = getSelectedSiteId( context.store.getState() );
const postId = getPostID( context );

renderRevision( context, siteId, postId );
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't these be revisions (current method) and renderRevisions (current line)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it should :) That said, I removed that code (for now) to keep the PR focused on redux changes. Will keep it in mind for the next PR, thx :)

@@ -14,6 +14,7 @@ module.exports = function() {
page( '/post', controller.pressThis, sitesController.siteSelection, sitesController.sites );
page( '/post/new', () => page.redirect( '/post' ) ); // redirect from beep-beep-boop
page( '/post/:site?/:post?', sitesController.siteSelection, sitesController.fetchJetpackSettings, controller.post );
page( '/post/revision/:site?/:post?', sitesController.siteSelection, sitesController.fetchJetpackSettings, controller.revision );
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer to see /post/:site/:post/revisions (note: revisions comes last, and there are no ? or optional parameters). I don't think we can ever reach this point before selecting a site and a post.

Also, it doesn't have to be done in this PR, but it's worth thinking about whether the approach we're using will handle revisions for post types other than post (page and custom post types).

Copy link
Contributor Author

@bperson bperson Apr 14, 2017

Choose a reason for hiding this comment

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

/post/:site/:post/revisions

will do :)

Also, it doesn't have to be done in this PR, but it's worth thinking about whether the approach we're using will handle revisions for post types other than post (page and custom post types).

It seems for pages, there will be some changes needed when querying as the URI changes to use /pages. But - as you said - considering this one PR is already XL, I will probably tackle that in a subsequent PR.

Copy link
Contributor Author

@bperson bperson Apr 14, 2017

Choose a reason for hiding this comment

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

will do :)

or maybe not ^^. It seems this breaks the expected "position" of the site ID in getSiteFragment: https://github.com/Automattic/wp-calypso/blob/0492a2f/client/lib/route/path.js#L23

So we should either stick to /post/revisions/:site/:post/ or maybe /revision/:site/:post. Last solution could be to attack the root cause and get getSiteFragment to use page.js's params first and then default to exploding the path. It seems most routes are using :site, I've found only 3 occurences of :siteId, there could be other weird usages out there so this feels like a risky move tbh.

Copy link
Contributor

Choose a reason for hiding this comment

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

From getSiteFragment code that you linked:

There are 2 URL positions where we should look for the site fragment: last (most sections) and second-to-last (post ID is last in editor)

Aren't we just adding a 3rd position here? So, the only changes to getSiteFragment needed to support /post/:site/:post/revisions should be as follows:

  • change i = 2 to i = 3
  • add pieces.length bounds checking, since otherwise we'll try to evaluate pieces[ -1 ]
  • update the comments and the tests accordingly

Last solution could be to attack the root cause and get getSiteFragment to use page.js's params first and then default to exploding the path ... there could be other weird usages out there so this feels like a risky move tbh.

yes, I also think this could easily break something, whether now or in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aren't we just adding a 3rd position here? So, the only changes to getSiteFragment needed to support /post/:site/:post/revisions should be as follows:

The more positions we accept, the harder it will be to maintain and the more chances we have of breaking something. After having read #9561, #9445 and #4614 it definitely feels like this is already quite fragile, hence why I'm kinda reluctant at touching it :)

I proposed the idea of using page.js params as that would free us from any positional guessing. The probability of having a slug-like (or number) at a position where we interpret it as a site identifier is considerably higher than naming a param :site or :siteId that wouldn't actually contain a site identifier. The (hopefully) net gain in maintainability makes the "risk" of touching it potentially worth it (but it's still a (very) risky move imho)

Copy link
Contributor

Choose a reason for hiding this comment

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

Things are less clear in the rest of Calypso because of (some) support for manipulating multiple sites at once, but in the post editor the URLs follow the convention of /verb/site/more-specific-things-here. Viewing revisions is a more specific action than viewing a particular post ID, so it should come after the post ID in the URL.

I think the only way adding an extra URL position in getSiteFragment would break anything is if we have a URL like /verb.with.dots/site.wordpress.com/numeric/numeric (something that would be newly and incorrectly recognized as a site ID in the length - 3 position). This seems extremely unlikely to me.

In the past when changing a function like getSiteFragment, I've successfully used a testing approach like this:

  • Implement both versions of the function side-by-side
  • Add debugging to print out return values from both versions of the code, especially if they differ
  • Navigate around the app, hitting as many sections as possible

Unfortunately for the issues you pointed to, this was missed in some of our longer and harder-to-reach routes (#9542 and #9550 for specific examples).

I agree that using page.js params would be a better design, but it's a much larger (and to me, much scarier) change - currently getSiteFragment is called using an actual parameterized URL rather than a page.js route, so this would require restructuring all of the code where it is used.

Finally I agree with @youknowriad's suggestion that we keep this PR to the Redux changes only. There are other things to discuss around URLs, like what the possible flows look like for getting to this feature (different post types, etc) and what that implies about the possible URL structures.

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 think the only way adding an extra URL position in getSiteFragment would break anything is if we have a URL like /verb.with.dots/site.wordpress.com/numeric/numeric (something that would be newly and incorrectly recognized as a site ID in the length - 3 position). This seems extremely unlikely to me.

Yes it's probably a fair assessment of the risk if we add the third-to-last position only in the slug detection (which introduces asymmetry between the slug and numeric ID detections). Adding it to the numeric ID detection might be a bit riskier. (but looking at the current page calls I couldn't see anything ;) )

Anyway, @youknowriad is right: this isn't needed for this PR ;)

@nylen
Copy link
Contributor

nylen commented Apr 13, 2017

Tests actions / reducers

I agree this needs tests.

Do not load all revisions for a post? (requires API update)

It's really unfortunate that this endpoint doesn't support pagination. I wasn't aware of that, and it's something we need to get fixed in WP core soon. I would suggest building the feature without this capability yet, but leaving the door open to add it later.

Then, once we have a patch for the API endpoint, we can run it on WP.com for a while to make sure it works well, and get it committed to WP core and released as part of the 4.7.x series.

One thing you could do in the meantime is create a ticket at https://core.trac.wordpress.org/ and provide some details, including links to the current/previous issues with running out of memory when displaying revisions.

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.

Great job here. I left some comments. I think the placeholder route is not necessary here, we could keep the PR only for adding redux stuff (minor).

Also, testing the selectors, reducers and action creators would be awesome.

this.request( this.props );
}

componentWillReceiveProps( nextProps ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use componentDidUpdate instead to trigger side effects? Here's a gist on what should we do where in React Lifecycle methods https://gist.github.com/bvaughn/923dffb2cd9504ee440791fade8db5f9

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TIL, great gist! PR updated :)

( state, siteId, postId ) => {
return map(
get( state.posts.revisions.revisions, [ siteId, postId ], [] ),
normalizePostForDisplay );
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: I guess it's personal preference, but I like to keep closing/opening parens on their own line
when we have long params like here.

return map(
  get( state.posts.revisions.revisions, [ siteId, postId ], [] ),
  normalizePostForDisplay
);

Same above with the if

if (
  this.props.siteId === nextProps.siteId &&
  this.props.postId === nextProps.postId 
) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

np, I wonder if it's worth maybe updating the if example in the Javascript Guidelines?

Currently it's showing:

if ( firstCondition() && secondCondition() &&
		thirdCondition() ) {
	doStuff();
}

@bperson
Copy link
Contributor Author

bperson commented Apr 21, 2017

Fixed comments + added tests for the reducers / selectors. I didn't see much value in creating tests for the action creators as they are currently "dumb" arrow functions with no logic in them.

As suggested by @nylen I also just created a ticket on trac to track the REST endpoint pagination enhancement request in WP Core: https://core.trac.wordpress.org/ticket/40510 . That said, while testing more edge cases / wordpress setups for revisions, I found out that Wordpress.com sites have a default limit of 25 revisions which means pagination wouldn't bring lots of value, but it could be useful for Jetpack sites?

...state[ action.siteId ],
[ action.postId ]: action.type === POST_REVISIONS_REQUEST,
},
};
Copy link
Contributor

Choose a reason for hiding this comment

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

we can probably use lodash merge here to avoid the multiple ...

revisions,
} );

function normalizeRevisionForState( revision ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should normalize the objects in the state or in the data middleware layer. I probably have a preference over the data layer (because we could imagine using different APIs with different shapes and reuse the same reducer)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, a couple of other places in the data-layer are already normalizing stuff "fromAPI": https://github.com/Automattic/wp-calypso/blob/master/client/state/data-layer/wpcom/read/tags/utils.js

Updated in the latest version + a couple of "dumb" tests of the revisions data-layer.

@@ -42,13 +38,11 @@ export function requesting( state = {}, action ) {
export function revisions( state = {}, action ) {
if ( action.type === POST_REVISIONS_RECEIVE ) {
const { siteId, postId } = action;
return {
...state,
return merge( {}, state, {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for these updates, this is looking really good. Just one last thing, I think merge is not suited in this exact place because revision are objects that would be merged recursively here and since the reducer is not really aware of the shape of these objects. So, it's possible to end up with a meged revision object (old and new revision of the same Id) and the intent here is to replace the object completely.

Sorry, If I confused you with my previous comment, I like to use merge in reducer of scalar values (like above) but not when the reducers objects with unknown shapes.

Copy link
Contributor Author

@bperson bperson Apr 21, 2017

Choose a reason for hiding this comment

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

nice catch!

Sorry, If I confused you with my previous comment, I like to use merge in reducer of scalar values (like above) but not when the reducers objects with unknown shapes.

hehe np, that's why code reviews are great :) that makes a lot of sense, will keep it in mind :)

I've added a "test" for it as well as reverting back to using spreads for that reducer.

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.

If you can just add a README for the query-post-revisions component.

This is looking really good. Great work 👍

The site ID for which we request post revisions.


### `siteId`
Copy link
Contributor

Choose a reason for hiding this comment

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

postId ? 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops :D

@youknowriad youknowriad changed the title WIP - Post revisions into redux Post revisions into redux Apr 24, 2017
@youknowriad youknowriad merged commit 63bfc14 into master Apr 24, 2017
@youknowriad youknowriad deleted the add/post-revisions branch April 24, 2017 16:37
@nylen
Copy link
Contributor

nylen commented Apr 27, 2017

I found out that Wordpress.com sites have a default limit of 25 revisions which means pagination wouldn't bring lots of value

What form does this limit take? I.e., when a post would go over 25 revisions, how is this handled?

@bperson
Copy link
Contributor Author

bperson commented Apr 27, 2017

What form does this limit take? I.e., when a post would go over 25 revisions, how is this handled?

it's dropping old revisions to make room for new ones (FIFO style) Not sure how it's done by the backend though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
OSS Citizen [Size] XL Probably needs to be broken down into multiple smaller issues
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants