From 8bd193f34f40fbe70ccaed5fe7076ed62f13f005 Mon Sep 17 00:00:00 2001 From: Derrick Koo Date: Mon, 20 Jul 2020 17:20:16 -0600 Subject: [PATCH 01/17] feat: enable public newsletter rewrites Adds a meta option to newsletter posts that allows specific newsletters to be publicly viewable on the site front-end. Currently uses the standard post template. --- includes/class-newspack-newsletters.php | 92 +++++++++++++++++-- ...rface-newspack-newsletters-wp-hookable.php | 5 +- .../class-newspack-newsletters-mailchimp.php | 64 +++++++------ src/components/send-button/index.js | 63 +++++++++++-- src/newsletter-editor/index.js | 2 + 5 files changed, 174 insertions(+), 52 deletions(-) diff --git a/includes/class-newspack-newsletters.php b/includes/class-newspack-newsletters.php index 04fa74fc8..558ffe618 100644 --- a/includes/class-newspack-newsletters.php +++ b/includes/class-newspack-newsletters.php @@ -68,6 +68,9 @@ public function __construct() { add_action( 'rest_api_init', [ __CLASS__, 'rest_api_init' ] ); add_action( 'default_title', [ __CLASS__, 'default_title' ], 10, 2 ); add_filter( 'display_post_states', [ __CLASS__, 'display_post_states' ], 10, 2 ); + add_action( 'template_redirect', [ __CLASS__, 'maybe_display_public_post' ] ); + add_filter( 'post_row_actions', [ __CLASS__, 'display_view_or_preview_link_in_admin' ] ); + add_action( 'admin_print_scripts', [ __CLASS__, 'maybe_disable_autosave' ], 10, 3 ); switch ( self::service_provider() ) { case 'mailchimp': @@ -156,15 +159,23 @@ public static function register_meta() { 'auth_callback' => '__return_true', ] ); + \register_meta( + 'post', + 'is_public', + [ + 'object_subtype' => self::NEWSPACK_NEWSLETTERS_CPT, + 'show_in_rest' => true, + 'type' => 'boolean', + 'single' => true, + 'auth_callback' => '__return_true', + ] + ); } /** * Register the custom post type. */ public static function register_cpt() { - if ( ! current_user_can( 'edit_others_posts' ) ) { - return; - } $labels = [ 'name' => _x( 'Newsletters', 'post type general name', 'newspack-newsletters' ), 'singular_name' => _x( 'Newsletter', 'post type singular name', 'newspack-newsletters' ), @@ -183,13 +194,18 @@ public static function register_cpt() { ]; $cpt_args = [ - 'labels' => $labels, - 'public' => false, - 'show_ui' => true, - 'show_in_rest' => true, - 'supports' => [ 'editor', 'title', 'custom-fields' ], - 'taxonomies' => [], - 'menu_icon' => 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJzNC40OCAxMCAxMCAxMGg1di0yaC01Yy00LjM0IDAtOC0zLjY2LTgtOHMzLjY2LTggOC04IDggMy42NiA4IDh2MS40M2MwIC43OS0uNzEgMS41Ny0xLjUgMS41N3MtMS41LS43OC0xLjUtMS41N1YxMmMwLTIuNzYtMi4yNC01LTUtNXMtNSAyLjI0LTUgNSAyLjI0IDUgNSA1YzEuMzggMCAyLjY0LS41NiAzLjU0LTEuNDcuNjUuODkgMS43NyAxLjQ3IDIuOTYgMS40NyAxLjk3IDAgMy41LTEuNiAzLjUtMy41N1YxMmMwLTUuNTItNC40OC0xMC0xMC0xMHptMCAxM2MtMS42NiAwLTMtMS4zNC0zLTNzMS4zNC0zIDMtMyAzIDEuMzQgMyAzLTEuMzQgMy0zIDN6IiBmaWxsPSJ3aGl0ZSIvPjwvc3ZnPgo=', + 'labels' => $labels, + 'public' => true, + 'public_queryable' => true, + 'query_var' => true, + 'show_ui' => true, + 'show_in_rest' => true, + 'supports' => [ 'editor', 'title', 'custom-fields' ], + 'taxonomies' => [], + 'menu_icon' => 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJzNC40OCAxMCAxMCAxMGg1di0yaC01Yy00LjM0IDAtOC0zLjY2LTgtOHMzLjY2LTggOC04IDggMy42NiA4IDh2MS40M2MwIC43OS0uNzEgMS41Ny0xLjUgMS41N3MtMS41LS43OC0xLjUtMS41N1YxMmMwLTIuNzYtMi4yNC01LTUtNXMtNSAyLjI0LTUgNSAyLjI0IDUgNSA1YzEuMzggMCAyLjY0LS41NiAzLjU0LTEuNDcuNjUuODkgMS43NyAxLjQ3IDIuOTYgMS40NyAxLjk3IDAgMy41LTEuNiAzLjUtMy41N1YxMmMwLTUuNTItNC40OC0xMC0xMC0xMHptMCAxM2MtMS42NiAwLTMtMS4zNC0zLTNzMS4zNC0zIDMtMyAzIDEuMzQgMyAzLTEuMzQgMy0zIDN6IiBmaWxsPSJ3aGl0ZSIvPjwvc3ZnPgo=', + 'rewrite' => [ + 'slug' => 'newsletter', + ], ]; \register_post_type( self::NEWSPACK_NEWSLETTERS_CPT, $cpt_args ); } @@ -208,6 +224,7 @@ public static function display_post_states( $post_states, $post ) { $post_status = get_post_status_object( $post->post_status ); $is_sent = 'publish' === $post_status->name; + $is_public = get_post_meta( $post->ID, 'is_public', true ); if ( $is_sent ) { $sent_date = get_the_time( 'U', $post ); @@ -222,11 +239,66 @@ public static function display_post_states( $post_states, $post ) { /* translators: Absolute time stamp of sent/published date */ $post_states[ $post_status->name ] = sprintf( __( 'Sent %1$s', 'newspack-newsletters' ), get_the_time( get_option( 'date_format' ), $post ) ); } + + if ( $is_public ) { + $post_states[ $post_status->name ] .= __( ' | Published as a page', 'newspack-newsletters' ); + } } return $post_states; } + /** + * Decide whether this newsletter should be publicly viewable as a page. + */ + public static function maybe_display_public_post() { + if ( self::NEWSPACK_NEWSLETTERS_CPT !== get_post_type() || current_user_can( 'edit_others_posts' ) ) { + return; + } + + $is_public = get_post_meta( get_the_ID(), 'is_public', true ); + + // If not marked public, make it a 404 to non-logged-in users. + if ( empty( $is_public ) ) { + global $wp_query; + status_header( 404 ); + nocache_headers(); + include get_query_template( '404' ); + die(); + } + } + + /** + * Make "View" links say "Preview" if the newsletter is not marked as public. + * + * @param array $actions Array of action links to be shown in admin posts list. + * @return array Filtered array of action links. + */ + public static function display_view_or_preview_link_in_admin( $actions ) { + if ( 'publish' !== get_post_status() || self::NEWSPACK_NEWSLETTERS_CPT !== get_post_type() ) { + return $actions; + } + + $is_public = get_post_meta( get_the_ID(), 'is_public', true ); + + if ( empty( $is_public ) && isset( $actions['view'] ) ) { + $actions['view'] = 'Preview'; + } + + return $actions; + } + + /** + * Disable auto-saves after newsletter has been sent. + */ + public static function maybe_disable_autosave() { + if ( 'publish' !== get_post_status() || self::NEWSPACK_NEWSLETTERS_CPT !== get_post_type() ) { + return false; + } + + wp_dequeue_script( 'autosave' ); + } + /** * Add newspack_popups_is_sitewide_default to Popup object. */ diff --git a/includes/service-providers/interface-newspack-newsletters-wp-hookable.php b/includes/service-providers/interface-newspack-newsletters-wp-hookable.php index b1eed8d62..3508a2e95 100644 --- a/includes/service-providers/interface-newspack-newsletters-wp-hookable.php +++ b/includes/service-providers/interface-newspack-newsletters-wp-hookable.php @@ -22,10 +22,11 @@ public function save( $post_id, $post, $update ); /** * Send a campaign. * - * @param integer $post_id Post ID to send. + * @param string $new_status New status of the post. + * @param string $old_status Old status of the post. * @param \WP_POST $post Post to send. */ - public function send( $post_id, $post ); + public function send( $new_status, $old_status, $post ); /** * After Newsletter post is deleted, clean up by deleting corresponding ESP campaign. diff --git a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php index 90aa12a05..4bdc4e737 100644 --- a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php +++ b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php @@ -22,7 +22,7 @@ public function __construct() { $this->controller = new Newspack_Newsletters_Mailchimp_Controller( $this ); add_action( 'save_post_' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, [ $this, 'save' ], 10, 3 ); - add_action( 'publish_' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, [ $this, 'send' ], 10, 2 ); + add_action( 'transition_post_status', [ $this, 'send' ], 10, 3 ); add_action( 'wp_trash_post', [ $this, 'trash' ], 10, 1 ); parent::__construct( $this ); @@ -383,45 +383,43 @@ public function save( $post_id, $post, $update ) { /** * Send a campaign. * - * @param integer $post_id Post ID to send. + * @param string $new_status New status of the post. + * @param string $old_status Old status of the post. * @param WP_POST $post Post to send. */ - public function send( $post_id, $post ) { - if ( ! Newspack_Newsletters::validate_newsletter_id( $post_id ) ) { - return new WP_Error( - 'newspack_newsletters_incorrect_post_type', - __( 'Post is not a Newsletter.', 'newspack-newsletters' ) - ); - } + public function send( $new_status, $old_status, $post ) { + $post_id = $post->ID; - try { - $sync_result = $this->sync( $post ); + if ( 'publish' === $new_status && 'publish' !== $old_status && Newspack_Newsletters::validate_newsletter_id( $post_id ) ) { + try { + $sync_result = $this->sync( $post ); - if ( is_wp_error( $sync_result ) ) { - return $sync_result; - } + if ( is_wp_error( $sync_result ) ) { + return $sync_result; + } - $mc_campaign_id = get_post_meta( $post_id, 'mc_campaign_id', true ); - if ( ! $mc_campaign_id ) { - return new WP_Error( - 'newspack_newsletters_no_campaign_id', - __( 'Mailchimp campaign ID not found.', 'newspack-newsletters' ) - ); - } + $mc_campaign_id = get_post_meta( $post_id, 'mc_campaign_id', true ); + if ( ! $mc_campaign_id ) { + return new WP_Error( + 'newspack_newsletters_no_campaign_id', + __( 'Mailchimp campaign ID not found.', 'newspack-newsletters' ) + ); + } - $mc = new Mailchimp( $this->api_key() ); + $mc = new Mailchimp( $this->api_key() ); - $payload = [ - 'send_type' => 'html', - ]; - $result = $this->validate( - $mc->post( "campaigns/$mc_campaign_id/actions/send", $payload ), - __( 'Error sending campaign.', 'newspack_newsletters' ) - ); - } catch ( Exception $e ) { - $transient = sprintf( 'newspack_newsletters_error_%s_%s', $post->ID, get_current_user_id() ); - set_transient( $transient, $e->getMessage(), 45 ); - return; + $payload = [ + 'send_type' => 'html', + ]; + $result = $this->validate( + $mc->post( "campaigns/$mc_campaign_id/actions/send", $payload ), + __( 'Error sending campaign.', 'newspack_newsletters' ) + ); + } catch ( Exception $e ) { + $transient = sprintf( 'newspack_newsletters_error_%s_%s', $post->ID, get_current_user_id() ); + set_transient( $transient, $e->getMessage(), 45 ); + return; + } } } diff --git a/src/components/send-button/index.js b/src/components/send-button/index.js index 1cdd2c595..e4b97661b 100644 --- a/src/components/send-button/index.js +++ b/src/components/send-button/index.js @@ -4,7 +4,7 @@ import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { Button, Modal, Notice } from '@wordpress/components'; -import { Fragment, useEffect, useState } from '@wordpress/element'; +import { Fragment, useEffect, useRef, useState } from '@wordpress/element'; import { __, sprintf, _n } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; @@ -38,7 +38,9 @@ export default compose( [ isSavingPost, isEditedPostBeingScheduled, } = select( 'core/editor' ); - const { newsletterData = {}, newsletterValidationErrors } = getEditedPostAttribute( 'meta' ); + const { newsletterData = {}, newsletterValidationErrors, is_public } = getEditedPostAttribute( + 'meta' + ); return { isPublishable: forceIsDirty || isEditedPostPublishable(), isSaveable: isEditedPostSaveable(), @@ -49,6 +51,7 @@ export default compose( [ hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ), visibility: getEditedPostVisibility(), newsletterData, + isPublic: is_public, }; } ), ] )( @@ -64,21 +67,46 @@ export default compose( [ hasPublishAction, visibility, newsletterData, + isPublic, } ) => { + // State to handle post-publish changes to Public setting. + const [ isDirty, setIsDirty ] = useState( false ); + const isPublicRef = useRef(); + const prevIsPublic = isPublicRef.current; + + useEffect( () => { + isPublicRef.current = isPublic; + } ); + + // If changing the Public setting post-sending. + useEffect(() => { + if ( undefined !== prevIsPublic && isPublic !== prevIsPublic && 'publish' === status ) { + setIsDirty( true ); + } + }, [ isPublic ]); + const isButtonEnabled = - ( isPublishable || isEditedPostBeingScheduled ) && isSaveable && 'publish' !== status; + ( isPublishable || isEditedPostBeingScheduled ) && + isSaveable && + 'publish' !== status && + ! isSaving; let label; if ( 'publish' === status ) { - label = isSaving - ? __( 'Sending', 'newspack-newsletters' ) - : __( 'Sent', 'newspack-newsletters' ); + if ( isSaving ) label = __( 'Sending', 'newspack-newsletters' ); + else { + label = isPublic + ? __( 'Sent and Published', 'newspack-newsletters' ) + : __( 'Sent', 'newspack-newsletters' ); + } } else if ( 'future' === status ) { // Scheduled to be sent label = __( 'Scheduled', 'newspack-newsletters' ); } else if ( isEditedPostBeingScheduled ) { label = __( 'Schedule sending', 'newspack-newsletters' ); } else { - label = __( 'Send', 'newspack-newsletters' ); + label = isPublic + ? __( 'Send and Publish', 'newspack-newsletters' ) + : __( 'Send', 'newspack-newsletters' ); } let publishStatus; @@ -121,6 +149,27 @@ export default compose( [ const [ modalVisible, setModalVisible ] = useState( false ); + // If we've changed the Public setting post-publish, allow the user to just save the post. + if ( isDirty && 'publish' === publishStatus ) { + return ( + + ); + } + return (