Skip to content

Commit

Permalink
Meta Box back compat and fall backs (#3554)
Browse files Browse the repository at this point in the history
Fall back to the classic editor when a meta box declares itself incompatible with Gutenberg, with the `__block_editor_compatible_meta_box` arg. This can later be expanded to automatically detect meta boxes that aren't compatible, and fall back.

When a meta box causes a fall back, and `WP_DEBUG` is enabled, show a warning to the end user in the meta box.
  • Loading branch information
pento committed Nov 27, 2017
1 parent 8db5772 commit 9864a60
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 3 deletions.
6 changes: 6 additions & 0 deletions docs/manifest.json
Expand Up @@ -77,6 +77,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
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',
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
Expand Down
116 changes: 116 additions & 0 deletions lib/meta-box-partial-page.php
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'] ) ) {
$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'] );
}

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 ) {
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
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 ) ) {
$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 ) {
?>
<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
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

0 comments on commit 9864a60

Please sign in to comment.