feat: enhance toast error messages with structured parsing

- Add parseErrorMessage() utility to parse JSON error responses
- Add showErrorToast() helper for consistent error display
- Update all toast.error calls to use structured error parsing
- Support multiple error formats: error+troubleshooting, title+description, message+details
- Enhance apiRequest() to support both 'body' and 'data' properties
- Add comprehensive unit tests for error parsing functionality
- Improve user experience with clear, actionable error messages

Fixes structured error messages from Gitea API responses that were showing as raw JSON
This commit is contained in:
Arunavo Ray
2025-05-28 11:11:28 +05:30
parent 99336e2607
commit ede5b4dbe8
10 changed files with 240 additions and 102 deletions

View File

@@ -16,7 +16,7 @@ import {
DialogTitle,
DialogTrigger,
} from '../ui/dialog';
import { apiRequest, formatDate } from '@/lib/utils';
import { apiRequest, formatDate, showErrorToast } from '@/lib/utils';
import { useAuth } from '@/hooks/useAuth';
import type { MirrorJob } from '@/lib/db/schema';
import type { ActivityApiResponse } from '@/types/activities';
@@ -155,7 +155,7 @@ export function ActivityLog() {
if (!res.success) {
// Only show error toast for manual refreshes to avoid spam during live updates
if (!isLiveRefresh) {
toast.error(res.message ?? 'Failed to fetch activities.');
showErrorToast(res.message ?? 'Failed to fetch activities.', toast);
}
return false;
}
@@ -184,9 +184,7 @@ export function ActivityLog() {
if (isMountedRef.current) {
// Only show error toast for manual refreshes to avoid spam during live updates
if (!isLiveRefresh) {
toast.error(
err instanceof Error ? err.message : 'Failed to fetch activities.',
);
showErrorToast(err, toast);
}
}
return false;
@@ -331,11 +329,11 @@ export function ActivityLog() {
setActivities([]);
toast.success(`All activities cleaned up successfully. Deleted ${res.result.mirrorJobsDeleted} mirror jobs and ${res.result.eventsDeleted} events.`);
} else {
toast.error(res.error || 'Failed to cleanup activities.');
showErrorToast(res.error || 'Failed to cleanup activities.', toast);
}
} catch (error) {
console.error('Error cleaning up activities:', error);
toast.error(error instanceof Error ? error.message : 'Failed to cleanup activities.');
showErrorToast(error, toast);
} finally {
setIsInitialLoading(false);
}

View File

@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { SiGitea } from 'react-icons/si';
import { toast, Toaster } from 'sonner';
import { showErrorToast } from '@/lib/utils';
import { FlipHorizontal } from 'lucide-react';
export function LoginForm() {
@@ -45,10 +46,10 @@ export function LoginForm() {
window.location.href = '/';
}, 1000);
} else {
toast.error(data.error || 'Login failed. Please try again.');
showErrorToast(data.error || 'Login failed. Please try again.', toast);
}
} catch (error) {
toast.error('An error occurred while logging in. Please try again.');
showErrorToast(error, toast);
} finally {
setIsLoading(false);
}

View File

@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { GitMerge } from 'lucide-react';
import { toast, Toaster } from 'sonner';
import { showErrorToast } from '@/lib/utils';
export function SignupForm() {
const [isLoading, setIsLoading] = useState(false);
@@ -51,10 +52,10 @@ export function SignupForm() {
window.location.href = '/';
}, 1500);
} else {
toast.error(data.error || 'Failed to create account. Please try again.');
showErrorToast(data.error || 'Failed to create account. Please try again.', toast);
}
} catch (error) {
toast.error('An error occurred while creating your account. Please try again.');
showErrorToast(error, toast);
} finally {
setIsLoading(false);
}

View File

@@ -14,7 +14,7 @@ import type {
} from '@/types/config';
import { Button } from '../ui/button';
import { useAuth } from '@/hooks/useAuth';
import { apiRequest } from '@/lib/utils';
import { apiRequest, showErrorToast } from '@/lib/utils';
import { RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
import { Skeleton } from '@/components/ui/skeleton';
@@ -134,16 +134,13 @@ export function ConfigTabs() {
'Configuration saved successfully! Now import your GitHub data to begin.',
);
} else {
toast.error(
showErrorToast(
`Failed to save configuration: ${result.message || 'Unknown error'}`,
toast
);
}
} catch (error) {
toast.error(
`An error occurred while saving the configuration: ${
error instanceof Error ? error.message : String(error)
}`,
);
showErrorToast(error, toast);
}
};
@@ -198,18 +195,13 @@ export function ConfigTabs() {
console.warn('Failed to fetch updated config after auto-save:', fetchError);
}
} else {
toast.error(
showErrorToast(
`Auto-save failed: ${result.message || 'Unknown error'}`,
{ duration: 3000 }
toast
);
}
} catch (error) {
toast.error(
`Auto-save error: ${
error instanceof Error ? error.message : String(error)
}`,
{ duration: 3000 }
);
showErrorToast(error, toast);
} finally {
setIsAutoSavingSchedule(false);
}
@@ -266,18 +258,13 @@ export function ConfigTabs() {
console.warn('Failed to fetch updated config after auto-save:', fetchError);
}
} else {
toast.error(
showErrorToast(
`Auto-save failed: ${result.message || 'Unknown error'}`,
{ duration: 3000 }
toast
);
}
} catch (error) {
toast.error(
`Auto-save error: ${
error instanceof Error ? error.message : String(error)
}`,
{ duration: 3000 }
);
showErrorToast(error, toast);
} finally {
setIsAutoSavingCleanup(false);
}

View File

@@ -5,7 +5,7 @@ import { GitFork, Clock, FlipHorizontal, Building2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { MirrorJob, Organization, Repository } from "@/lib/db/schema";
import { useAuth } from "@/hooks/useAuth";
import { apiRequest } from "@/lib/utils";
import { apiRequest, showErrorToast } from "@/lib/utils";
import type { DashboardApiResponse } from "@/types/dashboard";
import { useSSE } from "@/hooks/useSEE";
import { toast } from "sonner";
@@ -103,15 +103,11 @@ export function Dashboard() {
}
return true;
} else {
toast.error(response.error || "Error fetching dashboard data");
showErrorToast(response.error || "Error fetching dashboard data", toast);
return false;
}
} catch (error) {
toast.error(
error instanceof Error
? error.message
: "Error fetching dashboard data"
);
showErrorToast(error, toast);
return false;
} finally {
setIsLoading(false);

View File

@@ -5,7 +5,7 @@ import type { MirrorJob, Organization } from "@/lib/db/schema";
import { OrganizationList } from "./OrganizationsList";
import AddOrganizationDialog from "./AddOrganizationDialog";
import { useAuth } from "@/hooks/useAuth";
import { apiRequest } from "@/lib/utils";
import { apiRequest, showErrorToast } from "@/lib/utils";
import {
membershipRoleEnum,
type AddOrganizationApiRequest,
@@ -193,12 +193,10 @@ export function Organization() {
searchTerm: org,
}));
} else {
toast.error(response.error || "Error adding organization");
showErrorToast(response.error || "Error adding organization", toast);
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error adding organization"
);
showErrorToast(error, toast);
} finally {
setIsLoading(false);
}
@@ -250,12 +248,10 @@ export function Organization() {
})
);
} else {
toast.error(response.error || "Error starting mirror jobs");
showErrorToast(response.error || "Error starting mirror jobs", toast);
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error starting mirror jobs"
);
showErrorToast(error, toast);
} finally {
// Reset loading states - we'll let the SSE updates handle status changes
setLoadingOrgIds(new Set());

View File

@@ -9,7 +9,7 @@ import {
type RepositoryApiResponse,
type RepoStatus,
} from "@/types/Repository";
import { apiRequest } from "@/lib/utils";
import { apiRequest, showErrorToast } from "@/lib/utils";
import {
Select,
SelectContent,
@@ -108,16 +108,14 @@ export default function Repository() {
} else {
// Only show error toast for manual refreshes to avoid spam during live updates
if (!isLiveRefresh) {
toast.error(response.error || "Error fetching repositories");
showErrorToast(response.error || "Error fetching repositories", toast);
}
return false;
}
} catch (error) {
// Only show error toast for manual refreshes to avoid spam during live updates
if (!isLiveRefresh) {
toast.error(
error instanceof Error ? error.message : "Error fetching repositories"
);
showErrorToast(error, toast);
}
return false;
} finally {
@@ -184,12 +182,10 @@ export default function Repository() {
})
);
} else {
toast.error(response.error || "Error starting mirror job");
showErrorToast(response.error || "Error starting mirror job", toast);
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error starting mirror job"
);
showErrorToast(error, toast);
} finally {
setLoadingRepoIds((prev) => {
const newSet = new Set(prev);
@@ -248,12 +244,10 @@ export default function Repository() {
})
);
} else {
toast.error(response.error || "Error starting mirror jobs");
showErrorToast(response.error || "Error starting mirror jobs", toast);
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error starting mirror jobs"
);
showErrorToast(error, toast);
} finally {
// Reset loading states - we'll let the SSE updates handle status changes
setLoadingRepoIds(new Set());
@@ -287,12 +281,10 @@ export default function Repository() {
})
);
} else {
toast.error(response.error || "Error starting sync job");
showErrorToast(response.error || "Error starting sync job", toast);
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error starting sync job"
);
showErrorToast(error, toast);
} finally {
setLoadingRepoIds((prev) => {
const newSet = new Set(prev);
@@ -329,12 +321,10 @@ export default function Repository() {
})
);
} else {
toast.error(response.error || "Error retrying job");
showErrorToast(response.error || "Error retrying job", toast);
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error retrying job"
);
showErrorToast(error, toast);
} finally {
setLoadingRepoIds((prev) => {
const newSet = new Set(prev);
@@ -381,12 +371,10 @@ export default function Repository() {
searchTerm: repo,
}));
} else {
toast.error(response.error || "Error adding repository");
showErrorToast(response.error || "Error adding repository", toast);
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error adding repository"
);
showErrorToast(error, toast);
}
};