From 3859af22a340ac330001848b9fe70d7c3b3ca854 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:14:06 +0000 Subject: [PATCH 1/6] Initial plan From b088863d7188db1448c8bbb262ef837790f65754 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:24:50 +0000 Subject: [PATCH 2/6] Migrate post type meta fields to block editor sidebar with legacy meta box fallback Co-authored-by: mattyza <880803+mattyza@users.noreply.github.com> Agent-Logs-Url: https://github.com/mattyza/starter-plugin/sessions/91b47540-229e-43c5-a052-874979feeb05 --- assets/js/meta-fields.js | 110 +++++++++++++++++ classes/class-starter-plugin-post-type.php | 131 +++++++++++++++++++++ tests/test-starter-plugin.php | 59 ++++++++++ 3 files changed, 300 insertions(+) create mode 100644 assets/js/meta-fields.js diff --git a/assets/js/meta-fields.js b/assets/js/meta-fields.js new file mode 100644 index 0000000..32cb664 --- /dev/null +++ b/assets/js/meta-fields.js @@ -0,0 +1,110 @@ +/** + * Block Editor Meta Fields Sidebar + * + * Registers a PluginDocumentSettingPanel for each field section defined by + * the PHP class. Field definitions are passed via the `starterPluginMetaFields` + * global object that is localised by Starter_Plugin_Post_Type::enqueue_block_editor_assets(). + * + * When the classic editor is active for a post type this script is never + * enqueued, so legacy meta boxes remain in use instead. + * + * @package Starter_Plugin + * @since 1.0.0 + */ +( function () { + var fieldData = window.starterPluginMetaFields; + + if ( ! fieldData || ! fieldData.fields || ! fieldData.fields.length ) { + return; + } + + var el = wp.element.createElement; + var Fragment = wp.element.Fragment; + var registerPlugin = wp.plugins.registerPlugin; + var PluginDocumentSettingPanel = + ( wp.editPost && wp.editPost.PluginDocumentSettingPanel ) || + ( wp.editor && wp.editor.PluginDocumentSettingPanel ); + var TextControl = wp.components.TextControl; + var useSelect = wp.data.useSelect; + var useDispatch = wp.data.useDispatch; + + if ( ! PluginDocumentSettingPanel ) { + return; + } + + // Group fields by their declared section key. + var fieldsBySection = {}; + fieldData.fields.forEach( function ( field ) { + var section = field.section || 'default'; + if ( ! fieldsBySection[ section ] ) { + fieldsBySection[ section ] = []; + } + fieldsBySection[ section ].push( field ); + } ); + + var sectionKeys = Object.keys( fieldsBySection ); + + /** + * Renders one sidebar panel for each field section. + * + * Meta values are read from and written to the block editor's post entity + * through the `core/editor` data store, so they are saved automatically + * when the editor saves the post. + */ + function MetaFieldsPanels() { + var meta = useSelect( function ( select ) { + return select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {}; + }, [] ); + + var editPost = useDispatch( 'core/editor' ).editPost; + + function handleChange( metaKey, value ) { + var update = {}; + update[ metaKey ] = value; + editPost( { meta: update } ); + } + + return el( + Fragment, + null, + sectionKeys.map( function ( sectionKey ) { + var sectionLabel = + fieldData.sections && fieldData.sections[ sectionKey ] + ? fieldData.sections[ sectionKey ] + : sectionKey; + + return el( + PluginDocumentSettingPanel, + { + key: sectionKey, + name: fieldData.postType + '-' + sectionKey, + title: sectionLabel, + className: 'starter-plugin-meta-panel', + }, + fieldsBySection[ sectionKey ].map( function ( field ) { + var metaKey = '_' + field.key; + var value = + meta[ metaKey ] !== undefined + ? meta[ metaKey ] + : field.default || ''; + + return el( TextControl, { + key: field.key, + label: field.name, + help: field.description, + value: value, + type: field.type === 'url' ? 'url' : 'text', + onChange: function ( newValue ) { + handleChange( metaKey, newValue ); + }, + } ); + } ) + ); + } ) + ); + } + + registerPlugin( 'starter-plugin-meta-fields-' + fieldData.postType, { + render: MetaFieldsPanels, + } ); +} )(); diff --git a/classes/class-starter-plugin-post-type.php b/classes/class-starter-plugin-post-type.php index 585b2e2..4cd70ab 100644 --- a/classes/class-starter-plugin-post-type.php +++ b/classes/class-starter-plugin-post-type.php @@ -59,12 +59,14 @@ public function __construct( $post_type = 'thing', $singular = '', $plural = '', $this->args = $args; add_action( 'init', array( $this, 'register_post_type' ) ); + add_action( 'init', array( $this, 'register_post_meta_fields' ) ); if ( is_admin() ) { global $pagenow, $wp_query; add_action( 'admin_menu', array( $this, 'meta_box_setup' ), 20 ); add_action( 'save_post', array( $this, 'meta_box_save' ) ); + add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); add_filter( 'enter_title_here', array( $this, 'enter_title_here' ) ); add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) ); add_filter( 'manage_edit-' . $this->post_type . '_columns', array( $this, 'register_custom_column_headings' ), 10, 1 ); @@ -230,6 +232,9 @@ public function updated_messages ( $messages ) { * @return void */ public function meta_box_setup () { + if ( $this->is_block_editor_active() ) { + return; + } add_meta_box( $this->post_type . '-data', __( 'Thing Details', 'starter-plugin' ), array( $this, 'meta_box_content' ), $this->post_type, 'side', 'high' ); } @@ -314,6 +319,132 @@ public function meta_box_save ( $post_id ) { } } + /** + * Register post meta fields for REST API access and block editor support. + * + * Iterates over the field definitions returned by get_custom_fields_settings() + * and calls register_post_meta() for each one so that field values are + * readable and writable through the REST API (and therefore by the block editor). + * + * @access public + * @since 1.0.0 + * @return void + */ + public function register_post_meta_fields() { + $fields = $this->get_custom_fields_settings(); + + foreach ( $fields as $key => $field ) { + $type = isset( $field['type'] ) ? $field['type'] : 'text'; + + register_post_meta( + $this->post_type, + '_' . $key, + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'default' => isset( $field['default'] ) ? $field['default'] : '', + 'sanitize_callback' => ( 'url' === $type ) ? 'esc_url_raw' : 'sanitize_text_field', + 'auth_callback' => function () { + return current_user_can( 'edit_posts' ); + }, + ) + ); + } + } + + /** + * Get the section labels for the custom fields. + * + * Each key matches the 'section' value used in get_custom_fields_settings(). + * The value is the human-readable panel title shown in the block editor sidebar + * (and as the meta box title in the classic editor). + * + * @access public + * @since 1.0.0 + * @return array + */ + public function get_field_sections() { + $sections = array( + 'info' => __( 'Details', 'starter-plugin' ), + ); + + return apply_filters( 'starter_plugin_field_sections', $sections ); + } + + /** + * Whether the block editor is active for this post type. + * + * Returns true when Gutenberg (the block editor) handles editing for this + * post type, false when the classic editor is in use (e.g. the Classic + * Editor plugin is installed and configured to use the old editor). + * + * @access protected + * @since 1.0.0 + * @return bool + */ + protected function is_block_editor_active() { + if ( function_exists( 'use_block_editor_for_post_type' ) ) { + return use_block_editor_for_post_type( $this->post_type ); + } + + return false; + } + + /** + * Enqueue block editor assets for the meta fields sidebar panels. + * + * Runs only on the block editor screen for this post type. Enqueues the + * meta-fields JS and passes the field + section definitions as localised + * data so the script can build the correct sidebar panels without any + * hard-coded field knowledge. + * + * @access public + * @since 1.0.0 + * @return void + */ + public function enqueue_block_editor_assets() { + $screen = get_current_screen(); + if ( ! $screen || $screen->post_type !== $this->post_type ) { + return; + } + + $field_data = $this->get_custom_fields_settings(); + $sections = $this->get_field_sections(); + $fields_json = array(); + + foreach ( $field_data as $key => $field ) { + $fields_json[] = array( + 'key' => $key, + 'name' => isset( $field['name'] ) ? $field['name'] : $key, + 'description' => isset( $field['description'] ) ? $field['description'] : '', + 'type' => isset( $field['type'] ) ? $field['type'] : 'text', + 'default' => isset( $field['default'] ) ? $field['default'] : '', + 'section' => isset( $field['section'] ) ? $field['section'] : 'default', + ); + } + + $handle = 'starter-plugin-' . $this->post_type . '-meta-fields'; + + wp_enqueue_script( + $handle, + Starter_Plugin()->plugin_url . 'assets/js/meta-fields.js', + array( 'wp-plugins', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-data' ), + Starter_Plugin()->version, + true + ); + + wp_localize_script( + $handle, + 'starterPluginMetaFields', + array( + 'fields' => $fields_json, + 'sections' => $sections, + 'postType' => $this->post_type, + ) + ); + } + /** * Customise the "Enter title here" text. * @access public diff --git a/tests/test-starter-plugin.php b/tests/test-starter-plugin.php index 7a5fa7c..7de0093 100644 --- a/tests/test-starter-plugin.php +++ b/tests/test-starter-plugin.php @@ -52,4 +52,63 @@ public function test_has_load_plugin_textdomain() { $this->assertTrue( $has_load_plugin_textdomain ); } + + /** + * register_post_meta_fields() should register each field via register_post_meta() + * with show_in_rest enabled so the block editor can read and write the values. + */ + public function test_post_meta_fields_registered_in_rest() { + $post_type_obj = $this->starter_plugin->post_types['thing']; + $post_type_obj->register_post_meta_fields(); + + $registered = get_registered_meta_keys( 'post', 'thing' ); + + $this->assertArrayHasKey( '_url', $registered ); + $this->assertTrue( $registered['_url']['show_in_rest'] ); + } + + /** + * get_field_sections() should return an array that includes the 'info' section. + */ + public function test_get_field_sections_has_info_section() { + $post_type_obj = $this->starter_plugin->post_types['thing']; + $sections = $post_type_obj->get_field_sections(); + + $this->assertIsArray( $sections ); + $this->assertArrayHasKey( 'info', $sections ); + } + + /** + * The 'starter_plugin_field_sections' filter should allow external code + * to add or modify sections. + */ + public function test_get_field_sections_is_filterable() { + add_filter( + 'starter_plugin_field_sections', + function( $sections ) { + $sections['extra'] = 'Extra'; + return $sections; + } + ); + + $post_type_obj = $this->starter_plugin->post_types['thing']; + $sections = $post_type_obj->get_field_sections(); + + $this->assertArrayHasKey( 'extra', $sections ); + + // Clean up. + remove_all_filters( 'starter_plugin_field_sections' ); + } + + /** + * The init action should include a callback for register_post_meta_fields() + * so that meta registration runs at the correct hook. + */ + public function test_register_post_meta_fields_hooked_on_init() { + $post_type_obj = $this->starter_plugin->post_types['thing']; + $priority = has_action( 'init', array( $post_type_obj, 'register_post_meta_fields' ) ); + + $this->assertIsInt( $priority ); + } } + From 4a8f06c2e65b058b6b977c398c0c4c6b4b796998 Mon Sep 17 00:00:00 2001 From: Matt Cohen Date: Sun, 22 Mar 2026 21:20:52 +0200 Subject: [PATCH 3/6] Update meta-fields.js to reverse PluginDocumentSettingPanel order and destructure useDispatch result --- assets/js/meta-fields.js | 113 +++------------------------------------ 1 file changed, 6 insertions(+), 107 deletions(-) diff --git a/assets/js/meta-fields.js b/assets/js/meta-fields.js index 32cb664..e0acd0f 100644 --- a/assets/js/meta-fields.js +++ b/assets/js/meta-fields.js @@ -1,110 +1,9 @@ -/** - * Block Editor Meta Fields Sidebar - * - * Registers a PluginDocumentSettingPanel for each field section defined by - * the PHP class. Field definitions are passed via the `starterPluginMetaFields` - * global object that is localised by Starter_Plugin_Post_Type::enqueue_block_editor_assets(). - * - * When the classic editor is active for a post type this script is never - * enqueued, so legacy meta boxes remain in use instead. - * - * @package Starter_Plugin - * @since 1.0.0 - */ -( function () { - var fieldData = window.starterPluginMetaFields; +var PluginDocumentSettingPanel = + ( wp.editor && wp.editor.PluginDocumentSettingPanel ) || + ( wp.editPost && wp.editPost.PluginDocumentSettingPanel ); - if ( ! fieldData || ! fieldData.fields || ! fieldData.fields.length ) { - return; - } +// ... (other code) - var el = wp.element.createElement; - var Fragment = wp.element.Fragment; - var registerPlugin = wp.plugins.registerPlugin; - var PluginDocumentSettingPanel = - ( wp.editPost && wp.editPost.PluginDocumentSettingPanel ) || - ( wp.editor && wp.editor.PluginDocumentSettingPanel ); - var TextControl = wp.components.TextControl; - var useSelect = wp.data.useSelect; - var useDispatch = wp.data.useDispatch; +var { editPost } = useDispatch( 'core/editor' ); - if ( ! PluginDocumentSettingPanel ) { - return; - } - - // Group fields by their declared section key. - var fieldsBySection = {}; - fieldData.fields.forEach( function ( field ) { - var section = field.section || 'default'; - if ( ! fieldsBySection[ section ] ) { - fieldsBySection[ section ] = []; - } - fieldsBySection[ section ].push( field ); - } ); - - var sectionKeys = Object.keys( fieldsBySection ); - - /** - * Renders one sidebar panel for each field section. - * - * Meta values are read from and written to the block editor's post entity - * through the `core/editor` data store, so they are saved automatically - * when the editor saves the post. - */ - function MetaFieldsPanels() { - var meta = useSelect( function ( select ) { - return select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {}; - }, [] ); - - var editPost = useDispatch( 'core/editor' ).editPost; - - function handleChange( metaKey, value ) { - var update = {}; - update[ metaKey ] = value; - editPost( { meta: update } ); - } - - return el( - Fragment, - null, - sectionKeys.map( function ( sectionKey ) { - var sectionLabel = - fieldData.sections && fieldData.sections[ sectionKey ] - ? fieldData.sections[ sectionKey ] - : sectionKey; - - return el( - PluginDocumentSettingPanel, - { - key: sectionKey, - name: fieldData.postType + '-' + sectionKey, - title: sectionLabel, - className: 'starter-plugin-meta-panel', - }, - fieldsBySection[ sectionKey ].map( function ( field ) { - var metaKey = '_' + field.key; - var value = - meta[ metaKey ] !== undefined - ? meta[ metaKey ] - : field.default || ''; - - return el( TextControl, { - key: field.key, - label: field.name, - help: field.description, - value: value, - type: field.type === 'url' ? 'url' : 'text', - onChange: function ( newValue ) { - handleChange( metaKey, newValue ); - }, - } ); - } ) - ); - } ) - ); - } - - registerPlugin( 'starter-plugin-meta-fields-' + fieldData.postType, { - render: MetaFieldsPanels, - } ); -} )(); +// ... (other code) \ No newline at end of file From 9b065e6ed5d7f6a232f5be486eaf0124bb65b43b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 19:56:54 +0000 Subject: [PATCH 4/6] Fix auth_callback per-post permission, docblock, and asset URL in meta-fields migration Co-authored-by: mattyza <880803+mattyza@users.noreply.github.com> Agent-Logs-Url: https://github.com/mattyza/starter-plugin/sessions/51678230-aea0-445b-8613-da1a78cdd20e --- classes/class-starter-plugin-post-type.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/classes/class-starter-plugin-post-type.php b/classes/class-starter-plugin-post-type.php index 4cd70ab..5cbb04c 100644 --- a/classes/class-starter-plugin-post-type.php +++ b/classes/class-starter-plugin-post-type.php @@ -345,8 +345,8 @@ public function register_post_meta_fields() { 'type' => 'string', 'default' => isset( $field['default'] ) ? $field['default'] : '', 'sanitize_callback' => ( 'url' === $type ) ? 'esc_url_raw' : 'sanitize_text_field', - 'auth_callback' => function () { - return current_user_can( 'edit_posts' ); + 'auth_callback' => function ( $allowed, $meta_key, $post_id ) { + return current_user_can( 'edit_post', $post_id ); }, ) ); @@ -357,8 +357,7 @@ public function register_post_meta_fields() { * Get the section labels for the custom fields. * * Each key matches the 'section' value used in get_custom_fields_settings(). - * The value is the human-readable panel title shown in the block editor sidebar - * (and as the meta box title in the classic editor). + * The value is the human-readable panel title shown in the block editor sidebar. * * @access public * @since 1.0.0 @@ -428,7 +427,7 @@ public function enqueue_block_editor_assets() { wp_enqueue_script( $handle, - Starter_Plugin()->plugin_url . 'assets/js/meta-fields.js', + plugins_url( '../assets/js/meta-fields.js', __FILE__ ), array( 'wp-plugins', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-data' ), Starter_Plugin()->version, true From 34aa0ee888ae98258fa024b4c10dd11643c5e5da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 07:27:30 +0000 Subject: [PATCH 5/6] Restore full meta-fields.js (lost in 4a8f06c) with correct PluginDocumentSettingPanel priority and destructured useDispatch Co-authored-by: mattyza <880803+mattyza@users.noreply.github.com> Agent-Logs-Url: https://github.com/mattyza/starter-plugin/sessions/78000d73-c13a-4e2d-a7b4-d1a7d7731b98 --- assets/js/meta-fields.js | 113 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 6 deletions(-) diff --git a/assets/js/meta-fields.js b/assets/js/meta-fields.js index e0acd0f..ddfdeb6 100644 --- a/assets/js/meta-fields.js +++ b/assets/js/meta-fields.js @@ -1,9 +1,110 @@ -var PluginDocumentSettingPanel = - ( wp.editor && wp.editor.PluginDocumentSettingPanel ) || - ( wp.editPost && wp.editPost.PluginDocumentSettingPanel ); +/** + * Block Editor Meta Fields Sidebar + * + * Registers a PluginDocumentSettingPanel for each field section defined by + * the PHP class. Field definitions are passed via the `starterPluginMetaFields` + * global object that is localised by Starter_Plugin_Post_Type::enqueue_block_editor_assets(). + * + * When the classic editor is active for a post type this script is never + * enqueued, so legacy meta boxes remain in use instead. + * + * @package Starter_Plugin + * @since 1.0.0 + */ +( function () { + var fieldData = window.starterPluginMetaFields; -// ... (other code) + if ( ! fieldData || ! fieldData.fields || ! fieldData.fields.length ) { + return; + } -var { editPost } = useDispatch( 'core/editor' ); + var el = wp.element.createElement; + var Fragment = wp.element.Fragment; + var registerPlugin = wp.plugins.registerPlugin; + var PluginDocumentSettingPanel = + ( wp.editor && wp.editor.PluginDocumentSettingPanel ) || + ( wp.editPost && wp.editPost.PluginDocumentSettingPanel ); + var TextControl = wp.components.TextControl; + var useSelect = wp.data.useSelect; + var useDispatch = wp.data.useDispatch; -// ... (other code) \ No newline at end of file + if ( ! PluginDocumentSettingPanel ) { + return; + } + + // Group fields by their declared section key. + var fieldsBySection = {}; + fieldData.fields.forEach( function ( field ) { + var section = field.section || 'default'; + if ( ! fieldsBySection[ section ] ) { + fieldsBySection[ section ] = []; + } + fieldsBySection[ section ].push( field ); + } ); + + var sectionKeys = Object.keys( fieldsBySection ); + + /** + * Renders one sidebar panel for each field section. + * + * Meta values are read from and written to the block editor's post entity + * through the `core/editor` data store, so they are saved automatically + * when the editor saves the post. + */ + function MetaFieldsPanels() { + var meta = useSelect( function ( select ) { + return select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {}; + }, [] ); + + var { editPost } = useDispatch( 'core/editor' ); + + function handleChange( metaKey, value ) { + var update = {}; + update[ metaKey ] = value; + editPost( { meta: update } ); + } + + return el( + Fragment, + null, + sectionKeys.map( function ( sectionKey ) { + var sectionLabel = + fieldData.sections && fieldData.sections[ sectionKey ] + ? fieldData.sections[ sectionKey ] + : sectionKey; + + return el( + PluginDocumentSettingPanel, + { + key: sectionKey, + name: fieldData.postType + '-' + sectionKey, + title: sectionLabel, + className: 'starter-plugin-meta-panel', + }, + fieldsBySection[ sectionKey ].map( function ( field ) { + var metaKey = '_' + field.key; + var value = + meta[ metaKey ] !== undefined + ? meta[ metaKey ] + : field.default || ''; + + return el( TextControl, { + key: field.key, + label: field.name, + help: field.description, + value: value, + type: field.type === 'url' ? 'url' : 'text', + onChange: function ( newValue ) { + handleChange( metaKey, newValue ); + }, + } ); + } ) + ); + } ) + ); + } + + registerPlugin( 'starter-plugin-meta-fields-' + fieldData.postType, { + render: MetaFieldsPanels, + } ); +} )(); \ No newline at end of file From a956007252099e70018f93243d292ff11adc7e59 Mon Sep 17 00:00:00 2001 From: Matt Cohen <880803+mattyza@users.noreply.github.com> Date: Wed, 25 Mar 2026 09:13:11 +0200 Subject: [PATCH 6/6] Ensures the new meta fields save and display correctly, and moves the code into it's own file --- assets/js/meta-fields.js | 20 +- ...lass-starter-plugin-post-type-meta-box.php | 159 +++++++++++++ ...s-starter-plugin-post-type-meta-fields.php | 151 ++++++++++++ classes/class-starter-plugin-post-type.php | 219 ++---------------- classes/class-starter-plugin.php | 2 + 5 files changed, 344 insertions(+), 207 deletions(-) create mode 100644 classes/class-starter-plugin-post-type-meta-box.php create mode 100644 classes/class-starter-plugin-post-type-meta-fields.php diff --git a/assets/js/meta-fields.js b/assets/js/meta-fields.js index ddfdeb6..5c48b03 100644 --- a/assets/js/meta-fields.js +++ b/assets/js/meta-fields.js @@ -18,15 +18,13 @@ return; } - var el = wp.element.createElement; - var Fragment = wp.element.Fragment; - var registerPlugin = wp.plugins.registerPlugin; - var PluginDocumentSettingPanel = - ( wp.editor && wp.editor.PluginDocumentSettingPanel ) || - ( wp.editPost && wp.editPost.PluginDocumentSettingPanel ); - var TextControl = wp.components.TextControl; - var useSelect = wp.data.useSelect; - var useDispatch = wp.data.useDispatch; + var el = wp.element.createElement; + var Fragment = wp.element.Fragment; + var registerPlugin = wp.plugins.registerPlugin; + var PluginDocumentSettingPanel = wp.editor.PluginDocumentSettingPanel; + var TextControl = wp.components.TextControl; + var useSelect = wp.data.useSelect; + var useDispatch = wp.data.useDispatch; if ( ! PluginDocumentSettingPanel ) { return; @@ -54,12 +52,12 @@ function MetaFieldsPanels() { var meta = useSelect( function ( select ) { return select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {}; - }, [] ); + } ); var { editPost } = useDispatch( 'core/editor' ); function handleChange( metaKey, value ) { - var update = {}; + var update = {}; update[ metaKey ] = value; editPost( { meta: update } ); } diff --git a/classes/class-starter-plugin-post-type-meta-box.php b/classes/class-starter-plugin-post-type-meta-box.php new file mode 100644 index 0000000..ea39221 --- /dev/null +++ b/classes/class-starter-plugin-post-type-meta-box.php @@ -0,0 +1,159 @@ +post_type = $post_type; + $this->fields_callback = $fields_callback; + + add_action( 'admin_menu', array( $this, 'setup' ), 20 ); + add_action( 'save_post', array( $this, 'save' ) ); + } + + /** + * Register the meta box. + * + * Only runs when the classic editor is active for this post type. This check + * is intentionally deferred to the admin_menu hook so that init has already + * fired and the post type object exists when use_block_editor_for_post_type() + * is called. + * + * @access public + * @since 1.0.0 + * @return void + */ + public function setup() { + if ( function_exists( 'use_block_editor_for_post_type' ) && use_block_editor_for_post_type( $this->post_type ) ) { + return; + } + + add_meta_box( + $this->post_type . '-data', + __( 'Thing Details', 'starter-plugin' ), + array( $this, 'render' ), + $this->post_type, + 'side', + 'high' + ); + } + + /** + * Render the meta box contents. + * + * @access public + * @since 1.0.0 + * @return void + */ + public function render() { + global $post_id; + $fields = get_post_custom( $post_id ); + $field_data = call_user_func( $this->fields_callback ); + + $html = ''; + + $html .= ''; + + if ( 0 < count( $field_data ) ) : + foreach ( $field_data as $k => $v ) : + $data = $v['default']; + if ( isset( $fields[ '_' . $k ] ) && isset( $fields[ '_' . $k ][0] ) ) { + $data = $fields[ '_' . $k ][0]; + } + ?> +

+

+

+ post_type ) { + return $post_id; + } + + if ( ! isset( $_POST[ 'starter_plugin_' . $this->post_type . '_noonce' ] ) || ! wp_verify_nonce( $_POST[ 'starter_plugin_' . $this->post_type . '_noonce' ], plugin_basename( dirname( Starter_Plugin()->plugin_path ) ) ) ) { // phpcs:ignore + return $post_id; + } + + if ( isset( $_POST['post_type'] ) && 'page' === esc_attr( $_POST['post_type'] ) ) { // phpcs:ignore + if ( ! current_user_can( 'edit_page', $post_id ) ) { + return $post_id; + } + } else { + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return $post_id; + } + } + + $field_data = call_user_func( $this->fields_callback ); + $fields = array_keys( $field_data ); + + foreach ( $fields as $f ) { + ${$f} = wp_strip_all_tags( trim( $_POST[ $f ] ) ); // phpcs:ignore + + if ( 'url' === $field_data[ $f ]['type'] ) { + ${$f} = esc_url( ${$f} ); + } + + if ( '' === get_post_meta( $post_id, '_' . $f ) ) { + add_post_meta( $post_id, '_' . $f, ${$f}, true ); + } elseif ( get_post_meta( $post_id, '_' . $f, true ) !== ${$f} ) { + update_post_meta( $post_id, '_' . $f, ${$f} ); + } elseif ( '' === ${$f} ) { + delete_post_meta( $post_id, '_' . $f, get_post_meta( $post_id, '_' . $f, true ) ); + } + } + } +} diff --git a/classes/class-starter-plugin-post-type-meta-fields.php b/classes/class-starter-plugin-post-type-meta-fields.php new file mode 100644 index 0000000..d7b1fcd --- /dev/null +++ b/classes/class-starter-plugin-post-type-meta-fields.php @@ -0,0 +1,151 @@ +post_type = $post_type; + $this->fields_callback = $fields_callback; + $this->sections_callback = $sections_callback; + + add_action( 'init', array( $this, 'register' ) ); + + if ( is_admin() ) { + add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); + } + } + + /** + * Register post meta fields for REST API access and block editor support. + * + * Calls register_post_meta() for each field so that values are readable + * and writable through the REST API and therefore by the block editor sidebar. + * + * @access public + * @since 1.0.0 + * @return void + */ + public function register() { + $fields = call_user_func( $this->fields_callback ); + + foreach ( $fields as $key => $field ) { + $type = isset( $field['type'] ) ? $field['type'] : 'text'; + + register_post_meta( + $this->post_type, + '_' . $key, + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'default' => isset( $field['default'] ) ? $field['default'] : '', + 'sanitize_callback' => ( 'url' === $type ) ? 'esc_url_raw' : 'sanitize_text_field', + 'auth_callback' => function ( $allowed, $meta_key, $object_id, $user_id, $cap, $caps ) { + return current_user_can( 'edit_post', $object_id ); + }, + ) + ); + } + } + + /** + * Enqueue block editor assets for the meta fields sidebar panels. + * + * Runs only on the block editor screen for this post type. Enqueues + * meta-fields.js and passes field + section definitions as localised data. + * + * @access public + * @since 1.0.0 + * @return void + */ + public function enqueue_block_editor_assets() { + $screen = get_current_screen(); + if ( ! $screen || $screen->post_type !== $this->post_type ) { + return; + } + + $field_data = call_user_func( $this->fields_callback ); + $sections = call_user_func( $this->sections_callback ); + $fields_json = array(); + + foreach ( $field_data as $key => $field ) { + $fields_json[] = array( + 'key' => $key, + 'name' => isset( $field['name'] ) ? $field['name'] : $key, + 'description' => isset( $field['description'] ) ? $field['description'] : '', + 'type' => isset( $field['type'] ) ? $field['type'] : 'text', + 'default' => isset( $field['default'] ) ? $field['default'] : '', + 'section' => isset( $field['section'] ) ? $field['section'] : 'default', + ); + } + + $handle = 'starter-plugin-' . $this->post_type . '-meta-fields'; + + wp_enqueue_script( + $handle, + plugins_url( '../assets/js/meta-fields.js', __FILE__ ), + array( 'wp-plugins', 'wp-editor', 'wp-element', 'wp-components', 'wp-data' ), + Starter_Plugin()->version, + true + ); + + wp_localize_script( + $handle, + 'starterPluginMetaFields', + array( + 'fields' => $fields_json, + 'sections' => $sections, + 'postType' => $this->post_type, + ) + ); + } +} diff --git a/classes/class-starter-plugin-post-type.php b/classes/class-starter-plugin-post-type.php index 5cbb04c..a1db079 100644 --- a/classes/class-starter-plugin-post-type.php +++ b/classes/class-starter-plugin-post-type.php @@ -59,14 +59,23 @@ public function __construct( $post_type = 'thing', $singular = '', $plural = '', $this->args = $args; add_action( 'init', array( $this, 'register_post_type' ) ); - add_action( 'init', array( $this, 'register_post_meta_fields' ) ); + // Delegate meta field registration and block editor sidebar to the dedicated class. + new Starter_Plugin_Post_Type_Meta_Fields( + $post_type, + array( $this, 'get_custom_fields_settings' ), + array( $this, 'get_field_sections' ) + ); + + // Always wire up the meta box class — it guards itself inside setup() once init has run. if ( is_admin() ) { - global $pagenow, $wp_query; + new Starter_Plugin_Post_Type_Meta_Box( + $post_type, + array( $this, 'get_custom_fields_settings' ) + ); + } - add_action( 'admin_menu', array( $this, 'meta_box_setup' ), 20 ); - add_action( 'save_post', array( $this, 'meta_box_save' ) ); - add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) ); + if ( is_admin() ) { add_filter( 'enter_title_here', array( $this, 'enter_title_here' ) ); add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) ); add_filter( 'manage_edit-' . $this->post_type . '_columns', array( $this, 'register_custom_column_headings' ), 10, 1 ); @@ -117,7 +126,7 @@ public function register_post_type () { 'capability_type' => 'post', 'has_archive' => $archive_slug, 'hierarchical' => false, - 'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes' ), + 'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes', 'custom-fields' ), 'menu_position' => 5, 'menu_icon' => 'dashicons-smiley', ); @@ -225,152 +234,6 @@ public function updated_messages ( $messages ) { return $messages; } - /** - * Setup the meta box. - * @access public - * @since 1.0.0 - * @return void - */ - public function meta_box_setup () { - if ( $this->is_block_editor_active() ) { - return; - } - add_meta_box( $this->post_type . '-data', __( 'Thing Details', 'starter-plugin' ), array( $this, 'meta_box_content' ), $this->post_type, 'side', 'high' ); - } - - /** - * The contents of our meta box. - * @access public - * @since 1.0.0 - * @return void - */ - public function meta_box_content () { - global $post_id; - $fields = get_post_custom( $post_id ); - $field_data = $this->get_custom_fields_settings(); - - $html = ''; - - $html .= ''; - - if ( 0 < count( $field_data ) ) : - foreach ( $field_data as $k => $v ) : - $data = $v['default']; - if ( isset( $fields[ '_' . $k ] ) && isset( $fields[ '_' . $k ][0] ) ) { - $data = $fields[ '_' . $k ][0]; - } - ?> -

-

-

- post_type ) { - return $post_id; - } - - if ( ! isset( $_POST[ 'starter_plugin_' . $this->post_type . '_noonce' ] ) || ! wp_verify_nonce( $_POST[ 'starter_plugin_' . $this->post_type . '_noonce' ], plugin_basename( dirname( Starter_Plugin()->plugin_path ) ) ) ) { - return $post_id; - } - - if ( isset( $_POST['post_type'] ) && 'page' === esc_attr( $_POST['post_type'] ) ) { - if ( ! current_user_can( 'edit_page', $post_id ) ) { - return $post_id; - } - } else { - if ( ! current_user_can( 'edit_post', $post_id ) ) { - return $post_id; - } - } - - $field_data = $this->get_custom_fields_settings(); - $fields = array_keys( $field_data ); - - foreach ( $fields as $f ) { - - ${$f} = wp_strip_all_tags( trim( $_POST[ $f ] ) ); - - // Escape the URLs. - if ( 'url' === $field_data[ $f ]['type'] ) { - ${$f} = esc_url( ${$f} ); - } - - if ( '' === get_post_meta( $post_id, '_' . $f ) ) { - add_post_meta( $post_id, '_' . $f, ${$f}, true ); - } elseif ( get_post_meta( $post_id, '_' . $f, true ) !== ${$f} ) { - update_post_meta( $post_id, '_' . $f, ${$f} ); - } elseif ( '' === ${$f} ) { - delete_post_meta( $post_id, '_' . $f, get_post_meta( $post_id, '_' . $f, true ) ); - } - } - } - - /** - * Register post meta fields for REST API access and block editor support. - * - * Iterates over the field definitions returned by get_custom_fields_settings() - * and calls register_post_meta() for each one so that field values are - * readable and writable through the REST API (and therefore by the block editor). - * - * @access public - * @since 1.0.0 - * @return void - */ - public function register_post_meta_fields() { - $fields = $this->get_custom_fields_settings(); - - foreach ( $fields as $key => $field ) { - $type = isset( $field['type'] ) ? $field['type'] : 'text'; - - register_post_meta( - $this->post_type, - '_' . $key, - array( - 'show_in_rest' => true, - 'single' => true, - 'type' => 'string', - 'default' => isset( $field['default'] ) ? $field['default'] : '', - 'sanitize_callback' => ( 'url' === $type ) ? 'esc_url_raw' : 'sanitize_text_field', - 'auth_callback' => function ( $allowed, $meta_key, $post_id ) { - return current_user_can( 'edit_post', $post_id ); - }, - ) - ); - } - } - - /** - * Get the section labels for the custom fields. - * - * Each key matches the 'section' value used in get_custom_fields_settings(). - * The value is the human-readable panel title shown in the block editor sidebar. - * - * @access public - * @since 1.0.0 - * @return array - */ - public function get_field_sections() { - $sections = array( - 'info' => __( 'Details', 'starter-plugin' ), - ); - - return apply_filters( 'starter_plugin_field_sections', $sections ); - } - /** * Whether the block editor is active for this post type. * @@ -391,57 +254,21 @@ protected function is_block_editor_active() { } /** - * Enqueue block editor assets for the meta fields sidebar panels. + * Get the section labels for the custom fields. * - * Runs only on the block editor screen for this post type. Enqueues the - * meta-fields JS and passes the field + section definitions as localised - * data so the script can build the correct sidebar panels without any - * hard-coded field knowledge. + * Each key matches the 'section' value used in get_custom_fields_settings(). + * The value is the human-readable panel title shown in the block editor sidebar. * * @access public * @since 1.0.0 - * @return void + * @return array */ - public function enqueue_block_editor_assets() { - $screen = get_current_screen(); - if ( ! $screen || $screen->post_type !== $this->post_type ) { - return; - } - - $field_data = $this->get_custom_fields_settings(); - $sections = $this->get_field_sections(); - $fields_json = array(); - - foreach ( $field_data as $key => $field ) { - $fields_json[] = array( - 'key' => $key, - 'name' => isset( $field['name'] ) ? $field['name'] : $key, - 'description' => isset( $field['description'] ) ? $field['description'] : '', - 'type' => isset( $field['type'] ) ? $field['type'] : 'text', - 'default' => isset( $field['default'] ) ? $field['default'] : '', - 'section' => isset( $field['section'] ) ? $field['section'] : 'default', - ); - } - - $handle = 'starter-plugin-' . $this->post_type . '-meta-fields'; - - wp_enqueue_script( - $handle, - plugins_url( '../assets/js/meta-fields.js', __FILE__ ), - array( 'wp-plugins', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-data' ), - Starter_Plugin()->version, - true + public function get_field_sections() { + $sections = array( + 'info' => __( 'Details', 'starter-plugin' ), ); - wp_localize_script( - $handle, - 'starterPluginMetaFields', - array( - 'fields' => $fields_json, - 'sections' => $sections, - 'postType' => $this->post_type, - ) - ); + return apply_filters( 'starter_plugin_field_sections', $sections ); } /** diff --git a/classes/class-starter-plugin.php b/classes/class-starter-plugin.php index 643c297..e2f2eca 100644 --- a/classes/class-starter-plugin.php +++ b/classes/class-starter-plugin.php @@ -106,6 +106,8 @@ public function __construct () { // Admin - End // Post Types - Start + require_once 'class-starter-plugin-post-type-meta-fields.php'; + require_once 'class-starter-plugin-post-type-meta-box.php'; require_once 'class-starter-plugin-post-type.php'; require_once 'class-starter-plugin-taxonomy.php';