fix: Board admin submenu 404 — register after parent menu (v0.19.1)
Root cause: PRAutoBlogger_Board_Page::on_register_menu was hooked at
admin_menu default priority 10, same as PRAutoBlogger_Admin_Page::on_register_menu,
but registered first in register_admin_hooks(). WordPress calls
get_plugin_page_hookname() at add_submenu_page() time; if add_menu_page()
has not yet fired, $admin_page_hooks['prautoblogger-settings'] is unset and
WP falls back to the admin_page_prautoblogger-board hookname. At request
time WP recomputes the hookname as prautoblogger_page_prautoblogger-board,
finds no registered callback, and calls wp_die() — 404. Same mismatch
prevented board.css and board.js from loading (enqueue gate also checks
prautoblogger_page_ prefix).
Fix:
- class-prautoblogger.php: parent menu (Admin_Page) stays at priority 10;
board submenu (Board_Page) moves to priority 11. Explicit priorities added
with explanatory comment. See ARCHITECTURE.md §Board for the constraint.
- class-board-page.php: add $submenu reorder in on_register_menu() to keep
Board as the first submenu item (primary click target), since the board
entry can no longer be prepended by the auto-duplicate mechanism.
- tests/unit/Admin/BoardMenuRegistrationTest.php: 4 tests (new file):
(1) priority relationship regression guard — FAILS on unfixed code (both
at 10), PASSES with fix (board at 11); (2) fixed execution order;
(3) broken execution order documentation; (4) parent slug correctness.
- ARCHITECTURE.md: §22b updated with menu-ordering constraint.
- CHANGELOG.md: v0.19.1 entry.
Deletion audit: only intentional removals (old wrong-order registration block).
class-prautoblogger.php is 407 lines (was 400 pre-fix, pre-existing P2 debt).
Agent-Session: board-404-hotfix-20260612
This commit is contained in:
@@ -42,6 +42,14 @@ class PRAutoBlogger_Board_Page {
|
||||
* submenu duplicate of the top-level page; we name it "Board" so the
|
||||
* first click goes to the board, not the raw settings page.
|
||||
*
|
||||
* Registration order note: this method is hooked at admin_menu priority 11
|
||||
* so that PRAutoBlogger_Admin_Page::on_register_menu() (priority 10) has
|
||||
* already called add_menu_page(). This ensures WordPress has populated
|
||||
* $admin_page_hooks['prautoblogger-settings'] before add_submenu_page()
|
||||
* runs here, so get_plugin_page_hookname() resolves to the correct
|
||||
* 'prautoblogger_page_prautoblogger-board' suffix rather than the fallback
|
||||
* 'admin_page_prautoblogger-board'. See ARCHITECTURE.md §Board.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_register_menu(): void {
|
||||
@@ -54,6 +62,30 @@ class PRAutoBlogger_Board_Page {
|
||||
self::PAGE_SLUG,
|
||||
array( $this, 'render_page' )
|
||||
);
|
||||
|
||||
// Reorder submenu so Board is the FIRST entry (primary click target).
|
||||
// WordPress auto-appended a duplicate of the top-level page as submenu
|
||||
// index 0; our Board was appended at index 1. We swap them so the
|
||||
// top-level PRAutoBlogger click lands on the Board page, not Settings.
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- intentional submenu reorder.
|
||||
global $submenu;
|
||||
if ( isset( $submenu['prautoblogger-settings'] ) ) {
|
||||
$items = $submenu['prautoblogger-settings'];
|
||||
// Find the Board entry and move it to position 0.
|
||||
$board_idx = null;
|
||||
foreach ( $items as $i => $item ) {
|
||||
if ( isset( $item[2] ) && self::PAGE_SLUG === $item[2] ) {
|
||||
$board_idx = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( null !== $board_idx && $board_idx > 0 ) {
|
||||
$board_item = $items[ $board_idx ];
|
||||
unset( $items[ $board_idx ] );
|
||||
// Prepend board item at position 0 while preserving remaining order.
|
||||
$submenu['prautoblogger-settings'] = array_merge( array( $board_item ), array_values( $items ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,16 +67,23 @@ class PRAutoBlogger {
|
||||
|
||||
/** Register admin-only hooks (settings, notices, metabox, dashboard widget). */
|
||||
private function register_admin_hooks(): void {
|
||||
// Kanban board -- registered first so it appears as the first submenu item.
|
||||
$board_page = new PRAutoBlogger_Board_Page();
|
||||
add_action( 'admin_menu', array( $board_page, 'on_register_menu' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $board_page, 'on_enqueue_assets' ) );
|
||||
|
||||
// Parent top-level menu first (priority 10), then Board submenu (priority 11).
|
||||
// WHY: WordPress computes the page hookname via get_plugin_page_hookname() at
|
||||
// add_submenu_page() time. If add_menu_page() has not yet fired, the parent
|
||||
// slot in $admin_page_hooks is unset and WP falls back to the `admin_page_*`
|
||||
// hookname. At request time WP recomputes the hookname as `prautoblogger_page_*`
|
||||
// and finds no registered callback -> wp_die 404. Fixing priority ensures the
|
||||
// parent slot is populated before the submenu call. See ARCHITECTURE.md §Board.
|
||||
$admin_page = new PRAutoBlogger_Admin_Page();
|
||||
add_action( 'admin_menu', array( $admin_page, 'on_register_menu' ) );
|
||||
add_action( 'admin_menu', array( $admin_page, 'on_register_menu' ), 10 );
|
||||
add_action( 'admin_init', array( $admin_page, 'on_register_settings' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $admin_page, 'on_enqueue_assets' ) );
|
||||
|
||||
// Board at priority 11 -- fires AFTER add_menu_page, so hookname is correct.
|
||||
$board_page = new PRAutoBlogger_Board_Page();
|
||||
add_action( 'admin_menu', array( $board_page, 'on_register_menu' ), 11 );
|
||||
add_action( 'admin_enqueue_scripts', array( $board_page, 'on_enqueue_assets' ) );
|
||||
|
||||
add_action( 'admin_notices', array( new PRAutoBlogger_Admin_Notices(), 'on_display_notices' ) );
|
||||
add_action( 'add_meta_boxes', array( new PRAutoBlogger_Post_Metabox(), 'on_register_metabox' ) );
|
||||
add_action( 'admin_menu', array( new PRAutoBlogger_Metrics_Page(), 'on_register_menu' ) );
|
||||
|
||||
Reference in New Issue
Block a user