Files
thrilltrack-explorer/docs/FORM_SUBMISSION_PATTERNS.md
2025-11-03 16:51:49 +00:00

8.0 KiB

Form Submission Patterns

Overview

This document defines the standard patterns for handling form submissions, toast notifications, and modal behavior across ThrillWiki.

Core Principles

Separation of Concerns

  • Forms handle UI, validation, and data collection
  • Parent Pages handle submission logic and user feedback
  • Submission Helpers handle database operations

Single Source of Truth

  • Only parent pages show success toasts
  • Forms should not assume submission outcomes
  • Modal closing is controlled by parent after successful submission

Toast Notification Rules

DO

Parent Pages Show Toasts

const handleParkSubmit = async (data: FormData) => {
  try {
    await submitParkCreation(data, user.id);
    
    toast({
      title: "Park Submitted",
      description: "Your submission has been sent for review."
    });
    
    setIsModalOpen(false); // Close modal after success
  } catch (error) {
    // Error already handled by form via handleError utility
  }
};

Use Correct Terminology

  • "Submitted for review" (for new entities)
  • "Edit submitted" (for updates)
  • "Created" or "Updated" (implies immediate approval)

Conditional Toast in Forms (Only for standalone usage)

// Only show toast if NOT being called from a parent handler
if (!initialData?.id) {
  toast.success('Designer submitted for review');
  onCancel();
}

DON'T

Forms Should NOT Show Success Toasts for Main Submissions

// ❌ WRONG - Form doesn't know if submission succeeded
const handleFormSubmit = async (data: FormData) => {
  await onSubmit(data);
  
  toast({
    title: "Park Created", // ❌ Misleading terminology
    description: "The new park has been created successfully."
  });
};

Duplicate Toasts

// ❌ WRONG - Both form and parent showing toasts
// Form:
toast({ title: "Park Created" });

// Parent:
toast({ title: "Park Submitted" });

Modal Behavior

Expected Flow

  1. User fills form and clicks submit
  2. Form validates and calls onSubmit prop
  3. Parent page handles submission
  4. Parent shows appropriate toast
  5. Parent closes modal via setIsModalOpen(false)

Common Issues

Issue: Modal doesn't close after submission Cause: Form is showing a toast that interferes with normal flow Solution: Remove form-level success toasts

Issue: User sees "Created" but item isn't visible Cause: Using wrong terminology - submissions go to moderation Solution: Use "Submitted for review" instead of "Created"

Form Component Template

export function EntityForm({ onSubmit, onCancel, initialData }: EntityFormProps) {
  const { user } = useAuth();
  
  const { register, handleSubmit, /* ... */ } = useForm({
    // ... form config
  });

  return (
    <form onSubmit={handleSubmit(async (data) => {
      if (!user) {
        toast.error('You must be logged in to submit');
        return;
      }
      
      try {
        await onSubmit(data);
        
        // ⚠️ NO SUCCESS TOAST HERE - parent handles it
        // Exception: Standalone forms not in modals can show toast
      } catch (error: unknown) {
        handleError(error, {
          action: initialData?.id ? 'Update Entity' : 'Create Entity',
          metadata: { entityName: data.name }
        });
        
        // ⚠️ CRITICAL: Re-throw so parent can handle modal state
        throw error;
      }
    })}>
      {/* Form fields */}
    </form>
  );
}

Parent Page Template

export function EntityListPage() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const handleEntitySubmit = async (data: FormData) => {
    try {
      const result = await submitEntityCreation(data, user.id);
      
      // ✅ Parent shows success feedback
      toast({
        title: "Entity Submitted",
        description: "Your submission has been sent for review."
      });
      
      // ✅ Parent closes modal
      setIsModalOpen(false);
      
      // ✅ Parent refreshes data
      queryClient.invalidateQueries(['entities']);
    } catch (error) {
      // Form already showed error via handleError
      // Parent can optionally add additional handling
      console.error('Submission failed:', error);
    }
  };

  return (
    <>
      <Button onClick={() => setIsModalOpen(true)}>
        Add Entity
      </Button>
      
      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
        <EntityForm 
          onSubmit={handleEntitySubmit}
          onCancel={() => setIsModalOpen(false)}
        />
      </Dialog>
    </>
  );
}

Error Handling

⚠️ CRITICAL: Error Propagation Pattern

Forms MUST re-throw errors after logging them so parent components can respond appropriately (keep modals open, show additional context, etc.).

Forms MUST re-throw errors:

} catch (error: unknown) {
  // Log error for debugging and show toast to user
  handleError(error, {
    action: 'Submit Park',
    userId: user?.id,
    metadata: { parkName: data.name }
  });
  
  // ⚠️ CRITICAL: Re-throw so parent can handle modal state
  throw error;
}

Why Re-throw?

  • Parent needs to know submission failed
  • Modal should stay open so user can retry
  • User can fix validation issues and resubmit
  • Prevents "success" behavior on failures
  • Maintains proper error flow through the app

Parent-Level Error Handling

const handleParkSubmit = async (data: FormData) => {
  try {
    await submitParkCreation(data, user.id);
    toast.success('Park submitted for review');
    setIsModalOpen(false); // Only close on success
  } catch (error) {
    // Error already toasted by form via handleError()
    // Modal stays open automatically because we don't close it
    // User can fix issues and retry
    console.error('Submission failed:', error);
  }
};

Expected Error Flow:

  1. User submits form → onSubmit() called
  2. Submission fails → Form catches error
  3. Form shows error toast via handleError()
  4. Form re-throws error to parent
  5. Parent's catch block executes
  6. Modal stays open (no setIsModalOpen(false))
  7. User fixes issue and tries again

Common Mistake:

// ❌ WRONG - Error not re-thrown, parent never knows
} catch (error: unknown) {
  handleError(error, { action: 'Submit' });
  // Missing: throw error;
}

Current Implementation Status

Correct Implementation

  • DesignerForm.tsx - Shows "Designer submitted for review" only when !initialData?.id
  • OperatorForm.tsx - Shows "Operator submitted for review" only when !initialData?.id
  • PropertyOwnerForm.tsx - Shows "Property owner submitted for review" only when !initialData?.id
  • ManufacturerForm.tsx - Shows "Manufacturer submitted for review" only when !initialData?.id
  • RideModelForm.tsx - No toasts, parent handles everything
  • RideForm.tsx - Shows "Submission Sent" with conditional description
  • ParkForm.tsx - Fixed to remove premature success toast

Parent Pages

  • Parks.tsx - Shows "Park Submitted"
  • Operators.tsx - Shows "Operator Submitted"
  • Designers.tsx - Shows "Designer Submitted"
  • Manufacturers.tsx - Shows "Manufacturer Submitted"
  • ParkDetail.tsx - Shows "Submission Sent"

Testing Checklist

When implementing or updating a form:

  • Form validates input correctly
  • Form calls onSubmit prop with clean data
  • Form only shows error toasts, not success toasts (unless standalone)
  • Parent page shows appropriate success toast
  • Success toast uses correct terminology ("submitted" not "created")
  • Modal closes after successful submission
  • User sees single toast, not duplicates
  • Error handling provides actionable feedback
  • Form can be used both in modals and standalone
  • src/lib/errorHandler.ts - Error handling utilities
  • src/lib/entitySubmissionHelpers.ts - Submission logic
  • src/hooks/use-toast.ts - Toast notification hook
  • tests/e2e/submission/park-creation.spec.ts - E2E tests for submission flow