From 6e16fce1b9492ed87f3e533f5e4ebd3da3a47404 Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 11 Jan 2026 17:37:45 +0100 Subject: [PATCH] error-propagation: implement retry for failed mutations (MarketplaceHome, RolesPage, SettingsPage) --- .../src/features/roles/pages/RolesPage.tsx | 51 ++++++++++++++++--- .../features/settings/pages/SettingsPage.tsx | 42 +++++++++++++-- .../src/pages/marketplace/MarketplaceHome.tsx | 39 ++++++++++++-- 3 files changed, 116 insertions(+), 16 deletions(-) diff --git a/apps/web/src/features/roles/pages/RolesPage.tsx b/apps/web/src/features/roles/pages/RolesPage.tsx index 81c2f1598..b0c9fec68 100644 --- a/apps/web/src/features/roles/pages/RolesPage.tsx +++ b/apps/web/src/features/roles/pages/RolesPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { getRoles, deleteRole, updateRole } from '../services/roleService'; import type { Role } from '../types/role'; import { Button } from '@/components/ui/button'; @@ -29,6 +29,8 @@ export function RolesPage() { const [queryError, setQueryError] = useState(null); const [mutationError, setMutationError] = useState(null); const [editingRole, setEditingRole] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const lastMutationRef = useRef<(() => Promise) | null>(null); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isAssignModalOpen, setIsAssignModalOpen] = useState(false); const [assignUserId, setAssignUserId] = useState(''); @@ -54,13 +56,23 @@ export function RolesPage() { }, []); const handleToggleActive = async (role: Role) => { - try { - setMutationError(null); + // Action 3.4.1.3: Store mutation for retry + const performMutation = async () => { await updateRole(role.id, { is_active: !role.is_active }); toast.success( `Role ${!role.is_active ? 'activated' : 'deactivated'} successfully`, ); loadRoles(); + setMutationError(null); + setRetryCount(0); + lastMutationRef.current = null; + }; + + lastMutationRef.current = performMutation; + setMutationError(null); + + try { + await performMutation(); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to update role'; @@ -74,11 +86,21 @@ export function RolesPage() { return; } - try { - setMutationError(null); + // Action 3.4.1.3: Store mutation for retry + const performMutation = async () => { await deleteRole(role.id); toast.success('Role deleted successfully'); loadRoles(); + setMutationError(null); + setRetryCount(0); + lastMutationRef.current = null; + }; + + lastMutationRef.current = performMutation; + setMutationError(null); + + try { + await performMutation(); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to delete role'; @@ -86,6 +108,18 @@ export function RolesPage() { } }; + // Action 3.4.1.3: Retry handler for failed mutations + const handleRetry = async () => { + if (!lastMutationRef.current || retryCount >= 3) return; + + setRetryCount((prev) => prev + 1); + try { + await lastMutationRef.current(); + } catch (error) { + // Error will be handled by the mutation function + } + }; + const handleEdit = (role: Role) => { setEditingRole(role); setIsEditModalOpen(true); @@ -135,7 +169,12 @@ export function RolesPage() { action: 'updating role', resource: 'roles', }} - onDismiss={() => setMutationError(null)} + onRetry={retryCount < 3 ? handleRetry : undefined} + onDismiss={() => { + setMutationError(null); + setRetryCount(0); + lastMutationRef.current = null; + }} /> )}
diff --git a/apps/web/src/features/settings/pages/SettingsPage.tsx b/apps/web/src/features/settings/pages/SettingsPage.tsx index 599936075..e8f2ff304 100644 --- a/apps/web/src/features/settings/pages/SettingsPage.tsx +++ b/apps/web/src/features/settings/pages/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useAuthStore } from '@/features/auth/store/authStore'; import { getSettings, updateSettings } from '../services/settingsService'; import { UserSettings } from '../types/settings'; @@ -20,6 +20,8 @@ export function SettingsPage() { const [isSaving, setIsSaving] = useState(false); const [queryError, setQueryError] = useState(null); const [mutationError, setMutationError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const lastMutationRef = useRef<(() => Promise) | null>(null); const loadSettings = async () => { if (!user?.id) { @@ -61,11 +63,21 @@ export function SettingsPage() { return; } - try { - setIsSaving(true); - setMutationError(null); + // Action 3.4.1.3: Store mutation for retry + const performMutation = async () => { await updateSettings(user.id, settings); toast.success('Paramètres sauvegardés avec succès'); + setMutationError(null); + setRetryCount(0); + lastMutationRef.current = null; + }; + + lastMutationRef.current = performMutation; + setIsSaving(true); + setMutationError(null); + + try { + await performMutation(); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la sauvegarde'; @@ -75,6 +87,21 @@ export function SettingsPage() { } }; + // Action 3.4.1.3: Retry handler for failed mutations + const handleRetry = async () => { + if (!lastMutationRef.current || retryCount >= 3) return; + + setRetryCount((prev) => prev + 1); + setIsSaving(true); + try { + await lastMutationRef.current(); + } catch (error) { + // Error will be handled by the mutation function + } finally { + setIsSaving(false); + } + }; + if (isLoading) { return (
@@ -129,7 +156,12 @@ export function SettingsPage() { action: 'saving settings', resource: 'settings', }} - onDismiss={() => setMutationError(null)} + onRetry={retryCount < 3 ? handleRetry : undefined} + onDismiss={() => { + setMutationError(null); + setRetryCount(0); + lastMutationRef.current = null; + }} /> )}
diff --git a/apps/web/src/pages/marketplace/MarketplaceHome.tsx b/apps/web/src/pages/marketplace/MarketplaceHome.tsx index e68c29ec4..4dd64ce63 100644 --- a/apps/web/src/pages/marketplace/MarketplaceHome.tsx +++ b/apps/web/src/pages/marketplace/MarketplaceHome.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { marketplaceService } from '@/services/marketplaceService'; import { ProductCard } from '@/features/marketplace/components/ProductCard'; import { Product, ProductType } from '@/types/marketplace'; @@ -27,6 +27,8 @@ export function MarketplaceHome() { const [queryError, setQueryError] = useState(null); const [mutationError, setMutationError] = useState(null); const [purchasingProductId, setPurchasingProductId] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const lastMutationRef = useRef<(() => Promise) | null>(null); const [isCartOpen, setIsCartOpen] = useState(false); const [page, setPage] = useState(1); const [limit] = useState(12); @@ -94,12 +96,22 @@ export function MarketplaceHome() { }; // CRITIQUE FIX #70: Gestion d'erreur améliorée pour purchaseProduct + // Action 3.4.1.3: Store mutation for retry const handlePurchase = async (product: Product) => { - try { - setPurchasingProductId(product.id); - setMutationError(null); + const performMutation = async () => { await marketplaceService.purchaseProduct(product.id); toast.success(`Successfully purchased ${product.title}`); + setMutationError(null); + setRetryCount(0); + lastMutationRef.current = null; + }; + + lastMutationRef.current = performMutation; + setPurchasingProductId(product.id); + setMutationError(null); + + try { + await performMutation(); } catch (error: unknown) { // CRITIQUE FIX #70: Gestion d'erreur améliorée avec message détaillé const apiError = parseApiError(error); @@ -113,6 +125,18 @@ export function MarketplaceHome() { } }; + // Action 3.4.1.3: Retry handler for failed mutations + const handleRetry = async () => { + if (!lastMutationRef.current || retryCount >= 3) return; + + setRetryCount((prev) => prev + 1); + try { + await lastMutationRef.current(); + } catch (error) { + // Error will be handled by the mutation function + } + }; + const handleClearFilters = () => { setSearchQuery(''); setProductType(''); @@ -159,7 +183,12 @@ export function MarketplaceHome() { action: 'purchasing product', resource: 'marketplace', }} - onDismiss={() => setMutationError(null)} + onRetry={retryCount < 3 ? handleRetry : undefined} + onDismiss={() => { + setMutationError(null); + setRetryCount(0); + lastMutationRef.current = null; + }} /> )}