Przeglądaj źródła

Convert Category Manipulations page to lazy-loaded tabs — v1.6.0

Restructure the Category Manipulations admin page so its 6 operations
plus Important Notes become tabs. Each tab's markup and its (expensive)
data are fetched on demand via the new studiou_wcpcm_load_tab AJAX
handler, eliminating the page-load timeout on large stores.

- Shell page renders only the tab bar + empty container, no heavy calls
- 7 new view partials under views/manipulations/, each self-contained
- Tab controller keeps loaded panels hidden in the DOM so switching
  preserves form state and event handlers (no re-fetch, no re-init)
- Active tab tracked in the URL hash, survives refresh/reload
- get_categories_with_counts / get_media_categories_with_counts now
  transient-cached (5 min, busted on term/relationship changes)

Note: regenerate languages/studiou-wc-product-cat-manage-cs_CZ.mo
from the updated .po before release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dalibor Votruba 1 miesiąc temu
rodzic
commit
39b3136909

+ 10 - 5
studiou-wc-product-cat-manage/CLAUDE.md

@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 **QDR - Studiou WC Export/Import Product Categories** is a WordPress plugin that extends WooCommerce to provide batch export/import functionality for both product categories and products, along with category manipulation tools.
 
-**Current Version:** 1.5.3
+**Current Version:** 1.6.0
 
 ## Requirements
 
@@ -55,6 +55,8 @@ The plugin follows a modular class-based architecture:
   - Remove Not Assigned Media (deletes unused uncategorized media) (NEW in v1.5.2)
   - Update product variant prices (regular_price only, by category + attribute filter) (NEW in v1.5.3)
   - Uses direct database queries for reliable product detection
+  - `get_categories_with_counts()` / `get_media_categories_with_counts()` are transient-cached (5 min, busted on term changes) — uncached bodies live in the `*_uncached()` variants (NEW in v1.6.0)
+  - `handle_load_tab_ajax()` renders a manipulations tab partial on demand (NEW in v1.6.0)
   - Batch processing for delete operations (10 items per batch)
   - Auto-detects media category taxonomy (any custom taxonomy registered for attachments)
   - Robust media deletion with post-deletion verification and manual fallback cleanup
@@ -93,7 +95,9 @@ The plugin follows a modular class-based architecture:
 
 - **import.php** - Category import page UI
 - **export.php** - Category export page UI
-- **manipulations.php** - Category manipulations page UI with three operations:
+- **manipulations.php** - Category manipulations page **shell** (tab bar + empty content container). As of v1.6.0 it renders no heavy data; each tab is loaded on demand via the `studiou_wcpcm_load_tab` AJAX handler.
+- **manipulations/** - Tab partials, one per operation (NEW in v1.6.0): `tab-move.php`, `tab-batch-description.php`, `tab-delete-products.php`, `tab-clear-media.php`, `tab-remove-unassigned.php`, `tab-upvp.php`, `tab-notes.php`. Each partial fetches only its own data.
+  The page UI provides these operations:
   - Move products between categories (two dropdowns + "Do" button)
   - Batch description between categories (checkbox list + textarea + "Do" button)
   - Delete products in category (checkbox list + "Delete" button + progress bar)
@@ -343,11 +347,12 @@ The plugin registers the following AJAX actions:
 12. **studiou_wcpcm_upvp_get_attribute_values** - Lists values for the selected attribute, scoped to selected categories (NEW in v1.5.3)
 13. **studiou_wcpcm_upvp_count** - Counts and samples variations matching the current filter (NEW in v1.5.3)
 14. **studiou_wcpcm_upvp_apply** - Commits new regular price to matching variations via WC API (NEW in v1.5.3)
+15. **studiou_wcpcm_load_tab** - Renders a Category Manipulations tab partial on demand (NEW in v1.6.0)
 
 ### Product Operations (NEW in v1.4)
-15. **studiou_wcpcm_product_import_parse** - Parses CSV file and stores in transient (Step 1)
-16. **studiou_wcpcm_product_import_batch** - Processes batch of 5 products (Step 2, called recursively)
-17. **studiou_wcpcm_product_export** - Generates product CSV export by category selection
+16. **studiou_wcpcm_product_import_parse** - Parses CSV file and stores in transient (Step 1)
+17. **studiou_wcpcm_product_import_batch** - Processes batch of 5 products (Step 2, called recursively)
+18. **studiou_wcpcm_product_export** - Generates product CSV export by category selection
 
 All AJAX handlers include:
 - Nonce verification (`studiou-wcpcm-nonce`)

+ 23 - 0
studiou-wc-product-cat-manage/assets/css/admin.css

@@ -308,4 +308,27 @@
     padding: 6px 10px;
     border-bottom: 1px solid #eee;
     text-align: left;
+}
+
+/* Tabbed Category Manipulations page */
+.studiou-wcpcm-tabs {
+    margin-bottom: 0;
+}
+
+.studiou-wcpcm-tab-content {
+    margin-top: 20px;
+}
+
+.studiou-wcpcm-tab-loading,
+.studiou-wcpcm-tab-error {
+    padding: 40px;
+    text-align: center;
+    color: #666;
+    background: #fff;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+}
+
+.studiou-wcpcm-tab-error {
+    color: #a00;
 }

+ 127 - 17
studiou-wc-product-cat-manage/assets/js/admin.js

@@ -19,7 +19,7 @@
     // Initialize admin scripts
     $(document).ready(function() {
         // Debug: Log that script is loaded
-        console.log('STUDIOU WC: Admin script loaded v1.5.3');
+        console.log('STUDIOU WC: Admin script loaded v1.6.0');
 
         if (typeof studiouWcpcm === 'undefined') {
             console.error('STUDIOU WC: studiouWcpcm object is NOT defined - AJAX will not work');
@@ -30,16 +30,134 @@
 
         safeInit('Import form', '#studiou-wcpcm-import-form', initImportForm);
         safeInit('Export form', '#studiou-wcpcm-export-form', initExportForm);
-        safeInit('Move form', '#studiou-wcpcm-move-form', initMoveForm);
-        safeInit('Batch description form', '#studiou-wcpcm-batch-description-form', initBatchDescriptionForm);
-        safeInit('Delete products form', '#studiou-wcpcm-delete-products-form', initDeleteProductsForm);
-        safeInit('Clear media form', '#studiou-wcpcm-clear-media-form', initClearMediaForm);
-        safeInit('Remove unassigned media form', '#studiou-wcpcm-remove-unassigned-media-form', initRemoveUnassignedMediaForm);
         safeInit('Sample download', '#studiou-wcpcm-download-sample', initSampleDownload);
         safeInit('Product import form', '#studiou-wcpcm-product-import-form', initProductImportForm);
         safeInit('Product export form', '#studiou-wcpcm-product-export-form', initProductExportForm);
-        safeInit('Update variant prices form', '#studiou-wcpcm-upvp-form', initUpdateVariantPricesForm);
+
+        // Category Manipulations page: the manipulation forms live inside
+        // lazily-loaded tab partials, so they are initialised by the tab
+        // controller after their partial is injected (see initManipulationTabs).
+        safeInit('Manipulation tabs', '.studiou-wcpcm-tabs', initManipulationTabs);
     });
+
+    /**
+     * Initialize the tabbed Category Manipulations page.
+     *
+     * Each tab's markup is fetched on demand via the studiou_wcpcm_load_tab
+     * AJAX handler. The first time a tab is opened its partial is rendered into
+     * its own panel and its form handlers are wired; the panel is then kept in
+     * the DOM (hidden) so switching tabs preserves both state and event
+     * handlers - no re-fetch and no re-init.
+     */
+    function initManipulationTabs() {
+        // Maps a tab slug to the init function that wires its partial's handlers.
+        var TAB_INIT = {
+            'move':              initMoveForm,
+            'batch-description': initBatchDescriptionForm,
+            'delete-products':   initDeleteProductsForm,
+            'clear-media':       initClearMediaForm,
+            'remove-unassigned': initRemoveUnassignedMediaForm,
+            'upvp':              initUpdateVariantPricesForm,
+            'notes':             null // static, nothing to wire
+        };
+
+        var i18n     = (studiouWcpcm && studiouWcpcm.i18n) ? studiouWcpcm.i18n : {};
+        var $tabs    = $('.studiou-wcpcm-tabs .nav-tab');
+        var $content = $('#studiou-wcpcm-tab-content');
+        var loaded   = {}; // slug -> true once its panel exists in the DOM
+        var allowed  = $tabs.map(function() { return $(this).data('tab'); }).get();
+
+        function panelFor(slug) {
+            return $content.children('.studiou-wcpcm-tab-panel[data-tab="' + slug + '"]');
+        }
+
+        function activate(slug) {
+            if (allowed.indexOf(slug) === -1) {
+                slug = allowed[0];
+            }
+
+            $tabs.removeClass('nav-tab-active')
+                 .filter('[data-tab="' + slug + '"]').addClass('nav-tab-active');
+
+            if (window.history && history.replaceState) {
+                history.replaceState(null, '', '#' + slug);
+            } else {
+                window.location.hash = slug;
+            }
+
+            // Drop any stale loading / error placeholders, hide every panel.
+            $content.find('.studiou-wcpcm-tab-loading, .studiou-wcpcm-tab-error').remove();
+            $content.children('.studiou-wcpcm-tab-panel').hide();
+
+            // Already loaded - just reveal its panel (handlers + state intact).
+            if (loaded[slug]) {
+                panelFor(slug).show();
+                return;
+            }
+
+            $content.append('<div class="studiou-wcpcm-tab-loading">' +
+                (i18n.tabLoading || 'Loading...') + '</div>');
+
+            $.ajax({
+                url: studiouWcpcm.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcpcm_load_tab',
+                    nonce: studiouWcpcm.nonce,
+                    tab: slug
+                },
+                success: function(response) {
+                    $content.find('.studiou-wcpcm-tab-loading').remove();
+
+                    if (response.success && response.data && typeof response.data.html === 'string') {
+                        // Stale click guard: only reveal if this tab is still active.
+                        var stillActive = ($tabs.filter('.nav-tab-active').data('tab') === slug);
+
+                        var $panel = $('<div class="studiou-wcpcm-tab-panel"></div>')
+                            .attr('data-tab', slug)
+                            .html(response.data.html);
+                        $content.append($panel);
+                        loaded[slug] = true;
+
+                        var initFn = TAB_INIT[slug];
+                        if (typeof initFn === 'function') {
+                            try {
+                                initFn();
+                            } catch (e) {
+                                console.error('STUDIOU WC: tab init "' + slug + '" failed', e);
+                            }
+                        }
+
+                        if (stillActive) {
+                            $content.children('.studiou-wcpcm-tab-panel').hide();
+                            $panel.show();
+                        } else {
+                            $panel.hide();
+                        }
+                    } else {
+                        var msg = (response.data && response.data.message) ||
+                            (i18n.tabLoadError || 'Failed to load tab');
+                        $content.append('<div class="studiou-wcpcm-tab-error">' + msg + '</div>');
+                    }
+                },
+                error: function(xhr, status, error) {
+                    console.error('STUDIOU WC: load_tab AJAX error:', status, error);
+                    $content.find('.studiou-wcpcm-tab-loading').remove();
+                    $content.append('<div class="studiou-wcpcm-tab-error">' +
+                        (i18n.tabLoadError || 'Failed to load tab') + ': ' + error + '</div>');
+                }
+            });
+        }
+
+        $tabs.on('click', function(e) {
+            e.preventDefault();
+            activate($(this).data('tab'));
+        });
+
+        // Initial tab: a valid URL hash, otherwise the first tab.
+        var initial = (window.location.hash || '').replace(/^#/, '');
+        activate(allowed.indexOf(initial) !== -1 ? initial : allowed[0]);
+    }
     
     /**
      * Initialize import form
@@ -1338,20 +1456,12 @@
         $('#studiou-wcpcm-product-import-progress-bar').attr('data-progress', Math.round(percentage) + '%');
     }
 
-    // Additional debug: Check if elements exist after DOM load
+    // Additional debug: Check page state after DOM load
     $(window).on('load', function() {
         console.log('STUDIOU WC: Window loaded');
-        console.log('STUDIOU WC: Move form exists:', $('#studiou-wcpcm-move-form').length > 0);
-        console.log('STUDIOU WC: Move button exists:', $('#studiou-wcpcm-move-button').length > 0);
-        console.log('STUDIOU WC: Source select exists:', $('#source_category').length > 0);
-        console.log('STUDIOU WC: Target select exists:', $('#target_category').length > 0);
-        console.log('STUDIOU WC: Batch description form exists:', $('#studiou-wcpcm-batch-description-form').length > 0);
-        console.log('STUDIOU WC: Batch description button exists:', $('#studiou-wcpcm-batch-description-button').length > 0);
-        console.log('STUDIOU WC: Delete products form exists:', $('#studiou-wcpcm-delete-products-form').length > 0);
+        console.log('STUDIOU WC: Manipulation tabs exist:', $('.studiou-wcpcm-tabs').length > 0);
         console.log('STUDIOU WC: Product import form exists:', $('#studiou-wcpcm-product-import-form').length > 0);
         console.log('STUDIOU WC: Product export form exists:', $('#studiou-wcpcm-product-export-form').length > 0);
-        console.log('STUDIOU WC: Clear media form exists:', $('#studiou-wcpcm-clear-media-form').length > 0);
-        console.log('STUDIOU WC: UPVP form exists:', $('#studiou-wcpcm-upvp-form').length > 0);
     });
 
     /**

+ 9 - 1
studiou-wc-product-cat-manage/directory-structure.txt

@@ -16,6 +16,14 @@ studiou-wc-product-cat-manage/
 ├── views/
 │   ├── import.php
 │   ├── export.php
-│   └── manipulations.php
+│   ├── manipulations.php
+│   └── manipulations/
+│       ├── tab-move.php
+│       ├── tab-batch-description.php
+│       ├── tab-delete-products.php
+│       ├── tab-clear-media.php
+│       ├── tab-remove-unassigned.php
+│       ├── tab-upvp.php
+│       └── tab-notes.php
 ├── readme.md
 └── studiou-wc-product-cat-manage.php

+ 132 - 3
studiou-wc-product-cat-manage/includes/class-studiou-wc-product-cat-manage-manipulator.php

@@ -45,6 +45,17 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
         add_action('wp_ajax_studiou_wcpcm_upvp_count', array($this, 'handle_upvp_count_ajax'));
         add_action('wp_ajax_studiou_wcpcm_upvp_apply', array($this, 'handle_upvp_apply_ajax'));
 
+        // Tabbed manipulations page - load a tab partial on demand
+        add_action('wp_ajax_studiou_wcpcm_load_tab', array($this, 'handle_load_tab_ajax'));
+
+        // Invalidate the cached category/media counts whenever categories or
+        // product <-> category relationships change.
+        add_action('created_term', array($this, 'bust_count_cache'));
+        add_action('edited_term', array($this, 'bust_count_cache'));
+        add_action('delete_term', array($this, 'bust_count_cache'));
+        add_action('set_object_terms', array($this, 'bust_count_cache'));
+        add_action('deleted_term_relationships', array($this, 'bust_count_cache'));
+
         // Add a test AJAX handler to verify AJAX is working
         add_action('wp_ajax_studiou_wcpcm_test', array($this, 'handle_test_ajax'));
 
@@ -53,7 +64,87 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
             error_log('STUDIOU WC: AJAX handlers registered - move_products, batch_description, delete_products, and clear_media');
         }
     }
-    
+
+    /**
+     * Invalidate the cached category / media-category count data.
+     *
+     * Hooked to term and term-relationship changes so the counts shown on the
+     * manipulations tabs stay accurate.
+     */
+    public function bust_count_cache() {
+        delete_transient('studiou_wcpcm_cat_counts');
+        delete_transient('studiou_wcpcm_media_cat_counts');
+    }
+
+    /**
+     * Handle AJAX request to render a Category Manipulations tab partial.
+     *
+     * Each tab partial fetches only its own data, so the manipulations page
+     * shell can render instantly and heavy calculations run on demand.
+     */
+    public function handle_load_tab_ajax() {
+        // Clean any output buffers to prevent JSON corruption
+        while (ob_get_level()) {
+            ob_end_clean();
+        }
+
+        // Check nonce
+        if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce')) {
+            wp_send_json_error(array(
+                'message' => __('Security check failed', 'studiou-wc-product-cat-manage')
+            ));
+            return;
+        }
+
+        // Check permissions
+        if (!current_user_can('manage_woocommerce')) {
+            wp_send_json_error(array(
+                'message' => __('You do not have sufficient permissions', 'studiou-wc-product-cat-manage')
+            ));
+            return;
+        }
+
+        $tab = isset($_POST['tab']) ? sanitize_key($_POST['tab']) : '';
+        $allowed = array('move', 'batch-description', 'delete-products', 'clear-media', 'remove-unassigned', 'upvp', 'notes');
+
+        if (!in_array($tab, $allowed, true)) {
+            wp_send_json_error(array(
+                'message' => __('Unknown tab', 'studiou-wc-product-cat-manage')
+            ));
+            return;
+        }
+
+        $partial = STUDIOU_WCPCM_PLUGIN_DIR . 'views/manipulations/tab-' . $tab . '.php';
+
+        if (!file_exists($partial)) {
+            wp_send_json_error(array(
+                'message' => __('Tab content not found', 'studiou-wc-product-cat-manage')
+            ));
+            return;
+        }
+
+        // Made available to the partial; each partial fetches only what it needs.
+        $manipulator = $this;
+
+        try {
+            ob_start();
+            include $partial;
+            $html = ob_get_clean();
+
+            wp_send_json_success(array(
+                'tab' => $tab,
+                'html' => $html
+            ));
+        } catch (Exception $e) {
+            if (ob_get_level()) {
+                ob_end_clean();
+            }
+            error_log('STUDIOU WC TABS: tab "' . $tab . '" render failed: ' . $e->getMessage());
+            wp_send_json_error(array('message' => $e->getMessage()));
+        }
+    }
+
+
     /**
      * Handle AJAX batch description request
      */
@@ -527,11 +618,31 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
     }
     
     /**
-     * Get all categories with product counts for select lists
+     * Get all categories with product counts for select lists.
+     *
+     * Transient-cached wrapper around get_categories_with_counts_uncached().
+     * The N+1 per-category count queries are expensive on large stores, so the
+     * result is cached for 5 minutes and busted on any category change.
      *
      * @return array Array of categories with ID, name, and product count
      */
     public function get_categories_with_counts() {
+        $data = get_transient('studiou_wcpcm_cat_counts');
+
+        if (false === $data) {
+            $data = $this->get_categories_with_counts_uncached();
+            set_transient('studiou_wcpcm_cat_counts', $data, 5 * MINUTE_IN_SECONDS);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Get all categories with product counts for select lists (uncached).
+     *
+     * @return array Array of categories with ID, name, and product count
+     */
+    public function get_categories_with_counts_uncached() {
         $args = array(
             'taxonomy' => 'product_cat',
             'orderby' => 'name',
@@ -864,11 +975,29 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
     }
 
     /**
-     * Get all media categories with file counts for select lists
+     * Get all media categories with file counts for select lists.
+     *
+     * Transient-cached wrapper around get_media_categories_with_counts_uncached().
      *
      * @return array Array of media categories with ID, name, and file count
      */
     public function get_media_categories_with_counts() {
+        $data = get_transient('studiou_wcpcm_media_cat_counts');
+
+        if (false === $data) {
+            $data = $this->get_media_categories_with_counts_uncached();
+            set_transient('studiou_wcpcm_media_cat_counts', $data, 5 * MINUTE_IN_SECONDS);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Get all media categories with file counts for select lists (uncached).
+     *
+     * @return array Array of media categories with ID, name, and file count
+     */
+    public function get_media_categories_with_counts_uncached() {
         $taxonomy = $this->get_media_category_taxonomy();
 
         if (!$taxonomy) {

+ 17 - 1
studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage-cs_CZ.po

@@ -1088,4 +1088,20 @@ msgstr "Chyba aktualizace cen variant"
 
 #: views/manipulations.php
 msgid "The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified."
-msgstr "Operace aktualizace cen variant mění pouze běžnou cenu variant produktů. Položky objednávek, obsah košíku ani historická data objednávek nejsou nikdy měněny."
+msgstr "Operace aktualizace cen variant mění pouze běžnou cenu variant produktů. Položky objednávek, obsah košíku ani historická data objednávek nejsou nikdy měněny."
+
+#: studiou-wc-product-cat-manage.php views/manipulations.php
+msgid "Loading..."
+msgstr "Načítání..."
+
+#: studiou-wc-product-cat-manage.php
+msgid "Failed to load tab"
+msgstr "Nepodařilo se načíst záložku"
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Unknown tab"
+msgstr "Neznámá záložka"
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Tab content not found"
+msgstr "Obsah záložky nenalezen"

+ 16 - 0
studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage.po

@@ -1054,4 +1054,20 @@ msgstr ""
 
 #: views/manipulations.php
 msgid "The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified."
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php views/manipulations.php
+msgid "Loading..."
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php
+msgid "Failed to load tab"
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Unknown tab"
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Tab content not found"
 msgstr ""

+ 12 - 1
studiou-wc-product-cat-manage/readme.md

@@ -1,6 +1,6 @@
 # QDR - Studiou WC Export/Import Product Categories
 
-**Version: 1.5.3**
+**Version: 1.6.0**
 
 A WordPress plugin that extends WooCommerce to provide batch export and import functionality for both product categories and products, along with category manipulation tools.
 
@@ -28,6 +28,7 @@ The plugin provides a complete solution for bulk product and category management
 - **Clear Media Categories** - permanently delete all media files from selected media categories (NEW in v1.5)
 - **Remove Not Assigned Media** - permanently delete all unused uncategorized media files (NEW in v1.5.2)
 - **Update product variant prices** - bulk-update regular price of variations by category + attribute filter (NEW in v1.5.3)
+- **Tabbed Category Manipulations page** - each operation is a tab whose content loads on demand, preventing page-load timeouts on large stores (NEW in v1.6.0)
 
 ### Product Management (NEW in v1.4)
 - **Export products to CSV** - Export variable products and variations by category selection
@@ -351,6 +352,16 @@ For support, please visit [https://www.quadarax.com/plugins/studiou-wc-product-c
 
 ## Changelog
 
+### Version 1.6.0
+- **Category Manipulations page restructured into tabs**
+  - Each of the 6 operations plus Important Notes is now a separate tab
+  - A tab's HTML and its (expensive) data are fetched on demand via a separate AJAX request the first time the tab is opened
+  - Fixes the page-load timeout on stores with many product categories or media files - the shell page no longer runs every category/media calculation up front
+  - The active tab is preserved in the URL hash, so a refresh (and the automatic reload after move/delete/clear operations) returns to the same tab
+- New AJAX handler: `studiou_wcpcm_load_tab` renders a manipulations tab partial on demand
+- Category and media-category count queries are now transient-cached (5 minutes, automatically invalidated when categories or product/category relationships change)
+- 7 new view partials under `views/manipulations/`
+
 ### Version 1.5.3
 - **NEW: Update product variant prices** - bulk-update regular price of product variations
   - Filter by selected product categories (with Check All / Uncheck All)

+ 4 - 2
studiou-wc-product-cat-manage/studiou-wc-product-cat-manage.php

@@ -3,7 +3,7 @@
  * Plugin Name: QDR - Studiou WC Export/Import Product Categories
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-product-cat-manage
  * Description: Allows batch import and export WooCommerce product categories and products
- * Version: 1.5.3
+ * Version: 1.6.0
  * Requires at least: 6.8.1
  * Requires PHP: 8.2
  * Author: Dalibor Votruba
@@ -23,7 +23,7 @@ if (!defined('WPINC')) {
 }
 
 // Define plugin constants
-if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.5.3');
+if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.6.0');
 if (!defined('STUDIOU_WCPCM_PLUGIN_DIR')) define('STUDIOU_WCPCM_PLUGIN_DIR', plugin_dir_path(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_URL')) define('STUDIOU_WCPCM_PLUGIN_URL', plugin_dir_url(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_BASENAME')) define('STUDIOU_WCPCM_PLUGIN_BASENAME', plugin_basename(__FILE__));
@@ -291,6 +291,8 @@ class Studiou_WC_Product_Cat_Manage {
                     'upvpApplyError' => __('Variant prices update error', 'studiou-wc-product-cat-manage'),
                     'upvpNoMatches' => __('No variations match the current filter.', 'studiou-wc-product-cat-manage'),
                     'upvpConfirmApply' => __('Are you sure you want to update %d variations?', 'studiou-wc-product-cat-manage'),
+                    'tabLoading' => __('Loading...', 'studiou-wc-product-cat-manage'),
+                    'tabLoadError' => __('Failed to load tab', 'studiou-wc-product-cat-manage'),
                 )
             ));
             

+ 29 - 384
studiou-wc-product-cat-manage/views/manipulations.php

@@ -1,6 +1,11 @@
 <?php
 /**
- * Category Manipulations page view
+ * Category Manipulations page view (shell)
+ *
+ * Renders only the tab bar and an empty content container. Each tab's
+ * markup and its (potentially expensive) data is fetched on demand via the
+ * studiou_wcpcm_load_tab AJAX handler. This keeps the page-load request
+ * lightweight and avoids PHP timeouts on large stores.
  *
  * @since      1.0.0
  * @package    Studiou_WC_Product_Cat_Manage
@@ -11,397 +16,37 @@ if (!defined('WPINC')) {
     die;
 }
 
-// Get categories with counts for select boxes
-$manipulator = new Studiou_WC_Product_Cat_Manage_Manipulator(new Studiou_WC_Product_Cat_Manage_DB());
-$categories = $manipulator->get_categories_with_counts();
-$media_categories = $manipulator->get_media_categories_with_counts();
-$unassigned_media_count = $manipulator->get_unassigned_unused_media_count();
+$tabs = array(
+    'move'              => __('Move products between categories', 'studiou-wc-product-cat-manage'),
+    'batch-description' => __('Batch description between categories', 'studiou-wc-product-cat-manage'),
+    'delete-products'   => __('Delete products in category', 'studiou-wc-product-cat-manage'),
+    'clear-media'       => __('Clear Media Categories', 'studiou-wc-product-cat-manage'),
+    'remove-unassigned' => __('Remove Not Assigned Media', 'studiou-wc-product-cat-manage'),
+    'upvp'              => __('Update product variant prices', 'studiou-wc-product-cat-manage'),
+    'notes'             => __('Important Notes', 'studiou-wc-product-cat-manage'),
+);
 ?>
 
 <div class="wrap studiou-wcpcm-wrap">
     <h1><?php echo esc_html__('Category Manipulations', 'studiou-wc-product-cat-manage'); ?></h1>
-    
+
     <div class="studiou-wcpcm-notice-area">
         <!-- Area for displaying notices -->
     </div>
-    
-    <!-- Move Products Between Categories Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Move products between categories', 'studiou-wc-product-cat-manage'); ?></h2>
-        
-        <p><?php echo esc_html__('This operation allows you to move all products from one category to another. Products will be removed from the source category and added to the target category.', 'studiou-wc-product-cat-manage'); ?></p>
-        
-        <form id="studiou-wcpcm-move-form" method="post">
-            <table class="form-table">
-                <tr>
-                    <th scope="row">
-                        <label for="source_category"><?php echo esc_html__('Source Category', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <select name="source_category" id="source_category" class="regular-text" required>
-                            <option value=""><?php echo esc_html__('-- Select Source Category --', 'studiou-wc-product-cat-manage'); ?></option>
-                            <?php foreach ($categories as $category): ?>
-                                <option value="<?php echo esc_attr($category['id']); ?>">
-                                    <?php echo esc_html($category['display_name']); ?>
-                                </option>
-                            <?php endforeach; ?>
-                        </select>
-                        <p class="description"><?php echo esc_html__('Select the category from which products will be moved.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="target_category"><?php echo esc_html__('Target Category', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <select name="target_category" id="target_category" class="regular-text" required>
-                            <option value=""><?php echo esc_html__('-- Select Target Category --', 'studiou-wc-product-cat-manage'); ?></option>
-                            <?php foreach ($categories as $category): ?>
-                                <option value="<?php echo esc_attr($category['id']); ?>">
-                                    <?php echo esc_html($category['display_name']); ?>
-                                </option>
-                            <?php endforeach; ?>
-                        </select>
-                        <p class="description"><?php echo esc_html__('Select the category to which products will be moved.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-            </table>
-            
-            <div class="submit-buttons">
-                <button type="submit" class="button button-primary" id="studiou-wcpcm-move-button">
-                    <?php echo esc_html__('Do', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-        
-        <div class="studiou-wcpcm-move-results" style="display: none;">
-            <h3><?php echo esc_html__('Move Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-move-message"></div>
-        </div>
-    </div>
-    
-    <!-- Batch Description Between Categories Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Batch description between categories', 'studiou-wc-product-cat-manage'); ?></h2>
-        
-        <p><?php echo esc_html__('This operation allows you to set the same description for multiple categories at once.', 'studiou-wc-product-cat-manage'); ?></p>
-        
-        <form id="studiou-wcpcm-batch-description-form" method="post">
-            <table class="form-table">
-                <tr>
-                    <th scope="row">
-                        <label for="categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <div class="studiou-wcpcm-categories-list-container">
-                            <div class="studiou-wcpcm-list-actions">
-                                <button type="button" id="studiou-wcpcm-check-all" class="button">
-                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                                <button type="button" id="studiou-wcpcm-uncheck-all" class="button">
-                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                            </div>
-                            <div class="studiou-wcpcm-categories-list" id="categories_list">
-                                <?php foreach ($categories as $category): ?>
-                                    <label class="studiou-wcpcm-category-item">
-                                        <input type="checkbox" name="selected_categories[]" value="<?php echo esc_attr($category['id']); ?>">
-                                        <span><?php echo esc_html($category['display_name']); ?></span>
-                                    </label>
-                                <?php endforeach; ?>
-                            </div>
-                        </div>
-                        <p class="description"><?php echo esc_html__('Select the categories for which you want to set the description.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="category_description"><?php echo esc_html__('Product categories description', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <textarea name="category_description" id="category_description" rows="5" cols="50" class="large-text"></textarea>
-                        <p class="description"><?php echo esc_html__('Enter the description that will be applied to all selected categories.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-            </table>
-            
-            <div class="submit-buttons">
-                <button type="submit" class="button button-primary" id="studiou-wcpcm-batch-description-button">
-                    <?php echo esc_html__('Do', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-        
-        <div class="studiou-wcpcm-batch-description-results" style="display: none;">
-            <h3><?php echo esc_html__('Batch Description Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-batch-description-message"></div>
-        </div>
-    </div>
-    
-    <!-- Delete Products in Category Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Delete products in category', 'studiou-wc-product-cat-manage'); ?></h2>
-
-        <p><?php echo esc_html__('This operation allows you to permanently delete all products and their variants from selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></p>
-
-        <form id="studiou-wcpcm-delete-products-form" method="post">
-            <table class="form-table">
-                <tr>
-                    <th scope="row">
-                        <label for="delete_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <div class="studiou-wcpcm-categories-list-container">
-                            <div class="studiou-wcpcm-list-actions">
-                                <button type="button" id="studiou-wcpcm-delete-check-all" class="button">
-                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                                <button type="button" id="studiou-wcpcm-delete-uncheck-all" class="button">
-                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                            </div>
-                            <div class="studiou-wcpcm-categories-list" id="delete_categories_list">
-                                <?php foreach ($categories as $category): ?>
-                                    <label class="studiou-wcpcm-category-item">
-                                        <input type="checkbox" name="delete_categories[]" value="<?php echo esc_attr($category['id']); ?>">
-                                        <span><?php echo esc_html($category['display_name']); ?></span>
-                                    </label>
-                                <?php endforeach; ?>
-                            </div>
-                        </div>
-                        <p class="description"><?php echo esc_html__('Select the categories from which all products will be permanently deleted.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-            </table>
 
-            <div class="studiou-wcpcm-warning-message">
-                <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all products (including variants) in the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
-            </div>
+    <h2 class="nav-tab-wrapper studiou-wcpcm-tabs">
+        <?php foreach ($tabs as $slug => $label): ?>
+            <a href="#<?php echo esc_attr($slug); ?>"
+               class="nav-tab"
+               data-tab="<?php echo esc_attr($slug); ?>">
+                <?php echo esc_html($label); ?>
+            </a>
+        <?php endforeach; ?>
+    </h2>
 
-            <div class="submit-buttons">
-                <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-delete-products-button">
-                    <?php echo esc_html__('Delete', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-
-        <div class="studiou-wcpcm-delete-progress" style="display: none;">
-            <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div class="studiou-wcpcm-progress-bar-container">
-                <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-delete-progress-bar"></div>
-            </div>
-            <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-delete-progress-text"></div>
+    <div id="studiou-wcpcm-tab-content" class="studiou-wcpcm-tab-content">
+        <div class="studiou-wcpcm-tab-loading">
+            <?php echo esc_html__('Loading...', 'studiou-wc-product-cat-manage'); ?>
         </div>
-
-        <div class="studiou-wcpcm-delete-results" style="display: none;">
-            <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-delete-message"></div>
-        </div>
-    </div>
-
-    <!-- Clear Media Categories Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Clear Media Categories', 'studiou-wc-product-cat-manage'); ?></h2>
-
-        <p><?php echo esc_html__('This operation allows you to permanently delete all media files from selected media categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></p>
-
-        <?php if (empty($media_categories)): ?>
-            <p><em><?php echo esc_html__('No media categories found. Please install a media category plugin (e.g., Media Library Categories) to use this feature.', 'studiou-wc-product-cat-manage'); ?></em></p>
-        <?php else: ?>
-            <form id="studiou-wcpcm-clear-media-form" method="post" onsubmit="return false;">
-                <table class="form-table">
-                    <tr>
-                        <th scope="row">
-                            <label for="clear_media_categories_list"><?php echo esc_html__('Select Media Categories', 'studiou-wc-product-cat-manage'); ?></label>
-                        </th>
-                        <td>
-                            <div class="studiou-wcpcm-categories-list-container">
-                                <div class="studiou-wcpcm-list-actions">
-                                    <button type="button" id="studiou-wcpcm-clear-media-check-all" class="button">
-                                        <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
-                                    </button>
-                                    <button type="button" id="studiou-wcpcm-clear-media-uncheck-all" class="button">
-                                        <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
-                                    </button>
-                                </div>
-                                <div class="studiou-wcpcm-categories-list" id="clear_media_categories_list">
-                                    <?php foreach ($media_categories as $category): ?>
-                                        <label class="studiou-wcpcm-category-item">
-                                            <input type="checkbox" name="media_categories[]" value="<?php echo esc_attr($category['id']); ?>">
-                                            <span><?php echo esc_html($category['display_name']); ?></span>
-                                        </label>
-                                    <?php endforeach; ?>
-                                </div>
-                            </div>
-                            <p class="description"><?php echo esc_html__('Select the media categories from which all files will be permanently deleted.', 'studiou-wc-product-cat-manage'); ?></p>
-                        </td>
-                    </tr>
-                </table>
-
-                <div class="studiou-wcpcm-warning-message">
-                    <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all media files in the selected categories. The files will be removed from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
-                </div>
-
-                <div class="submit-buttons">
-                    <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-clear-media-button">
-                        <?php echo esc_html__('Delete', 'studiou-wc-product-cat-manage'); ?>
-                    </button>
-                    <span class="spinner"></span>
-                </div>
-            </form>
-
-            <div class="studiou-wcpcm-clear-media-progress" style="display: none;">
-                <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
-                <div class="studiou-wcpcm-progress-bar-container">
-                    <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-clear-media-progress-bar"></div>
-                </div>
-                <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-clear-media-progress-text"></div>
-            </div>
-
-            <div class="studiou-wcpcm-clear-media-results" style="display: none;">
-                <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
-                <div id="studiou-wcpcm-clear-media-message"></div>
-            </div>
-        <?php endif; ?>
-    </div>
-
-    <!-- Remove Not Assigned Media Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Remove Not Assigned Media', 'studiou-wc-product-cat-manage'); ?></h2>
-
-        <p><?php echo esc_html__('This operation permanently deletes all media files that are not assigned to any media category and are not used as featured image, product gallery image, or attached to any post.', 'studiou-wc-product-cat-manage'); ?></p>
-
-        <form id="studiou-wcpcm-remove-unassigned-media-form" method="post" onsubmit="return false;">
-            <div class="studiou-wcpcm-warning-message">
-                <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all unused uncategorized media files from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
-            </div>
-
-            <div class="submit-buttons">
-                <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-remove-unassigned-media-button">
-                    <?php echo esc_html(sprintf(__('Delete unused uncategorized media (%d)', 'studiou-wc-product-cat-manage'), $unassigned_media_count)); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-
-        <div class="studiou-wcpcm-remove-unassigned-media-progress" style="display: none;">
-            <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div class="studiou-wcpcm-progress-bar-container">
-                <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-remove-unassigned-media-progress-bar"></div>
-            </div>
-            <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-remove-unassigned-media-progress-text"></div>
-        </div>
-
-        <div class="studiou-wcpcm-remove-unassigned-media-results" style="display: none;">
-            <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-remove-unassigned-media-message"></div>
-        </div>
-    </div>
-
-    <!-- Update Product Variant Prices Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Update product variant prices', 'studiou-wc-product-cat-manage'); ?></h2>
-
-        <p><?php echo esc_html__('Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected.', 'studiou-wc-product-cat-manage'); ?></p>
-
-        <form id="studiou-wcpcm-upvp-form" method="post" onsubmit="return false;">
-            <table class="form-table">
-                <tr>
-                    <th scope="row">
-                        <label for="upvp_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <div class="studiou-wcpcm-categories-list-container">
-                            <div class="studiou-wcpcm-list-actions">
-                                <button type="button" id="studiou-wcpcm-upvp-check-all" class="button">
-                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                                <button type="button" id="studiou-wcpcm-upvp-uncheck-all" class="button">
-                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                            </div>
-                            <div class="studiou-wcpcm-categories-list" id="upvp_categories_list">
-                                <?php foreach ($categories as $category): ?>
-                                    <label class="studiou-wcpcm-category-item">
-                                        <input type="checkbox" name="upvp_categories[]" value="<?php echo esc_attr($category['id']); ?>">
-                                        <span><?php echo esc_html($category['display_name']); ?></span>
-                                    </label>
-                                <?php endforeach; ?>
-                            </div>
-                        </div>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="upvp_attribute"><?php echo esc_html__('Variant attribute', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <select id="upvp_attribute" name="upvp_attribute" class="regular-text">
-                            <option value=""><?php echo esc_html__('-- Select attribute --', 'studiou-wc-product-cat-manage'); ?></option>
-                        </select>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="upvp_attribute_value"><?php echo esc_html__('Variant attribute value', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <select id="upvp_attribute_value" name="upvp_attribute_value" class="regular-text" disabled>
-                            <option value=""><?php echo esc_html__('-- Select value --', 'studiou-wc-product-cat-manage'); ?></option>
-                        </select>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="upvp_new_price"><?php echo esc_html__('New Price', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <input type="number" step="0.01" min="0" id="upvp_new_price" name="upvp_new_price" class="regular-text">
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <?php echo esc_html__('Affected variations', 'studiou-wc-product-cat-manage'); ?>
-                    </th>
-                    <td>
-                        <strong id="studiou-wcpcm-upvp-affected-count">0</strong>
-                    </td>
-                </tr>
-            </table>
-
-            <div class="submit-buttons">
-                <button type="button" id="studiou-wcpcm-upvp-dry-run-button" class="button">
-                    <?php echo esc_html__('Dry Run', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <button type="button" id="studiou-wcpcm-upvp-apply-button" class="button button-primary">
-                    <?php echo esc_html__('Apply', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-
-        <div class="studiou-wcpcm-upvp-results" style="display: none;">
-            <h3><?php echo esc_html__('Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-upvp-message"></div>
-        </div>
-    </div>
-
-    <!-- Important Notes -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Important Notes', 'studiou-wc-product-cat-manage'); ?></h2>
-        <ul>
-            <li><?php echo esc_html__('Products can be assigned to multiple categories. This operation will remove the product from the source category and add it to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><?php echo esc_html__('If a product is only assigned to the source category, it will be moved entirely to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><?php echo esc_html__('If a product is assigned to multiple categories including the source category, it will remain in the other categories and be moved to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><?php echo esc_html__('The operation processes all products regardless of their status (published, draft, private).', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><?php echo esc_html__('The batch description operation will overwrite the existing descriptions for all selected categories.', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><strong><?php echo esc_html__('The delete products operation will permanently delete all products and variants from the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
-            <li><strong><?php echo esc_html__('The clear media categories operation will permanently delete all media files from the selected media categories and remove them from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
-            <li><strong><?php echo esc_html__('The remove not assigned media operation will permanently delete all media files that are not categorized and not used anywhere. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
-            <li><?php echo esc_html__('The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified.', 'studiou-wc-product-cat-manage'); ?></li>
-        </ul>
     </div>
-</div>
+</div>

+ 74 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-batch-description.php

@@ -0,0 +1,74 @@
+<?php
+/**
+ * Category Manipulations tab partial: Batch description between categories.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$categories = $manipulator->get_categories_with_counts();
+?>
+<!-- Batch Description Between Categories Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Batch description between categories', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation allows you to set the same description for multiple categories at once.', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-batch-description-form" method="post">
+        <table class="form-table">
+            <tr>
+                <th scope="row">
+                    <label for="categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <div class="studiou-wcpcm-categories-list-container">
+                        <div class="studiou-wcpcm-list-actions">
+                            <button type="button" id="studiou-wcpcm-check-all" class="button">
+                                <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                            <button type="button" id="studiou-wcpcm-uncheck-all" class="button">
+                                <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                        </div>
+                        <div class="studiou-wcpcm-categories-list" id="categories_list">
+                            <?php foreach ($categories as $category): ?>
+                                <label class="studiou-wcpcm-category-item">
+                                    <input type="checkbox" name="selected_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                    <span><?php echo esc_html($category['display_name']); ?></span>
+                                </label>
+                            <?php endforeach; ?>
+                        </div>
+                    </div>
+                    <p class="description"><?php echo esc_html__('Select the categories for which you want to set the description.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="category_description"><?php echo esc_html__('Product categories description', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <textarea name="category_description" id="category_description" rows="5" cols="50" class="large-text"></textarea>
+                    <p class="description"><?php echo esc_html__('Enter the description that will be applied to all selected categories.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+        </table>
+
+        <div class="submit-buttons">
+            <button type="submit" class="button button-primary" id="studiou-wcpcm-batch-description-button">
+                <?php echo esc_html__('Do', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-batch-description-results" style="display: none;">
+        <h3><?php echo esc_html__('Batch Description Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-batch-description-message"></div>
+    </div>
+</div>

+ 81 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-clear-media.php

@@ -0,0 +1,81 @@
+<?php
+/**
+ * Category Manipulations tab partial: Clear Media Categories.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$media_categories = $manipulator->get_media_categories_with_counts();
+?>
+<!-- Clear Media Categories Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Clear Media Categories', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation allows you to permanently delete all media files from selected media categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <?php if (empty($media_categories)): ?>
+        <p><em><?php echo esc_html__('No media categories found. Please install a media category plugin (e.g., Media Library Categories) to use this feature.', 'studiou-wc-product-cat-manage'); ?></em></p>
+    <?php else: ?>
+        <form id="studiou-wcpcm-clear-media-form" method="post" onsubmit="return false;">
+            <table class="form-table">
+                <tr>
+                    <th scope="row">
+                        <label for="clear_media_categories_list"><?php echo esc_html__('Select Media Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                    </th>
+                    <td>
+                        <div class="studiou-wcpcm-categories-list-container">
+                            <div class="studiou-wcpcm-list-actions">
+                                <button type="button" id="studiou-wcpcm-clear-media-check-all" class="button">
+                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                                </button>
+                                <button type="button" id="studiou-wcpcm-clear-media-uncheck-all" class="button">
+                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                                </button>
+                            </div>
+                            <div class="studiou-wcpcm-categories-list" id="clear_media_categories_list">
+                                <?php foreach ($media_categories as $category): ?>
+                                    <label class="studiou-wcpcm-category-item">
+                                        <input type="checkbox" name="media_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                        <span><?php echo esc_html($category['display_name']); ?></span>
+                                    </label>
+                                <?php endforeach; ?>
+                            </div>
+                        </div>
+                        <p class="description"><?php echo esc_html__('Select the media categories from which all files will be permanently deleted.', 'studiou-wc-product-cat-manage'); ?></p>
+                    </td>
+                </tr>
+            </table>
+
+            <div class="studiou-wcpcm-warning-message">
+                <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all media files in the selected categories. The files will be removed from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
+            </div>
+
+            <div class="submit-buttons">
+                <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-clear-media-button">
+                    <?php echo esc_html__('Delete', 'studiou-wc-product-cat-manage'); ?>
+                </button>
+                <span class="spinner"></span>
+            </div>
+        </form>
+
+        <div class="studiou-wcpcm-clear-media-progress" style="display: none;">
+            <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
+            <div class="studiou-wcpcm-progress-bar-container">
+                <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-clear-media-progress-bar"></div>
+            </div>
+            <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-clear-media-progress-text"></div>
+        </div>
+
+        <div class="studiou-wcpcm-clear-media-results" style="display: none;">
+            <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
+            <div id="studiou-wcpcm-clear-media-message"></div>
+        </div>
+    <?php endif; ?>
+</div>

+ 77 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-delete-products.php

@@ -0,0 +1,77 @@
+<?php
+/**
+ * Category Manipulations tab partial: Delete products in category.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$categories = $manipulator->get_categories_with_counts();
+?>
+<!-- Delete Products in Category Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Delete products in category', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation allows you to permanently delete all products and their variants from selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-delete-products-form" method="post">
+        <table class="form-table">
+            <tr>
+                <th scope="row">
+                    <label for="delete_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <div class="studiou-wcpcm-categories-list-container">
+                        <div class="studiou-wcpcm-list-actions">
+                            <button type="button" id="studiou-wcpcm-delete-check-all" class="button">
+                                <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                            <button type="button" id="studiou-wcpcm-delete-uncheck-all" class="button">
+                                <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                        </div>
+                        <div class="studiou-wcpcm-categories-list" id="delete_categories_list">
+                            <?php foreach ($categories as $category): ?>
+                                <label class="studiou-wcpcm-category-item">
+                                    <input type="checkbox" name="delete_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                    <span><?php echo esc_html($category['display_name']); ?></span>
+                                </label>
+                            <?php endforeach; ?>
+                        </div>
+                    </div>
+                    <p class="description"><?php echo esc_html__('Select the categories from which all products will be permanently deleted.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+        </table>
+
+        <div class="studiou-wcpcm-warning-message">
+            <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all products (including variants) in the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
+        </div>
+
+        <div class="submit-buttons">
+            <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-delete-products-button">
+                <?php echo esc_html__('Delete', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-delete-progress" style="display: none;">
+        <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div class="studiou-wcpcm-progress-bar-container">
+            <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-delete-progress-bar"></div>
+        </div>
+        <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-delete-progress-text"></div>
+    </div>
+
+    <div class="studiou-wcpcm-delete-results" style="display: none;">
+        <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-delete-message"></div>
+    </div>
+</div>

+ 71 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-move.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * Category Manipulations tab partial: Move products between categories.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$categories = $manipulator->get_categories_with_counts();
+?>
+<!-- Move Products Between Categories Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Move products between categories', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation allows you to move all products from one category to another. Products will be removed from the source category and added to the target category.', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-move-form" method="post">
+        <table class="form-table">
+            <tr>
+                <th scope="row">
+                    <label for="source_category"><?php echo esc_html__('Source Category', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <select name="source_category" id="source_category" class="regular-text" required>
+                        <option value=""><?php echo esc_html__('-- Select Source Category --', 'studiou-wc-product-cat-manage'); ?></option>
+                        <?php foreach ($categories as $category): ?>
+                            <option value="<?php echo esc_attr($category['id']); ?>">
+                                <?php echo esc_html($category['display_name']); ?>
+                            </option>
+                        <?php endforeach; ?>
+                    </select>
+                    <p class="description"><?php echo esc_html__('Select the category from which products will be moved.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="target_category"><?php echo esc_html__('Target Category', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <select name="target_category" id="target_category" class="regular-text" required>
+                        <option value=""><?php echo esc_html__('-- Select Target Category --', 'studiou-wc-product-cat-manage'); ?></option>
+                        <?php foreach ($categories as $category): ?>
+                            <option value="<?php echo esc_attr($category['id']); ?>">
+                                <?php echo esc_html($category['display_name']); ?>
+                            </option>
+                        <?php endforeach; ?>
+                    </select>
+                    <p class="description"><?php echo esc_html__('Select the category to which products will be moved.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+        </table>
+
+        <div class="submit-buttons">
+            <button type="submit" class="button button-primary" id="studiou-wcpcm-move-button">
+                <?php echo esc_html__('Do', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-move-results" style="display: none;">
+        <h3><?php echo esc_html__('Move Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-move-message"></div>
+    </div>
+</div>

+ 29 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-notes.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * Category Manipulations tab partial: Important Notes (static).
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+?>
+<!-- Important Notes -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Important Notes', 'studiou-wc-product-cat-manage'); ?></h2>
+    <ul>
+        <li><?php echo esc_html__('Products can be assigned to multiple categories. This operation will remove the product from the source category and add it to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><?php echo esc_html__('If a product is only assigned to the source category, it will be moved entirely to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><?php echo esc_html__('If a product is assigned to multiple categories including the source category, it will remain in the other categories and be moved to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><?php echo esc_html__('The operation processes all products regardless of their status (published, draft, private).', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><?php echo esc_html__('The batch description operation will overwrite the existing descriptions for all selected categories.', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><strong><?php echo esc_html__('The delete products operation will permanently delete all products and variants from the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
+        <li><strong><?php echo esc_html__('The clear media categories operation will permanently delete all media files from the selected media categories and remove them from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
+        <li><strong><?php echo esc_html__('The remove not assigned media operation will permanently delete all media files that are not categorized and not used anywhere. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
+        <li><?php echo esc_html__('The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified.', 'studiou-wc-product-cat-manage'); ?></li>
+    </ul>
+</div>

+ 48 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-remove-unassigned.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Category Manipulations tab partial: Remove Not Assigned Media.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$unassigned_media_count = $manipulator->get_unassigned_unused_media_count();
+?>
+<!-- Remove Not Assigned Media Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Remove Not Assigned Media', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation permanently deletes all media files that are not assigned to any media category and are not used as featured image, product gallery image, or attached to any post.', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-remove-unassigned-media-form" method="post" onsubmit="return false;">
+        <div class="studiou-wcpcm-warning-message">
+            <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all unused uncategorized media files from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
+        </div>
+
+        <div class="submit-buttons">
+            <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-remove-unassigned-media-button">
+                <?php echo esc_html(sprintf(__('Delete unused uncategorized media (%d)', 'studiou-wc-product-cat-manage'), $unassigned_media_count)); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-remove-unassigned-media-progress" style="display: none;">
+        <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div class="studiou-wcpcm-progress-bar-container">
+            <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-remove-unassigned-media-progress-bar"></div>
+        </div>
+        <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-remove-unassigned-media-progress-text"></div>
+    </div>
+
+    <div class="studiou-wcpcm-remove-unassigned-media-results" style="display: none;">
+        <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-remove-unassigned-media-message"></div>
+    </div>
+</div>

+ 103 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-upvp.php

@@ -0,0 +1,103 @@
+<?php
+/**
+ * Category Manipulations tab partial: Update product variant prices.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$categories = $manipulator->get_categories_with_counts();
+?>
+<!-- Update Product Variant Prices Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Update product variant prices', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected.', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-upvp-form" method="post" onsubmit="return false;">
+        <table class="form-table">
+            <tr>
+                <th scope="row">
+                    <label for="upvp_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <div class="studiou-wcpcm-categories-list-container">
+                        <div class="studiou-wcpcm-list-actions">
+                            <button type="button" id="studiou-wcpcm-upvp-check-all" class="button">
+                                <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                            <button type="button" id="studiou-wcpcm-upvp-uncheck-all" class="button">
+                                <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                        </div>
+                        <div class="studiou-wcpcm-categories-list" id="upvp_categories_list">
+                            <?php foreach ($categories as $category): ?>
+                                <label class="studiou-wcpcm-category-item">
+                                    <input type="checkbox" name="upvp_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                    <span><?php echo esc_html($category['display_name']); ?></span>
+                                </label>
+                            <?php endforeach; ?>
+                        </div>
+                    </div>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="upvp_attribute"><?php echo esc_html__('Variant attribute', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <select id="upvp_attribute" name="upvp_attribute" class="regular-text">
+                        <option value=""><?php echo esc_html__('-- Select attribute --', 'studiou-wc-product-cat-manage'); ?></option>
+                    </select>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="upvp_attribute_value"><?php echo esc_html__('Variant attribute value', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <select id="upvp_attribute_value" name="upvp_attribute_value" class="regular-text" disabled>
+                        <option value=""><?php echo esc_html__('-- Select value --', 'studiou-wc-product-cat-manage'); ?></option>
+                    </select>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="upvp_new_price"><?php echo esc_html__('New Price', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <input type="number" step="0.01" min="0" id="upvp_new_price" name="upvp_new_price" class="regular-text">
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <?php echo esc_html__('Affected variations', 'studiou-wc-product-cat-manage'); ?>
+                </th>
+                <td>
+                    <strong id="studiou-wcpcm-upvp-affected-count">0</strong>
+                </td>
+            </tr>
+        </table>
+
+        <div class="submit-buttons">
+            <button type="button" id="studiou-wcpcm-upvp-dry-run-button" class="button">
+                <?php echo esc_html__('Dry Run', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <button type="button" id="studiou-wcpcm-upvp-apply-button" class="button button-primary">
+                <?php echo esc_html__('Apply', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-upvp-results" style="display: none;">
+        <h3><?php echo esc_html__('Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-upvp-message"></div>
+    </div>
+</div>