From 82192eea4c2b1969ccdafc309c1d358d5568e2c5 Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Wed, 20 Nov 2019 11:01:00 -0500 Subject: [PATCH] Manage a Pledge: Enable adding/removing contributors from wp-admin. (#99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This updates the display of contributors into a table view, and adds the ability to add and remove contributors to existing pledges. The display has been refactored to use JS templates & JSON contributor data– the data is output onto the page when loaded from the server, and rendered when the page finishes loading. Adding & removing contributors now submits to an admin-ajax.php endpoint, which, if successful, return the new list of contributors. This ensures the display is always up to date. Fixes #3 --- plugins/wporg-5ftf/assets/css/admin.css | 53 ++++++- plugins/wporg-5ftf/assets/js/admin.js | 147 ++++++++++++++++++ plugins/wporg-5ftf/includes/contributor.php | 41 ++++- plugins/wporg-5ftf/includes/endpoints.php | 79 ++++++++++ plugins/wporg-5ftf/includes/pledge-form.php | 3 +- plugins/wporg-5ftf/includes/pledge-meta.php | 29 ++-- plugins/wporg-5ftf/index.php | 1 + .../views/inputs-pledge-contributors.php | 4 +- .../views/inputs-pledge-org-email.php | 12 +- .../wporg-5ftf/views/manage-contributors.php | 141 +++++++++++------ 10 files changed, 437 insertions(+), 73 deletions(-) create mode 100644 plugins/wporg-5ftf/assets/js/admin.js create mode 100644 plugins/wporg-5ftf/includes/endpoints.php diff --git a/plugins/wporg-5ftf/assets/css/admin.css b/plugins/wporg-5ftf/assets/css/admin.css index 1298cca..7e545e5 100755 --- a/plugins/wporg-5ftf/assets/css/admin.css +++ b/plugins/wporg-5ftf/assets/css/admin.css @@ -7,17 +7,17 @@ display: inline-block; } -.pledge-form .form-field input[type=text], -.pledge-form .form-field input[type=url], -.pledge-form .form-field input[type=number], -.pledge-form .form-field input[type=email], +.pledge-form .form-field input[type="text"], +.pledge-form .form-field input[type="url"], +.pledge-form .form-field input[type="number"], +.pledge-form .form-field input[type="email"], .pledge-form .form-field textarea { display: block; width: 100%; padding: 8px; } -.pledge-form .form-field input[type=number] { +.pledge-form .form-field input[type="number"] { max-width: 10em; height: auto; } @@ -32,6 +32,45 @@ line-height: 1; } -.pledge-form .form-field__agree label { - margin-bottom: 0; +.pledge-form .form-field .email-status { + font-size: 1.1em; +} + +.pledge-form .email-status.is-confirmed { + color: #297531; +} + +.pledge-form .email-status.is-unconfirmed { + color: #c92c2c; +} + +.pledge-form .contributor-list-heading { + margin: 1rem 0; +} + +.pledge-form .contributor-list { + margin-bottom: 1.5rem; +} + +.pledge-contributors.pledge-status__draft .resend-confirm { + display: none; +} + +.contributor-list th, +.contributor-list td, +.contributor-list th *, +.contributor-list td * { + vertical-align: middle; +} + +.contributor-list .avatar { + margin-right: 8px; +} + +.contributor-list .button-link-delete { + text-decoration: none; +} + +.contributor-list .button-link-delete .dashicons { + margin-top: -2px; } diff --git a/plugins/wporg-5ftf/assets/js/admin.js b/plugins/wporg-5ftf/assets/js/admin.js new file mode 100644 index 0000000..4d83add --- /dev/null +++ b/plugins/wporg-5ftf/assets/js/admin.js @@ -0,0 +1,147 @@ +/* global ajaxurl, FiveForTheFuture, fftfContributors, jQuery */ +/* eslint no-alert: "off" */ +jQuery( document ).ready( function( $ ) { + /** + * Render the contributor lists using the contributors template into the pledge-contributors container. This + * uses `_renderContributors` to render a list of contributors per status (published, pending). + * + * @param {Object} contributors - An object listing all contributors on a pledge. + * @param {Object[]} contributors.publish - The list of published/confirmed contributors. + * @param {Object[]} contributors.pending - The list of pending/unconfirmed contributors. + * @param {Object} container - The parent container for this section. + */ + function render( contributors, container ) { + const listContainer = container.querySelector( '.pledge-contributors' ); + const template = wp.template( '5ftf-contributor-lists' ); + const data = { + publish: _renderContributors( contributors.publish ), + pending: _renderContributors( contributors.pending ), + }; + $( listContainer ).html( template( data ) ); + } + + /** + * Render a given contributor list using the contributor template. + * + * @param {Object[]} contributors - An array of contributor data objects. + * @return {string} An HTML string of contributors. + */ + function _renderContributors( contributors ) { + if ( ! contributors ) { + return []; + } + const template = wp.template( '5ftf-contributor' ); + return contributors.map( template ).join( '' ); + } + + /** + * The default callback for AJAX actions. + * + * @param {Object} response - An array of contributor data objects. + * @param {string} response.message - An optional message to display to the user. + * @param {Object[]} response.contributors - The new list of contributors. + */ + function defaultCallback( response ) { + if ( response.message ) { + alert( response.message ); + } + if ( response.contributors ) { + render( response.contributors, container ); + } + } + + /** + * Send an ajax request using the `manage-contributors` action. This function also automatically adds the + * nonce, which should be defined in the global FiveForTheFuture variable. + * + * @param {Object} data - A list of data to send to the endpoint. + * @param {Function} callback - A function to be called when the request completes. + */ + function sendAjaxRequest( data, callback ) { + if ( ! callback ) { + callback = defaultCallback; + } + $.ajax( { + type: 'POST', + url: ajaxurl, + data: Object.assign( { + action: 'manage-contributors', + pledge_id: FiveForTheFuture.pledgeId, + _ajax_nonce: FiveForTheFuture.manageNonce, + }, data ), + success: callback, + dataType: 'json', + } ); + } + + /** + * Send off the AJAX request with contributors pulled from the contributor text field. + */ + function _addContributors() { + const contribs = $( '#5ftf-pledge-contributors' ).val(); + if ( ! contribs.length ) { + return; + } + + sendAjaxRequest( { + contributors: contribs, + manage_action: 'add-contributor', + }, function( response ) { + if ( ! response.success ) { + const $message = $( '
' ) + .attr( 'id', 'add-contrib-message' ) + .addClass( 'notice notice-error notice-alt' ) + .append( $( '

' ).text( response.message ) ); + + $( '#add-contrib-message' ).replaceWith( $message ); + } else if ( response.contributors ) { + render( response.contributors, container ); + $( '#5ftf-pledge-contributors' ).val( '' ); + } + } ); + } + + // Initialize. + const container = document.getElementById( '5ftf-contributors' ); + render( fftfContributors, container ); + + // Remove Contributor button action. + $( container ).on( 'click', '[data-action="remove-contributor"]', function( event ) { + event.preventDefault(); + + const confirmMsg = event.currentTarget.dataset.confirm; + if ( confirmMsg && confirm( confirmMsg ) ) { + const data = event.currentTarget.dataset; + + sendAjaxRequest( { + contributor_id: data.contributorPost || 0, + manage_action: data.action || '', + } ); + } + } ); + + // Resend Contributor Confirmation button action. + $( container ).on( 'click', '[data-action="resend-contributor-confirmation"]', function( event ) { + event.preventDefault(); + const data = event.currentTarget.dataset; + + sendAjaxRequest( { + contributor_id: data.contributorPost || 0, + manage_action: data.action || '', + } ); + } ); + + // Add Contributor button action. + $( container ).on( 'click', '[data-action="add-contributor"]', function( event ) { + event.preventDefault(); + _addContributors(); + } ); + + // Prevent "enter" in the contributor field from submitting the whole post form. + $( container ).on( 'keydown', '#5ftf-pledge-contributors', function( event ) { + if ( 13 === event.which ) { + event.preventDefault(); + _addContributors(); + } + } ); +} ); diff --git a/plugins/wporg-5ftf/includes/contributor.php b/plugins/wporg-5ftf/includes/contributor.php index a7eacae..27d681f 100644 --- a/plugins/wporg-5ftf/includes/contributor.php +++ b/plugins/wporg-5ftf/includes/contributor.php @@ -135,7 +135,7 @@ function populate_list_table_columns( $column, $post_id ) { * @param int $pledge_id The post ID of the pledge. * @param array $contributors Array of contributor wporg usernames. * - * @return void + * @return array List of the new contributor post IDs, mapped from username => ID. */ function add_pledge_contributors( $pledge_id, $contributors ) { $results = array(); @@ -162,6 +162,8 @@ function add_pledge_contributors( $pledge_id, $contributors ) { * or an error code on failure. */ do_action( FiveForTheFuture\PREFIX . '_add_pledge_contributors', $pledge_id, $contributors, $results ); + + return $results; } /** @@ -238,6 +240,43 @@ function get_pledge_contributors( $pledge_id, $status = 'publish', $contributor_ return $posts; } +/** + * Get the contributor posts in the format used for the JS templates. + * + * @param int $pledge_id The post ID of the pledge. + * + * @return array An array of contributor data, ready to be used in the JS templates. + */ +function get_pledge_contributors_data( $pledge_id ) { + $contrib_data = array(); + $contributors = get_pledge_contributors( $pledge_id, 'all' ); + + foreach ( $contributors as $contributor_status => $group ) { + $contrib_data[ $contributor_status ] = array_map( + function( $contributor_post ) use ( $contributor_status, $pledge_id ) { + $name = $contributor_post->post_title; + $contributor = get_user_by( 'login', $name ); + + return [ + 'pledgeId' => $pledge_id, + 'contributorId' => $contributor_post->ID, + 'status' => $contributor_status, + 'avatar' => get_avatar( $contributor, 32 ), + // @todo Add full name, from `$contributor`? + 'name' => $name, + 'displayName' => $contributor->display_name, + 'publishDate' => get_the_date( '', $contributor_post ), + 'resendLabel' => __( 'Resend Confirmation', 'wporg' ), + 'removeConfirm' => sprintf( __( 'Remove %s from this pledge?', 'wporg-5ftf' ), $name ), + 'removeLabel' => sprintf( __( 'Remove %s', 'wporg' ), $name ), + ]; + }, + $group + ); + } + return $contrib_data; +} + /** * Get the user objects that correspond with pledge contributor posts. * diff --git a/plugins/wporg-5ftf/includes/endpoints.php b/plugins/wporg-5ftf/includes/endpoints.php new file mode 100644 index 0000000..70b4c70 --- /dev/null +++ b/plugins/wporg-5ftf/includes/endpoints.php @@ -0,0 +1,79 @@ + false, + 'message' => $authenticated->get_error_message(), + ] ) ); + } + + switch ( $action ) { + case 'resend-contributor-confirmation': + $contribution = get_post( $contributor_id ); + Email\send_contributor_confirmation_emails( $pledge_id, $contributor_id ); + wp_die( wp_json_encode( [ + 'success' => true, + 'message' => sprintf( __( 'Confirmation email sent to %s.', 'wporg-5ftf' ), $contribution->post_title ), + ] ) ); + break; + + case 'remove-contributor': + // Trash contributor. + Contributor\remove_contributor( $contributor_id ); + wp_die( wp_json_encode( [ + 'success' => true, + 'contributors' => Contributor\get_pledge_contributors_data( $pledge_id ), + ] ) ); + break; + + case 'add-contributor': + $pledge = get_post( $pledge_id ); + $new_contributors = PledgeForm\parse_contributors( $_POST['contributors'] ); + if ( is_wp_error( $new_contributors ) ) { + wp_die( wp_json_encode( [ + 'success' => false, + 'message' => $new_contributors->get_error_message(), + ] ) ); + } + $contributor_ids = Contributor\add_pledge_contributors( $pledge_id, $new_contributors ); + if ( 'publish' === $pledge->post_status ) { + foreach ( $contributor_ids as $contributor_id ) { + Email\send_contributor_confirmation_emails( $pledge_id, $contributor_id ); + } + } + + // Fetch all contributors, now that the new ones have been added. + $contributors = Contributor\get_pledge_contributors_data( $pledge_id ); + + wp_die( wp_json_encode( [ + 'success' => true, + 'contributors' => $contributors, + ] ) ); + break; + } + + // No matching action, we can just exit. + wp_die(); +} diff --git a/plugins/wporg-5ftf/includes/pledge-form.php b/plugins/wporg-5ftf/includes/pledge-form.php index c5eca80..b510fcd 100755 --- a/plugins/wporg-5ftf/includes/pledge-form.php +++ b/plugins/wporg-5ftf/includes/pledge-form.php @@ -155,7 +155,8 @@ function render_form_manage() { $updated = false; // @todo Get pledge ID from somewhere. - $data = PledgeMeta\get_pledge_meta(); + $data = PledgeMeta\get_pledge_meta(); + $is_manage = true; if ( 'Update Pledge' === $action ) { $processed = process_form_manage(); diff --git a/plugins/wporg-5ftf/includes/pledge-meta.php b/plugins/wporg-5ftf/includes/pledge-meta.php index 4a8b53f..1e9d0d8 100755 --- a/plugins/wporg-5ftf/includes/pledge-meta.php +++ b/plugins/wporg-5ftf/includes/pledge-meta.php @@ -202,14 +202,15 @@ function add_meta_boxes() { * @param array $box */ function render_meta_boxes( $pledge, $box ) { - $readonly = ! current_user_can( 'edit_page', $pledge->ID ); + $readonly = ! current_user_can( 'edit_page', $pledge->ID ); + $is_manage = true; $data = array(); foreach ( get_pledge_meta_config() as $key => $config ) { $data[ $key ] = get_post_meta( $pledge->ID, META_PREFIX . $key, $config['single'] ); } - $contributors = Contributor\get_pledge_contributors( $pledge->ID, 'all' ); + $contributors = Contributor\get_pledge_contributors_data( $pledge->ID ); echo '

'; @@ -279,13 +280,6 @@ function save_pledge( $pledge_id, $pledge ) { get_page_by_path( 'for-organizations' )->ID ); } - - if ( filter_input( INPUT_POST, 'resend-contributor-confirmation' ) ) { - Email\send_contributor_confirmation_emails( - $pledge_id, - filter_input( INPUT_GET, 'resend-contributor-id', FILTER_VALIDATE_INT ) - ); - } } /** @@ -485,8 +479,25 @@ function enqueue_assets() { $ver = filemtime( FiveForTheFuture\PATH . '/assets/css/admin.css' ); wp_register_style( '5ftf-admin', plugins_url( 'assets/css/admin.css', __DIR__ ), [], $ver ); + $ver = filemtime( FiveForTheFuture\PATH . '/assets/js/admin.js' ); + wp_register_script( '5ftf-admin', plugins_url( 'assets/js/admin.js', __DIR__ ), [ 'jquery', 'wp-util' ], $ver ); + + $script_data = [ + 'pledgeId' => get_the_ID(), + 'manageNonce' => wp_create_nonce( 'manage-contributors' ), + ]; + wp_add_inline_script( + '5ftf-admin', + sprintf( + 'var FiveForTheFuture = JSON.parse( decodeURIComponent( \'%s\' ) );', + rawurlencode( wp_json_encode( $script_data ) ) + ), + 'before' + ); + $current_page = get_current_screen(); if ( Pledge\CPT_ID === $current_page->id ) { wp_enqueue_style( '5ftf-admin' ); + wp_enqueue_script( '5ftf-admin' ); } } diff --git a/plugins/wporg-5ftf/index.php b/plugins/wporg-5ftf/index.php index 070ed41..c165f3b 100755 --- a/plugins/wporg-5ftf/index.php +++ b/plugins/wporg-5ftf/index.php @@ -32,6 +32,7 @@ function load() { require_once get_includes_path() . 'pledge-meta.php'; require_once get_includes_path() . 'pledge-form.php'; require_once get_includes_path() . 'xprofile.php'; + require_once get_includes_path() . 'endpoints.php'; require_once get_includes_path() . 'miscellaneous.php'; // The logger expects things like `$_POST` which aren't set during unit tests. diff --git a/plugins/wporg-5ftf/views/inputs-pledge-contributors.php b/plugins/wporg-5ftf/views/inputs-pledge-contributors.php index 12c8b83..c2eedcc 100644 --- a/plugins/wporg-5ftf/views/inputs-pledge-contributors.php +++ b/plugins/wporg-5ftf/views/inputs-pledge-contributors.php @@ -2,7 +2,7 @@ namespace WordPressDotOrg\FiveForTheFuture\View; /** @var array $data */ -/** @var bool $readonly */ +/** @var bool $is_manage */ ?>
@@ -15,7 +15,7 @@ namespace WordPressDotOrg\FiveForTheFuture\View; name="pledge-contributors" placeholder="sanguine.zoe206, captain-mal, kayleefixesyou" value="" - required + aria-describedby="5ftf-pledge-contributors-help" />

diff --git a/plugins/wporg-5ftf/views/inputs-pledge-org-email.php b/plugins/wporg-5ftf/views/inputs-pledge-org-email.php index 44e455e..1d1461c 100644 --- a/plugins/wporg-5ftf/views/inputs-pledge-org-email.php +++ b/plugins/wporg-5ftf/views/inputs-pledge-org-email.php @@ -31,11 +31,15 @@ use WP_Post; - - +

- - + -
- - $group ) : ?> - -

- -

+ -
    - post_title ); - ?> -
  • - user_email, 32 ); ?> - post_title ); ?> - - post_status ) : ?> - add_query_arg( 'resend-contributor-id', $contributor_post->ID ) ) - ); ?> - -
  • - -
- - - -

- + -