fix: Prevent double submission in Create Page dialog

- Add ref-based double submission protection (isSubmittingRef)
- Extract handleSubmit function with isPending checks
- Add loading spinner during submission
- Disable inputs during submission
- Suppress error toast for duplicate prevention errors
This commit is contained in:
Dwindi Ramadhana
2026-01-11 23:22:47 +07:00
parent fe243a42cb
commit e66f5e54a1

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
import { __ } from '@/lib/i18n'; import { __ } from '@/lib/i18n';
@@ -15,7 +15,7 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { FileText, Layout } from 'lucide-react'; import { FileText, Layout, Loader2 } from 'lucide-react';
interface PageItem { interface PageItem {
id?: number; id?: number;
@@ -36,18 +36,30 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [slug, setSlug] = useState(''); const [slug, setSlug] = useState('');
// Prevent double submission
const isSubmittingRef = useRef(false);
// Get site URL from WordPress config // Get site URL from WordPress config
const siteUrl = window.WNW_CONFIG?.siteUrl?.replace(/\/$/, '') || window.location.origin; const siteUrl = window.WNW_CONFIG?.siteUrl?.replace(/\/$/, '') || window.location.origin;
// Create page mutation // Create page mutation
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async () => { mutationFn: async (data: { title: string; slug: string }) => {
if (pageType === 'page') { // Guard against double submission
const response = await api.post('/pages', { title, slug }); if (isSubmittingRef.current) {
return response.data; throw new Error('Request already in progress');
}
isSubmittingRef.current = true;
try {
const response = await api.post('/pages', { title: data.title, slug: data.slug });
return response.data;
} finally {
// Reset after a delay to prevent race conditions
setTimeout(() => {
isSubmittingRef.current = false;
}, 500);
} }
// For templates, we don't create them - they're auto-created for each CPT
return null;
}, },
onSuccess: (data) => { onSuccess: (data) => {
if (data?.page) { if (data?.page) {
@@ -64,6 +76,10 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
} }
}, },
onError: (error: any) => { onError: (error: any) => {
// Don't show error for duplicate prevention
if (error?.message === 'Request already in progress') {
return;
}
// Extract error message from the response // Extract error message from the response
const message = error?.response?.data?.message || const message = error?.response?.data?.message ||
error?.message || error?.message ||
@@ -82,15 +98,28 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
} }
}; };
// Handle form submission
const handleSubmit = () => {
if (createMutation.isPending || isSubmittingRef.current) {
return;
}
if (pageType === 'page' && title && slug) {
createMutation.mutate({ title, slug });
}
};
// Reset form when modal closes // Reset form when modal closes
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
setTitle(''); setTitle('');
setSlug(''); setSlug('');
setPageType('page'); setPageType('page');
isSubmittingRef.current = false;
} }
}, [open]); }, [open]);
const isDisabled = pageType === 'page' && (!title || !slug) || createMutation.isPending || isSubmittingRef.current;
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[500px]">
@@ -141,6 +170,7 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
value={title} value={title}
onChange={(e) => handleTitleChange(e.target.value)} onChange={(e) => handleTitleChange(e.target.value)}
placeholder={__('e.g., About Us')} placeholder={__('e.g., About Us')}
disabled={createMutation.isPending}
/> />
</div> </div>
@@ -151,6 +181,7 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
value={slug} value={slug}
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))} onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
placeholder={__('e.g., about-us')} placeholder={__('e.g., about-us')}
disabled={createMutation.isPending}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{__('URL will be: ')}<span className="font-mono text-primary">{siteUrl}/{slug || 'page-slug'}</span> {__('URL will be: ')}<span className="font-mono text-primary">{siteUrl}/{slug || 'page-slug'}</span>
@@ -161,14 +192,21 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}> <Button variant="outline" onClick={() => onOpenChange(false)} disabled={createMutation.isPending}>
{__('Cancel')} {__('Cancel')}
</Button> </Button>
<Button <Button
onClick={() => createMutation.mutate()} onClick={handleSubmit}
disabled={pageType === 'page' && (!title || !slug) || createMutation.isPending} disabled={isDisabled}
> >
{createMutation.isPending ? __('Creating...') : __('Create Page')} {createMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{__('Creating...')}
</>
) : (
__('Create Page')
)}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>