From b57a23ffbd715b6839bf425367126abc8083aca0 Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 6 Nov 2025 13:50:33 +0700 Subject: [PATCH] feat: Implement drag-and-drop sorting for payment methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented sortable payment gateways using @dnd-kit Features: ✅ Drag-and-drop for Manual Payment Methods ✅ Drag-and-drop for Online Payment Methods ✅ Visual drag handle (GripVertical icon) ✅ Smooth animations during drag ✅ Separate sorting for each category ✅ Order persists in component state ✅ Toast notification on reorder UI Changes: - Added drag handle on left side of each gateway card - Cursor changes to grab/grabbing during drag - Dragged item becomes semi-transparent (50% opacity) - Smooth transitions between positions Implementation: 1. DnD Context Setup - PointerSensor for mouse/touch - KeyboardSensor for accessibility - closestCenter collision detection 2. Sortable Items - SortableGatewayItem wrapper component - Handles drag attributes and listeners - Applies transform and transition styles 3. State Management - manualOrder: Array of manual gateway IDs - onlineOrder: Array of online gateway IDs - Initialized from gateways on mount - Updated on drag end 4. Sorting Logic - useMemo to sort gateways by custom order - arrayMove from @dnd-kit/sortable - Separate handlers for each category 5. Visual Feedback - GripVertical icon (left side, 8px from edge) - Opacity 0.5 when dragging - Smooth CSS transitions - Cursor: grab/grabbing TODO (Backend): - Save order to WordPress options - Load order on page load - API endpoint: POST /payments/gateways/order Benefits: ✅ Better UX for organizing payment methods ✅ Visual feedback during drag ✅ Accessible (keyboard support) ✅ Separate sorting per category ✅ No page reload needed Files Modified: - Payments.tsx: DnD implementation - package.json: @dnd-kit dependencies (already installed) --- admin-spa/src/routes/Settings/Payments.tsx | 256 +++++++++++++-------- 1 file changed, 161 insertions(+), 95 deletions(-) diff --git a/admin-spa/src/routes/Settings/Payments.tsx b/admin-spa/src/routes/Settings/Payments.tsx index 298a7d3..796a977 100644 --- a/admin-spa/src/routes/Settings/Payments.tsx +++ b/admin-spa/src/routes/Settings/Payments.tsx @@ -109,6 +109,16 @@ export default function PaymentsPage() { staleTime: 5 * 60 * 1000, // 5 minutes }); + // Initialize order from 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); + } + }, [gateways, manualOrder.length, onlineOrder.length]); + // Toggle gateway mutation const toggleMutation = useMutation({ mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) => { @@ -151,15 +161,65 @@ export default function PaymentsPage() { setIsModalOpen(true); }; + // Handle drag end for manual gateways + const handleManualDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const oldIndex = manualOrder.indexOf(active.id as string); + const newIndex = manualOrder.indexOf(over.id as string); + + const newOrder = arrayMove(manualOrder, oldIndex, newIndex); + setManualOrder(newOrder); + + // TODO: Save order to backend + toast.success('Payment methods reordered'); + }; + + // Handle drag end for online gateways + const handleOnlineDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const oldIndex = onlineOrder.indexOf(active.id as string); + const newIndex = onlineOrder.indexOf(over.id as string); + + const newOrder = arrayMove(onlineOrder, oldIndex, newIndex); + setOnlineOrder(newOrder); + + // TODO: Save order to backend + toast.success('Payment methods reordered'); + }; + const handleSaveGateway = async (settings: Record) => { if (!selectedGateway) return; await saveMutation.mutateAsync({ id: selectedGateway.id, settings }); }; - // Categorize gateways - const manualGateways = gateways.filter((g: PaymentGateway) => g.type === 'manual'); - // Combine provider and other into single "3rd party" category - const thirdPartyGateways = gateways.filter((g: PaymentGateway) => g.type === 'provider' || g.type === 'other'); + // Separate and sort gateways by type and custom order + const manualGateways = React.useMemo(() => { + const manual = gateways.filter((g: PaymentGateway) => g.type === 'manual'); + if (manualOrder.length === 0) return manual; + return [...manual].sort((a, b) => { + const indexA = manualOrder.indexOf(a.id); + const indexB = manualOrder.indexOf(b.id); + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + }, [gateways, manualOrder]); + + const thirdPartyGateways = React.useMemo(() => { + const online = gateways.filter((g: PaymentGateway) => g.type === 'provider' || g.type === 'other'); + if (onlineOrder.length === 0) return online; + return [...online].sort((a, b) => { + const indexA = onlineOrder.indexOf(a.id); + const indexB = onlineOrder.indexOf(b.id); + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + }, [gateways, onlineOrder]); if (isLoading) { return ( @@ -197,48 +257,51 @@ export default function PaymentsPage() { {manualGateways.length === 0 ? (

No manual payment methods available

) : ( -
- {manualGateways.map((gateway: PaymentGateway) => ( -
-
-
-
- + + g.id)} strategy={verticalListSortingStrategy}> +
+ {manualGateways.map((gateway: PaymentGateway) => ( + +
+
+
+
+ +
+
+

{gateway.method_title || gateway.title}

+ {gateway.description && ( +

+ {gateway.description} +

+ )} +
+
+
+ {gateway.enabled && ( + + )} + handleToggle(gateway.id, checked)} + disabled={togglingGateway === gateway.id} + /> +
+
-
-

{gateway.method_title || gateway.title}

- {gateway.description && ( -

- {gateway.description} -

- )} -
-
-
- {gateway.enabled && ( - - )} - handleToggle(gateway.id, checked)} - disabled={togglingGateway === gateway.id} - /> -
-
+ + ))}
- ))} -
+ + )} @@ -248,60 +311,63 @@ export default function PaymentsPage() { title="Online Payment Methods" description="Accept credit cards, digital wallets, and other online payments" > -
- {thirdPartyGateways.map((gateway: PaymentGateway) => ( -
-
-
-
- -
-
-
-

- {gateway.method_title || gateway.title} -

+ + g.id)} strategy={verticalListSortingStrategy}> +
+ {thirdPartyGateways.map((gateway: PaymentGateway) => ( + +
+
+
+
+ +
+
+
+

+ {gateway.method_title || gateway.title} +

+
+ {gateway.method_description && ( +

+ {gateway.method_description} +

+ )} + {!gateway.requirements.met && ( + + + + Requirements not met: {gateway.requirements.missing.join(', ')} + + + )} +
+
+
+ {gateway.enabled && ( + + )} + handleToggle(gateway.id, checked)} + disabled={!gateway.requirements.met || togglingGateway === gateway.id} + /> +
- {gateway.method_description && ( -

- {gateway.method_description} -

- )} - {!gateway.requirements.met && ( - - - - Requirements not met: {gateway.requirements.missing.join(', ')} - - - )}
-
-
- {gateway.enabled && ( - - )} - handleToggle(gateway.id, checked)} - disabled={!gateway.requirements.met || togglingGateway === gateway.id} - /> -
-
+ + ))}
- ))} -
+ + )}