diff --git a/assets/src/js/commands/admin-commands.js b/assets/src/js/commands/admin-commands.js new file mode 100644 index 00000000..4756a1ec --- /dev/null +++ b/assets/src/js/commands/admin-commands.js @@ -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 ); +} diff --git a/assets/src/js/commands/custom-post-type-commands.js b/assets/src/js/commands/custom-post-type-commands.js new file mode 100644 index 00000000..9b7ce140 --- /dev/null +++ b/assets/src/js/commands/custom-post-type-commands.js @@ -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 ); +} diff --git a/includes/admin/admin-commands.php b/includes/admin/admin-commands.php new file mode 100644 index 00000000..5d84425c --- /dev/null +++ b/includes/admin/admin-commands.php @@ -0,0 +1,80 @@ +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( + 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' ); diff --git a/includes/assets.php b/includes/assets.php index 6793e273..5b166adb 100644 --- a/includes/assets.php +++ b/includes/assets.php @@ -243,6 +243,28 @@ public function register_scripts() { ); } + wp_register_script( + 'scf-commands-admin', + acf_get_url( 'assets/build/js/commands/scf-admin' . $suffix . '.js' ), + array( 'acf', 'wp-plugins', 'wp-element', 'wp-components', 'wp-data', 'wp-commands', 'wp-i18n', 'wp-dom-ready' ), + $version, + array( + 'in_footer' => true, + 'defer' => true, + ) + ); + + wp_register_script( + 'scf-commands-custom-post-types', + acf_get_url( 'assets/build/js/commands/scf-custom-post-types' . $suffix . '.js' ), + array( 'acf', 'wp-plugins', 'wp-element', 'wp-components', 'wp-data', 'wp-commands', 'wp-i18n', 'wp-dom-ready' ), + $version, + array( + 'in_footer' => true, + 'defer' => true, + ) + ); + // Register styles. foreach ( $styles as $style ) { wp_register_style( @@ -731,4 +753,4 @@ function acf_enqueue_scripts( $args = array() ) { */ function acf_enqueue_uploader() { return acf_get_instance( 'ACF_Assets' )->enqueue_uploader(); -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 11f063b6..b1feaf8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@wordpress/dependency-extraction-webpack-plugin": "^6.20.0", "@wordpress/e2e-test-utils-playwright": "^1.20.0", "@wordpress/prettier-config": "^4.22.0", + "@wordpress/priority-queue": "^3.22.0", "@wordpress/scripts": "^30.14.0", "babel-loader": "^9.2.1", "css-loader": "^7.1.2", @@ -5072,6 +5073,20 @@ "prettier": ">=3" } }, + "node_modules/@wordpress/priority-queue": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.22.0.tgz", + "integrity": "sha512-mu6I1svNj5dYlbd92Zs/Y0JuEizerOhf/zGlJEbuZPquu+n4MFZR89zYpm+OfXvvr9ixKXSou7J8Ca1UjrZuFw==", + "dev": true, + "dependencies": { + "@babel/runtime": "7.25.7", + "requestidlecallback": "^0.3.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "node_modules/@wordpress/scripts": { "version": "30.14.0", "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.14.0.tgz", @@ -17669,6 +17684,12 @@ "regjsparser": "bin/parser" } }, + "node_modules/requestidlecallback": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/requestidlecallback/-/requestidlecallback-0.3.0.tgz", + "integrity": "sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==", + "dev": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index a976d8a9..82b38299 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@wordpress/dependency-extraction-webpack-plugin": "^6.20.0", "@wordpress/e2e-test-utils-playwright": "^1.20.0", "@wordpress/prettier-config": "^4.22.0", + "@wordpress/priority-queue": "^3.22.0", "@wordpress/scripts": "^30.14.0", "babel-loader": "^9.2.1", "css-loader": "^7.1.2", diff --git a/secure-custom-fields.php b/secure-custom-fields.php index 87c872fc..ebce163c 100644 --- a/secure-custom-fields.php +++ b/secure-custom-fields.php @@ -213,6 +213,7 @@ public function initialize() { acf_include( 'includes/admin/admin-notices.php' ); acf_include( 'includes/admin/admin-tools.php' ); acf_include( 'includes/admin/admin-upgrade.php' ); + acf_include( 'includes/admin/admin-commands.php' ); acf_include( 'includes/admin/class-acf-admin-options-page.php' ); } @@ -231,6 +232,7 @@ public function initialize() { add_filter( 'posts_where', array( $this, 'posts_where' ), 10, 2 ); } + /** * Completes the setup process on "init" of earlier. * @@ -840,4 +842,4 @@ function scf_plugin_deactivated_notice() { } add_action( 'pre_current_active_plugins', 'scf_plugin_deactivated_notice' ); -} +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index e44cab84..699b7fe2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,6 +15,8 @@ const commonConfig = { 'js/acf-input': './assets/src/js/acf-input.js', 'js/acf-internal-post-type': './assets/src/js/acf-internal-post-type.js', + 'js/commands/scf-admin': './assets/src/js/commands/admin-commands.js', + 'js/commands/scf-custom-post-types': './assets/src/js/commands/custom-post-type-commands.js', 'js/acf': './assets/src/js/acf.js', 'js/pro/acf-pro-blocks': './assets/src/js/pro/acf-pro-blocks.js', 'js/pro/acf-pro-field-group':