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

Reusable Blocks: Support importing and exporting reusable blocks #9788

Merged
merged 8 commits into from
Sep 14, 2018

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented Sep 11, 2018

This PR adds Export/Import capabilities to the page listing the available reusable blocks.

Remaining

  • Polish the design (especially the import form)
  • E2e tests

@youknowriad youknowriad added the [Feature] Synced Patterns Related to synced patterns (formerly reusable blocks) label Sep 11, 2018
@youknowriad youknowriad self-assigned this Sep 11, 2018
@youknowriad youknowriad requested review from mtias, jasmussen and a team September 11, 2018 11:31
reader.onload = function() {
resolve( reader.result );
};
reader.readAsText( file );
Copy link
Contributor Author

@youknowriad youknowriad Sep 11, 2018

Choose a reason for hiding this comment

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

I have to check browser support for this one to see if we need a polyfill.

Copy link
Member

Choose a reason for hiding this comment

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

I have to check browser support for this one to see if we need a polyfill.

Did you check? As best I can tell from browser support resources, it should be supported.

https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsText#Browser_compatibility

@jasmussen
Copy link
Contributor

This is amazing. If you were more on fire, you'd cause nuclear fusion and solve the world energy crisis. Totally love it. Thanks so much for working on this.

A few screenshots.

  1. This is where management happens:

screen shot 2018-09-11 at 14 25 28

  1. This is the management page:

screen shot 2018-09-11 at 14 25 36

  1. When you press "Export", a JSON file is immediately downloaded. Here's what a JSON file could look like:
{
  "title": "Hello World",
  "content": "<!-- wp:paragraph -->\n<p>This is really dang cool.</p>\n<!-- /wp:paragraph -->"
}
  1. If you presse Import, this is what you see:

screen shot 2018-09-11 at 14 25 45

  1. I got a small bug after importing:

screen shot 2018-09-11 at 14 25 58

But overall, this is SO SO SO VERY SOLID. It works remarkably well, and I really dig how you've gone for the simplest possible solution here. Along with the multi-selection reusable blocks PR, this is going to be magical.


It's also so magical that my excitement is getting the better of me. So please categorize my feedback into two categories, soon and future.

Things we should do soon, if not in this PR:

2, 3, 4 are all fine.

5 obviously needs to be fixed.

1 is the weakest part, simply because it's so hard to find out where to manage your reusable blocks. I think it's probably fine to have the cog in the reusable blocks panel, but the feature is becoming so cool I feel it needs better placement. The first place I would look for this is in the More menu. We could have an item there that simply says "Manage reusable blocks".

Or, we could have multiple items there — Manage Reusable Blocks, Import Blocks, Export Blocks. Some of these items also depend on where we go with #9732. For example I think it would be neat to default to importing blocks that are immediately editable, i.e. they are not reusable. And then repositioning "reusable" to be an addition on top of that. They could even be renamed Templates (immediately editable blocks) and Global Templates (new name for reusable blocks).

In such a situation perhaps the page could be "Manage Block Templates".

Things we should think about and maybe do afterwards, or in the future:

  • Wouldn't it be cool if you could drag and drop a .json file directly on to the editing canvas, resulting in a prompt asking you if you wanted to [Import Block Template] or [Import and insert Block Template]?
  • I personally have a great desire to copy and paste these Template json files intead of having to manage local saved .json files. I imagine people sharing these snippets via pastebin on Twitter, or in lots of different places, and I imagine the increased sharability of a string of text than a file is hugely valuable. As such, it would be nice if pressing "Export" opened a modal prompt asking you if you wanted to download the .json file, or copy it to your clipboard. Similarly, pressing Import could open a modal asking if you wanted to paste it in, or upload a .json file.
  • Since we're in "crazy dreaming" territory, how cool would it be if you could paste this JSON directly into the editor in an empty paragraph, and our paste filters detected and imported it on the fly?

Okay I'm going to splash some cold water on my face.

Immediate next step to me seems like it should be to copy the "Manage Reusable Blocks" link to the More menu in the to right corner. With that in place (and 5 fixed), I think this could actually be good to go.

@ZebulanStanphill
Copy link
Member

This is a great addition!

@jasmussen

I think it's probably fine to have the cog in the reusable blocks panel, but the feature is becoming so cool I feel it needs better placement. The first place I would look for this is in the More menu. We could have an item there that simply says "Manage reusable blocks".

I agree that making this feature more visible is a good idea.

They could even be renamed Templates (immediately editable blocks) and Global Templates (new name for reusable blocks).

Speaking of using reusable blocks as templates: #8403. I recommend checking out how Beaver Builder, Divi, and Oxygen implement this kind of thing for some inspiration.

  • Wouldn't it be cool if you could drag and drop a .json file directly on to the editing canvas, resulting in a prompt asking you if you wanted to [Import Block Template] or [Import and insert Block Template]?
  • I personally have a great desire to copy and paste these Template json files intead of having to manage local saved .json files. I imagine people sharing these snippets via pastebin on Twitter, or in lots of different places, and I imagine the increased sharability of a string of text than a file is hugely valuable. As such, it would be nice if pressing "Export" opened a modal prompt asking you if you wanted to download the .json file, or copy it to your clipboard. Similarly, pressing Import could open a modal asking if you wanted to paste it in, or upload a .json file.
  • Since we're in "crazy dreaming" territory, how cool would it be if you could paste this JSON directly into the editor in an empty paragraph, and our paste filters detected and imported it on the fly?

Yes to all of this!

Immediate next step to me seems like it should be to copy the "Manage Reusable Blocks" link to the More menu in the to right corner. With that in place (and 5 fixed), I think this could actually be good to go.

Sounds good to me!

@youknowriad youknowriad force-pushed the try/reusable-blocks-import-export branch from 0112cb4 to 41a2ac5 Compare September 13, 2018 08:17
@youknowriad
Copy link
Contributor Author

youknowriad commented Sep 13, 2018

  • I added the more menu item to "Manage reusable blocks"
  • I fixed the bug on import success. A compromise I made though is to avoid refreshing the page on a successful import. The idea is that later on, we probably would avoid the PHP generated page entirely so instead of battling and trying to find a way to properly refresh and show the notice, we should the success notice and at some point when the refactoring of this page is done to be full JS rendered, it would be easy to just refresh the listing.
  • I added an explicit __file: 'wp_block' tag to the JSON exported file to avoid ambiguity about the file's content.

@youknowriad youknowriad added this to the 3.9 milestone Sep 13, 2018
@aduth aduth self-requested a review September 13, 2018 17:54
@aduth
Copy link
Member

aduth commented Sep 13, 2018

What are testing instructions here?

Trying to infer from code, when navigating to /wp-admin/edit.php?post_type=wp_block, clicking the Import from JSON button causes the button to disappear, with a few errors in console:

Uncaught TypeError: Cannot set property isMounted of #<Component> which has only a getter
    at new ImportForm (index.js:23)
    at constructClassInstance (react-dom.24169eaf.js:12025)
    at updateClassComponent (react-dom.24169eaf.js:13722)
    at beginWork (react-dom.24169eaf.js:14402)
    at performUnitOfWork (react-dom.24169eaf.js:16441)
    at workLoop (react-dom.24169eaf.js:16480)
    at HTMLUnknownElement.callCallback (react-dom.24169eaf.js:140)
    at Object.invokeGuardedCallbackDev (react-dom.24169eaf.js:178)
    at invokeGuardedCallback (react-dom.24169eaf.js:227)
    at replayUnitOfWork (react-dom.24169eaf.js:15888)
ImportForm @ index.js:23
constructClassInstance @ react-dom.24169eaf.js:12025
updateClassComponent @ react-dom.24169eaf.js:13722
beginWork @ react-dom.24169eaf.js:14402
performUnitOfWork @ react-dom.24169eaf.js:16441
workLoop @ react-dom.24169eaf.js:16480
callCallback @ react-dom.24169eaf.js:140
invokeGuardedCallbackDev @ react-dom.24169eaf.js:178
invokeGuardedCallback @ react-dom.24169eaf.js:227
replayUnitOfWork @ react-dom.24169eaf.js:15888
renderRoot @ react-dom.24169eaf.js:16540
performWorkOnRoot @ react-dom.24169eaf.js:17138
performWork @ react-dom.24169eaf.js:17060
performSyncWork @ react-dom.24169eaf.js:17032
interactiveUpdates$1 @ react-dom.24169eaf.js:17297
interactiveUpdates @ react-dom.24169eaf.js:2326
dispatchInteractiveEvent @ react-dom.24169eaf.js:4882

react-dom.24169eaf.js:14804 The above error occurred in the <ImportForm> component:
    in ImportForm (created by WithInstanceId(ImportForm))
    in WithInstanceId(ImportForm) (created by Dropdown)
    in div (created by Popover)
    in div (created by Popover)
    in PopoverDetectOutside (created by WrappedPopoverDetectOutside)
    in WrappedPopoverDetectOutside (created by Popover)
    in Unknown (created by WithFocusReturn(Component))
    in div (created by WithFocusReturn(Component))
    in WithFocusReturn(Component) (created by WithConstrainedTabbing(WithFocusReturn(Component)))
    in div (created by WithConstrainedTabbing(WithFocusReturn(Component)))
    in WithConstrainedTabbing(WithFocusReturn(Component)) (created by Popover)
    in span (created by Popover)
    in Popover (created by Dropdown)
    in div (created by Dropdown)
    in Dropdown (created by ImportDropdown)
    in ImportDropdown

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.
logCapturedError @ react-dom.24169eaf.js:14804
logError @ react-dom.24169eaf.js:14843
update.callback @ react-dom.24169eaf.js:15496
callCallback @ react-dom.24169eaf.js:11456
commitUpdateQueue @ react-dom.24169eaf.js:11500
commitLifeCycles @ react-dom.24169eaf.js:14974
commitAllLifeCycles @ react-dom.24169eaf.js:16040
callCallback @ react-dom.24169eaf.js:140
invokeGuardedCallbackDev @ react-dom.24169eaf.js:178
invokeGuardedCallback @ react-dom.24169eaf.js:227
commitRoot @ react-dom.24169eaf.js:16181
completeRoot @ react-dom.24169eaf.js:17196
performWorkOnRoot @ react-dom.24169eaf.js:17141
performWork @ react-dom.24169eaf.js:17060
performSyncWork @ react-dom.24169eaf.js:17032
interactiveUpdates$1 @ react-dom.24169eaf.js:17297
interactiveUpdates @ react-dom.24169eaf.js:2326
dispatchInteractiveEvent @ react-dom.24169eaf.js:4882

react-dom.24169eaf.js:17121 Uncaught TypeError: Cannot set property isMounted of #<Component> which has only a getter
    at new ImportForm (index.js:23)
    at constructClassInstance (react-dom.24169eaf.js:12025)
    at updateClassComponent (react-dom.24169eaf.js:13722)
    at beginWork (react-dom.24169eaf.js:14402)
    at performUnitOfWork (react-dom.24169eaf.js:16441)
    at workLoop (react-dom.24169eaf.js:16480)
    at renderRoot (react-dom.24169eaf.js:16520)
    at performWorkOnRoot (react-dom.24169eaf.js:17138)
    at performWork (react-dom.24169eaf.js:17060)
    at performSyncWork (react-dom.24169eaf.js:17032)

file: null,
};

this.isMounted = true;
Copy link
Member

@aduth aduth Sep 13, 2018

Choose a reason for hiding this comment

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

Re: Previous comment, this is actually a part of the React component API, though discouraged, which may explain errors around attempts to set it.

https://reactjs.org/blog/2015/12/16/ismounted-antipattern.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.

I know the antipattern but sometimes I'm pragmatic and don't want to introduce more complex solutions like a cancelable promise or something like that. Especially since it's a small JS file for a separate page here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, noting that I wasn't able to reproduce any of the errors you had, so I'd appreciate another check

Copy link
Member

Choose a reason for hiding this comment

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

The point was less about it being discouraged / deprecated, and more to the point that this is already a property defined on a default React component, and we're overriding it.

} );

// Setup Import Form
document.addEventListener( 'DOMContentLoaded', function() {
Copy link
Member

Choose a reason for hiding this comment

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

Inconsistency: function() { (function keyword) here vs. ( event ) => { (arrow function) above.

const showNotice = () => {
const notice = document.createElement( 'div' );
notice.className = 'notice notice-success is-dismissible';
notice.innerHTML = `<p>${ __( 'Reusable block imported with success!' ) }</p>`;
Copy link
Member

Choose a reason for hiding this comment

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

Grammar: "successfully" might work better than "with success" here.

}

const showNotice = () => {
const notice = document.createElement( 'div' );
Copy link
Member

Choose a reason for hiding this comment

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

Could we render a <Notice /> component?

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 did try it, the issue is that it's styling is very different from the regular notices in this kind of pages. We should reconsider if we rewrite the whole page in JS

const postType = await apiFetch( { path: `/wp/v2/types/wp_block` } );
const reusableBlock = await apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } );
const fileContent = JSON.stringify( {
__file: 'wp_block',
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 trying to understand what this is for. Is this a marker of the file being of block format? Does it really matter? Or could we just infer by validity of the rest of the file (being JSON, having title, etc).

Or at least, "file" may not be a well-describing word here (vs. "format", "type", etc).

Also wondering if there's some prior art here to lean on without over-complicating (JSON-LD, JSON Schema, xmlns).

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, this is just a marker. My idea is that this will remove any ambiguity while trying to auto-import this file (like if we implement drag and drop to the editor). We could detect that the imported file has a "title" and "content" strings (JSON Schema) but this seems too common, I prefer as always explicitness over implicitness. What if we add another exported file with the same format that should imported differently? (like in a File block or something).

I also considered adding a version field if ever we change the format but I decided against because we can think of a missing version as version: 0

Copy link
Member

Choose a reason for hiding this comment

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

Another thought: Do we ever intend to support batch import / export? Would this file contain multiple at some point? Will this format support that as a future enhancement?

Copy link
Member

Choose a reason for hiding this comment

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

Explicitness is fine, just seems a format arbitrarily decided upon.

try {
parsedContent = JSON.parse( fileContent );
} catch ( e ) {
throw new Error( __( 'Invalid JSON file' ) );
Copy link
Member

Choose a reason for hiding this comment

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

Should an error message be localized? Maybe what we present in the UI, but it doesn't seem like we're barred from translating the language-neutral error.message to a localized form.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is present in the UI, that's why I did it. We do something similar in media-upload. I can add a code and a message maybe, that way the code is constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, we can't attach a code to the Error object, maybe it's fine for now?

Copy link
Member

Choose a reason for hiding this comment

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

The usage in UI should be secondary to having consistent, predictable messages for errors. One place I've seen this become an issue is in error aggregators (think Sentry, Rollbar), where you'll have duplicates of the "same" (but localized variant of) errors.

I'd even consider something like:

let uiMessage;
switch ( error.message ) {
	case 'Invalid JSON file':
		uiMessage = __( 'Invalid JSON file' );
		break;
}

{
"name": "@wordpress/list-reusable-blocks",
"version": "1.0.0",
"description": "Adding Export/Import support to the reusable blocks listing.",
Copy link
Member

Choose a reason for hiding this comment

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

Really stress-testing our policy of publishing all the things as modules, huh 😅

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 made it private :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that I think about it. Making it private means it can't be included in Core as an external package and begs the question of whether it should be moved to Core or kept in the repo post-merge.

Copy link
Member

Choose a reason for hiding this comment

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

I mean, I never said I was opposed to it being published to npm 😉

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, I know it was private even before the comment :)

@youknowriad youknowriad force-pushed the try/reusable-blocks-import-export branch from 904f010 to 34d4b4d Compare September 14, 2018 11:04
@aduth
Copy link
Member

aduth commented Sep 14, 2018

Small usability thing: A bit disconcerting to see the success notice, but not the block that I'd just imported (until a refresh).

image

$actions['export'] = sprintf(
'<a class="wp-list-reusable-blocks__export" href="#" data-id="%s" aria-label="%s">%s</a>',
$post->ID,
__( 'Export as JSON', 'gutenberg' ),
Copy link
Member

Choose a reason for hiding this comment

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

Is this convention lifted from somewhere? Doesn't seem obvious why we'd need an aria-label which has the same text as the element itself.

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, it's inspired by the way we add the "Classic editor" link in the post list actions

const postType = await apiFetch( { path: `/wp/v2/types/wp_block` } );
const reusableBlock = await apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } );
const fileContent = JSON.stringify( {
__file: 'wp_block',
Copy link
Member

Choose a reason for hiding this comment

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

Another thought: Do we ever intend to support batch import / export? Would this file contain multiple at some point? Will this format support that as a future enhancement?

reader.onload = function() {
resolve( reader.result );
};
reader.readAsText( file );
Copy link
Member

Choose a reason for hiding this comment

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

I have to check browser support for this one to see if we need a polyfill.

Did you check? As best I can tell from browser support resources, it should be supported.

https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsText#Browser_compatibility

const postType = await apiFetch( { path: `/wp/v2/types/wp_block` } );
const reusableBlock = await apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ id }` } );
const fileContent = JSON.stringify( {
__file: 'wp_block',
Copy link
Member

Choose a reason for hiding this comment

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

Explicitness is fine, just seems a format arbitrarily decided upon.

uiMessage = __( 'Invalid Reusable Block JSON file' );
break;
default:
uiMessage = __( 'Unknow error' );
Copy link
Member

Choose a reason for hiding this comment

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

Typo: "Unknow" -> "Unknown"

@youknowriad youknowriad merged commit ff4bc70 into master Sep 14, 2018
@aduth aduth deleted the try/reusable-blocks-import-export branch September 14, 2018 13:54
@mtias
Copy link
Member

mtias commented Sep 14, 2018

A good puzzle to solve is going to be handling local resources (IDs, etc) through this process.

@aduth
Copy link
Member

aduth commented Sep 17, 2018

Some usability issues with editing reusable blocks noted at #9964, made more apparent here with the introduction of the "Manage All Reusable Blocks" link.

@janicecchua
Copy link

Was able to export and import reusable blocks with no issues. Just one thing which is a bit inconvenient is you'd have to refresh the page after importing to show the imported block.

@youknowriad
Copy link
Contributor Author

@janicecchua yes, that's true and that's a compromise we made for now. I talked about it above

I fixed the bug on import success. A compromise I made though is to avoid refreshing the page on a successful import. The idea is that later on, we probably would avoid the PHP generated page entirely so instead of battling and trying to find a way to properly refresh and show the notice, we should the success notice and at some point when the refactoring of this page is done to be full JS rendered, it would be easy to just refresh the listing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Synced Patterns Related to synced patterns (formerly reusable blocks)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants