-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Revisions #2: Revisions state update and diff-ing #14241
Conversation
bcda7f6
to
defb645
Compare
forEach( normalizedRevisions, ( revision, index ) => { | ||
revision.changes = index === normalizedRevisions.length - 1 | ||
? { added: 0, removed: 0 } | ||
: countDiffWords( diffWords( normalizedRevisions[ index + 1 ].content, revision.content ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious what's the reasoning behind computing the diff when fetching the revisions? Naively, I'd have let this for a memoized selector, for example, assuming this can be heavy and compute the diff only when needed.
Maybe not needed but what if we want to diff an old revision with the last one for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went this way precisely because it's quite expansive [0]: persisting the summary results along the revisions speeds up the first draw of the revisions list after a reload compared to the equivalent memoized-selector-based solution. That said, it's also partially un-normalizing the revision data in Redux as you get redundant data (the summary can be recomputed from the contents).
Maybe not needed but what if we want to diff an old revision with the last one for example.
The ability to diff between any 2 revisions is dropped for now (internal reference: p4TIVU-6pW-p2#comment-7039). That said, for those features outside the "main" one, it's quite easy to recompute the diff between two revisions with the utils functions introduced here.
As an interesting footnote here: we can most likely go even further and only update the Redux state if we receive deleted or new revisions if we make the assumption that revisions are immutable, AFAICS, there is no way of editing a revision from the API or in wp-admin.
[0] Myers algorithm (used by jsdiff) has a pretty interesting time complexity for revisions like object: it's in O(ND) with N the sum of the number of words in revisions A and B, and D the size of the shortest edit sequence A -> B. If we suppose that most revisions are incremental updates on a post, D should stay small :)
dispatch( receivePostRevisionsSuccess( siteId, postId ) ); | ||
dispatch( receivePostRevisions( siteId, postId, map( revisions, normalizeRevision ) ) ); | ||
dispatch( receivePostRevisions( siteId, postId, normalizedRevisions ) ); | ||
dispatch( requestUsers( siteId, uniq( map( normalizedRevisions, 'author' ) ) ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want to query the users as soon as we receive the revisions? My first thought is that we should probably use a query component for this when needed. Is there a particular reason for this to be fetched here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial reason for it to be in there was to bundle revisions and users by dispatching receivePostRevisions
, receivePostRevisionsSuccess
and receiveUsers
in the same tick. But I dropped it, from a UX pov, having the author names is nice but not as important as accessing the revisions asap: it's better to have a first render of the revisions while we request more information about the various authors imho.
I've updated this PR to remove this and will probably bundle QueryUsers
with its "usage" in the last PR for revisions.
@@ -48,6 +48,7 @@ | |||
"creditcards": "2.1.2", | |||
"cross-env": "4.0.0", | |||
"debug": "2.2.0", | |||
"diff": "1.4.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we update the lock https://wpcalypso.wordpress.com/devdocs/docs/shrinkwrap.md?term=shrinkw ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind, I just saw your note above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is not depending on #13367 anymore, this can be merged on its own, I've updated the shrinkmap :)
… update `date` and `modified` to contain timezone information in their values
defb645
to
1cf76fe
Compare
1cf76fe
to
a9cc31b
Compare
Updated
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
It'd be good to have a second opinion.
client/lib/text-utils/index.js
Outdated
/** | ||
* External Dependencies | ||
*/ | ||
import JsDiff from 'diff'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we import { diffWords } from 'diff';
here instead of accessing property below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we definitely can :) fixed
client/lib/text-utils/index.js
Outdated
@@ -24,4 +30,26 @@ function countWords( content ) { | |||
return 0; | |||
} | |||
|
|||
export default { countWords }; | |||
const diffWords = JsDiff.diffWords; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: Destructuring could help avoid some repetition: const { diffWords } = JsDiff;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed it completely as we're directly importing (and re-exporting in the "default" object diffWords
)
client/lib/text-utils/index.js
Outdated
diffChanges, | ||
( accumulator, change ) => { | ||
const count = countWords( change.value ); | ||
change.added && ( accumulator.added += count ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: I found myself staring at this function for minutes convinced we'd be inaccurately incrementing both added
and removed
for each change, until I noticed that the first half of each line is treated effectively as a condition. This is one case where I'd advocate for the less magical if ( change.added ) {
variation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've no strong feelings about either so I updated it to use ifs
); | ||
} | ||
|
||
export default { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor and not related to these changes specifically: Exporting only a default of an object makes it difficult to tree shake (dead code elimination) unused functions within a file. We're not yet on Webpack 2 (#10799, #11204) so this isn't entirely relevant yet anyways, but in anticipation it's typically better to export/import functions individually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL :)
I don't really want to touch the imports of countWords
if at all possible in this PR ;) Keeping it on my todo once this PR is merged though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created a new PR to make sure we don't drop this: #14794
it's based on this one so we should merge this one first.
...flow( | ||
pick( [ 'date_gmt', 'modified_gmt' ] ), | ||
mapValues( val => `${ val }Z` ), | ||
mapKeys( key => key.slice( 0, -'_gmt'.length ) ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: Readability: I think it's nice that this isn't a magic number, but at the same time the -
is a bit foreign at first glance. Wondering if either -1 * '_gmt'.length
or -4 /* "_gmt" */
would be more readable. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we want to truly explain why the -1: -4 /* truncate "_gmt" */
? I don't see any way to give the "truncate" meaning to the -1
in code?
@@ -1,21 +1,74 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All new selectors should ideally be added to the client/state/selectors
directory. selectors.js
is a legacy pattern.
https://github.com/Automattic/wp-calypso/blob/master/docs/our-approach-to-data.md#selectors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
return { | ||
...revision, | ||
...{ author } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified as just author
.
return {
...revision,
author
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
...flow( | ||
pick( [ 'title', 'content', 'excerpt' ] ), | ||
mapValues( ( val = {} ) => val.rendered ) | ||
)( revision ), | ||
...flow( | ||
pick( [ 'date_gmt', 'modified_gmt' ] ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm bewildered by how this is working, specifically the flow/currying applied to pick
. In my own Node console it errors as I'd expect it to, because pick
isn't supposed to return a function but rather the object subset:
n_ > _.flow( _.pick( 'foo' ) )( { foo: 'bar' } )
TypeError: Expected a function
Can you explain what I'm missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first guess would be: _.pick
is the non-fp version? which means it returns an empty object {}
, and flow
doesn't like non-functions as an arg?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, I'm not answering the question in fact :)
Lodash FP follows a few "defaults" ideas, one of them is to re-order arguments in a "data-last" fashion: _.pick( { foo: 'bar' }, 'foo' ) === fpPick( 'foo' )( { foo: 'bar' } )
Once that's established, what you're missing is probably that the pick
you see in the code is the FP version not the "classical" one :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But pick
here is imported from lodash
, not lodash/fp
.
And to this point, I don't think we'd want to reintroduce lodash/fp
, as we'd made a point previously to try to remove it (#11354).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, it is imported from lodash/fp
. I totally missed that. Hmm!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking a bit more about it, I haven't found any usage of Lodash FP in calypso (no import, no usage inside a flow AFAICS), it might not be a good idea to start importing it (even more so if people aren't familiar with it since it can cause confusions like this one ;) and the FP docs aren't amazing :( )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well didn't refresh :) Will fix it to use the "classical" one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I actually quite like how this looks here. It's nice to compose the operations this way. But I'd agree we might not want to set this precedent for risk of confusing developers with the presence of both variants (i.e. being consistent on using one vs. the other).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good 👍
This is the second chunk coming from my WIP PR to introduce post revisions in calypso: #13367. It pushes the normalisation of revisions a bit further and adds support for diff-ing between revisions
changes
It builds upon what was introduced in #12733, adding:
date_gmt
) to a format with timezone information embedded in the datetime strings (simply adding the zone designator for the zero UTC offset:Z
). This means we can keep strings in Redux ( instead of moment.js objects) and still use components parsing our strings without any timezone issue.meta
when redispatching #14157 need to be merged first).We compute the diff between revisions:
getPostRevisionChanges
selector.testing
Besides unit tests, this is currently used nowhere. You can see how it can be used in the WIP PR #13367, for instance in the diff viewer.