Skip to content

Add Command Palette support #121

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

Open
wants to merge 33 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5215215
First attempt at adding command palette support
priethor Apr 17, 2025
d6006fe
Command polishing
priethor Apr 17, 2025
4ec2af9
Add commands for registered CPTs
priethor Apr 17, 2025
18e250f
Refactor
priethor Apr 17, 2025
77bdff1
Refactor to use wp.scf
priethor Apr 17, 2025
b8894cf
Refactor
priethor Apr 18, 2025
6fce2a9
Refactor to pass the data in acf.data.customPostTypes
priethor Apr 18, 2025
f28d69d
Fix redirections
priethor Apr 18, 2025
6e4431c
Check capabilities before registering comands
priethor Apr 23, 2025
65485ac
Filter post types that don't enable the setting "show in UI"
priethor Apr 23, 2025
b06b8a7
Redo webpack config linting
priethor Apr 23, 2025
f5a6cba
Redo webpack linting
priethor Apr 23, 2025
4c6f5da
Apply linting
priethor Apr 24, 2025
c592beb
Polish
priethor Apr 24, 2025
3907d63
Refactor
priethor Apr 24, 2025
60b8184
Fix coding standards in comments
priethor Apr 24, 2025
af46e4a
Ensure we only register the commands when the palette is available
priethor Apr 24, 2025
b2d95ab
Refactor to remove scf and acf prefixes, organizing files in folders …
priethor Apr 24, 2025
101de27
Optimize return early
priethor Apr 25, 2025
4b6a953
Remove unnecessary script registration
priethor Apr 25, 2025
1a62407
Use the `scf` prefix in admin commands.
priethor Apr 25, 2025
47c480c
Use `scf` prefix for CPT commands
priethor Apr 25, 2025
9bed553
Only pass CPTs to the frontend when the array is not empty
priethor Apr 25, 2025
dfa0104
CPT commands: only add label as a keyword if not empty
priethor Apr 25, 2025
54176c5
Polish
priethor Apr 25, 2025
a23722c
Refactor the depencies to user import statements
priethor Apr 25, 2025
887f2cf
Optimize scripts loading and execution
priethor Apr 25, 2025
9944ea2
Update package-lock.json
priethor Apr 25, 2025
4def381
Slight refactor
priethor Apr 25, 2025
1df01fe
Assets: remove redundant registrations, prefix registered scritps
priethor Apr 25, 2025
ccb0edc
Remove redundant admin check
priethor Apr 28, 2025
b5f9199
Remove unnecesary checking of `acf_get_acf_post_types`
priethor Apr 28, 2025
71342ac
Change the priority queue to a simpler `requestIdleCallback`
priethor Apr 28, 2025
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
192 changes: 192 additions & 0 deletions assets/src/js/commands/admin-commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* Admin Commands
*
* Core WordPress commands for Secure Custom Fields administration.
* This file registers navigation commands for all primary SCF admin screens,
* enabling quick access through the WordPress commands interface (Cmd+K / Ctrl+K).
*
* @since 6.5.0
*/

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { createElement } from '@wordpress/element';
import { Icon } from '@wordpress/components';
import { dispatch } from '@wordpress/data';

/**
* Register admin commands for SCF
*/
const registerAdminCommands = () => {
if ( ! dispatch( 'core/commands' ) || ! window.acf?.data ) {
return;
}

const commandStore = dispatch( 'core/commands' );
const adminUrl = window.acf?.data?.admin_url || '';

const commands = [
{
name: 'field-groups',
label: __( 'Field Groups', 'secure-custom-fields' ),
url: 'edit.php?post_type=acf-field-group',
icon: 'layout',
description: __(
'SCF: View and manage custom field groups',
'secure-custom-fields'
),
keywords: [
'acf',
'custom fields',
'field editor',
'manage fields',
],
},
{
name: 'new-field-group',
label: __( 'Create New Field Group', 'secure-custom-fields' ),
url: 'post-new.php?post_type=acf-field-group',
icon: 'plus',
description: __(
'SCF: Create a new field group to organize custom fields',
'secure-custom-fields'
),
keywords: [
'add',
'new',
'create',
'field group',
'custom fields',
],
},
{
name: 'post-types',
label: __( 'Post Types', 'secure-custom-fields' ),
url: 'edit.php?post_type=acf-post-type',
icon: 'admin-post',
description: __(
'SCF: Manage custom post types',
'secure-custom-fields'
),
keywords: [ 'cpt', 'content types', 'manage post types' ],
},
{
name: 'new-post-type',
label: __( 'Create New Post Type', 'secure-custom-fields' ),
url: 'post-new.php?post_type=acf-post-type',
icon: 'plus',
description: __(
'SCF: Create a new custom post type',
'secure-custom-fields'
),
keywords: [ 'add', 'new', 'create', 'cpt', 'content type' ],
},
{
name: 'taxonomies',
label: __( 'Taxonomies', 'secure-custom-fields' ),
url: 'edit.php?post_type=acf-taxonomy',
icon: 'category',
description: __(
'SCF: Manage custom taxonomies for organizing content',
'secure-custom-fields'
),
keywords: [ 'categories', 'tags', 'terms', 'custom taxonomies' ],
},
{
name: 'new-taxonomy',
label: __( 'Create New Taxonomy', 'secure-custom-fields' ),
url: 'post-new.php?post_type=acf-taxonomy',
icon: 'plus',
description: __(
'SCF: Create a new custom taxonomy',
'secure-custom-fields'
),
keywords: [
'add',
'new',
'create',
'taxonomy',
'categories',
'tags',
],
},
{
name: 'options-pages',
label: __( 'Options Pages', 'secure-custom-fields' ),
url: 'edit.php?post_type=acf-ui-options-page',
icon: 'admin-settings',
description: __(
'SCF: Manage custom options pages for global settings',
'secure-custom-fields'
),
keywords: [ 'settings', 'global options', 'site options' ],
},
{
name: 'new-options-page',
label: __( 'Create New Options Page', 'secure-custom-fields' ),
url: 'post-new.php?post_type=acf-ui-options-page',
icon: 'plus',
description: __(
'SCF: Create a new custom options page',
'secure-custom-fields'
),
keywords: [ 'add', 'new', 'create', 'options', 'settings page' ],
},
{
name: 'tools',
label: __( 'SCF Tools', 'secure-custom-fields' ),
url: 'admin.php?page=acf-tools',
icon: 'admin-tools',
description: __(
'SCF: Access SCF utility tools',
'secure-custom-fields'
),
keywords: [ 'utilities', 'import export', 'json' ],
},
{
name: 'import',
label: __( 'Import SCF Data', 'secure-custom-fields' ),
url: 'admin.php?page=acf-tools&tool=import',
icon: 'upload',
description: __(
'SCF: Import field groups, post types, taxonomies, and options pages',
'secure-custom-fields'
),
keywords: [ 'upload', 'json', 'migration', 'transfer' ],
},
{
name: 'export',
label: __( 'Export SCF Data', 'secure-custom-fields' ),
url: 'admin.php?page=acf-tools&tool=export',
icon: 'download',
description: __(
'SCF: Export field groups, post types, taxonomies, and options pages',
'secure-custom-fields'
),
keywords: [ 'download', 'json', 'backup', 'migration' ],
},
];

commands.forEach( ( command ) => {
commandStore.registerCommand( {
name: 'scf/' + command.name,
label: command.label,
icon: createElement( Icon, { icon: command.icon } ),
context: 'admin',
description: command.description,
keywords: command.keywords,
callback: ( { close } ) => {
document.location = adminUrl + command.url;
close();
},
} );
} );
};

if ( 'requestIdleCallback' in window ) {
window.requestIdleCallback( registerAdminCommands, { timeout: 500 } );
} else {
setTimeout( registerAdminCommands, 500 );
}
117 changes: 117 additions & 0 deletions assets/src/js/commands/custom-post-type-commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Custom Post Type Commands
*
* Dynamic commands for user-created custom post types in Secure Custom Fields.
* This file generates navigation commands for each registered post type that
* the current user has access to, creating both "View All" and "Add New" commands.
*
* Post type data is provided via acf.data.customPostTypes, which is populated
* by the PHP side after capability checks ensure the user has appropriate access.
*
* @since 6.5.0
*/

/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { createElement } from '@wordpress/element';
import { Icon } from '@wordpress/components';
import { dispatch } from '@wordpress/data';

/**
* Register custom post type commands
*/
const registerPostTypeCommands = () => {
// Only proceed when WordPress commands API and there are custom post types accessible
if (
! dispatch( 'core/commands' ) ||
! window.acf?.data?.customPostTypes?.length
) {
return;
}

const commandStore = dispatch( 'core/commands' );
const adminUrl = window.acf.data.admin_url || '';
const postTypes = window.acf.data.customPostTypes;

postTypes.forEach( ( postType ) => {
// Skip invalid post types
if ( ! postType?.name ) {
return;
}

const pluralLabel = postType.label || postType.name;
const singularLabel = postType.singular_label || pluralLabel;

// Register "View All" command for this post type
commandStore.registerCommand( {
name: `scf/cpt-${ postType.name }`,
label: pluralLabel,
icon: createElement( Icon, { icon: 'admin-page' } ),
context: 'admin',
description:
/* translators: %s: Post type plural label */
sprintf(
__( 'SCF: View all %s', 'secure-custom-fields' ),
pluralLabel
),
keywords: [
'post type',
'content',
'cpt',
postType.name,
...( postType.label ? [ postType.label ] : [] ),
],
callback: ( { close } ) => {
document.location =
adminUrl +
`edit.php?post_type=${ encodeURIComponent(
postType.name
) }`;
close();
},
} );

// Register "Add New" command for this post type
commandStore.registerCommand( {
name: `scf/new-${ postType.name }`,
label:
/* translators: %s: Post type singular label */
sprintf(
__( 'Add New %s', 'secure-custom-fields' ),
singularLabel
),
icon: createElement( Icon, { icon: 'plus' } ),
context: 'admin',
description:
/* translators: %s: Post type singular label */
sprintf(
__( 'SCF: Create a new %s', 'secure-custom-fields' ),
singularLabel
),
keywords: [
'add',
'new',
'create',
'content',
postType.name,
...( postType.label ? [ postType.label ] : [] ),
],
callback: ( { close } ) => {
document.location =
adminUrl +
`post-new.php?post_type=${ encodeURIComponent(
postType.name
) }`;
close();
},
} );
} );
};

if ( 'requestIdleCallback' in window ) {
window.requestIdleCallback( registerPostTypeCommands, { timeout: 500 } );
} else {
setTimeout( registerPostTypeCommands, 500 );
}
80 changes: 80 additions & 0 deletions includes/admin/admin-commands.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
/**
* SCF Commands Integration
*
* @package Secure Custom Fields
*/

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

/**
* Initializes SCF commands integration
*
* This function handles the integration with WordPress Commands (Cmd+K / Ctrl+K),
* providing navigation commands for SCF admin pages and custom post types.
*
* The implementation follows these principles:
* 1. Only loads in screens where WordPress commands are available.
* 2. Performs capability checks to ensure users only see commands they can access.
* 3. Core administrative commands are only shown to users with SCF admin capabilities.
* 4. Custom post type commands are conditionally shown based on edit_posts capability
* for each specific post type.
* 5. Post types must have UI enabled (show_ui setting) to appear in commands.
*
* @since 6.5.0
*/
function acf_commands_init() {
// Ensure we only load our commands where the WordPress commands API is available.
if ( ! wp_script_is( 'wp-commands', 'registered' ) ) {
return;
}

$custom_post_types = array();

$scf_post_types = acf_get_acf_post_types();

foreach ( $scf_post_types as $post_type ) {
// Skip if post type name is not set (defensive) or post type is inactive.
if ( empty( $post_type['post_type'] ) || ( isset( $post_type['active'] ) && ! $post_type['active'] ) ) {
continue;
}

$plural_label = $post_type['labels']['name'] ?? $post_type['label'] ?? $post_type['post_type'];
$singular_label = $post_type['labels']['singular_name'] ?? $post_type['singular_label'] ?? $plural_label;

$post_type_obj = get_post_type_object( $post_type['post_type'] );

// Three conditions must be met to include this post type in the commands:
// 1. Post type object must exist
// 2. Current user must have permission to edit posts of this type.
// 3. Post type must have admin UI enabled (show_ui setting).
if ( $post_type_obj &&
current_user_can( $post_type_obj->cap->edit_posts ) &&
$post_type_obj->show_ui ) {
$custom_post_types[] = array(
'name' => $post_type['post_type'],
'label' => $plural_label,
'singular_label' => $singular_label,
'icon' => $post_type['menu_icon'] ?? '',
);
}
}

if ( ! empty( $custom_post_types ) ) {
acf_localize_data(

Choose a reason for hiding this comment

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

Personally, I'd argue that we should move away from localized variables like that as much as possible and instead use REST API endpoints. Do SCF has endpoints to retrieve these things, can't we just rely on Core endpoints for post types...?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the only endpoints registered are the ones that fetch custom post types and post fields, in every field you can chose to include it or not in the WP default API endpoints for posts fetching, but there are no internal endpoints to retrieve all post types or all post fields created with the plugin.

They are instead localizing the data:

acf_localize_data( $data_to_localize );

Moving everything to a REST API could be done, but this needs to define a new API structure, with its own documentation and decide the approach for the store handling ( using Redux based like wp-data, or move to React Query or something similar )

What's your opinion on that @youknowriad ?

Copy link
Contributor Author

@priethor priethor Apr 28, 2025

Choose a reason for hiding this comment

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

There are two issues why using the Rest API would require some extra work and I didn't consider for this:

  • While it does a PHP function to retrieve SCF Post types (acf_get_acf_post_types), it doesn't have a custom enpoint to do so, just WordPress` built in endpoints.
  • More importantly, in order to show CPTs in the REST API, each CPT needs to enable the Show in REST API setting.

Therefore, my guess is we will need custom SCF endpoints for the admin that show all, as we will encounter a similar issue when implementing DataViews. What do you think about this approach, Riad?

Choose a reason for hiding this comment

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

We're on WordPress, so for me, we should be use the entities and core-data here. no need to duplicate things that exist.

More importantly, in order to show CPTs in the REST API, each CPT needs to enable the Show in REST API setting.

For me this is something that we should try to get rid of. Every CPT should be visible in the endpoints, I think this is a setting of another age.

That said, for now, a custom endpoint/entity could work if we're not ready to make all CPTs created by SCF show_in_rest true. Are there any valid reasons for not doing it?

Copy link
Contributor Author

@priethor priethor Apr 28, 2025

Choose a reason for hiding this comment

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

Are there any valid reasons for not doing it?

If by this you mean a custom endpoint, the only reason not to do it right now is the extra complexity it adds to this PR, we should probably tackle that separately.

As per the existing setting, I wouldn't change an existing setting as we don't know all the cases or real users (e.g., having CPTs only for the admin to manage private data), though.

Choose a reason for hiding this comment

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

As per the existing setting, I wouldn't change an existing setting as we don't know all the cases or real users (e.g., having CPTs only for the admin to manage private data), though.

This is different than show_in_rest, this should be mostly handled by the public boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking a bit more about this:

  • Right now, we would need to use the types endpoint.
  • show_in_rest determines whether a custom post type gets its own endpoint, but also hides the CPT definition from the types endpoint.
  • In any case, the types endpoint doesn't return any information to know whether a type is a CPT and/or created by SCF (e.g., no post-type field)

Therefore, my inclination is to create a new endpoint for custom post types that ignores the show_in_rest. This will be needed, too, when migrating the Post Types screen to DataViews.

Copy link

@youknowriad youknowriad Apr 28, 2025

Choose a reason for hiding this comment

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

The ideal scenario is to actually use the types endpoint and add "metadata" to the post type. That said, I think it's a good interim solution to have a custom endpoint.

array(
'customPostTypes' => $custom_post_types,
)
);
wp_enqueue_script( 'scf-commands-custom-post-types' );
}

// Only load admin commands if user has SCF admin capabilities.
if ( current_user_can( acf_get_setting( 'capability' ) ) ) {
wp_enqueue_script( 'scf-commands-admin' );
}
}

add_action( 'admin_enqueue_scripts', 'acf_commands_init' );
Loading
Loading