PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
← All changes | includes/Core/Admin.php +128 -43 4.9.1 → 4.9.5 View file →
@@ -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',