fix: Modal initial values + sticky footer + HTML descriptions

 Issue 1: Modal Not Showing Current Values (FIXED!)
Problem: Opening modal showed defaults, not current saved values
Root Cause: Backend only sent field.default, not current value
Solution:
- Backend: Added field.value with current saved value
- normalize_field() now includes: value: $current_settings[$key]
- Frontend: Use field.value ?? field.default for initial data
- GenericGatewayForm initializes with current values

Result:  Modal now shows "BNI Virtual Account 2" not "BNI Virtual Account"

 Issue 2: Sticky Modal Footer (FIXED!)
Problem: Footer scrolls away with long forms
Solution:
- Restructured modal: header + scrollable body + sticky footer
- DialogContent: flex flex-col with overflow on body only
- Footer: sticky bottom-0 with border-t
- Save button triggers form.requestSubmit()

Result:  Cancel, View in WooCommerce, Save always visible

 Issue 3: HTML in Descriptions (FIXED!)
Problem: TriPay icon shows as raw HTML string
Solution:
- Changed: {field.description}
- To: dangerouslySetInnerHTML={{ __html: field.description }}
- Respects vendor creativity (images, formatting, links)

Result:  TriPay icon image renders properly

📋 Technical Details:

Backend Changes (PaymentGatewaysProvider.php):
- get_gateway_settings() passes $current_settings to extractors
- normalize_field() adds 'value' => $current_settings[$key]
- All fields now have both default and current value

Frontend Changes:
- GatewayField interface: Added value?: string | boolean
- GenericGatewayForm: Initialize with field.value
- Modal structure: Header + Body (scroll) + Footer (sticky)
- Descriptions: Render as HTML with dangerouslySetInnerHTML

Files Modified:
- PaymentGatewaysProvider.php: Add current values to fields
- Payments.tsx: Restructure modal layout + add value to interface
- GenericGatewayForm.tsx: Use field.value + sticky footer + HTML descriptions

🎯 Result:
 Modal shows current saved values
 Footer always visible (no scrolling)
 Vendor HTML/images render properly
This commit is contained in:
dwindown
2025-11-05 23:52:57 +07:00
parent b578dfaeb0
commit 96f0482cfb
3 changed files with 84 additions and 43 deletions

View File

@@ -21,6 +21,7 @@ interface GatewayField {
title: string;
description: string;
default: string | boolean;
value?: string | boolean; // Current saved value from backend
placeholder?: string;
required: boolean;
options?: Record<string, string>;
@@ -56,6 +57,26 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
const [isSaving, setIsSaving] = useState(false);
const [unsupportedFields, setUnsupportedFields] = useState<string[]>([]);
// Initialize form data with current gateway values
React.useEffect(() => {
const initialData: Record<string, unknown> = {};
const categories: Record<string, GatewayField>[] = [
gateway.settings.basic,
gateway.settings.api,
gateway.settings.advanced,
];
categories.forEach((category) => {
Object.values(category).forEach((field) => {
// Use current value from field (backend sends this now!)
initialData[field.id] = field.value ?? field.default;
});
});
setFormData(initialData);
}, [gateway]);
// Check for unsupported fields
React.useEffect(() => {
const unsupported: string[] = [];
@@ -121,7 +142,10 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
{field.required && <span className="text-destructive ml-1">*</span>}
</Label>
{field.description && (
<p className="text-sm text-muted-foreground">{field.description}</p>
<p
className="text-sm text-muted-foreground"
dangerouslySetInnerHTML={{ __html: field.description }}
/>
)}
</div>
</div>
@@ -225,25 +249,26 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
const useMultiPage = totalFields >= 20;
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Warning for unsupported fields */}
{unsupportedFields.length > 0 && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Some advanced settings are not supported in this interface.{' '}
<a
href={gateway.wc_settings_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium underline"
>
Configure in WooCommerce
<ExternalLink className="h-3 w-3" />
</a>
</AlertDescription>
</Alert>
)}
<>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Warning for unsupported fields */}
{unsupportedFields.length > 0 && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Some advanced settings are not supported in this interface.{' '}
<a
href={gateway.wc_settings_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium underline"
>
Configure in WooCommerce
<ExternalLink className="h-3 w-3" />
</a>
</AlertDescription>
</Alert>
)}
{useMultiPage ? (
<Tabs defaultValue="basic" className="w-full">
@@ -295,9 +320,10 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
)}
</div>
)}
</form>
{/* Actions */}
<div className="flex items-center justify-between pt-4 border-t">
{/* Sticky Footer */}
<div className="sticky bottom-0 bg-background border-t px-6 py-4 flex items-center justify-between mt-6">
<Button
type="button"
variant="outline"
@@ -324,11 +350,18 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
</a>
</Button>
<Button type="submit" disabled={isSaving}>
<Button
onClick={(e) => {
e.preventDefault();
const form = document.querySelector('form');
if (form) form.requestSubmit();
}}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
</form>
</>
);
}

View File

@@ -18,6 +18,7 @@ interface GatewayField {
title: string;
description: string;
default: string | boolean;
value: string | boolean; // Current saved value
placeholder?: string;
required: boolean;
options?: Record<string, string>;
@@ -327,15 +328,17 @@ export default function PaymentsPage() {
{/* Gateway Settings Modal */}
{selectedGateway && (
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col p-0">
<DialogHeader className="px-6 pt-6 pb-4 border-b">
<DialogTitle>{selectedGateway.title} Settings</DialogTitle>
</DialogHeader>
<GenericGatewayForm
gateway={selectedGateway}
onSave={handleSaveGateway}
onCancel={() => setIsModalOpen(false)}
/>
<div className="flex-1 overflow-y-auto px-6 py-4">
<GenericGatewayForm
gateway={selectedGateway}
onSave={handleSaveGateway}
onCancel={() => setIsModalOpen(false)}
/>
</div>
</DialogContent>
</Dialog>
)}