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

Use AJAX for activating features / plugins in Performance Lab #1646

Open
wants to merge 18 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
47f3938
Update print plugin progress indicator script function to support aja…
b1ink0 Nov 7, 2024
9d97f59
Add admin notice dynamically, Update to use async/await
b1ink0 Nov 8, 2024
6f172ad
Add settings URL of plugin in admin notice
b1ink0 Nov 11, 2024
337da6c
Move inline JavaScript to separate file
b1ink0 Nov 11, 2024
58c37d9
Refactor JavaScript code to use jQuery
b1ink0 Nov 11, 2024
a59909d
Update to use IIFE, Remove unnecessary doc comment
b1ink0 Nov 11, 2024
f9faaea
Merge branch 'trunk' into update/use-ajax-for-activate-plugin
b1ink0 Nov 11, 2024
770d462
Merge branch 'WordPress:trunk' into update/use-ajax-for-activate-plugin
b1ink0 Nov 12, 2024
e6710a3
Remove admin notice part, Remove constant, Use plugin_url for JS file…
b1ink0 Nov 12, 2024
c3d3588
Refactor to use vanilla JavaScript
b1ink0 Nov 12, 2024
fdf7e0b
Fix duplicate request on multiple click, Fix settings URL not showing…
b1ink0 Nov 12, 2024
b3ecc98
Remove wp-a11y enqueue, Revert changes to Speculative Loading plugin …
b1ink0 Nov 13, 2024
c713e47
Add REST API endpoints for activating plugin and getting plugin setti…
b1ink0 Nov 13, 2024
1ebe323
Merge branch 'trunk' into update/use-ajax-for-activate-plugin
b1ink0 Nov 13, 2024
c134fe2
Update REST API routes, Add validation to args of REST API request, R…
b1ink0 Nov 14, 2024
369487f
Update REST API routes, Update JavaScript code to use updated REST AP…
b1ink0 Nov 15, 2024
d84e9bd
Merge branch 'trunk' into update/use-ajax-for-activate-plugin
b1ink0 Nov 15, 2024
9bee27e
Fix inline comments and error messages
b1ink0 Nov 15, 2024
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
49 changes: 8 additions & 41 deletions plugins/performance-lab/includes/admin/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ function perflab_load_features_page(): void {

// Handle style for settings page.
add_action( 'admin_head', 'perflab_print_features_page_style' );

// Handle script for settings page.
add_action( 'admin_footer', 'perflab_print_plugin_progress_indicator_script' );
}

/**
Expand Down Expand Up @@ -228,8 +225,14 @@ function perflab_enqueue_features_page_scripts(): void {
wp_enqueue_style( 'thickbox' );
wp_enqueue_script( 'plugin-install' );

// Enqueue the a11y script.
wp_enqueue_script( 'wp-a11y' );
// Enqueue plugin activate AJAX script and localize script data.
wp_enqueue_script(
'perflab-plugin-activate-ajax',
plugins_url( 'includes/admin/plugin-activate-ajax.js', PERFLAB_MAIN_FILE ),
array( 'wp-i18n', 'wp-a11y', 'wp-api-fetch' ),
PERFLAB_VERSION,
true
);
}

/**
Expand Down Expand Up @@ -396,42 +399,6 @@ static function ( $name ) {
}
}

/**
* Callback function that print plugin progress indicator script.
*
* @since 3.1.0
*/
function perflab_print_plugin_progress_indicator_script(): void {
$js_function = <<<JS
function addPluginProgressIndicator( message ) {
document.addEventListener( 'DOMContentLoaded', function () {
document.addEventListener( 'click', function ( event ) {
if (
event.target.classList.contains(
'perflab-install-active-plugin'
)
) {
const target = event.target;
target.classList.add( 'updating-message' );
target.textContent = message;
wp.a11y.speak( message );
}
} );
} );
}
JS;

wp_print_inline_script_tag(
sprintf(
'( %s )( %s );',
$js_function,
wp_json_encode( __( 'Activating...', 'default' ) )
),
array( 'type' => 'module' )
);
}

/**
* Gets the URL to the plugin settings screen if one exists.
*
Expand Down
87 changes: 87 additions & 0 deletions plugins/performance-lab/includes/admin/plugin-activate-ajax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Handles activation of Performance Features (Plugins) using AJAX.
*/

( function () {
// @ts-ignore
const { i18n, a11y, apiFetch } = wp;
const { __ } = i18n;

/**
* Handles click events on elements with the class 'perflab-install-active-plugin'.
*
* This asynchronous function listens for click events on the document and executes
* the provided callback function if triggered.
*
* @param {MouseEvent} event - The click event object that is triggered when the user clicks on the document.
*
* @return {Promise<void>} The asynchronous function returns a promise that resolves to void.
*/
async function handlePluginActivationClick( event ) {
const target = /** @type {HTMLElement} */ ( event.target );

// Prevent the default link behavior.
event.preventDefault();

westonruter marked this conversation as resolved.
Show resolved Hide resolved
if (
target.classList.contains( 'updating-message' ) ||
target.classList.contains( 'disabled' )
) {
return;
}

target.classList.add( 'updating-message' );
target.textContent = __( 'Activating…', 'performance-lab' );

a11y.speak( __( 'Activating…', 'performance-lab' ) );

const pluginSlug = target.dataset.pluginSlug;

try {
// Activate the plugin/feature via the REST API.
await apiFetch( {
path: `/performance-lab/v1/features/${ pluginSlug }:activate`,
method: 'POST',
} );

// Fetch the plugin/feature information via the REST API.
const featureInfo = await apiFetch( {
path: `/performance-lab/v1/features/${ pluginSlug }`,
method: 'GET',
} );

if ( featureInfo?.settingsUrl ) {
const actionButtonList = document.querySelector(
`.plugin-card-${ pluginSlug } .plugin-action-buttons`
);

const listItem = document.createElement( 'li' );
const anchor = document.createElement( 'a' );

anchor.href = featureInfo.settingsUrl;
anchor.textContent = __( 'Settings', 'performance-lab' );

listItem.appendChild( anchor );
actionButtonList.appendChild( listItem );
}

a11y.speak( __( 'Plugin activated.', 'performance-lab' ) );

target.textContent = __( 'Active', 'performance-lab' );
target.classList.remove( 'updating-message' );
target.classList.add( 'disabled' );
} catch ( error ) {
a11y.speak( __( 'Plugin failed to activate.', 'performance-lab' ) );

target.classList.remove( 'updating-message' );
target.textContent = __( 'Activate', 'performance-lab' );
}
}

Copy link
Member

Choose a reason for hiding this comment

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

Per above:

Suggested change
try {
// Fetch the plugin settings URL via the REST API.
const settingsResponse = await apiFetch( {
path: `/performance-lab/v1/plugin-settings-url/${ pluginSlug }`,
method: 'GET',
} );
const actionButtonList = document.querySelector(
`.plugin-card-${ pluginSlug } .plugin-action-buttons`
);
const listItem = document.createElement( 'li' );
const anchor = document.createElement( 'a' );
anchor.href = settingsResponse.pluginSettingsURL;
anchor.textContent = __( 'Settings', 'performance-lab' );
listItem.appendChild( anchor );
actionButtonList.appendChild( listItem );
} catch ( error ) {}

Copy link
Author

@b1ink0 b1ink0 Nov 14, 2024

Choose a reason for hiding this comment

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

Hi, There will be UX issue if settings URL try catch is moved after the changing DOM for the anchor tag / button. As it will become disabled after first request is finished signaling the user that the action is done but then the settings URL fetch request after some time if the plugin has settings URL then the DOM will be modified after some delay.

Also I think we should only send the settings URL request when the activate request is successful.

Should I just move the settings URL try catch inside the activate plugin try catch and handle DOM of anchor tag / button after the settings URL try catch block?

delayed_activation.mp4

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that works.

Copy link
Member

Choose a reason for hiding this comment

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

Also, if the endpoint returns null instead of a 404, then no separate try/catch would be needed.

// Attach the event listeners.
document
.querySelectorAll( '.perflab-install-active-plugin' )
.forEach( ( item ) => {
item.addEventListener( 'click', handlePluginActivationClick );
} );
} )();
3 changes: 2 additions & 1 deletion plugins/performance-lab/includes/admin/plugins.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,9 @@ function perflab_render_plugin_card( array $plugin_data ): void {
);

$action_links[] = sprintf(
'<a class="button perflab-install-active-plugin" href="%s">%s</a>',
'<a class="button perflab-install-active-plugin" href="%s" data-plugin-slug="%s">%s</a>',
esc_url( $url ),
esc_attr( $plugin_data['slug'] ),
esc_html__( 'Activate', 'default' )
);
} else {
Expand Down
167 changes: 167 additions & 0 deletions plugins/performance-lab/includes/admin/rest-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php
/**
* REST API integration for the plugin.
*
* @package performance-lab
* @since n.e.x.t
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* Namespace for performance-lab REST API.
*
* @var string
*/
const PERFLAB_REST_API_NAMESPACE = 'performance-lab/v1';

/**
* Route for activating plugin/feature.
*
* Note the `:activate` art of the endpoint follows Google's guidance in AIP-136 for the use of the POST method in a way
* that does not strictly follow the standard usage.
*
* @link https://google.aip.dev/136
* @var string
*/
const PERFLAB_FEATURES_ACTIVATE_ROUTE = '/features/(?P<slug>[a-z0-9_-]+):activate';

/**
* Route for fetching plugin/feature information.
*
* @var string
*/
const PERFLAB_FEATURES_INFORMATION_ROUTE = '/features/(?P<slug>[a-z0-9_-]+)';

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/**
* Validates whether the provided plugin slug is a valid Performance Lab plugin.
*
* Note that an enum is not being used because additional PHP files have to be required to access the necessary functions,
* and this would not be ideal to do at rest_api_init.
*
* @since n.e.x.t
* @access private
*
* @param string $slug Plugin slug.
* @return bool Whether valid.
*/
function perflab_validate_slug_endpoint_arg( string $slug ): bool {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/load.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/plugins.php';
return in_array( $slug, perflab_get_standalone_plugins(), true );
}

/**
* Registers endpoint for performance-lab REST API.
*
* @since n.e.x.t
* @access private
*/
function perflab_register_endpoint(): void {
register_rest_route(
PERFLAB_REST_API_NAMESPACE,
PERFLAB_FEATURES_ACTIVATE_ROUTE,
array(
'methods' => 'POST',
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Plugin slug of plugin/feature that needs to be activated.', 'performance-lab' ),
'required' => true,
'validate_callback' => 'perflab_validate_slug_endpoint_arg',
),
),
'callback' => 'perflab_handle_feature_activation',
'permission_callback' => static function () {
if ( current_user_can( 'install_plugins' ) ) {
return true;
}

return new WP_Error( 'cannot_install_plugin', __( 'Sorry, you are not allowed to install plugins on this site.', 'performance-lab' ) );
},
)
);

register_rest_route(
PERFLAB_REST_API_NAMESPACE,
PERFLAB_FEATURES_INFORMATION_ROUTE,
array(
'methods' => 'GET',
'args' => array(
'slug' => array(
'type' => 'string',
'description' => __( 'Plugin slug of plugin/feature whose information is needed.', 'performance-lab' ),
'required' => true,
'validate_callback' => 'perflab_validate_slug_endpoint_arg',
),
),
'callback' => 'perflab_handle_get_feature_information',
'permission_callback' => static function () {
if ( current_user_can( 'manage_options' ) ) {
return true;
}

return new WP_Error( 'cannot_access_plugin_settings_url', __( 'Sorry, you are not allowed to access plugin/feature information on this site.', 'performance-lab' ) );
},
)
);
}
add_action( 'rest_api_init', 'perflab_register_endpoint' );

/**
* Validates whether the provided plugin slug is a valid Performance Lab plugin.
*
* Note that an enum is not being used because additional PHP files have to be required to access the necessary functions,
* and this would not be ideal to do at rest_api_init.
*
* @since n.e.x.t
* @access private
*
* @param string $slug Plugin slug.
* @return bool Whether valid.
*/
function perflab_validate_slug_endpoint_arg( string $slug ): bool {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/load.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/plugins.php';
return in_array( $slug, perflab_get_standalone_plugins(), true );
}

/**
* Handles REST API request to activate plugin/feature.
*
* @since n.e.x.t
* @access private
*
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
*
* @param WP_REST_Request $request Request.
* @return WP_REST_Response|WP_Error Response.
*/
function perflab_handle_feature_activation( WP_REST_Request $request ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';

// Install and activate the plugin/feature and its dependencies.
$result = perflab_install_and_activate_plugin( $request['slug'] );
if ( is_wp_error( $result ) ) {
return new WP_Error(
'plugin_activation_failed',
$result->get_error_message(),
array( 'status' => 500 )
);
}

return new WP_REST_Response(
array(
'success' => true,
)
);
}

/**
* Handles REST API request to get plugin/feature information.
*
* @since n.e.x.t
* @access private
*
* @phpstan-param WP_REST_Request<array<string, mixed>> $request
*
* @param WP_REST_Request $request Request.
* @return WP_REST_Response Response.
*/
function perflab_handle_get_feature_information( WP_REST_Request $request ): WP_REST_Response {
$plugin_settings_url = perflab_get_plugin_settings_url( $request['slug'] );

return new WP_REST_Response(
array(
'slug' => $request['slug'],
'settingsUrl' => $plugin_settings_url,
)
);
}
3 changes: 3 additions & 0 deletions plugins/performance-lab/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,6 @@ function perflab_cleanup_option(): void {
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/server-timing.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/plugins.php';
}

// Load REST API.
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/rest-api.php';