| @@ -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 | */ |
| @@ -597,8 +604,9 @@ | ||
| 597 | 604 | 'betterdocs-settings', |
| 598 | 605 | 'betterdocs-admin', |
| 599 | 606 | 'betterdocs-dashboard', |
| 600 | 607 | 'betterdocs-analytics', |
| 608 | + 'betterdocs-content-iq', | |
| 601 | 609 | 'betterdocs-glossaries', |
| 602 | 610 | 'betterdocs-faq', |
| 603 | 611 | 'betterdocs-doc-categories', |
| 604 | 612 | 'betterdocs-doc-tags', |
| @@ -876,8 +884,12 @@ | ||
| 876 | 884 | 'analytics_older' => version_compare( betterdocs()->pro_version(), '3.3.4', '<=' ), |
| 877 | 885 | 'betterdocs_ChatBot_plugin' => is_plugin_active( 'betterdocs-ai-chatbot/betterdocs-ai-chatbot.php' ), |
| 878 | 886 | 'api_docs_teaser' => betterdocs()->show_api_docs_teaser(), |
| 879 | 887 | 'glossaries_teaser' => betterdocs()->show_glossary_teaser(), |
| 888 | + 'glossaries_needs_pro_update' => betterdocs()->glossaries_needs_pro_update(), | |
| 889 | + 'glossaries_min_pro_version' => betterdocs()->glossaries_min_pro_version(), | |
| 890 | + 'glossaries_pro_update_url' => self_admin_url( 'plugins.php' ), | |
| 891 | + 'content_intelligence_teaser' => betterdocs()->show_content_intelligence_teaser(), | |
| 880 | 892 | 'is_woocommerce_active' => class_exists( 'WooCommerce' ), |
| 881 | 893 | 'total_doc_category_terms' => wp_count_terms( 'doc_category' ), |
| 882 | 894 | 'current_admin_language' => Helper::get_current_admin_language(), |
| 883 | 895 | 'is_multilingual' => Helper::is_multilingual_active(), |
| @@ -944,24 +956,10 @@ | ||
| 944 | 956 | 'betterdocs_settings' => $betterdocs_settings, |
| 945 | 957 | ) |
| 946 | 958 | ); |
| 947 | 959 | |
| 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 | - ); | |
| 960 | + // Glossaries is Pro — Pro's Core\Glossaries::enqueue() owns that bundle | |
| 961 | + // and both of its localized objects (`betterdocs`, `betterdocsGlossary`). | |
| 964 | 962 | } |
| 965 | 963 | |
| 966 | 964 | /** |
| 967 | 965 | * All admin pages header |
| @@ -1096,9 +1094,9 @@ | ||
| 1096 | 1094 | } |
| 1097 | 1095 | |
| 1098 | 1096 | if ( isset( $submenu[ $this->slug ] ) && is_array( $submenu[ $this->slug ] ) ) { |
| 1099 | 1097 | foreach ( $submenu[ $this->slug ] as &$sub_item ) { |
| 1100 | - if ( isset( $sub_item[2] ) && 'betterdocs-mcp' === $sub_item[2] ) { | |
| 1098 | + if ( isset( $sub_item[2] ) && 'betterdocs-content-iq' === $sub_item[2] ) { | |
| 1101 | 1099 | $sub_item[0] .= self::mcp_submenu_pill(); |
| 1102 | 1100 | break; |
| 1103 | 1101 | } |
| 1104 | 1102 | } |
| @@ -1183,9 +1181,9 @@ | ||
| 1183 | 1181 | public static function is_mcp_screen_request() { |
| 1184 | 1182 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection; see mark_mcp_seen(). |
| 1185 | 1183 | $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; |
| 1186 | 1184 | |
| 1187 | - if ( 'betterdocs-mcp' !== $page ) { | |
| 1185 | + if ( 'betterdocs-content-iq' !== $page ) { | |
| 1188 | 1186 | return false; |
| 1189 | 1187 | } |
| 1190 | 1188 | |
| 1191 | 1189 | return current_user_can( 'manage_options' ); |
| @@ -1390,8 +1388,18 @@ | ||
| 1390 | 1388 | 'output', |
| 1391 | 1389 | ), |
| 1392 | 1390 | $parent_slug |
| 1393 | 1391 | ), |
| 1392 | + 'content_intelligence' => $this->normalize_menu( | |
| 1393 | + __( 'Content IQ', 'betterdocs' ), | |
| 1394 | + 'betterdocs-content-iq', | |
| 1395 | + 'read_docs_analytics', | |
| 1396 | + array( | |
| 1397 | + $this, | |
| 1398 | + 'output', | |
| 1399 | + ), | |
| 1400 | + $parent_slug | |
| 1401 | + ), | |
| 1394 | 1402 | 'faq' => $this->normalize_menu( |
| 1395 | 1403 | __( 'FAQ Builder', 'betterdocs' ), |
| 1396 | 1404 | 'betterdocs-faq', |
| 1397 | 1405 | 'read_faq_builder', |
| @@ -1402,8 +1410,17 @@ | ||
| 1402 | 1410 | $parent_slug |
| 1403 | 1411 | ), |
| 1404 | 1412 | ); |
| 1405 | 1413 | |
| 1414 | + // Content Intelligence ships in Pro, which overwrites that same key in place. | |
| 1415 | + // Unlike API Docs it has to sit directly after Analytics, and `menus()` walks | |
| 1416 | + // this array in insertion order — so the slot is declared inside the literal | |
| 1417 | + // above and only withdrawn here. Appending it after the fact, api_docs-style, | |
| 1418 | + // would park it at the bottom of the menu. | |
| 1419 | + if ( ! ( betterdocs()->show_content_intelligence_teaser() || betterdocs()->has_content_intelligence() ) ) { | |
| 1420 | + unset( $betterdocs_admin_pages['content_intelligence'] ); | |
| 1421 | + } | |
| 1422 | + | |
| 1406 | 1423 | // Glossaries is Pro. Reserve this same 'glossaries' slot for Free's locked |
| 1407 | 1424 | // teaser so the item keeps this position; once Pro is active the real |
| 1408 | 1425 | // screen overwrites the key in place (same pattern as API Docs below). |
| 1409 | 1426 | if ( betterdocs()->show_glossary_teaser() || ( betterdocs()->is_pro_active() && betterdocs()->settings->get( 'enable_glossaries' ) == true ) ) { |
| @@ -1450,8 +1467,70 @@ | ||
| 1450 | 1467 | |
| 1451 | 1468 | return apply_filters( 'betterdocs_admin_menu', $betterdocs_admin_pages, array( $this, 'output' ), $parent_slug ); |
| 1452 | 1469 | } |
| 1453 | 1470 | |
| 1471 | + /** | |
| 1472 | + * Put the BetterDocs submenu in a deliberate order. | |
| 1473 | + * | |
| 1474 | + * Order used to be an accident of *when* each item was added: Free declares | |
| 1475 | + * most of them inline, and reserves in-place slots for `glossaries` / | |
| 1476 | + * `api_docs` so Pro can overwrite the key without moving it. Anything added | |
| 1477 | + * purely through this filter, though, could only land at the end — which is | |
| 1478 | + * why Multiple KB (Pro, priority 100) and AI Chatbot Logs sat after | |
| 1479 | + * everything else regardless of where they belong. | |
| 1480 | + * | |
| 1481 | + * Sorting here, at priority 999, fixes that for every source at once: Free's | |
| 1482 | + * own entries, Pro's, and any add-on's. Knowledge Base now follows Tags (it | |
| 1483 | + * is the third taxonomy-ish thing, so it belongs with Categories and Tags | |
| 1484 | + * rather than past Analytics), and API Docs follows Knowledge Base. | |
| 1485 | + * | |
| 1486 | + * Keys not listed keep their relative order and are appended, so an add-on | |
| 1487 | + * that registers something unknown to this list is never dropped. | |
| 1488 | + * | |
| 1489 | + * @param array $pages Menu pages keyed by slug id. | |
| 1490 | + * @return array | |
| 1491 | + */ | |
| 1492 | + public function order_admin_menu( $pages ) { | |
| 1493 | + if ( ! is_array( $pages ) ) { | |
| 1494 | + return $pages; | |
| 1495 | + } | |
| 1496 | + | |
| 1497 | + $order = array( | |
| 1498 | + 'betterdocs', | |
| 1499 | + 'dashboard', | |
| 1500 | + 'all_docs', | |
| 1501 | + 'add_new', | |
| 1502 | + 'categories', | |
| 1503 | + 'tags', | |
| 1504 | + 'multiple_kb', | |
| 1505 | + 'api_docs', | |
| 1506 | + 'settings', | |
| 1507 | + 'mcp', | |
| 1508 | + 'analytics', | |
| 1509 | + // Content IQ reads as a second Analytics screen, so it has to stay | |
| 1510 | + // pinned directly behind it. Without this entry it would fall into | |
| 1511 | + // the unknown-key bucket below and be appended to the bottom of the | |
| 1512 | + // menu — the exact placement the menu literal avoids by declaring | |
| 1513 | + // the slot inline rather than filtering it in api_docs-style. | |
| 1514 | + 'content_intelligence', | |
| 1515 | + 'faq', | |
| 1516 | + 'glossaries', | |
| 1517 | + 'ai_chatbot', | |
| 1518 | + 'ai_chatbot_logs', | |
| 1519 | + ); | |
| 1520 | + | |
| 1521 | + $ordered = array(); | |
| 1522 | + foreach ( $order as $key ) { | |
| 1523 | + if ( array_key_exists( $key, $pages ) ) { | |
| 1524 | + $ordered[ $key ] = $pages[ $key ]; | |
| 1525 | + unset( $pages[ $key ] ); | |
| 1526 | + } | |
| 1527 | + } | |
| 1528 | + | |
| 1529 | + // `$pages` now holds only unknown keys, still in their original order. | |
| 1530 | + return array_merge( $ordered, $pages ); | |
| 1531 | + } | |
| 1532 | + | |
| 1454 | 1533 | public function add_custom_classes_to_menu_items() { |
| 1455 | 1534 | global $menu, $submenu; |
| 1456 | 1535 | |
| 1457 | 1536 | $menu_items = array( |
| @@ -1462,8 +1541,9 @@ | ||
| 1462 | 1541 | 'betterdocs-doc-tags' => 'betterdocs-tags', |
| 1463 | 1542 | 'betterdocs-settings' => 'betterdocs-settings', |
| 1464 | 1543 | 'betterdocs-mcp' => 'betterdocs-mcp', |
| 1465 | 1544 | 'betterdocs-analytics' => 'betterdocs-analytics', |
| 1545 | + 'betterdocs-content-iq' => 'betterdocs-content-iq', | |
| 1466 | 1546 | 'betterdocs-faq' => 'betterdocs-faq', |
| 1467 | 1547 | 'betterdocs-glossaries' => 'betterdocs-glossaries', |
| 1468 | 1548 | 'betterdocs-ai-chatbot' => 'betterdocs-ai-chatbot', |
| 1469 | 1549 | 'betterdocs-api-docs' => 'betterdocs-api-docs', |