PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.2
ShopBuilder – WooCommerce Builder For Elementor v3.4.2
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
← All changes | app/Controllers/BuilderController.php +187 -91 2.1.23.4.2 View file →
@@ -8,15 +8,14 @@
8 8 namespace RadiusTheme\SB\Controllers;
9 9
10 10 use RadiusTheme\SB\Helpers\Fns;
11 11 use RadiusTheme\SB\Helpers\BuilderFns;
12 +use RadiusTheme\SB\Models\ElementList;
12 13 use RadiusTheme\SB\Traits\SingletonTrait;
13 14 use RadiusTheme\SB\Controllers\Builder\BuilderCpt;
14 15 use RadiusTheme\SB\Controllers\Hooks\BuilderHooks;
15 16 use RadiusTheme\SB\Elementor\Controls\ImageSelectorControl;
16 17 use RadiusTheme\SB\Elementor\Controls\Select2AjaxControl;
17 -use RadiusTheme\SB\Models\ElementList;
18 -use RadiusTheme\SBPRO\Helpers\FnsPro;
19 18
20 19 // Do not allow directly accessing this file.
21 20 if ( ! defined( 'ABSPATH' ) ) {
22 21 exit( 'This script cannot be accessed directly.' );
@@ -55,28 +54,24 @@
55 54 */
56 55 private function __construct() {
57 56 $this->builder_page_id = BuilderFns::builder_page_id_by_page();
58 57 $this->page_edit_with = Fns::page_edit_with( $this->builder_page_id );
59 -
60 58 if ( ! is_admin() ) {
61 59 BuilderHooks::instance();
62 60 }
63 61
64 62 BuilderCpt::instance();
65 -
66 - $this->elementor_init();
67 - $this->both_builder_frontend();
68 -
69 - // add_action( 'elementor/init', [ $this, 'elementor_init' ] );
70 - add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'editor_scripts' ] );
71 - // add_action( 'template_redirect', [ $this, 'both_builder_frontend' ], 99 );
72 -
63 + if ( defined( 'ELEMENTOR_VERSION' ) ) {
64 + $this->elementor_init();
65 + $this->both_builder_frontend();
66 + add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'editor_scripts' ] );
67 + }
73 68 // RT Select2 Ajax.
74 - add_action( 'wp_ajax_rt_select2_object_search', [ $this, 'select2_ajax_posts_filter_autocomplete' ] );
75 - add_action( 'wp_ajax_nopriv_rt_select2_object_search', [ $this, 'select2_ajax_posts_filter_autocomplete' ] );
69 + add_action( 'wp_ajax_rtsb_select2_object_search', [ $this, 'select2_ajax_posts_filter_autocomplete' ] );
70 + add_action( 'wp_ajax_nopriv_rtsb_select2_object_search', [ $this, 'select2_ajax_posts_filter_autocomplete' ] );
76 71 // Select2 ajax save data.
77 - add_action( 'wp_ajax_rt_select2_get_title', [ $this, 'select2_ajax_get_posts_value_titles' ] );
78 - add_action( 'wp_ajax_nopriv_rt_select2_get_title', [ $this, 'select2_ajax_get_posts_value_titles' ] );
72 + add_action( 'wp_ajax_rtsb_select2_get_title', [ $this, 'select2_ajax_get_posts_value_titles' ] );
73 + add_action( 'wp_ajax_nopriv_rtsb_select2_get_title', [ $this, 'select2_ajax_get_posts_value_titles' ] );
79 74 }
80 75
81 76 /**
82 77 * Apply for frontend.
@@ -93,13 +88,20 @@
93 88 *
94 89 * @return void
95 90 */
96 91 public function elementor_init() {
97 - add_action( 'elementor/elements/categories_registered', [ $this, 'add_elementor_widget_categories' ] );
98 - add_action( 'elementor/widgets/register', [ $this, 'init_widgets' ] );
99 - add_action( 'elementor/controls/register', [ $this, 'init_controls' ] );
100 - add_filter( 'elementor/editor/localize_settings', [ $this, 'promotePremiumWidgets' ] );
101 - add_action( 'elementor/icons_manager/additional_tabs', [ $this, 'rtsb_icons' ] );
92 + add_action(
93 + 'init',
94 + function () {
95 + if ( Fns::should_load_elementor_scripts() ) {
96 + add_action( 'elementor/elements/categories_registered', [ $this, 'add_elementor_widget_categories' ] );
97 + add_action( 'elementor/widgets/register', [ $this, 'init_widgets' ] );
98 + add_action( 'elementor/controls/register', [ $this, 'init_controls' ] );
99 + add_filter( 'elementor/editor/localize_settings', [ $this, 'promotePremiumWidgets' ] );
100 + add_action( 'elementor/icons_manager/additional_tabs', [ $this, 'rtsb_icons' ] );
101 + }
102 + }
103 + );
102 104 }
103 105
104 106 /**
105 107 * Register Category
@@ -108,21 +110,29 @@
108 110 *
109 111 * @return void
110 112 */
111 113 public function add_elementor_widget_categories( $elements_manager ) {
112 - $type = BuilderFns::builder_type( get_the_ID() );
113 - $builder_type = ! empty( BuilderFns::builder_page_types()[ $type ] ) ? BuilderFns::builder_page_types()[ $type ] : '';
114 - $builder_type = str_replace( 'My Account - ', '', $builder_type );
115 - $categories['rtsb-shopbuilder'] = [
116 - 'title' => esc_html__( 'ShopBuilder', 'shopbuilder' ) . ' - ' . ucfirst( $builder_type ),
117 - 'icon' => 'fa fa-plug',
118 - ];
119 - $categories['rtsb-shopbuilder-general'] = [
120 - 'title' => esc_html__( 'Shopbuilder - General', 'shopbuilder' ),
121 - 'icon' => 'fa fa-plug',
122 - ];
114 + $categories = [];
115 + $builder_id = get_queried_object_id();
116 + $id = Fns::wpml_object_id( $builder_id, BuilderFns::$post_type_tb, 'default' );
117 + $type = BuilderFns::builder_type( $id );
123 118
124 - $categories = apply_filters( 'rtsb_elementor_widgets_category_lists', $categories );
119 + if ( $type && 'popup-builder' !== $type ) {
120 + $builder_type = ! empty( BuilderFns::builder_page_types()[ $type ] ) ? BuilderFns::builder_page_types()[ $type ] : '';
121 + $builder_type = str_replace( [ 'My Account - ', 'Endpoint: ' ], '', $builder_type );
122 + $categories['rtsb-shopbuilder'] = [
123 + 'title' => esc_html__( 'ShopBuilder', 'shopbuilder' ) . ' - ' . ucfirst( $builder_type ),
124 + 'icon' => 'fa fa-plug',
125 + ];
126 + }
127 + if ( 'quick-view' !== $type ) {
128 + $categories['rtsb-shopbuilder-general'] = [
129 + 'title' => esc_html__( 'Shopbuilder - General', 'shopbuilder' ),
130 + 'icon' => 'fa fa-plug',
131 + ];
132 + }
133 + $categories = apply_filters( 'rtsb_elementor_widgets_category_lists', $categories );
134 +
125 135 $other_categories = $elements_manager->get_categories();
126 136 $categories = array_merge(
127 137 array_slice( $other_categories, 0, 1 ),
128 138 $categories,
@@ -128,10 +138,10 @@
128 138 $categories,
129 139 array_slice( $other_categories, 1 )
130 140 );
131 141
132 - $set_categories = function ( $categories ) {
133 - $this->categories = $categories;
142 + $set_categories = function ( $categories ) use ( $elements_manager ) {
143 + $elements_manager->categories = $categories;
134 144 };
135 145
136 146 $set_categories->call( $elements_manager, $categories );
137 147 }
@@ -152,10 +162,14 @@
152 162
153 163 /**
154 164 * Init Widgets
155 165 *
156 - * Include widgets files and register them
166 + * Include widget files and register them
157 167 *
168 + * @param object $widgets_manager Widgets Manager.
169 + *
170 + * @return void
171 + *
158 172 * @since 1.0.0
159 173 */
160 174 public function init_widgets( $widgets_manager ) {
161 175
@@ -173,45 +187,24 @@
173 187 continue;
174 188 }
175 189
176 190 if ( ! empty( $element['category'] ) ) {
177 - $widget = false;
178 - $import = apply_filters( 'rtsb_import_status', false );
179 - switch ( $element['category'] ) {
180 - case 'product':
181 - if ( BuilderFns::is_product() || $import ) {
182 - $widget = new $element['base_class']();
183 - }
184 - break;
185 - case 'shop':
186 - if ( BuilderFns::is_archive() || BuilderFns::is_shop() || $import ) {
187 - $widget = new $element['base_class']();
188 - }
189 - break;
190 - case 'archive':
191 - if ( BuilderFns::is_archive() || $import ) {
192 - $widget = new $element['base_class']();
193 - }
194 - break;
195 - case 'cart':
196 - if ( BuilderFns::is_cart() || $import ) {
197 - $widget = new $element['base_class']();
198 - }
199 - break;
200 - case 'checkout':
201 - if ( BuilderFns::is_checkout() || $import ) {
202 - $widget = new $element['base_class']();
203 - }
204 - break;
205 - case 'general':
191 + try {
192 + $widget = false;
193 + $import = apply_filters( 'rtsb_import_status', false );
194 +
195 + if ( $this->should_register_widget( $element['category'], $import ) ) {
206 196 $widget = new $element['base_class']();
207 - break;
208 - default:
209 - }
210 - $widget = apply_filters( 'rtsb/element/list/init/widgets', $widget, $element );
197 + }
211 198
212 - if ( $widget && is_object( $widget ) ) {
213 - $widgets_manager->register( $widget );
199 + $widget = apply_filters( 'rtsb/element/list/init/widgets', $widget, $element );
200 +
201 + if ( $widget && is_object( $widget ) ) {
202 + $widgets_manager->register( $widget );
203 + }
204 + } catch ( \Throwable $e ) {
205 + error_log( 'ShopBuilder widget error (' . $element['base_class'] . '): ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
206 + continue;
214 207 }
215 208 }
216 209 }
217 210 }
@@ -216,8 +209,48 @@
216 209 }
217 210 }
218 211
219 212 /**
213 + * Determine whether a widget for the given category should be registered
214 + * in the current request context.
215 + *
216 + * @param string $category Element category.
217 + * @param bool $import Whether an import is in progress.
218 + *
219 + * @return bool
220 + */
221 + protected function should_register_widget( $category, $import ) {
222 + // On the frontend the Elementor Widgets Manager can initialise at an
223 + // unpredictable time — e.g. Elementor Pro's Theme Builder boots it on
224 + // template_redirect — when WooCommerce query conditionals such as
225 + // is_shop()/is_archive() are not yet reliable. Widget registration is
226 + // cached for the whole request, so gating it by those conditionals can
227 + // permanently skip ShopBuilder widgets, leaving the builder template to
228 + // render empty container shells. Register unconditionally on the
229 + // frontend; keep the per-category gate only for the editor widget panel.
230 + if ( ! is_admin() ) {
231 + return true;
232 + }
233 +
234 + switch ( $category ) {
235 + case 'product':
236 + return BuilderFns::is_product() || $import;
237 + case 'shop':
238 + return BuilderFns::is_archive() || BuilderFns::is_shop() || $import;
239 + case 'archive':
240 + return BuilderFns::is_archive() || $import;
241 + case 'cart':
242 + return BuilderFns::is_cart() || $import;
243 + case 'checkout':
244 + return BuilderFns::is_checkout() || $import;
245 + case 'general':
246 + return true;
247 + default:
248 + return false;
249 + }
250 + }
251 +
252 + /**
220 253 * Adding custom icons in Elementor
221 254 *
222 255 * @param array $tabs Tabs.
223 256 *
@@ -223,9 +256,9 @@
223 256 *
224 257 * @return array
225 258 */
226 259 public function rtsb_icons( $tabs = [] ) {
227 - $custom_icons = $this->get_icons();
260 + $custom_icons = Fns::get_custom_icon_names();
228 261
229 262 $tabs['shopbuilder-icons'] = [
230 263 'name' => 'rtsb-fonts',
231 264 'label' => esc_html__( 'ShopBuilder Icons', 'shopbuilder' ),
@@ -282,16 +315,34 @@
282 315 *
283 316 * @since 2.0.0
284 317 */
285 318 if ( did_action( 'elementor/loaded' ) && 'elementor' === $this->page_edit_with ) {
286 - do_action( 'elementor/page_templates/header-footer/before_content' );
319 + do_action( 'elementor/page_templates/header-footer/before_content' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
287 320 }
288 321
289 322 $parent_class = apply_filters( 'rtsb/builder/content/parent_class', [] );
290 323
291 324 if ( BuilderFns::is_product() ) {
292 - $product_id = Fns::get_prepared_product_id();
325 + $product_id = Fns::get_prepared_product_id();
326 +
327 + // Ensure the WordPress global $product is a valid WC_Product before
328 + // wc_get_product_class() fires the `woocommerce_post_class` filter.
329 + // Some themes (e.g. Blocksy) read the global $product directly inside
330 + // that filter and call methods like get_type() on it, which throws a
331 + // fatal error when the global is still an unresolved string.
332 + $_product = Fns::get_product();
333 +
293 334 $parent_class = wc_get_product_class( $parent_class, $product_id );
335 +
336 + if ( function_exists( 'rtwpvs' ) && $_product instanceof \WC_Product ) {
337 + if ( $_product->is_type( 'variable' ) ) {
338 + $parent_class[] = 'rtwpvs-product';
339 + $beside_label = function_exists( 'rtwpvsp' ) && rtwpvs()->get_option( 'attribute_on_click_behavior' );
340 + if ( $beside_label ) {
341 + $parent_class[] = 'rtwpvs-selected-term-beside-label';
342 + }
343 + }
344 + }
294 345 }
295 346
296 347 if ( BuilderFns::is_shop() ) {
297 348 $parent_class[] = 'shop';
@@ -312,10 +363,34 @@
312 363 if ( BuilderFns::is_checkout() ) {
313 364 if ( ! apply_filters( 'rtsb/multi/step/checkout/widget', true ) ) {
314 365 return;
315 366 }
367 +
368 + $checkout = WC()->checkout();
369 +
370 + // Temporarily remove callbacks that Elementor checkout
371 + // handles via separate widgets to prevent duplicates.
372 + remove_action( 'woocommerce_before_checkout_form', [ Hooks\ActionHooks::class, 'woocommerce_checkout_login_form' ], 10 );
373 + remove_action( 'woocommerce_before_checkout_form', [ Hooks\ActionHooks::class, 'woocommerce_checkout_coupon_form' ], 10 );
374 + remove_action( 'woocommerce_before_checkout_form', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
375 +
376 + do_action( 'woocommerce_before_checkout_form', $checkout );
377 +
378 + // Restore callbacks for non-Elementor contexts.
379 + add_action( 'woocommerce_before_checkout_form', [ Hooks\ActionHooks::class, 'woocommerce_checkout_login_form' ], 10 );
380 + add_action( 'woocommerce_before_checkout_form', [ Hooks\ActionHooks::class, 'woocommerce_checkout_coupon_form' ], 10 );
381 + add_action( 'woocommerce_before_checkout_form', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
382 +
383 + // If checkout registration is disabled and not logged in, the user cannot checkout.
384 + if ( ! $checkout->is_registration_enabled() && $checkout->is_registration_required() && ! is_user_logged_in() ) {
385 + echo esc_html( apply_filters( 'woocommerce_checkout_must_be_logged_in_message', __( 'You must be logged in to checkout.', 'shopbuilder' ) ) );
386 + return;
387 + }
316 388 ?>
317 - <form name="checkout" method="post" class="rtsb-woocommerce-checkout checkout" action="<?php echo esc_url( wc_get_checkout_url() ); ?>" enctype="multipart/form-data">
389 + <!--
390 + woocommerce-checkout added for fix issue with payment method ( Revolut Pay )
391 + -->
392 + <form name="checkout" method="post" class="rtsb-woocommerce-checkout woocommerce-checkout checkout" action="<?php echo esc_url( wc_get_checkout_url() ); ?>" enctype="multipart/form-data">
318 393 <?php
319 394 }
320 395 }
321 396
@@ -332,9 +407,9 @@
332 407 *
333 408 * @since 2.0.0
334 409 */
335 410 if ( did_action( 'elementor/loaded' ) && 'elementor' === $this->page_edit_with ) {
336 - do_action( 'elementor/page_templates/header-footer/after_content' );
411 + do_action( 'elementor/page_templates/header-footer/after_content' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
337 412 }
338 413
339 414 if ( BuilderFns::is_checkout() ) {
340 415 if ( ! apply_filters( 'rtsb/multi/step/checkout/widget', true ) ) {
@@ -342,8 +417,9 @@
342 417 }
343 418 ?>
344 419 </form>
345 420 <?php
421 + do_action( 'woocommerce_after_checkout_form', WC()->checkout() );
346 422 }
347 423 ?>
348 424 </div>
349 425 <!-- After Content end -->
@@ -355,24 +431,28 @@
355 431 *
356 432 * @return void
357 433 */
358 434 public function select2_ajax_posts_filter_autocomplete() {
359 -
435 + if ( ! wp_verify_nonce( Fns::get_nonce(), rtsb()->nonceText ) ) {
436 + wp_send_json_error( [] );
437 + }
360 438 $query_per_page = 15;
361 439 $post_type = 'post';
362 440 $source_name = 'post_type';
363 - $paged = $_POST['page'] ?? 1;
364 -
441 + $paged = absint( $_POST['page'] ?? 1 );
365 442 if ( ! empty( $_POST['post_type'] ) ) {
443 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
366 444 $post_type = sanitize_text_field( $_POST['post_type'] );
367 445 }
368 446
369 447 if ( ! empty( $_POST['source_name'] ) ) {
448 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
370 449 $source_name = sanitize_text_field( $_POST['source_name'] );
371 450 }
372 451
452 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
373 453 $search = ! empty( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '';
374 - $results = $post_list = [];
454 + $results = $post_list = []; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
375 455 switch ( $source_name ) {
376 456 case 'taxonomy':
377 457 $args = [
378 458 'hide_empty' => false,
@@ -377,13 +457,13 @@
377 457 $args = [
378 458 'hide_empty' => false,
379 459 'orderby' => 'name',
380 460 'order' => 'ASC',
381 - 'search' => $search,
461 + 'name__like' => $search,
382 462 'number' => '5',
383 463 ];
384 464
385 - if ( $post_type !== 'all' ) {
465 + if ( 'all' !== $post_type ) {
386 466 $args['taxonomy'] = $post_type;
387 467 }
388 468
389 469 $post_list = wp_list_pluck( get_terms( $args ), 'name', 'term_id' );
@@ -430,17 +510,22 @@
430 510 * @return void
431 511 */
432 512 public function select2_ajax_get_posts_value_titles() {
433 513
514 + if ( ! wp_verify_nonce( Fns::get_nonce(), rtsb()->nonceText ) ) {
515 + wp_send_json_error( [] );
516 + }
517 +
434 518 if ( empty( $_POST['id'] ) ) {
435 519 wp_send_json_error( [] );
436 520 }
437 521
522 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
438 523 if ( empty( array_filter( $_POST['id'] ) ) ) {
439 524 wp_send_json_error( [] );
440 525 }
441 526 $ids = array_map( 'intval', $_POST['id'] );
442 - $source_name = ! empty( $_POST['source_name'] ) ? sanitize_text_field( $_POST['source_name'] ) : '';
527 + $source_name = ! empty( $_POST['source_name'] ) ? sanitize_text_field( $_POST['source_name'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
443 528
444 529 switch ( $source_name ) {
445 530 case 'taxonomy':
446 531 $args = [
@@ -449,9 +534,11 @@
449 534 'order' => 'ASC',
450 535 'include' => implode( ',', $ids ),
451 536 ];
452 537
453 - if ( $_POST['post_type'] !== 'all' ) {
538 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
539 + if ( 'all' !== $_POST['post_type'] ) {
540 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
454 541 $args['taxonomy'] = sanitize_text_field( $_POST['post_type'] );
455 542 }
456 543
457 544 $response = wp_list_pluck( get_terms( $args ), 'name', 'term_id' );
@@ -469,9 +556,9 @@
469 556 break;
470 557 default:
471 558 $post_info = get_posts(
472 559 [
473 - 'post_type' => sanitize_text_field( $_POST['post_type'] ),
560 + 'post_type' => sanitize_text_field( $_POST['post_type'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
474 561 'include' => implode( ',', $ids ),
475 562 ]
476 563 );
477 564 $response = wp_list_pluck( $post_info, 'post_title', 'ID' );
@@ -487,12 +574,12 @@
487 574
488 575 /**
489 576 * Ajax callback for rt-select2
490 577 *
491 - * @param $post_type
492 - * @param $limit
493 - * @param $search
494 - * @param $paged
578 + * @param string $post_type Post type.
579 + * @param int $limit Limit.
580 + * @param string $search Search.
581 + * @param int $paged Paged.
495 582 *
496 583 * @return array
497 584 */
498 585 public function get_query_data( $post_type = 'any', $limit = 10, $search = '', $paged = 1 ) {
@@ -529,10 +616,19 @@
529 616 if ( ! empty( $search ) ) {
530 617 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_title LIKE %s", '%' . esc_sql( $search ) . '%' );
531 618 }
532 619
620 + // Add a query for shop_coupon post-type to check 'show on frontend' meta.
621 + if ( 'shop_coupon' === $post_type ) {
622 + $where .= " AND {$wpdb->posts}.ID IN (
623 + SELECT post_id FROM {$wpdb->postmeta}
624 + WHERE meta_key = 'rtsb_show_on_frontend'
625 + AND meta_value = 'on'
626 + )";
627 + }
628 +
533 629 $query = "select post_title,ID from $wpdb->posts where post_status = 'publish' {$where} {$limit}";
534 - $results = $wpdb->get_results( $query );
630 + $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
535 631
536 632 if ( ! empty( $results ) ) {
537 633 foreach ( $results as $row ) {
538 634 $data[ $row->ID ] = $row->post_title . ' [#' . $row->ID . ']';
@@ -563,10 +659,10 @@
563 659
564 660 $pro_widgets = [
565 661 [
566 662 'name' => 'rtsb-ajax-product-filters',
567 - 'title' => esc_html__( 'Ajax Product Filters', 'testimonial-slider-showcase' ),
568 - 'description' => esc_html__( 'Ajax Product Filters', 'testimonial-slider-showcase' ),
663 + 'title' => esc_html__( 'Ajax Product Filters', 'shopbuilder' ),
664 + 'description' => esc_html__( 'Ajax Product Filters', 'shopbuilder' ),
569 665 'icon' => 'rtsb-el-custom rtsb-element icon-rtsb-ajax-product-filters rtsb-promotional-element',
570 666 'categories' => '[ "rtsb-shopbuilder" ]',
571 667 ],
572 668 ];