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

Merged
merged 25 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 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
ce217ba
Add missing since, Remove unnecessary check in JavaScript
b1ink0 Nov 15, 2024
fa5d869
Add typing for featureInfo
westonruter Nov 15, 2024
3a333ae
Clarify description of slug arg
westonruter Nov 15, 2024
6250658
Use `plugin_dir_url()` for now.
felixarntz Nov 15, 2024
a8d89a8
Fix capability check on installation/activation endpoint.
felixarntz Nov 15, 2024
c593ae6
Use more appropriate HTTP response codes for different errors.
felixarntz Nov 15, 2024
e0691bf
Add missing require_once to prevent fatal error when installing plugi…
felixarntz 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
98 changes: 59 additions & 39 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 @@ -230,6 +227,24 @@ function perflab_enqueue_features_page_scripts(): void {

// Enqueue the a11y script.
wp_enqueue_script( 'wp-a11y' );
Copy link
Member

Choose a reason for hiding this comment

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

Do we need these enqueue now? as it already added through new script dependancy.

Copy link
Contributor Author

@b1ink0 b1ink0 Nov 12, 2024

Choose a reason for hiding this comment

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

We could remove it as it is not being used anywhere other than the AJAX script as it already added in its dependancy. Should I remove it then?


// 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 ),
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
array( 'wp-i18n', 'wp-a11y' ),
westonruter marked this conversation as resolved.
Show resolved Hide resolved
PERFLAB_VERSION,
true
);

wp_localize_script(
'perflab-plugin-activate-ajax',
'perflabPluginActivateAjaxData',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'perflab_install_activate_plugin_ajax' ),
)
);
westonruter marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -290,6 +305,47 @@ function perflab_install_activate_plugin_callback(): void {
}
add_action( 'admin_action_perflab_install_activate_plugin', 'perflab_install_activate_plugin_callback' );

/**
* Callback for handling installation/activation of plugin using ajax.
*
* @since n.e.x.t
*/
function perflab_install_activate_plugin_ajax_callback(): void {
check_ajax_referer( 'perflab_install_activate_plugin_ajax', '_ajax_nonce' );

require_once ABSPATH . 'wp-admin/includes/plugin.php';
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';

if ( ! isset( $_POST['slug'] ) ) {
wp_send_json_error( __( 'Missing required parameter.', 'performance-lab' ), 400 );
}

$plugin_slug = perflab_sanitize_plugin_slug( wp_unslash( $_POST['slug'] ) );
if ( null === $plugin_slug ) {
wp_send_json_error( __( 'Invalid plugin.', 'performance-lab' ), 400 );
}

// Install and activate the plugin and its dependencies.
$result = perflab_install_and_activate_plugin( $plugin_slug );
if ( $result instanceof WP_Error ) {
wp_send_json_error( $result->get_error_message() );
}

$plugin_settings_url = perflab_get_plugin_settings_url( $plugin_slug );
if ( null !== $plugin_settings_url ) {
wp_send_json_success(
array(
'pluginSettingsURL' => esc_url_raw( $plugin_settings_url ),
)
);
}

wp_send_json_success();
}
add_action( 'wp_ajax_perflab_install_activate_plugin', 'perflab_install_activate_plugin_ajax_callback' );
westonruter marked this conversation as resolved.
Show resolved Hide resolved

/**
* Callback function to handle admin inline style.
*
Expand Down Expand Up @@ -396,42 +452,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
107 changes: 107 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,107 @@
/**
* Handles activation of Performance Features (Plugins) using AJAX.
*/

/* global perflabPluginActivateAjaxData */
( function () {
// @ts-ignore
const { i18n, a11y } = wp;
westonruter marked this conversation as resolved.
Show resolved Hide resolved
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.
westonruter marked this conversation as resolved.
Show resolved Hide resolved
*/
async function handlePluginActivationClick( event ) {
const target = /** @type {HTMLElement} */ ( event.target );

if ( ! target.classList.contains( 'perflab-install-active-plugin' ) ) {
return;
}

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

westonruter marked this conversation as resolved.
Show resolved Hide resolved
target.classList.add( 'updating-message' );
target.textContent = __( 'Activating…', 'performance-lab' );
target.style.pointerEvents = 'none';
target.parentElement.style.cursor = 'not-allowed';
westonruter marked this conversation as resolved.
Show resolved Hide resolved

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

const pluginSlug = target.getAttribute( 'data-plugin-slug' ).trim();
b1ink0 marked this conversation as resolved.
Show resolved Hide resolved

// Send an AJAX POST request to activate the plugin.
try {
const response = await fetch(
// @ts-ignore
perflabPluginActivateAjaxData.ajaxUrl,
westonruter marked this conversation as resolved.
Show resolved Hide resolved
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams( {
action: 'perflab_install_activate_plugin',
slug: pluginSlug,
// @ts-ignore
_ajax_nonce: perflabPluginActivateAjaxData.nonce,
} ),
}
);

const responseData = await response.json();

if ( ! responseData.success ) {
westonruter marked this conversation as resolved.
Show resolved Hide resolved
target.classList.remove( 'updating-message' );
target.textContent = __( 'Activate', 'performance-lab' );
target.style.pointerEvents = '';
target.parentElement.style.cursor = '';
westonruter marked this conversation as resolved.
Show resolved Hide resolved

return;
}

const newButton = document.createElement( 'button' );

newButton.type = 'button';
newButton.className = 'button button-disabled';
newButton.disabled = true;
newButton.textContent = __( 'Active', 'performance-lab' );
target.parentElement.style.cursor = '';

target.parentNode.replaceChild( newButton, target );
westonruter marked this conversation as resolved.
Show resolved Hide resolved

const pluginSettingsURL = responseData?.data?.pluginSettingsURL;

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

if ( pluginSettingsURL && actionButtonList ) {
const listItem = document.createElement( 'li' );
const anchor = document.createElement( 'a' );

anchor.setAttribute( 'href', pluginSettingsURL );
westonruter marked this conversation as resolved.
Show resolved Hide resolved
anchor.textContent = __( 'Settings', 'performance-lab' );

listItem.appendChild( anchor );
actionButtonList.appendChild( listItem );
}
} catch ( error ) {
target.classList.remove( 'updating-message' );
target.textContent = __( 'Activate', 'performance-lab' );
target.style.pointerEvents = '';
target.parentElement.style.cursor = '';
westonruter marked this conversation as resolved.
Show resolved Hide resolved
}
}

westonruter marked this conversation as resolved.
Show resolved Hide resolved
// Attach the event listener.
document.addEventListener( 'click', handlePluginActivationClick );
Copy link
Member

Choose a reason for hiding this comment

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

Why does this add a listener for the entire document instead of adding listeners to the buttons specifically?

I feel like that would be a more reasonable choice, as it avoids having the listener fire for more or less every click on the page. Since the buttons are present in the HTML response from the beginning, I don't see a need for listening on document.

} )();
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 @@ -431,8 +431,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
40 changes: 23 additions & 17 deletions plugins/speculation-rules/load.php
Copy link
Member

Choose a reason for hiding this comment

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

Actually, in #1646 (comment) I just posed this change as a possibility but then recommended against it since it would only account for Speculative Loading. It wouldn't account for other plugins that have delayed bootstrapping. So to address the problem of accessing the settings link after a plugin is activated, I think the best solution is to make another request specifically to an endpoint that exposes the settings link URLs.

Aside: I think the REST API would be better for this instead of admin-ajax.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,24 @@
* @param Closure $load Callback that loads the plugin.
*/
static function ( string $global_var_name, string $version, Closure $load ): void {
if ( ! isset( $GLOBALS[ $global_var_name ] ) ) {
$needs_bootstrap = ! isset( $GLOBALS[ $global_var_name ] );

// Register this copy of the plugin.
if (
// Register this copy if none has been registered yet.
! isset( $GLOBALS[ $global_var_name ]['version'] )
||
// Or register this copy if the version greater than what is currently registered.
version_compare( $version, $GLOBALS[ $global_var_name ]['version'], '>' )
||
// Otherwise, register this copy if it is actually the one installed in the directory for plugins.
rtrim( WP_PLUGIN_DIR, '/' ) === dirname( __DIR__ )
) {
$GLOBALS[ $global_var_name ]['version'] = $version;
$GLOBALS[ $global_var_name ]['load'] = $load;
}

if ( $needs_bootstrap ) {
$bootstrap = static function () use ( $global_var_name ): void {
if (
isset( $GLOBALS[ $global_var_name ]['load'], $GLOBALS[ $global_var_name ]['version'] )
Expand All @@ -45,22 +62,11 @@ static function ( string $global_var_name, string $version, Closure $load ): voi

// Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used
// because it is the first action that fires once the theme is loaded.
add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN );
}

// Register this copy of the plugin.
if (
// Register this copy if none has been registered yet.
! isset( $GLOBALS[ $global_var_name ]['version'] )
||
// Or register this copy if the version greater than what is currently registered.
version_compare( $version, $GLOBALS[ $global_var_name ]['version'], '>' )
||
// Otherwise, register this copy if it is actually the one installed in the directory for plugins.
rtrim( WP_PLUGIN_DIR, '/' ) === dirname( __DIR__ )
) {
$GLOBALS[ $global_var_name ]['version'] = $version;
$GLOBALS[ $global_var_name ]['load'] = $load;
if ( (bool) did_action( 'after_setup_theme' ) ) {
$bootstrap();
} else {
add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN );
}
}
}
)(
Expand Down
Loading