Skip to content

Commit ca6c864

Browse files
author
Soare Robert-Daniel
committed
feat: allow product search on Attach to Product select
1 parent ebfb9b6 commit ca6c864

13 files changed

Lines changed: 311 additions & 93 deletions

File tree

classes/admin.class.php

Lines changed: 149 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public function __construct() {
6464

6565
// Getting products list
6666
add_action( 'wp_ajax_ppom_get_products', array( $this, 'get_products' ) );
67+
add_action( 'wp_ajax_ppom_search_products', array( $this, 'search_products' ) );
6768
add_action( 'wp_ajax_ppom_attach_ppoms', array( $this, 'ppom_attach_ppoms' ) );
6869

6970
// Adding setting tab in WooCommerce
@@ -329,10 +330,11 @@ public function get_products() {
329330
->set_select(
330331
array_merge(
331332
array(
332-
'label' => __( 'Choose Products:', 'woocommerce-product-addon' ),
333-
'name' => 'ppom-attach-to-products[]',
334-
'multiple' => true,
335-
'is_used' => true,
333+
'label' => __( 'Choose Products:', 'woocommerce-product-addon' ),
334+
'name' => 'ppom-attach-to-products[]',
335+
'multiple' => true,
336+
'is_used' => true,
337+
'render_empty_option' => false,
336338
),
337339
$this->get_wc_products( $ppom_id )
338340
)
@@ -418,67 +420,162 @@ function ( $html_content ) use ( $popup_components ) {
418420
}
419421

420422
/**
421-
* Returns product selector options for the attach popup.
422-
*
423-
* Marks which products already reference the current PPOM ID through
424-
* {@see PPOM_PRODUCT_META_KEY}.
425-
*
426-
* @param int $ppom_id PPOM field-group ID being attached.
423+
* Returns paginated product matches for the attach modal Select2 control.
427424
*
428-
* @return array
425+
* @return void
429426
*/
430-
public function get_wc_products( $ppom_id ) {
431-
$result = array(
432-
'options' => array(),
433-
'is_used' => true,
427+
public function search_products() {
428+
$nonce = isset( $_GET['ppom_attached_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['ppom_attached_nonce'] ) ) : '';
429+
430+
if ( ! wp_verify_nonce( $nonce, 'ppom_attached_nonce_action' ) || ! ppom_security_role() ) {
431+
wp_send_json_error(
432+
array(
433+
'message' => __( 'Sorry, you are not allowed to perform this action please try again', 'woocommerce-product-addon' ),
434+
),
435+
403
436+
);
437+
}
438+
439+
$search_term = '';
440+
if ( isset( $_GET['q'] ) ) {
441+
$search_term = sanitize_text_field( wp_unslash( $_GET['q'] ) );
442+
} elseif ( isset( $_GET['term'] ) ) {
443+
$search_term = sanitize_text_field( wp_unslash( $_GET['term'] ) );
444+
}
445+
446+
$page = isset( $_GET['page'] ) ? max( 1, absint( $_GET['page'] ) ) : 1;
447+
$per_page = 20;
448+
$query_args = array(
449+
'post_type' => 'product',
450+
'post_status' => 'publish',
451+
'posts_per_page' => $per_page,
452+
'paged' => $page,
453+
'orderby' => 'title',
454+
'order' => 'ASC',
455+
'fields' => 'ids',
456+
'no_found_rows' => false,
457+
'update_post_meta_cache' => false,
458+
'update_post_term_cache' => false,
434459
);
435460

436-
if ( 'valid' === apply_filters( 'product_ppom_license_status', '' ) ) {
437-
$result['options'][] = array(
438-
'value' => '-1',
439-
'selected' => false,
440-
'label' => __( 'Select a product', 'woocommerce-product-addon' ),
441-
'disabled' => true,
442-
);
461+
if ( '' !== $search_term ) {
462+
$query_args['s'] = $search_term;
443463
}
444464

445-
$query = new WP_Query(
465+
$query = new WP_Query( $query_args );
466+
$results = array_map(
467+
function ( $product_id ) {
468+
$product_id = $product_id instanceof WP_Post ? (int) $product_id->ID : absint( $product_id );
469+
470+
return array(
471+
'id' => (string) $product_id,
472+
'text' => get_the_title( $product_id ),
473+
);
474+
},
475+
$query->posts
476+
);
477+
478+
wp_send_json(
446479
array(
447-
'post_type' => 'product',
448-
'posts_per_page' => -1, // Get all products
449-
'post_status' => 'publish',
480+
'results' => $results,
481+
'pagination' => array(
482+
'more' => $page < (int) $query->max_num_pages,
483+
),
450484
)
451485
);
486+
}
452487

453-
if ( ! $query->have_posts() ) {
454-
return $result;
488+
/**
489+
* Returns product IDs currently attached to a PPOM field group.
490+
*
491+
* Supports both the current serialized-array storage and the older scalar
492+
* storage used by {@see PPOM_PRODUCT_META_KEY}.
493+
*
494+
* @param int $ppom_id PPOM field-group ID.
495+
* @return int[]
496+
*/
497+
public static function get_attached_product_ids( $ppom_id ) {
498+
$ppom_id = absint( $ppom_id );
499+
if ( 0 === $ppom_id ) {
500+
return array();
455501
}
456502

457-
while ( $query->have_posts() ) {
458-
$query->the_post();
503+
$ppom_id_string = (string) $ppom_id;
504+
$product_ids = get_posts(
505+
array(
506+
'post_type' => 'product',
507+
'post_status' => 'publish',
508+
// Intentionally load all matched attachments, not the full catalog.
509+
// The modal must pre-render every currently attached product as a
510+
// selected option so Select2 shows the existing state correctly.
511+
'posts_per_page' => -1,
512+
'fields' => 'ids',
513+
'orderby' => 'title',
514+
'order' => 'ASC',
515+
'no_found_rows' => true,
516+
'update_post_meta_cache' => false,
517+
'update_post_term_cache' => false,
518+
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Attachments are stored in post meta and this helper needs every selected product ID for modal preloading.
519+
'meta_query' => array(
520+
'relation' => 'OR',
521+
array(
522+
'key' => PPOM_PRODUCT_META_KEY,
523+
'value' => $ppom_id_string,
524+
'compare' => '=',
525+
),
526+
array(
527+
'key' => PPOM_PRODUCT_META_KEY,
528+
'value' => sprintf( 'i:%d;', $ppom_id ),
529+
'compare' => 'LIKE',
530+
),
531+
array(
532+
'key' => PPOM_PRODUCT_META_KEY,
533+
'value' => sprintf( 's:%d:"%s";', strlen( $ppom_id_string ), $ppom_id_string ),
534+
'compare' => 'LIKE',
535+
),
536+
),
537+
)
538+
);
459539

460-
$is_used = false;
461-
$product_id = get_the_ID();
462-
$attached_meta = get_post_meta( $product_id, PPOM_PRODUCT_META_KEY, true );
540+
$verified = array();
541+
foreach ( $product_ids as $candidate_id ) {
542+
$candidate_id = absint( $candidate_id );
543+
$attached_meta = get_post_meta( $candidate_id, PPOM_PRODUCT_META_KEY, true );
463544

464-
if ( is_array( $attached_meta ) ) {
465-
$is_used = in_array( $ppom_id, $attached_meta );
466-
} elseif ( is_numeric( $attached_meta ) ) {
467-
$is_used = $product_id === $attached_meta; // Note: Legacy format.
545+
if ( is_array( $attached_meta ) && in_array( $ppom_id, array_map( 'absint', $attached_meta ), true ) ) {
546+
$verified[] = $candidate_id;
547+
} elseif ( is_numeric( $attached_meta ) && absint( $attached_meta ) === $ppom_id ) {
548+
$verified[] = $candidate_id;
468549
}
550+
}
469551

470-
if ( $is_used ) {
471-
$result['is_used'] = true;
472-
}
552+
return array_values( array_unique( $verified ) );
553+
}
473554

555+
/**
556+
* Returns product selector options for the attach popup.
557+
*
558+
* Marks which products already reference the current PPOM ID through
559+
* {@see PPOM_PRODUCT_META_KEY}.
560+
*
561+
* @param int $ppom_id PPOM field-group ID being attached.
562+
*
563+
* @return array
564+
*/
565+
public function get_wc_products( $ppom_id ) {
566+
$result = array(
567+
'options' => array(),
568+
'is_used' => true,
569+
);
570+
571+
foreach ( self::get_attached_product_ids( $ppom_id ) as $product_id ) {
474572
$result['options'][] = array(
475-
'value' => $product_id,
476-
'selected' => $is_used,
477-
'label' => get_the_title(),
573+
'value' => (string) $product_id,
574+
'selected' => true,
575+
'label' => get_the_title( $product_id ),
478576
);
479577
}
480578

481-
wp_reset_postdata();
482579
return $result;
483580
}
484581

@@ -511,6 +608,9 @@ public function get_wc_categories( $current_values ) {
511608
$used_categories = array();
512609
if ( ! empty( $current_values['productmeta_categories'] ) ) {
513610
$used_categories = preg_split( '/\r\n|\n/', $current_values['productmeta_categories'] );
611+
if ( false === $used_categories ) {
612+
$used_categories = array();
613+
}
514614
}
515615

516616
foreach ( $product_categories as $category ) {
@@ -615,8 +715,9 @@ public function get_db_field( $ppom_field_id ) {
615715
* @see ppom_admin_process_product_meta()
616716
*/
617717
public function ppom_attach_ppoms() {
618-
if ( ! isset( $_POST['ppom_attached_nonce'] )
619-
|| ! wp_verify_nonce( $_POST['ppom_attached_nonce'], 'ppom_attached_nonce_action' )
718+
$attached_nonce = isset( $_POST['ppom_attached_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ppom_attached_nonce'] ) ) : '';
719+
if ( '' === $attached_nonce
720+
|| ! wp_verify_nonce( $attached_nonce, 'ppom_attached_nonce_action' )
620721
|| ! ppom_security_role()
621722
) {
622723
$response = array(
@@ -627,7 +728,7 @@ public function ppom_attach_ppoms() {
627728
wp_send_json( $response );
628729
}
629730

630-
$ppom_id = intval( $_POST['ppom_id'] );
731+
$ppom_id = isset( $_POST['ppom_id'] ) ? absint( $_POST['ppom_id'] ) : 0;
631732
$is_pro_user = 'valid' === apply_filters( 'product_ppom_license_status', '' );
632733

633734
// +----- Attach Field to Product -----+
@@ -788,7 +889,7 @@ public function save_settings() {
788889
*/
789890
public function ppom_setting_wpml( $value, $option, $raw_value ) {
790891

791-
if ( isset( $option['type'] ) && isset( $option['type'] ) == 'text' ) {
892+
if ( isset( $option['type'] ) && 'text' === $option['type'] ) {
792893
$value = ppom_wpml_translate( $value, 'PPOM' );
793894
}
794895

classes/attach-popup/select-component.class.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
<?php
2+
/**
3+
* Renders attach-popup Select2-based selectors for PPOM assignment flows.
4+
*
5+
* @package PPOM
6+
* @subpackage AttachPopup
7+
*/
28

39
namespace PPOM\Attach;
410

@@ -53,6 +59,7 @@ public function render() {
5359
$is_multiple = isset( $select['multiple'] ) && $select['multiple'];
5460
$input_label = isset( $select['label'] ) && is_string( $select['label'] ) ? $select['label'] : '';
5561
$is_used = isset( $select['is_used'] ) && $select['is_used'];
62+
$render_empty = ! isset( $select['render_empty_option'] ) || $select['render_empty_option'];
5663

5764
$initial_values = array();
5865
foreach ( $select_options as $option ) {
@@ -96,7 +103,7 @@ class="ppom-attach"
96103

97104
echo '<option ' . esc_attr( $status ) . ' value="' . esc_attr( $value ) . '" ' . selected( true, $option['selected'], false ) . ' ' . disabled( true, isset( $option['disabled'] ) && $option['disabled'], false ) . '>' . esc_html( $label ) . '</option>';
98105
}
99-
} else {
106+
} elseif ( $render_empty ) {
100107
echo '<option value="" disabled>' . esc_html__( 'No options available!', 'woocommerce-product-addon' ) . '</option>';
101108
}
102109
?>
@@ -107,7 +114,7 @@ class="ppom-attach"
107114
</span>
108115
<?php if ( 'valid' !== $this->get_status() ) { ?>
109116

110-
<?php echo '<a class="ppom-field-filter-pro-available" target="_blank" href="' . tsdk_utmify( tsdk_translate_link( PPOM_UPGRADE_URL ), 'tags-fields' ) . '">' . esc_html__( 'Available in PRO', 'woocommerce-product-addon' ) . '</a>'; ?>
117+
<?php echo '<a class="ppom-field-filter-pro-available" target="_blank" href="' . esc_url( tsdk_utmify( tsdk_translate_link( PPOM_UPGRADE_URL ), 'tags-fields' ) ) . '">' . esc_html__( 'Available in PRO', 'woocommerce-product-addon' ) . '</a>'; ?>
111118

112119
<?php } ?>
113120
<input

classes/fields.class.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ public function load_script( $hook ) {
199199
'plugin_admin_page' => admin_url( 'admin.php?page=ppom' ),
200200
'loader' => PPOM_URL . '/images/loading.gif',
201201
'ppomProActivated' => ppom_pro_is_installed() && PPOM()->is_license_of_type( 'pro' ) ? 'yes' : 'no',
202+
'attach' => array(
203+
'searchAction' => 'ppom_search_products',
204+
'productsPlaceholder' => __( 'Search products...', 'woocommerce-product-addon' ),
205+
),
202206
'i18n' => array(
203207
'addGroupUrl' => esc_url( add_query_arg( array( 'action' => 'new' ) ) ),
204208
'addGroupLabel' => esc_html__( 'Add New Group', 'woocommerce-product-addon' ),

css/ppom-admin.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1886,4 +1886,4 @@ div.ppom-banner .themeisle-sale {
18861886

18871887
.ppom-banner .themeisle-sale .themeisle-sale-action a {
18881888
text-decoration: none;
1889-
}
1889+
}

js/admin/ppom-meta-table.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,49 @@ jQuery( function ( $ ) {
1515
* @return {void}
1616
*/
1717
function initAttachSelects() {
18-
const attachSelects = $( '.ppom-attach-container-item select' );
18+
const productSelect = $( '#attach-to-products .ppom-attach' );
19+
const attachSelects = $( '.ppom-attach-container-item select' ).not(
20+
productSelect
21+
);
22+
const attachNonce = $( '#ppom-product-form [name="ppom_attached_nonce"]' )
23+
.val();
1924

2025
if ( typeof $.fn.select2 === 'function' ) {
2126
attachSelects.select2();
27+
28+
productSelect.select2( {
29+
width: '100%',
30+
closeOnSelect: false,
31+
minimumInputLength: 0,
32+
dropdownParent: $( '#ppom-product-modal' ),
33+
placeholder: window?.ppom_vars?.attach?.productsPlaceholder,
34+
ajax: {
35+
url: ajaxurl,
36+
dataType: 'json',
37+
delay: 300,
38+
cache: true,
39+
data: function ( params ) {
40+
return {
41+
action:
42+
window?.ppom_vars?.attach?.searchAction ||
43+
'ppom_search_products',
44+
ppom_attached_nonce: attachNonce,
45+
q: params.term || '',
46+
page: params.page || 1,
47+
};
48+
},
49+
processResults: function ( data ) {
50+
return {
51+
results: Array.isArray( data?.results )
52+
? data.results
53+
: [],
54+
pagination: {
55+
more: Boolean( data?.pagination?.more ),
56+
},
57+
};
58+
},
59+
},
60+
} );
2261
}
2362
}
2463

@@ -132,8 +171,13 @@ jQuery( function ( $ ) {
132171
initAttachSelects();
133172
$( '#ppom_id' ).val( ppom_id );
134173
$( 'body' ).append( append_overlay_modal );
135-
$( '#' + model_id ).fadeIn();
136-
$( '#attach-to-products input' ).focus();
174+
$( '#' + model_id ).fadeIn( function () {
175+
const productSelect = $( '#attach-to-products .ppom-attach' );
176+
177+
if ( productSelect.length ) {
178+
productSelect.select2( 'open' );
179+
}
180+
} );
137181
} );
138182
}
139183
);

0 commit comments

Comments
 (0)