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 (
+ {/* 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({
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);
}
/**