Skip to content

Commit be909ac

Browse files
authored
Add Command Palette support (#121)
* First attempt at adding command palette support * Command polishing * Add commands for registered CPTs * Refactor * Refactor to use wp.scf * Refactor * Refactor to pass the data in acf.data.customPostTypes * Fix redirections * Check capabilities before registering comands Includes a refactor to divide commands between core and post-type based: core commands require admin permissions, whereas post-types are dynamic and depend on their own capabilities. * Filter post types that don't enable the setting "show in UI" * Redo webpack config linting * Redo webpack linting * Apply linting * Polish * Refactor * Fix coding standards in comments * Ensure we only register the commands when the palette is available * Refactor to remove scf and acf prefixes, organizing files in folders similar to modules * Optimize return early * Remove unnecessary script registration * Use the `scf` prefix in admin commands. * Use `scf` prefix for CPT commands * Only pass CPTs to the frontend when the array is not empty * CPT commands: only add label as a keyword if not empty * Polish * Refactor the depencies to user import statements * Optimize scripts loading and execution - Defer the script loading (defer was introduced in WP 6.3) - Defer script execution by using the priority queue (introduced in WP 5.0) Since the scripts will only be registered in versions with Command API support, which is 6.3+, we can use both * Update package-lock.json * Slight refactor * Assets: remove redundant registrations, prefix registered scritps * Remove redundant admin check * Remove unnecesary checking of `acf_get_acf_post_types` * Change the priority queue to a simpler `requestIdleCallback` * Update assets/src/js/commands/custom-post-type-commands.js * Improved URL building * Fix command labels for better i18n * Simplify label calculation leveraging WP core functions * Remove redundant client-side label calculation * Fixed `@since` annotations to follow the steps of #129 * Switch to using the proper labels for the commands
1 parent 7294c33 commit be909ac

File tree

8 files changed

+431
-2
lines changed

8 files changed

+431
-2
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/**
2+
* Admin Commands
3+
*
4+
* Core WordPress commands for Secure Custom Fields administration.
5+
* This file registers navigation commands for all primary SCF admin screens,
6+
* enabling quick access through the WordPress commands interface (Cmd+K / Ctrl+K).
7+
*
8+
* @since SCF 6.5.0
9+
*/
10+
11+
/**
12+
* WordPress dependencies
13+
*/
14+
import { __ } from '@wordpress/i18n';
15+
import { createElement } from '@wordpress/element';
16+
import { Icon } from '@wordpress/components';
17+
import { dispatch } from '@wordpress/data';
18+
import { addQueryArgs } from '@wordpress/url';
19+
20+
/**
21+
* Register admin commands for SCF
22+
*/
23+
const registerAdminCommands = () => {
24+
if ( ! dispatch( 'core/commands' ) || ! window.acf?.data ) {
25+
return;
26+
}
27+
28+
const commandStore = dispatch( 'core/commands' );
29+
const adminUrl = window.acf?.data?.admin_url || '';
30+
31+
const commands = [
32+
{
33+
name: 'field-groups',
34+
label: __( 'Field Groups', 'secure-custom-fields' ),
35+
url: 'edit.php',
36+
urlArgs: { post_type: 'acf-field-group' },
37+
icon: 'layout',
38+
description: __(
39+
'SCF: View and manage custom field groups',
40+
'secure-custom-fields'
41+
),
42+
keywords: [
43+
'acf',
44+
'custom fields',
45+
'field editor',
46+
'manage fields',
47+
],
48+
},
49+
{
50+
name: 'new-field-group',
51+
label: __( 'Create New Field Group', 'secure-custom-fields' ),
52+
url: 'post-new.php',
53+
urlArgs: { post_type: 'acf-field-group' },
54+
icon: 'plus',
55+
description: __(
56+
'SCF: Create a new field group to organize custom fields',
57+
'secure-custom-fields'
58+
),
59+
keywords: [
60+
'add',
61+
'new',
62+
'create',
63+
'field group',
64+
'custom fields',
65+
],
66+
},
67+
{
68+
name: 'post-types',
69+
label: __( 'Post Types', 'secure-custom-fields' ),
70+
url: 'edit.php',
71+
urlArgs: { post_type: 'acf-post-type' },
72+
icon: 'admin-post',
73+
description: __(
74+
'SCF: Manage custom post types',
75+
'secure-custom-fields'
76+
),
77+
keywords: [ 'cpt', 'content types', 'manage post types' ],
78+
},
79+
{
80+
name: 'new-post-type',
81+
label: __( 'Create New Post Type', 'secure-custom-fields' ),
82+
url: 'post-new.php',
83+
urlArgs: { post_type: 'acf-post-type' },
84+
icon: 'plus',
85+
description: __(
86+
'SCF: Create a new custom post type',
87+
'secure-custom-fields'
88+
),
89+
keywords: [ 'add', 'new', 'create', 'cpt', 'content type' ],
90+
},
91+
{
92+
name: 'taxonomies',
93+
label: __( 'Taxonomies', 'secure-custom-fields' ),
94+
url: 'edit.php',
95+
urlArgs: { post_type: 'acf-taxonomy' },
96+
icon: 'category',
97+
description: __(
98+
'SCF: Manage custom taxonomies for organizing content',
99+
'secure-custom-fields'
100+
),
101+
keywords: [ 'categories', 'tags', 'terms', 'custom taxonomies' ],
102+
},
103+
{
104+
name: 'new-taxonomy',
105+
label: __( 'Create New Taxonomy', 'secure-custom-fields' ),
106+
url: 'post-new.php',
107+
urlArgs: { post_type: 'acf-taxonomy' },
108+
icon: 'plus',
109+
description: __(
110+
'SCF: Create a new custom taxonomy',
111+
'secure-custom-fields'
112+
),
113+
keywords: [
114+
'add',
115+
'new',
116+
'create',
117+
'taxonomy',
118+
'categories',
119+
'tags',
120+
],
121+
},
122+
{
123+
name: 'options-pages',
124+
label: __( 'Options Pages', 'secure-custom-fields' ),
125+
url: 'edit.php',
126+
urlArgs: { post_type: 'acf-ui-options-page' },
127+
icon: 'admin-settings',
128+
description: __(
129+
'SCF: Manage custom options pages for global settings',
130+
'secure-custom-fields'
131+
),
132+
keywords: [ 'settings', 'global options', 'site options' ],
133+
},
134+
{
135+
name: 'new-options-page',
136+
label: __( 'Create New Options Page', 'secure-custom-fields' ),
137+
url: 'post-new.php',
138+
urlArgs: { post_type: 'acf-ui-options-page' },
139+
icon: 'plus',
140+
description: __(
141+
'SCF: Create a new custom options page',
142+
'secure-custom-fields'
143+
),
144+
keywords: [ 'add', 'new', 'create', 'options', 'settings page' ],
145+
},
146+
{
147+
name: 'tools',
148+
label: __( 'SCF Tools', 'secure-custom-fields' ),
149+
url: 'admin.php',
150+
urlArgs: { page: 'acf-tools' },
151+
icon: 'admin-tools',
152+
description: __(
153+
'SCF: Access SCF utility tools',
154+
'secure-custom-fields'
155+
),
156+
keywords: [ 'utilities', 'import export', 'json' ],
157+
},
158+
{
159+
name: 'import',
160+
label: __( 'Import SCF Data', 'secure-custom-fields' ),
161+
url: 'admin.php',
162+
urlArgs: { page: 'acf-tools', tool: 'import' },
163+
icon: 'upload',
164+
description: __(
165+
'SCF: Import field groups, post types, taxonomies, and options pages',
166+
'secure-custom-fields'
167+
),
168+
keywords: [ 'upload', 'json', 'migration', 'transfer' ],
169+
},
170+
{
171+
name: 'export',
172+
label: __( 'Export SCF Data', 'secure-custom-fields' ),
173+
url: 'admin.php',
174+
urlArgs: { page: 'acf-tools', tool: 'export' },
175+
icon: 'download',
176+
description: __(
177+
'SCF: Export field groups, post types, taxonomies, and options pages',
178+
'secure-custom-fields'
179+
),
180+
keywords: [ 'download', 'json', 'backup', 'migration' ],
181+
},
182+
];
183+
184+
commands.forEach( ( command ) => {
185+
commandStore.registerCommand( {
186+
name: 'scf/' + command.name,
187+
label: command.label,
188+
icon: createElement( Icon, { icon: command.icon } ),
189+
context: 'admin',
190+
description: command.description,
191+
keywords: command.keywords,
192+
callback: ( { close } ) => {
193+
document.location = command.urlArgs
194+
? addQueryArgs( adminUrl + command.url, command.urlArgs )
195+
: adminUrl + command.url;
196+
close();
197+
},
198+
} );
199+
} );
200+
};
201+
202+
if ( 'requestIdleCallback' in window ) {
203+
window.requestIdleCallback( registerAdminCommands, { timeout: 500 } );
204+
} else {
205+
setTimeout( registerAdminCommands, 500 );
206+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Custom Post Type Commands
3+
*
4+
* Dynamic commands for user-created custom post types in Secure Custom Fields.
5+
* This file generates navigation commands for each registered post type that
6+
* the current user has access to, creating both "View All" and "Add New" commands.
7+
*
8+
* Post type data is provided via acf.data.customPostTypes, which is populated
9+
* by the PHP side after capability checks ensure the user has appropriate access.
10+
*
11+
* @since SCF 6.5.0
12+
*/
13+
14+
/**
15+
* WordPress dependencies
16+
*/
17+
import { __, sprintf } from '@wordpress/i18n';
18+
import { createElement } from '@wordpress/element';
19+
import { Icon } from '@wordpress/components';
20+
import { dispatch } from '@wordpress/data';
21+
import { addQueryArgs } from '@wordpress/url';
22+
23+
/**
24+
* Register custom post type commands
25+
*/
26+
const registerPostTypeCommands = () => {
27+
// Only proceed when WordPress commands API and there are custom post types accessible
28+
if (
29+
! dispatch( 'core/commands' ) ||
30+
! window.acf?.data?.customPostTypes?.length
31+
) {
32+
return;
33+
}
34+
35+
const commandStore = dispatch( 'core/commands' );
36+
const adminUrl = window.acf.data.admin_url || '';
37+
const postTypes = window.acf.data.customPostTypes;
38+
39+
postTypes.forEach( ( postType ) => {
40+
// Skip invalid post types or those missing required labels
41+
if ( ! postType?.name || ! postType?.all_items || ! postType?.add_new_item ) {
42+
return;
43+
}
44+
45+
// Register "View All" command for this post type
46+
commandStore.registerCommand( {
47+
name: `scf/cpt-${ postType.name }`,
48+
label: postType.all_items,
49+
icon: createElement( Icon, { icon: 'admin-page' } ),
50+
context: 'admin',
51+
description: postType.all_items,
52+
keywords: [
53+
'post type',
54+
'content',
55+
'cpt',
56+
postType.name,
57+
postType.label,
58+
].filter( Boolean ),
59+
callback: ( { close } ) => {
60+
document.location = addQueryArgs(adminUrl + 'edit.php', {
61+
post_type: postType.name
62+
});
63+
close();
64+
},
65+
} );
66+
67+
// Register "Add New" command for this post type
68+
commandStore.registerCommand( {
69+
name: `scf/new-${ postType.name }`,
70+
label: postType.add_new_item,
71+
icon: createElement( Icon, { icon: 'plus' } ),
72+
context: 'admin',
73+
description: postType.add_new_item,
74+
keywords: [
75+
'add',
76+
'new',
77+
'create',
78+
'content',
79+
postType.name,
80+
...( postType.label ? [ postType.label ] : [] ),
81+
],
82+
callback: ( { close } ) => {
83+
document.location = addQueryArgs(adminUrl + 'post-new.php', {
84+
post_type: postType.name
85+
});
86+
close();
87+
},
88+
} );
89+
} );
90+
};
91+
92+
if ( 'requestIdleCallback' in window ) {
93+
window.requestIdleCallback( registerPostTypeCommands, { timeout: 500 } );
94+
} else {
95+
setTimeout( registerPostTypeCommands, 500 );
96+
}

includes/admin/admin-commands.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
/**
3+
* SCF Commands Integration
4+
*
5+
* @package Secure Custom Fields
6+
*/
7+
8+
if ( ! defined( 'ABSPATH' ) ) {
9+
exit; // Exit if accessed directly.
10+
}
11+
12+
/**
13+
* Initializes SCF commands integration
14+
*
15+
* This function handles the integration with WordPress Commands (Cmd+K / Ctrl+K),
16+
* providing navigation commands for SCF admin pages and custom post types.
17+
*
18+
* The implementation follows these principles:
19+
* 1. Only loads in screens where WordPress commands are available.
20+
* 2. Performs capability checks to ensure users only see commands they can access.
21+
* 3. Core administrative commands are only shown to users with SCF admin capabilities.
22+
* 4. Custom post type commands are conditionally shown based on edit_posts capability
23+
* for each specific post type.
24+
* 5. Post types must have UI enabled (show_ui setting) to appear in commands.
25+
*
26+
* @since SCF 6.5.0
27+
*/
28+
function acf_commands_init() {
29+
// Ensure we only load our commands where the WordPress commands API is available.
30+
if ( ! wp_script_is( 'wp-commands', 'registered' ) ) {
31+
return;
32+
}
33+
34+
$custom_post_types = array();
35+
36+
$scf_post_types = acf_get_acf_post_types();
37+
38+
foreach ( $scf_post_types as $post_type ) {
39+
// Skip if post type name is not set (defensive) or post type is inactive.
40+
if ( empty( $post_type['post_type'] ) || ( isset( $post_type['active'] ) && ! $post_type['active'] ) ) {
41+
continue;
42+
}
43+
44+
$post_type_obj = get_post_type_object( $post_type['post_type'] );
45+
46+
// Three conditions must be met to include this post type in the commands:
47+
// 1. Post type object must exist
48+
// 2. Current user must have permission to edit posts of this type.
49+
// 3. Post type must have admin UI enabled (show_ui setting).
50+
if ( $post_type_obj &&
51+
current_user_can( $post_type_obj->cap->edit_posts ) &&
52+
$post_type_obj->show_ui ) {
53+
54+
$labels = get_post_type_labels( $post_type_obj );
55+
56+
$custom_post_types[] = array(
57+
'name' => $post_type['post_type'],
58+
'all_items' => $labels->all_items,
59+
'add_new_item' => $labels->add_new_item,
60+
'icon' => $post_type['menu_icon'] ?? '',
61+
);
62+
}
63+
}
64+
65+
if ( ! empty( $custom_post_types ) ) {
66+
acf_localize_data(
67+
array(
68+
'customPostTypes' => $custom_post_types,
69+
)
70+
);
71+
wp_enqueue_script( 'scf-commands-custom-post-types' );
72+
}
73+
74+
// Only load admin commands if user has SCF admin capabilities.
75+
if ( current_user_can( acf_get_setting( 'capability' ) ) ) {
76+
wp_enqueue_script( 'scf-commands-admin' );
77+
}
78+
}
79+
80+
add_action( 'admin_enqueue_scripts', 'acf_commands_init' );

0 commit comments

Comments
 (0)