From 52f7c1b99d5bc5243c6342538b4f07b236a94cff Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 6 Nov 2025 13:59:37 +0700 Subject: [PATCH] feat: Hide drag handle on mobile + persist sort order to database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- admin-spa/src/routes/Settings/Payments.tsx | 67 ++++++++++++++---- includes/Api/PaymentsController.php | 79 +++++++++++++++++++++- 2 files changed, 130 insertions(+), 16 deletions(-) diff --git a/admin-spa/src/routes/Settings/Payments.tsx b/admin-spa/src/routes/Settings/Payments.tsx index 796a977..a393f60 100644 --- a/admin-spa/src/routes/Settings/Payments.tsx +++ b/admin-spa/src/routes/Settings/Payments.tsx @@ -74,10 +74,11 @@ function SortableGatewayItem({ gateway, children }: { gateway: PaymentGateway; c return (
-
+ {/* Drag handle - hidden on mobile for better UX */} +
-
+
{children}
@@ -102,22 +103,36 @@ export default function PaymentsPage() { ); // Fetch all payment gateways - const { data: gateways = [], isLoading, refetch } = useQuery({ + const { data: gatewayData, isLoading, refetch } = useQuery({ queryKey: ['payment-gateways'], queryFn: () => api.get('/payments/gateways'), refetchOnWindowFocus: true, 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(() => { if (gateways.length > 0 && manualOrder.length === 0 && onlineOrder.length === 0) { - const manual = gateways.filter((g: PaymentGateway) => g.type === 'manual').map((g: PaymentGateway) => g.id); - const online = gateways.filter((g: PaymentGateway) => g.type === 'provider' || g.type === 'other').map((g: PaymentGateway) => g.id); - setManualOrder(manual); - setOnlineOrder(online); + // Use saved order if available, otherwise use gateway order + if (savedOrder.manual.length > 0) { + setManualOrder(savedOrder.manual); + } 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 const toggleMutation = useMutation({ @@ -162,7 +177,7 @@ export default function PaymentsPage() { }; // Handle drag end for manual gateways - const handleManualDragEnd = (event: DragEndEvent) => { + const handleManualDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; @@ -172,12 +187,23 @@ export default function PaymentsPage() { const newOrder = arrayMove(manualOrder, oldIndex, newIndex); setManualOrder(newOrder); - // TODO: Save order to backend - toast.success('Payment methods reordered'); + // Save order to backend + 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 - const handleOnlineDragEnd = (event: DragEndEvent) => { + const handleOnlineDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; @@ -187,8 +213,19 @@ export default function PaymentsPage() { const newOrder = arrayMove(onlineOrder, oldIndex, newIndex); setOnlineOrder(newOrder); - // TODO: Save order to backend - toast.success('Payment methods reordered'); + // Save order to backend + 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) => { diff --git a/includes/Api/PaymentsController.php b/includes/Api/PaymentsController.php index 823ea93..a4a507c 100644 --- a/includes/Api/PaymentsController.php +++ b/includes/Api/PaymentsController.php @@ -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 { $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 $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 *