From d13a356331193d777843d19d34f7f609b121eb34 Mon Sep 17 00:00:00 2001 From: dwindown Date: Wed, 19 Nov 2025 22:59:31 +0700 Subject: [PATCH] fix: Major UX improvements and API error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed Issues: 1. ✅ API error handling - Added try-catch and validation 2. ✅ Pipe separator UX - Now accepts comma OR pipe naturally 3. ✅ Tab restructuring - Merged pricing into General tab Changes: 🔧 API (ProductsController.php): - Added try-catch error handling in create_product - Validate required fields (name) - Better empty field checks (use !empty instead of ??) - Support virtual, downloadable, featured flags - Array validation for categories/tags/variations - Return proper WP_Error on exceptions 🎨 UX Improvements: 1. Attribute Options Input (VariationsTab.tsx): ❌ Old: Pipe only, spaces rejected ✅ New: Comma OR pipe, spaces allowed - Split by /[,|]/ regex - Display as comma-separated (more natural) - Help text: "Type naturally: Red, Blue, Green" - No more cursor gymnastics! 2. Tab Restructuring (ProductFormTabbed.tsx): ❌ Old: 5 tabs (General, Pricing, Inventory, Variations, Organization) ✅ New: 3-4 tabs (General+Pricing, Inventory, Variations*, Organization) - Pricing merged into General tab - Variable products: 4 tabs (Variations shown) - Simple products: 3 tabs (Variations hidden) - Dynamic grid: grid-cols-3 or grid-cols-4 - Less tab switching! 3. GeneralTab.tsx Enhancement: - Added pricing fields section - SKU, Regular Price, Sale Price - Savings calculator ("Customers save X%") - Context-aware help text: * Simple: "Base price before discounts" * Variable: "Base price (can override per variation)" - All in one place! 📊 Result: - Simpler navigation (3-4 tabs vs 5) - Natural typing (comma works!) - Better context (pricing with product info) - Less cognitive load - Faster product creation --- .../Products/partials/ProductFormTabbed.tsx | 26 +-- .../Products/partials/tabs/GeneralTab.tsx | 90 ++++++++++ .../Products/partials/tabs/VariationsTab.tsx | 12 +- includes/Api/ProductsController.php | 160 +++++++++++------- 4 files changed, 204 insertions(+), 84 deletions(-) diff --git a/admin-spa/src/routes/Products/partials/ProductFormTabbed.tsx b/admin-spa/src/routes/Products/partials/ProductFormTabbed.tsx index ba05635..4cf6965 100644 --- a/admin-spa/src/routes/Products/partials/ProductFormTabbed.tsx +++ b/admin-spa/src/routes/Products/partials/ProductFormTabbed.tsx @@ -7,7 +7,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Package, DollarSign, Layers, Tag } from 'lucide-react'; import { toast } from 'sonner'; import { GeneralTab } from './tabs/GeneralTab'; -import { PricingTab } from './tabs/PricingTab'; import { InventoryTab } from './tabs/InventoryTab'; import { VariationsTab, ProductVariant } from './tabs/VariationsTab'; import { OrganizationTab } from './tabs/OrganizationTab'; @@ -106,7 +105,7 @@ export function ProductFormTabbed({ if (type === 'simple' && !regularPrice) { toast.error(__('Regular price is required for simple products')); - setActiveTab('pricing'); + setActiveTab('general'); return; } @@ -145,23 +144,21 @@ export function ProductFormTabbed({ return (
- + {__('General')} - - - {__('Pricing')} - {__('Inventory')} - - - {__('Variations')} - + {type === 'variable' && ( + + + {__('Variations')} + + )} {__('Organization')} @@ -187,13 +184,6 @@ export function ProductFormTabbed({ setDownloadable={setDownloadable} featured={featured} setFeatured={setFeatured} - /> - - - {/* Pricing Tab */} - - void; featured: boolean; setFeatured: (value: boolean) => void; + // Pricing props + sku: string; + setSku: (value: string) => void; + regularPrice: string; + setRegularPrice: (value: string) => void; + salePrice: string; + setSalePrice: (value: string) => void; }; export function GeneralTab({ @@ -44,7 +52,17 @@ export function GeneralTab({ setDownloadable, featured, setFeatured, + sku, + setSku, + regularPrice, + setRegularPrice, + salePrice, + setSalePrice, }: GeneralTabProps) { + const savingsPercent = + salePrice && regularPrice && parseFloat(salePrice) < parseFloat(regularPrice) + ? Math.round((1 - parseFloat(salePrice) / parseFloat(regularPrice)) * 100) + : 0; return ( @@ -145,6 +163,78 @@ export function GeneralTab({

+ {/* Pricing Section */} + +
+

{__('Pricing')}

+
+
+ + setSku(e.target.value)} + placeholder={__('PROD-001')} + className="mt-1.5 font-mono" + /> +

+ {__('Stock Keeping Unit (optional)')} +

+
+ +
+ +
+ + setRegularPrice(e.target.value)} + placeholder="0.00" + required={type === 'simple'} + className="pl-10" + /> +
+

+ {type === 'variable' + ? __('Base price (can override per variation)') + : __('Base price before discounts')} +

+
+ +
+ +
+ + setSalePrice(e.target.value)} + placeholder="0.00" + className="pl-10" + /> +
+

+ {__('Discounted price (optional)')} +

+
+
+ + {savingsPercent > 0 && ( +
+

+ 💰 {__('Customers save')} {savingsPercent}% +

+
+ )} +
+ {/* Additional Options */}
diff --git a/admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx b/admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx index 608dcef..38b111c 100644 --- a/admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx +++ b/admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx @@ -136,19 +136,21 @@ export function VariationsTab({
{ - const options = e.target.value - .split('|') + const input = e.target.value; + // Split by comma or pipe, trim whitespace + const options = input + .split(/[,|]/) .map(s => s.trim()) .filter(Boolean); updateAttribute(index, 'options', options); }} - placeholder={__('Red | Blue | Green (use | to separate)')} + placeholder={__('Red, Blue, Green (comma or pipe separated)')} className="mt-1.5" />

- {__('Separate options with | (pipe symbol)')} + {__('Type naturally: Red, Blue, Green (comma or | works)')}

diff --git a/includes/Api/ProductsController.php b/includes/Api/ProductsController.php index 8f06279..67d8b59 100644 --- a/includes/Api/ProductsController.php +++ b/includes/Api/ProductsController.php @@ -183,70 +183,108 @@ class ProductsController { * Create new product */ public static function create_product(WP_REST_Request $request) { - $data = $request->get_json_params(); - - // Determine product type - $type = $data['type'] ?? 'simple'; - - if ($type === 'variable') { - $product = new WC_Product_Variable(); - } else { - $product = new WC_Product_Simple(); - } - - // Set basic data - $product->set_name($data['name'] ?? ''); - $product->set_slug($data['slug'] ?? ''); - $product->set_status($data['status'] ?? 'publish'); - $product->set_description($data['description'] ?? ''); - $product->set_short_description($data['short_description'] ?? ''); - $product->set_sku($data['sku'] ?? ''); - $product->set_regular_price($data['regular_price'] ?? ''); - $product->set_sale_price($data['sale_price'] ?? ''); - $product->set_manage_stock($data['manage_stock'] ?? false); - - if ($data['manage_stock']) { - $product->set_stock_quantity($data['stock_quantity'] ?? 0); - $product->set_stock_status($data['stock_status'] ?? 'instock'); - } else { - $product->set_stock_status($data['stock_status'] ?? 'instock'); - } - - $product->set_weight($data['weight'] ?? ''); - $product->set_length($data['length'] ?? ''); - $product->set_width($data['width'] ?? ''); - $product->set_height($data['height'] ?? ''); - - // Categories - if (!empty($data['categories'])) { - $product->set_category_ids($data['categories']); - } - - // Tags - if (!empty($data['tags'])) { - $product->set_tag_ids($data['tags']); - } - - // Images - if (!empty($data['image_id'])) { - $product->set_image_id($data['image_id']); - } - if (!empty($data['gallery_image_ids'])) { - $product->set_gallery_image_ids($data['gallery_image_ids']); - } - - $product->save(); - - // Handle variations for variable products - if ($type === 'variable' && !empty($data['attributes'])) { - self::save_product_attributes($product, $data['attributes']); + try { + $data = $request->get_json_params(); - if (!empty($data['variations'])) { - self::save_product_variations($product, $data['variations']); + // Validate required fields + if (empty($data['name'])) { + return new WP_Error('missing_name', __('Product name is required', 'woonoow'), ['status' => 400]); } + + // Determine product type + $type = $data['type'] ?? 'simple'; + + if ($type === 'variable') { + $product = new WC_Product_Variable(); + } else { + $product = new WC_Product_Simple(); + } + + // Set basic data + $product->set_name($data['name']); + if (!empty($data['slug'])) { + $product->set_slug($data['slug']); + } + $product->set_status($data['status'] ?? 'publish'); + $product->set_description($data['description'] ?? ''); + $product->set_short_description($data['short_description'] ?? ''); + + if (!empty($data['sku'])) { + $product->set_sku($data['sku']); + } + + if (!empty($data['regular_price'])) { + $product->set_regular_price($data['regular_price']); + } + if (!empty($data['sale_price'])) { + $product->set_sale_price($data['sale_price']); + } + + $product->set_manage_stock($data['manage_stock'] ?? false); + + if (!empty($data['manage_stock'])) { + $product->set_stock_quantity($data['stock_quantity'] ?? 0); + } + $product->set_stock_status($data['stock_status'] ?? 'instock'); + + // Optional fields + if (!empty($data['weight'])) { + $product->set_weight($data['weight']); + } + if (!empty($data['length'])) { + $product->set_length($data['length']); + } + if (!empty($data['width'])) { + $product->set_width($data['width']); + } + if (!empty($data['height'])) { + $product->set_height($data['height']); + } + + // Virtual and downloadable + if (!empty($data['virtual'])) { + $product->set_virtual(true); + } + if (!empty($data['downloadable'])) { + $product->set_downloadable(true); + } + if (!empty($data['featured'])) { + $product->set_featured(true); + } + + // Categories + if (!empty($data['categories']) && is_array($data['categories'])) { + $product->set_category_ids($data['categories']); + } + + // Tags + if (!empty($data['tags']) && is_array($data['tags'])) { + $product->set_tag_ids($data['tags']); + } + + // Images + if (!empty($data['image_id'])) { + $product->set_image_id($data['image_id']); + } + if (!empty($data['gallery_image_ids']) && is_array($data['gallery_image_ids'])) { + $product->set_gallery_image_ids($data['gallery_image_ids']); + } + + $product->save(); + + // Handle variations for variable products + if ($type === 'variable' && !empty($data['attributes']) && is_array($data['attributes'])) { + self::save_product_attributes($product, $data['attributes']); + + if (!empty($data['variations']) && is_array($data['variations'])) { + self::save_product_variations($product, $data['variations']); + } + } + + return new WP_REST_Response(self::format_product_full($product), 201); + } catch (Exception $e) { + return new WP_Error('create_failed', $e->getMessage(), ['status' => 500]); } - - return new WP_REST_Response(self::format_product_full($product), 201); } /**