fix: Modal footer outside scroll + checkbox yes/no conversion

This commit is contained in:
dwindown
2025-11-06 00:05:22 +07:00
parent 96f0482cfb
commit 91449bec60
2 changed files with 95 additions and 43 deletions

View File

@@ -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> )}
</> </>
); );
} }

View File

@@ -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>
)} )}