Plugin Directory

Changeset 3419781


Ignore:
Timestamp:
12/15/2025 08:28:46 AM (3 months ago)
Author:
htplugins
Message:

Update to version 1.8.0 from GitHub

Location:
just-tables
Files:
12 edited
1 copied

Legend:

Unmodified
Added
Removed
  • just-tables/tags/1.8.0/assets/css/jtpt-frontend-base.css

    r3370225 r3419781  
    362362    line-height: 40px;
    363363    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;
    364369}
    365370.jtpt-search .jtpt-search-clear {
     
    368373    height: 40px;
    369374    top: 0;
    370     right: 15px;
    371     font-size: 24px;
    372     line-height: 36px;
     375    right: 12px;
     376    font-size: 18px;
     377    line-height: 40px;
    373378    color: #999;
    374379    cursor: pointer;
    375380    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;
    377388}
    378389.jtpt-search .jtpt-search-clear:hover {
  • just-tables/tags/1.8.0/assets/js/jtpt-frontend-ajax.js

    r3061894 r3419781  
    77    'use strict';
    88
     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
    976    $( document ).ready( function () {
    1077
     
    33100                url: jtpt_data.ajax_url,
    34101                data: data,
     102                timeout: JTPT_AJAX_TIMEOUT,
    35103                beforeSend: function(){
    36104                    tableWrapper.addClass( 'jtpt-loading' );
     
    49117                    tableWrapper.removeClass( 'jtpt-loading' );
    50118                },
    51                 error: function() {
    52                     tableWrapper.removeClass( 'jtpt-loading' );
     119                error: function( jqXHR, textStatus ) {
     120                    tableWrapper.removeClass( 'jtpt-loading' );
     121                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    53122                },
    54123            } );
     
    87156
    88157            if ( '' !== variation ) {
    89                 variation = JSON.parse( variation );
     158                variation = jtptSafeJsonParse( variation, '' );
    90159            }
    91160
     
    112181                url: jtpt_data.ajax_url,
    113182                data: data,
     183                timeout: JTPT_AJAX_TIMEOUT,
    114184                beforeSend: function(){
    115185                    tableWrapper.addClass( 'jtpt-loading' );
     
    128198                    tableWrapper.removeClass( 'jtpt-loading' );
    129199                },
    130                 error: function() {
    131                     tableWrapper.removeClass( 'jtpt-loading' );
     200                error: function( jqXHR, textStatus ) {
     201                    tableWrapper.removeClass( 'jtpt-loading' );
     202                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    132203                },
    133204            } );
     
    167238                url: jtpt_data.ajax_url,
    168239                data: data,
     240                timeout: JTPT_AJAX_TIMEOUT,
    169241                beforeSend: function(){
    170242                    tableWrapper.addClass( 'jtpt-loading' );
     
    183255                    tableWrapper.removeClass( 'jtpt-loading' );
    184256                },
    185                 error: function() {
    186                     tableWrapper.removeClass( 'jtpt-loading' );
     257                error: function( jqXHR, textStatus ) {
     258                    tableWrapper.removeClass( 'jtpt-loading' );
     259                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    187260                },
    188261            } );
     
    224297                url: jtpt_data.ajax_url,
    225298                data: data,
     299                timeout: JTPT_AJAX_TIMEOUT,
    226300                beforeSend: function(){
    227301                    tableWrapper.addClass( 'jtpt-loading' );
     
    240314                    tableWrapper.removeClass( 'jtpt-loading' );
    241315                },
    242                 error: function() {
    243                     tableWrapper.removeClass( 'jtpt-loading' );
     316                error: function( jqXHR, textStatus ) {
     317                    tableWrapper.removeClass( 'jtpt-loading' );
     318                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    244319                },
    245320            } );
     
    277352                url: jtpt_data.ajax_url,
    278353                data: data,
     354                timeout: JTPT_AJAX_TIMEOUT,
    279355                beforeSend: function(){
    280356                    tableWrapper.addClass( 'jtpt-loading' );
     
    293369                    tableWrapper.removeClass( 'jtpt-loading' );
    294370                },
    295                 error: function() {
    296                     tableWrapper.removeClass( 'jtpt-loading' );
     371                error: function( jqXHR, textStatus ) {
     372                    tableWrapper.removeClass( 'jtpt-loading' );
     373                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    297374                },
    298375            } );
     
    330407
    331408            if ( '' !== variation ) {
    332                 variation = JSON.parse( variation );
     409                variation = jtptSafeJsonParse( variation, {} );
    333410            }
    334411
     
    349426                url: jtpt_data.ajax_url.toString().replace( '%%endpoint%%', 'add_to_cart' ),
    350427                data: data,
     428                timeout: JTPT_AJAX_TIMEOUT,
    351429                beforeSend: function(){
    352430                    tableWrapper.addClass( 'jtpt-loading' );
     
    371449                    tableWrapper.removeClass( 'jtpt-loading' );
    372450                },
    373                 error: function() {
    374                     tableWrapper.removeClass( 'jtpt-loading' );
     451                error: function( jqXHR, textStatus ) {
     452                    tableWrapper.removeClass( 'jtpt-loading' );
     453                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    375454                },
    376455                dataType: 'json'
  • just-tables/tags/1.8.0/assets/js/jtpt-frontend-base.js

    r3370225 r3419781  
    77    'use strict';
    88
     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
    959    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        }
    1065
    1166        /**
     
    1469         * @since 1.0.0
    1570         */
    16         $( '.jtpt-ftrigger-button' ).on( 'click', function ( e ) {
     71        $( '.jtpt-ftrigger-button' ).off( 'click.jtpt' ).on( 'click.jtpt', function ( e ) {
    1772            e.preventDefault();
    1873
     
    2580         * @since 1.0.0
    2681         */
    27         $( '.jtpt-freset-button' ).on( 'click', function ( e ) {
     82        $( '.jtpt-freset-button' ).off( 'click.jtpt' ).on( 'click.jtpt', function ( e ) {
    2883            e.preventDefault();
    2984
     
    184239            var sku = tableWrapper.find( '.jtpt-sku-' + productId ),
    185240                simpleSKU = sku.attr( 'data-jtpt-simple-sku' ),
    186                 variationSKU = sku.attr( 'data-jtpt-simple-sku' );
     241                variationSKU = '';
    187242
    188243            // Weight.
     
    229284                simpleQuantityInputMin = quantity.attr( 'data-jtpt-simple-min-qty' ),
    230285                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 = '';
    233288
    234289            // Add to cart button.
     
    244299                variationNotAvailableText = '';
    245300
    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, {} );
    252307
    253308            // Loop through variation selects.
     
    373428                addToCartButton.attr( 'data-jtpt-variation', variation );
    374429                addToCartButton.removeClass( 'jtpt-variation-selection-needed' );
    375                 if ( ( 'simple' === productType ) && ( 'variable' === productType ) ) {
     430                if ( ( 'simple' === productType ) || ( 'variable' === productType ) ) {
    376431                    addToCartButton.addClass( 'jtpt-ajax-add-to-cart' );
    377432                }
     
    488543
    489544            if ( '' !== variation ) {
    490                 variation = JSON.parse( variation );
     545                variation = jtptSafeJsonParse( variation, '' );
    491546            }
    492547
     
    494549                selectedProducts = {};
    495550            } else {
    496                 selectedProducts = JSON.parse( selectedProducts );
     551                selectedProducts = jtptSafeJsonParse( selectedProducts, {} );
    497552            }
    498553
     
    582637                tableLoading = productTableWrapper.next( '.jtpt-product-table-loading' );
    583638
    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, {} );
    592647
    593648            // DataTable options.
     
    606661                            taxonomyQueryExcludeArgs = reloadingTableWrapper.attr( 'data-jtpt-taxonomy-query-filter-exclude-args-json' );
    607662
    608                             taxonomyQueryIncludeArgs = JSON.parse( taxonomyQueryIncludeArgs );
    609                             taxonomyQueryExcludeArgs = JSON.parse( taxonomyQueryExcludeArgs );
     663                            taxonomyQueryIncludeArgs = jtptSafeJsonParse( taxonomyQueryIncludeArgs, {} );
     664                            taxonomyQueryExcludeArgs = jtptSafeJsonParse( taxonomyQueryExcludeArgs, {} );
    610665
    611666                            reloadingTableWrapper.removeClass( 'jtpt-ajax-reloading-' + productTableId );
     
    722777                            td = thisAttributesHtml.closest( 'td' );
    723778
    724                         attributes = JSON.parse( attributes );
     779                        attributes = jtptSafeJsonParse( attributes, {} );
    725780
    726781                        if ( 'object' === typeof attributes ) {
     
    740795                            tr = thisAttributesHtml.closest( 'tr' );
    741796
    742                         attributes = JSON.parse( attributes );
     797                        attributes = jtptSafeJsonParse( attributes, {} );
    743798
    744799                        if ( 'object' === typeof attributes ) {
     
    850905                    let noResultsCell = thisProductTableWrapper.find( 'td.dataTables_empty' );
    851906
    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 +
    855912                                ' <span style="color:#666; font-size:14px;">(Searching for: <strong>' + searchKeyword + '</strong>)</span>';
    856913                            noResultsCell.html( enhancedMessage );
    857                         } else {
    858                             noResultsCell.html( dataTableConfiguration.products_not_found_text );
    859914                        }
    860915                    }
     
    877932
    878933            // Catch the first and last column on window resize.
    879             $( window ).on( 'resize', function ( e ) {
     934            $( window ).off( 'resize.jtpt' ).on( 'resize.jtpt', function ( e ) {
    880935                tableHeadData.removeClass( 'jtpt-head-data-first' );
    881936                tableHeadData.removeClass( 'jtpt-head-data-last' );
  • just-tables/tags/1.8.0/includes/Admin/Posts_Columns.php

    r3237765 r3419781  
    2323        add_filter( 'manage_jt-product-table_posts_columns', array( $this, 'filter_posts_columns' ) );
    2424        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' ) );
    2528    }
    2629
     
    6467    }
    6568
     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
    66213}
  • just-tables/tags/1.8.0/just-tables.php

    r3395023 r3419781  
    44 * Plugin URI: https://hasthemes.com/wp/justtables/
    55 * Description: Display WooCommerce products as table.
    6  * Version: 1.7.3
     6 * Version: 1.8.0
    77 * Author: HasThemes
    88 * Author URI: https://hasthemes.com
     
    3737         * @var string $version
    3838         */
    39         public $version = '1.7.3';
     39        public $version = '1.8.0';
    4040
    4141        /**
  • just-tables/tags/1.8.0/readme.txt

    r3410349 r3419781  
    44Requires at least: 5.0
    55Tested up to: 6.9
    6 Stable tag: 1.7.3
     6Stable tag: 1.8.0
    77License: GPLv2 or later
    88License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    112112== Changelog ==
    113113
     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
    114126= Version: 1.7.3 - Date: 13 November, 2025 =
    115127* Fixed: Minor bug fix.
  • just-tables/trunk/assets/css/jtpt-frontend-base.css

    r3370225 r3419781  
    362362    line-height: 40px;
    363363    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;
    364369}
    365370.jtpt-search .jtpt-search-clear {
     
    368373    height: 40px;
    369374    top: 0;
    370     right: 15px;
    371     font-size: 24px;
    372     line-height: 36px;
     375    right: 12px;
     376    font-size: 18px;
     377    line-height: 40px;
    373378    color: #999;
    374379    cursor: pointer;
    375380    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;
    377388}
    378389.jtpt-search .jtpt-search-clear:hover {
  • just-tables/trunk/assets/js/jtpt-frontend-ajax.js

    r3061894 r3419781  
    77    'use strict';
    88
     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
    976    $( document ).ready( function () {
    1077
     
    33100                url: jtpt_data.ajax_url,
    34101                data: data,
     102                timeout: JTPT_AJAX_TIMEOUT,
    35103                beforeSend: function(){
    36104                    tableWrapper.addClass( 'jtpt-loading' );
     
    49117                    tableWrapper.removeClass( 'jtpt-loading' );
    50118                },
    51                 error: function() {
    52                     tableWrapper.removeClass( 'jtpt-loading' );
     119                error: function( jqXHR, textStatus ) {
     120                    tableWrapper.removeClass( 'jtpt-loading' );
     121                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    53122                },
    54123            } );
     
    87156
    88157            if ( '' !== variation ) {
    89                 variation = JSON.parse( variation );
     158                variation = jtptSafeJsonParse( variation, '' );
    90159            }
    91160
     
    112181                url: jtpt_data.ajax_url,
    113182                data: data,
     183                timeout: JTPT_AJAX_TIMEOUT,
    114184                beforeSend: function(){
    115185                    tableWrapper.addClass( 'jtpt-loading' );
     
    128198                    tableWrapper.removeClass( 'jtpt-loading' );
    129199                },
    130                 error: function() {
    131                     tableWrapper.removeClass( 'jtpt-loading' );
     200                error: function( jqXHR, textStatus ) {
     201                    tableWrapper.removeClass( 'jtpt-loading' );
     202                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    132203                },
    133204            } );
     
    167238                url: jtpt_data.ajax_url,
    168239                data: data,
     240                timeout: JTPT_AJAX_TIMEOUT,
    169241                beforeSend: function(){
    170242                    tableWrapper.addClass( 'jtpt-loading' );
     
    183255                    tableWrapper.removeClass( 'jtpt-loading' );
    184256                },
    185                 error: function() {
    186                     tableWrapper.removeClass( 'jtpt-loading' );
     257                error: function( jqXHR, textStatus ) {
     258                    tableWrapper.removeClass( 'jtpt-loading' );
     259                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    187260                },
    188261            } );
     
    224297                url: jtpt_data.ajax_url,
    225298                data: data,
     299                timeout: JTPT_AJAX_TIMEOUT,
    226300                beforeSend: function(){
    227301                    tableWrapper.addClass( 'jtpt-loading' );
     
    240314                    tableWrapper.removeClass( 'jtpt-loading' );
    241315                },
    242                 error: function() {
    243                     tableWrapper.removeClass( 'jtpt-loading' );
     316                error: function( jqXHR, textStatus ) {
     317                    tableWrapper.removeClass( 'jtpt-loading' );
     318                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    244319                },
    245320            } );
     
    277352                url: jtpt_data.ajax_url,
    278353                data: data,
     354                timeout: JTPT_AJAX_TIMEOUT,
    279355                beforeSend: function(){
    280356                    tableWrapper.addClass( 'jtpt-loading' );
     
    293369                    tableWrapper.removeClass( 'jtpt-loading' );
    294370                },
    295                 error: function() {
    296                     tableWrapper.removeClass( 'jtpt-loading' );
     371                error: function( jqXHR, textStatus ) {
     372                    tableWrapper.removeClass( 'jtpt-loading' );
     373                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    297374                },
    298375            } );
     
    330407
    331408            if ( '' !== variation ) {
    332                 variation = JSON.parse( variation );
     409                variation = jtptSafeJsonParse( variation, {} );
    333410            }
    334411
     
    349426                url: jtpt_data.ajax_url.toString().replace( '%%endpoint%%', 'add_to_cart' ),
    350427                data: data,
     428                timeout: JTPT_AJAX_TIMEOUT,
    351429                beforeSend: function(){
    352430                    tableWrapper.addClass( 'jtpt-loading' );
     
    371449                    tableWrapper.removeClass( 'jtpt-loading' );
    372450                },
    373                 error: function() {
    374                     tableWrapper.removeClass( 'jtpt-loading' );
     451                error: function( jqXHR, textStatus ) {
     452                    tableWrapper.removeClass( 'jtpt-loading' );
     453                    jtptShowErrorMessage( tableWrapper, jtptGetErrorMessage( jqXHR, textStatus ) );
    375454                },
    376455                dataType: 'json'
  • just-tables/trunk/assets/js/jtpt-frontend-base.js

    r3370225 r3419781  
    77    'use strict';
    88
     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
    959    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        }
    1065
    1166        /**
     
    1469         * @since 1.0.0
    1570         */
    16         $( '.jtpt-ftrigger-button' ).on( 'click', function ( e ) {
     71        $( '.jtpt-ftrigger-button' ).off( 'click.jtpt' ).on( 'click.jtpt', function ( e ) {
    1772            e.preventDefault();
    1873
     
    2580         * @since 1.0.0
    2681         */
    27         $( '.jtpt-freset-button' ).on( 'click', function ( e ) {
     82        $( '.jtpt-freset-button' ).off( 'click.jtpt' ).on( 'click.jtpt', function ( e ) {
    2883            e.preventDefault();
    2984
     
    184239            var sku = tableWrapper.find( '.jtpt-sku-' + productId ),
    185240                simpleSKU = sku.attr( 'data-jtpt-simple-sku' ),
    186                 variationSKU = sku.attr( 'data-jtpt-simple-sku' );
     241                variationSKU = '';
    187242
    188243            // Weight.
     
    229284                simpleQuantityInputMin = quantity.attr( 'data-jtpt-simple-min-qty' ),
    230285                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 = '';
    233288
    234289            // Add to cart button.
     
    244299                variationNotAvailableText = '';
    245300
    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, {} );
    252307
    253308            // Loop through variation selects.
     
    373428                addToCartButton.attr( 'data-jtpt-variation', variation );
    374429                addToCartButton.removeClass( 'jtpt-variation-selection-needed' );
    375                 if ( ( 'simple' === productType ) && ( 'variable' === productType ) ) {
     430                if ( ( 'simple' === productType ) || ( 'variable' === productType ) ) {
    376431                    addToCartButton.addClass( 'jtpt-ajax-add-to-cart' );
    377432                }
     
    488543
    489544            if ( '' !== variation ) {
    490                 variation = JSON.parse( variation );
     545                variation = jtptSafeJsonParse( variation, '' );
    491546            }
    492547
     
    494549                selectedProducts = {};
    495550            } else {
    496                 selectedProducts = JSON.parse( selectedProducts );
     551                selectedProducts = jtptSafeJsonParse( selectedProducts, {} );
    497552            }
    498553
     
    582637                tableLoading = productTableWrapper.next( '.jtpt-product-table-loading' );
    583638
    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, {} );
    592647
    593648            // DataTable options.
     
    606661                            taxonomyQueryExcludeArgs = reloadingTableWrapper.attr( 'data-jtpt-taxonomy-query-filter-exclude-args-json' );
    607662
    608                             taxonomyQueryIncludeArgs = JSON.parse( taxonomyQueryIncludeArgs );
    609                             taxonomyQueryExcludeArgs = JSON.parse( taxonomyQueryExcludeArgs );
     663                            taxonomyQueryIncludeArgs = jtptSafeJsonParse( taxonomyQueryIncludeArgs, {} );
     664                            taxonomyQueryExcludeArgs = jtptSafeJsonParse( taxonomyQueryExcludeArgs, {} );
    610665
    611666                            reloadingTableWrapper.removeClass( 'jtpt-ajax-reloading-' + productTableId );
     
    722777                            td = thisAttributesHtml.closest( 'td' );
    723778
    724                         attributes = JSON.parse( attributes );
     779                        attributes = jtptSafeJsonParse( attributes, {} );
    725780
    726781                        if ( 'object' === typeof attributes ) {
     
    740795                            tr = thisAttributesHtml.closest( 'tr' );
    741796
    742                         attributes = JSON.parse( attributes );
     797                        attributes = jtptSafeJsonParse( attributes, {} );
    743798
    744799                        if ( 'object' === typeof attributes ) {
     
    850905                    let noResultsCell = thisProductTableWrapper.find( 'td.dataTables_empty' );
    851906
    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 +
    855912                                ' <span style="color:#666; font-size:14px;">(Searching for: <strong>' + searchKeyword + '</strong>)</span>';
    856913                            noResultsCell.html( enhancedMessage );
    857                         } else {
    858                             noResultsCell.html( dataTableConfiguration.products_not_found_text );
    859914                        }
    860915                    }
     
    877932
    878933            // Catch the first and last column on window resize.
    879             $( window ).on( 'resize', function ( e ) {
     934            $( window ).off( 'resize.jtpt' ).on( 'resize.jtpt', function ( e ) {
    880935                tableHeadData.removeClass( 'jtpt-head-data-first' );
    881936                tableHeadData.removeClass( 'jtpt-head-data-last' );
  • just-tables/trunk/includes/Admin/Posts_Columns.php

    r3237765 r3419781  
    2323        add_filter( 'manage_jt-product-table_posts_columns', array( $this, 'filter_posts_columns' ) );
    2424        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' ) );
    2528    }
    2629
     
    6467    }
    6568
     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
    66213}
  • just-tables/trunk/just-tables.php

    r3395023 r3419781  
    44 * Plugin URI: https://hasthemes.com/wp/justtables/
    55 * Description: Display WooCommerce products as table.
    6  * Version: 1.7.3
     6 * Version: 1.8.0
    77 * Author: HasThemes
    88 * Author URI: https://hasthemes.com
     
    3737         * @var string $version
    3838         */
    39         public $version = '1.7.3';
     39        public $version = '1.8.0';
    4040
    4141        /**
  • just-tables/trunk/readme.txt

    r3410349 r3419781  
    44Requires at least: 5.0
    55Tested up to: 6.9
    6 Stable tag: 1.7.3
     6Stable tag: 1.8.0
    77License: GPLv2 or later
    88License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    112112== Changelog ==
    113113
     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
    114126= Version: 1.7.3 - Date: 13 November, 2025 =
    115127* Fixed: Minor bug fix.
Note: See TracChangeset for help on using the changeset viewer.