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

Block comparison: show difference between invalid block conversion options #7995

Merged
merged 12 commits into from
Sep 13, 2018

Conversation

johngodley
Copy link
Contributor

@johngodley johngodley commented Jul 17, 2018

Description

When you have an invalid block the result of the available actions can be unknown.

edit_post_ wordpress_latest _wordpress

This PR adds a 'compare conversion' option that shows the actual HTML difference when 'convert to blocks' is applied.

The difference appears in a modal and the user can select to keep the current contents or apply the conversion, with full knowledge of the result of their action.

another_post___ wordpress_latest _wordpress

The modal adjusts to the height of the block:

edit_post_ wordpress_latest _wordpress

Overflowing, if too tall, and scrollable:

edit_post_ wordpress_latest _wordpress

And adjusts accordingly on smaller screens (using overflow scroll if too tall):

edit_post_ wordpress_latest _wordpress

Note: this is based on the discussion in #7604

How has this been tested?

  1. Manually check that the 'compare conversion' option displays correctly for different types of blocks and at different screen sizes
  2. Manually verify that the 'keep as HTML' converts the block to an HTML block
  3. Manually verify that the 'convert to blocks' applies a conversion identical to picking 'convert to blocks' from the invalid block modal

Types of changes

This adds a new feature.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.

@johngodley johngodley added [Feature] Blocks Overall functionality of blocks [Status] In Progress Tracking issues with work in progress [Feature] Block Transforms Block transforms from one block to another labels Jul 17, 2018
@johngodley johngodley force-pushed the add/invalid-block-diff branch 2 times, most recently from 5ffb94b to 27ed264 Compare July 24, 2018 08:54
@johngodley johngodley force-pushed the add/invalid-block-diff branch 2 times, most recently from 5c9e4b3 to 0ec836a Compare July 29, 2018 14:48
@johngodley johngodley force-pushed the add/invalid-block-diff branch 4 times, most recently from 06c5849 to f181dce Compare August 14, 2018 05:46
@johngodley johngodley force-pushed the add/invalid-block-diff branch 3 times, most recently from b7196b4 to 8079b91 Compare September 2, 2018 12:12
@johngodley
Copy link
Contributor Author

Noting that the comparison currently uses code from jsdiff to perform proper diffing. This is a BSD licensed open source library. It works well, but we don't need the entire library. I'm unsure about the best route for proceeding here, and I don't know if it's appropriate to include the library as-is, or the full package.

While Gutenberg does have it's own HTML parsing, and also HTML comparison (via isEquivalentHTML), it's doesn't find the longest common sequences, and would result in a poor diff that would need adapting to a proper diffing algorithm.

An alternative is to re-implement jsdiff ourselves.

Open to suggestions.

@johngodley
Copy link
Contributor Author

Also noting that the style is a best attempt at matching that proposed in #7604, but needs some improving. @jasmussen I'd very much appreciate any suggestions/help you can give.

In particular I assumed that we would want to show deleted characters in the right panel, and I've included these in red. We can not show those at all, if it makes more sense.

@jasmussen
Copy link
Contributor

Sorry I completely missed this. Feel free to assign me as a reviewer next time and I'll be sure to get to it. I'll try and take a look today, maybe push code.

@jasmussen jasmussen self-requested a review September 3, 2018 06:25
@jasmussen
Copy link
Contributor

@johngodley This is impressive work. I think it's super duper solid, and it's VERY much good enough to get into master so we can iterate.

I pushed a small fix so it only scrolls when necessary, and I made the menu option titlecase.

Can you rebase this?

Question: does this use the modal API? Assuming yes as it really seems to be, which is great, just verifying.

@johngodley johngodley changed the title WIP - Block comparison: show difference between invalid block conversion options Block comparison: show difference between invalid block conversion options Sep 5, 2018
@johngodley johngodley removed the [Status] In Progress Tracking issues with work in progress label Sep 5, 2018
@johngodley
Copy link
Contributor Author

does this use the modal API

If you mean <Modal /> then yep it does.

Rebased. There's a codecov failure which I don't understand as the unit test coverage has increased.

I've noticed some problems with a few of the more esoteric blocks so I'll look at fixing those.

@johngodley johngodley changed the title Block comparison: show difference between invalid block conversion options WIP: Block comparison: show difference between invalid block conversion options Sep 5, 2018
@jasmussen
Copy link
Contributor

If you mean then yep it does.

Yes, awesome!

I'm extending the review range and adding a milestone so we can get this in. Amazing work! This is a KILLER feature.

@jasmussen jasmussen requested a review from a team September 6, 2018 09:08
@jasmussen jasmussen added this to the 3.9 milestone Sep 6, 2018
Shows two blocks side by side with the difference in HTML highlighted
@johngodley
Copy link
Contributor Author

I made a some small tweaks to the CSS regarding line height. If you had a deleted section wrapping over lines it could overlap:

edit_post_ wordpress_latest _wordpress

This has now been fixed.

I noticed two other problems:

  • The Modal component doesn't prevent focus/blur events from propagating through to the page underneath. This means you can trigger tooltips and block dropdown menus, making it a bit twitchy feeling. I've got a fix in place which I'll put in a separate PR
  • Some blocks don't behave well to 'convert to blocks', and can result in the entire block being deleted. This isn't caused by the code here, but the preview ability makes it more noticeable. I'll also look at this separately

@jasmussen
Copy link
Contributor

The Modal component doesn't prevent focus/blur events from propagating through to the page underneath. This means you can trigger tooltips and block dropdown menus, making it a bit twitchy feeling. I've got a fix in place which I'll put in a separate PR

🏅

Yeah this is a separate issue that would be sweet to fix.

Some blocks don't behave well to 'convert to blocks', and can result in the entire block being deleted. This isn't caused by the code here, but the preview ability makes it more noticeable. I'll also look at this separately

You rule the world.

The vertical spacing would become strange if the converted preview was shorter than the original preview
@johngodley johngodley changed the title WIP: Block comparison: show difference between invalid block conversion options Block comparison: show difference between invalid block conversion options Sep 6, 2018
@johngodley johngodley mentioned this pull request Sep 6, 2018
4 tasks
Copy link
Contributor

@talldan talldan left a comment

Choose a reason for hiding this comment

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

@johngodley - This is really cool. In fact, I think it's so useful I was wondering if we would want to make it more prominent?

I added some comments, I think the main one that needs to be fixed is the max-height: 70% issue which is visible to the user.

Will probably also need someone more knowledgeable to comment on the library inclusion. Not sure who would be best.

const difference = differ.diff( originalContent, newContent );

return difference.map( ( item, pos ) => {
if ( item.added ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could use the classnames utility here to remove the need for the if and shorten to something like:

const className = classnames( {
  'editor-block-compare__added': item.added,
  'editor-block-compare__removed': item.removed,
} );

return <span className={ className } key={ pos }>{ item.value }</span>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Excellent idea!


return {
content: block.originalContent,
render: getSaveElement( blockType, block.attributes ),
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good to make these property names consistent with the props on the BlockView, either changing them here or there. I think the BlockView names are quite nice.

render() {
const { block, onKeep, onConvert, convertor, convertButtonText } = this.props;
const original = this.getOriginalContent( block );
const converted = this.getConvertedContent( convertor( block ), original );
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the definition of getConvertedContent, it seems like the second arg isn't needed.


getConvertedContent( block ) {
// The convertor may return an array of items or a single item
const newBlocks = Array.isArray( block ) ? block : [ block ];
Copy link
Contributor

Choose a reason for hiding this comment

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

I found out recently that castArray is another succinct way to do this:
https://lodash.com/docs/4.17.10#castArray


// Ensure the modal fits the content, otherwise it could be too big
.editor-block-compare {
max-height: 70%;
Copy link
Contributor

@talldan talldan Sep 7, 2018

Choose a reason for hiding this comment

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

At low screen width breakpoints the modal becomes full screen, but still only takes up 70% of screen height, so I think we need a rule to set it back to 100%


.editor-block-compare__wrapper {
display: flex;
justify-content: space-between;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure space-between is needed as the content seems to take up 100% of the width.

}
}

.editor-block-compare__converted {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have any thoughts on nesting element selectors inside the block selector @jasmussen? It might introduce unnecessary specificity, but then I'm not sure if we'd expect to have other styles try to override these, so probably ok.

One option is to do &__converted and in the resulting css the parent selector won't be included, but that only works one level deep.

padding-bottom: $panel-padding;

> div {
display: flex;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think flex is needed here, could just rely on normal layout.

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's being used to push the buttons to the same position at the bottom of the modal, otherwise they become vertically misaligned.


if ( compare ) {
return (
<Modal
Copy link
Contributor

Choose a reason for hiding this comment

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

Just some food for thought - I think I would have approached this differently and made BlockCompareModal a component that manages its own open/closed state. I'm not sure there's a right or wrong.

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, it's a good point. My thinking is that the comparison component could (potentially, and currently without any need) be used elsewhere that might be outside of a modal or an on/off situation and so I made the component do it's thing, and left it up to the parent to decide when to show it.

@johngodley
Copy link
Contributor Author

I think it's so useful I was wondering if we would want to make it more prominent?

Thanks for the review @talldan. I do wonder if it might make more sense if the 'convert to blocks' button just defaults to showing the comparison component directly, and we remove the option from the dropdown menu?

The effect of 'convert to blocks' is unknown to the user, and sometimes quite arbitrary and destructive. At the point the user is being presented with this warning I don't think it's excessive to add an additional click in the process. It saves having to undo the action and being annoyed at Gutenberg if the conversion isn't as expected.

Without this the button doesnt stick to the bottom
/**
* External dependencies
*/
import classname from 'classnames';
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a typo here, the imported function should be classnames instead of classname

@talldan
Copy link
Contributor

talldan commented Sep 10, 2018

@johngodley This is looking good. I've spotted one additional thing here:
https://github.com/WordPress/gutenberg/pull/7995/files#r216273361

I think the only thing we need now is to confirm that the script/dependency inclusion is ok. (edit: worth noting that at least if we use an npm package, the licence is checked by the licence-checker script: #8808).

On the user experience, I feel like it could definitely be iterated on this so that it's part of the workflow when editing a block HTML:

  1. User edits block HTML
  2. The diff is present to them when they've finished editing with the two options.
  3. User decides an option.

There are other times when the user might end up with invalid html (e.g. using the classic editor), so we might present that slightly differently to what I've proposed above. Would be good to get additional input on this though.

@johngodley
Copy link
Contributor Author

Thanks for that link, and it's pushed me towards just bundling the module normally

@@ -0,0 +1,28 @@
/**
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit hesitant about including a component BlockCompare in the components package, since it's not as general-purpose as intended by the package, but rather domain-specific to the idea of WordPress editors. Was editors considered instead?

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'm not sure what you mean by editors - can you point me in the direction?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can't speak for Andrew but since he's asleep I'll take an educated guess. Maybe he meant edit-post?

screen shot 2018-09-12 at 10 49 29

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 ok. I'm not entirely clear on the difference between editor and WordPress editors components. Are you suggesting it work like the keyboard shortcuts modal and isModalActive?

If the concern is the general purposeness of the component then could it be made local to the invalid warning component? I'm not sure if modals are designed to be used locally like this as the only other one I can find is the keyboard shortcut modal, which is 'global'.

Copy link
Contributor

Choose a reason for hiding this comment

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

Again I can't but make educated guesses — I think it's something about the packages getting published on NPM but not sure.

Copy link
Contributor

@talldan talldan Sep 12, 2018

Choose a reason for hiding this comment

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

I'll wait for Andrew to clarify, but the component is already in the editor package, not in the components package, so maybe it was a misread. Or maybe he did mean edit-post, but I'm not sure we should be implementing components from edit-post in editor like this would be.

Copy link
Member

Choose a reason for hiding this comment

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

Oof, I'm sorry for the trouble, it was definitely a misread on my part.

I had mistaken it as being part of packages/components.

Copy link
Contributor

@talldan talldan left a comment

Choose a reason for hiding this comment

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

This is really close, I just spotted one thing in further testing. Will also wait for @aduth to clarify his comment.


return {
rawContent: block.originalContent,
renderedContent: getSaveElement( blockType, block.attributes ),
Copy link
Contributor

Choose a reason for hiding this comment

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

I've just given this another test, and I'm not sure the original content's preview displays quite what I was expecting. My steps:

  1. Add some simple text to a paragraph block
  2. Edit the html and add an inline style style="font-size:25px;"
  3. View the comparison
  4. Note that the original content preview doesn't show the font size at 25px, which is what I'd get if I selected 'Keep as HTML'.

To make the preview display correctly I tried using RawHTML like the HTML block does for its preview, which seemed to do the trick:

	getOriginalContent( block ) {
		return {
			rawContent: block.originalContent,
			renderedContent: (
				<RawHTML>{ block.originalContent }</RawHTML>
			),
		};
	}

Copy link
Contributor Author

@johngodley johngodley Sep 12, 2018

Choose a reason for hiding this comment

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

Interesting!

RawHTML uses dangerouslySetInnerHTML, which opens up things like <p onclick="alert(1)">fdsfsdfds fd sfds</p>. I'm not sure if this is a problem in this context and what the behaviour is supposed to be so I'll have a poke around.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The 'preview' mode of the HTML block uses the <Sandbox /> component to display arbitrary HTML, which neutralises my above example, and seems to be the way to go. The styling is different so I'll see if it can be made to fit in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After some playing around it seems <Sandbox /> is an iframe and while it does prevent the JS above, it is also unstyled.

Although I think the scope for mischief is small I'm not sure we should introduce it.

With that in mind we can either accept the unstyled appearance (looks ok for paragraphs, doesnt look ok for images), or keep the current not-quite-full preview (it's displaying the block without the invalid bits, but with the valid changes). There may be another way I'm unaware of, too

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, yep, thanks for looking into it. Lets go with what we've got and then maybe look into this on a separate issue.

@johngodley johngodley merged commit 5ea0283 into master Sep 13, 2018
@johngodley
Copy link
Contributor Author

Thanks for the feedback and suggestions!

@johngodley johngodley deleted the add/invalid-block-diff branch September 13, 2018 08:51
@jasmussen
Copy link
Contributor

Amazing work!

return (
<div className={ className }>
<div className="editor-block-compare__content">
<h1 className="components-modal__header-heading">{ title }</h1>
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 not the modal component, so should not be stealing / inheriting styles. Components should be insulated from one another to avoid maintenance issues with cascading consequences of future changes.

</div>

<div className="editor-block-compare__action">
<Button isLarge tabindex="0" onClick={ action }>{ actionText }</Button>
Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing a console warning is being logged on this line?

We should pass tabIndex as the DOM property tabIndex={ 0 }, not the attribute name.

Note that you should still use the canonical React naming for known attributes:

// Yes, please
<div tabIndex="-1" />

// Warning: Invalid DOM property `tabindex`. Did you mean `tabIndex`?
<div tabindex="-1" />

In other words, the way you use DOM components in React hasn’t changed, but now you have some new capabilities.

https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oddly it doesn't log a warning, but both very valid points and I'll get those fixed in another PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Block Transforms Block transforms from one block to another [Feature] Blocks Overall functionality of blocks
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants