diff --git a/src/components/admin/DesignerForm.tsx b/src/components/admin/DesignerForm.tsx index 154089a5..f96210ec 100644 --- a/src/components/admin/DesignerForm.tsx +++ b/src/components/admin/DesignerForm.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; @@ -35,6 +36,7 @@ interface DesignerFormProps { export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormProps): React.JSX.Element { const { isModerator } = useUserRole(); const { user } = useAuth(); + const [isSubmitting, setIsSubmitting] = useState(false); const { register, @@ -75,6 +77,7 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr return; } + setIsSubmitting(true); try { const formData = { ...data, @@ -97,6 +100,8 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr // Re-throw so parent can handle modal closing throw error; + } finally { + setIsSubmitting(false); } })} className="space-y-6"> {/* Basic Information */} @@ -274,12 +279,15 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr type="button" variant="outline" onClick={onCancel} + disabled={isSubmitting} > Cancel diff --git a/src/components/admin/ManufacturerForm.tsx b/src/components/admin/ManufacturerForm.tsx index 8044d64b..1d9cc8c5 100644 --- a/src/components/admin/ManufacturerForm.tsx +++ b/src/components/admin/ManufacturerForm.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; @@ -37,6 +38,7 @@ interface ManufacturerFormProps { export function ManufacturerForm({ onSubmit, onCancel, initialData }: ManufacturerFormProps): React.JSX.Element { const { isModerator } = useUserRole(); const { user } = useAuth(); + const [isSubmitting, setIsSubmitting] = useState(false); const { register, @@ -79,6 +81,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur return; } + setIsSubmitting(true); try { const formData = { ...data, @@ -86,7 +89,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined, }; - onSubmit(formData); + await onSubmit(formData); // Only show success toast and close if not editing through moderation queue if (!initialData?.id) { @@ -101,6 +104,8 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur // Re-throw so parent can handle modal closing throw error; + } finally { + setIsSubmitting(false); } })} className="space-y-6"> {/* Basic Information */} @@ -284,12 +289,15 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur type="button" variant="outline" onClick={onCancel} + disabled={isSubmitting} > Cancel diff --git a/src/components/admin/OperatorForm.tsx b/src/components/admin/OperatorForm.tsx index 4f68bb0a..a9ac1d70 100644 --- a/src/components/admin/OperatorForm.tsx +++ b/src/components/admin/OperatorForm.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; @@ -35,6 +36,7 @@ interface OperatorFormProps { export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormProps): React.JSX.Element { const { isModerator } = useUserRole(); const { user } = useAuth(); + const [isSubmitting, setIsSubmitting] = useState(false); const { register, @@ -75,6 +77,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr return; } + setIsSubmitting(true); try { const formData = { ...data, @@ -82,7 +85,7 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined, }; - onSubmit(formData); + await onSubmit(formData); // Only show success toast and close if not editing through moderation queue if (!initialData?.id) { @@ -97,6 +100,8 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr // Re-throw so parent can handle modal closing throw error; + } finally { + setIsSubmitting(false); } })} className="space-y-6"> {/* Basic Information */} @@ -274,12 +279,15 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr type="button" variant="outline" onClick={onCancel} + disabled={isSubmitting} > Cancel {showFilters && ( diff --git a/src/components/admin/TestDataGenerator.tsx b/src/components/admin/TestDataGenerator.tsx index e29424ee..58ae081a 100644 --- a/src/components/admin/TestDataGenerator.tsx +++ b/src/components/admin/TestDataGenerator.tsx @@ -435,7 +435,12 @@ export function TestDataGenerator(): React.JSX.Element { )}
- diff --git a/src/components/admin/VersionCleanupSettings.tsx b/src/components/admin/VersionCleanupSettings.tsx index b26cdc40..28ca9ea1 100644 --- a/src/components/admin/VersionCleanupSettings.tsx +++ b/src/components/admin/VersionCleanupSettings.tsx @@ -148,9 +148,9 @@ export function VersionCleanupSettings() { onChange={(e) => setRetentionDays(Number(e.target.value))} className="w-32" /> - +

Keep most recent 10 versions per item, delete older ones beyond this period @@ -176,15 +176,12 @@ export function VersionCleanupSettings() {

diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 61873347..cc280543 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; +import { Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { breadcrumb } from "@/lib/errorBreadcrumbs"; @@ -36,13 +37,33 @@ export interface ButtonProps VariantProps { asChild?: boolean; trackingLabel?: string; // Optional label for breadcrumb tracking + loading?: boolean; // Show loading state with spinner + loadingText?: string; // Optional text to display during loading } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, onClick, trackingLabel, ...props }, ref) => { + ({ + className, + variant, + size, + asChild = false, + onClick, + trackingLabel, + loading = false, + loadingText, + children, + disabled, + ...props + }, ref) => { const Comp = asChild ? Slot : "button"; const handleClick = (e: React.MouseEvent) => { + // Prevent clicks while loading + if (loading) { + e.preventDefault(); + return; + } + // Add breadcrumb for button click if (trackingLabel) { breadcrumb.userAction('clicked', trackingLabel); @@ -57,8 +78,18 @@ const Button = React.forwardRef( className={cn(buttonVariants({ variant, size, className }))} ref={ref} onClick={handleClick} - {...props} - /> + disabled={disabled || loading} + {...props} + > + {loading ? ( + <> + + {loadingText || children} + + ) : ( + children + )} + ); }, );