WordPress Hosting

Deberías añadir comandos de tu plugin a la paleta de comandos de WordPress, ni te lo pienses

La paleta de comandos de WordPress es una de esas funcionalidades que los usuarios descubren y ya no sueltan, pero la gracia de verdad está en que tú, como desarrollador de plugins, puedes meter tus propias opciones dentro de ella.

Tus usuarios pulsan Cmd+K o Ctrl+K, escriben algo relacionado con tu plugin y ahí apareces, sin que tengan que navegar por menús.

Si todavía no conoces la paleta de comandos como usuario echa un vistazo al artículo donde explico qué es y cómo se usa. Este artículo es otra cosa, esto va de código, aquí vamos a ver cómo funciona la API, qué tipos de comandos hay y cómo registrar los tuyos paso a paso.

Para que no sea un ejercicio abstracto los ejemplos son sobre un plugin del repositorio de WordPress.org, Multiple Sale Prices Scheduler, un plugin gratuito para WooCommerce con el que programar múltiples ofertas de precios por producto. No te voy a poner el código de ese plugin, sino el patrón para aplicarlo al tuyo.

Y lo mejor es que no necesitas npm, Node.js ni proceso de compilación, los paquetes de WordPress que usa la API están disponibles como variables globales en el admin (wp.commands, wp.data, wp.element).

Creas un archivo JavaScript normal, lo encolas con wp_enqueue_script() declarando las dependencias correctas, y funciona.

Una API con dos caminos

La documentación oficial habla de dos formas de registrar comandos: con hooks de React (useCommand, useCommandLoader) o con la API imperativa del store de datos (wp.data.dispatch).

Los ganchos de React necesitan estar dentro de un componente montado por registerPlugin, y eso solo funciona en los editores de bloques, donde existe un PluginArea que renderiza esos componentes. En las pantallas normales del admin (plugins, ajustes, dashboard…) esos componentes no se montan y tus comandos no aparecen.

La API imperativa registra los comandos directamente en el store de la paleta y funciona en cualquier pantalla del admin:

// Hooks de React: solo funcionan dentro de los editores de bloques
wp.commands.useCommand( { ... } );

// API imperativa: funciona en TODO el admin
wp.data.dispatch( wp.commands.store ).registerCommand( { ... } );

Si tu plugin solo necesita comandos dentro del editor de bloques (porque trabaja con bloques, por ejemplo), los ganchos pueden servirte, para todo lo demás usa la API imperativa, es la que vamos a usar en esta guía porque es la que yo usé para el plugin, para que los comandos estén disponibles en todo el admin, que es lo más chulo.

Qué comandos merece la pena ofrecer

Antes de escribir código piensa en qué acciones de tu plugin se benefician de estar en la paleta. La clave es, ¿esto le ahorra tiempo al usuario comparado con navegar por menús?

Hay cosas que WordPress ya registra automáticamente, como todos los menús y submenús de tu plugin aparecen en la paleta como comandos de navegación («Ir a: Tu Plugin > Submenú«), no necesitas registrarlos tú, así que esta parte te la puedes ahorrar.

Lo que sí tiene sentido es registrar acciones que van más allá de la simple navegación:

  • Lanzar una exportación o descarga directa.
  • Ejecutar una acción de mantenimiento (vaciar cachés, regenerar algo).
  • Abrir una pantalla concreta que no tiene menú propio (una pestaña específica dentro de los ajustes, por ejemplo).

En el caso de Multiple Sale Prices Scheduler los dos comandos que he registrado son estos:

  • Exportar ofertas programadas a CSV: descarga el archivo directamente, sin tener que ir a la pestaña de importar/exportar.
  • Mostrar ofertas programadas: abre la pestaña de resumen, que es donde está la información que más consultas.

Son acciones que el usuario haría varias veces por semana y que con la paleta pasan de varios clics a uno solo.

Solo 2 archivos para añadir comandos a tu plugin

Lo que necesitas en tu plugin:

  • Un archivo JavaScript (por ejemplo assets/js/command-palette.js) que registra los comandos usando la API imperativa.
  • Un archivo PHP (o una sección en tu código existente) que encola el script, le pasa datos y, si algún comando ejecuta una acción en el servidor, define los handlers de AJAX o REST correspondientes.

No hay proceso de compilación, los paquetes de WordPress que usa la paleta (wp.commands, wp.data, wp.element) están disponibles como variables globales en el admin. Solo tienes que declarar las dependencias correctas al encolar el script.

¿Lo tienes?, pues a por ello, verás qué fácil. El PHP es soprendentemente sencillo, el JS te reconozco que no es lo mío y me tuve que leer la documentación y ayudarme un poco con la IA.

Paso 1: encolar el script con las dependencias

Este es el punto donde la mayoría de tutoriales fallan, y que me hizo perder media mañana, porque si no declaras las dependencias correctamente las variables globales de WordPress no estarán disponibles cuando tu script se ejecute, y no verás ningún error, simplemente no pasará nada.

function mi_plugin_enqueue_command_palette() {
    // Solo para usuarios con permisos
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    // Evitar doble enqueue (ambos hooks pueden dispararse en editores)
    if ( wp_script_is( 'mi-plugin-commands', 'enqueued' ) ) {
        return;
    }

    wp_enqueue_script(
        'mi-plugin-commands',
        plugins_url( 'assets/js/command-palette.js', __FILE__ ),
        array(
            'wp-commands',
            'wp-element',
            'wp-primitives',
            'wp-data',
        ),
        MI_PLUGIN_VERSION,
        true
    );

    wp_localize_script(
        'mi-plugin-commands',
        'miPluginCommands',
        array(
            'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
            'nonce'       => wp_create_nonce( 'mi_plugin_commands' ),
            'settingsUrl' => admin_url( 'admin.php?page=mi-plugin-settings' ),
            'labels'      => array(
                'export'   => __( 'Export data to CSV', 'mi-plugin' ),
                'settings' => __( 'Open settings', 'mi-plugin' ),
            ),
        )
    );
}
add_action( 'admin_enqueue_scripts', 'mi_plugin_enqueue_command_palette' );
add_action( 'enqueue_block_editor_assets', 'mi_plugin_enqueue_command_palette' );

Vamos por partes:

  • Las dependencias: El array le dice a WordPress qué scripts cargar antes que el tuyo. wp-commands es la paleta en sí, wp-data es el sistema de stores, wp-element y wp-primitives son necesarios para crear los iconos SVG (si tu comando no usa iconos puedes quitar las dos últimas).
  • Dos ganchos: Utilizaremos admin_enqueue_scripts para las pantallas normales del admin y enqueue_block_editor_assets para los editores de bloques. Desde WordPress 6.9 la paleta funciona en todo el admin así que necesitas ambos.
  • Los labels desde PHP: Los textos de los comandos van en el array de wp_localize_script(), envueltos en __() en PHP, así se traducen con los PO/MO normales del plugin, sin depender de archivos JSON de traducción ni de wp.i18n. Tu JavaScript simplemente lee miPluginCommands.labels.export y ya tiene la cadena traducida.
  • El nonce: Si alguno de tus comandos ejecuta una acción AJAX necesitas pasar un nonce. Si todos son de navegación (solo redirigen a una URL) puedes omitirlo.

Paso 2: registrar los comandos en JavaScript

Cada comando necesita cuatro cosas: un nombre único, un label (lo que ve el usuario), un icono y un callback (lo que pasa cuando lo seleccionan).

Comando de navegación

El más sencillo es este, que al seleccionarlo redirige a una URL:

( function () {
    'use strict';

    var dispatch = wp.data.dispatch;
    var commandsStore = wp.commands.store;
    var config = miPluginCommands;

    dispatch( commandsStore ).registerCommand( {
        name: 'mi-plugin/abrir-ajustes',
        label: config.labels.settings,
        icon: miIcono,
        callback: function ( options ) {
            options.close();
            document.location = config.settingsUrl;
        }
    } );

} )();
  • options.close() cierra la paleta: Llámalo siempre al principio del callback, antes de hacer cualquier otra cosa. Si la acción tarda o redirige a otra página la paleta tiene que estar ya cerrada.
  • El nombre debe seguir el formato tu-plugin/nombre-del-comando: Si usas nombres genéricos como export o settings puedes entrar en conflicto con otros plugins.

Comando que ejecuta una acción

Cuando el comando necesita hacer algo en el servidor (exportar, limpiar, regenerar) usas una petición al servidor. Para descargar un archivo redirige directamente a la URL de AJAX:

dispatch( commandsStore ).registerCommand( {
    name: 'mi-plugin/exportar-csv',
    label: config.labels.export,
    icon: miIconoDescargar,
    callback: function ( options ) {
        options.close();

        var url = new URL( config.ajaxUrl );
        url.searchParams.set( 'action', 'mi_plugin_export_csv' );
        url.searchParams.set( '_ajax_nonce', config.nonce );

        window.location.href = url.toString();
    }
} );

No puedes usar fetch() para descargar archivos porque necesitas que el navegador procese las cabeceras de descarga, así que la solución es construir la URL con los parámetros de AJAX y redirigir con window.location.href.

De este modo el navegador recibe las cabeceras Content-Disposition: attachment y lanza la descarga sin salir de la página actual.

Si tu acción no devuelve un archivo sino que ejecuta algo en el servidor (limpiar caché, forzar una sincronización), ahí sí usas fetch(). Pero ojo con el feedback, que el snackbar nativo de WordPress (wp.data.dispatch('core/notices').createNotice) solo se procesa dentro de los editores de bloques.

En las pantallas normales del admin no se ve. Si necesitas confirmar que la acción se ejecutó inyecta un <div class="notice"> directamente en el DOM.

Paso 3: los iconos sin compilación

Los iconos de la paleta se crean con wp.element.createElement y wp.primitives.SVG, que son los mismos paquetes que WordPress usa internamente para la librería de iconos de Gutenberg. Sin JSX, sin import, sin build:

var createElement = wp.element.createElement;
var SVG = wp.primitives.SVG;
var Path = wp.primitives.Path;

var miIconoDescargar = createElement(
    SVG, { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24' },
    createElement( Path, {
        d: 'M18 15v3H6v-3H4v3c0 1.1.9 2 2 2h12c1.1 ' +
           '0 2-.9 2-2v-3h-2zm-1-4l-1.41-1.41L13 12.17V4h-2v8.17L8.41 ' +
           '9.59 7 11l5 5 5-5z'
    } )
);

Los path SVG los puedes sacar de la librería de iconos de Gutenberg. Busca el que te guste, inspecciona el SVG en las herramientas del navegador y copia el valor del atributo d del <path>.

Si no quieres complicarte con iconos puedes pasar null en la propiedad icon y el comando aparecerá sin icono.

Paso 4: el handler de AJAX en PHP

Si tu comando ejecuta una acción en el servidor necesitas el handler correspondiente en PHP. Lo de siempre: nonce, verificación de permisos y la lógica:

function mi_plugin_command_export_csv() {
    check_ajax_referer( 'mi_plugin_commands' );

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Permission denied.' );
    }

    $filename = 'export-' . gmdate( 'Y-m-d' ) . '.csv';
    header( 'Content-Type: text/csv; charset=utf-8' );
    header( 'Content-Disposition: attachment; filename="' . $filename . '"' );

    // Tu lógica de exportación aquí

    exit;
}
add_action( 'wp_ajax_mi_plugin_export_csv', 'mi_plugin_command_export_csv' );

El nonce que verificas (mi_plugin_commands) es el mismo que pasaste con wp_localize_script(). El action de AJAX (mi_plugin_export_csv) tiene que coincidir con el que pones en el JavaScript.

Un detalle importante es que este handler usa su propio nonce, independiente del que pueda usar tu plugin en sus pantallas de ajustes. Esto es necesario porque el comando se ejecuta desde cualquier pantalla del admin, no solo desde las páginas de tu plugin donde se genera el nonce habitual.

El ejemplo de un plugin real

Para que veas cómo encajan todas las piezas en un plugin que puedas probar y curiosear, aquí tienes lo que añadí al programador de ofertas múltiples para WooCommerce.

El plugin registra dos comandos:

  • Exportar ofertas a CSV.
  • Mostrar la pantalla de resumen.

El JavaScript (assets/js/command-palette.js)

( function () {
    'use strict';

    var dispatch = wp.data.dispatch;
    var commandsStore = wp.commands.store;
    var createElement = wp.element.createElement;
    var SVG = wp.primitives.SVG;
    var Path = wp.primitives.Path;
    var config = mspsCommandPalette;

    // Los iconos
    var downloadIcon = createElement(
        SVG, { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24' },
        createElement( Path, {
            d: 'M18 15v3H6v-3H4v3c0 1.1.9 2 2 2h12c1.1 ' +
               '0 2-.9 2-2v-3h-2zm-1-4l-1.41-1.41L13 12.17V4h-2v8.17L8.41 ' +
               '9.59 7 11l5 5 5-5z'
        } )
    );

    var listIcon = createElement(
        SVG, { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24' },
        createElement( Path, {
            d: 'M4 4h2v2H4V4zm4 0h12v2H8V4zM4 10h2v2H4v-2zm4 ' +
               '0h12v2H8v-2zM4 16h2v2H4v-2zm4 0h12v2H8v-2z'
        } )
    );

    // El comando de exportar las ofertas a CSV
    dispatch( commandsStore ).registerCommand( {
        name: 'msps/export-csv',
        label: config.labels.exportCsv,
        icon: downloadIcon,
        callback: function ( options ) {
            options.close();
            var url = new URL( config.ajaxUrl );
            url.searchParams.set( 'action', 'msps_command_export_csv' );
            url.searchParams.set( '_ajax_nonce', config.nonce );
            window.location.href = url.toString();
        }
    } );

    // El comando de mostrar el resumen de ofertas
    dispatch( commandsStore ).registerCommand( {
        name: 'msps/show-offers',
        label: config.labels.showOffers,
        icon: listIcon,
        callback: function ( options ) {
            options.close();
            document.location = config.overviewUrl;
        }
    } );

} )();

El PHP (includes/class-command-palette.php)

class AyudaWP_MSPS_Command_Palette {

    public function __construct() {
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
        add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ) );
        add_action( 'wp_ajax_msps_command_export_csv', array( $this, 'ajax_export_csv' ) );
    }

    public function enqueue_assets() {
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return;
        }
        if ( wp_script_is( 'msps-command-palette', 'enqueued' ) ) {
            return;
        }

        wp_enqueue_script(
            'msps-command-palette',
            MSPS_PLUGIN_URL . 'assets/js/command-palette.js',
            array( 'wp-commands', 'wp-element', 'wp-primitives', 'wp-data' ),
            MSPS_VERSION,
            true
        );

        wp_localize_script( 'msps-command-palette', 'mspsCommandPalette', array(
            'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
            'nonce'       => wp_create_nonce( 'msps_command_palette' ),
            'overviewUrl' => admin_url( 'admin.php?page=multiple-sale-prices-scheduler' ),
            'labels'      => array(
                'exportCsv'  => __( 'Export scheduled offers to CSV', 'multiple-sale-prices-scheduler' ),
                'showOffers' => __( 'Show scheduled offers', 'multiple-sale-prices-scheduler' ),
            ),
        ) );
    }

    public function ajax_export_csv() {
        check_ajax_referer( 'msps_command_palette' );
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            wp_die( 'Permission denied.' );
        }

        // ... lógica de exportación CSV del plugin ...

        exit;
    }
}

La clase se instancia desde el método init_components() del núcleo del plugin junto con el resto de componentes.

El JavaScript va en assets/js/, siguiendo la misma estructura que los demás scripts del plugin. Sin build, sin dependencias externas, un archivo PHP y un archivo JS como hemos visto.

El resultado es este, y si quieres, puedes probarlo en cualquier web. Simplemente usa la combinación de teclas habitual de la paleta de comandos (Cmd+K o Ctrl+K según el sistema operativo) y en la ventana emergente empieza a lanzar alguno de los comandos que he creado.

Los contextos

Los comandos pueden tener prioridad en determinadas pantallas si les añades la propiedad context.

A día de hoy hay dos contextos disponibles:

  • site-editor (navegando por el editor del sitio)
  • site-editor-edit (editando una plantilla)

Al vincular un comando a un contexto aparece primero en la lista cuando el usuario está en esa pantalla.

dispatch( commandsStore ).registerCommand( {
    name: 'mi-plugin/accion-plantillas',
    label: 'Mi acción para plantillas',
    icon: miIcono,
    context: 'site-editor-edit',
    callback: function ( options ) {
        options.close();
        // Tu lógica
    }
} );

Para los comandos que hemos visto en esta guía no he usado contextos porque nos interesa que estén disponibles desde cualquier pantalla del admin. Parece ser que se irán añadiendo más contextos a medida que la paleta evolucione.

Desregistrar comandos

Si necesitas quitar un comando registrado por WordPress o por otro plugin puedes hacerlo así:

wp.data.dispatch( wp.commands.store ).unregisterCommand( 'nombre-del-comando' );

Necesitas conocer el nombre exacto del comando, los del core los puedes ver en el código fuente de Gutenberg en GitHub.

Lo que he aprendido haciéndolo

Integrar la paleta en Multiple Sale Prices Scheduler me dejó unas cuantas lecciones que no están en ninguna documentación oficial:

  • La API imperativa es la que funciona en el admin: Todos los tutoriales que encuentras por ahí usan registerPlugin + useCommand. Eso solo funciona dentro de los editores de bloques porque depende de PluginArea. Si quieres que tus comandos aparezcan en cualquier pantalla del admin necesitas wp.data.dispatch( wp.commands.store ).registerCommand().
  • No necesitas npm: Los paquetes de WordPress están disponibles como globales, solo declaras las dependencias correctas en wp_enqueue_script() y listo. Para comandos estáticos montar un proceso de build no tiene sentido.
  • El snackbar no se ve fuera de los editores: wp.data.dispatch('core/notices').createNotice() crea un aviso, pero solo se renderiza en pantallas que tienen el componente de notices montado (los editores de bloques). En el admin normal el aviso se crea en el store pero no se pinta. Si necesitas respuesta visual en todo el admin inyecta un <div class="notice"> directamente en el DOM.
  • Piensa bien qué comandos añadir: WordPress ya registra automáticamente los menús de tu plugin como comandos de navegación. No tiene sentido duplicarlos. Añade solo los que hagan algo que no se puede hacer navegando por menús.
  • Las traducciones van mejor desde PHP: Usar wp.i18n.__() en el JavaScript obliga a gestionar archivos JSON de traducción (que genera @wordpress/scripts). Si pasas los labels con wp_localize_script() las cadenas se traducen con los PO/MO normales del plugin. Más sencillo, sin dependencias adicionales y se cargan al instante desde el sistema de traducciones de WordPress.org .

Con npm o sin npm

Todo lo que hemos visto funciona sin herramientas de compilación, pero si tu plugin ya usa @wordpress/scripts (porque tiene bloques personalizados, por ejemplo) puedes escribir el mismo código con JSX y queda algo más limpio:

import { store as commandsStore } from '@wordpress/commands';
import { dispatch } from '@wordpress/data';
import { download } from '@wordpress/icons';

dispatch( commandsStore ).registerCommand( {
    name: 'mi-plugin/exportar',
    label: 'Exportar datos',
    icon: download,
    callback: ( { close } ) => {
        close();
        // ...
    },
} );

La diferencia es estética pero la funcionalidad es la misma.

Buenas prácticas

Algunos consejos finales, casi todos de cosas que yo hice mal:

  • Prefija los nombres de tus comandos: Usa el formato tu-plugin/nombre-del-comando para evitar conflictos con otros plugins o con el core.
  • Labels descriptivos: El usuario tiene que entender qué hace el comando con solo leerlo. «Exportar ofertas programadas a CSV» es claro, «Exportar CSV» no dice casi nada, puede ser de cualquier plugin.
  • Cierra la paleta antes de ejecutar: Llama a options.close() al principio del callback, no al final.
  • Verifica permisos en el servidor: El JavaScript controla quién ve los comandos (no cargando el script si el usuario no tiene permisos), pero la seguridad de verdad está en el PHP. Nonce y current_user_can() en cada handler, como siempre.
  • No te pases con los comandos: Registra solo los que tengan sentido desde la paleta. Si una acción necesita un formulario o una interacción compleja redirige al usuario a la pantalla correspondiente.

Referencias

A día de hoy muy pocos plugins registran comandos en la paleta. Es una oportunidad para diferenciarte y darle a tus usuarios algo que van a agradecer cada vez que pulsen Cmd+K.

A mi la paleta de comandos me parece una herramienta brutal, a la que se le saca muy poco partido y mucha gente no sabe ni que existe, pero cuando la descubres es un ahorrador de tiempo brutal y te engancha. A poco que vayamos incorporando este tipo de comandos personalizados que aporten de verdad una utilidad práctica a los usuarios todos ganaremos.

Compartir en redes
Resumir con IA

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en las estrellas para valorarlo!

Promedio de puntuación 5 / 5. Total de votos: 2

¡Todavía no hay votos! Sé el primero en valorar este contenido.

Ya que has encontrado útil este contenido...

¡Sígueme en las redes sociales!

¿Te gustó este artículo? ¡Ni te imaginas lo que te estás perdiendo en YouTube!



Sobre el autor

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio