@@ -43,6 +43,8 @@ class Product_Section_Selector {
4343 * Constructor.
4444 */
4545 public function __construct () {
46+ Hook_Registry::add_filter ( 'wp_terms_checklist_args ' , array ( $ this , 'use_hierarchical_section_walker ' ), 10 , 2 );
47+
4648 if ( 0 === (int ) wzkb_get_option ( 'multi_product ' , 0 ) ) {
4749 return ;
4850 }
@@ -361,6 +363,7 @@ public function sync_sections_after_rest_save( \WP_Post $post, \WP_REST_Request
361363 $ this ->assign_section_terms ( $ post ->ID , $ request ['meta ' ]['_wzkb_section_ids ' ] );
362364 }
363365
366+ $ this ->sync_products_from_sections ( $ post ->ID );
364367 $ this ->sync_product_meta ( $ post ->ID );
365368 $ this ->sync_section_product_meta ( $ post ->ID );
366369 }
@@ -382,18 +385,33 @@ public function maybe_sync_sections_during_save( $post_id, $post ) {
382385
383386 $ this ->is_syncing = true ;
384387
385- $ section_ids = get_post_meta ( $ post_id , '_wzkb_section_ids ' , true );
388+ // Quick Edit saves taxonomy terms directly without updating post meta.
389+ // Read back what WordPress just saved so meta stays in sync, then let
390+ // sync_products_from_sections derive product assignments from sections.
391+ // phpcs:ignore WordPress.Security.NonceVerification.Missing -- action detection only, no data used.
392+ $ is_quick_edit = isset ( $ _POST ['action ' ] ) && 'inline-save ' === $ _POST ['action ' ]; // phpcs:ignore WordPress.Security.NonceVerification.Missing
386393
387- if ( empty ( $ section_ids ) ) {
394+ if ( $ is_quick_edit ) {
388395 $ current_terms = get_the_terms ( $ post_id , 'wzkb_category ' );
389- if ( ! empty ( $ current_terms ) && ! is_wp_error ( $ current_terms ) ) {
390- $ section_ids = array_map ( 'absint ' , wp_list_pluck ( $ current_terms , 'term_id ' ) );
391- update_post_meta ( $ post_id , '_wzkb_section_ids ' , $ section_ids );
396+ $ section_ids = ( ! empty ( $ current_terms ) && ! is_wp_error ( $ current_terms ) )
397+ ? array_map ( 'absint ' , wp_list_pluck ( $ current_terms , 'term_id ' ) )
398+ : array ();
399+ update_post_meta ( $ post_id , '_wzkb_section_ids ' , $ section_ids );
400+ } else {
401+ $ section_ids = get_post_meta ( $ post_id , '_wzkb_section_ids ' , true );
402+
403+ if ( empty ( $ section_ids ) ) {
404+ $ current_terms = get_the_terms ( $ post_id , 'wzkb_category ' );
405+ if ( ! empty ( $ current_terms ) && ! is_wp_error ( $ current_terms ) ) {
406+ $ section_ids = array_map ( 'absint ' , wp_list_pluck ( $ current_terms , 'term_id ' ) );
407+ update_post_meta ( $ post_id , '_wzkb_section_ids ' , $ section_ids );
408+ }
392409 }
393410 }
394411
395412 try {
396413 $ this ->assign_section_terms ( $ post_id , $ section_ids );
414+ $ this ->sync_products_from_sections ( $ post_id );
397415 $ this ->sync_product_meta ( $ post_id );
398416 $ this ->sync_section_product_meta ( $ post_id );
399417 } finally {
@@ -473,6 +491,47 @@ private function sync_product_meta( $post_id ) {
473491 update_post_meta ( $ post_id , '_wzkb_product_ids ' , array_values ( $ product_ids ) );
474492 }
475493
494+ /**
495+ * Merge product assignments derived from assigned sections.
496+ *
497+ * Each section can carry a `product_id` term meta. Any product referenced
498+ * this way is added to the post's wzkb_product taxonomy so the article is
499+ * always discoverable from its product. Existing product assignments are
500+ * preserved — this only ever adds, never removes.
501+ *
502+ * @param int $post_id Post ID.
503+ */
504+ private function sync_products_from_sections ( int $ post_id ): void {
505+ $ section_terms = get_the_terms ( $ post_id , 'wzkb_category ' );
506+ if ( empty ( $ section_terms ) || is_wp_error ( $ section_terms ) ) {
507+ return ;
508+ }
509+
510+ $ derived_ids = array ();
511+ foreach ( $ section_terms as $ section ) {
512+ $ product_id = (int ) get_term_meta ( $ section ->term_id , 'product_id ' , true );
513+ if ( $ product_id > 0 ) {
514+ $ derived_ids [] = $ product_id ;
515+ }
516+ }
517+
518+ if ( empty ( $ derived_ids ) ) {
519+ return ;
520+ }
521+
522+ $ current_terms = get_the_terms ( $ post_id , 'wzkb_product ' );
523+ $ current_ids = ( ! empty ( $ current_terms ) && ! is_wp_error ( $ current_terms ) )
524+ ? array_map ( 'absint ' , wp_list_pluck ( $ current_terms , 'term_id ' ) )
525+ : array ();
526+
527+ $ to_add = array_diff ( $ derived_ids , $ current_ids );
528+ if ( empty ( $ to_add ) ) {
529+ return ;
530+ }
531+
532+ wp_set_post_terms ( $ post_id , array_values ( array_unique ( array_merge ( $ current_ids , $ derived_ids ) ) ), 'wzkb_product ' , false );
533+ }
534+
476535 /**
477536 * Backfill product_id term meta on sections that have none.
478537 *
@@ -500,8 +559,7 @@ private function sync_section_product_meta( int $post_id ): void {
500559 $ product_id = (int ) $ product_terms [0 ]->term_id ;
501560
502561 foreach ( $ section_terms as $ section ) {
503- $ existing_product_id = get_term_meta ( $ section ->term_id , 'product_id ' , true );
504- if ( '' === $ existing_product_id ) {
562+ if ( 0 === (int ) get_term_meta ( $ section ->term_id , 'product_id ' , true ) ) {
505563 update_term_meta ( $ section ->term_id , 'product_id ' , $ product_id );
506564 }
507565 }
@@ -622,4 +680,21 @@ private function get_ui_strings(): array {
622680 'productOverflow ' => esc_html__ ( 'Showing first %1$s products out of %2$s. Refine your search. ' , 'knowledgebase ' ),
623681 );
624682 }
683+
684+ /**
685+ * Inject Walker_Section_Checklist for the wzkb_category checklist (Quick Edit and post editor).
686+ *
687+ * @param array $args wp_terms_checklist() arguments.
688+ * @param int $post_id Post ID (unused).
689+ * @return array
690+ */
691+ public function use_hierarchical_section_walker ( array $ args , $ post_id ): array {
692+ if ( ! isset ( $ args ['taxonomy ' ] ) || 'wzkb_category ' !== $ args ['taxonomy ' ] ) {
693+ return $ args ;
694+ }
695+
696+ $ args ['walker ' ] = new Walker_Section_Checklist ();
697+
698+ return $ args ;
699+ }
625700}
0 commit comments