Changeset 3419781
- Timestamp:
- 12/15/2025 08:28:46 AM (3 months ago)
- Location:
- just-tables
- Files:
-
- 12 edited
- 1 copied
-
tags/1.8.0 (copied) (copied from just-tables/trunk)
-
tags/1.8.0/assets/css/jtpt-frontend-base.css (modified) (2 diffs)
-
tags/1.8.0/assets/js/jtpt-frontend-ajax.js (modified) (15 diffs)
-
tags/1.8.0/assets/js/jtpt-frontend-base.js (modified) (15 diffs)
-
tags/1.8.0/includes/Admin/Posts_Columns.php (modified) (2 diffs)
-
tags/1.8.0/just-tables.php (modified) (2 diffs)
-
tags/1.8.0/readme.txt (modified) (2 diffs)
-
trunk/assets/css/jtpt-frontend-base.css (modified) (2 diffs)
-
trunk/assets/js/jtpt-frontend-ajax.js (modified) (15 diffs)
-
trunk/assets/js/jtpt-frontend-base.js (modified) (15 diffs)
-
trunk/includes/Admin/Posts_Columns.php (modified) (2 diffs)
-
trunk/just-tables.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
just-tables/tags/1.8.0/assets/css/jtpt-frontend-base.css
r3370225 r3419781 362 362 line-height: 40px; 363 363 color: #59585A; 364 transition: opacity 0.2s ease, visibility 0.2s ease; 365 } 366 .jtpt-search.has-value .jtpt-search-icon { 367 opacity: 0; 368 visibility: hidden; 364 369 } 365 370 .jtpt-search .jtpt-search-clear { … … 368 373 height: 40px; 369 374 top: 0; 370 right: 1 5px;371 font-size: 24px;372 line-height: 36px;375 right: 12px; 376 font-size: 18px; 377 line-height: 40px; 373 378 color: #999; 374 379 cursor: pointer; 375 380 text-align: center; 376 transition: color 0.2s ease; 381 opacity: 0; 382 visibility: hidden; 383 transition: color 0.2s ease, opacity 0.2s ease, visibility 0.2s ease; 384 } 385 .jtpt-search.has-value .jtpt-search-clear { 386 opacity: 1; 387 visibility: visible; 377 388 } 378 389 .jtpt-search .jtpt-search-clear:hover { -
just-tables/tags/1.8.0/assets/js/jtpt-frontend-ajax.js
r3061894 r3419781 7 7 'use strict'; 8 8 9 /** 10 * AJAX timeout in milliseconds (30 seconds). 11 * @since 1.8.0 12 */ 13 var JTPT_AJAX_TIMEOUT = 30000; 14 15 /** 16 * Safe JSON parse helper function. 17 * @since 1.8.0 18 */ 19 function jtptSafeJsonParse( jsonString, defaultValue ) { 20 if ( ! jsonString || '' === jsonString ) { 21 return defaultValue; 22 } 23 try { 24 return JSON.parse( jsonString ); 25 } catch ( e ) { 26 return defaultValue; 27 } 28 } 29 30 /** 31 * Show error message to user. 32 * @since 1.8.0 33 */ 34 function jtptShowErrorMessage( tableWrapper, message ) { 35 var noticeBoard = tableWrapper.find( '.jtpt-notices' ); 36 if ( noticeBoard.length === 0 ) { 37 noticeBoard = $( '.jtpt-notices' ); 38 } 39 40 var errorHtml = '<div class="woocommerce-error" role="alert" style="margin: 10px 0; padding: 10px 15px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;">' + 41 '<strong>Error:</strong> ' + message + 42 '</div>'; 43 44 noticeBoard.html( errorHtml ); 45 46 if ( typeof tb_show === 'function' ) { 47 try { 48 tb_remove(); 49 tb_show( '', '#TB_inline?&inlineId=jtpt-notices-popup' ); 50 } catch ( e ) { 51 // Thickbox not available, show inline 52 } 53 } 54 } 55 56 /** 57 * Get user-friendly error message. 58 * @since 1.8.0 59 */ 60 function jtptGetErrorMessage( jqXHR, textStatus ) { 61 if ( textStatus === 'timeout' ) { 62 return 'The request timed out. Please try again.'; 63 } else if ( textStatus === 'abort' ) { 64 return 'The request was cancelled.'; 65 } else if ( jqXHR.status === 0 ) { 66 return 'Unable to connect. Please check your internet connection.'; 67 } else if ( jqXHR.status === 404 ) { 68 return 'The requested resource was not found.'; 69 } else if ( jqXHR.status === 500 ) { 70 return 'A server error occurred. Please try again later.'; 71 } else { 72 return 'An error occurred. Please try again.'; 73 } 74 } 75 9 76 $( document ).ready( function () { 10 77 … … 33 100 url: jtpt_data.ajax_url, 34 101 data: data, 102 timeout: JTPT_AJAX_TIMEOUT, 35 103 beforeSend: function(){ 36 104 tableWrapper.addClass( 'jtpt-loading' ); … … 49 117 tableWrapper.removeClass( 'jtpt-loading' ); 50 118 }, 51 error: function() { 52 tableWrapper.removeClass( 'jtpt-loading' ); 119 error: function( jqXHR, textStatus ) { 120 tableWrapper.removeClass( 'jtpt-loading' ); 121 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 53 122 }, 54 123 } ); … … 87 156 88 157 if ( '' !== variation ) { 89 variation = JSON.parse( variation);158 variation = jtptSafeJsonParse( variation, '' ); 90 159 } 91 160 … … 112 181 url: jtpt_data.ajax_url, 113 182 data: data, 183 timeout: JTPT_AJAX_TIMEOUT, 114 184 beforeSend: function(){ 115 185 tableWrapper.addClass( 'jtpt-loading' ); … … 128 198 tableWrapper.removeClass( 'jtpt-loading' ); 129 199 }, 130 error: function() { 131 tableWrapper.removeClass( 'jtpt-loading' ); 200 error: function( jqXHR, textStatus ) { 201 tableWrapper.removeClass( 'jtpt-loading' ); 202 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 132 203 }, 133 204 } ); … … 167 238 url: jtpt_data.ajax_url, 168 239 data: data, 240 timeout: JTPT_AJAX_TIMEOUT, 169 241 beforeSend: function(){ 170 242 tableWrapper.addClass( 'jtpt-loading' ); … … 183 255 tableWrapper.removeClass( 'jtpt-loading' ); 184 256 }, 185 error: function() { 186 tableWrapper.removeClass( 'jtpt-loading' ); 257 error: function( jqXHR, textStatus ) { 258 tableWrapper.removeClass( 'jtpt-loading' ); 259 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 187 260 }, 188 261 } ); … … 224 297 url: jtpt_data.ajax_url, 225 298 data: data, 299 timeout: JTPT_AJAX_TIMEOUT, 226 300 beforeSend: function(){ 227 301 tableWrapper.addClass( 'jtpt-loading' ); … … 240 314 tableWrapper.removeClass( 'jtpt-loading' ); 241 315 }, 242 error: function() { 243 tableWrapper.removeClass( 'jtpt-loading' ); 316 error: function( jqXHR, textStatus ) { 317 tableWrapper.removeClass( 'jtpt-loading' ); 318 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 244 319 }, 245 320 } ); … … 277 352 url: jtpt_data.ajax_url, 278 353 data: data, 354 timeout: JTPT_AJAX_TIMEOUT, 279 355 beforeSend: function(){ 280 356 tableWrapper.addClass( 'jtpt-loading' ); … … 293 369 tableWrapper.removeClass( 'jtpt-loading' ); 294 370 }, 295 error: function() { 296 tableWrapper.removeClass( 'jtpt-loading' ); 371 error: function( jqXHR, textStatus ) { 372 tableWrapper.removeClass( 'jtpt-loading' ); 373 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 297 374 }, 298 375 } ); … … 330 407 331 408 if ( '' !== variation ) { 332 variation = JSON.parse( variation);409 variation = jtptSafeJsonParse( variation, {} ); 333 410 } 334 411 … … 349 426 url: jtpt_data.ajax_url.toString().replace( '%%endpoint%%', 'add_to_cart' ), 350 427 data: data, 428 timeout: JTPT_AJAX_TIMEOUT, 351 429 beforeSend: function(){ 352 430 tableWrapper.addClass( 'jtpt-loading' ); … … 371 449 tableWrapper.removeClass( 'jtpt-loading' ); 372 450 }, 373 error: function() { 374 tableWrapper.removeClass( 'jtpt-loading' ); 451 error: function( jqXHR, textStatus ) { 452 tableWrapper.removeClass( 'jtpt-loading' ); 453 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 375 454 }, 376 455 dataType: 'json' -
just-tables/tags/1.8.0/assets/js/jtpt-frontend-base.js
r3370225 r3419781 7 7 'use strict'; 8 8 9 /** 10 * Safe JSON parse helper function. 11 * Prevents crashes when parsing invalid JSON data. 12 * 13 * @since 1.8.0 14 * @param {string} jsonString - The JSON string to parse 15 * @param {*} defaultValue - Default value to return on parse failure 16 * @return {*} Parsed JSON or default value 17 */ 18 function jtptSafeJsonParse( jsonString, defaultValue ) { 19 if ( ! jsonString || '' === jsonString ) { 20 return defaultValue; 21 } 22 try { 23 return JSON.parse( jsonString ); 24 } catch ( e ) { 25 if ( typeof console !== 'undefined' && console.error ) { 26 console.error( 'JustTables: Failed to parse JSON data', e ); 27 } 28 return defaultValue; 29 } 30 } 31 32 /** 33 * Cleanup function to prevent memory leaks and event listener accumulation. 34 * Called before re-initializing tables (especially important for Elementor). 35 * 36 * @since 1.8.0 37 */ 38 function justTablesCleanup() { 39 // Remove namespaced event listeners from filter buttons 40 $( '.jtpt-ftrigger-button' ).off( 'click.jtpt' ); 41 $( '.jtpt-freset-button' ).off( 'click.jtpt' ); 42 43 // Remove namespaced window resize handlers 44 $( window ).off( 'resize.jtpt' ); 45 46 // Destroy existing DataTable instances to prevent duplicates 47 $( '.jtpt-product-table-wrapper' ).each( function() { 48 var table = $( this ).find( '.jtpt-product-table' ); 49 if ( $.fn.DataTable.isDataTable( table ) ) { 50 try { 51 table.DataTable().destroy(); 52 } catch ( e ) { 53 // Table may already be destroyed, ignore error 54 } 55 } 56 }); 57 } 58 9 59 function justTablesGlobal ( context = '' ) { 60 61 // Cleanup before re-initializing (prevents Elementor hanging issue) 62 if ( 'elementor-editor' === context || 'elementor-popup' === context || 'elementor-tab' === context ) { 63 justTablesCleanup(); 64 } 10 65 11 66 /** … … 14 69 * @since 1.0.0 15 70 */ 16 $( '.jtpt-ftrigger-button' ).o n( 'click', function ( e ) {71 $( '.jtpt-ftrigger-button' ).off( 'click.jtpt' ).on( 'click.jtpt', function ( e ) { 17 72 e.preventDefault(); 18 73 … … 25 80 * @since 1.0.0 26 81 */ 27 $( '.jtpt-freset-button' ).o n( 'click', function ( e ) {82 $( '.jtpt-freset-button' ).off( 'click.jtpt' ).on( 'click.jtpt', function ( e ) { 28 83 e.preventDefault(); 29 84 … … 184 239 var sku = tableWrapper.find( '.jtpt-sku-' + productId ), 185 240 simpleSKU = sku.attr( 'data-jtpt-simple-sku' ), 186 variationSKU = sku.attr( 'data-jtpt-simple-sku' );241 variationSKU = ''; 187 242 188 243 // Weight. … … 229 284 simpleQuantityInputMin = quantity.attr( 'data-jtpt-simple-min-qty' ), 230 285 simpleQuantityInputMax = quantity.attr( 'data-jtpt-simple-max-qty' ), 231 variationQuantityInputMin = quantity.attr( 'data-jtpt-simple-min-qty' ),232 variationQuantityInputMax = quantity.attr( 'data-jtpt-simple-max-qty' );286 variationQuantityInputMin = '', 287 variationQuantityInputMax = ''; 233 288 234 289 // Add to cart button. … … 244 299 variationNotAvailableText = ''; 245 300 246 // Parse json value .247 availableVariations = JSON.parse( availableVariations);248 variationsStockHtml = JSON.parse( variationsStockHtml);249 defaultAttributes = JSON.parse( defaultAttributes);250 elementConfiguration = JSON.parse( elementConfiguration);251 woocommerceSettings = JSON.parse( woocommerceSettings);301 // Parse json value with safe fallbacks. 302 availableVariations = jtptSafeJsonParse( availableVariations, [] ); 303 variationsStockHtml = jtptSafeJsonParse( variationsStockHtml, {} ); 304 defaultAttributes = jtptSafeJsonParse( defaultAttributes, {} ); 305 elementConfiguration = jtptSafeJsonParse( elementConfiguration, {} ); 306 woocommerceSettings = jtptSafeJsonParse( woocommerceSettings, {} ); 252 307 253 308 // Loop through variation selects. … … 373 428 addToCartButton.attr( 'data-jtpt-variation', variation ); 374 429 addToCartButton.removeClass( 'jtpt-variation-selection-needed' ); 375 if ( ( 'simple' === productType ) &&( 'variable' === productType ) ) {430 if ( ( 'simple' === productType ) || ( 'variable' === productType ) ) { 376 431 addToCartButton.addClass( 'jtpt-ajax-add-to-cart' ); 377 432 } … … 488 543 489 544 if ( '' !== variation ) { 490 variation = JSON.parse( variation);545 variation = jtptSafeJsonParse( variation, '' ); 491 546 } 492 547 … … 494 549 selectedProducts = {}; 495 550 } else { 496 selectedProducts = JSON.parse( selectedProducts);551 selectedProducts = jtptSafeJsonParse( selectedProducts, {} ); 497 552 } 498 553 … … 582 637 tableLoading = productTableWrapper.next( '.jtpt-product-table-loading' ); 583 638 584 queryArgs = JSON.parse( queryArgs);585 taxonomyQueryIncludeArgs = JSON.parse( taxonomyQueryIncludeArgs);586 taxonomyQueryExcludeArgs = JSON.parse( taxonomyQueryExcludeArgs);587 activeColumns = JSON.parse( activeColumns);588 activeColumnsId = JSON.parse( activeColumnsId);589 elementConfiguration = JSON.parse( elementConfiguration);590 dataTableConfiguration = JSON.parse( dataTableConfiguration);591 woocommerceSettings = JSON.parse( woocommerceSettings);639 queryArgs = jtptSafeJsonParse( queryArgs, {} ); 640 taxonomyQueryIncludeArgs = jtptSafeJsonParse( taxonomyQueryIncludeArgs, {} ); 641 taxonomyQueryExcludeArgs = jtptSafeJsonParse( taxonomyQueryExcludeArgs, {} ); 642 activeColumns = jtptSafeJsonParse( activeColumns, [] ); 643 activeColumnsId = jtptSafeJsonParse( activeColumnsId, [] ); 644 elementConfiguration = jtptSafeJsonParse( elementConfiguration, {} ); 645 dataTableConfiguration = jtptSafeJsonParse( dataTableConfiguration, {} ); 646 woocommerceSettings = jtptSafeJsonParse( woocommerceSettings, {} ); 592 647 593 648 // DataTable options. … … 606 661 taxonomyQueryExcludeArgs = reloadingTableWrapper.attr( 'data-jtpt-taxonomy-query-filter-exclude-args-json' ); 607 662 608 taxonomyQueryIncludeArgs = JSON.parse( taxonomyQueryIncludeArgs);609 taxonomyQueryExcludeArgs = JSON.parse( taxonomyQueryExcludeArgs);663 taxonomyQueryIncludeArgs = jtptSafeJsonParse( taxonomyQueryIncludeArgs, {} ); 664 taxonomyQueryExcludeArgs = jtptSafeJsonParse( taxonomyQueryExcludeArgs, {} ); 610 665 611 666 reloadingTableWrapper.removeClass( 'jtpt-ajax-reloading-' + productTableId ); … … 722 777 td = thisAttributesHtml.closest( 'td' ); 723 778 724 attributes = JSON.parse( attributes);779 attributes = jtptSafeJsonParse( attributes, {} ); 725 780 726 781 if ( 'object' === typeof attributes ) { … … 740 795 tr = thisAttributesHtml.closest( 'tr' ); 741 796 742 attributes = JSON.parse( attributes);797 attributes = jtptSafeJsonParse( attributes, {} ); 743 798 744 799 if ( 'object' === typeof attributes ) { … … 850 905 let noResultsCell = thisProductTableWrapper.find( 'td.dataTables_empty' ); 851 906 852 if ( noResultsCell.length > 0 ) { 853 if ( searchKeyword.length > 0 ) { 854 let enhancedMessage = dataTableConfiguration.products_not_found_text + 907 if ( noResultsCell.length > 0 && searchKeyword.length > 0 ) { 908 let currentText = noResultsCell.text().trim(); 909 // Only enhance if not already enhanced 910 if ( currentText.indexOf( 'Searching for:' ) === -1 ) { 911 let enhancedMessage = currentText + 855 912 ' <span style="color:#666; font-size:14px;">(Searching for: <strong>' + searchKeyword + '</strong>)</span>'; 856 913 noResultsCell.html( enhancedMessage ); 857 } else {858 noResultsCell.html( dataTableConfiguration.products_not_found_text );859 914 } 860 915 } … … 877 932 878 933 // Catch the first and last column on window resize. 879 $( window ).o n( 'resize', function ( e ) {934 $( window ).off( 'resize.jtpt' ).on( 'resize.jtpt', function ( e ) { 880 935 tableHeadData.removeClass( 'jtpt-head-data-first' ); 881 936 tableHeadData.removeClass( 'jtpt-head-data-last' ); -
just-tables/tags/1.8.0/includes/Admin/Posts_Columns.php
r3237765 r3419781 23 23 add_filter( 'manage_jt-product-table_posts_columns', array( $this, 'filter_posts_columns' ) ); 24 24 add_action( 'manage_jt-product-table_posts_custom_column', array( $this, 'posts_shortcode_column_content' ), 10, 2 ); 25 add_filter( 'post_row_actions', array( $this, 'add_duplicate_action' ), 10, 2 ); 26 add_action( 'admin_init', array( $this, 'handle_duplicate_table' ) ); 27 add_action( 'admin_notices', array( $this, 'duplicate_admin_notice' ) ); 25 28 } 26 29 … … 64 67 } 65 68 69 /** 70 * Add duplicate action link to post row actions. 71 * 72 * @since 1.8.0 73 * 74 * @param array $actions Row actions. 75 * @param \WP_Post $post Current post object. 76 * @return array Modified actions. 77 */ 78 public function add_duplicate_action( $actions, $post ) { 79 if ( 'jt-product-table' !== $post->post_type ) { 80 return $actions; 81 } 82 83 if ( ! current_user_can( 'edit_posts' ) ) { 84 return $actions; 85 } 86 87 $duplicate_url = wp_nonce_url( 88 add_query_arg( 89 array( 90 'action' => 'jtpt_duplicate_table', 91 'post' => $post->ID, 92 ), 93 admin_url( 'admin.php' ) 94 ), 95 'jtpt_duplicate_table_' . $post->ID 96 ); 97 98 $actions['duplicate'] = sprintf( 99 '<a href="%s" title="%s">%s</a>', 100 esc_url( $duplicate_url ), 101 esc_attr__( 'Duplicate this table', 'just-tables' ), 102 esc_html__( 'Duplicate', 'just-tables' ) 103 ); 104 105 return $actions; 106 } 107 108 /** 109 * Handle table duplication. 110 * 111 * @since 1.8.0 112 */ 113 public function handle_duplicate_table() { 114 if ( ! isset( $_GET['action'] ) || 'jtpt_duplicate_table' !== $_GET['action'] ) { 115 return; 116 } 117 118 if ( ! isset( $_GET['post'] ) ) { 119 return; 120 } 121 122 $post_id = absint( $_GET['post'] ); 123 124 if ( ! $post_id ) { 125 return; 126 } 127 128 // Verify nonce. 129 if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'jtpt_duplicate_table_' . $post_id ) ) { 130 wp_die( esc_html__( 'Security check failed.', 'just-tables' ) ); 131 } 132 133 // Check permissions. 134 if ( ! current_user_can( 'edit_posts' ) ) { 135 wp_die( esc_html__( 'You do not have permission to duplicate this table.', 'just-tables' ) ); 136 } 137 138 // Get original post. 139 $original_post = get_post( $post_id ); 140 141 if ( ! $original_post || 'jt-product-table' !== $original_post->post_type ) { 142 wp_die( esc_html__( 'Invalid table.', 'just-tables' ) ); 143 } 144 145 // Create duplicate post. 146 $new_post_id = wp_insert_post( 147 array( 148 'post_title' => $original_post->post_title . ' ' . esc_html__( '(Copy)', 'just-tables' ), 149 'post_status' => 'draft', 150 'post_type' => 'jt-product-table', 151 'post_author' => get_current_user_id(), 152 'post_content' => $original_post->post_content, 153 'post_excerpt' => $original_post->post_excerpt, 154 ) 155 ); 156 157 if ( is_wp_error( $new_post_id ) ) { 158 wp_die( esc_html__( 'Failed to duplicate table.', 'just-tables' ) ); 159 } 160 161 // Copy all post meta. 162 $post_meta = get_post_meta( $post_id ); 163 164 if ( $post_meta ) { 165 foreach ( $post_meta as $meta_key => $meta_values ) { 166 // Skip internal WordPress meta keys. 167 if ( '_edit_lock' === $meta_key || '_edit_last' === $meta_key ) { 168 continue; 169 } 170 171 foreach ( $meta_values as $meta_value ) { 172 add_post_meta( $new_post_id, $meta_key, maybe_unserialize( $meta_value ) ); 173 } 174 } 175 } 176 177 // Redirect to edit screen. 178 wp_safe_redirect( 179 add_query_arg( 180 array( 181 'post' => $new_post_id, 182 'action' => 'edit', 183 'jtpt_duplicated' => '1', 184 ), 185 admin_url( 'post.php' ) 186 ) 187 ); 188 exit; 189 } 190 191 /** 192 * Show admin notice after successful duplication. 193 * 194 * @since 1.8.0 195 */ 196 public function duplicate_admin_notice() { 197 if ( ! isset( $_GET['jtpt_duplicated'] ) || '1' !== $_GET['jtpt_duplicated'] ) { 198 return; 199 } 200 201 $screen = get_current_screen(); 202 if ( ! $screen || 'jt-product-table' !== $screen->post_type ) { 203 return; 204 } 205 206 ?> 207 <div class="notice notice-success is-dismissible"> 208 <p><?php esc_html_e( 'Table duplicated successfully. You are now editing the copy.', 'just-tables' ); ?></p> 209 </div> 210 <?php 211 } 212 66 213 } -
just-tables/tags/1.8.0/just-tables.php
r3395023 r3419781 4 4 * Plugin URI: https://hasthemes.com/wp/justtables/ 5 5 * Description: Display WooCommerce products as table. 6 * Version: 1. 7.36 * Version: 1.8.0 7 7 * Author: HasThemes 8 8 * Author URI: https://hasthemes.com … … 37 37 * @var string $version 38 38 */ 39 public $version = '1. 7.3';39 public $version = '1.8.0'; 40 40 41 41 /** -
just-tables/tags/1.8.0/readme.txt
r3410349 r3419781 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 1. 7.36 Stable tag: 1.8.0 7 7 License: GPLv2 or later 8 8 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 112 112 == Changelog == 113 113 114 = Version: 1.8.0 - Date: 15 December, 2025 = 115 * Added: Table duplicate feature - easily duplicate tables from the admin list. 116 * Fixed: Elementor compatibility issue causing page editor to hang when editing. 117 * Fixed: Variable products not adding to cart properly due to logic bug. 118 * Fixed: JavaScript crashes when parsing invalid variation data. 119 * Fixed: Variation SKU and quantity limits not updating correctly when selecting variations. 120 * Improved: AJAX requests now have 30-second timeout for better error handling. 121 * Improved: User-friendly error messages for AJAX failures. 122 * Improved: Search clear button with smooth visibility transitions. 123 * Tested: Compatibility with the latest version of WordPress. 124 * Tested: Compatibility with the latest version of WooCommerce. 125 114 126 = Version: 1.7.3 - Date: 13 November, 2025 = 115 127 * Fixed: Minor bug fix. -
just-tables/trunk/assets/css/jtpt-frontend-base.css
r3370225 r3419781 362 362 line-height: 40px; 363 363 color: #59585A; 364 transition: opacity 0.2s ease, visibility 0.2s ease; 365 } 366 .jtpt-search.has-value .jtpt-search-icon { 367 opacity: 0; 368 visibility: hidden; 364 369 } 365 370 .jtpt-search .jtpt-search-clear { … … 368 373 height: 40px; 369 374 top: 0; 370 right: 1 5px;371 font-size: 24px;372 line-height: 36px;375 right: 12px; 376 font-size: 18px; 377 line-height: 40px; 373 378 color: #999; 374 379 cursor: pointer; 375 380 text-align: center; 376 transition: color 0.2s ease; 381 opacity: 0; 382 visibility: hidden; 383 transition: color 0.2s ease, opacity 0.2s ease, visibility 0.2s ease; 384 } 385 .jtpt-search.has-value .jtpt-search-clear { 386 opacity: 1; 387 visibility: visible; 377 388 } 378 389 .jtpt-search .jtpt-search-clear:hover { -
just-tables/trunk/assets/js/jtpt-frontend-ajax.js
r3061894 r3419781 7 7 'use strict'; 8 8 9 /** 10 * AJAX timeout in milliseconds (30 seconds). 11 * @since 1.8.0 12 */ 13 var JTPT_AJAX_TIMEOUT = 30000; 14 15 /** 16 * Safe JSON parse helper function. 17 * @since 1.8.0 18 */ 19 function jtptSafeJsonParse( jsonString, defaultValue ) { 20 if ( ! jsonString || '' === jsonString ) { 21 return defaultValue; 22 } 23 try { 24 return JSON.parse( jsonString ); 25 } catch ( e ) { 26 return defaultValue; 27 } 28 } 29 30 /** 31 * Show error message to user. 32 * @since 1.8.0 33 */ 34 function jtptShowErrorMessage( tableWrapper, message ) { 35 var noticeBoard = tableWrapper.find( '.jtpt-notices' ); 36 if ( noticeBoard.length === 0 ) { 37 noticeBoard = $( '.jtpt-notices' ); 38 } 39 40 var errorHtml = '<div class="woocommerce-error" role="alert" style="margin: 10px 0; padding: 10px 15px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24;">' + 41 '<strong>Error:</strong> ' + message + 42 '</div>'; 43 44 noticeBoard.html( errorHtml ); 45 46 if ( typeof tb_show === 'function' ) { 47 try { 48 tb_remove(); 49 tb_show( '', '#TB_inline?&inlineId=jtpt-notices-popup' ); 50 } catch ( e ) { 51 // Thickbox not available, show inline 52 } 53 } 54 } 55 56 /** 57 * Get user-friendly error message. 58 * @since 1.8.0 59 */ 60 function jtptGetErrorMessage( jqXHR, textStatus ) { 61 if ( textStatus === 'timeout' ) { 62 return 'The request timed out. Please try again.'; 63 } else if ( textStatus === 'abort' ) { 64 return 'The request was cancelled.'; 65 } else if ( jqXHR.status === 0 ) { 66 return 'Unable to connect. Please check your internet connection.'; 67 } else if ( jqXHR.status === 404 ) { 68 return 'The requested resource was not found.'; 69 } else if ( jqXHR.status === 500 ) { 70 return 'A server error occurred. Please try again later.'; 71 } else { 72 return 'An error occurred. Please try again.'; 73 } 74 } 75 9 76 $( document ).ready( function () { 10 77 … … 33 100 url: jtpt_data.ajax_url, 34 101 data: data, 102 timeout: JTPT_AJAX_TIMEOUT, 35 103 beforeSend: function(){ 36 104 tableWrapper.addClass( 'jtpt-loading' ); … … 49 117 tableWrapper.removeClass( 'jtpt-loading' ); 50 118 }, 51 error: function() { 52 tableWrapper.removeClass( 'jtpt-loading' ); 119 error: function( jqXHR, textStatus ) { 120 tableWrapper.removeClass( 'jtpt-loading' ); 121 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 53 122 }, 54 123 } ); … … 87 156 88 157 if ( '' !== variation ) { 89 variation = JSON.parse( variation);158 variation = jtptSafeJsonParse( variation, '' ); 90 159 } 91 160 … … 112 181 url: jtpt_data.ajax_url, 113 182 data: data, 183 timeout: JTPT_AJAX_TIMEOUT, 114 184 beforeSend: function(){ 115 185 tableWrapper.addClass( 'jtpt-loading' ); … … 128 198 tableWrapper.removeClass( 'jtpt-loading' ); 129 199 }, 130 error: function() { 131 tableWrapper.removeClass( 'jtpt-loading' ); 200 error: function( jqXHR, textStatus ) { 201 tableWrapper.removeClass( 'jtpt-loading' ); 202 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 132 203 }, 133 204 } ); … … 167 238 url: jtpt_data.ajax_url, 168 239 data: data, 240 timeout: JTPT_AJAX_TIMEOUT, 169 241 beforeSend: function(){ 170 242 tableWrapper.addClass( 'jtpt-loading' ); … … 183 255 tableWrapper.removeClass( 'jtpt-loading' ); 184 256 }, 185 error: function() { 186 tableWrapper.removeClass( 'jtpt-loading' ); 257 error: function( jqXHR, textStatus ) { 258 tableWrapper.removeClass( 'jtpt-loading' ); 259 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 187 260 }, 188 261 } ); … … 224 297 url: jtpt_data.ajax_url, 225 298 data: data, 299 timeout: JTPT_AJAX_TIMEOUT, 226 300 beforeSend: function(){ 227 301 tableWrapper.addClass( 'jtpt-loading' ); … … 240 314 tableWrapper.removeClass( 'jtpt-loading' ); 241 315 }, 242 error: function() { 243 tableWrapper.removeClass( 'jtpt-loading' ); 316 error: function( jqXHR, textStatus ) { 317 tableWrapper.removeClass( 'jtpt-loading' ); 318 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 244 319 }, 245 320 } ); … … 277 352 url: jtpt_data.ajax_url, 278 353 data: data, 354 timeout: JTPT_AJAX_TIMEOUT, 279 355 beforeSend: function(){ 280 356 tableWrapper.addClass( 'jtpt-loading' ); … … 293 369 tableWrapper.removeClass( 'jtpt-loading' ); 294 370 }, 295 error: function() { 296 tableWrapper.removeClass( 'jtpt-loading' ); 371 error: function( jqXHR, textStatus ) { 372 tableWrapper.removeClass( 'jtpt-loading' ); 373 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 297 374 }, 298 375 } ); … … 330 407 331 408 if ( '' !== variation ) { 332 variation = JSON.parse( variation);409 variation = jtptSafeJsonParse( variation, {} ); 333 410 } 334 411 … … 349 426 url: jtpt_data.ajax_url.toString().replace( '%%endpoint%%', 'add_to_cart' ), 350 427 data: data, 428 timeout: JTPT_AJAX_TIMEOUT, 351 429 beforeSend: function(){ 352 430 tableWrapper.addClass( 'jtpt-loading' ); … … 371 449 tableWrapper.removeClass( 'jtpt-loading' ); 372 450 }, 373 error: function() { 374 tableWrapper.removeClass( 'jtpt-loading' ); 451 error: function( jqXHR, textStatus ) { 452 tableWrapper.removeClass( 'jtpt-loading' ); 453 jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) ); 375 454 }, 376 455 dataType: 'json' -
just-tables/trunk/assets/js/jtpt-frontend-base.js
r3370225 r3419781 7 7 'use strict'; 8 8 9 /** 10 * Safe JSON parse helper function. 11 * Prevents crashes when parsing invalid JSON data. 12 * 13 * @since 1.8.0 14 * @param {string} jsonString - The JSON string to parse 15 * @param {*} defaultValue - Default value to return on parse failure 16 * @return {*} Parsed JSON or default value 17 */ 18 function jtptSafeJsonParse( jsonString, defaultValue ) { 19 if ( ! jsonString || '' === jsonString ) { 20 return defaultValue; 21 } 22 try { 23 return JSON.parse( jsonString ); 24 } catch ( e ) { 25 if ( typeof console !== 'undefined' && console.error ) { 26 console.error( 'JustTables: Failed to parse JSON data', e ); 27 } 28 return defaultValue; 29 } 30 } 31 32 /** 33 * Cleanup function to prevent memory leaks and event listener accumulation. 34 * Called before re-initializing tables (especially important for Elementor). 35 * 36 * @since 1.8.0 37 */ 38 function justTablesCleanup() { 39 // Remove namespaced event listeners from filter buttons 40 $( '.jtpt-ftrigger-button' ).off( 'click.jtpt' ); 41 $( '.jtpt-freset-button' ).off( 'click.jtpt' ); 42 43 // Remove namespaced window resize handlers 44 $( window ).off( 'resize.jtpt' ); 45 46 // Destroy existing DataTable instances to prevent duplicates 47 $( '.jtpt-product-table-wrapper' ).each( function() { 48 var table = $( this ).find( '.jtpt-product-table' ); 49 if ( $.fn.DataTable.isDataTable( table ) ) { 50 try { 51 table.DataTable().destroy(); 52 } catch ( e ) { 53 // Table may already be destroyed, ignore error 54 } 55 } 56 }); 57 } 58 9 59 function justTablesGlobal ( context = '' ) { 60 61 // Cleanup before re-initializing (prevents Elementor hanging issue) 62 if ( 'elementor-editor' === context || 'elementor-popup' === context || 'elementor-tab' === context ) { 63 justTablesCleanup(); 64 } 10 65 11 66 /** … … 14 69 * @since 1.0.0 15 70 */ 16 $( '.jtpt-ftrigger-button' ).o n( 'click', function ( e ) {71 $( '.jtpt-ftrigger-button' ).off( 'click.jtpt' ).on( 'click.jtpt', function ( e ) { 17 72 e.preventDefault(); 18 73 … … 25 80 * @since 1.0.0 26 81 */ 27 $( '.jtpt-freset-button' ).o n( 'click', function ( e ) {82 $( '.jtpt-freset-button' ).off( 'click.jtpt' ).on( 'click.jtpt', function ( e ) { 28 83 e.preventDefault(); 29 84 … … 184 239 var sku = tableWrapper.find( '.jtpt-sku-' + productId ), 185 240 simpleSKU = sku.attr( 'data-jtpt-simple-sku' ), 186 variationSKU = sku.attr( 'data-jtpt-simple-sku' );241 variationSKU = ''; 187 242 188 243 // Weight. … … 229 284 simpleQuantityInputMin = quantity.attr( 'data-jtpt-simple-min-qty' ), 230 285 simpleQuantityInputMax = quantity.attr( 'data-jtpt-simple-max-qty' ), 231 variationQuantityInputMin = quantity.attr( 'data-jtpt-simple-min-qty' ),232 variationQuantityInputMax = quantity.attr( 'data-jtpt-simple-max-qty' );286 variationQuantityInputMin = '', 287 variationQuantityInputMax = ''; 233 288 234 289 // Add to cart button. … … 244 299 variationNotAvailableText = ''; 245 300 246 // Parse json value .247 availableVariations = JSON.parse( availableVariations);248 variationsStockHtml = JSON.parse( variationsStockHtml);249 defaultAttributes = JSON.parse( defaultAttributes);250 elementConfiguration = JSON.parse( elementConfiguration);251 woocommerceSettings = JSON.parse( woocommerceSettings);301 // Parse json value with safe fallbacks. 302 availableVariations = jtptSafeJsonParse( availableVariations, [] ); 303 variationsStockHtml = jtptSafeJsonParse( variationsStockHtml, {} ); 304 defaultAttributes = jtptSafeJsonParse( defaultAttributes, {} ); 305 elementConfiguration = jtptSafeJsonParse( elementConfiguration, {} ); 306 woocommerceSettings = jtptSafeJsonParse( woocommerceSettings, {} ); 252 307 253 308 // Loop through variation selects. … … 373 428 addToCartButton.attr( 'data-jtpt-variation', variation ); 374 429 addToCartButton.removeClass( 'jtpt-variation-selection-needed' ); 375 if ( ( 'simple' === productType ) &&( 'variable' === productType ) ) {430 if ( ( 'simple' === productType ) || ( 'variable' === productType ) ) { 376 431 addToCartButton.addClass( 'jtpt-ajax-add-to-cart' ); 377 432 } … … 488 543 489 544 if ( '' !== variation ) { 490 variation = JSON.parse( variation);545 variation = jtptSafeJsonParse( variation, '' ); 491 546 } 492 547 … … 494 549 selectedProducts = {}; 495 550 } else { 496 selectedProducts = JSON.parse( selectedProducts);551 selectedProducts = jtptSafeJsonParse( selectedProducts, {} ); 497 552 } 498 553 … … 582 637 tableLoading = productTableWrapper.next( '.jtpt-product-table-loading' ); 583 638 584 queryArgs = JSON.parse( queryArgs);585 taxonomyQueryIncludeArgs = JSON.parse( taxonomyQueryIncludeArgs);586 taxonomyQueryExcludeArgs = JSON.parse( taxonomyQueryExcludeArgs);587 activeColumns = JSON.parse( activeColumns);588 activeColumnsId = JSON.parse( activeColumnsId);589 elementConfiguration = JSON.parse( elementConfiguration);590 dataTableConfiguration = JSON.parse( dataTableConfiguration);591 woocommerceSettings = JSON.parse( woocommerceSettings);639 queryArgs = jtptSafeJsonParse( queryArgs, {} ); 640 taxonomyQueryIncludeArgs = jtptSafeJsonParse( taxonomyQueryIncludeArgs, {} ); 641 taxonomyQueryExcludeArgs = jtptSafeJsonParse( taxonomyQueryExcludeArgs, {} ); 642 activeColumns = jtptSafeJsonParse( activeColumns, [] ); 643 activeColumnsId = jtptSafeJsonParse( activeColumnsId, [] ); 644 elementConfiguration = jtptSafeJsonParse( elementConfiguration, {} ); 645 dataTableConfiguration = jtptSafeJsonParse( dataTableConfiguration, {} ); 646 woocommerceSettings = jtptSafeJsonParse( woocommerceSettings, {} ); 592 647 593 648 // DataTable options. … … 606 661 taxonomyQueryExcludeArgs = reloadingTableWrapper.attr( 'data-jtpt-taxonomy-query-filter-exclude-args-json' ); 607 662 608 taxonomyQueryIncludeArgs = JSON.parse( taxonomyQueryIncludeArgs);609 taxonomyQueryExcludeArgs = JSON.parse( taxonomyQueryExcludeArgs);663 taxonomyQueryIncludeArgs = jtptSafeJsonParse( taxonomyQueryIncludeArgs, {} ); 664 taxonomyQueryExcludeArgs = jtptSafeJsonParse( taxonomyQueryExcludeArgs, {} ); 610 665 611 666 reloadingTableWrapper.removeClass( 'jtpt-ajax-reloading-' + productTableId ); … … 722 777 td = thisAttributesHtml.closest( 'td' ); 723 778 724 attributes = JSON.parse( attributes);779 attributes = jtptSafeJsonParse( attributes, {} ); 725 780 726 781 if ( 'object' === typeof attributes ) { … … 740 795 tr = thisAttributesHtml.closest( 'tr' ); 741 796 742 attributes = JSON.parse( attributes);797 attributes = jtptSafeJsonParse( attributes, {} ); 743 798 744 799 if ( 'object' === typeof attributes ) { … … 850 905 let noResultsCell = thisProductTableWrapper.find( 'td.dataTables_empty' ); 851 906 852 if ( noResultsCell.length > 0 ) { 853 if ( searchKeyword.length > 0 ) { 854 let enhancedMessage = dataTableConfiguration.products_not_found_text + 907 if ( noResultsCell.length > 0 && searchKeyword.length > 0 ) { 908 let currentText = noResultsCell.text().trim(); 909 // Only enhance if not already enhanced 910 if ( currentText.indexOf( 'Searching for:' ) === -1 ) { 911 let enhancedMessage = currentText + 855 912 ' <span style="color:#666; font-size:14px;">(Searching for: <strong>' + searchKeyword + '</strong>)</span>'; 856 913 noResultsCell.html( enhancedMessage ); 857 } else {858 noResultsCell.html( dataTableConfiguration.products_not_found_text );859 914 } 860 915 } … … 877 932 878 933 // Catch the first and last column on window resize. 879 $( window ).o n( 'resize', function ( e ) {934 $( window ).off( 'resize.jtpt' ).on( 'resize.jtpt', function ( e ) { 880 935 tableHeadData.removeClass( 'jtpt-head-data-first' ); 881 936 tableHeadData.removeClass( 'jtpt-head-data-last' ); -
just-tables/trunk/includes/Admin/Posts_Columns.php
r3237765 r3419781 23 23 add_filter( 'manage_jt-product-table_posts_columns', array( $this, 'filter_posts_columns' ) ); 24 24 add_action( 'manage_jt-product-table_posts_custom_column', array( $this, 'posts_shortcode_column_content' ), 10, 2 ); 25 add_filter( 'post_row_actions', array( $this, 'add_duplicate_action' ), 10, 2 ); 26 add_action( 'admin_init', array( $this, 'handle_duplicate_table' ) ); 27 add_action( 'admin_notices', array( $this, 'duplicate_admin_notice' ) ); 25 28 } 26 29 … … 64 67 } 65 68 69 /** 70 * Add duplicate action link to post row actions. 71 * 72 * @since 1.8.0 73 * 74 * @param array $actions Row actions. 75 * @param \WP_Post $post Current post object. 76 * @return array Modified actions. 77 */ 78 public function add_duplicate_action( $actions, $post ) { 79 if ( 'jt-product-table' !== $post->post_type ) { 80 return $actions; 81 } 82 83 if ( ! current_user_can( 'edit_posts' ) ) { 84 return $actions; 85 } 86 87 $duplicate_url = wp_nonce_url( 88 add_query_arg( 89 array( 90 'action' => 'jtpt_duplicate_table', 91 'post' => $post->ID, 92 ), 93 admin_url( 'admin.php' ) 94 ), 95 'jtpt_duplicate_table_' . $post->ID 96 ); 97 98 $actions['duplicate'] = sprintf( 99 '<a href="%s" title="%s">%s</a>', 100 esc_url( $duplicate_url ), 101 esc_attr__( 'Duplicate this table', 'just-tables' ), 102 esc_html__( 'Duplicate', 'just-tables' ) 103 ); 104 105 return $actions; 106 } 107 108 /** 109 * Handle table duplication. 110 * 111 * @since 1.8.0 112 */ 113 public function handle_duplicate_table() { 114 if ( ! isset( $_GET['action'] ) || 'jtpt_duplicate_table' !== $_GET['action'] ) { 115 return; 116 } 117 118 if ( ! isset( $_GET['post'] ) ) { 119 return; 120 } 121 122 $post_id = absint( $_GET['post'] ); 123 124 if ( ! $post_id ) { 125 return; 126 } 127 128 // Verify nonce. 129 if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'jtpt_duplicate_table_' . $post_id ) ) { 130 wp_die( esc_html__( 'Security check failed.', 'just-tables' ) ); 131 } 132 133 // Check permissions. 134 if ( ! current_user_can( 'edit_posts' ) ) { 135 wp_die( esc_html__( 'You do not have permission to duplicate this table.', 'just-tables' ) ); 136 } 137 138 // Get original post. 139 $original_post = get_post( $post_id ); 140 141 if ( ! $original_post || 'jt-product-table' !== $original_post->post_type ) { 142 wp_die( esc_html__( 'Invalid table.', 'just-tables' ) ); 143 } 144 145 // Create duplicate post. 146 $new_post_id = wp_insert_post( 147 array( 148 'post_title' => $original_post->post_title . ' ' . esc_html__( '(Copy)', 'just-tables' ), 149 'post_status' => 'draft', 150 'post_type' => 'jt-product-table', 151 'post_author' => get_current_user_id(), 152 'post_content' => $original_post->post_content, 153 'post_excerpt' => $original_post->post_excerpt, 154 ) 155 ); 156 157 if ( is_wp_error( $new_post_id ) ) { 158 wp_die( esc_html__( 'Failed to duplicate table.', 'just-tables' ) ); 159 } 160 161 // Copy all post meta. 162 $post_meta = get_post_meta( $post_id ); 163 164 if ( $post_meta ) { 165 foreach ( $post_meta as $meta_key => $meta_values ) { 166 // Skip internal WordPress meta keys. 167 if ( '_edit_lock' === $meta_key || '_edit_last' === $meta_key ) { 168 continue; 169 } 170 171 foreach ( $meta_values as $meta_value ) { 172 add_post_meta( $new_post_id, $meta_key, maybe_unserialize( $meta_value ) ); 173 } 174 } 175 } 176 177 // Redirect to edit screen. 178 wp_safe_redirect( 179 add_query_arg( 180 array( 181 'post' => $new_post_id, 182 'action' => 'edit', 183 'jtpt_duplicated' => '1', 184 ), 185 admin_url( 'post.php' ) 186 ) 187 ); 188 exit; 189 } 190 191 /** 192 * Show admin notice after successful duplication. 193 * 194 * @since 1.8.0 195 */ 196 public function duplicate_admin_notice() { 197 if ( ! isset( $_GET['jtpt_duplicated'] ) || '1' !== $_GET['jtpt_duplicated'] ) { 198 return; 199 } 200 201 $screen = get_current_screen(); 202 if ( ! $screen || 'jt-product-table' !== $screen->post_type ) { 203 return; 204 } 205 206 ?> 207 <div class="notice notice-success is-dismissible"> 208 <p><?php esc_html_e( 'Table duplicated successfully. You are now editing the copy.', 'just-tables' ); ?></p> 209 </div> 210 <?php 211 } 212 66 213 } -
just-tables/trunk/just-tables.php
r3395023 r3419781 4 4 * Plugin URI: https://hasthemes.com/wp/justtables/ 5 5 * Description: Display WooCommerce products as table. 6 * Version: 1. 7.36 * Version: 1.8.0 7 7 * Author: HasThemes 8 8 * Author URI: https://hasthemes.com … … 37 37 * @var string $version 38 38 */ 39 public $version = '1. 7.3';39 public $version = '1.8.0'; 40 40 41 41 /** -
just-tables/trunk/readme.txt
r3410349 r3419781 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 1. 7.36 Stable tag: 1.8.0 7 7 License: GPLv2 or later 8 8 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 112 112 == Changelog == 113 113 114 = Version: 1.8.0 - Date: 15 December, 2025 = 115 * Added: Table duplicate feature - easily duplicate tables from the admin list. 116 * Fixed: Elementor compatibility issue causing page editor to hang when editing. 117 * Fixed: Variable products not adding to cart properly due to logic bug. 118 * Fixed: JavaScript crashes when parsing invalid variation data. 119 * Fixed: Variation SKU and quantity limits not updating correctly when selecting variations. 120 * Improved: AJAX requests now have 30-second timeout for better error handling. 121 * Improved: User-friendly error messages for AJAX failures. 122 * Improved: Search clear button with smooth visibility transitions. 123 * Tested: Compatibility with the latest version of WordPress. 124 * Tested: Compatibility with the latest version of WooCommerce. 125 114 126 = Version: 1.7.3 - Date: 13 November, 2025 = 115 127 * Fixed: Minor bug fix.
Note: See TracChangeset
for help on using the changeset viewer.