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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user