WordPress / gutenberg

The Block Editor project for WordPress and beyond. Plugin is available from the official repository.
https://wordpress.org/gutenberg/
Other
10.34k stars 4.13k forks source link

Introduce formal API for displaying notices #5975

Closed danielbachhuber closed 5 years ago

danielbachhuber commented 6 years ago

Core WordPress has an admin_notices action where it's quite common for plugins / themes to display notices.

In Gutenberg, this looks quite bad:

image

Gutenberg should have some formal API for plugins / themes to register their notices. This should support notices specific to the view (e.g. editor) and user actions (e.g. saving a post).

Previously: #5590 #5927 #3964

jsmoriss commented 6 years ago

FYI - My own plugins provide important notices to users after creating / updating the metabox. I need a way to submit notices to Gutenberg for display. I cannot make my plugins compatible with Gutenberg without this functionality. Here is the code I've started, but have stopped development because 1) I need to exclude auto-saves (don't want to refresh on auto-save), and 2) I need to submit notices to Gutenberg, which does not appear to be possible right now.

var editPost = wp.data.select( 'core/edit-post' ), lastIsSaving = false;

wp.data.subscribe( function() {
        var isSaving = editPost.isSavingMetaBoxes();
        if ( isSaving !== lastIsSaving && !isSaving ) {
                lastIsSaving = isSaving;

                // TODO: ajax call to get metabox HTML

                // TODO: refresh the metabox container

                // TODO: ajax call to get the notices array

                // TODO: submit notices to Gutenberg for display

        }
        lastIsSaving = isSaving;
} );

Thanks,

js.

adamsilverstein commented 6 years ago

For this task it would be worth considering adding direct support for add_settings_error (https://developer.wordpress.org/reference/functions/add_settings_error/) so existing uses will continue to work. I'm imaging an api similar to the customizer where the PHP size maps to a similar JS based api that plugins could also use.

westonruter commented 6 years ago

Are not notices already supported? I've been using wp.data.dispatch( 'core/editor' ).createWarningNotice() successfully to create them in https://github.com/Automattic/amp-wp/blob/286381c/assets/js/amp-block-validation.js#L175-L185

adamsilverstein commented 6 years ago

Right, we can add notices this way in JavaScript.... @danielbachhuber is this proposing adding a PHP side API?

Supporting the existing PHP API ( add_settings_error) could be useful for error states that occur on the PHP side, in an endpoint, or at page load for example, or for plugins that may have no post JavaScript at all and still have a reason to display a notice (for example, an api required to save posts is not available).

danielbachhuber commented 6 years ago

I was unaware of wp.data.dispatch( 'core/editor' ).createWarningNotice(). If there is such equivalent API, this may be a matter of documentation (or some magic transformation of the existing PHP admin notices into Gutenberg notices).

jsmoriss commented 6 years ago

Is createWarningNotice() unable to handle HTML? I'm getting an "TypeError: e.replace is not a function" error...

var noticeMsgHtml = data[noticeType][noticeNum];
var noticeElement = wp.element.createElement( 'div' );

noticeElement.innerHTML = noticeMsgHtml;

createWarningNotice( noticeElement );

Thanks,

js.

adamsilverstein commented 6 years ago

@jsmoriss you can try following this pattern from guternberg internals: https://github.com/WordPress/gutenberg/blob/ebe5dd4dfb64bb949dac6f3aabb13cc122497dac/editor/store/effects.js#L172-L179

mtias commented 5 years ago

Closing in favor of https://github.com/WordPress/gutenberg/issues/6388 proposal.

rgomezp commented 5 years ago

I have yet to find good documentation on how to do notices in Gutenberg. The best I could find was this one: https://github.com/WordPress/gutenberg/tree/master/docs/designers-developers/developers/tutorials/notices

However, it does not really describe how best to display a notice based on data from an asynchronous HTTP call. I want to grab data from the call, pass it to Javascript somehow (using wp_enqueue_script perhaps?), and get the notice to display with my custom content.

main.php:

$response = wp_remote_post($url, $request);
$status = $response['response']['code']
if($status==200) {
    // display notice 1
} else {
   // display notice 2
}

Any ideas on how best to do this? Thanks

danielbachhuber commented 5 years ago

I want to grab data from the call, pass it to Javascript somehow (using wp_enqueue_script perhaps?), and get the notice to display with my custom content.

We did exactly this in our implementation.

Here's where we load the data into the window variable:

/**
 * Registers data we need client-side as a part of the initial page load.
 */
public static function action_enqueue_block_editor_assets() {
    $blocks_data = array(
        'editorNotices' => array(),
    );
    $post_id     = self::get_current_post_id();
    if ( $post_id ) {
        $blocks_data['editorNotices'] = Editor::get_converter_messages( $post_id );
    }
    wp_localize_script( 'tasty-recipes-block-editor', 'tastyRecipesBlockEditor', $blocks_data );
}

And here's where we create the notices client-side:

/**
 * WordPress dependencies
 */
const {
    dispatch,
} = wp.data;

/**
 * Fetches existing notice data and generates the corresponding notice.
 */
export default function generateNotices() {
    const { createNotice } = dispatch( 'core/notices' );
    window.tastyRecipesBlockEditor.editorNotices.forEach( ( notice ) => {
        createNotice( notice.type, notice.content, {
            isDismissible: notice.dismissible || false,
            actions: notice.actions || null,
        } );
    } );
}
rgomezp commented 5 years ago

@danielbachhuber Thanks for the response Daniel. However, I didn't quite understand what will invoke the generateNotices function? I would think it shouldn't run until AFTER the data is available in the window variable (e.g: receives some sort of trigger from server as opposed to "as part of initial page load^^"). Also, should I add the javascript using wp_enqueue_script?

danielbachhuber commented 5 years ago

However, I didn't quite understand what will invoke the generateNotices function? I would think it shouldn't run until AFTER the data is available in the window variable (e.g: receives some sort of trigger from server as opposed to "as part of initial page load^^"). Also, should I add the javascript using wp_enqueue_script?

Yes. It's ES6 so you'll need to hook up Webpack to transpile it down. You'll also need to set up wp_enqueue_script to enqueue it.

Here's the full starting point on using JavaScript: https://wordpress.org/gutenberg/handbook/designers-developers/developers/tutorials/javascript/

davidfcarr commented 5 years ago

@danielbachhuber I've had a similar challenge about finding a good tutorial on this, and I appreciate your code sample. One thing I don't understand is whether the code you're showing tells Gutenberg when the notice should be displayed.

My challenge is creating a notice for a specific post type that will be displayed when the post is saved. Actually, the display of this notice would be conditional -- it should only be shown if the post contains metadata marking it as a template. I can figure out the application logic, but I can't figure out how to

You've explained how to create the notice, but I don't see how it's connected to something like a publish or update event.

I'm far enough into learning Gutenberg that I've created a number of custom blocks, but certain aspects such as notifications, modals remain mysteries. I've managed to do a few things with the wp.data state storage system, but that also remains a tough one for me to understand.

Any further clues about notices would be appreciated.

danielbachhuber commented 5 years ago

You've explained how to create the notice, but I don't see how it's connected to something like a publish or update event.

Right — it's just connected to the page load event at the moment. Off the top of my head, I'm not sure of how to hook into a "Save Post" event. You'd need to do some research to sort that out.

davidfcarr commented 5 years ago

Thanks. At least that tells me it's not obvious.

davidfcarr commented 5 years ago

@danielbachhuber @rgomezp I've made progress figuring out how to display a notice triggered by an event such as post save. The routine below is loosely based on code Gutenberg uses internally to save metabox content on post save.

In my use case, certain posts of the type rsvpmaker are used as templates for specific events. The RSVPMaker plugin needs to display a notice prompting the user who has updated a template to click through to another screen if they want to create or update events based on that template. The url for the admin page where you do that is localized under the rsvpmaker_json variable. On the PHP side, we test that the post is of type rsvpmaker before outputting that variable.

My Gutenberg code tests whether the post is in the isSavingPost state but not an autosave.

const { subscribe } = wp.data;
if((typeof rsvpmaker_json !== 'undefined' ) && rsvpmaker_json.projected_url) {
        let wasSavingPost = wp.data.select( 'core/editor' ).isSavingPost();
        let wasAutosavingPost = wp.data.select( 'core/editor' ).isAutosavingPost();
        let wasPreviewingPost = wp.data.select( 'core/editor' ).isPreviewingPost();
        // determine whether to show notice
        subscribe( () => {
            const isSavingPost = wp.data.select( 'core/editor' ).isSavingPost();
            const isAutosavingPost = wp.data.select( 'core/editor' ).isAutosavingPost();
            const isPreviewingPost = wp.data.select( 'core/editor' ).isPreviewingPost();
            const hasActiveMetaBoxes = wp.data.select( 'core/edit-post' ).hasMetaBoxes();
            // Save metaboxes on save completion, except for autosaves that are not a post preview.
            const shouldTriggerTemplateNotice = (
                    ( wasSavingPost && ! isSavingPost && ! wasAutosavingPost ) ||
                    ( wasAutosavingPost && wasPreviewingPost && ! isPreviewingPost )
                );
            // Save current state for next inspection.
            wasSavingPost = isSavingPost;
            wasAutosavingPost = isAutosavingPost;
            wasPreviewingPost = isPreviewingPost;
            if ( shouldTriggerTemplateNotice ) {
    wp.data.dispatch('core/notices').createNotice(
        'info', // Can be one of: success, info, warning, error.
        __('After updating this template, click'), // Text string to display.
        {
            isDismissible: true, // Whether the user can dismiss the notice.
            // Any actions the user can perform.
            actions: [
                {
                    url: rsvpmaker_json.projected_url,
                    label: __('create / update events')
                }
            ]
        }
    );
            }
            /* placeholder for logic to remove notice
            else {
                console.log('remove notice');
            }
            */
} );    
}

This works pretty well. One lingering issue is that if you save the post multiple times, the notice gets output multiple times. If you can suggest a way of preventing that from happening, I'd like to hear it.

rgomezp commented 5 years ago

@davidfcarr @danielbachhuber , I've been wondering the same thing. I get duplicate notices (especially after release of WP 5.1.1 for some reason) and would easily be able to fix this if I could just replace one notice with the other so the latest is the only one to show.

Thanks

davidfcarr commented 5 years ago

Here's the current code I'm using, which eliminated the issue of the redundant messages being displayed. Adding an ID parameter to the notification prevents it from being added repeatedly. This version also uses multiple action parameters to present two alternatives with the word "or" in between.

This appears as an additional message after the default "Post saved" message. Ideally, I might want to either delete or modify that notification. I know there is a remove notification command, but I'm not sure how to determine the ID of that default notification.

if((typeof rsvpmaker_json !== 'undefined' ) && rsvpmaker_json.projected_url) {
let wasSavingPost = wp.data.select( 'core/editor' ).isSavingPost();
let wasAutosavingPost = wp.data.select( 'core/editor' ).isAutosavingPost();
let wasPreviewingPost = wp.data.select( 'core/editor' ).isPreviewingPost();
// determine whether to show notice
subscribe( () => {
const isSavingPost = wp.data.select( 'core/editor' ).isSavingPost();
const isAutosavingPost = wp.data.select( 'core/editor' ).isAutosavingPost();
const isPreviewingPost = wp.data.select( 'core/editor' ).isPreviewingPost();
const hasActiveMetaBoxes = wp.data.select( 'core/edit-post' ).hasMetaBoxes();
 // trigger on save completion, except for autosaves that are not a post preview.
 const shouldTriggerTemplateNotice = (
 ( wasSavingPost && ! isSavingPost && ! wasAutosavingPost ) \|\|
 ( wasAutosavingPost && wasPreviewingPost && ! isPreviewingPost )
 );
 // Save current state for next inspection.
wasSavingPost = isSavingPost;
wasAutosavingPost = isAutosavingPost;
wasPreviewingPost = isPreviewingPost;
if ( shouldTriggerTemplateNotice ) {
var newurl = rsvpmaker_json.projected_url.replace('template_list','setup');
wp.data.dispatch('core/notices').createNotice(
'info', // Can be one of: success, info, warning, error.
__('After updating this template, click'), // Text string to display.
{
id: 'rsvptemplateupdate', //assigning an ID prevents the notice from being added repeatedly
isDismissible: true, // Whether the user can dismiss the notice.
// Any actions the user can perform.
actions: [
{
url: newurl,
label: __('New Event based on template'),
 },
 {
 label: ' or ',
 },
 {
 url: rsvpmaker_json.projected_url,
 label: __('Create / Update events'),
  },
  ]
  }
  );
  }
  } );
  }
davidfcarr commented 5 years ago

Here's what that looks like

rsvp-notification