fix: Major UX improvements and API error handling
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
This commit is contained in:
@@ -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 (
|
||||
<form ref={formRef} onSubmit={handleSubmit} className={className}>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-5 mb-6">
|
||||
<TabsList className={`grid w-full mb-6 ${type === 'variable' ? 'grid-cols-4' : 'grid-cols-3'}`}>
|
||||
<TabsTrigger value="general" className="flex items-center gap-2">
|
||||
<Package className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">{__('General')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pricing" className="flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">{__('Pricing')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="inventory" className="flex items-center gap-2">
|
||||
<Layers className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">{__('Inventory')}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="variations" className="flex items-center gap-2" disabled={type !== 'variable'}>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">{__('Variations')}</span>
|
||||
</TabsTrigger>
|
||||
{type === 'variable' && (
|
||||
<TabsTrigger value="variations" className="flex items-center gap-2">
|
||||
<Layers className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">{__('Variations')}</span>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="organization" className="flex items-center gap-2">
|
||||
<Tag className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">{__('Organization')}</span>
|
||||
@@ -187,13 +184,6 @@ export function ProductFormTabbed({
|
||||
setDownloadable={setDownloadable}
|
||||
featured={featured}
|
||||
setFeatured={setFeatured}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Pricing Tab */}
|
||||
<TabsContent value="pricing">
|
||||
<PricingTab
|
||||
type={type}
|
||||
sku={sku}
|
||||
setSku={setSku}
|
||||
regularPrice={regularPrice}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { DollarSign } from 'lucide-react';
|
||||
|
||||
type GeneralTabProps = {
|
||||
name: string;
|
||||
@@ -25,6 +26,13 @@ type GeneralTabProps = {
|
||||
setDownloadable: (value: boolean) => 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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -145,6 +163,78 @@ export function GeneralTab({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium">{__('Pricing')}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="sku">{__('SKU')}</Label>
|
||||
<Input
|
||||
id="sku"
|
||||
value={sku}
|
||||
onChange={(e) => setSku(e.target.value)}
|
||||
placeholder={__('PROD-001')}
|
||||
className="mt-1.5 font-mono"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{__('Stock Keeping Unit (optional)')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="regular_price">
|
||||
{__('Regular Price')} {type === 'simple' && '*'}
|
||||
</Label>
|
||||
<div className="relative mt-1.5">
|
||||
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="regular_price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={regularPrice}
|
||||
onChange={(e) => setRegularPrice(e.target.value)}
|
||||
placeholder="0.00"
|
||||
required={type === 'simple'}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{type === 'variable'
|
||||
? __('Base price (can override per variation)')
|
||||
: __('Base price before discounts')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="sale_price">{__('Sale Price')}</Label>
|
||||
<div className="relative mt-1.5">
|
||||
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="sale_price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={salePrice}
|
||||
onChange={(e) => setSalePrice(e.target.value)}
|
||||
placeholder="0.00"
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{__('Discounted price (optional)')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{savingsPercent > 0 && (
|
||||
<div className="bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-lg p-3">
|
||||
<p className="text-sm text-emerald-800 dark:text-emerald-300">
|
||||
💰 {__('Customers save')} {savingsPercent}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Additional Options */}
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -136,19 +136,21 @@ export function VariationsTab({
|
||||
<div>
|
||||
<Label>{__('Options')}</Label>
|
||||
<Input
|
||||
value={attr.options.join(' | ')}
|
||||
value={attr.options.join(', ')}
|
||||
onChange={(e) => {
|
||||
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"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{__('Separate options with | (pipe symbol)')}
|
||||
{__('Type naturally: Red, Blue, Green (comma or | works)')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user