← All changes
|
includes/core/classes/class-wpfnl-admin-menus.php
+303
-34
3.12.1
→
3.13.3
View file →
| @@ -21,9 +21,11 @@ | ||
| 21 | 21 | { |
| 22 | 22 | add_action('admin_menu', [$this, 'register_plugin_menus']); |
| 23 | 23 | add_filter('admin_head', [$this, 'remove_submenu'], 10, 2); |
| 24 | 24 | add_filter('admin_head', [$this, 'remove_notices_from_funnel_window'], 10, 2); |
| 25 | + add_action('in_admin_header', [$this, 'remove_notices_from_react_admin']); | |
| 25 | 26 | add_action('admin_init', [$this, 'disallow_all_step_view']); |
| 27 | + add_action('admin_init', [$this, 'maybe_redirect_to_react_admin']); | |
| 26 | 28 | add_action('admin_footer', [$this, 'doc_link_with_new_page']); |
| 27 | 29 | add_filter('wpfnl_dashboard_nav_lists', [$this, 'add_store_checkout_nav'], 15, 1); |
| 28 | 30 | add_action('admin_bar_menu', [$this, 'register_admin_bar_menu'], 100); |
| 29 | 31 | |
| @@ -34,9 +36,9 @@ | ||
| 34 | 36 | add_filter( 'parent_file', function( $parent_file ) { |
| 35 | 37 | return WPFNL_MAIN_PAGE_SLUG; |
| 36 | 38 | }); |
| 37 | 39 | add_filter( 'submenu_file', function( $submenu_file ) { |
| 38 | - return WPFNL_STORE_CHECKOUT_SLUG; | |
| 40 | + return self::react_admin_slug( WPFNL_STORE_CHECKOUT_SLUG ); | |
| 39 | 41 | }); |
| 40 | 42 | } else { |
| 41 | 43 | add_filter( "admin_body_class", array($this, 'add_folded_menu_class') ); |
| 42 | 44 | } |
| @@ -44,8 +46,66 @@ | ||
| 44 | 46 | } |
| 45 | 47 | |
| 46 | 48 | |
| 47 | 49 | /** |
| 50 | + * Screens that live as routes of the React single-page admin. | |
| 51 | + * | |
| 52 | + * Keyed by the WordPress page slug each screen had before the move; the | |
| 53 | + * value is the hash route that replaced it. Three things read this: the | |
| 54 | + * submenu items, so a click only changes the fragment; the hidden | |
| 55 | + * registrations of the old pages; and maybe_redirect_to_react_admin(), | |
| 56 | + * which sends those old pages to the same routes. | |
| 57 | + * | |
| 58 | + * @return array Legacy page slug => hash route. | |
| 59 | + * @since 3.13.7 | |
| 60 | + */ | |
| 61 | + public static function react_routes() | |
| 62 | + { | |
| 63 | + $routes = [ | |
| 64 | + WPFNL_FUNNEL_PAGE_SLUG => '#/funnels', | |
| 65 | + WPFNL_TRASH_FUNNEL_SLUG => '#/funnels/trash', | |
| 66 | + WPFNL_STORE_CHECKOUT_SLUG => '#/store-checkout', | |
| 67 | + WPFNL_AUTOMATIONS_SLUG => '#/automation', | |
| 68 | + WPFNL_TEMPLATE_PAGE_SLUG => '#/funnel-templates', | |
| 69 | + WPFNL_AI_FUNNEL_PAGE_SLUG => '#/ai-assistant', | |
| 70 | + WPFNL_GLOBAL_SETTINGS_SLUG => '#/settings', | |
| 71 | + ]; | |
| 72 | + | |
| 73 | + /* | |
| 74 | + * Store Checkout takes over the WooCommerce checkout, so without | |
| 75 | + * WooCommerce the screen has nothing to list and its route has nothing | |
| 76 | + * to show. That case keeps its server-rendered view, which offers to | |
| 77 | + * install WooCommerce — see render_store_checkout_page(). | |
| 78 | + */ | |
| 79 | + if ( ! Wpfnl_functions::is_wc_active() ) { | |
| 80 | + unset( $routes[ WPFNL_STORE_CHECKOUT_SLUG ] ); | |
| 81 | + } | |
| 82 | + | |
| 83 | + return $routes; | |
| 84 | + } | |
| 85 | + | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * Menu slug that opens a screen without leaving the React admin. | |
| 89 | + * | |
| 90 | + * The app is one WordPress page with the route in the fragment, so a link | |
| 91 | + * of the form `wpfunnels#/funnels` moves between screens without a page | |
| 92 | + * load. A screen with no route — Store Checkout on a site without | |
| 93 | + * WooCommerce — keeps its own page slug. | |
| 94 | + * | |
| 95 | + * @param string $page Legacy page slug of the screen. | |
| 96 | + * @return string Slug to register the menu item under. | |
| 97 | + * @since 3.13.7 | |
| 98 | + */ | |
| 99 | + public static function react_admin_slug( $page ) | |
| 100 | + { | |
| 101 | + $routes = self::react_routes(); | |
| 102 | + | |
| 103 | + return isset( $routes[ $page ] ) ? WPFNL_MAIN_PAGE_SLUG . $routes[ $page ] : $page; | |
| 104 | + } | |
| 105 | + | |
| 106 | + | |
| 107 | + /** | |
| 48 | 108 | * Register plugin menus and submenus |
| 49 | 109 | * |
| 50 | 110 | * @since 1.0.0 |
| 51 | 111 | */ |
| @@ -63,9 +123,15 @@ | ||
| 63 | 123 | 6 |
| 64 | 124 | ); |
| 65 | 125 | |
| 66 | 126 | |
| 67 | - // Register dashboard page for WPF | |
| 127 | + // Every screen below is a route of the React single-page admin, which | |
| 128 | + // WordPress serves as one page: `?page=wpfunnels`, with the route in | |
| 129 | + // the fragment. The submenu items carry that fragment in their slugs, | |
| 130 | + // so moving between them only changes the hash and the browser never | |
| 131 | + // reloads the admin — the same way the app's own top navigation | |
| 132 | + // behaves. The pages these screens used to be are registered after | |
| 133 | + // them, hidden from the menu, so every old link still resolves. | |
| 68 | 134 | add_submenu_page( |
| 69 | 135 | WPFNL_MAIN_PAGE_SLUG, |
| 70 | 136 | __('Dashboard', 'wpfnl'), |
| 71 | 137 | __('Dashboard', 'wpfnl'), |
| @@ -73,23 +139,38 @@ | ||
| 73 | 139 | WPFNL_MAIN_PAGE_SLUG, |
| 74 | 140 | [$this, 'render'] |
| 75 | 141 | ); |
| 76 | 142 | |
| 143 | + // Alias the React admin was first built under. Kept registered so links | |
| 144 | + // and bookmarks from that period still resolve, but hidden from the menu | |
| 145 | + // by remove_submenu() — the dashboard entry above is the real one. | |
| 77 | 146 | add_submenu_page( |
| 78 | 147 | WPFNL_MAIN_PAGE_SLUG, |
| 148 | + __('Dashboard', 'wpfnl'), | |
| 149 | + __('Dashboard', 'wpfnl'), | |
| 150 | + 'wpf_manage_funnels', | |
| 151 | + WPFNL_REACT_ADMIN_SLUG, | |
| 152 | + [$this, 'render'] | |
| 153 | + ); | |
| 154 | + | |
| 155 | + add_submenu_page( | |
| 156 | + WPFNL_MAIN_PAGE_SLUG, | |
| 79 | 157 | __('Funnels', 'wpfnl'), |
| 80 | 158 | __('Funnels', 'wpfnl'), |
| 81 | 159 | Wpfnl_functions::role_permission_to_allow_wpfunnel( $role_permission ), |
| 82 | - WPFNL_FUNNEL_PAGE_SLUG, | |
| 160 | + self::react_admin_slug( WPFNL_FUNNEL_PAGE_SLUG ), | |
| 83 | 161 | [$this, 'render_funnels_page'] |
| 84 | 162 | ); |
| 85 | 163 | |
| 164 | + // Without WooCommerce this one has no route to open: react_admin_slug() | |
| 165 | + // hands back the page slug and the server-rendered view stays, which is | |
| 166 | + // what offers to install WooCommerce. | |
| 86 | 167 | add_submenu_page( |
| 87 | 168 | WPFNL_MAIN_PAGE_SLUG, |
| 88 | 169 | __('Store Checkout', 'wpfnl'), |
| 89 | 170 | __('Store Checkout', 'wpfnl'), |
| 90 | 171 | Wpfnl_functions::role_permission_to_allow_wpfunnel( $role_permission ), |
| 91 | - WPFNL_STORE_CHECKOUT_SLUG, | |
| 172 | + self::react_admin_slug( WPFNL_STORE_CHECKOUT_SLUG ), | |
| 92 | 173 | [$this, 'render_store_checkout_page'] |
| 93 | 174 | ); |
| 94 | 175 | |
| 95 | 176 | add_submenu_page( |
| @@ -96,12 +177,18 @@ | ||
| 96 | 177 | WPFNL_MAIN_PAGE_SLUG, |
| 97 | 178 | __('Automations', 'wpfnl'), |
| 98 | 179 | __('Automations', 'wpfnl'), |
| 99 | 180 | Wpfnl_functions::role_permission_to_allow_wpfunnel( $role_permission ), |
| 100 | - WPFNL_AUTOMATIONS_SLUG, | |
| 181 | + self::react_admin_slug( WPFNL_AUTOMATIONS_SLUG ), | |
| 101 | 182 | [$this, 'render_automations_page'] |
| 102 | 183 | ); |
| 103 | 184 | |
| 185 | + // Asked below and by render_integration_page(); admin_menu runs before | |
| 186 | + // WordPress loads the plugin API on its own. | |
| 187 | + if ( ! function_exists( 'is_plugin_active' ) ) { | |
| 188 | + require_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
| 189 | + } | |
| 190 | + | |
| 104 | 191 | $integrations_in_pro = defined( 'WPFNL_PRO_VERSION' ) && version_compare( WPFNL_PRO_VERSION, '2.9.0', '>=' ); |
| 105 | 192 | $legacy_addon_active = is_plugin_active( 'wpfunnels-pro-integrations/wpfunnels-pro-integrations.php' ); |
| 106 | 193 | |
| 107 | 194 | if ( $integrations_in_pro || ! $legacy_addon_active ) { |
| @@ -128,9 +215,9 @@ | ||
| 128 | 215 | WPFNL_MAIN_PAGE_SLUG, |
| 129 | 216 | __('Templates', 'wpfnl'), |
| 130 | 217 | __('Templates', 'wpfnl'), |
| 131 | 218 | Wpfnl_functions::role_permission_to_allow_wpfunnel( $role_permission ), |
| 132 | - WPFNL_TEMPLATE_PAGE_SLUG, | |
| 219 | + self::react_admin_slug( WPFNL_TEMPLATE_PAGE_SLUG ), | |
| 133 | 220 | [$this, 'render_funnel_template_page'] |
| 134 | 221 | ); |
| 135 | 222 | |
| 136 | 223 | // add_submenu_page( |
| @@ -141,23 +228,56 @@ | ||
| 141 | 228 | // WPFNL_ADDONS_SLUG, |
| 142 | 229 | // [$this, 'render_addons_page'] |
| 143 | 230 | // ); |
| 144 | 231 | |
| 145 | - | |
| 146 | - // Show Integrations menu if WPFunnels Pro Integrations is deactivated | |
| 147 | - if ( ! function_exists( 'is_plugin_active' ) ) { | |
| 148 | - require_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
| 149 | - } | |
| 150 | - | |
| 232 | + // Settings is a route of the React admin (`#/settings`); the page it | |
| 233 | + // used to be is registered below, hidden, and redirected to that route. | |
| 151 | 234 | add_submenu_page( |
| 152 | 235 | WPFNL_MAIN_PAGE_SLUG, |
| 153 | 236 | __('Settings', 'wpfnl'), |
| 154 | 237 | __('Settings', 'wpfnl'), |
| 155 | 238 | Wpfnl_functions::role_permission_to_allow_wpfunnel( $role_permission ), |
| 156 | - WPFNL_GLOBAL_SETTINGS_SLUG, | |
| 239 | + self::react_admin_slug( WPFNL_GLOBAL_SETTINGS_SLUG ), | |
| 157 | 240 | [$this, 'render_settings_page'] |
| 158 | 241 | ); |
| 159 | 242 | |
| 243 | + add_submenu_page( | |
| 244 | + WPFNL_MAIN_PAGE_SLUG, | |
| 245 | + __('AI Assistant', 'wpfnl'), | |
| 246 | + __('AI Assistant', 'wpfnl'), | |
| 247 | + Wpfnl_functions::role_permission_to_allow_wpfunnel( $role_permission ), | |
| 248 | + self::react_admin_slug( WPFNL_AI_FUNNEL_PAGE_SLUG ), | |
| 249 | + [$this, 'render_ai_funnel_page'] | |
| 250 | + ); | |
| 251 | + | |
| 252 | + /* | |
| 253 | + * The pages those screens used to be, each still answering on its own | |
| 254 | + * slug so bookmarks, the admin bar and anything built from the slugs | |
| 255 | + * keep working — admin_init sends them on to the matching route. They | |
| 256 | + * are dropped from the menu by remove_submenu(); the items above are | |
| 257 | + * what people click, and those never leave the page. | |
| 258 | + */ | |
| 259 | + $legacy_renderers = [ | |
| 260 | + WPFNL_FUNNEL_PAGE_SLUG => 'render_funnels_page', | |
| 261 | + WPFNL_TRASH_FUNNEL_SLUG => 'render_trash_funnel_page', | |
| 262 | + WPFNL_STORE_CHECKOUT_SLUG => 'render_store_checkout_page', | |
| 263 | + WPFNL_AUTOMATIONS_SLUG => 'render_automations_page', | |
| 264 | + WPFNL_TEMPLATE_PAGE_SLUG => 'render_funnel_template_page', | |
| 265 | + WPFNL_AI_FUNNEL_PAGE_SLUG => 'render_ai_funnel_page', | |
| 266 | + WPFNL_GLOBAL_SETTINGS_SLUG => 'render_settings_page', | |
| 267 | + ]; | |
| 268 | + | |
| 269 | + foreach ( array_keys( self::react_routes() ) as $legacy_slug ) { | |
| 270 | + add_submenu_page( | |
| 271 | + WPFNL_MAIN_PAGE_SLUG, | |
| 272 | + __('WPFunnels', 'wpfnl'), | |
| 273 | + __('WPFunnels', 'wpfnl'), | |
| 274 | + Wpfnl_functions::role_permission_to_allow_wpfunnel( $role_permission ), | |
| 275 | + $legacy_slug, | |
| 276 | + [$this, $legacy_renderers[ $legacy_slug ]] | |
| 277 | + ); | |
| 278 | + } | |
| 279 | + | |
| 160 | 280 | // add_submenu_page( |
| 161 | 281 | // WPFNL_MAIN_PAGE_SLUG, |
| 162 | 282 | // __('Documentation', 'wpfnl'), |
| 163 | 283 | // '<span id="wpfnl-documentation">'. __('Documentation', 'wpfnl').'</span>', |
| @@ -173,17 +293,8 @@ | ||
| 173 | 293 | WPFNL_EDIT_FUNNEL_SLUG, |
| 174 | 294 | [$this, 'render_edit_funnel_page'] |
| 175 | 295 | ); |
| 176 | 296 | |
| 177 | - add_submenu_page( | |
| 178 | - WPFNL_MAIN_PAGE_SLUG, | |
| 179 | - __('Trash', 'wpfnl'), | |
| 180 | - __('Trash', 'wpfnl'), | |
| 181 | - Wpfnl_functions::role_permission_to_allow_wpfunnel( $role_permission ), | |
| 182 | - WPFNL_TRASH_FUNNEL_SLUG, | |
| 183 | - [$this, 'render_trash_funnel_page'] | |
| 184 | - ); | |
| 185 | - | |
| 186 | 297 | add_submenu_page( |
| 187 | 298 | WPFNL_MAIN_PAGE_SLUG, |
| 188 | 299 | __('Request a Feature', 'wpfnl'), |
| 189 | 300 | '<span id="wpfnl-request-feature">'. __('Request a Feature', 'wpfnl').'</span>', |
| @@ -228,13 +339,105 @@ | ||
| 228 | 339 | * |
| 229 | 340 | * @since 3.1.7 |
| 230 | 341 | */ |
| 231 | 342 | public function render() { |
| 232 | - include_once WPFNL_ADMIN_DIR . 'views/base.php'; | |
| 343 | + include_once WPFNL_ADMIN_DIR . 'views/react-admin.php'; | |
| 233 | 344 | } |
| 234 | 345 | |
| 235 | 346 | |
| 347 | + /** | |
| 348 | + * Send the screens that moved into the React admin to their routes. | |
| 349 | + * | |
| 350 | + * Each screen in react_routes() was its own WordPress page with a | |
| 351 | + * server-rendered view. They are routes of the single-page admin now (see | |
| 352 | + * src/admin/pages), so the pages are kept registered — bookmarks and every | |
| 353 | + * link built from the slugs still resolve — and each one hands over to the | |
| 354 | + * route that replaced it. | |
| 355 | + * | |
| 356 | + * The menu items no longer come through here: they carry their route in the | |
| 357 | + * fragment of their own slug (see react_admin_slug), so clicking one moves | |
| 358 | + * between screens without a page load at all. This is the path for links | |
| 359 | + * made before the move, which still name the old page. | |
| 360 | + * | |
| 361 | + * @return void | |
| 362 | + * @since 3.13.5 | |
| 363 | + */ | |
| 364 | + public function maybe_redirect_to_react_admin() { | |
| 365 | + if ( wp_doing_ajax() || wp_doing_cron() ) { | |
| 366 | + return; | |
| 367 | + } | |
| 236 | 368 | |
| 369 | + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 370 | + | |
| 371 | + $routes = self::react_routes(); | |
| 372 | + | |
| 373 | + if ( ! isset( $routes[ $page ] ) ) { | |
| 374 | + return; | |
| 375 | + } | |
| 376 | + | |
| 377 | + /** | |
| 378 | + * Filters whether a legacy screen redirects to its React admin route. | |
| 379 | + * | |
| 380 | + * @param bool $redirect Whether to redirect. Default true. | |
| 381 | + * @param string $page Page slug being visited. | |
| 382 | + * | |
| 383 | + * @since 3.13.5 | |
| 384 | + */ | |
| 385 | + if ( ! apply_filters( 'wpfunnels/redirect_to_react_admin', true, $page ) ) { | |
| 386 | + return; | |
| 387 | + } | |
| 388 | + | |
| 389 | + $url = admin_url( 'admin.php?page=' . WPFNL_MAIN_PAGE_SLUG ) . $routes[ $page ]; | |
| 390 | + | |
| 391 | + // A search carried over from the legacy funnel list keeps working: the | |
| 392 | + // React list reads it out of the hash route's query string. | |
| 393 | + $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 394 | + | |
| 395 | + if ( $search ) { | |
| 396 | + $url .= '?s=' . rawurlencode( $search ); | |
| 397 | + } | |
| 398 | + | |
| 399 | + /* | |
| 400 | + * The AI Assistant's own parameters travel the same way. `conversation_id` | |
| 401 | + * is the deep link into one thread; `wpfnl_prompt` (with an optional | |
| 402 | + * `wpfnl_context`) is the hand-off from a prompt typed elsewhere in the | |
| 403 | + * admin, which the page sends on arrival. Every link to the old page | |
| 404 | + * carries them in the query string, so they have to come across into the | |
| 405 | + * hash route's own query string, where the router can read them. | |
| 406 | + */ | |
| 407 | + if ( WPFNL_AI_FUNNEL_PAGE_SLUG === $page ) { | |
| 408 | + $carried = []; | |
| 409 | + | |
| 410 | + $conversation_id = isset( $_GET['conversation_id'] ) ? absint( wp_unslash( $_GET['conversation_id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 411 | + | |
| 412 | + if ( $conversation_id ) { | |
| 413 | + $carried['conversation_id'] = $conversation_id; | |
| 414 | + } | |
| 415 | + | |
| 416 | + $prompt = isset( $_GET['wpfnl_prompt'] ) ? sanitize_textarea_field( wp_unslash( $_GET['wpfnl_prompt'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 417 | + | |
| 418 | + if ( $prompt ) { | |
| 419 | + $carried['wpfnl_prompt'] = $prompt; | |
| 420 | + | |
| 421 | + $context = isset( $_GET['wpfnl_context'] ) ? sanitize_key( wp_unslash( $_GET['wpfnl_context'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 422 | + | |
| 423 | + if ( $context ) { | |
| 424 | + $carried['wpfnl_context'] = $context; | |
| 425 | + } | |
| 426 | + } | |
| 427 | + | |
| 428 | + if ( $carried ) { | |
| 429 | + $url .= '?' . http_build_query( $carried ); | |
| 430 | + } | |
| 431 | + } | |
| 432 | + | |
| 433 | + if ( wp_safe_redirect( $url ) ) { | |
| 434 | + exit; | |
| 435 | + } | |
| 436 | + } | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 237 | 440 | public function render_email_builder_page() { ?> |
| 238 | 441 | <div id="email-builder"></div> |
| 239 | 442 | <?php } |
| 240 | 443 | |
| @@ -246,13 +449,24 @@ | ||
| 246 | 449 | * @since 1.0.0 |
| 247 | 450 | */ |
| 248 | 451 | public function render_funnels_page() |
| 249 | 452 | { |
| 250 | - Wpfnl::$instance->module_manager->get_admin_modules('funnels')->get_view(); | |
| 453 | + $this->render(); | |
| 251 | 454 | } |
| 252 | 455 | |
| 253 | 456 | |
| 254 | 457 | /** |
| 458 | + * Render AI funnel page. | |
| 459 | + * | |
| 460 | + * @since 3.10.0 | |
| 461 | + */ | |
| 462 | + public function render_ai_funnel_page() | |
| 463 | + { | |
| 464 | + $this->render(); | |
| 465 | + } | |
| 466 | + | |
| 467 | + | |
| 468 | + /** | |
| 255 | 469 | * Render funnel template page |
| 256 | 470 | * |
| 257 | 471 | * @since 1.0.0 |
| 258 | 472 | */ |
| @@ -257,9 +471,9 @@ | ||
| 257 | 471 | * @since 1.0.0 |
| 258 | 472 | */ |
| 259 | 473 | public function render_funnel_template_page() |
| 260 | 474 | { |
| 261 | - require WPFNL_DIR . '/admin/partials/templates.php'; | |
| 475 | + $this->render(); | |
| 262 | 476 | } |
| 263 | 477 | |
| 264 | 478 | |
| 265 | 479 | /** |
| @@ -292,9 +506,9 @@ | ||
| 292 | 506 | * @since 3.1.8 |
| 293 | 507 | */ |
| 294 | 508 | public function render_trash_funnel_page() |
| 295 | 509 | { |
| 296 | - Wpfnl::$instance->module_manager->get_admin_modules('funnels')->get_view(); | |
| 510 | + $this->render(); | |
| 297 | 511 | } |
| 298 | 512 | |
| 299 | 513 | /** |
| 300 | 514 | * Render create funnel page |
| @@ -337,15 +551,27 @@ | ||
| 337 | 551 | /** |
| 338 | 552 | * Render Store Checkout page |
| 339 | 553 | * |
| 340 | 554 | * Since 3.6.0 multiple store checkouts are allowed, so this page always |
| 341 | - * shows the listing rather than redirecting to a single canvas. | |
| 555 | + * shows the listing rather than redirecting to a single canvas. Since | |
| 556 | + * 3.13.6 that listing is the React route `#/store-checkout`. | |
| 342 | 557 | * |
| 343 | 558 | * @since 3.5.0 |
| 344 | 559 | */ |
| 345 | 560 | public function render_store_checkout_page() |
| 346 | 561 | { |
| 347 | - require WPFNL_DIR . '/admin/modules/store-checkout/views/store-checkout-listing.php'; | |
| 562 | + // Store Checkout takes over the WooCommerce checkout, so the screen is | |
| 563 | + // meaningless without WooCommerce. Offer to install/activate it instead | |
| 564 | + // of rendering an empty listing. | |
| 565 | + if ( ! Wpfnl_functions::is_wc_active() ) { | |
| 566 | + require WPFNL_DIR . '/admin/modules/store-checkout/views/woocommerce-required.php'; | |
| 567 | + return; | |
| 568 | + } | |
| 569 | + | |
| 570 | + // The listing is a route of the React admin now (`#/store-checkout`), | |
| 571 | + // which maybe_redirect_to_react_admin() sends this page to. Rendering | |
| 572 | + // the app here keeps the page working if that redirect is filtered off. | |
| 573 | + $this->render(); | |
| 348 | 574 | } |
| 349 | 575 | |
| 350 | 576 | /** |
| 351 | 577 | * Render Integration page |
| @@ -410,9 +636,21 @@ | ||
| 410 | 636 | */ |
| 411 | 637 | public function remove_submenu() |
| 412 | 638 | { |
| 413 | 639 | remove_submenu_page(WPFNL_MAIN_PAGE_SLUG, 'edit_funnel'); |
| 414 | - remove_submenu_page(WPFNL_MAIN_PAGE_SLUG, 'trash_funnels'); | |
| 640 | + // The React admin's legacy alias; it renders the same dashboard as the | |
| 641 | + // entry above it, so only one of the two belongs in the menu. | |
| 642 | + remove_submenu_page(WPFNL_MAIN_PAGE_SLUG, WPFNL_REACT_ADMIN_SLUG); | |
| 643 | + | |
| 644 | + /* | |
| 645 | + * The pages the React screens used to be. They stay registered — the | |
| 646 | + * callbacks still answer, so old links resolve — but the menu items | |
| 647 | + * people click are the ones registered alongside them with the route | |
| 648 | + * in the fragment, which never reload the page. | |
| 649 | + */ | |
| 650 | + foreach ( array_keys( self::react_routes() ) as $legacy_slug ) { | |
| 651 | + remove_submenu_page( WPFNL_MAIN_PAGE_SLUG, $legacy_slug ); | |
| 652 | + } | |
| 415 | 653 | } |
| 416 | 654 | |
| 417 | 655 | |
| 418 | 656 | /** |
| @@ -426,9 +664,9 @@ | ||
| 426 | 664 | } |
| 427 | 665 | if (('edit_funnel' == sanitize_text_field( $_GET['page'] ) )) { |
| 428 | 666 | remove_all_actions( 'admin_notices' ); |
| 429 | 667 | } |
| 430 | - if ( 'wp_funnels' == sanitize_text_field( $_GET['page'] ) || 'wpf_templates' == sanitize_text_field( $_GET['page'] ) || 'trash_funnels' == sanitize_text_field( $_GET['page'] ) || 'wpfnl_settings' == sanitize_text_field( $_GET['page'] ) || 'wpf-license' == sanitize_text_field( $_GET['page'] ) ) { | |
| 668 | + if ( 'wp_funnels' == sanitize_text_field( $_GET['page'] ) || 'wpf_templates' == sanitize_text_field( $_GET['page'] ) || 'trash_funnels' == sanitize_text_field( $_GET['page'] ) || 'wpfnl_settings' == sanitize_text_field( $_GET['page'] ) || 'wpf-license' == sanitize_text_field( $_GET['page'] ) || WPFNL_AI_FUNNEL_PAGE_SLUG == sanitize_text_field( $_GET['page'] ) ) { | |
| 431 | 669 | add_action('admin_footer', array( $this, 'remove_admin_notices' )); |
| 432 | 670 | } |
| 433 | 671 | |
| 434 | 672 | } |
| @@ -495,8 +733,39 @@ | ||
| 495 | 733 | } |
| 496 | 734 | |
| 497 | 735 | |
| 498 | 736 | /** |
| 737 | + * Drop every admin notice on the React admin screen. | |
| 738 | + * | |
| 739 | + * The React admin is a single-page app that owns the whole content column, | |
| 740 | + * so a notice printed above it by WordPress or by an unrelated plugin (the | |
| 741 | + * Stripe "Apple Pay" warning, update nags, review prompts) pushes the app | |
| 742 | + * down and breaks the layout. Unhooking the notice actions stops them from | |
| 743 | + * being rendered at all rather than hiding them after the fact; anything a | |
| 744 | + * plugin injects later from JavaScript is caught by the CSS fallback in | |
| 745 | + * src/scss/style.scss. | |
| 746 | + * | |
| 747 | + * Runs on `in_admin_header`, the last hook to fire before WordPress renders | |
| 748 | + * the notices, so a plugin that registers one late is still caught. | |
| 749 | + * | |
| 750 | + * @return void | |
| 751 | + * @since 3.13.2 | |
| 752 | + */ | |
| 753 | + public function remove_notices_from_react_admin() { | |
| 754 | + $page = empty( $_GET['page'] ) ? '' : sanitize_text_field( wp_unslash( $_GET['page'] ) ); | |
| 755 | + | |
| 756 | + if ( ! in_array( $page, array( WPFNL_MAIN_PAGE_SLUG, WPFNL_REACT_ADMIN_SLUG ), true ) ) { | |
| 757 | + return; | |
| 758 | + } | |
| 759 | + | |
| 760 | + remove_all_actions( 'admin_notices' ); | |
| 761 | + remove_all_actions( 'all_admin_notices' ); | |
| 762 | + remove_all_actions( 'user_admin_notices' ); | |
| 763 | + remove_all_actions( 'network_admin_notices' ); | |
| 764 | + } | |
| 765 | + | |
| 766 | + | |
| 767 | + /** | |
| 499 | 768 | * Add Store Checkout to dashboard navigation |
| 500 | 769 | * |
| 501 | 770 | * @param array $list Navigation list |
| 502 | 771 | * @return array Modified navigation list |
| @@ -555,19 +824,19 @@ | ||
| 555 | 824 | ], |
| 556 | 825 | [ |
| 557 | 826 | 'id' => 'wpfunnels-funnels', |
| 558 | 827 | 'title' => __('Funnels', 'wpfnl'), |
| 559 | - 'href' => admin_url('admin.php?page=' . WPFNL_FUNNEL_PAGE_SLUG), | |
| 828 | + 'href' => admin_url('admin.php?page=' . self::react_admin_slug( WPFNL_FUNNEL_PAGE_SLUG )), | |
| 560 | 829 | ], |
| 561 | 830 | [ |
| 562 | 831 | 'id' => 'wpfunnels-store-checkout', |
| 563 | 832 | 'title' => __('Store Checkout', 'wpfnl'), |
| 564 | - 'href' => admin_url('admin.php?page=' . WPFNL_STORE_CHECKOUT_SLUG), | |
| 833 | + 'href' => admin_url('admin.php?page=' . self::react_admin_slug( WPFNL_STORE_CHECKOUT_SLUG )), | |
| 565 | 834 | ], |
| 566 | 835 | [ |
| 567 | 836 | 'id' => 'wpfunnels-automations', |
| 568 | 837 | 'title' => __('Automations', 'wpfnl'), |
| 569 | - 'href' => admin_url('admin.php?page=' . WPFNL_AUTOMATIONS_SLUG), | |
| 838 | + 'href' => admin_url('admin.php?page=' . self::react_admin_slug( WPFNL_AUTOMATIONS_SLUG )), | |
| 570 | 839 | ], |
| 571 | 840 | ]; |
| 572 | 841 | |
| 573 | 842 | if ( ! function_exists( 'is_plugin_active' ) ) { |
| @@ -587,9 +856,9 @@ | ||
| 587 | 856 | |
| 588 | 857 | $subnodes[] = [ |
| 589 | 858 | 'id' => 'wpfunnels-templates', |
| 590 | 859 | 'title' => __('Templates', 'wpfnl'), |
| 591 | - 'href' => admin_url('admin.php?page=' . WPFNL_TEMPLATE_PAGE_SLUG), | |
| 860 | + 'href' => admin_url('admin.php?page=' . WPFNL_MAIN_PAGE_SLUG) . '#/funnel-templates', | |
| 592 | 861 | ]; |
| 593 | 862 | |
| 594 | 863 | $subnodes[] = [ |
| 595 | 864 | 'id' => 'wpfunnels-settings', |