feat: Hide drag handle on mobile + persist sort order to database
1. Hide Drag Handle on Mobile ✅ Problem: Drag handle looks messy on mobile Solution: Hide on mobile, show only on desktop Changes: - Added 'hidden md:block' to drag handle - Added 'md:pl-8' to content wrapper - Mobile: Clean list without drag handle - Desktop: Drag handle visible for sorting UX Priority: Better mobile experience > sorting on mobile 2. Persist Sort Order to Database ✅ Backend Implementation: A. New API Endpoint POST /woonoow/v1/payments/gateways/order Body: { category: 'manual'|'online', order: ['id1', 'id2'] } B. Save to WordPress Options - woonoow_payment_gateway_order_manual - woonoow_payment_gateway_order_online C. Load Order on Page Load GET /payments/gateways returns: { gateways: [...], order: { manual: ['bacs', 'cheque', 'cod'], online: ['paypal', 'stripe'] } } Frontend Implementation: A. Save on Drag End - Calls API immediately after reorder - Shows success toast - Reverts on error with error toast B. Load Saved Order - Extracts order from API response - Uses saved order if available - Falls back to gateway order if no saved order C. Error Handling - Try/catch on save - Revert order on failure - User feedback via toast 3. Flow Diagram Page Load: ┌─────────────────────────────────────┐ │ GET /payments/gateways │ ├─────────────────────────────────────┤ │ Returns: { gateways, order } │ │ - order.manual: ['bacs', 'cod'] │ │ - order.online: ['paypal'] │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Initialize State │ │ - setManualOrder(order.manual) │ │ - setOnlineOrder(order.online) │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Display Sorted List │ │ - useMemo sorts by saved order │ └─────────────────────────────────────┘ User Drags: ┌─────────────────────────────────────┐ │ User drags item │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ handleDragEnd │ │ - Calculate new order │ │ - Update state (optimistic) │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ POST /payments/gateways/order │ │ Body: { category, order } │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Success: Toast notification │ │ Error: Revert + error toast │ └─────────────────────────────────────┘ 4. Mobile vs Desktop Mobile (< 768px): ✅ Clean list without drag handle ✅ No left padding ✅ Better UX ❌ No sorting (desktop only) Desktop (≥ 768px): ✅ Drag handle visible ✅ Full sorting capability ✅ Visual feedback ✅ Keyboard accessible Benefits: ✅ Order persists across sessions ✅ Order persists across page reloads ✅ Clean mobile UI ✅ Full desktop functionality ✅ Error handling with rollback ✅ Optimistic UI updates Files Modified: - PaymentsController.php: New endpoint + load order - Payments.tsx: Save order + load order + mobile hide - Database: 2 new options for order storage
This commit is contained in:
@@ -74,10 +74,11 @@ function SortableGatewayItem({ gateway, children }: { gateway: PaymentGateway; c
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setNodeRef} style={style} className="relative">
|
<div ref={setNodeRef} style={style} className="relative">
|
||||||
<div className="absolute left-2 top-1/2 -translate-y-1/2 cursor-grab active:cursor-grabbing" {...attributes} {...listeners}>
|
{/* Drag handle - hidden on mobile for better UX */}
|
||||||
|
<div className="hidden md:block absolute left-2 top-1/2 -translate-y-1/2 cursor-grab active:cursor-grabbing" {...attributes} {...listeners}>
|
||||||
<GripVertical className="h-5 w-5 text-muted-foreground" />
|
<GripVertical className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-8">
|
<div className="md:pl-8">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,22 +103,36 @@ export default function PaymentsPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fetch all payment gateways
|
// Fetch all payment gateways
|
||||||
const { data: gateways = [], isLoading, refetch } = useQuery({
|
const { data: gatewayData, isLoading, refetch } = useQuery({
|
||||||
queryKey: ['payment-gateways'],
|
queryKey: ['payment-gateways'],
|
||||||
queryFn: () => api.get('/payments/gateways'),
|
queryFn: () => api.get('/payments/gateways'),
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize order from gateways
|
// Extract gateways and order from response
|
||||||
|
const gateways = gatewayData?.gateways || [];
|
||||||
|
const savedOrder = gatewayData?.order || { manual: [], online: [] };
|
||||||
|
|
||||||
|
// Initialize order from saved order or gateways
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (gateways.length > 0 && manualOrder.length === 0 && onlineOrder.length === 0) {
|
if (gateways.length > 0 && manualOrder.length === 0 && onlineOrder.length === 0) {
|
||||||
const manual = gateways.filter((g: PaymentGateway) => g.type === 'manual').map((g: PaymentGateway) => g.id);
|
// Use saved order if available, otherwise use gateway order
|
||||||
const online = gateways.filter((g: PaymentGateway) => g.type === 'provider' || g.type === 'other').map((g: PaymentGateway) => g.id);
|
if (savedOrder.manual.length > 0) {
|
||||||
setManualOrder(manual);
|
setManualOrder(savedOrder.manual);
|
||||||
setOnlineOrder(online);
|
} else {
|
||||||
|
const manual = gateways.filter((g: PaymentGateway) => g.type === 'manual').map((g: PaymentGateway) => g.id);
|
||||||
|
setManualOrder(manual);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedOrder.online.length > 0) {
|
||||||
|
setOnlineOrder(savedOrder.online);
|
||||||
|
} else {
|
||||||
|
const online = gateways.filter((g: PaymentGateway) => g.type === 'provider' || g.type === 'other').map((g: PaymentGateway) => g.id);
|
||||||
|
setOnlineOrder(online);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [gateways, manualOrder.length, onlineOrder.length]);
|
}, [gateways, savedOrder, manualOrder.length, onlineOrder.length]);
|
||||||
|
|
||||||
// Toggle gateway mutation
|
// Toggle gateway mutation
|
||||||
const toggleMutation = useMutation({
|
const toggleMutation = useMutation({
|
||||||
@@ -162,7 +177,7 @@ export default function PaymentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle drag end for manual gateways
|
// Handle drag end for manual gateways
|
||||||
const handleManualDragEnd = (event: DragEndEvent) => {
|
const handleManualDragEnd = async (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return;
|
||||||
|
|
||||||
@@ -172,12 +187,23 @@ export default function PaymentsPage() {
|
|||||||
const newOrder = arrayMove(manualOrder, oldIndex, newIndex);
|
const newOrder = arrayMove(manualOrder, oldIndex, newIndex);
|
||||||
setManualOrder(newOrder);
|
setManualOrder(newOrder);
|
||||||
|
|
||||||
// TODO: Save order to backend
|
// Save order to backend
|
||||||
toast.success('Payment methods reordered');
|
try {
|
||||||
|
await api.post('/payments/gateways/order', {
|
||||||
|
category: 'manual',
|
||||||
|
order: newOrder,
|
||||||
|
});
|
||||||
|
toast.success('Payment methods reordered');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save order:', error);
|
||||||
|
toast.error('Failed to save order');
|
||||||
|
// Revert order on error
|
||||||
|
setManualOrder(manualOrder);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle drag end for online gateways
|
// Handle drag end for online gateways
|
||||||
const handleOnlineDragEnd = (event: DragEndEvent) => {
|
const handleOnlineDragEnd = async (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return;
|
||||||
|
|
||||||
@@ -187,8 +213,19 @@ export default function PaymentsPage() {
|
|||||||
const newOrder = arrayMove(onlineOrder, oldIndex, newIndex);
|
const newOrder = arrayMove(onlineOrder, oldIndex, newIndex);
|
||||||
setOnlineOrder(newOrder);
|
setOnlineOrder(newOrder);
|
||||||
|
|
||||||
// TODO: Save order to backend
|
// Save order to backend
|
||||||
toast.success('Payment methods reordered');
|
try {
|
||||||
|
await api.post('/payments/gateways/order', {
|
||||||
|
category: 'online',
|
||||||
|
order: newOrder,
|
||||||
|
});
|
||||||
|
toast.success('Payment methods reordered');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save order:', error);
|
||||||
|
toast.error('Failed to save order');
|
||||||
|
// Revert order on error
|
||||||
|
setOnlineOrder(onlineOrder);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveGateway = async (settings: Record<string, unknown>) => {
|
const handleSaveGateway = async (settings: Record<string, unknown>) => {
|
||||||
|
|||||||
@@ -94,6 +94,31 @@ class PaymentsController extends WP_REST_Controller {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// POST /woonoow/v1/payments/gateways/order
|
||||||
|
register_rest_route($this->namespace, '/' . $this->rest_base . '/gateways/order', [
|
||||||
|
[
|
||||||
|
'methods' => WP_REST_Server::EDITABLE,
|
||||||
|
'callback' => [$this, 'save_gateway_order'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
'args' => [
|
||||||
|
'category' => [
|
||||||
|
'description' => 'Gateway category (manual or online)',
|
||||||
|
'type' => 'string',
|
||||||
|
'required' => true,
|
||||||
|
'enum' => ['manual', 'online'],
|
||||||
|
],
|
||||||
|
'order' => [
|
||||||
|
'description' => 'Array of gateway IDs in desired order',
|
||||||
|
'type' => 'array',
|
||||||
|
'required' => true,
|
||||||
|
'items' => [
|
||||||
|
'type' => 'string',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +131,17 @@ class PaymentsController extends WP_REST_Controller {
|
|||||||
try {
|
try {
|
||||||
$gateways = PaymentGatewaysProvider::get_gateways();
|
$gateways = PaymentGatewaysProvider::get_gateways();
|
||||||
|
|
||||||
$response = rest_ensure_response($gateways);
|
// Get saved order
|
||||||
|
$manual_order = get_option('woonoow_payment_gateway_order_manual', []);
|
||||||
|
$online_order = get_option('woonoow_payment_gateway_order_online', []);
|
||||||
|
|
||||||
|
$response = rest_ensure_response([
|
||||||
|
'gateways' => $gateways,
|
||||||
|
'order' => [
|
||||||
|
'manual' => $manual_order,
|
||||||
|
'online' => $online_order,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
// Cache for 5 minutes
|
// Cache for 5 minutes
|
||||||
$response->header('Cache-Control', 'max-age=300');
|
$response->header('Cache-Control', 'max-age=300');
|
||||||
@@ -265,6 +300,48 @@ class PaymentsController extends WP_REST_Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save gateway order
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request object
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error
|
||||||
|
*/
|
||||||
|
public function save_gateway_order(WP_REST_Request $request) {
|
||||||
|
$category = $request->get_param('category');
|
||||||
|
$order = $request->get_param('order');
|
||||||
|
|
||||||
|
// Validate category
|
||||||
|
if (!in_array($category, ['manual', 'online'], true)) {
|
||||||
|
return new WP_Error(
|
||||||
|
'invalid_category',
|
||||||
|
'Category must be either "manual" or "online"',
|
||||||
|
['status' => 400]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate order is array
|
||||||
|
if (!is_array($order)) {
|
||||||
|
return new WP_Error(
|
||||||
|
'invalid_order',
|
||||||
|
'Order must be an array of gateway IDs',
|
||||||
|
['status' => 400]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to WordPress options
|
||||||
|
$option_key = 'woonoow_payment_gateway_order_' . $category;
|
||||||
|
update_option($option_key, $order, false);
|
||||||
|
|
||||||
|
error_log(sprintf('[WooNooW] Saved %s gateway order: %s', $category, implode(', ', $order)));
|
||||||
|
|
||||||
|
return rest_ensure_response([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Gateway order saved successfully',
|
||||||
|
'category' => $category,
|
||||||
|
'order' => $order,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check permission
|
* Check permission
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user