*/ const SETTINGS_SCREENS = array( 'ai-o-optimiser-settings' => 'settings', 'ai-o-optimiser-notifications' => 'notifications', 'ai-o-optimiser-system-check' => 'system-check', 'ai-o-optimiser-environment-check' => 'safety-check', 'ai-o-optimiser-logs' => 'logs', 'ai-o-optimiser-deep-review-prompt' => 'review-prompt', ); /** Replace the legacy in-window nav renderer and promote Local. */ public function __construct( AI_O_Optimiser_Admin $admin ) { $this->admin = $admin; remove_action( 'admin_notices', array( $admin, 'render_global_nav' ), 1 ); add_action( 'admin_notices', array( $this, 'render_global_nav' ), 1 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_shell_assets' ) ); /* * The rail is the product's primary navigation as of 1.37.0. It prints at * admin_notices priority 0, ahead of the shell below, because it is a * float and a float only clears what follows it in source order. */ if ( class_exists( 'AI_O_Optimiser_Admin_Rail' ) ) { AI_O_Optimiser_Admin_Rail::boot(); } // AI_O_Optimiser_Admin performs its historical menu cleanup at 9999. Run immediately // afterwards so Local can be promoted without changing its stable URL. add_action( 'admin_menu', array( $this, 'finalise_primary_menu' ), 10000 ); } /** Load the compact shared admin shell only on AI-O workspaces. */ public function enqueue_shell_assets() { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- admin routing only. $pages = array( 'ai-o-optimiser-dashboard', 'ai-o-optimiser-optimise', 'ai-o-optimiser-metadata-workflow', 'ai-o-optimiser-content-opportunities', 'ai-o-optimiser-schema-review', 'ai-o-optimiser-local-discovery', 'ai-o-optimiser-system-check', 'ai-o-optimiser-guidance', 'ai-o-optimiser-environment-check', 'ai-o-optimiser-logs', 'ai-o-optimiser-deep-review-prompt', 'ai-o-optimiser-settings', 'ai-o-optimiser-notifications', ); if ( ! in_array( $page, $pages, true ) ) { return; } // Stylesheet only. assets/admin-shell.js existed to lift the Local tab // row into the slot after first paint, retrying on a timer and watching // the DOM for it; the row is printed into the slot by the server now. wp_enqueue_style( 'ai-o-optimiser-admin-shell', AI_O_Optimiser_PLUGIN_URL . 'assets/admin-shell.css', array(), AI_O_Optimiser_VERSION ); } /** Promote Local into the WordPress submenu, then enforce primary order. */ public function finalise_primary_menu() { global $submenu; if ( ! current_user_can( AI_O_Optimiser_SENSITIVE_CAPABILITY ) ) { return; } if ( ! $this->submenu_has_slug( 'ai-o-optimiser-schema-review' ) ) { add_submenu_page( 'ai-o-optimiser-dashboard', __( 'Schema', 'ai-o-optimiser' ), __( 'Schema', 'ai-o-optimiser' ), AI_O_Optimiser_SENSITIVE_CAPABILITY, 'ai-o-optimiser-schema-review', array( $this, 'render_schema_entrypoint' ), 30 ); } if ( ! $this->submenu_has_slug( 'ai-o-optimiser-local-discovery' ) ) { add_submenu_page( 'ai-o-optimiser-dashboard', __( 'NAP', 'ai-o-optimiser' ), __( 'NAP', 'ai-o-optimiser' ), AI_O_Optimiser_SENSITIVE_CAPABILITY, 'ai-o-optimiser-local-discovery', array( $this, 'render_local_entrypoint' ), 40 ); } if ( empty( $submenu['ai-o-optimiser-dashboard'] ) || ! is_array( $submenu['ai-o-optimiser-dashboard'] ) ) { return; } $desired_order = array( 'ai-o-optimiser-dashboard', 'ai-o-optimiser-optimise', 'ai-o-optimiser-schema-review', 'ai-o-optimiser-local-discovery', 'ai-o-optimiser-settings', ); $by_slug = array(); foreach ( $submenu['ai-o-optimiser-dashboard'] as $item ) { if ( isset( $item[2] ) ) { $by_slug[ $item[2] ] = $item; } } $ordered = array(); foreach ( $desired_order as $slug ) { if ( isset( $by_slug[ $slug ] ) ) { $ordered[] = $by_slug[ $slug ]; } } $submenu['ai-o-optimiser-dashboard'] = $ordered; } /** Check whether the Optimiser submenu already contains a slug. */ private function submenu_has_slug( $slug ) { global $submenu; if ( empty( $submenu['ai-o-optimiser-dashboard'] ) || ! is_array( $submenu['ai-o-optimiser-dashboard'] ) ) { return false; } foreach ( $submenu['ai-o-optimiser-dashboard'] as $item ) { if ( isset( $item[2] ) && $slug === $item[2] ) { return true; } } return false; } /** * Fallback Schema callback for the lightweight menu registration. * * Core owns the URL; the Pro add-on owns the screen, registered for * 'schema' through AI_O_Optimiser_Extensions (modules/schema/bootstrap.php). * When nothing is registered -- the add-on is not installed -- core shows * its own absent-state presentation. */ public function render_schema_entrypoint() { if ( class_exists( 'AI_O_Optimiser_Extensions' ) && AI_O_Optimiser_Extensions::render( 'schema' ) ) { return; } $pro_features = AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-pro-features.php'; if ( is_readable( $pro_features ) ) { require_once $pro_features; AI_O_Optimiser_Pro_Features::render_workspace( 'schema' ); return; } wp_die( esc_html__( 'Schema workspace could not be loaded.', 'ai-o-optimiser' ) ); } /** The NAP menu callback: same arrangement as Schema, for 'local'. */ public function render_local_entrypoint() { if ( class_exists( 'AI_O_Optimiser_Extensions' ) && AI_O_Optimiser_Extensions::render( 'local' ) ) { return; } $pro_features = AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-pro-features.php'; if ( is_readable( $pro_features ) ) { require_once $pro_features; call_user_func( array( 'AI_O_Optimiser_Pro_Features', 'render_workspace_local' ) ); return; } wp_die( esc_html__( 'NAP workspace could not be loaded.', 'ai-o-optimiser' ) ); } /** Persistent in-window navigation shared by all Optimiser workspaces. */ public function render_global_nav() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only screen state (filters, tab selection, pagination, redirect result notices). Nothing is written here, so there is no form submission to carry a nonce. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; $groups = array( 'ai-o-optimiser-dashboard' => 'dashboard', 'ai-o-optimiser-optimise' => 'metadata', 'ai-o-optimiser-metadata-workflow' => 'metadata', 'ai-o-optimiser-content-opportunities' => 'metadata', 'ai-o-optimiser-schema-review' => 'schema', 'ai-o-optimiser-local-discovery' => 'local', 'ai-o-optimiser-system-check' => 'settings', // Its own group, so the secondary slot prints nothing: the rail carries // AI Guidance Files under Publish and it has no sub-screens of its own. 'ai-o-optimiser-guidance' => 'guidance', 'ai-o-optimiser-environment-check' => 'settings', 'ai-o-optimiser-logs' => 'settings', 'ai-o-optimiser-deep-review-prompt' => 'settings', 'ai-o-optimiser-settings' => 'settings', 'ai-o-optimiser-notifications' => 'settings', ); if ( ! isset( $groups[ $page ] ) ) { return; } $active = $groups[ $page ]; $is_admin = current_user_can( AI_O_Optimiser_SENSITIVE_CAPABILITY ); /* * The five primary links that used to sit here -- Dashboard, Metadata, * Schema, NAP, Settings -- are in the left rail as of 1.37.0. See * AI_O_Optimiser_Admin_Rail::groups(), which is where the pathway is now * defined and where NAP keeps its stable URL. * * Printing them here as well would be two rows of primary navigation on one * screen, which is what 1.28.0 spent three review rounds taking out of * Settings. What is left in this bar is about the current screen rather than * about where you can go: the advanced-area warning, the way to send * feedback, and the slot each workspace prints its own section row into. * * The product mark moved to the rail with the links, so it is not repeated * here either. */ ?>