| @@ -19,19 +19,26 @@ | ||
| 19 | 19 | use WPDeveloper\BetterDocs\Dependencies\DI\Container; |
| 20 | 20 | |
| 21 | 21 | class Admin extends Base { |
| 22 | 22 | /** |
| 23 | - * Per-user flag recording that this administrator has opened the MCP screen. | |
| 23 | + * Per-user flag recording that this administrator has opened the Content IQ | |
| 24 | + * screen — the discovery badge (ADR-063) now flags Content Intelligence, the | |
| 25 | + * headline feature, rather than MCP. | |
| 24 | 26 | * |
| 25 | 27 | * Stores the timestamp of the first visit, but only its **presence** is read: |
| 26 | - * absent means "this user has not seen MCP yet", which is what puts the | |
| 27 | - * one-time discovery badge on the menu (ADR-063). Per user on purpose — two | |
| 28 | - * administrators each get their own first look, and neither clears the other's. | |
| 28 | + * absent means "this user has not seen Content IQ yet", which is what puts the | |
| 29 | + * one-time discovery badge on the menu. Per user on purpose — two administrators | |
| 30 | + * each get their own first look, and neither clears the other's. A deliberately | |
| 31 | + * fresh meta key (not the old `betterdocs_mcp_seen`) so a user who already | |
| 32 | + * dismissed the MCP badge still gets this one for the new feature. | |
| 29 | 33 | * |
| 34 | + * The private `*_mcp_*` helper names below are kept as-is to hold the diff to | |
| 35 | + * the target slug + this key; they now paint the Content IQ item. | |
| 36 | + * | |
| 30 | 37 | * @var string |
| 31 | 38 | * @since 4.9.0 |
| 32 | 39 | */ |
| 33 | - const MCP_SEEN_META = 'betterdocs_mcp_seen'; | |
| 40 | + const MCP_SEEN_META = 'betterdocs_content_iq_seen'; | |
| 34 | 41 | |
| 35 | 42 | /** |
| 36 | 43 | * Whether this request painted the MCP discovery badge onto the menu. |
| 37 | 44 | * |
| @@ -99,9 +106,8 @@ | ||
| 99 | 106 | * |
| 100 | 107 | * @var FAQBuilder |
| 101 | 108 | */ |
| 102 | 109 | private $faq_builder; |
| 103 | - private $glossaries; | |
| 104 | 110 | |
| 105 | 111 | public function __construct( Container $container, PostType $type, Enqueue $assets, Settings $settings, KBMigration $kbmigration ) { |
| 106 | 112 | $this->container = $container; |
| 107 | 113 | $this->assets = $assets; |
| @@ -115,9 +121,8 @@ | ||
| 115 | 121 | $type->init(); |
| 116 | 122 | $type->admin_init(); |
| 117 | 123 | |
| 118 | 124 | $this->faq_builder = $this->container->get( FAQBuilder::class ); |
| 119 | - $this->glossaries = $this->container->get( Glossaries::class ); | |
| 120 | 125 | |
| 121 | 126 | /** |
| 122 | 127 | * Register usage tracking (including the daily `put_do_weekly_action` cron |
| 123 | 128 | * handler) on every request — WP-Cron runs with is_admin() === false, so |
| @@ -170,8 +175,10 @@ | ||
| 170 | 175 | add_filter( 'admin_body_class', array( $this, 'body_classes' ) ); |
| 171 | 176 | add_filter( 'parent_file', array( $type, 'highlight_admin_menu' ) ); |
| 172 | 177 | add_filter( 'submenu_file', array( $type, 'highlight_admin_submenu' ), 10, 2 ); |
| 173 | 178 | add_filter( 'betterdocs_admin_menu', array( $this, 'quick_setup_menu' ), 10, 1 ); |
| 179 | + // Runs last so it also orders items Pro/add-ons append through this filter. | |
| 180 | + add_filter( 'betterdocs_admin_menu', array( $this, 'order_admin_menu' ), 999, 1 ); | |
| 174 | 181 | |
| 175 | 182 | /** |
| 176 | 183 | * Remove Comments Column from List Table. |
| 177 | 184 | */ |
| @@ -476,8 +483,26 @@ | ||
| 476 | 483 | ) |
| 477 | 484 | ); |
| 478 | 485 | } |
| 479 | 486 | |
| 487 | + // BetterDocs' own admin screens. Promotional notices are limited to these | |
| 488 | + // (WP.org guideline 11), never the whole of wp-admin. | |
| 489 | + $betterdocs_screens = array( | |
| 490 | + 'toplevel_page_betterdocs-dashboard', | |
| 491 | + 'admin_page_betterdocs-admin', | |
| 492 | + 'betterdocs_page_betterdocs-admin', | |
| 493 | + 'betterdocs_page_betterdocs-settings', | |
| 494 | + 'betterdocs_page_betterdocs-faq', | |
| 495 | + 'betterdocs_page_betterdocs-analytics', | |
| 496 | + 'betterdocs_page_betterdocs-glossaries', | |
| 497 | + 'betterdocs_page_betterdocs-ai-chatbot', | |
| 498 | + 'betterdocs_page_betterdocs-api-docs', | |
| 499 | + 'betterdocs_page_betterdocs-doc-categories', | |
| 500 | + 'betterdocs_page_betterdocs-doc-tags', | |
| 501 | + 'edit-doc_category', | |
| 502 | + 'edit-doc_tag', | |
| 503 | + ); | |
| 504 | + | |
| 480 | 505 | $summer_campaign_message = '<div class="betterdocs-summer-notice-body"><p style="margin-top: 0; margin-bottom: 0;">🏖️ <strong>Summer Savings:</strong> Build AI-powered Knowledge Bases & FAQs to cut support tickets and improve user experience – now <strong>up to $100 OFF!</strong></p></div>'; |
| 481 | 506 | $_summer_campaign_notice = array( |
| 482 | 507 | 'thumbnail' => $this->assets->icon( 'betterdocs-logo.svg', true ), |
| 483 | 508 | 'html' => $summer_campaign_message, |
| @@ -492,10 +517,11 @@ | ||
| 492 | 517 | ), |
| 493 | 518 | 'maybe_later' => array( |
| 494 | 519 | 'label' => __( 'I’ll Grab It Later', 'betterdocs' ), |
| 495 | 520 | 'attributes' => array( |
| 496 | - 'target' => '_blank', | |
| 497 | - 'data-later' => true, | |
| 521 | + 'target' => '_blank', | |
| 522 | + // A promo dismissal is permanent: no 15-day re-show. | |
| 523 | + 'data-dismiss' => true, | |
| 498 | 524 | 'class' => 'dismiss-btn', |
| 499 | 525 | ), |
| 500 | 526 | ), |
| 501 | 527 | ), |
| @@ -507,11 +533,11 @@ | ||
| 507 | 533 | array( |
| 508 | 534 | 'start' => $notices->time(), |
| 509 | 535 | 'recurrence' => false, |
| 510 | 536 | 'dismissible' => true, |
| 511 | - 'refresh' => BETTERDOCS_VERSION, | |
| 512 | 537 | 'expire' => strtotime( '11:59:59pm June 25, 2026' ), |
| 513 | 538 | 'display_if' => ! is_plugin_active( 'betterdocs-pro/betterdocs-pro.php' ), |
| 539 | + 'screens' => $betterdocs_screens, | |
| 514 | 540 | ) |
| 515 | 541 | ); |
| 516 | 542 | |
| 517 | 543 | self::$cache_bank->create_account( $notices ); |
| @@ -517,23 +543,9 @@ | ||
| 517 | 543 | self::$cache_bank->create_account( $notices ); |
| 518 | 544 | self::$cache_bank->calculate_deposits( $notices ); |
| 519 | 545 | if ( method_exists( self::$cache_bank, 'clear_notices_in_' ) ) { |
| 520 | 546 | self::$cache_bank->clear_notices_in_( |
| 521 | - array( | |
| 522 | - 'toplevel_page_betterdocs-dashboard', | |
| 523 | - 'admin_page_betterdocs-admin', | |
| 524 | - 'betterdocs_page_betterdocs-admin', | |
| 525 | - 'betterdocs_page_betterdocs-settings', | |
| 526 | - 'betterdocs_page_betterdocs-faq', | |
| 527 | - 'betterdocs_page_betterdocs-analytics', | |
| 528 | - 'betterdocs_page_betterdocs-glossaries', | |
| 529 | - 'betterdocs_page_betterdocs-ai-chatbot', | |
| 530 | - 'betterdocs_page_betterdocs-api-docs', | |
| 531 | - 'betterdocs_page_betterdocs-doc-categories', | |
| 532 | - 'betterdocs_page_betterdocs-doc-tags', | |
| 533 | - 'edit-doc_category', | |
| 534 | - 'edit-doc_tag', | |
| 535 | - ), | |
| 547 | + $betterdocs_screens, | |
| 536 | 548 | $notices, |
| 537 | 549 | true |
| 538 | 550 | ); |
| 539 | 551 | } |
| @@ -597,8 +609,9 @@ | ||
| 597 | 609 | 'betterdocs-settings', |
| 598 | 610 | 'betterdocs-admin', |
| 599 | 611 | 'betterdocs-dashboard', |
| 600 | 612 | 'betterdocs-analytics', |
| 613 | + 'betterdocs-content-iq', | |
| 601 | 614 | 'betterdocs-glossaries', |
| 602 | 615 | 'betterdocs-faq', |
| 603 | 616 | 'betterdocs-doc-categories', |
| 604 | 617 | 'betterdocs-doc-tags', |
| @@ -876,8 +889,12 @@ | ||
| 876 | 889 | 'analytics_older' => version_compare( betterdocs()->pro_version(), '3.3.4', '<=' ), |
| 877 | 890 | 'betterdocs_ChatBot_plugin' => is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' ), |
| 878 | 891 | 'api_docs_teaser' => betterdocs()->show_api_docs_teaser(), |
| 879 | 892 | 'glossaries_teaser' => betterdocs()->show_glossary_teaser(), |
| 893 | + 'glossaries_needs_pro_update' => betterdocs()->glossaries_needs_pro_update(), | |
| 894 | + 'glossaries_min_pro_version' => betterdocs()->glossaries_min_pro_version(), | |
| 895 | + 'glossaries_pro_update_url' => self_admin_url( 'plugins.php' ), | |
| 896 | + 'content_intelligence_teaser' => betterdocs()->show_content_intelligence_teaser(), | |
| 880 | 897 | 'is_woocommerce_active' => class_exists( 'WooCommerce' ), |
| 881 | 898 | 'total_doc_category_terms' => wp_count_terms( 'doc_category' ), |
| 882 | 899 | 'current_admin_language' => Helper::get_current_admin_language(), |
| 883 | 900 | 'is_multilingual' => Helper::is_multilingual_active(), |
| @@ -944,24 +961,10 @@ | ||
| 944 | 961 | 'betterdocs_settings' => $betterdocs_settings, |
| 945 | 962 | ) |
| 946 | 963 | ); |
| 947 | 964 | |
| 948 | - // Glossaries Related Localization | |
| 949 | - betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/css/faq.css' ); | |
| 950 | - | |
| 951 | - betterdocs()->assets->enqueue( 'betterdocs-admin-glossaries', 'admin/js/glossaries.js' ); | |
| 952 | - | |
| 953 | - betterdocs()->assets->localize( | |
| 954 | - 'betterdocs-admin-glossaries', | |
| 955 | - 'betterdocsGlossary', | |
| 956 | - array( | |
| 957 | - 'dir_url' => BETTERDOCS_ABSURL, | |
| 958 | - 'rest_url' => esc_url_raw( rest_url() ), | |
| 959 | - 'free_version' => betterdocs()->version, | |
| 960 | - 'nonce' => wp_create_nonce( 'wp_rest' ), | |
| 961 | - 'betterdocs_settings' => $betterdocs_settings, | |
| 962 | - ) | |
| 963 | - ); | |
| 965 | + // Glossaries is Pro — Pro's Core\Glossaries::enqueue() owns that bundle | |
| 966 | + // and both of its localized objects (`betterdocs`, `betterdocsGlossary`). | |
| 964 | 967 | } |
| 965 | 968 | |
| 966 | 969 | /** |
| 967 | 970 | * All admin pages header |
| @@ -1096,9 +1099,9 @@ | ||
| 1096 | 1099 | } |
| 1097 | 1100 | |
| 1098 | 1101 | if ( isset( $submenu[ $this->slug ] ) && is_array( $submenu[ $this->slug ] ) ) { |
| 1099 | 1102 | foreach ( $submenu[ $this->slug ] as &$sub_item ) { |
| 1100 | - if ( isset( $sub_item[2] ) && 'betterdocs-mcp' === $sub_item[2] ) { | |
| 1103 | + if ( isset( $sub_item[2] ) && 'betterdocs-content-iq' === $sub_item[2] ) { | |
| 1101 | 1104 | $sub_item[0] .= self::mcp_submenu_pill(); |
| 1102 | 1105 | break; |
| 1103 | 1106 | } |
| 1104 | 1107 | } |
| @@ -1183,9 +1186,9 @@ | ||
| 1183 | 1186 | public static function is_mcp_screen_request() { |
| 1184 | 1187 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection; see mark_mcp_seen(). |
| 1185 | 1188 | $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; |
| 1186 | 1189 | |
| 1187 | - if ( 'betterdocs-mcp' !== $page ) { | |
| 1190 | + if ( 'betterdocs-content-iq' !== $page ) { | |
| 1188 | 1191 | return false; |
| 1189 | 1192 | } |
| 1190 | 1193 | |
| 1191 | 1194 | return current_user_can( 'manage_options' ); |
| @@ -1390,8 +1393,18 @@ | ||
| 1390 | 1393 | 'output', |
| 1391 | 1394 | ), |
| 1392 | 1395 | $parent_slug |
| 1393 | 1396 | ), |
| 1397 | + 'content_intelligence' => $this->normalize_menu( | |
| 1398 | + __( 'Content IQ', 'betterdocs' ), | |
| 1399 | + 'betterdocs-content-iq', | |
| 1400 | + 'read_docs_analytics', | |
| 1401 | + array( | |
| 1402 | + $this, | |
| 1403 | + 'output', | |
| 1404 | + ), | |
| 1405 | + $parent_slug | |
| 1406 | + ), | |
| 1394 | 1407 | 'faq' => $this->normalize_menu( |
| 1395 | 1408 | __( 'FAQ Builder', 'betterdocs' ), |
| 1396 | 1409 | 'betterdocs-faq', |
| 1397 | 1410 | 'read_faq_builder', |
| @@ -1402,8 +1415,17 @@ | ||
| 1402 | 1415 | $parent_slug |
| 1403 | 1416 | ), |
| 1404 | 1417 | ); |
| 1405 | 1418 | |
| 1419 | + // Content Intelligence ships in Pro, which overwrites that same key in place. | |
| 1420 | + // Unlike API Docs it has to sit directly after Analytics, and `menus()` walks | |
| 1421 | + // this array in insertion order — so the slot is declared inside the literal | |
| 1422 | + // above and only withdrawn here. Appending it after the fact, api_docs-style, | |
| 1423 | + // would park it at the bottom of the menu. | |
| 1424 | + if ( ! ( betterdocs()->show_content_intelligence_teaser() || betterdocs()->has_content_intelligence() ) ) { | |
| 1425 | + unset( $betterdocs_admin_pages['content_intelligence'] ); | |
| 1426 | + } | |
| 1427 | + | |
| 1406 | 1428 | // Glossaries is Pro. Reserve this same 'glossaries' slot for Free's locked |
| 1407 | 1429 | // teaser so the item keeps this position; once Pro is active the real |
| 1408 | 1430 | // screen overwrites the key in place (same pattern as API Docs below). |
| 1409 | 1431 | if ( betterdocs()->show_glossary_teaser() || ( betterdocs()->is_pro_active() && betterdocs()->settings->get( 'enable_glossaries' ) == true ) ) { |
| @@ -1450,8 +1472,70 @@ | ||
| 1450 | 1472 | |
| 1451 | 1473 | return apply_filters( 'betterdocs_admin_menu', $betterdocs_admin_pages, array( $this, 'output' ), $parent_slug ); |
| 1452 | 1474 | } |
| 1453 | 1475 | |
| 1476 | + /** | |
| 1477 | + * Put the BetterDocs submenu in a deliberate order. | |
| 1478 | + * | |
| 1479 | + * Order used to be an accident of *when* each item was added: Free declares | |
| 1480 | + * most of them inline, and reserves in-place slots for `glossaries` / | |
| 1481 | + * `api_docs` so Pro can overwrite the key without moving it. Anything added | |
| 1482 | + * purely through this filter, though, could only land at the end — which is | |
| 1483 | + * why Multiple KB (Pro, priority 100) and AI Chatbot Logs sat after | |
| 1484 | + * everything else regardless of where they belong. | |
| 1485 | + * | |
| 1486 | + * Sorting here, at priority 999, fixes that for every source at once: Free's | |
| 1487 | + * own entries, Pro's, and any add-on's. Knowledge Base now follows Tags (it | |
| 1488 | + * is the third taxonomy-ish thing, so it belongs with Categories and Tags | |
| 1489 | + * rather than past Analytics), and API Docs follows Knowledge Base. | |
| 1490 | + * | |
| 1491 | + * Keys not listed keep their relative order and are appended, so an add-on | |
| 1492 | + * that registers something unknown to this list is never dropped. | |
| 1493 | + * | |
| 1494 | + * @param array $pages Menu pages keyed by slug id. | |
| 1495 | + * @return array | |
| 1496 | + */ | |
| 1497 | + public function order_admin_menu( $pages ) { | |
| 1498 | + if ( ! is_array( $pages ) ) { | |
| 1499 | + return $pages; | |
| 1500 | + } | |
| 1501 | + | |
| 1502 | + $order = array( | |
| 1503 | + 'betterdocs', | |
| 1504 | + 'dashboard', | |
| 1505 | + 'all_docs', | |
| 1506 | + 'add_new', | |
| 1507 | + 'categories', | |
| 1508 | + 'tags', | |
| 1509 | + 'multiple_kb', | |
| 1510 | + 'api_docs', | |
| 1511 | + 'settings', | |
| 1512 | + 'mcp', | |
| 1513 | + 'analytics', | |
| 1514 | + // Content IQ reads as a second Analytics screen, so it has to stay | |
| 1515 | + // pinned directly behind it. Without this entry it would fall into | |
| 1516 | + // the unknown-key bucket below and be appended to the bottom of the | |
| 1517 | + // menu — the exact placement the menu literal avoids by declaring | |
| 1518 | + // the slot inline rather than filtering it in api_docs-style. | |
| 1519 | + 'content_intelligence', | |
| 1520 | + 'faq', | |
| 1521 | + 'glossaries', | |
| 1522 | + 'ai_chatbot', | |
| 1523 | + 'ai_chatbot_logs', | |
| 1524 | + ); | |
| 1525 | + | |
| 1526 | + $ordered = array(); | |
| 1527 | + foreach ( $order as $key ) { | |
| 1528 | + if ( array_key_exists( $key, $pages ) ) { | |
| 1529 | + $ordered[ $key ] = $pages[ $key ]; | |
| 1530 | + unset( $pages[ $key ] ); | |
| 1531 | + } | |
| 1532 | + } | |
| 1533 | + | |
| 1534 | + // `$pages` now holds only unknown keys, still in their original order. | |
| 1535 | + return array_merge( $ordered, $pages ); | |
| 1536 | + } | |
| 1537 | + | |
| 1454 | 1538 | public function add_custom_classes_to_menu_items() { |
| 1455 | 1539 | global $menu, $submenu; |
| 1456 | 1540 | |
| 1457 | 1541 | $menu_items = array( |
| @@ -1462,8 +1546,9 @@ | ||
| 1462 | 1546 | 'betterdocs-doc-tags' => 'betterdocs-tags', |
| 1463 | 1547 | 'betterdocs-settings' => 'betterdocs-settings', |
| 1464 | 1548 | 'betterdocs-mcp' => 'betterdocs-mcp', |
| 1465 | 1549 | 'betterdocs-analytics' => 'betterdocs-analytics', |
| 1550 | + 'betterdocs-content-iq' => 'betterdocs-content-iq', | |
| 1466 | 1551 | 'betterdocs-faq' => 'betterdocs-faq', |
| 1467 | 1552 | 'betterdocs-glossaries' => 'betterdocs-glossaries', |
| 1468 | 1553 | 'betterdocs-ai-chatbot' => 'betterdocs-ai-chatbot', |
| 1469 | 1554 | 'betterdocs-api-docs' => 'betterdocs-api-docs', |