fix: Modal footer outside scroll + checkbox yes/no conversion
This commit is contained in:
@@ -47,12 +47,13 @@ interface GenericGatewayFormProps {
|
|||||||
};
|
};
|
||||||
onSave: (settings: Record<string, unknown>) => Promise<void>;
|
onSave: (settings: Record<string, unknown>) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
hideFooter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supported field types (outside component to avoid re-renders)
|
// Supported field types (outside component to avoid re-renders)
|
||||||
const SUPPORTED_FIELD_TYPES = ['text', 'password', 'checkbox', 'select', 'textarea', 'number', 'email', 'url'];
|
const SUPPORTED_FIELD_TYPES = ['text', 'password', 'checkbox', 'select', 'textarea', 'number', 'email', 'url'];
|
||||||
|
|
||||||
export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGatewayFormProps) {
|
export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = false }: GenericGatewayFormProps) {
|
||||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [unsupportedFields, setUnsupportedFields] = useState<string[]>([]);
|
const [unsupportedFields, setUnsupportedFields] = useState<string[]>([]);
|
||||||
@@ -126,12 +127,14 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
|
|||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
|
// WooCommerce uses "yes"/"no" strings, convert to boolean
|
||||||
|
const isChecked = value === 'yes' || value === true;
|
||||||
return (
|
return (
|
||||||
<div key={field.id} className="flex items-center space-x-2">
|
<div key={field.id} className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={field.id}
|
id={field.id}
|
||||||
checked={value as boolean}
|
checked={isChecked}
|
||||||
onCheckedChange={(checked) => handleFieldChange(field.id, checked)}
|
onCheckedChange={(checked) => handleFieldChange(field.id, checked ? 'yes' : 'no')}
|
||||||
/>
|
/>
|
||||||
<div className="grid gap-1.5 leading-none">
|
<div className="grid gap-1.5 leading-none">
|
||||||
<Label
|
<Label
|
||||||
@@ -159,7 +162,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 }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Select
|
<Select
|
||||||
value={value as string}
|
value={value as string}
|
||||||
@@ -188,7 +194,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 }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Textarea
|
<Textarea
|
||||||
id={field.id}
|
id={field.id}
|
||||||
@@ -210,7 +219,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 }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
id={field.id}
|
id={field.id}
|
||||||
@@ -322,46 +334,48 @@ export function GenericGatewayForm({ gateway, onSave, onCancel }: GenericGateway
|
|||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Sticky Footer */}
|
{/* Footer - only render if not hidden */}
|
||||||
<div className="sticky bottom-0 bg-background border-t px-6 py-4 flex items-center justify-between mt-6">
|
{!hideFooter && (
|
||||||
<Button
|
<div className="sticky bottom-0 bg-background border-t py-4 -mx-6 px-6 flex items-center justify-between mt-6">
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
asChild
|
onClick={onCancel}
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={gateway.wc_settings_url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1"
|
|
||||||
>
|
|
||||||
View in WooCommerce
|
|
||||||
<ExternalLink className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = document.querySelector('form');
|
|
||||||
if (form) form.requestSubmit();
|
|
||||||
}}
|
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
{isSaving ? 'Saving...' : 'Save Settings'}
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={gateway.wc_settings_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
View in WooCommerce
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
if (form) form.requestSubmit();
|
||||||
|
}}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? 'Saving...' : 'Save Settings'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,17 +328,55 @@ 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] flex flex-col p-0">
|
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col p-0 gap-0">
|
||||||
<DialogHeader className="px-6 pt-6 pb-4 border-b">
|
<DialogHeader className="px-6 pt-6 pb-4 border-b shrink-0">
|
||||||
<DialogTitle>{selectedGateway.title} Settings</DialogTitle>
|
<DialogTitle>{selectedGateway.title} Settings</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
<div className="flex-1 overflow-y-auto px-6 py-4 min-h-0">
|
||||||
<GenericGatewayForm
|
<GenericGatewayForm
|
||||||
gateway={selectedGateway}
|
gateway={selectedGateway}
|
||||||
onSave={handleSaveGateway}
|
onSave={handleSaveGateway}
|
||||||
onCancel={() => setIsModalOpen(false)}
|
onCancel={() => setIsModalOpen(false)}
|
||||||
|
hideFooter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Footer outside scrollable area */}
|
||||||
|
<div className="border-t px-6 py-4 flex items-center justify-between shrink-0 bg-background">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={selectedGateway.wc_settings_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
View in WooCommerce
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
if (form) form.requestSubmit();
|
||||||
|
}}
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
>
|
||||||
|
{saveMutation.isPending ? 'Saving...' : 'Save Settings'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user