fix(admin): set explicit width for product search dropdown in order form

Prevents the search dropdown from shrinking or overflowing unpredictably
in the flex container. Also ensures better alignment.
This commit is contained in:
Dwindi Ramadhana
2026-01-07 23:34:48 +07:00
parent 5170aea882
commit a52f5fc707

View File

@@ -44,9 +44,9 @@ import { SearchableSelect } from '@/components/ui/searchable-select';
export type CountryOption = { code: string; name: string };
export type StatesMap = Record<string, Record<string, string>>; // { US: { CA: 'California' } }
export type PaymentChannel = { id: string; title: string; meta?: any };
export type PaymentMethod = {
id: string;
title: string;
export type PaymentMethod = {
id: string;
title: string;
enabled?: boolean;
channels?: PaymentChannel[]; // If present, show channels instead of gateway
};
@@ -113,7 +113,7 @@ type Props = {
hideSubmitButton?: boolean;
};
const STATUS_LIST = ['pending','processing','on-hold','completed','cancelled','refunded','failed'];
const STATUS_LIST = ['pending', 'processing', 'on-hold', 'completed', 'cancelled', 'refunded', 'failed'];
// --- Component --------------------------------------------------------
export default function OrderForm({
@@ -167,11 +167,11 @@ export default function OrderForm({
const only = countries[0]?.code || '';
if (shipDiff) {
if (only && shippingData.country !== only) {
setShippingData({...shippingData, country: only});
setShippingData({ ...shippingData, country: only });
}
} else {
// keep shipping synced to billing when not different
setShippingData({...shippingData, country: bCountry});
setShippingData({ ...shippingData, country: bCountry });
}
}
}, [oneCountryOnly, countries, shipDiff, bCountry, shippingData.country]);
@@ -233,12 +233,12 @@ export default function OrderForm({
// Debounce city input to avoid hitting backend on every keypress
const [debouncedCity, setDebouncedCity] = React.useState(effectiveShippingAddress.city);
React.useEffect(() => {
const timer = setTimeout(() => {
setDebouncedCity(effectiveShippingAddress.city);
}, 500); // Wait 500ms after user stops typing
return () => clearTimeout(timer);
}, [effectiveShippingAddress.city]);
@@ -295,10 +295,10 @@ export default function OrderForm({
const products: ProductSearchItem[] = Array.isArray(raw)
? raw
: Array.isArray(raw?.data)
? raw.data
: Array.isArray(raw?.rows)
? raw.rows
: [];
? raw.data
: Array.isArray(raw?.rows)
? raw.rows
: [];
const customersRaw = customersQ.data as any;
const customers: any[] = Array.isArray(customersRaw) ? customersRaw : [];
@@ -311,7 +311,7 @@ export default function OrderForm({
() => items.reduce((sum, it) => sum + (Number(it.qty) || 0) * (Number(it.price) || 0), 0),
[items]
);
// Calculate shipping cost
// In edit mode: use existing order shipping total (fixed unless address changes)
// In create mode: calculate from selected shipping method
@@ -325,34 +325,34 @@ export default function OrderForm({
const method = shippings.find(s => s.id === shippingMethod);
return method ? Number(method.cost) || 0 : 0;
}, [mode, initial?.totals?.shipping, shippingMethod, shippings]);
// Calculate discount from validated coupons
const couponDiscount = React.useMemo(() => {
return validatedCoupons.reduce((sum, c) => sum + (c.discount_amount || 0), 0);
}, [validatedCoupons]);
// Calculate order total (items + shipping - coupons)
const orderTotal = React.useMemo(() => {
return Math.max(0, itemsTotal + shippingCost - couponDiscount);
}, [itemsTotal, shippingCost, couponDiscount]);
// Validate coupon
const validateCoupon = async (code: string) => {
if (!code.trim()) return;
// Check if already added
if (validatedCoupons.some(c => c.code.toLowerCase() === code.toLowerCase())) {
toast.error(__('Coupon already added'));
return;
}
setCouponValidating(true);
try {
const response = await api.post('/coupons/validate', {
code: code.trim(),
subtotal: itemsTotal,
});
if (response.valid) {
setValidatedCoupons([...validatedCoupons, response]);
setCouponInput('');
@@ -366,7 +366,7 @@ export default function OrderForm({
setCouponValidating(false);
}
};
const removeCoupon = (code: string) => {
setValidatedCoupons(validatedCoupons.filter(c => c.code !== code));
};
@@ -408,7 +408,7 @@ export default function OrderForm({
// Keep shipping country synced to billing when unchecked
React.useEffect(() => {
if (!shipDiff) setShippingData({...shippingData, country: bCountry});
if (!shipDiff) setShippingData({ ...shippingData, country: bCountry });
}, [shipDiff, bCountry]);
// Clamp states when country changes
@@ -417,7 +417,7 @@ export default function OrderForm({
}, [bCountry]);
React.useEffect(() => {
if (shippingData.state && !states[shippingData.country]?.[shippingData.state]) {
setShippingData({...shippingData, state: ''});
setShippingData({ ...shippingData, state: '' });
}
}, [shippingData.country]);
@@ -426,7 +426,7 @@ export default function OrderForm({
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// For virtual-only products, don't send address fields
const billingData: any = {
first_name: bFirst,
@@ -434,7 +434,7 @@ export default function OrderForm({
email: bEmail,
phone: bPhone,
};
// Only add address fields for physical products
if (hasPhysicalProduct) {
billingData.address_1 = bAddr1;
@@ -443,7 +443,7 @@ export default function OrderForm({
billingData.postcode = bPost;
billingData.country = bCountry;
}
const payload: OrderPayload = {
status,
billing: billingData,
@@ -502,7 +502,7 @@ export default function OrderForm({
onChange={(val: string) => {
const p = products.find((prod: ProductSearchItem) => String(prod.id) === val);
if (!p) return;
// If variable product, show variation selector
if (p.type === 'variable' && p.variations && p.variations.length > 0) {
setSelectedProduct(p);
@@ -510,7 +510,7 @@ export default function OrderForm({
setShowVariationDrawer(true);
return;
}
// Simple product - add directly (but allow duplicates for different quantities)
setItems(prev => [
...prev,
@@ -530,6 +530,7 @@ export default function OrderForm({
onSearch={setSearchQ}
disabled={!itemsEditable}
showCheckIndicator={false}
className="w-[200px] md:w-[300px]"
/>
</div>
) : (
@@ -733,7 +734,7 @@ export default function OrderForm({
.map(([key, value]) => `${key}: ${value || ''}`)
.filter(([_, value]) => value) // Remove empty values
.join(', ');
return (
<button
key={variation.id}
@@ -743,11 +744,11 @@ export default function OrderForm({
const existingIndex = items.findIndex(
item => item.product_id === selectedProduct.id && item.variation_id === variation.id
);
if (existingIndex !== -1) {
// Increment quantity of existing item
setItems(prev => prev.map((item, idx) =>
idx === existingIndex
setItems(prev => prev.map((item, idx) =>
idx === existingIndex
? { ...item, qty: item.qty + 1 }
: item
));
@@ -831,7 +832,7 @@ export default function OrderForm({
.map(([key, value]) => `${key}: ${value || ''}`)
.filter(([_, value]) => value) // Remove empty values
.join(', ');
return (
<button
key={variation.id}
@@ -841,11 +842,11 @@ export default function OrderForm({
const existingIndex = items.findIndex(
item => item.product_id === selectedProduct.id && item.variation_id === variation.id
);
if (existingIndex !== -1) {
// Increment quantity of existing item
setItems(prev => prev.map((item, idx) =>
idx === existingIndex
setItems(prev => prev.map((item, idx) =>
idx === existingIndex
? { ...item, qty: item.qty + 1 }
: item
));
@@ -924,7 +925,7 @@ export default function OrderForm({
<span className="text-xs opacity-70">({__('locked')})</span>
)}
</div>
{/* Coupon Input */}
<div className="flex gap-2">
<Input
@@ -949,7 +950,7 @@ export default function OrderForm({
{couponValidating ? __('Validating...') : __('Apply')}
</Button>
</div>
{/* Applied Coupons */}
{validatedCoupons.length > 0 && (
<div className="space-y-2">
@@ -982,7 +983,7 @@ export default function OrderForm({
))}
</div>
)}
<div className="text-[11px] opacity-70">
{__('Enter coupon code and click Apply to validate and calculate discount')}
</div>
@@ -1011,7 +1012,7 @@ export default function OrderForm({
onChange={async (val: string) => {
const customer = customers.find((c: any) => String(c.id) === val);
if (!customer) return;
// Fetch full customer data
try {
const data = await CustomersApi.searchByEmail(customer.email);
@@ -1021,7 +1022,7 @@ export default function OrderForm({
setBLast(data.billing.last_name || data.last_name || '');
setBEmail(data.email || '');
setBPhone(data.billing.phone || '');
// Only fill address fields if cart has physical products
if (hasPhysicalProduct) {
setBAddr1(data.billing.address_1 || '');
@@ -1029,7 +1030,7 @@ export default function OrderForm({
setBPost(data.billing.postcode || '');
setBCountry(data.billing.country || bCountry);
setBState(data.billing.state || '');
// Autofill shipping if available
if (data.shipping && data.shipping.address_1) {
setShipDiff(true);
@@ -1044,14 +1045,14 @@ export default function OrderForm({
});
}
}
// Mark customer as selected
setSelectedCustomerId(data.user_id);
}
} catch (e) {
console.error('Customer autofill error:', e);
}
setCustomerSearchQ('');
}}
onSearch={setCustomerSearchQ}
@@ -1063,40 +1064,40 @@ export default function OrderForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<Label>{__('First name')}</Label>
<Input className="rounded-md border px-3 py-2" value={bFirst} onChange={e=>setBFirst(e.target.value)} />
<Input className="rounded-md border px-3 py-2" value={bFirst} onChange={e => setBFirst(e.target.value)} />
</div>
<div>
<Label>{__('Last name')}</Label>
<Input className="rounded-md border px-3 py-2" value={bLast} onChange={e=>setBLast(e.target.value)} />
<Input className="rounded-md border px-3 py-2" value={bLast} onChange={e => setBLast(e.target.value)} />
</div>
<div>
<Label>{__('Email')}</Label>
<Input
inputMode="email"
autoComplete="email"
className="rounded-md border px-3 py-2 appearance-none"
value={bEmail}
onChange={e=>setBEmail(e.target.value)}
<Input
inputMode="email"
autoComplete="email"
className="rounded-md border px-3 py-2 appearance-none"
value={bEmail}
onChange={e => setBEmail(e.target.value)}
/>
</div>
<div>
<Label>{__('Phone')}</Label>
<Input className="rounded-md border px-3 py-2" value={bPhone} onChange={e=>setBPhone(e.target.value)} />
<Input className="rounded-md border px-3 py-2" value={bPhone} onChange={e => setBPhone(e.target.value)} />
</div>
{/* Only show full address fields for physical products */}
{hasPhysicalProduct && (
<>
<div className="md:col-span-2">
<Label>{__('Address')}</Label>
<Input className="rounded-md border px-3 py-2" value={bAddr1} onChange={e=>setBAddr1(e.target.value)} />
<Input className="rounded-md border px-3 py-2" value={bAddr1} onChange={e => setBAddr1(e.target.value)} />
</div>
<div>
<Label>{__('City')}</Label>
<Input className="rounded-md border px-3 py-2" value={bCity} onChange={e=>setBCity(e.target.value)} />
<Input className="rounded-md border px-3 py-2" value={bCity} onChange={e => setBCity(e.target.value)} />
</div>
<div>
<Label>{__('Postcode')}</Label>
<Input className="rounded-md border px-3 py-2" value={bPost} onChange={e=>setBPost(e.target.value)} />
<Input className="rounded-md border px-3 py-2" value={bPost} onChange={e => setBPost(e.target.value)} />
</div>
<div>
<Label>{__('Country')}</Label>
@@ -1137,7 +1138,7 @@ export default function OrderForm({
{hasPhysicalProduct && (
<div className="pt-2 mt-4">
<div className="flex items-center gap-2 text-sm">
<Checkbox id="shipDiff" checked={shipDiff} onCheckedChange={(v)=> setShipDiff(Boolean(v))} />
<Checkbox id="shipDiff" checked={shipDiff} onCheckedChange={(v) => setShipDiff(Boolean(v))} />
<Label htmlFor="shipDiff" className="leading-none">{__('Ship to a different address')}</Label>
</div>
</div>
@@ -1154,7 +1155,7 @@ export default function OrderForm({
.map((field: any) => {
const isWide = ['address_1', 'address_2'].includes(field.key.replace('shipping_', ''));
const fieldKey = field.key.replace('shipping_', '');
return (
<div key={field.key} className={isWide ? 'md:col-span-2' : ''}>
<Label>
@@ -1162,9 +1163,9 @@ export default function OrderForm({
{field.required && <span className="text-destructive ml-1">*</span>}
</Label>
{field.type === 'select' && field.options ? (
<Select
value={shippingData[fieldKey] || ''}
onValueChange={(v) => setShippingData({...shippingData, [fieldKey]: v})}
<Select
value={shippingData[fieldKey] || ''}
onValueChange={(v) => setShippingData({ ...shippingData, [fieldKey]: v })}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={field.placeholder || field.label} />
@@ -1179,14 +1180,14 @@ export default function OrderForm({
<SearchableSelect
options={countryOptions}
value={shippingData.country || ''}
onChange={(v) => setShippingData({...shippingData, country: v})}
onChange={(v) => setShippingData({ ...shippingData, country: v })}
placeholder={field.placeholder || __('Select country')}
disabled={oneCountryOnly}
/>
) : field.type === 'textarea' ? (
<Textarea
value={shippingData[fieldKey] || ''}
onChange={(e) => setShippingData({...shippingData, [fieldKey]: e.target.value})}
onChange={(e) => setShippingData({ ...shippingData, [fieldKey]: e.target.value })}
placeholder={field.placeholder}
required={field.required}
/>
@@ -1194,7 +1195,7 @@ export default function OrderForm({
<Input
type={field.type === 'email' ? 'email' : field.type === 'tel' ? 'tel' : 'text'}
value={shippingData[fieldKey] || ''}
onChange={(e) => setShippingData({...shippingData, [fieldKey]: e.target.value})}
onChange={(e) => setShippingData({ ...shippingData, [fieldKey]: e.target.value })}
placeholder={field.placeholder}
required={field.required}
/>
@@ -1281,7 +1282,7 @@ export default function OrderForm({
<div className="rounded border p-4 space-y-2">
<Label>{__('Customer note (optional)')}</Label>
<Textarea value={note} onChange={e=>setNote(e.target.value)} placeholder={__('Write a note for this order…')} />
<Textarea value={note} onChange={e => setNote(e.target.value)} placeholder={__('Write a note for this order…')} />
</div>
{!hideSubmitButton && (
@@ -1297,6 +1298,6 @@ export default function OrderForm({
function isEmptyAddress(a: any) {
if (!a) return true;
const keys = ['first_name','last_name','address_1','city','state','postcode','country'];
const keys = ['first_name', 'last_name', 'address_1', 'city', 'state', 'postcode', 'country'];
return keys.every(k => !a[k]);
}