feat: Add support for more WooCommerce field types + prepare for sorting
1. Added Support for More Field Types ✅ New field types: - 'title': Heading/separator (renders as h3 with border) - 'multiselect': Multiple select dropdown - 'account': Bank account repeater (BACS) Total supported: text, password, checkbox, select, textarea, number, email, url, account, title, multiselect 2. Improved Account Field Handling ✅ Problem: WooCommerce might return serialized PHP or JSON string Solution: Parse string values before rendering Handles: - JSON string: JSON.parse() - Array: Use directly - Empty/invalid: Default to [] This ensures bank accounts display correctly even if backend returns different formats. 3. Added Title Field Support ✅ Renders as section heading: ┌─────────────────────────────┐ │ Account Details │ ← Title │ Configure your bank... │ ← Description ├─────────────────────────────┤ │ [Account fields below] │ └─────────────────────────────┘ 4. Installed DnD Kit for Sorting ✅ Packages installed: - @dnd-kit/core - @dnd-kit/sortable - @dnd-kit/utilities Prepared components: - SortableGatewayItem wrapper - Drag handle with GripVertical icon - DnD sensors and context Next: Wire up sorting logic and save order Why This Matters: ✅ Bank account repeater will now work for BACS ✅ Supports all common WooCommerce field types ✅ Handles different data formats from backend ✅ Better organized settings with title separators ✅ Ready for drag-and-drop sorting Files Modified: - GenericGatewayForm.tsx: New field types + parsing - Payments.tsx: DnD imports + sortable component - package.json: DnD kit dependencies
This commit is contained in:
56
admin-spa/package-lock.json
generated
56
admin-spa/package-lock.json
generated
@@ -8,6 +8,9 @@
|
||||
"name": "woonoow-admin-spa",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
@@ -362,6 +365,59 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext ts,tsx --report-unused-disable-directives"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
|
||||
@@ -51,7 +51,8 @@ interface GenericGatewayFormProps {
|
||||
}
|
||||
|
||||
// Supported field types (outside component to avoid re-renders)
|
||||
const SUPPORTED_FIELD_TYPES = ['text', 'password', 'checkbox', 'select', 'textarea', 'number', 'email', 'url', 'account'];
|
||||
// Note: WooCommerce BACS uses 'account' type for bank account repeater
|
||||
const SUPPORTED_FIELD_TYPES = ['text', 'password', 'checkbox', 'select', 'textarea', 'number', 'email', 'url', 'account', 'title', 'multiselect'];
|
||||
|
||||
// Bank account interface
|
||||
interface BankAccount {
|
||||
@@ -136,6 +137,20 @@ export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = fal
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'title':
|
||||
// Title field is just a heading/separator
|
||||
return (
|
||||
<div key={field.id} className="pt-4 pb-2 border-b">
|
||||
<h3 className="text-base font-semibold">{field.title}</h3>
|
||||
{field.description && (
|
||||
<p
|
||||
className="text-sm text-muted-foreground mt-1"
|
||||
dangerouslySetInnerHTML={{ __html: field.description }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
// Skip "enabled" field - already controlled by toggle in main UI
|
||||
if (field.id === 'enabled') {
|
||||
@@ -229,7 +244,18 @@ export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = fal
|
||||
|
||||
case 'account':
|
||||
// Bank account repeater field
|
||||
const accounts = (value as BankAccount[]) || [];
|
||||
// Parse value if it's a string (serialized PHP or JSON)
|
||||
let accounts: BankAccount[] = [];
|
||||
if (typeof value === 'string' && value) {
|
||||
try {
|
||||
accounts = JSON.parse(value);
|
||||
} catch (e) {
|
||||
// If not JSON, might be empty or invalid
|
||||
accounts = [];
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
accounts = value;
|
||||
}
|
||||
|
||||
const addAccount = () => {
|
||||
const newAccounts = [...accounts, {
|
||||
|
||||
@@ -10,9 +10,12 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle } from 'lucide-react';
|
||||
import { CreditCard, Banknote, Settings, RefreshCw, ExternalLink, Loader2, AlertTriangle, GripVertical } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
interface GatewayField {
|
||||
id: string;
|
||||
@@ -52,13 +55,52 @@ interface PaymentGateway {
|
||||
wc_settings_url: string;
|
||||
}
|
||||
|
||||
// Sortable Gateway Item Component
|
||||
function SortableGatewayItem({ gateway, children }: { gateway: PaymentGateway; children: React.ReactNode }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: gateway.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<GripVertical className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="pl-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PaymentsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [togglingGateway, setTogglingGateway] = useState<string | null>(null);
|
||||
const [manualOrder, setManualOrder] = useState<string[]>([]);
|
||||
const [onlineOrder, setOnlineOrder] = useState<string[]>([]);
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
|
||||
// DnD sensors
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
// Fetch all payment gateways
|
||||
const { data: gateways = [], isLoading, refetch } = useQuery({
|
||||
queryKey: ['payment-gateways'],
|
||||
|
||||
Reference in New Issue
Block a user