fix: Mobile accordion + deduplicate shipping methods
Fixes: ✅ Issue #2: Mobile drawer now uses accordion (no nested modals) ✅ Issue #3: Duplicate "Local pickup" - now shows as: - Local pickup - Local pickup (local_pickup_plus) Changes: - Mobile drawer matches desktop accordion pattern - Smaller text/spacing for mobile - Deduplication logic in backend API - Adds method ID suffix for duplicate titles Result: ✅ No modal-over-modal on any device ✅ Consistent UX desktop/mobile ✅ Clear distinction between similar methods
This commit is contained in:
@@ -502,19 +502,25 @@ export default function ShippingPage() {
|
|||||||
{__('Add Delivery Option')}
|
{__('Add Delivery Option')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Delivery Options List */}
|
{/* Delivery Options Accordion (Mobile) */}
|
||||||
<div className="space-y-2">
|
<Accordion type="single" collapsible value={expandedMethod} onValueChange={(value) => {
|
||||||
|
setExpandedMethod(value);
|
||||||
|
if (value) {
|
||||||
|
const instanceId = parseInt(value);
|
||||||
|
fetchMethodSettings(instanceId);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
{selectedZone.rates?.map((rate: any) => (
|
{selectedZone.rates?.map((rate: any) => (
|
||||||
<div key={rate.id} className="border rounded-lg p-3">
|
<AccordionItem key={rate.id} value={String(rate.instance_id)} className="border rounded-lg mb-2">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-center justify-between pr-2">
|
||||||
<div className="flex-1 min-w-0">
|
<AccordionTrigger className="flex-1 hover:no-underline px-3 py-2">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<Truck className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
<Truck className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||||
<span className="font-medium text-sm line-clamp-1" dangerouslySetInnerHTML={{ __html: rate.name }} />
|
<div className="flex-1 text-left min-w-0">
|
||||||
</div>
|
<div className="font-medium text-sm line-clamp-1" dangerouslySetInnerHTML={{ __html: rate.name }} />
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
||||||
<span className="font-semibold" dangerouslySetInnerHTML={{ __html: rate.price }} />
|
<span className="font-semibold" dangerouslySetInnerHTML={{ __html: rate.price }} />
|
||||||
<span className={`px-2 py-0.5 rounded-full whitespace-nowrap ${
|
<span className={`px-1.5 py-0.5 rounded-full whitespace-nowrap ${
|
||||||
rate.enabled
|
rate.enabled
|
||||||
? 'bg-green-100 text-green-700'
|
? 'bg-green-100 text-green-700'
|
||||||
: 'bg-gray-100 text-gray-600'
|
: 'bg-gray-100 text-gray-600'
|
||||||
@@ -523,32 +529,110 @@ export default function ShippingPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-7 w-7 p-0"
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
setEditingMethod(rate);
|
e.stopPropagation();
|
||||||
setShowEditDialog(true);
|
handleDeleteMethod(rate.instance_id);
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<Edit className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
onClick={() => handleDeleteMethod(rate.instance_id)}
|
|
||||||
disabled={deleteMethodMutation.isPending}
|
disabled={deleteMethodMutation.isPending}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
<Trash2 className="h-3 w-3 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<AccordionContent className="px-3 pb-3">
|
||||||
|
{methodSettings[rate.instance_id] ? (
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium mb-1.5 block">
|
||||||
|
{__('Display Name')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-2.5 py-1.5 text-sm border rounded-md"
|
||||||
|
defaultValue={methodSettings[rate.instance_id].settings?.title?.value || ''}
|
||||||
|
id={`method-title-mobile-${rate.instance_id}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{methodSettings[rate.instance_id].settings?.cost && (
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium mb-1.5 block">
|
||||||
|
{methodSettings[rate.instance_id].settings.cost.title || __('Cost')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-2.5 py-1.5 text-sm border rounded-md"
|
||||||
|
defaultValue={methodSettings[rate.instance_id].settings.cost.value || ''}
|
||||||
|
id={`method-cost-mobile-${rate.instance_id}`}
|
||||||
|
placeholder={methodSettings[rate.instance_id].settings.cost.placeholder || '0'}
|
||||||
|
/>
|
||||||
|
{methodSettings[rate.instance_id].settings.cost.description && (
|
||||||
|
<p
|
||||||
|
className="text-xs text-muted-foreground mt-1"
|
||||||
|
dangerouslySetInnerHTML={{ __html: methodSettings[rate.instance_id].settings.cost.description }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{methodSettings[rate.instance_id].settings?.min_amount && (
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium mb-1.5 block">
|
||||||
|
{methodSettings[rate.instance_id].settings.min_amount.title || __('Minimum Order')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="w-full px-2.5 py-1.5 text-sm border rounded-md"
|
||||||
|
defaultValue={methodSettings[rate.instance_id].settings.min_amount.value || ''}
|
||||||
|
id={`method-min-amount-mobile-${rate.instance_id}`}
|
||||||
|
placeholder={methodSettings[rate.instance_id].settings.min_amount.placeholder || '0'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setExpandedMethod('')}
|
||||||
|
>
|
||||||
|
{__('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const settings: any = {};
|
||||||
|
const titleInput = document.getElementById(`method-title-mobile-${rate.instance_id}`) as HTMLInputElement;
|
||||||
|
const costInput = document.getElementById(`method-cost-mobile-${rate.instance_id}`) as HTMLInputElement;
|
||||||
|
const minAmountInput = document.getElementById(`method-min-amount-mobile-${rate.instance_id}`) as HTMLInputElement;
|
||||||
|
|
||||||
|
if (titleInput) settings.title = titleInput.value;
|
||||||
|
if (costInput) settings.cost = costInput.value;
|
||||||
|
if (minAmountInput) settings.min_amount = minAmountInput.value;
|
||||||
|
|
||||||
|
updateSettingsMutation.mutate({
|
||||||
|
zoneId: selectedZone.id,
|
||||||
|
instanceId: rate.instance_id,
|
||||||
|
settings
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={updateSettingsMutation.isPending}
|
||||||
|
>
|
||||||
|
{updateSettingsMutation.isPending ? __('Saving...') : __('Save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center py-3">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-4 border-t flex flex-col gap-2">
|
<div className="px-4 py-4 border-t flex flex-col gap-2">
|
||||||
|
|||||||
@@ -398,10 +398,21 @@ class ShippingController extends WP_REST_Controller {
|
|||||||
$methods = $shipping->get_shipping_methods();
|
$methods = $shipping->get_shipping_methods();
|
||||||
|
|
||||||
$available = array();
|
$available = array();
|
||||||
|
$seen_titles = array();
|
||||||
|
|
||||||
foreach ( $methods as $method ) {
|
foreach ( $methods as $method ) {
|
||||||
|
$title = $method->get_method_title();
|
||||||
|
$id = $method->id;
|
||||||
|
|
||||||
|
// Deduplicate by adding ID suffix if title already exists
|
||||||
|
if ( isset( $seen_titles[ $title ] ) ) {
|
||||||
|
$title .= ' (' . $id . ')';
|
||||||
|
}
|
||||||
|
$seen_titles[ $title ] = true;
|
||||||
|
|
||||||
$available[] = array(
|
$available[] = array(
|
||||||
'id' => $method->id,
|
'id' => $id,
|
||||||
'title' => $method->get_method_title(),
|
'title' => $title,
|
||||||
'description' => $method->get_method_description(),
|
'description' => $method->get_method_description(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user