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:
@@ -21,6 +21,7 @@ interface GatewayField {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
default: string | boolean;
|
default: string | boolean;
|
||||||
|
value?: string | boolean; // Current saved value from backend
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
options?: Record<string, string>;
|
options?: Record<string, string>;
|
||||||
@@ -56,6 +57,26 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [unsupportedFields, setUnsupportedFields] = useState<string[]>([]);
|
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
|
// Check for unsupported fields
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const unsupported: string[] = [];
|
const unsupported: string[] = [];
|
||||||
@@ -121,7 +142,10 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
|
|||||||
{field.required && <span className="text-destructive ml-1">*</span>}
|
{field.required && <span className="text-destructive ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
{field.description && (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,25 +249,26 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
|
|||||||
const useMultiPage = totalFields >= 20;
|
const useMultiPage = totalFields >= 20;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<>
|
||||||
{/* Warning for unsupported fields */}
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{unsupportedFields.length > 0 && (
|
{/* Warning for unsupported fields */}
|
||||||
<Alert>
|
{unsupportedFields.length > 0 && (
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertTriangle className="h-4 w-4" />
|
||||||
Some advanced settings are not supported in this interface.{' '}
|
<AlertDescription>
|
||||||
<a
|
Some advanced settings are not supported in this interface.{' '}
|
||||||
href={gateway.wc_settings_url}
|
<a
|
||||||
target="_blank"
|
href={gateway.wc_settings_url}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="inline-flex items-center gap-1 font-medium underline"
|
rel="noopener noreferrer"
|
||||||
>
|
className="inline-flex items-center gap-1 font-medium underline"
|
||||||
Configure in WooCommerce
|
>
|
||||||
<ExternalLink className="h-3 w-3" />
|
Configure in WooCommerce
|
||||||
</a>
|
<ExternalLink className="h-3 w-3" />
|
||||||
</AlertDescription>
|
</a>
|
||||||
</Alert>
|
</AlertDescription>
|
||||||
)}
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{useMultiPage ? (
|
{useMultiPage ? (
|
||||||
<Tabs defaultValue="basic" className="w-full">
|
<Tabs defaultValue="basic" className="w-full">
|
||||||
@@ -295,9 +320,10 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</form>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Sticky Footer */}
|
||||||
<div className="flex items-center justify-between pt-4 border-t">
|
<div className="sticky bottom-0 bg-background border-t px-6 py-4 flex items-center justify-between mt-6">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -324,11 +350,18 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
|
|||||||
</a>
|
</a>
|
||||||
</Button>
|
</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'}
|
{isSaving ? 'Saving...' : 'Save Settings'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface GatewayField {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
default: string | boolean;
|
default: string | boolean;
|
||||||
|
value: string | boolean; // Current saved value
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
options?: Record<string, string>;
|
options?: Record<string, string>;
|
||||||
@@ -327,15 +328,17 @@ export default function PaymentsPage() {
|
|||||||
{/* Gateway Settings Modal */}
|
{/* Gateway Settings Modal */}
|
||||||
{selectedGateway && (
|
{selectedGateway && (
|
||||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col p-0">
|
||||||
<DialogHeader>
|
<DialogHeader className="px-6 pt-6 pb-4 border-b">
|
||||||
<DialogTitle>{selectedGateway.title} Settings</DialogTitle>
|
<DialogTitle>{selectedGateway.title} Settings</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<GenericGatewayForm
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
gateway={selectedGateway}
|
<GenericGatewayForm
|
||||||
onSave={handleSaveGateway}
|
gateway={selectedGateway}
|
||||||
onCancel={() => setIsModalOpen(false)}
|
onSave={handleSaveGateway}
|
||||||
/>
|
onCancel={() => setIsModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -194,11 +194,12 @@ class PaymentGatewaysProvider {
|
|||||||
*/
|
*/
|
||||||
private static function get_gateway_settings(WC_Payment_Gateway $gateway): array {
|
private static function get_gateway_settings(WC_Payment_Gateway $gateway): array {
|
||||||
$form_fields = $gateway->get_form_fields();
|
$form_fields = $gateway->get_form_fields();
|
||||||
|
$current_settings = $gateway->settings; // Get current saved values
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'basic' => self::extract_basic_fields($form_fields),
|
'basic' => self::extract_basic_fields($form_fields, $current_settings),
|
||||||
'api' => self::extract_api_fields($form_fields),
|
'api' => self::extract_api_fields($form_fields, $current_settings),
|
||||||
'advanced' => self::extract_advanced_fields($form_fields),
|
'advanced' => self::extract_advanced_fields($form_fields, $current_settings),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +209,9 @@ class PaymentGatewaysProvider {
|
|||||||
* @param array $form_fields All form fields
|
* @param array $form_fields All form fields
|
||||||
* @return array Basic fields
|
* @return array Basic fields
|
||||||
*/
|
*/
|
||||||
private static function extract_basic_fields(array $form_fields): array {
|
private static function extract_basic_fields(array $form_fields, array $current_settings): array {
|
||||||
$basic_keys = ['enabled', 'title', 'description', 'instructions'];
|
$basic_keys = ['enabled', 'title', 'description', 'instructions'];
|
||||||
return self::filter_fields($form_fields, $basic_keys);
|
return self::filter_fields($form_fields, $basic_keys, $current_settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,14 +220,14 @@ class PaymentGatewaysProvider {
|
|||||||
* @param array $form_fields All form fields
|
* @param array $form_fields All form fields
|
||||||
* @return array API fields
|
* @return array API fields
|
||||||
*/
|
*/
|
||||||
private static function extract_api_fields(array $form_fields): array {
|
private static function extract_api_fields(array $form_fields, array $current_settings): array {
|
||||||
$api_patterns = ['key', 'secret', 'token', 'api', 'client', 'merchant', 'account'];
|
$api_patterns = ['key', 'secret', 'token', 'api', 'client', 'merchant', 'account'];
|
||||||
$api_fields = [];
|
$api_fields = [];
|
||||||
|
|
||||||
foreach ($form_fields as $key => $field) {
|
foreach ($form_fields as $key => $field) {
|
||||||
foreach ($api_patterns as $pattern) {
|
foreach ($api_patterns as $pattern) {
|
||||||
if (stripos($key, $pattern) !== false) {
|
if (stripos($key, $pattern) !== false) {
|
||||||
$api_fields[$key] = self::normalize_field($key, $field);
|
$api_fields[$key] = self::normalize_field($key, $field, $current_settings);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +242,7 @@ class PaymentGatewaysProvider {
|
|||||||
* @param array $form_fields All form fields
|
* @param array $form_fields All form fields
|
||||||
* @return array Advanced fields
|
* @return array Advanced fields
|
||||||
*/
|
*/
|
||||||
private static function extract_advanced_fields(array $form_fields): array {
|
private static function extract_advanced_fields(array $form_fields, array $current_settings): array {
|
||||||
$basic_keys = ['enabled', 'title', 'description', 'instructions'];
|
$basic_keys = ['enabled', 'title', 'description', 'instructions'];
|
||||||
$advanced_fields = [];
|
$advanced_fields = [];
|
||||||
|
|
||||||
@@ -262,7 +263,7 @@ class PaymentGatewaysProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$is_api) {
|
if (!$is_api) {
|
||||||
$advanced_fields[$key] = self::normalize_field($key, $field);
|
$advanced_fields[$key] = self::normalize_field($key, $field, $current_settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,12 +277,12 @@ class PaymentGatewaysProvider {
|
|||||||
* @param array $keys Keys to extract
|
* @param array $keys Keys to extract
|
||||||
* @return array Filtered fields
|
* @return array Filtered fields
|
||||||
*/
|
*/
|
||||||
private static function filter_fields(array $form_fields, array $keys): array {
|
private static function filter_fields(array $form_fields, array $keys, array $current_settings): array {
|
||||||
$filtered = [];
|
$filtered = [];
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
foreach ($keys as $key) {
|
||||||
if (isset($form_fields[$key])) {
|
if (isset($form_fields[$key])) {
|
||||||
$filtered[$key] = self::normalize_field($key, $form_fields[$key]);
|
$filtered[$key] = self::normalize_field($key, $form_fields[$key], $current_settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,13 +296,17 @@ class PaymentGatewaysProvider {
|
|||||||
* @param array $field Field data
|
* @param array $field Field data
|
||||||
* @return array Normalized field
|
* @return array Normalized field
|
||||||
*/
|
*/
|
||||||
private static function normalize_field(string $key, array $field): array {
|
private static function normalize_field(string $key, array $field, array $current_settings): array {
|
||||||
|
// Use current value if available, otherwise use default
|
||||||
|
$current_value = $current_settings[$key] ?? $field['default'] ?? '';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $key,
|
'id' => $key,
|
||||||
'type' => $field['type'] ?? 'text',
|
'type' => $field['type'] ?? 'text',
|
||||||
'title' => $field['title'] ?? '',
|
'title' => $field['title'] ?? '',
|
||||||
'description' => $field['description'] ?? '',
|
'description' => $field['description'] ?? '',
|
||||||
'default' => $field['default'] ?? '',
|
'default' => $field['default'] ?? '',
|
||||||
|
'value' => $current_value, // Add current value!
|
||||||
'placeholder' => $field['placeholder'] ?? '',
|
'placeholder' => $field['placeholder'] ?? '',
|
||||||
'required' => !empty($field['required']),
|
'required' => !empty($field['required']),
|
||||||
'options' => $field['options'] ?? null,
|
'options' => $field['options'] ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user