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

Meta Box back compat and fall backs #3554

Merged
merged 14 commits into from
Nov 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
"markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/themes.md",
"parent": "reference"
},
{
"title": "Meta Boxes",
"slug": "meta-box",
"markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/meta-box.md",
"parent": "reference"
},
{
"title": "Glossary",
"slug": "glossary",
Expand Down
38 changes: 36 additions & 2 deletions docs/meta-box.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ the superior developer and user experience of blocks however, especially once,
block templates are available, **converting PHP meta boxes to blocks is highly
encouraged!**

### Testing, Converting, and Maintaining Existing Meta Boxes

Before converting meta boxes to blocks, it may be easier to test if a meta box works with Gutenberg, and explicitly mark it as such.

If a meta box *doesn't* work with in Gutenberg, and updating it to work correctly is not an option, the next step is to add the `__block_editor_compatible_meta_box` argument to the meta box declaration:

```php
add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback',
null, 'normal', 'high',
Copy link
Member

Choose a reason for hiding this comment

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

Mixed whitespace, tabs and spaces.

Copy link
Member Author

Choose a reason for hiding this comment

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

Noice. #3684.

array(
'__block_editor_compatible_meta_box' => false,
)
);
```

This will cause WordPress to fall back to the Classic editor, where the meta box will continue working as before.

Explicitly setting `__block_editor_compatible_meta_box` to `true` will cause WordPress to stay in Gutenberg (assuming another meta box doesn't cause a fallback, of course).

After a meta box is converted to a block, it can be declared as existing for backwards compatibility:

```php
add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback',
null, 'normal', 'high',
array(
'__back_compat_meta_box' => false,
)
);
```

When Gutenberg is run, this meta box will no longer be displayed in the meta box area, as it now only exists for backwards compatibility purposes. It will continue to be displayed correctly in the Classic editor, should some other meta box cause a fallback.

### Meta Box Data Collection

On each Gutenberg page load, the global state of post.php is mimicked, this is
Expand All @@ -22,7 +54,9 @@ namely `add_meta_boxes`, `add_meta_boxes_{$post->post_type}`, and `do_meta_boxes

A copy of the global `$wp_meta_boxes` is made then filtered through
`apply_filters( 'filter_gutenberg_meta_boxes', $_meta_boxes_copy );`, which will
strip out any core meta boxes along with standard custom taxonomy meta boxes.
strip out any core meta boxes, standard custom taxonomy meta boxes, and any meta
boxes that have declared themselves as only existing for backwards compatibility
purposes.

Then each location for this particular type of meta box is checked for whether it
is active. If it is not empty a value of true is stored, if it is empty a value
Expand All @@ -36,7 +70,7 @@ have to do, unless we want to move `createEditorInstance()` to fire in the foote
or at some point after `admin_head`. With recent changes to editor bootstrapping
this might now be possible. Test with ACF to make sure.

### Redux and React Meta Box Management.
### Redux and React Meta Box Management

*The Redux store by default will hold all meta boxes as inactive*. When
`INITIALIZE_META_BOX_STATE` comes in, the store will update any active meta box
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated but we should add this file to the Gutenberg Handbook.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, I didn't even notice that it wasn't there. 🙂 Added!

Expand Down
116 changes: 116 additions & 0 deletions lib/meta-box-partial-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,10 @@ function gutenberg_filter_meta_boxes( $meta_boxes ) {
if ( isset( $data['callback'] ) && in_array( $data['callback'], $taxonomy_callbacks_to_unset ) ) {
unset( $meta_boxes[ $page ][ $context ][ $priority ][ $name ] );
}
// Filter out meta boxes that are just registered for back compat.
if ( isset( $data['args']['__back_compat_meta_box'] ) && $data['args']['__back_compat_meta_box'] ) {
unset( $meta_boxes[ $page ][ $context ][ $priority ][ $name ] );
}
}
}
}
Expand Down Expand Up @@ -550,3 +554,115 @@ function gutenberg_is_meta_box_empty( $meta_boxes, $context, $post_type ) {
}

add_filter( 'filter_gutenberg_meta_boxes', 'gutenberg_filter_meta_boxes' );

/**
* Go through the global metaboxes, and override the render callback, so we can trigger our warning if needed.
*
* @since 1.8.0
*/
function gutenberg_intercept_meta_box_render() {
global $wp_meta_boxes;

foreach ( $wp_meta_boxes as $post_type => $contexts ) {
foreach ( $contexts as $context => $priorities ) {
foreach ( $priorities as $priority => $boxes ) {
foreach ( $boxes as $id => $box ) {
if ( ! is_array( $wp_meta_boxes[ $post_type ][ $context ][ $priority ][ $id ]['args'] ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we replace $wp_meta_boxes[ $post_type ][ $context ][ $priority ][ $id ] by $box in this loop?

Copy link
Member Author

Choose a reason for hiding this comment

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

We can really only replace the first instance - $box is a copy of the array element, so modifications need to be applied to $wp_meta_boxes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Too many JS in my head :)

$wp_meta_boxes[ $post_type ][ $context ][ $priority ][ $id ]['args'] = array();
}
if ( ! isset( $wp_meta_boxes[ $post_type ][ $context ][ $priority ][ $id ]['args']['__original_callback'] ) ) {
$wp_meta_boxes[ $post_type ][ $context ][ $priority ][ $id ]['args']['__original_callback'] = $box['callback'];
$wp_meta_boxes[ $post_type ][ $context ][ $priority ][ $id ]['callback'] = 'gutenberg_override_meta_box_callback';
}
}
}
}
}
}
add_action( 'submitpost_box', 'gutenberg_intercept_meta_box_render' );
add_action( 'submitpage_box', 'gutenberg_intercept_meta_box_render' );
add_action( 'edit_page_form', 'gutenberg_intercept_meta_box_render' );
add_action( 'edit_form_advanced', 'gutenberg_intercept_meta_box_render' );

/**
* Check if this metabox only exists for back compat purposes, show a warning if it doesn't.
*
* @since 1.8.0
*
* @param mixed $object The object being operated on, on this screen.
* @param array $box The current meta box definition.
*/
function gutenberg_override_meta_box_callback( $object, $box ) {
$callback = $box['args']['__original_callback'];
unset( $box['args']['__original_callback'] );

$block_compatible = true;
if ( isset( $box['args']['__block_editor_compatible_meta_box'] ) ) {
$block_compatible = (bool) $box['args']['__block_editor_compatible_meta_box'];
unset( $box['args']['__block_editor_compatible_meta_box'] );
}

if ( isset( $box['args']['__back_compat_meta_box'] ) ) {
$block_compatible |= (bool) $box['args']['__back_compat_meta_box'];
unset( $box['args']['__back_compat_meta_box'] );
Copy link
Contributor

Choose a reason for hiding this comment

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

Just per curiosity, why do we unset these keys?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copying the same pattern from Core - the meta box render function doesn't need to know about these keys.

}

if ( ! $block_compatible ) {
gutenberg_show_meta_box_warning( $callback );
}

call_user_func( $callback, $object, $box );
}

/**
* Display a warning in the metabox that the current plugin is causing the fallback to the old editor.
*
* @since 1.8.0
*
* @param callable $callback The function that a plugin has defined to render a meta box.
*/
function gutenberg_show_meta_box_warning( $callback ) {
// Only show the warning when WP_DEBUG is enabled.
if ( ! WP_DEBUG ) {
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 not show it even in "prod" while we're on the plugin phase and introduce this change later. To encourage people to check their metaboxes?

Copy link
Member Author

Choose a reason for hiding this comment

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

I did this way while we're still working out the path for plugin devs to move their meta boxes to blocks - it doesn't seem fair to show a warning that the dev can't do anything to fix.

Once there are well documented upgrade paths, we can take away this check. Once it comes time to merge, we can evaluate how we want to merge it. It might even be valuable to have a "plugin x caused this fall back" message in prod, so users know what happened, and a "here are links to documentation and tutorials to upgrade your meta boxes" when WP_DEBUG is set, so plugin devs have the information they need.

return;
}

// Don't show in the Gutenberg meta box UI.
if ( ! isset( $_REQUEST['classic-editor'] ) ) {
return;
}

if ( is_array( $callback ) ) {
$reflection = new ReflectionMethod( $callback[0], $callback[1] );
} else {
$reflection = new ReflectionFunction( $callback );
}

if ( $reflection->isInternal() ) {
return;
}

$filename = $reflection->getFileName();
if ( strpos( $filename, WP_PLUGIN_DIR ) !== 0 ) {
return;
}

$filename = str_replace( WP_PLUGIN_DIR, '', $filename );
$filename = preg_replace( '|^/([^/]*/).*$|', '\\1', $filename );

$plugins = get_plugins();
foreach ( $plugins as $name => $plugin ) {
if ( strpos( $name, $filename ) === 0 ) {
?>
<div class="error inline">
<p>
<?php
/* translators: %s is the name of the plugin that generated this meta box. */
printf( __( 'Gutenberg incompatible meta box, from the "%s" plugin.', 'gutenberg' ), $plugin['Name'] );
?>
</p>
</div>
<?php
}
}
}
32 changes: 31 additions & 1 deletion lib/register.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,40 @@ function gutenberg_collect_meta_box_data() {

// If the meta box should be empty set to false.
foreach ( $locations as $location ) {
if ( isset( $_meta_boxes_copy[ $post->post_type ][ $location ] ) && gutenberg_is_meta_box_empty( $_meta_boxes_copy, $location, $post->post_type ) ) {
if ( gutenberg_is_meta_box_empty( $_meta_boxes_copy, $location, $post->post_type ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This call is always returning false for "side" meta boxes because the array doesn't have a "side" key even if I do have a side meta box? Any idea how to fix this @pento

Copy link
Contributor

Choose a reason for hiding this comment

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

Nevermind, I might have had a weird setup at that moment. It's working as expected right now.

$meta_box_data[ $location ] = false;
} else {
$meta_box_data[ $location ] = true;
$incompatible_meta_box = false;
// Check if we have a meta box that has declared itself incompatible with the block editor.
foreach ( $_meta_boxes_copy[ $post->post_type ][ $location ] as $boxes ) {
foreach ( $boxes as $box ) {
/*
* If __block_editor_compatible_meta_box is declared as a false-y value,
* the meta box is not compatible with the block editor.
*/
if ( is_array( $box['args'] )
&& isset( $box['args']['__block_editor_compatible_meta_box'] )
&& ! $box['args']['__block_editor_compatible_meta_box'] ) {
$incompatible_meta_box = true;
break 2;
}
}
}

// Incompatible meta boxes require an immediate redirect to the classic editor.
if ( $incompatible_meta_box ) {
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 not perform the redirect server-side here?

Copy link
Member Author

Choose a reason for hiding this comment

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

With the current code, no. gutenberg_collect_meta_box_data() happens in the admin_head action, well after HTML has started being sent.

?>
<script type="text/javascript">
var joiner = '?';
if ( window.location.search ) {
joiner = '&';
}
window.location.href += joiner + 'classic-editor';
</script>
<?php
exit;
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions phpunit/class-meta-box-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,37 @@ public function test_gutenberg_filter_meta_boxes() {
$this->assertEquals( $expected, $actual );
}

/**
* Test filtering back compat meta boxes
*/
public function test_gutenberg_filter_back_compat_meta_boxes() {
$meta_boxes = $this->meta_boxes;

// Add in a back compat meta box.
$meta_boxes['post']['normal']['high']['some-meta-box'] = array(
'id' => 'some-meta-box',
'title' => 'Some Meta Box',
'callback' => 'some_meta_box',
'args' => array(
'__back_compat_meta_box' => true,
),
);

// Add in a normal meta box.
$meta_boxes['post']['normal']['high']['some-other-meta-box'] = array( 'other-meta-box-stuff' );

$expected_meta_boxes = $this->meta_boxes;
// We expect to remove only core meta boxes.
$expected_meta_boxes['post']['normal']['core'] = array();
$expected_meta_boxes['post']['side']['core'] = array();
$expected_meta_boxes['post']['normal']['high']['some-other-meta-box'] = array( 'other-meta-box-stuff' );

$actual = gutenberg_filter_meta_boxes( $meta_boxes );
$expected = $expected_meta_boxes;

$this->assertEquals( $expected, $actual );
}

/**
* Test filtering of meta box data with taxonomy meta boxes.
*
Expand Down