PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.2.6
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.2.6
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 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
← All changes | includes/Core/FAQBuilder.php +72 -782 4.6.24.2.6 View file →
@@ -1,19 +1,10 @@
1 1 <?php
2 +
2 3 namespace WPDeveloper\BetterDocs\Core;
3 4
4 -if ( ! defined( 'ABSPATH' ) ) {
5 - exit;
6 -}
7 -
8 -// FAQ-by-category lookups require tax_query / meta_key filters by design.
9 -// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
10 -// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
11 -// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
12 -
13 5 use WP_Error;
14 6 use WP_Query;
15 -use WP_REST_Request;
16 7 use WPDeveloper\BetterDocs\Utils\Base;
17 8 use WPDeveloper\BetterDocs\Utils\Helper;
18 9
19 10 class FAQBuilder extends Base {
@@ -25,63 +16,9 @@
25 16 public $post_type = 'betterdocs_faq';
26 17 public $category = 'betterdocs_faq_category';
27 18
28 19 /**
29 - * Product FAQ groups taxonomy. Shares the betterdocs_faq post type with the
30 - * General FAQ groups but is a separate taxonomy so the two never mix in the
31 - * admin or on the front end. Used by the WooCommerce Product FAQ tab.
32 20 *
33 - * @var string
34 - */
35 - public $product_category = 'betterdocs_product_faq_category';
36 -
37 - /**
38 - * Post meta recording which FAQ Builder tab an FAQ belongs to: 'product' or
39 - * 'general'.
40 - *
41 - * A General and a Product FAQ are the same post type, distinguished only by which
42 - * taxonomy their group lives in — so an FAQ with NO group had no scope at all, and
43 - * an ungrouped Product FAQ (its group deleted, or created without one) was
44 - * indistinguishable from a General one. That's why it used to surface under the
45 - * General tab's "Uncategorized". Recording the scope on the post lets each tab own
46 - * its own Uncategorized bucket.
47 - */
48 - const SCOPE_META = '_betterdocs_faq_scope';
49 -
50 - /** Option flag for the one-time backfill of SCOPE_META on pre-existing FAQs. */
51 - const SCOPE_BACKFILL_OPTION = 'betterdocs_faq_scope_backfilled';
52 -
53 - /**
54 - * Term meta on a Product FAQ group holding the product_cat term IDs the
55 - * group is assigned to. Products in those categories inherit the group.
56 - */
57 - const GROUP_PRODUCT_CATS_META = '_betterdocs_faq_group_product_cats';
58 -
59 - /**
60 - * Term meta on a Product FAQ group holding the individual product IDs the
61 - * group is assigned to directly.
62 - */
63 - const GROUP_PRODUCTS_META = '_betterdocs_faq_group_products';
64 -
65 - /**
66 - * Term meta flagging a Product FAQ group as shown on every product (no
67 - * per-product/per-category targeting). Set on the store-aware sample groups
68 - * so they render storefront-wide immediately; cleared the moment the owner
69 - * saves explicit category/product assignments.
70 - */
71 - const GROUP_ALL_PRODUCTS_META = '_betterdocs_faq_group_all_products';
72 -
73 - /**
74 - * Per-request cache of draft FAQ counts keyed by taxonomy → [ term_id => count ].
75 - * Lets the betterdocs_draft_count REST field resolve every term from a single
76 - * grouped query instead of one WP_Query per term (N+1).
77 - *
78 - * @var array<string, array<int, int>>
79 - */
80 - protected $draft_count_cache = [];
81 -
82 - /**
83 - *
84 21 * Initialize the class and start calling our hooks and filters
85 22 *
86 23 * @since 1.0.0
87 24 *
@@ -90,97 +27,12 @@
90 27 // assign default admin capabilities for docs, doc terms, doc tags, knowledge base
91 28 add_action( 'init', [ $this, 'register_post' ] );
92 29 // fires after a new betterdocs_faq_category is created
93 30 add_action( 'created_betterdocs_faq_category', [ $this, 'action_created_betterdocs_faq_category' ], 10, 2 );
94 - add_action( 'created_betterdocs_product_faq_category', [ $this, 'action_created_betterdocs_faq_category' ], 10, 2 );
95 31 add_action( 'rest_api_init', [ $this, 'register_api_endpoint' ] );
96 - add_action( 'rest_api_init', [ $this, 'register_category_count_fields' ] );
97 -
98 - // Keep each FAQ's scope ('general' | 'product') in sync however its group is
99 - // assigned — the Builder, the sample-FAQ generator, an import, or the classic
100 - // post editor all end up here — so an FAQ that later loses its group is still
101 - // known to belong to its own tab's "Uncategorized" bucket.
102 - add_action( 'set_object_terms', [ $this, 'sync_faq_scope_meta' ], 10, 4 );
103 - // One-time backfill for FAQs that predate the scope meta.
104 - add_action( 'admin_init', [ $this, 'maybe_backfill_faq_scope' ] );
105 32 add_action( 'rest_betterdocs_faq_category_query', [ $this, 'faq_category_orderby_meta' ], 10, 2 );
106 - add_action( 'rest_betterdocs_product_faq_category_query', [ $this, 'faq_category_orderby_meta' ], 10, 2 );
107 -
108 - // Classic FAQ Group list (edit-tags.php?taxonomy=betterdocs_faq_category):
109 - // enable drag-and-drop ordering + persist it, mirroring the classic Doc
110 - // Categories screen.
111 - add_action( 'admin_enqueue_scripts', [ $this, 'classic_category_scripts' ] );
112 - add_action( 'wp_ajax_update_faq_cat_order', [ $this, 'ajax_update_category_order' ] );
113 - add_action( 'admin_head', [ $this, 'order_admin_terms' ] );
114 33 }
115 34
116 - /**
117 - * Expose a per-group draft FAQ count on the betterdocs_faq_category REST
118 - * response so the FAQ Builder can show "N questions • M draft". The term
119 - * `count` only tracks published FAQs (via _update_post_term_count), so the
120 - * draft count is computed here.
121 - *
122 - * @since 4.4.0
123 - */
124 - public function register_category_count_fields() {
125 - foreach ( [ $this->category, $this->product_category ] as $taxonomy ) {
126 - register_rest_field(
127 - $taxonomy,
128 - 'betterdocs_draft_count',
129 - [
130 - 'get_callback' => function ( $term ) {
131 - $counts = $this->get_draft_counts_by_term( $term['taxonomy'] );
132 - return isset( $counts[ (int) $term['id'] ] ) ? (int) $counts[ (int) $term['id'] ] : 0;
133 - },
134 - 'schema' => [
135 - 'type' => 'integer',
136 - 'context' => [ 'view', 'edit' ],
137 - ],
138 - ]
139 - );
140 - }
141 - }
142 -
143 - /**
144 - * Draft FAQ counts for every term in a taxonomy, keyed by term_id.
145 - *
146 - * Computed once per request with a single grouped query and memoized, so the
147 - * betterdocs_draft_count REST field no longer fires a WP_Query per term when
148 - * a category list is assembled (avoids the N+1 on large group counts).
149 - *
150 - * @param string $taxonomy
151 - * @return array<int, int>
152 - */
153 - protected function get_draft_counts_by_term( $taxonomy ) {
154 - if ( isset( $this->draft_count_cache[ $taxonomy ] ) ) {
155 - return $this->draft_count_cache[ $taxonomy ];
156 - }
157 -
158 - global $wpdb;
159 -
160 - $rows = $wpdb->get_results(
161 - $wpdb->prepare(
162 - "SELECT tt.term_id, COUNT( p.ID ) AS draft_count
163 - FROM {$wpdb->posts} p
164 - INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
165 - INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
166 - WHERE p.post_type = %s
167 - AND p.post_status = 'draft'
168 - AND tt.taxonomy = %s
169 - GROUP BY tt.term_id",
170 - $this->post_type,
171 - $taxonomy
172 - )
173 - ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
174 -
175 - $counts = [];
176 - foreach ( (array) $rows as $row ) {
177 - $counts[ (int) $row->term_id ] = (int) $row->draft_count;
178 - }
179 -
180 - return $this->draft_count_cache[ $taxonomy ] = $counts;
181 - }
182 -
183 35 public function output() {
184 36 betterdocs()->views->get( 'admin/faq-builder' );
185 37 }
186 38
@@ -232,62 +84,8 @@
232 84 register_term_meta( $this->category, '_betterdocs_faq_order', [ 'show_in_rest' => true ] );
233 85 register_term_meta( $this->category, 'faq_group_icon', [ 'show_in_rest' => true ]);
234 86
235 87 /**
236 - * Register the Product FAQ groups taxonomy (WooCommerce tab). It mirrors
237 - * the General FAQ category taxonomy and shares the betterdocs_faq post
238 - * type, but is kept separate so Product FAQ groups never appear in the
239 - * General FAQ Builder tab.
240 - */
241 - $product_category_labels = [
242 - 'name' => __( 'Product FAQ Categories', 'betterdocs' ),
243 - 'singular_name' => __( 'Product FAQ Category', 'betterdocs' ),
244 - 'all_items' => __( 'Product FAQ Categories', 'betterdocs' ),
245 - 'parent_item' => __( 'Parent Product FAQ Category', 'betterdocs' ),
246 - 'parent_item_colon' => __( 'Parent Product FAQ Category:', 'betterdocs' ),
247 - 'edit_item' => __( 'Edit Product FAQ Category', 'betterdocs' ),
248 - 'update_item' => __( 'Update Product FAQ Category', 'betterdocs' ),
249 - 'add_new_item' => __( 'Add New Product FAQ Category', 'betterdocs' ),
250 - 'new_item_name' => __( 'New Product FAQ Category Name', 'betterdocs' ),
251 - 'menu_name' => __( 'Product FAQ Categories', 'betterdocs' )
252 - ];
253 -
254 - $product_category_args = $category_args;
255 - $product_category_args['labels'] = $product_category_labels;
256 - $product_category_args['show_admin_column'] = false;
257 -
258 - register_taxonomy( $this->product_category, [ $this->post_type ], $product_category_args );
259 - register_term_meta( $this->product_category, 'order', [ 'show_in_rest' => true ] );
260 - register_term_meta( $this->product_category, 'status', [ 'show_in_rest' => true ] );
261 - register_term_meta( $this->product_category, '_betterdocs_faq_order', [ 'show_in_rest' => true ] );
262 - register_term_meta( $this->product_category, 'faq_group_icon', [ 'show_in_rest' => true ] );
263 -
264 - // Product FAQ group assignments: which WooCommerce product categories and
265 - // which individual products this group is shown on. Stored on the group
266 - // term so the assignment lives in the Create/Update FAQ Group screen.
267 - $assignment_meta_args = [
268 - 'single' => true,
269 - 'type' => 'array',
270 - 'show_in_rest' => [
271 - 'schema' => [
272 - 'type' => 'array',
273 - 'items' => [ 'type' => 'integer' ],
274 - ],
275 - ],
276 - ];
277 - register_term_meta( $this->product_category, self::GROUP_PRODUCT_CATS_META, $assignment_meta_args );
278 - register_term_meta( $this->product_category, self::GROUP_PRODUCTS_META, $assignment_meta_args );
279 - register_term_meta(
280 - $this->product_category,
281 - self::GROUP_ALL_PRODUCTS_META,
282 - [
283 - 'single' => true,
284 - 'type' => 'boolean',
285 - 'show_in_rest' => true,
286 - ]
287 - );
288 -
289 - /**
290 88 * Register post type
291 89 */
292 90 $labels = [
293 91 'name' => __( 'BetterDocs FAQ', 'betterdocs' ),
@@ -334,11 +132,9 @@
334 132 *
335 133 * @param string $tax_slug The taxonomy's slug.
336 134 */
337 135 public function action_created_betterdocs_faq_category( $term_id ) {
338 - $term = get_term( $term_id );
339 - $taxonomy = ( $term && ! is_wp_error( $term ) ) ? $term->taxonomy : $this->category;
340 - $order = $this->get_max_taxonomy_order( $taxonomy );
136 + $order = $this->get_max_taxonomy_order( 'betterdocs_faq_category' );
341 137 update_term_meta( $term_id, 'order', $order++ );
342 138 update_term_meta( $term_id, 'status', 1 );
343 139 }
344 140
@@ -368,14 +164,14 @@
368 164 * Get the maximum order for this taxonomy. This will be applied to terms that don't have a tax position.
369 165 */
370 166 private function get_max_taxonomy_order( $tax_slug ) {
371 167 global $wpdb;
372 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- live max-order needed when assigning new terms; cache would be stale.
168 + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder
373 169 $max_term_order = $wpdb->get_col(
374 170 $wpdb->prepare(
375 171 "SELECT MAX( CAST( tm.meta_value AS UNSIGNED ) )
376 172 FROM $wpdb->terms t
377 - JOIN $wpdb->term_taxonomy tt ON t.term_id = tt.term_id AND tt.taxonomy = %s
173 + JOIN $wpdb->term_taxonomy tt ON t.term_id = tt.term_id AND tt.taxonomy = '%s'
378 174 JOIN $wpdb->termmeta tm ON tm.term_id = t.term_id WHERE tm.meta_key = 'order'",
379 175 $tax_slug
380 176 )
381 177 );
@@ -392,15 +188,8 @@
392 188 * @param array $taxonomies Array of taxonomy names.
393 189 * @param array $args Array of term query args.
394 190 */
395 191 public function set_tax_order( $pieces, $taxonomies, $args ) {
396 - // Only force the manual drag-drop `order` meta when the FAQ Builder
397 - // header dropdown is on "default". For name/count/id sort modes, let
398 - // get_terms() keep its own orderby so the front end matches the builder.
399 - if ( betterdocs()->query->get_faq_order_key() !== 'default' ) {
400 - return $pieces;
401 - }
402 -
403 192 foreach ( $taxonomies as $taxonomy ) {
404 193 global $wpdb;
405 194
406 195 if ( $taxonomy === 'betterdocs_faq_category' ) {
@@ -437,138 +226,8 @@
437 226 protected function does_substring_exist( $string, $substring ) {
438 227 return strstr( $string, $substring ) !== false;
439 228 }
440 229
441 - /**
442 - * Load the shared drag-and-drop sorter on the classic FAQ Group list
443 - * (edit-tags.php?taxonomy=betterdocs_faq_category). Reuses the generic,
444 - * config-driven admin/js/category-edit.js — the same script the classic Doc
445 - * Categories screen uses — pointed at the FAQ category ordering AJAX action.
446 - *
447 - * jquery + jquery-ui-sortable are declared explicitly so `.sortable()` is
448 - * always defined (the bare script reported jQuery/Sortable as undefined here
449 - * because no sorter was enqueued on this screen at all).
450 - *
451 - * @param string $hook Current admin page hook.
452 - */
453 - public function classic_category_scripts( $hook ) {
454 - if ( 'edit-tags.php' !== $hook ) {
455 - return;
456 - }
457 -
458 - $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
459 - if ( ! $screen || $screen->taxonomy !== $this->category ) {
460 - return;
461 - }
462 -
463 - betterdocs()->assets->enqueue(
464 - 'betterdocs-category-edit',
465 - 'admin/js/category-edit.js',
466 - [ 'jquery', 'jquery-ui-sortable' ]
467 - );
468 -
469 - betterdocs()->assets->localize(
470 - 'betterdocs-category-edit',
471 - 'betterdocsCategorySorting',
472 - [
473 - 'action' => 'update_faq_cat_order',
474 - 'selector' => '.taxonomy-' . $this->category,
475 - 'ajaxurl' => admin_url( 'admin-ajax.php' ),
476 - 'nonce' => wp_create_nonce( 'faq_cat_order_nonce' ),
477 - // Paged only sets the ordering base offset (the AJAX write itself
478 - // is nonce-protected), so read it directly from the WP term-list
479 - // pagination link so cross-page ordering stays correct.
480 - 'paged' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : 0, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
481 - 'per_page_id' => "edit_{$this->category}_per_page",
482 - ]
483 - );
484 - }
485 -
486 - /**
487 - * Persist FAQ Group order from the classic drag-and-drop sorter. Writes the
488 - * `order` term meta that the React FAQ Builder and the front end already read,
489 - * so all three stay in sync. Mirrors PostType::update_category_order.
490 - */
491 - public function ajax_update_category_order() {
492 - if ( ! check_ajax_referer( 'faq_cat_order_nonce', 'nonce', false ) ) {
493 - wp_send_json_error( __( 'Nonce Failed', 'betterdocs' ) );
494 - }
495 -
496 - if ( ! current_user_can( 'edit_others_posts' ) ) {
497 - wp_send_json_error( __( 'You don\'t have permission to manage FAQ groups.', 'betterdocs' ) );
498 - }
499 -
500 - $base_index = isset( $_POST['base_index'] ) ? intval( $_POST['base_index'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
501 -
502 - if ( isset( $_POST['data'] ) && is_array( $_POST['data'] ) ) {
503 - $ordering = array_filter(
504 - array_map(
505 - function ( $item ) {
506 - if ( is_array( $item ) && isset( $item['term_id'], $item['order'] ) ) {
507 - return [
508 - 'term_id' => intval( $item['term_id'] ),
509 - 'order' => intval( $item['order'] ),
510 - ];
511 - }
512 - return null;
513 - },
514 - wp_unslash( $_POST['data'] ) // phpcs:ignore
515 - )
516 - );
517 - } else {
518 - $ordering = [];
519 - }
520 -
521 - foreach ( $ordering as $order_data ) {
522 - update_term_meta( $order_data['term_id'], 'order', $order_data['order'] + $base_index );
523 - }
524 -
525 - wp_send_json_success( __( 'Successfully updated.', 'betterdocs' ) );
526 - }
527 -
528 - /**
529 - * Order the classic FAQ Group list by the manual `order` term meta so the
530 - * drag-and-drop order persists across reloads. Seeds the meta for any terms
531 - * missing it first (e.g. groups created before ordering existed). Mirrors
532 - * PostType::order_terms for the Doc Categories screen.
533 - */
534 - public function order_admin_terms() {
535 - global $current_screen;
536 -
537 - if (
538 - ! isset( $_GET['orderby'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
539 - $current_screen &&
540 - ! empty( $current_screen->base ) &&
541 - $current_screen->base === 'edit-tags' &&
542 - $current_screen->taxonomy === $this->category
543 - ) {
544 - $this->default_term_order( $this->category );
545 - add_filter( 'terms_clauses', [ $this, 'admin_order_terms_clauses' ], 10, 3 );
546 - }
547 - }
548 -
549 - /**
550 - * terms_clauses filter (classic FAQ Group admin list only): force ORDER BY
551 - * the `order` term meta. Unlike set_tax_order this is not gated on the
552 - * builder's saved order key — the classic screen is always manual ordering.
553 - */
554 - public function admin_order_terms_clauses( $pieces, $taxonomies, $args ) {
555 - if ( ! in_array( $this->category, (array) $taxonomies, true ) ) {
556 - return $pieces;
557 - }
558 -
559 - global $wpdb;
560 - $join_statement = " LEFT JOIN $wpdb->termmeta AS bd_faq_order ON t.term_id = bd_faq_order.term_id AND bd_faq_order.meta_key = 'order'";
561 -
562 - if ( ! $this->does_substring_exist( $pieces['join'], $join_statement ) ) {
563 - $pieces['join'] .= $join_statement;
564 - }
565 -
566 - $pieces['orderby'] = 'ORDER BY CAST( bd_faq_order.meta_value AS UNSIGNED )';
567 -
568 - return $pieces;
569 - }
570 -
571 230 public function register_api_endpoint() {
572 231 register_rest_route(
573 232 $this->namespace,
574 233 '/faq/sample_data',
@@ -586,11 +245,9 @@
586 245 '/faq/posts/(?P<type>\S+)',
587 246 [
588 247 'methods' => [ 'GET' ],
589 248 'callback' => [ $this, 'fetch_faq_posts' ],
590 - 'permission_callback' => function () {
591 - return current_user_can( 'edit_others_posts' );
592 - }
249 + 'permission_callback' => '__return_true'
593 250 ]
594 251 );
595 252
596 253 register_rest_route(
@@ -702,27 +359,13 @@
702 359 );
703 360
704 361 register_rest_route(
705 362 $this->namespace,
706 - '/faq/order',
707 - [
708 - 'methods' => [ 'POST' ],
709 - 'callback' => [ $this, 'update_faq_order_preference' ],
710 - 'permission_callback' => function () {
711 - return current_user_can( 'edit_others_posts' );
712 - }
713 - ]
714 - );
715 -
716 - register_rest_route(
717 - $this->namespace,
718 363 '/faq/uncategorised',
719 364 [
720 365 'methods' => [ 'GET' ],
721 366 'callback' => [ $this, 'get_uncategorised_faq' ],
722 - 'permission_callback' => function () {
723 - return current_user_can( 'edit_others_posts' );
724 - }
367 + 'permission_callback' => '__return_true'
725 368 ]
726 369 );
727 370
728 371 register_rest_route(
@@ -730,21 +373,13 @@
730 373 '/faq/category_search',
731 374 [
732 375 'methods' => [ 'GET' ],
733 376 'callback' => [ $this, 'category_search' ],
734 - 'permission_callback' => function () {
735 - return current_user_can( 'edit_others_posts' );
736 - },
377 + 'permission_callback' => '__return_true',
737 378 'args' => [
738 379 'title' => [
739 - 'type' => 'string',
740 - 'required' => true,
741 - 'sanitize_callback' => 'sanitize_text_field'
742 - ],
743 - 'taxonomy' => [
744 - 'type' => 'string',
745 - 'required' => false,
746 - 'sanitize_callback' => 'sanitize_text_field'
380 + 'type' => 'string',
381 + 'required' => true
747 382 ]
748 383 ]
749 384 ]
750 385 );
@@ -765,63 +400,27 @@
765 400 }
766 401 return true;
767 402 }
768 403
769 - /**
770 - * Resolve the taxonomy from a request, whitelisted to the General and
771 - * Product FAQ category taxonomies. Defaults to the General taxonomy so
772 - * existing callers (which never send a taxonomy) keep their behavior.
773 - *
774 - * @param \WP_REST_Request $params
775 - * @return string
776 - */
777 - private function resolve_taxonomy( $params ) {
778 - $taxonomy = $params->get_param( 'taxonomy' );
779 - return in_array( $taxonomy, [ $this->category, $this->product_category ], true )
780 - ? $taxonomy
781 - : $this->category;
782 - }
783 -
784 404 public function create_faq_category( $params ) {
785 - // The WP term `name` column is varchar(200); cap the length so an
786 - // over-long title can't trigger a DB insert error (the React form caps
787 - // this too, this is the server-side guard).
788 - $title = mb_substr( (string) $params->get_param( 'title' ), 0, 200 );
405 + $title = $params->get_param( 'title' );
789 406 $description = $params->get_param( 'description' );
790 407 $description = ( $description !== 'undefined' ) ? $description : '';
791 408 $slug = $params->get_param( 'slug' );
792 409 $group_icon_url = $params->get_param('group_icon_url');
793 - $taxonomy = $this->resolve_taxonomy( $params );
794 - $result = $this->insert_betterdocs_faq_category(
795 - $title,
796 - $description,
797 - $group_icon_url,
798 - $slug,
799 - $taxonomy,
800 - (array) $params->get_param( 'product_cats' ),
801 - (array) $params->get_param( 'products' ),
802 - $this->all_products_param( $params )
803 - );
804 -
805 - if ( is_wp_error( $result ) ) {
806 - return $result;
807 - }
808 -
809 - return rest_ensure_response( array( 'success' => true, 'term_id' => (int) $result ) );
410 + return $this->insert_betterdocs_faq_category( $title, $description, $group_icon_url, $slug );
810 411 }
811 412
812 413 public function update_faq_category( $params ) {
813 414 $term_id = $params->get_param( 'term_id' );
814 - // Cap at the varchar(200) term-name limit (server-side guard).
815 - $title = mb_substr( (string) $params->get_param( 'title' ), 0, 200 );
415 + $title = $params->get_param( 'title' );
816 416 $description = $params->get_param( 'description' );
817 417 $group_icon_url = $params->get_param('group_icon_url');
818 418 $description = ( $description !== 'undefined' ) ? $description : '';
819 419 $slug = $params->get_param( 'slug' );
820 - $taxonomy = $this->resolve_taxonomy( $params );
821 420 $update = wp_update_term(
822 421 $term_id,
823 - $taxonomy,
422 + 'betterdocs_faq_category',
824 423 [
825 424 'name' => $title,
826 425 'slug' => $slug,
827 426 'description' => $description
@@ -834,15 +433,8 @@
834 433 $term_id = isset( $update['term_id'] ) ? $update['term_id'] : 0;
835 434 if( $term_id != 0 ) {
836 435 $previous_icon_url = get_term_meta($term_id, 'faq_group_icon', true);
837 436 update_term_meta($term_id, 'faq_group_icon', $group_icon_url, $previous_icon_url);
838 - $this->save_group_assignments(
839 - $term_id,
840 - $taxonomy,
841 - (array) $params->get_param( 'product_cats' ),
842 - (array) $params->get_param( 'products' ),
843 - $this->all_products_param( $params )
844 - );
845 437 }
846 438 return true;
847 439 }
848 440 }
@@ -849,15 +441,14 @@
849 441
850 442 public function delete_faq_category( $params ) {
851 443 $term_id = $params->get_param( 'term_id' );
852 444 $delete_all_docs = $params->get_param('with_all_post');
853 - $taxonomy = $this->resolve_taxonomy( $params );
854 445
855 446 if ( $delete_all_docs ) {
856 - Helper::delete_specific_faq_posts_by_faq_category( $term_id, $taxonomy );
447 + Helper::delete_specific_faq_posts_by_faq_category( $term_id );
857 448 }
858 449
859 - $delete = wp_delete_term( $term_id, $taxonomy );
450 + $delete = wp_delete_term( $term_id, 'betterdocs_faq_category' );
860 451
861 452 if ( is_wp_error( $delete ) ) {
862 453 return $delete;
863 454 } else {
@@ -864,12 +455,12 @@
864 455 return true;
865 456 }
866 457 }
867 458
868 - public function insert_betterdocs_faq_category( $title, $description, $icon_url, $slug = '', $taxonomy = 'betterdocs_faq_category', $product_cats = [], $products = [], $all_products = false ) {
459 + public function insert_betterdocs_faq_category( $title, $description, $icon_url, $slug = '' ) {
869 460 $insert_term = wp_insert_term(
870 461 $title,
871 - $taxonomy,
462 + 'betterdocs_faq_category',
872 463 [
873 464 'slug' => $slug,
874 465 'description' => $description
875 466 ]
@@ -880,89 +471,13 @@
880 471 } else {
881 472 $term_id = isset( $insert_term['term_id'] ) ? $insert_term['term_id'] : 0;
882 473 if( $term_id != 0 ) {
883 474 update_term_meta($term_id, 'faq_group_icon', $icon_url);
884 - $this->save_group_assignments( $term_id, $taxonomy, $product_cats, $products, $all_products );
885 475 }
886 - // Return the new term id so the admin can jump to its pagination
887 - // page and highlight it (mirrors the new-FAQ reveal flow).
888 - return (int) $term_id;
476 + return true;
889 477 }
890 478 }
891 479
892 - /**
893 - * Persist a Product FAQ group's targeting. Three mutually-exclusive scopes:
894 - * - all products → GROUP_ALL_PRODUCTS_META, shown on every product page;
895 - * - specific cats/products → GROUP_PRODUCT_CATS_META / GROUP_PRODUCTS_META;
896 - * - none → the group is hidden on the storefront.
897 - *
898 - * No-op for any taxonomy other than the Product FAQ groups taxonomy.
899 - *
900 - * @param int $term_id
901 - * @param string $taxonomy
902 - * @param array $product_cats Incoming product_cat term IDs.
903 - * @param array $products Incoming product post IDs.
904 - * @param bool $all_products When true, show on ALL products and clear the cat/product
905 - * targets (the scopes are mutually exclusive).
906 - */
907 - private function save_group_assignments( $term_id, $taxonomy, $product_cats, $products, $all_products = false ) {
908 - if ( $taxonomy !== $this->product_category ) {
909 - return;
910 - }
911 -
912 - // "Show on all products" wins and clears the specific targets, so a store-wide
913 - // group (e.g. the generated "Shipping, Returns & Payments") can be switched to
914 - // specific categories/products and back, from the same modal.
915 - if ( $all_products ) {
916 - update_term_meta( $term_id, self::GROUP_ALL_PRODUCTS_META, true );
917 - delete_term_meta( $term_id, self::GROUP_PRODUCT_CATS_META );
918 - delete_term_meta( $term_id, self::GROUP_PRODUCTS_META );
919 - return;
920 - }
921 -
922 - $cat_ids = [];
923 - foreach ( (array) $product_cats as $cid ) {
924 - $cid = (int) $cid;
925 - if ( $cid > 0 && term_exists( $cid, 'product_cat' ) ) {
926 - $cat_ids[] = $cid;
927 - }
928 - }
929 - $cat_ids = array_values( array_unique( $cat_ids ) );
930 -
931 - $product_ids = [];
932 - foreach ( (array) $products as $pid ) {
933 - $pid = (int) $pid;
934 - if ( $pid > 0 && 'product' === get_post_type( $pid ) ) {
935 - $product_ids[] = $pid;
936 - }
937 - }
938 - $product_ids = array_values( array_unique( $product_ids ) );
939 -
940 - if ( empty( $cat_ids ) ) {
941 - delete_term_meta( $term_id, self::GROUP_PRODUCT_CATS_META );
942 - } else {
943 - update_term_meta( $term_id, self::GROUP_PRODUCT_CATS_META, $cat_ids );
944 - }
945 -
946 - if ( empty( $product_ids ) ) {
947 - delete_term_meta( $term_id, self::GROUP_PRODUCTS_META );
948 - } else {
949 - update_term_meta( $term_id, self::GROUP_PRODUCTS_META, $product_ids );
950 - }
951 -
952 - // Explicitly not "all products" → drop the match-all flag.
953 - delete_term_meta( $term_id, self::GROUP_ALL_PRODUCTS_META );
954 - }
955 -
956 - /**
957 - * Read + normalize the `all_products` REST param (accepts '1'/'0'/true/false).
958 - *
959 - * @return bool
960 - */
961 - private function all_products_param( $params ) {
962 - return filter_var( $params->get_param( 'all_products' ), FILTER_VALIDATE_BOOLEAN );
963 - }
964 -
965 480 public function update_faq_category_order( $params ) {
966 481 $faq_category_order = $params->get_param( 'faq_category_order' );
967 482 $faq_category_order = json_decode( $faq_category_order, true );
968 483
@@ -973,9 +488,9 @@
973 488 }
974 489 return true;
975 490 }
976 491
977 - public function insert_betterdocs_faq( $post_title, $post_content, $term_id, $taxonomy = 'betterdocs_faq_category' ) {
492 + public function insert_betterdocs_faq( $post_title, $post_content, $term_id ) {
978 493 $post = wp_insert_post(
979 494 [
980 495 'post_type' => 'betterdocs_faq',
981 496 'post_title' => wp_strip_all_tags( $post_title ),
@@ -983,42 +498,31 @@
983 498 'post_status' => 'publish'
984 499 ]
985 500 );
986 501
987 - // Stamp the scope from the tab this FAQ was created in. Doing it here (and not
988 - // only in sync_faq_scope_meta) is what makes an FAQ created WITHOUT a group land
989 - // in the right tab's "Uncategorized" — with no terms assigned, set_object_terms
990 - // never fires.
991 - if ( $post && ! is_wp_error( $post ) ) {
992 - update_post_meta(
993 - $post,
994 - self::SCOPE_META,
995 - $taxonomy === $this->product_category ? 'product' : 'general'
996 - );
997 - }
998 -
999 502 if ( $term_id ) {
1000 - $set_terms = wp_set_object_terms( $post, $term_id, $taxonomy );
503 + $set_terms = wp_set_object_terms( $post, $term_id, 'betterdocs_faq_category' );
1001 504 if ( is_wp_error( $set_terms ) ) {
1002 505 return $set_terms;
506 + } else {
507 + return $this->update_faq_order_on_insert( $term_id, $post );
1003 508 }
1004 - $this->update_faq_order_on_insert( $term_id, $post );
509 + } else {
510 + return $post;
1005 511 }
1006 -
1007 - // Always return the new post ID so the admin can reveal/highlight the
1008 - // created FAQ (the grouped path previously returned the term-meta result).
1009 - return $post;
1010 512 }
1011 513
1012 514 public function update_faq_order_on_insert( $term_id, $post ) {
1013 - // QA-008: read the single stored value and strip blanks so the very first
1014 - // insert (no existing meta) doesn't explode(',', null) into a corrupt [''].
1015 - $existing = (string) get_term_meta( $term_id, '_betterdocs_faq_order', true );
1016 - $term_meta_arr = array_filter( array_map( 'trim', explode( ',', $existing ) ), 'strlen' );
1017 - if ( ! in_array( (string) $post, $term_meta_arr, true ) ) {
1018 - array_unshift( $term_meta_arr, $post );
1019 - $docs_ordering_data = filter_var_array( wp_unslash( $term_meta_arr ), FILTER_SANITIZE_NUMBER_INT );
1020 - return update_term_meta( $term_id, '_betterdocs_faq_order', implode( ',', $docs_ordering_data ) );
515 + $term_meta = get_term_meta( $term_id, '_betterdocs_faq_order' );
516 + if ( ! empty( $term_meta ) ) {
517 + $term_meta_arr = explode( ',', $term_meta[0] );
518 + if ( ! in_array( $post, $term_meta_arr ) ) {
519 + array_unshift( $term_meta_arr, $post );
520 + $docs_ordering_data = filter_var_array( wp_unslash( $term_meta_arr ), FILTER_SANITIZE_NUMBER_INT );
521 + return update_term_meta( $term_id, '_betterdocs_faq_order', implode( ',', $docs_ordering_data ) );
522 + }
523 + } else {
524 + return update_term_meta( $term_id, '_betterdocs_faq_order', $post );
1021 525 }
1022 526 }
1023 527
1024 528 /**
@@ -1030,48 +534,21 @@
1030 534 $posts = $params->get_param( 'posts' );
1031 535 return update_term_meta( $term_id, '_betterdocs_faq_order', $posts );
1032 536 }
1033 537
1034 - /**
1035 - * Persist the FAQ Builder header order dropdown so the front end can mirror
1036 - * it. Stored as a single global preference in the `betterdocs_faq_order`
1037 - * option and read back by Query::get_faq_order_key().
1038 - */
1039 - public function update_faq_order_preference( $params ) {
1040 - $order = $params->get_param( 'order' );
1041 - $allowed = array( 'default', 'most_recent', 'least_recent', 'a_to_z', 'z_to_a', 'most_questions' );
1042 -
1043 - if ( ! in_array( $order, $allowed, true ) ) {
1044 - $order = 'default';
1045 - }
1046 -
1047 - update_option( 'betterdocs_faq_order', $order );
1048 -
1049 - return rest_ensure_response( array( 'success' => true, 'order' => $order ) );
1050 - }
1051 -
1052 538 public function create_betterdocs_faq( $params ) {
1053 539 $post_title = $params->get_param( 'post_title' );
1054 540 $post_content = $params->get_param( 'post_content' );
1055 541 $term_id = $params->get_param( 'term_id' );
1056 - $taxonomy = $this->resolve_taxonomy( $params );
1057 - return $this->insert_betterdocs_faq( $post_title, $post_content, $term_id, $taxonomy );
542 + return $this->insert_betterdocs_faq( $post_title, $post_content, $term_id );
1058 543 }
1059 544
1060 545 public function update_betterdocs_faq( $params ) {
1061 - $post_id = (int) $params->get_param( 'post_id' );
1062 -
1063 - // QA-001: never let an arbitrary post ID be retyped/overwritten through
1064 - // this endpoint. Only operate on posts that are already BetterDocs FAQs.
1065 - if ( ! $post_id || get_post_type( $post_id ) !== $this->post_type ) {
1066 - return new WP_Error( 'betterdocs_invalid_faq', __( 'Invalid FAQ ID.', 'betterdocs' ), [ 'status' => 404 ] );
1067 - }
1068 -
546 + $post_id = $params->get_param( 'post_id' );
1069 547 $post_title = $params->get_param( 'post_title' );
1070 548 $post_content = $params->get_param( 'post_content' );
1071 549 $status = $params->get_param( 'status' );
1072 - $term_id = (int) $params->get_param( 'term_id' );
1073 - $taxonomy = $this->resolve_taxonomy( $params );
550 + $term_id = $params->get_param( 'term_id' );
1074 551 if ( $status ) {
1075 552 $data = [
1076 553 'post_type' => 'betterdocs_faq',
1077 554 'ID' => $post_id,
@@ -1080,30 +557,20 @@
1080 557 } else {
1081 558 $data = [
1082 559 'post_type' => 'betterdocs_faq',
1083 560 'ID' => $post_id,
1084 - // QA-009: sanitize server-side — the editor posts raw TinyMCE
1085 - // HTML. Title is plain text; content keeps post-safe HTML only
1086 - // (scripts / event handlers stripped) even for unfiltered_html users.
1087 - 'post_title' => sanitize_text_field( $post_title ),
1088 - 'post_content' => wp_kses_post( $post_content ),
561 + 'post_title' => $post_title,
562 + 'post_content' => $post_content
1089 563 ];
1090 564
1091 - // QA-007: only assign a term that actually exists in the resolved FAQ
1092 - // taxonomy — an invalid term_id would silently orphan the FAQ.
1093 - if ( $term_id && term_exists( $term_id, $taxonomy ) ) {
565 + if ( $term_id ) {
1094 566 $data['tax_input'] = [
1095 - $taxonomy => [ $term_id ],
567 + 'betterdocs_faq_category' => $term_id
1096 568 ];
1097 569
1098 - // QA-008: build the order list defensively. On the first update the
1099 - // term has no _betterdocs_faq_order meta; the old code did
1100 - // explode(',', null) → [''] which corrupted the order with a blank
1101 - // entry (plus a PHP 8.1 "null to explode" deprecation). Read the
1102 - // single value and drop blanks instead.
1103 - $existing = (string) get_term_meta( $term_id, '_betterdocs_faq_order', true );
1104 - $term_meta_arr = array_filter( array_map( 'trim', explode( ',', $existing ) ), 'strlen' );
1105 - if ( ! in_array( (string) $post_id, $term_meta_arr, true ) ) {
570 + $term_meta = get_term_meta( $term_id, '_betterdocs_faq_order' );
571 + $term_meta_arr = explode( ',', $term_meta[0] );
572 + if ( ! in_array( $post_id, $term_meta_arr ) ) {
1106 573 array_unshift( $term_meta_arr, $post_id );
1107 574 $docs_ordering_data = filter_var_array( wp_unslash( $term_meta_arr ), FILTER_SANITIZE_NUMBER_INT );
1108 575 update_term_meta( $term_id, '_betterdocs_faq_order', implode( ',', $docs_ordering_data ) );
1109 576 }
@@ -1113,15 +580,9 @@
1113 580 return wp_update_post( $data );
1114 581 }
1115 582
1116 583 public function delete_betterdocs_faq( $params ) {
1117 - $post_id = (int) $params->get_param( 'post_id' );
1118 -
1119 - // QA-001: refuse to delete anything that isn't a BetterDocs FAQ.
1120 - if ( ! $post_id || get_post_type( $post_id ) !== $this->post_type ) {
1121 - return new WP_Error( 'betterdocs_invalid_faq', __( 'Invalid FAQ ID.', 'betterdocs' ), [ 'status' => 404 ] );
1122 - }
1123 -
584 + $post_id = $params->get_param( 'post_id' );
1124 585 return wp_delete_post( $post_id );
1125 586 }
1126 587
1127 588 public function faq_post_loop( $args ) {
@@ -1157,9 +618,8 @@
1157 618 );
1158 619
1159 620 if ( $taxonomy_objects && ! is_wp_error( $taxonomy_objects ) ) :
1160 621 foreach ( $taxonomy_objects as $term ) :
1161 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- core FAQ-by-category query; tax filtering is required functionality.
1162 622 $args = [
1163 623 'post_type' => 'betterdocs_faq',
1164 624 'post_status' => 'publish',
1165 625 'post_per_page' => -1,
@@ -1192,172 +652,47 @@
1192 652
1193 653 return $faq;
1194 654 }
1195 655
1196 - /**
1197 - * Record an FAQ's scope whenever its group is (re)assigned, so the scope survives
1198 - * the group being deleted. Fires for every path that assigns terms.
1199 - *
1200 - * @param int $object_id Post ID.
1201 - * @param array $terms Terms assigned (unused).
1202 - * @param array $tt_ids Term-taxonomy IDs.
1203 - * @param string $taxonomy Taxonomy the terms belong to.
1204 - */
1205 - public function sync_faq_scope_meta( $object_id, $terms, $tt_ids, $taxonomy ) {
1206 - if ( get_post_type( $object_id ) !== $this->post_type ) {
1207 - return;
1208 - }
656 + public function get_uncategorised_faq() {
657 + $available_terms = array_map(
658 + function ( $term ) {
659 + return $term->term_id;
660 + },
661 + get_terms(
662 + [
663 + 'taxonomy' => 'betterdocs_faq_category',
664 + 'hide_empty' => false
665 + ]
666 + )
667 + );
1209 668
1210 - if ( $taxonomy === $this->product_category && ! empty( $tt_ids ) ) {
1211 - update_post_meta( $object_id, self::SCOPE_META, 'product' );
1212 - return;
1213 - }
1214 -
1215 - // Assigning a General group makes it a General FAQ — but only claim it if it
1216 - // isn't already a Product FAQ that merely also carries a General term.
1217 - if ( $taxonomy === $this->category && ! empty( $tt_ids ) ) {
1218 - if ( get_post_meta( $object_id, self::SCOPE_META, true ) !== 'product' ) {
1219 - update_post_meta( $object_id, self::SCOPE_META, 'general' );
1220 - }
1221 - }
1222 - }
1223 -
1224 - /**
1225 - * One-time backfill: stamp the scope onto FAQs created before SCOPE_META existed.
1226 - * An FAQ holding a Product group is 'product'; everything else is 'general'.
1227 - * Ungrouped legacy FAQs have no way to be identified as product ones, so they stay
1228 - * in the General tab, which is exactly where they are today.
1229 - */
1230 - public function maybe_backfill_faq_scope() {
1231 - if ( get_option( self::SCOPE_BACKFILL_OPTION ) ) {
1232 - return;
1233 - }
1234 -
1235 - $product_faqs = get_posts(
669 + return get_posts(
1236 670 [
1237 - 'post_type' => $this->post_type,
1238 - 'post_status' => 'any',
671 + 'post_type' => 'betterdocs_faq',
672 + 'post_status' => [ 'publish', 'draft' ],
1239 673 'posts_per_page' => -1,
1240 - 'fields' => 'ids',
1241 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- one-time backfill.
1242 674 'tax_query' => [
675 + 'relation' => 'AND',
1243 676 [
1244 - 'taxonomy' => $this->product_category,
1245 - 'operator' => 'EXISTS',
1246 - ],
1247 - ],
677 + 'taxonomy' => 'betterdocs_faq_category',
678 + 'field' => 'term_id',
679 + 'terms' => $available_terms,
680 + 'operator' => 'NOT IN'
681 + ]
682 + ]
1248 683 ]
1249 684 );
1250 -
1251 - foreach ( $product_faqs as $faq_id ) {
1252 - update_post_meta( $faq_id, self::SCOPE_META, 'product' );
1253 - }
1254 -
1255 - update_option( self::SCOPE_BACKFILL_OPTION, 1 );
1256 685 }
1257 686
1258 - /**
1259 - * FAQs with no group, for the tab that asked. Each tab owns its own bucket:
1260 - * the Product tab lists ungrouped FAQs scoped to 'product', the General tab lists
1261 - * everything else that is ungrouped (so an ungrouped Product FAQ no longer leaks
1262 - * into General).
1263 - */
1264 - public function get_uncategorised_faq( $request = null ) {
1265 - $taxonomy = $request instanceof WP_REST_Request ? $this->resolve_taxonomy( $request ) : $this->category;
1266 - $is_product = ( $taxonomy === $this->product_category );
1267 -
1268 - $term_ids = function ( $tax ) {
1269 - $terms = get_terms( [ 'taxonomy' => $tax, 'hide_empty' => false ] );
1270 - return is_wp_error( $terms ) ? [] : array_map(
1271 - function ( $term ) {
1272 - return $term->term_id;
1273 - },
1274 - $terms
1275 - );
1276 - };
1277 -
1278 - // "Ungrouped" always means: no group in THIS tab's taxonomy.
1279 - $tax_query = [
1280 - 'relation' => 'AND',
1281 - [
1282 - 'taxonomy' => $taxonomy,
1283 - 'field' => 'term_id',
1284 - 'terms' => $term_ids( $taxonomy ),
1285 - 'operator' => 'NOT IN'
1286 - ]
1287 - ];
1288 -
1289 - $meta_query = [];
1290 -
1291 - if ( $is_product ) {
1292 - // Product tab: only FAQs that belong to the Product side.
1293 - $meta_query[] = [
1294 - 'key' => self::SCOPE_META,
1295 - 'value' => 'product',
1296 - ];
1297 - } else {
1298 - // General tab: exclude anything scoped to Product…
1299 - $meta_query[] = [
1300 - 'relation' => 'OR',
1301 - [
1302 - 'key' => self::SCOPE_META,
1303 - 'compare' => 'NOT EXISTS',
1304 - ],
1305 - [
1306 - 'key' => self::SCOPE_META,
1307 - 'value' => 'product',
1308 - 'compare' => '!=',
1309 - ],
1310 - ];
1311 -
1312 - // …and, as before, anything still holding a Product group (belt and braces
1313 - // for FAQs whose scope meta never got written).
1314 - $product_terms = $term_ids( $this->product_category );
1315 - if ( ! empty( $product_terms ) ) {
1316 - $tax_query[] = [
1317 - 'taxonomy' => $this->product_category,
1318 - 'field' => 'term_id',
1319 - 'terms' => $product_terms,
1320 - 'operator' => 'NOT IN'
1321 - ];
1322 - }
1323 - }
1324 -
1325 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query, WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- intentional NOT-IN scan to find FAQs without any group.
1326 - $posts = get_posts(
1327 - [
1328 - 'post_type' => 'betterdocs_faq',
1329 - 'post_status' => current_user_can( 'edit_others_posts' ) ? [ 'publish', 'draft' ] : 'publish',
1330 - 'posts_per_page' => -1,
1331 - 'tax_query' => $tax_query,
1332 - 'meta_query' => $meta_query,
1333 - ]
1334 - );
1335 -
1336 - // get_posts() returns raw WP_Post objects without a `meta` property. The
1337 - // FAQ Builder reads `faq.meta.faq_open_by_default` to seed the "Keep It
1338 - // Open By default" switcher, so attach it here to mirror the scalar shape
1339 - // the wp/v2 endpoint returns for categorized FAQs ('' when unset, '1'
1340 - // when enabled). Without this the value is undefined and the switcher
1341 - // defaults to ON for every uncategorized FAQ.
1342 - foreach ( $posts as $post ) {
1343 - $post->meta = [
1344 - 'faq_open_by_default' => get_post_meta( $post->ID, 'faq_open_by_default', true ),
1345 - ];
1346 - }
1347 -
1348 - return $posts;
1349 - }
1350 -
1351 687 public function category_search( $request ) {
1352 688
1353 - $title = $request['title'];
1354 - $taxonomy = $this->resolve_taxonomy( $request );
689 + $title = $request['title'];
1355 690
1356 691 // Perform the taxonomy search
1357 692 $taxonomy_args = [
1358 693 'name__like' => $title,
1359 - 'taxonomy' => $taxonomy,
694 + 'taxonomy' => 'betterdocs_faq_category',
1360 695 'hide_empty' => false
1361 696 ];
1362 697
1363 698 $taxonomies = get_terms( $taxonomy_args );
@@ -1382,56 +717,11 @@
1382 717 }
1383 718 }
1384 719
1385 720 public function faq_category_orderby_meta( $args, $request ) {
1386 - if ( ! in_array( $args['taxonomy'], [ $this->category, $this->product_category ], true ) ) {
1387 - return $args;
721 + if ( $args['taxonomy'] === 'betterdocs_faq_category' ) {
722 + $args['orderby'] = 'meta_value_num';
723 + $args['meta_key'] = 'order';
1388 724 }
1389 -
1390 - // Order (and therefore paginate) the whole group list server-side to
1391 - // match the FAQ Builder header dropdown, so page boundaries are cut on
1392 - // the same sort the rows are shown in — otherwise page 1 could end on
1393 - // "W" and page 2 start on "B". The admin passes the order key explicitly
1394 - // (independent of when the preference option finishes saving); fall back
1395 - // to the saved preference, then to the manual drag-drop order.
1396 - $order_key = $request->get_param( 'betterdocs_faq_order' );
1397 - if ( empty( $order_key ) ) {
1398 - $order_key = get_option( 'betterdocs_faq_order', 'default' );
1399 - }
1400 -
1401 - switch ( $order_key ) {
1402 - case 'most_recent':
1403 - $args['orderby'] = 'term_id';
1404 - $args['order'] = 'DESC';
1405 - unset( $args['meta_key'] );
1406 - break;
1407 - case 'least_recent':
1408 - $args['orderby'] = 'term_id';
1409 - $args['order'] = 'ASC';
1410 - unset( $args['meta_key'] );
1411 - break;
1412 - case 'a_to_z':
1413 - $args['orderby'] = 'name';
1414 - $args['order'] = 'ASC';
1415 - unset( $args['meta_key'] );
1416 - break;
1417 - case 'z_to_a':
1418 - $args['orderby'] = 'name';
1419 - $args['order'] = 'DESC';
1420 - unset( $args['meta_key'] );
1421 - break;
1422 - case 'most_questions':
1423 - $args['orderby'] = 'count';
1424 - $args['order'] = 'DESC';
1425 - unset( $args['meta_key'] );
1426 - break;
1427 - case 'default':
1428 - default:
1429 - $args['orderby'] = 'meta_value_num';
1430 - $args['meta_key'] = 'order';
1431 - $args['order'] = 'ASC';
1432 - break;
1433 - }
1434 -
1435 725 return $args;
1436 726 }
1437 727 }