From ab56aadf5747ad8a91d595a9317200143e0ec03a Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Tue, 21 Jul 2026 21:08:05 +0600 Subject: [PATCH] Add command palette commands to navigate to options pages (#140) Register one Command Palette command per SCF options page so users can jump straight to a page by typing its name (Cmd/Ctrl+K). - Localize registered options pages (filtered by capability) for the admin command script. - Build one command per page, deduplicated against WP 6.9+ auto-registered menu commands via a shared helper. - Add E2E fixture plugin and tests covering registration and navigation. --- assets/src/js/commands/admin-commands.js | 33 +++++++++++- includes/admin/admin-commands.php | 25 ++++++++++ tests/e2e/command-palette.spec.ts | 50 +++++++++++++++++++ .../plugins/scf-test-setup-options-page.php | 37 ++++++++++++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/plugins/scf-test-setup-options-page.php diff --git a/assets/src/js/commands/admin-commands.js b/assets/src/js/commands/admin-commands.js index 168e371b..270bee11 100644 --- a/assets/src/js/commands/admin-commands.js +++ b/assets/src/js/commands/admin-commands.js @@ -23,6 +23,7 @@ import { tool, upload, download, + page, } from '@wordpress/icons'; /** @@ -165,10 +166,34 @@ const registerAdminCommands = () => { } ); }; + // Options page commands — one per registered SCF options page. + const optionsPageCommands = []; + if ( Array.isArray( window.scfOptionsPages ) ) { + window.scfOptionsPages.forEach( ( optionsPage ) => { + if ( ! optionsPage?.menu_slug ) { + return; + } + optionsPageCommands.push( { + name: 'options-page-' + optionsPage.menu_slug, + label: optionsPage.menu_title || optionsPage.page_title, + url: 'admin.php', + urlArgs: { page: optionsPage.menu_slug }, + icon: page, + keywords: [ + 'options', + 'settings', + 'scf', + optionsPage.menu_title, + optionsPage.page_title, + ].filter( Boolean ), + } ); + } ); + } + // WordPress 6.9+ adds Command Palette commands for all admin menu items. // For older versions, we need to register them manually. The most reliable way to // detect this is to check if the commands are already registered. - viewCommands.forEach( ( command ) => { + const registerIfNotAutoRegistered = ( command ) => { const commandUrl = addQueryArgs( command.url, command.urlArgs ); // WordPress stores destination URLs in the command *name*, appended to // the menu slug (which is also a relative URL), resulting in somewhat @@ -178,11 +203,15 @@ const registerAdminCommands = () => { return; } registerCommand( command ); - } ); + }; + + viewCommands.forEach( registerIfNotAutoRegistered ); // "Create New" commands are not automatically registered by WordPress, // so we always register them. createCommands.forEach( registerCommand ); + + optionsPageCommands.forEach( registerIfNotAutoRegistered ); }; if ( 'requestIdleCallback' in window ) { diff --git a/includes/admin/admin-commands.php b/includes/admin/admin-commands.php index 19970b68..270854f4 100644 --- a/includes/admin/admin-commands.php +++ b/includes/admin/admin-commands.php @@ -39,6 +39,31 @@ function acf_commands_init() { // Only load admin commands if user has SCF admin capabilities. if ( current_user_can( acf_get_setting( 'capability' ) ) ) { wp_enqueue_script( 'scf-commands-admin' ); + + // Localize registered options pages so the JS can register a palette + // command per page. Filtered by capability so users only see pages + // they can access. + $pages = acf_get_options_pages(); + if ( ! empty( $pages ) ) { + $localized = array(); + foreach ( $pages as $page ) { + if ( ! current_user_can( $page['capability'] ) ) { + continue; + } + $localized[] = array( + 'menu_slug' => $page['menu_slug'], + 'menu_title' => $page['menu_title'], + 'page_title' => $page['page_title'], + ); + } + if ( ! empty( $localized ) ) { + wp_localize_script( + 'scf-commands-admin', + 'scfOptionsPages', + $localized + ); + } + } } } diff --git a/tests/e2e/command-palette.spec.ts b/tests/e2e/command-palette.spec.ts index 467836bd..bc70f425 100644 --- a/tests/e2e/command-palette.spec.ts +++ b/tests/e2e/command-palette.spec.ts @@ -171,6 +171,56 @@ test.describe( 'Command Palette', () => { ).toHaveValue( 'SCF E2E Test Type' ); } ); } ); + + test.describe( 'Options page-specific commands', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'scf-test-setup-options-page' + ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'scf-test-setup-options-page' + ); + } ); + + test( 'should register a command for the registered options page', async ( { + page, + } ) => { + await openCommandPalette( page ); + + const input = getCommandPaletteInput( page ); + + await input.fill( 'SCF E2E Test Options' ); + + const option = page.getByRole( 'option', { + name: /SCF E2E Test Options/, + } ); + + await expect( option ).toHaveCount( 1 ); + } ); + + test( 'should navigate to the options page via command palette', async ( { + page, + } ) => { + await openCommandPalette( page ); + + const input = getCommandPaletteInput( page ); + + await input.fill( 'SCF E2E Test Options' ); + + await page + .getByRole( 'option', { + name: /SCF E2E Test Options/, + } ) + .click(); + + await expect( page ).toHaveURL( + /admin\.php\?page=scf-e2e-test-options/ + ); + } ); + } ); } ); } ); } ); diff --git a/tests/e2e/plugins/scf-test-setup-options-page.php b/tests/e2e/plugins/scf-test-setup-options-page.php new file mode 100644 index 00000000..e5ff2653 --- /dev/null +++ b/tests/e2e/plugins/scf-test-setup-options-page.php @@ -0,0 +1,37 @@ + 'SCF E2E Test Options', + 'menu_title' => 'SCF E2E Test Options', + 'menu_slug' => 'scf-e2e-test-options', + 'capability' => 'edit_posts', + 'redirect' => false, + ) + ); +} + +add_action( 'acf/init', 'scf_test_register_options_page', 20 );